2024年4月

在Spring Boot应用开发中,MyBatis-Plus是一个强大且易于使用的MyBatis增强工具,它提供了很多实用的功能,如代码生成器、条件构造器、分页插件等,极大地简化了MyBatis的使用和配置。本篇文章将指导大家如何在Spring Boot项目中集成MyBatis-Plus。

一、项目准备

首先,确保你已经安装了Java开发环境(JDK)和Maven或Gradle等构建工具。接下来,创建一个新的Spring Boot项目。你可以使用Spring Initializr
https://start.spring.io
来快速生成项目结构,选择你需要的依赖项,比如Spring Web、MySQL Driver等。

二、添加MyBatis-Plus依赖

在Gradle项目中集成MyBatis-Plus,你需要在项目的build.gradle文件中添加MyBatis-Plus的依赖。以下是添加MyBatis-Plus依赖的示例:

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-web'
        compileOnly 'org.projectlombok:lombok'
        annotationProcessor 'org.projectlombok:lombok'
        runtimeOnly 'mysql:mysql-connector-java:8.0.17'
        // MyBatis-Plus 依赖
        implementation 'com.baomidou:mybatis-plus-spring-boot3-starter:3.5.6'
        // 如果需要分页插件,可以添加以下依赖
        implementation 'com.baomidou:mybatis-plus-extension:3.5.6'
    }

三、配置数据源

在application.properties或application.yml文件中,配置你的数据库连接信息。以下是application.yml的示例:

# datasource
spring.datasource.url=jdbc:mysql://localhost:3306/your_database?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false&allowPublicKeyRetrieval=true  
spring.datasource.username=your_username
spring.datasource.password=your_password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# mybatis-plus
mybatis-plus.mapper-locations=classpath:mapper/*.xml
mybatis-plus.type-aliases-package=cn.daimajiangxin.springboot.learning.model

这里的your_database、your_username和your_password需要替换为你的数据库名称、用户名和密码。mapper-locations指定了MyBatis映射文件的位置,type-aliases-package指定了实体类所在的包。

四、创建实体类

创建与数据库表对应的实体类,并放在type-aliases-package指定的包下。

package cn.daimajiangxin.springboot.learning.model;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import lombok.Data;

@TableName(value ="user")
@Data
public class User implements Serializable {
    /**
     * 学生ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 姓名
     */
    private String name;

    /**
     * 邮箱
     */
    private String email;

    /**
     * 年龄
     */
    private Integer age;

    /**
     * 备注
     */
    private String remark;

    @TableField(exist = false)
    private static final long serialVersionUID = 1L;
}

五、创建Mapper接口

创建对应的Mapper接口,通常放在与实体类相同的包下,并继承BaseMapper 接口。例如:

package cn.daimajiangxin.springboot.learning.mapper;

import cn.daimajiangxin.springboot.learning.model.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface UserMapper extends BaseMapper<User> {

}

六、创建Mapper XML文件

在resources的mapper目录下创建对应的XML文件,例如UserMapper.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.daimajiangxin.springboot.learning.mapper.UserMapper">

    <resultMap id="BaseResultMap" type="cn.daimajiangxin.springboot.learning.model.User">
            <id property="id" column="id" jdbcType="BIGINT"/>
            <result property="name" column="name" jdbcType="VARCHAR"/>
            <result property="email" column="email" jdbcType="VARCHAR"/>
            <result property="age" column="age" jdbcType="INTEGER"/>
            <result property="remark" column="remark" jdbcType="VARCHAR"/>
    </resultMap>

    <sql id="Base_Column_List">
        id,name,email,age,remark
    </sql>
  
    <select id="findAllUsers"  resultMap="BaseResultMap">
     select
       <include refid="Base_Column_List"></include>
     from user
    </select>
</mapper>

七、创建Service 接口

在service目录下服务类接口UserService

package cn.daimajiangxin.springboot.learning.service;

import cn.daimajiangxin.springboot.learning.model.User;
import com.baomidou.mybatisplus.extension.service.IService;

public interface UserService extends IService<User> {

}

八、创建Service实现类

在service目录下创建一个impl目录,并创建UserService实现类UserServiceImpl

package cn.daimajiangxin.springboot.learning.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import cn.daimajiangxin.springboot.learning.model.User;
import cn.daimajiangxin.springboot.learning.service.UserService;
import cn.daimajiangxin.springboot.learning.mapper.UserMapper;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>implements UserService{

}

九、创建Controller

控制层注入 UserService,并使用IService的list和save 方法。

package cn.daimajiangxin.springboot.learning.controller;

import cn.daimajiangxin.springboot.learning.model.User;
import cn.daimajiangxin.springboot.learning.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class UserController {
    private final UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }
    @GetMapping("/users")
    public List<User> getAllUsers() {
        return userService.list();
    }
    @PostMapping("/user")
    public String add() {
        User user=new User();
        user.setName("test");
        user.setAge(18);
        user.setEmail("test@163.com");
        userService.save(user);
        return "保存成功";
    }

}

十、测试

运行你的Spring Boot应用程序,并通过调用UserService的list方法来测试MyBatis-plus的集成效果。你可以通过编写单元测试或使用Postman或apipost等工具来发送HTTP请求,访问来
http://localhost:8080/users
查看所有用户的列表。
20240404002027

总结

在本篇文章中,我们详细介绍了如何在Spring Boot项目中集成MyBatis-Plus,并使用XML文件定义Mapper接口的实现。首先,我们添加了MyBatis-Plus的依赖,并配置了数据源。接着,我们创建了实体类,并使用了MyBatis-Plus的注解来指定实体类与数据库表的映射关系。然后,我们创建了Mapper接口,并继承了BaseMapper以使用MyBatis-Plus提供的CRUD方法。为了更灵活地控制SQL语句的编写,我们还编写了Mapper的XML文件,并在其中定义了自定义的SQL语句。最后,在Service层或Controller层中,我们通过注入Mapper接口来使用这些功能。

通过集成MyBatis-Plus并使用XML实现Mapper,我们可以在Spring Boot项目中高效地进行数据库操作,同时保持代码的清晰和可维护性。MyBatis-Plus的增强功能和XML的灵活性相结合,使得数据访问层的开发变得更加简单和高效。


我是代码匠心,和我一起学习更多精彩知识!!!扫描二维码!关注我,实时获取推送。
公众号
源文来自:
https://daimajiangxin.cn

在购买域名的时候我相信很多人都遇到了对于证书的问题,之前我也是使用阿里云的免费一年的证书,那时候感觉还好,一年更换一次,但是近期阿里云对于证书的过期时间直接砍到了三个月!让我难以接受,所以我在想吧他直接集成到我的FastGateway中,让他自动申请,自动续期!下面我将教大家如何使用Fast Gateway自动申请证书!

安装部署FastGateway

目前来说
FastGateway
可以使用俩种最简单的方式,前提条件就是使用
Docker

Docker Compose

Docker指令安装:

docker run -d --restart=always --name=gateway-api -e PASSWORD=Aa123456 -p 8080:8080 -p 80:80 -p 443:443 -v $(pwd)/data:/data/ registry.cn-shenzhen.aliyuncs.com/tokengo/gateway-api:v1.0.0

这个指令会创建一个
gateway-api
的容器,并且监听了8080,80,443,8080的端口是我们的管理界面的端口,80和443则是外部访问的端口。

Docker-Compose安装:

services:
  gateway-api:
    image: registry.cn-shenzhen.aliyuncs.com/tokengo/gateway-api:v1.0.0
    restart: always
    container_name: gateway-api
    environment:
      PASSWORD: Aa123456
    ports:
      - 8080:8080 # web管理端
      - 80:80 # Http代理端口
      - 443:443/udp # Https代理端口
      - 443:443/tcp # Https代理端口 Http3需要开启UDP和TCP,请注意防火墙设置是否允许
    volumes:
      - ./data:/data/

安装完成以后我们打开浏览器访问 ip:8080的管理界面,进入到下面的界面以后输入默认的密码
Aa123456
即可访问

进入默认的首页:

然后我们打开证书管理-点击新增证书,目前只支持自动申请证书,第一步填写你的域名,这个域名是不包含
http://
的只是完整的域名,邮箱不要乱填,因为邮箱会去注册账号申请证书,乱填可能导致申请失败!

添加卧槽以后点击HTTP代理-点击新增站点,在这里我们需要添加80端口的一个服务,域名需要和你在上面创建的域名一致,负责会导致无法访问的可能性,并且这里的端口是唯一的,点击我们的提交就添加了一个站点服务。


:需要申请的域名必须要解析到这个服务器,并且需要创建一个80的端口监听因为申请证书需要http校验,他会请求你申请的域名地址加上80的端口进行访问,校验完成才会下发证书。

然后在回到我们的证书管理,点击我们需要申请的证书的申请按钮就会申请(或定时任务自动执行申请),看到证书状态
成功
就什么成功了!

如何使用HTTPS证书?

子啊上面我们创建好了证书,也成功申请了证书,下面我们就需要创建一个
HTTPS
的端口服务,下面我们打开HTTP代理,然后打开新增站点,然后填写下面类似参数,域名需要填写自己申请的域名即可,端口需要填写成443,然后勾选
SSL
,这样会启用
HTTPS
,点击添加即可

开源项目

FastGateway项目是完全开源可商用的

FastGateway Github

FastGateway Gitee

技术交流群:737776595


什么是SpringSecurity

Spring Security是一个Java框架,用于保护应用程序的安全性。它提供了一套全面的安全解决方案,包括身份验证、授权、防止攻击等功能。Spring Security基于过滤器链的概念,可以轻松地集成到任何基于Spring的应用程序中。它支持多种身份验证选项和授权策略,开发人员可以根据需要选择适合的方式。此外,Spring Security还提供了一些附加功能,如集成第三方身份验证提供商和单点登录,以及会话管理和密码编码等。

Spring Security是一个强大且易于使用的框架,可以帮助开发人员提高应用程序的安全性和可靠性。而我们最常用的两个功能就是认证和鉴权,因此作为入门文章本文也只介绍这两个功能的使用。

Spring Security可以用于Servlet应用和Reactive应用,本文主要介绍基于Servlet应用的场景

如需更详细的使用方式请参考官方文档:
https://spring.io/projects/spring-security


架构

Untitled

上图是Spring Security官方提供的架构图。我们先看图的左边部分,就是一个典型
Servlet
Filter
(过滤器)处理流程,我们依次讲解流程涉及的组件。



FilterChain

FilterChain
:过滤器链,是
Servlet
容器在接收到客户端发送的请求时创建的,一个
FilterChain
可以包含多个
Filter
和一个
Servlet

Servlet
容器根据请求URI的路径来处理
HttpServletRequest

在Spring MVC中,
Servlet
就是
DispatcherServlet
实例。一个
Servlet
最多只能处理一个
HttpServletRequest

HttpServletResponse
。然而,可以使用多个
Filter
来完成如下工作。

  • 防止下游的 Filter Servlet 被调用。在这种阻断请求的情况下, Filter 通常会使用 HttpServletResponse 对客户端写入响应内容。
  • 修改下游的 Filter Servlet 所使用的 HttpServletRequest HttpServletResponse



DelegatingFilterProxy

DelegatingFilterProxy
:Spring Security 对
Servlet
的支持是基于
Servlet
Filter
的,而
DelegatingFilterProxy
就是Spring Security的
Filter
实现。

DelegatingFilterProxy
允许在
Servlet
容器的生命周期和 Spring 的
ApplicationContext
之间建立桥梁。
Servlet
容器允许通过使用自己的标准来注册
Filter
实例,但
Servlet
容器不知道 Spring 定义的 Bean。因此大多数情况下我们通过标准的
Servlet
容器机制来注册
DelegatingFilterProxy
,但将所有工作委托给实现
Filter
的Spring Bean。

Spring Security会自动向
Servlet
容器机制注册
DelegatingFilterProxy
,无需我们手动去注册



FilterChainProxy

FilterChainProxy
:是 Spring Security 提供的一个特殊的
Filter
,允许通过
SecurityFilterChain
委托给许多
Filter
实例。由于
FilterChainProxy
是一个Spring Bean,因此它被包含在
DelegatingFilterProxy
中。



SecurityFilterChain

SecurityFilterChain
:是
FilterChainProxy
用来确定当前请求应该调用哪些Spring Security
Filter
实例的过滤器链。

SecurityFilterChain
中的
Security
Filter
一般都是Spring Bean,但这些
Security
Filter
是用
FilterChainProxy
进行注册,而不是通过
DelegatingFilterProxy
注册。与直接向Servlet容器或
DelegatingFilterProxy
注册相比,
FilterChainProxy
有很多优势。

  • 首先,由于 FilterChainProxy 是 Spring Security 使用的核心,它可以处理一些必须要做的事情。 例如:
    • 清除 SecurityContext 以避免内存泄漏。
    • 应用Spring Security的 HttpFirewall 来保护应用程序免受某些类型的攻击。
  • 其次,它在确定何时应该调用 SecurityFilterChain 方面提供了更大的灵活性。在Servlet容器中, Filter 实例仅基于URL被调用。 然而, FilterChainProxy 可以通过使用 RequestMatcher 接口,根据 HttpServletRequest 中的任何内容确定调用。

图的右边部分是存在多个
SecurityFilterChain

FilterChainProxy
的匹配策略则是匹配第一个满足的
SecurityFilterChain

比如,请求的URL是
/api/messages/
,它首先与
/api/**

SecurityFilterChain 0
模式匹配,所以只有
SecurityFilterChain0
被调用;虽然它也与
SecurityFilterChain n
匹配。

如果请求的URL是
/messages/
,它与
/api/**

SecurityFilterChain 0
模式不匹配,所以
FilterChainProxy
继续尝试每个
SecurityFilterChain
。如果没有其他
SecurityFilterChain
实例相匹配,则调用
SecurityFilterChain n



SecurityFilter

SecurityFilter
:是指通过
SecurityFilterChain
插入
FilterChainProxy
中的
Filter

这些
Filter
可以用于许多不同的目的,如 认证、 授权、 漏洞保护等。
Filter
是按照特定的顺序执行的,以保证它们在正确的时间被调用。

例如,执行认证的
Filter
应该在执行授权的
Filter
之前被调用。如果想要知道 Spring Security 的
Filter
的顺序,可以查看
FilterOrderRegistration
源码。

如果想查看你应用中注册了哪些
SecurityFilter
的话可以将org.springframework.security的日志级别调到info,这样在你应用启动的时候就会在控制台打印出当前应用注册的所有
SecurityFilter
。效果如下:

2023-06-14T08:55:22.321-03:00  INFO 76975 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [
org.springframework.security.web.session.DisableEncodeUrlFilter@404db674,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]

至此,Spring Security官方架构图中涉及的组件就基本介绍完了,大家先对整体架构和执行流程有一个了解,只有先了解了整体架构,才方便接下来我们去理解Spring Security是如何去实现认证和授权的。



常用Spring Security开启的
SecurityFilter

  • CsrfFilter:防止Csrf攻击的 SecurityFilter
  • AuthorizationFilter:授权 SecurityFilter
  • ExceptionTranslationFilter:处理认证和授权异常的 SecurityFilter


异常处理

Spring Security中有一个
ExceptionTranslationFilter

ExceptionTranslationFilter
作为 Security Filter 之一被插入到 FilterChainProxy 中。

ExceptionTranslationFilter
可以处理AuthenticationException或AccessDeniedException,其逻辑大概是这样:

try {
 filterChain.doFilter(request, response);
catch (AccessDeniedException | AuthenticationException ex) {
 if (!authenticated || ex instanceof AuthenticationException) {
  startAuthentication();
 } else {
  accessDenied();
 }
}

这段代码的逻辑大致就是,拦截AccessDeniedException 或 AuthenticationException,如果不是这两个异常则不处理。

ExceptionTranslationFilter
流程如下:

Untitled

因此如果我们想自己处理AuthenticationException或者AccessDeniedException,分别实现
AuthenticationEntryPoint
或者
AccessDeniedHandler
即可

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        log.error("AccessDeniedException 请求URI:{}", request.getRequestURI(), accessDeniedException);
        response.setCharacterEncoding("UTF-8");
        HashMap<String, String> result = new HashMap();
        result.put("code""401");
        result.put("message""权限不足");
        // 处理没有权限的错误错误
        response.getWriter().write(JsonUtil.toString(result));
    }
}
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        log.error("AccessDeniedException 请求URI:{}", request.getRequestURI(), authException);
        response.setCharacterEncoding("UTF-8");
        // 处理认证失败的错误
        HashMap<String, String> result = new HashMap();
        result.put("code""500");
        result.put("message""用户名或密码错误");
        // 处理没有权限的错误错误
        response.getWriter().write(JsonUtil.toString(result));
    }
}
@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain apiFilterChain(HttpSecurity httpSecurity,
                                              AuthenticationEntryPoint authenticationEntryPoint,
                                              AccessDeniedHandler accessDeniedHandler)
 throws Exception 
{
        // 配置异常处理
        httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
        httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
        return httpSecurity.build();
    }
 }

上面是Spring Security对于认证和鉴权异常的处理机制,但是如果我们自定义了一个Filter。如果这个Filter抛出异常,Spring的全局异常处理机制是无法处理的(原因自行搜索)。所以我们还需要自己做一个Filter异常的处理流程。

首先,我们自定义一个Filter,要在FilterChain中的位置比较靠前,没有其他逻辑就是拦截后面filter抛出的异常,然后转发到指定Controller去处理,然后再用全局异常去处理Filter抛出的异常。

@Component
public class ExceptionFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try{
            filterChain.doFilter(request, response);
        }catch (Exception e){
            // 将异常信息写入请求
            request.setAttribute("filterException", e);
            // 重定向到处理异常的controller
            request.getRequestDispatcher("/exception").forward(request, response);
        }
    }
}
@RestController
public class ExceptionController {

    @RequestMapping("/exception")
    public void handleException(HttpServletRequest request) throws Exception {
        Object attribute = request.getAttribute("filterException");
        if (ObjectUtil.isNotEmpty(attribute) && attribute instanceof Exception e){
            throw e;
        }
    }
}
@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain apiFilterChain(HttpSecurity httpSecurity,
                                              ExceptionFilter exceptionFilter)
 throws Exception 
{
        // 配置异常处理过滤器 
        // 这里我们配置在ExceptionTranslationFilter之前 让ExceptionTranslationFilter先处理AuthenticationException或者AccessDeniedException
     // 剩下的其他Exception再交由我们自定义的ExceptionFilter处理
        httpSecurity.addFilterBefore(exceptionFilter, ExceptionTranslationFilter.class);
        return httpSecurity.build();
    }
 }


认证

上面我们已经把Spring Security整体流程讲完了,接下来我们就看一下具体认证的流程是怎样的。Spring Security有提供一套基于标准页面的流程,但是不适用于基于前后端分离的开发模式。地址给大家贴这,有需要的可自行去看看:
认证 :: Spring Security Reference (springdoc.cn)

接下来介绍基于前后端分离的流程:

Untitled
  1. 先配置AuthenticationManager(认证管理器),其中最常用的AuthenticationManager实现是ProviderManager
@Configuration
public class SecurityConfig {

   /**
     * 配置AuthenticationManager(认证管理器)
     * @return
     */

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationProvider authenticationProvider){
       // ProviderManager 是 AuthenticationManager 最常用的实现
        return new ProviderManager(authenticationProvider);
    }
}
  1. 配置AuthenticationProvider,这里我们的认证方案是使用数据库存储的密码和登录请求的密码进行匹配验证,因此我们选择 DaoAuthenticationProvider DaoAuthenticationProvider 是一个 AuthenticationProvider 的实现,它使用 UserDetailsService PasswordEncoder 来验证一个用户名和密码。
@Configuration
public class SecurityConfig {

   /**
     * 配置PasswordEncoder(密码编码器)
     * @return
     */

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置AuthenticationProvider
     * @return
     */

    @Bean
    public AuthenticationProvider authenticationProvider(UserDetailsService userDetailsService, 
                                   PasswordEncoder passwordEncoder)
{
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        return daoAuthenticationProvider;
    }
}
  1. 配置 UserDetails UserDetailsService
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

@Setter
@EqualsAndHashCode
@ToString
public class SecurityUserDetail implements UserDetails {

    @Getter
    private Long userId;

    private String userName;

    private String password;

    private List<GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.userName;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

}

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AuthUserDetailsService implements UserDetailsService {

    private final UserService userService ;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user= userService .getValidUser(username);
        Assert.notEmpty(user, "用户名或密码错误");
        SecurityUserDetail userDetail = new SecurityUserDetail();
        userDetail.setUserId(user.getId());
        userDetail.setUserName(user.getUserName());
        userDetail.setPassword(user.getPassword());
        return userDetail;
    }
}

  1. LoginController提供登录接口,伪代码如下:
@Slf4j
@RestController
@RequiredArgsConstructor
public class LoginController{

    private final AuthenticationManager authenticationManager;

    public Response<LoginVO> login(LoginAO loginAo) {
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginAo.getKey(), loginAo.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        Assert.notEmpty(authenticate, "用户名或密码错误");
        if (authenticate.getPrincipal() instanceof SecurityUserDetail userDetail){
            User user = User.builder().id(userDetail.getUserId()).userName(userDetail.getUsername()).build();
            String token = JwtUtil.generateToken(user);
            // 设置上下文
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, nullnull);
            // 设置子线程支持从父线程获取用户上下文 注意使用ForkJoinPool无法生效
            // SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            return Response.success(LoginVO.builder().token(token).build());
        }else {
            log.error("登录异常,从上下文获取用户信息失败,authenticate:{}", JsonUtil.toString(authenticate));
            return Response.fail(null);
        }
    }
}


授权

本文这里分享两种主流的Spring Security授权方式,一种是基于注解的方式,一种是基于配置的方式。



基于注解的授权校验

基于注解的方式校验授权,是通过Spring aop实现的,其流程如下:

Untitled

首先,我们要开启注解鉴权的功能

@Configuration
/**
 * 开启基于注解的方式控制权限
 */

@EnableMethodSecurity
public class SecurityConfig {

}

然后在需要鉴权的方法上添加权限注解

@RestController
public class UserController{

  private final UserService userService;

    @PreAuthorize("hasAuthority('sys:user:page')")
    public Response<IPage<UserVO>> page(QueryUserPageParam param) {
        return Response.success(userService.page(param));
    }
}

Spring Security的常用权限注解有:

  • @PreAuthorize:前置校验权限,在方法执行之前校验权限,支持Spel表达式
  • @PostAuthorize:后置权限校验,在方法执行结束以后进行校验,可以对返回结果进行校验,支持Spel表达式
  • @PreFilter:对方法参数进行过滤
  • @PostFilter:对方法结果进行过滤

具体每个权限注解的使用方式可以自行去官网学习,这里就不具体介绍了。

下面就以最常用的@PreAuthorize注解为例,介绍一下Spring Security基于注解鉴权的流程与原理:

  1. AuthorizationManagerBeforeMethodInterceptor
    (授权管理前置方法拦截器),会将权限注解与
    AuthorizationManager
    (授权管理器)进行关联及初始化

    Untitled
  2. AuthorizationManagerBeforeMethodInterceptor
    拦截器拦截到请求后,会根据权限注解
    @PostAuthorize
    调用匹配的
    PreAuthorizeAuthorizationManager#check
    方法,并从SecurityContextHolder上下文中获取
    Authentication
    对象,将
    Supplier<Authentication>

    MethodInvocation
    作为参数传递给
    PreAuthorizeAuthorizationManager#check
    方法。

Untitled
Untitled
Untitled
  1. AuthorizationManager
    授权管理器使用
    MethodSecurityExpressionHandler
    解析
    @PostAuthorize
    注解的 SpEL 表达式,并从包含
    Supplier<Authentication>

    MethodInvocation

    MethodSecurityExpressionRoot
    构建相应的
    EvaluationContext

    Untitled
  2. 然后从
    Supplier
    读取
    Authentication
    ,并检查其权限集合中是否有
    sys:user:page

    Untitled
  3. 如果校验通过,将继续调用业务方法。如果校验不通过,会发布一个
    AuthorizationDeniedEvent
    ,并抛出一个
    AccessDeniedException

    ExceptionTranslationFilter
    会捕获并处理。



基于配置的授权校验

基于配置的授权校验是通过
AuthorizationFilter
实现的,首先我们需要配置授权校验规则:

@Configuration
public class SecurityConfig {

  /**
     * 不需要校验权限的资源
     */

    public static final String[] PERMIT_URL = new String[]{
            // knife4j 资源
            "/doc.html",
            "/favicon.ico",
            "/swagger-resources",
            "/v3/**",
            "/webjars/**",
            // 监控接口
            "/actuator/**",
            // 登录接口
           "/login",
            // 注册接口
           "/register",
    };

    @Bean
    public SecurityFilterChain apiFilterChain(HttpSecurity httpSecurity,
                                              TokenFilter tokenFilter,
                                              AuthenticationEntryPoint authenticationEntryPoint,
                                              ExceptionFilter exceptionFilter,
                                              AccessDeniedHandler accessDeniedHandler)
 throws Exception 
{
        // 配置token校验过滤器
        httpSecurity.addFilterBefore(tokenFilter, AuthorizationFilter.class);
        // 配置异常处理过滤器
        httpSecurity.addFilterBefore(exceptionFilter, ExceptionTranslationFilter.class);
        // 配置异常处理
        httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
        httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
        // 配置授权拦截规则
        httpSecurity.authorizeHttpRequests()
             // 配置放行的规则 这样配置只能在AuthorizationManager#check方法时返回true 如果不经过AuthorizationManager则不生效(比如自定义Filter)
             // 可以通过自定义WebSecurityCustomizer 来达到SecurityFilterChain中的Filter忽略处理 参考下方自定义WebSecurityCustomizer 
             .antMatchers(PERMIT_URL).permitAll()
                .antMatchers("/sys/user/page").hasAuthority("sys:user:page")
                .anyRequest().authenticated();
        // 配置组件 基于JWT认证 因此禁用csrf
        httpSecurity.csrf().disable();
        // 基于JWT认证 因此禁用session
        httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        // 禁用缓存
        httpSecurity.headers().cacheControl().disable();
        // 允许跨域
        httpSecurity.cors();
        return httpSecurity.build();
    }

    /**
    自定义WebSecurityCustomizer忽略指定路径 但是Spring Security不建议这么做 建议通过antMatchers(PERMIT_URL).permitAll()实现 
    如果想使用此方式 只需要将其注册为Spring Bean即可
    */

    // @Bean
    public WebSecurityCustomizer webSecurityCustomizer(){
        // 配置放行规则
        return customizer -> customizer.ignoring().antMatchers(PERMIT_URL);
    }
}

这里咱们还是以前置校验是否拥有某个权限为例:

// 配置授权拦截规则 校验
httpSecurity.authorizeHttpRequests()
                .antMatchers("/sys/user/page").hasAuthority("sys:user:page")
                .anyRequest().authenticated();
Untitled
Untitled

这里初始化设置了
AuthorityAuthorizationManager
作为
AuthorizationManager
的实现,不同的权限校验功能可能对应的
AuthorizationManager
实现会不一样,比如
anyRequest().authenticated()
对应的
AuthorizationManager
实现则是
AuthenticatedAuthorizationManager

Untitled


AuthorizationFilter
执行的时候,会根据配置的授权规则找到对应的
AuthorizationManager
实现,然后执行
check
方法,并从SecurityContextHolder上下文中获取
Authentication
对象,将
Authentication

request
作为参数传递给
AuthorizationManager#check
方法。 这里根据上面的规则
hasAuthority()
对应的
AuthorizationManager
实现就是
AuthorityAuthorizationManager

Untitled
Untitled
Untitled

然后
AuthorityAuthorizationManager
会根据SecurityContextHolder的
Authentication
中获取所有权限和配置需要的权限进行对比,如果用户上下文SecurityContextHolder中存储的权限集合包含配置需要的权限则返回true通过,反之则返回false。



注意事项

  1. 需要特别说明的是,Spring Security存储角色和权限都是使用的 GrantedAuthority 对象,因此Spring Security规定角色需要加上统一前缀方便与权限区分开

这个统一前缀默认为
ROLE_
,无论是基于配置还是基于注解的授权校验都是同样的规则。

Untitled

当然你也可以自定义这个前缀,只需要将自定义的
GrantedAuthorityDefaults
对象注册进Spring容器即可。

@Configuration
public class SecurityConfig {

    /**
     * 配置Role前缀
     * @return
     */

    @Bean
    static GrantedAuthorityDefaults grantedAuthorityDefaults() {
        return new GrantedAuthorityDefaults("ROLE_");
    }
}
Untitled
  1. 细心的朋友可能已经发现了,无论是基于注解还是基于配置的授权校验,都是从用户上下文SecurityContextHolder中获取当前用户拥有的角色和权限,然后再和需要的权限去比较是否拥有权限。所以我们需要在授权校验之前需要往用户上下文SecurityContextHolder中设置当前用户所拥有的权限。这里就需要用到自定义Filter了。


自定义Filter

如果Spring Security中的
SecurityFilter
不能满足你的业务需求,需要自定义
SecurityFilter
。比如我们需要自定义一个Filter用于解析请求的Token,然后从Token中获取用户信息和权限。自定义
SecurityFilter
有两种方式:

  1. 自定义 SecurityFilter 实现jakarta.servlet.Filter,在doFilter方法中实现自己的业务逻辑,参考案例:
public class TokenFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String token= request.getHeader("Authorization");
        boolean hasAccess = checkToken(token);
        if (hasAccess) {
            filterChain.doFilter(request, response);
            return;
        }
        // 注意AuthenticationException或AccessDeniedException会被ExceptionTranslationFilter处理 如果是其他异常需要自己处理 Springboot全局异常无法处理
        throw new AuthenticationException("权限不足");
    }

}

然后将该
SecurityFilter
注册进
SecurityFilterChain

@Configuration
public class SecurityConfig {

 @Bean
 SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
     http.addFilterBefore(new TokenFilter(), AuthorizationFilter.class);
     return http.build();
 }
}

注意,如果想把jakarta.servlet.Filter的实现注册为Spring Bean,这可能会导致 filter 被调用两次,一次由容器调用,一次由 Spring Security 调用,而且顺序不同。可以通过声明
FilterRegistrationBean
Bean 并将其
enabled
属性设置为
false
来告诉 Spring Boot不要向容器注册它。配置如下:

@Bean
public FilterRegistrationBean<TokenFilter> tenantFilterRegistration(TokenFilter filter) {
    FilterRegistrationBean<TokenFilter> registration = new FilterRegistrationBean<>(filter);
    registration.setEnabled(false);
    return registration;
}
  1. 自定义 SecurityFilter 继承OncePerRequestFilter,这样能保证每个请求只会调用一次的filter(
    推荐方式
    ),然后将该 SecurityFilter 注册进 SecurityFilterChain
@Component
public class TokenFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        // 如果时不需要授权的URI直接放行
        if (!AntPathUtil.match(requestURI, SecurityConfig.PERMIT_URL)){
          String token = request.getHeader("Authorization");
          if (StrUtil.isBlank(token)){
            // 注意AuthenticationException或AccessDeniedException会被ExceptionTranslationFilter处理 如果是其他异常需要自己处理 Springboot全局异常无法处理
            // AuthenticationCredentialsNotFoundException 是 AuthenticationException的子类
              throw new AuthenticationCredentialsNotFoundException("请先登录");
          }
          // 这里是伪代码 逻辑就是通过token解析出用户信息 然后查询出用户所有角色和权限
          User user = parse(token);
          List<GrantedAuthority> authorities = listUserAllPermissions(user.getId());
          PreAuthenticatedAuthenticationToken authenticationToken = new PreAuthenticatedAuthenticationToken(user, null, authorities);
          // 设置子线程支持从父线程获取用户上下文 注意使用ForkJoinPool无法生效 如果是线程池可能导致数据错误 谨慎使用
          SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
          // 设置上下文
          SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        // 放行
        filterChain.doFilter(request, response);
    }
}
public class AntPathUtil {

    private AntPathUtil(){}

    public static final AntPathMatcher MATCHER = createMatcher();

    public static boolean match(String path, String... pattern){
        if (ArrayUtil.isEmpty(pattern)){
            return true;
        }
        if (StrUtil.isBlank(path)){
            return false;
        }
        return Arrays.stream(pattern).filter(p -> MATCHER.match(p, path)).findAny().isPresent();
    }

    private static AntPathMatcher createMatcher(){
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        antPathMatcher.setCaseSensitive(false);
        return antPathMatcher;
    }
}

然后将该
SecurityFilter
注册进
SecurityFilterChain

@Configuration
public class SecurityConfig {
 @Bean
 SecurityFilterChain filterChain(HttpSecurity http, TokenFilter tokenFilter) throws Exception {
     http.addFilterBefore(tokenFilter, AuthorizationFilter.class);
     return http.build();
 }
}


总结

最后总结Spring Security的认证和授权的流程如下:

Untitled

梳理一下上面的流程:

  1. 首先,用户携带用户名密码通过LoginController进行登录(认证流程),如果登录成功则返回token(推荐使用JWT作为token)
  2. 后续其他请求,携带通过登录获取得到的token,然后先被TokenFilter解析token获取用户信息,并将用户信息写入SecurityContextHolder
  3. 然后进行授权流程
  4. AuthorizationManager 通过从SecurityContextHolder获取到当前用户的authentication(权限集合),然后与需要的权限进行对比,从而校验当前用户是否有权限使用当前业务功能

本文使用
markdown.com.cn
排版

写在开头

上篇文章写到CAS算法时,里面使用AtomicInteger举例说明,这个类在java.unit.concurrent.atomic包中,存储的都是一些原子类,除此之外,
“java.unit.concurrent”
,这个包作为Java中最重要的一个并发工具包,大部分的并发类都在其中,我们今天就来继续学习这个包中的其他并发工具类。

image

Java并发包

本来今日计划是学习ReentrantLock(可重入锁)的,但打开包后发现还有AbstractOwnableSynchronizer、AbstractQueuedSynchronizer、AbstractQueuedLongSynchronizer这三个类,基于它们在锁中的重要性,我们今天就花一篇的时间,单独来学习一下啦。

image

AQS相关类

AOS、AQS、AQLS

  • AOS(AbstractOwnableSynchronizer)
    : JDK1.6时发布的,是AQS和AQLS的父类,这个类的主要作用是表示持有者与锁之间的关系。
    image
AOS
  • AQS(AbstractQueuedSynchronizer)
    :JDK1.5时发布,
    抽象队列同步器
    ,是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的同步器,诸如:ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue等等皆是基于 AQS 的。AQS 内部使用了一个 volatile 的变量 state(int类型) 来作为资源的标识。
  • AQLS(AbstractQueuedLongSynchronizer)
    :这个类诞生于JDK1.6,原因时上述的int类型的state资源,在当下的业务场景中,资源数量有可能超过int范围,因此,便诞生了这个类,采用Long类型的state。
//AQS中共享变量,使用volatile修饰保证线程可见性
private volatile int state;

//AQLS中共享变量,采用long类型
private volatile long state;

AQS的底层原理

以上我们大致的介绍了一下AQS的周边,在很多大厂的面试中提及AQS,被问到最多的就是:“麻烦介绍一下AQS的底层原理?”,很多同学都浅尝辄止,导致答不出面试官满意的答案,今天我们就花一定的篇幅去一起学习下AQS的底层结构与实现!

AQS的核心思想

AQS的核心思想或者说实现原理是:在多线程访问共享资源时,若标识的共享资源空闲,则将当前获取到共享资源的线程设置为有效工作线程,共享资源设置为锁定状态(独占模式下),其他线程没有获取到资源的线程进入阻塞队列,等待当前线程释放资源后,继续尝试获取。

AQS的数据结构

其实AQS的实现主要基于两个内容,分别是
state

CLH
队列

①state

state 变量由 volatile 修饰,用于展示当前临界资源的获锁情况。

// 共享变量,使用volatile修饰保证线程可见性
private volatile int state;

AQS内部还提供了获取和修改state的方法,注意,这里的方法都是final修饰的,意味着不能被子类重写!

【源码解析1】

//返回同步状态的当前值
protected final int getState() {
     return state;
}
 // 设置同步状态的值
protected final void setState(int newState) {
     state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
      return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

②CLH双向队列

我们在上面提到了独占模式下,没有获取资源的线程会被放入队列,然后阻塞、唤醒、锁的重分配机制,就是基于CLH实现的。
CLH 锁
(Craig, Landin, and Hagersten locks)是一种自旋锁的改进,是一个虚拟的双向队列,所谓虚拟是指没有队列的实例,内部仅存各结点之间的关联关系。

AQS 将每条请求共享资源的线程封装成一个 CLH 队列锁的一个节点(Node)来实现锁的分配。在 CLH 队列锁中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。

image

CLH结构

CLH的原理

image

CLH原理描述

AQS资源共享

在AQS的框架中对于资源的获取有两种方式:

  • 独占模式(Exclusive)
    :资源是独有的,每次只能一个线程获取,如ReentrantLock;
  • 共享模式(Share)
    :资源可同时被多个线程获取,具体可获取个数可通过参数设定,如CountDownLatch。

①独占模式

以ReentrantLock为例,其内部维护了一个state字段,用来标识锁的占用状态,初始值为0,当线程1调用lock()方法时,会尝试通过tryAcquire()方法(
钩子方法
)独占该锁,并将state值设置为1,如果方法返回值为true表示成功,false表示失败,失败后线程1被放入等待队列中(CLH队列),直到其他线程释放该锁。

但需要注意的是,在线程1获取到锁后,在释放锁之前,自身可以多次获取该锁,每获取一次state加1,这就是锁的可重入性,这也说明ReentrantLock是可重入锁,
在多次获取锁后,释放时要释放相同的次数,这样才能保证最终state为0
,让锁恢复到未锁定状态,其他线程去尝试获取!

image

独占模式流程图

②共享模式

CountDownLatch(倒计时器)就是基于AQS共享模式实现的同步类,任务分为 N 个子线程去执行,state 也初始化为 N(
注意 N 要与线程个数一致
)。这 N 个子线程开始执行任务,每执行完一个子线程,就调用一次 countDown() 方法。该方法会尝试使用 CAS(Compare and Swap) 操作,让 state 的值减少 1。当所有的子线程都执行完毕后(即 state 的值变为 0),CountDownLatch 会调用 unpark() 方法,唤醒主线程。这时,主线程就可以从 await() 方法(CountDownLatch 中的await() 方法而非 AQS 中的)返回,继续执行后续的操作。

【注意】
一般情况下,子类只需要根据需求实现其中一种模式就可以,当然也有同时实现两种模式的同步类,如 ReadWriteLock。

AQS的Node节点

上述的两种共享模式、线程的引用、前驱节点、后继节点等都存储在Node对象中,我们接下来就走进Node的源码中一探究竟!

【源码解析2】

static final class Node {
    // 标记一个结点(对应的线程)在共享模式下等待
    static final Node SHARED = new Node();
    // 标记一个结点(对应的线程)在独占模式下等待
    static final Node EXCLUSIVE = null;

    // waitStatus的值,表示该结点(对应的线程)已被取消
    static final int CANCELLED = 1;
    // waitStatus的值,表示后继结点(对应的线程)需要被唤醒
    static final int SIGNAL = -1;
    // waitStatus的值,表示该结点(对应的线程)在等待某一条件
    static final int CONDITION = -2;
    /*waitStatus的值,表示有资源可用,新head结点需要继续唤醒后继结点(共享模式下,多线程并发释放资源,而head唤醒其后继结点后,需要把多出来的资源留给后面的结点;设置新的head结点时,会继续唤醒其后继结点)*/
    static final int PROPAGATE = -3;

    // 等待状态,取值范围,-3,-2,-1,0,1
    volatile int waitStatus;
    volatile Node prev; // 前驱结点
    volatile Node next; // 后继结点
    volatile Thread thread; // 结点对应的线程
    Node nextWaiter; // 等待队列里下一个等待条件的结点


    // 判断共享模式的方法
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    // 其它方法忽略,可以参考具体的源码
}

// AQS里面的addWaiter私有方法
private Node addWaiter(Node mode) {
    // 使用了Node的这个构造函数
    Node node = new Node(Thread.currentThread(), mode);
    // 其它代码省略
}

CANCELLED:
表示当前节点(对应的线程)已被取消。当等待超时或被中断,会触发进入为此状态,进入该状态后节点状态不再变化;
SIGNAL:
后面节点等待当前节点唤醒;
CONDITION:
Condition 中使用,当前线程阻塞在Condition,如果其他线程调用了Condition的signal方法,这个节点将从等待队列转移到同步队列队尾,等待获取同步锁;
PROPAGATE:
共享模式,前置节点唤醒后面节点后,唤醒操作无条件传播下去;
0:中间状态,当前节点后面的节点已经唤醒,但是当前节点线程还没有执行完成。

AQS的获取资源与释放资源

有了以上的知识积累后,我们再来看一下AQS中关于获取资源和释放资源的实现吧。

获取资源

在AQS中获取资源的是入口是acquire(int arg)方法,arg 是要获取的资源个数,在独占模式下始终为 1。

【源码解析3】

public final void accquire(int arg) {
    // tryAcquire 再次尝试获取锁资源,如果尝试成功,返回true,尝试失败返回false
    if (!tryAcquire(arg) &&
        // 走到这,代表获取锁资源失败,需要将当前线程封装成一个Node,追加到AQS的队列中
        //并将节点设置为独占模式下等待
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 线程中断
        selfInterrupt();
}

tryAcquire()是一个可被子类具体实现的钩子方法,用以在独占模式下获取锁资源,如果获取失败,则把线程封装为Node节点,存入等待队列中,实现方法是addWaiter(),我们继续跟入源码去看看。

【源码解析4】

private Node addWaiter(Node mode) {
 //创建 Node 类,并且设置 thread 为当前线程,设置为排它锁
 Node node = new Node(Thread.currentThread(), mode);
 // 获取 AQS 中队列的尾部节点
 Node pred = tail;
 // 如果 tail == null,说明是空队列,
 // 不为 null,说明现在队列中有数据,
 if (pred != null) {
  // 将当前节点的 prev 指向刚才的尾部节点,那么当前节点应该设置为尾部节点
  node.prev = pred;
  // CAS 将 tail 节点设置为当前节点
  if (compareAndSetTail(pred, node)) {
   // 将之前尾节点的 next 设置为当前节点
   pred.next = node;
   // 返回当前节点
   return node;
  }
 }
 enq(node);
 return node;
}

// 自旋CAS插入等待队列
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

在这部分源码中,将获取资源失败的线程封装后的Node节点存入队列尾部,考虑到多线程情况下的节点插入问题,这里提供了自旋CAS的方式保证节点的安全性。

等待队列中的所有线程,依旧从头结点开始,一个个的尝试去获取共享资源,这部分的实现可以看acquireQueued()方法,我们继续跟入。

【源码解析5】

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        // interrupted用于记录线程是否被中断过
        boolean interrupted = false;
        for (;;) { // 自旋操作
            // 获取当前节点的前驱节点
            final Node p = node.predecessor();
            // 如果前驱节点是head节点,并且尝试获取同步状态成功
            if (p == head && tryAcquire(arg)) {
                // 设置当前节点为head节点
                setHead(node);
                // 前驱节点的next引用设为null,这时节点被独立,垃圾回收器回收该节点
                p.next = null; 
                // 获取同步状态成功,将failed设为false
                failed = false;
                // 返回线程是否被中断过
                return interrupted;
            }
            // 如果应该让当前线程阻塞并且线程在阻塞时被中断,则将interrupted设为true
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 如果获取同步状态失败,取消尝试获取同步状态
        if (failed)
            cancelAcquire(node);
    }
}

在这个方法中,从等待队列的head节点开始,循环向后尝试获取资源,获取失败则继续阻塞,头结点若获取资源成功,则将后继结点设置为头结点,原头结点从队列中回收掉。

释放资源

相对于获取资源,AQS中的资源释放就简单多啦,我们直接上源码!

【源码解析6】

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

private void unparkSuccessor(Node node) {
    // 如果状态是负数,尝试把它设置为0
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // 得到头结点的后继结点head.next
    Node s = node.next;
    // 如果这个后继结点为空或者状态大于0
    // 通过前面的定义我们知道,大于0只有一种可能,就是这个结点已被取消(只有 Node.CANCELLED(=1) 这一种状态大于0)
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从尾部开始倒着寻找第一个还未取消的节点(真正的后继者)
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 如果后继结点不为空,
    if (s != null)
        LockSupport.unpark(s.thread);
}

这里的tryRelease(arg)通过是个钩子方法,需要子类自己去实现,比如在ReentrantLock中的实现,会去做state的减少操作
int c = getState() - releases;
,毕竟这是一个可重入锁,直到state的值减少为0,表示锁释放完毕!

接下来会检查队列的头结点。如果头结点存在并且waitStatus不为0,这意味着还有线程在等待,那么会调用unparkSuccessor(Node h)方法来唤醒后续等待的线程。

总结


好啦,到这里我们对于AQS的学习就告一段落啦,后面我们准备使用AQS去自定义一个同步类,持续关注唷

前言

Git是一种强大的分布式版本控制系统,在实际项目开发中使用Git进行分支管理是非常常见的做法,因为它可以帮助团队高效的协作和管理项目的不同版本,今天我们来讲讲在实际项目中最常用的Git分支管理策略
Git Flow

常见的Git分支管理策略

Git Flow:

Git Flow是一种非常流行的分支管理策略,由Vincent Driessen提出。Git Flow的核心思想是保持主分支稳定,通过使用不同类型的分支来隔离不同类型的工作。

GitHub Flow:

GitHub Flow是由GitHub提出的一种更简化的分支管理策略。它强调持续交付和快速部署。

GitLab Flow:

GitLab Flow是GitLab推荐的一种分支管理策略,它结合了Git Flow和GitHub Flow的一些特点。

Trunk Based Development

Trunk-Based Development是一种极端简化的分支管理策略,它强调在单一的主分支(通常称为trunk或mainline)上进行所有的开发工作。

Git开发、发布、缺陷分离模型介绍

在一些流程完善的公司往往都会有着自己一套比较完善的Git分支管理模型来保障开发和生成环境的代码稳定性,而Git开发、发布、缺陷分离模型是一种流行且适用于大多数团队的Git分支管理模型,它支持
master/develop/feature/release/hotfix
类型分支。使用这种分支管理模型可以有效地隔离开发、发布和缺陷修复工作,提高代码的质量和稳定性。同时,也可以更好地协作和进行版本管理。如下是一张详细的
master/develop/feature/release/hotfix类型
分支管理图:

对应分支的作用

master分支

master分支是主分支,包含了已经发布到生产环境的稳定,可靠版本的代码。一般情况下,master分支应该只用于发布新版本,而不应该直接修改或提交新的功能。

develop分支

develop分支是开发分支,包含了当前正在进行的所有功能和任务。所有新功能开发、改进、优化等都应该从此分支开始,并最终合并回此分支。

feature分支

feature分支是从develop分支创建的分支,通常用于开发新功能。每个新功能都应该从develop分支开始,并在一个独立的feature分支上进行开发工作。一旦新功能得到完全实现、测试并且可靠,该分支就会被合并回develop分支。

release分支

release分支是从develop分支创建的分支,通常用于为即将发布的版本做准备工作。在此分支上可以进行最终的测试、修复bug、检查文档等操作,以确保发布版本的质量。一旦准备工作完成并且得到完全测试,该分支就会被合并回master分支,并作为新的发布版本。并将该分支合并回develop分支,以便后续的开发工作。

hotfix分支

hotfix分支是从master分支创建的分支,用于在生产环境中紧急修复问题。修复完毕后,该分支将会被合并回master和develop分支。

Git快速入门教程

Git可视化管理工具

参考文章

https://nvie.com/posts/a-successful-git-branching-model/