程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • Spring

    • Spring6 - 概述
    • Spring6 - 入门
    • Spring6 - IOC(基于XML)
    • Spring6 - IOC(基于注解)
    • spring6 - FactoryBean
    • Spring6 - Bean的作用域
    • Spring6 - Bean生命周期
    • Spring6 - Bean循环依赖
    • Spring6 - 手写IOC容器
    • Spring6 - AOP
    • Spring6 - 自定义注解
      • 一、前言
      • 二、 java自定义注解的定义、描述
        • 1. 创建一个注解的基本元素
        • 2. 元注解(@Target、@Retention、@Inherited、@Documented)
      • 三、自定义注解的使用DEMO
        • 1.权限校验注解(校验token)
        • 1) 首先我们创建一个注解,标志那些类需要校验token
        • 2) 然后再创建一个AOP切面类来拦截这个注解
        • 3)把注解加在需要校验的接口方法上
        • 2.角色校验注解(springsecurity中的角色校验)
        • 1)创建一个自定义注解
        • 2)创建一个拦截器
        • 3)配置拦截器
        • 4)把注解加到接口的类或方法上验证
        • 3.字段属性注入注解
        • 1)创建自定义注解
        • 2)需要写一个实现类实现HandlerMethodArgumentResolver接口
        • 3)创建拦截器
        • 4)把注解加到接口的参数上
        • 4.对象的属性校验注解
        • 1)创建一个自定义验证注解
        • 2)实现验证的类
        • 3)使用@Valid注解加在类上校验
        • 4)在全局异常里捕获校验异常
        • 5.接口的操作日志注解
        • 1)创建自定义注解
        • 2)通过AOP切面实现注解功能
        • 3)把注解加在接口方法上
        • 6.缓存注解
        • 1)创建自定义注解
        • 2)然后再创建一个AOP切面类来实现这个注解
        • 3)把注解加在查询用户信息的Service上
        • 7.防刷新注解
        • 1)创建自定义注解
        • 2)再创建一个AOP切面类来实现这个注解
        • 8.动态切换数据源注解
        • 1)创建自定义注解
        • 2)再创建一个AOP切面类来实现这个注解
        • 3)把这个注解加在需要使用多数据源的service方法或类上
    • Spring6 - Junit
    • Spring6 - 事务
    • Spring6 - Resource
    • Spring6 - 国际化
    • Spring6 - 数据校验
    • Spring6 - Cache
    • Spring集成Swagger2
  • Spring生态
  • Spring
scholar
2024-04-25
目录

Spring6 - 自定义注解

# 一、前言

初学spring的时候使用注解总觉得使用注解很神奇,加一个注解就能实现想要的功能,很好奇,也想自己根据需要写一些自己实现的自定义注解。问题来了,自定义注解到底是什么?肯定会有人和我一样有这个疑惑,我根据自己的理解总结一下。看完下面的几个使用自定义注解的实战demo,小伙伴大概就懂怎么用了。

其实注解一点也不神奇,注解就是一种标志,单独使用注解,就相当于在类、方法、参数和包上加上一个装饰,什么功能也没有,仅仅是一个标志,然后这个标志可以加上一些自己定义的参数。就像下面这样,创建一个@interface的注解,然后就可以使用这个注解了,加在我们需要装饰的方法上,但是什么功能也没有

@Target(ElementType.METHOD) // 注解应用于方法
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时可用,使得可以通过反射读取
@Inherited // 允许子类继承父类的注解
@Documented // 注解将被包含在JavaDoc中
public @interface XinLinLog {
    String value() default ""; // 注解的参数,默认值为空字符串
}
1
2
3
4
5
6
7

注解中可以定义多种类型的参数,这些参数定义了注解在使用时可以接受的配置信息:

  • 基本数据类型(int、long、float等)
  • String
  • Class类型
  • 枚举类型
  • 注解类型
  • 以上类型的数组

参数在注解内部通过无参方法的方式声明。例如:

// 这看起来像是一个方法,但实际上是一个注解的参数定义。
String value() default ""; // 默认值为空字符串
1
2

这里value()方法定义了一个名为value的参数,它的类型为String,并指定了一个默认值为空字符串。虽然语法上看起来像是方法,但在使用时这些元素实际上代表了注解的配置属性。

参数使用示例

定义好注解及其参数后,注解可以被附加到代码的相应部分,并在使用时通过参数传递特定的值或使用定义的默认值。例如,在一个方法上使用XinLinLog注解:

@XinLinLog(value = "This is a log message.")
public void someMethod() {
    // 方法体
}
1
2
3
4

在这个示例中,XinLinLog注解应用于someMethod方法,并通过value参数传递了一个字符串。

运行时的参数处理

自定义注解本身不会影响程序的行为,要使注解"生效",需要在运行时通过反射来处理这些注解。例如,可以在程序运行时检查方法是否包含XinLinLog注解,并获取value参数的内容:

// 假设我们已经获取了某个类的Method对象实例,此处使用省略号表示
Method method = ...; // 获取某个方法,例如method = someClass.getMethod("someMethodName");

// 检查这个方法是否被指定的注解XinLinLog标记
if (method.isAnnotationPresent(XinLinLog.class)) {
    // 如果方法上存在XinLinLog注解,则获取该注解的实例
    XinLinLog log = method.getAnnotation(XinLinLog.class);
    
    // 从注解实例中获取'value'属性的值,并打印它
    System.out.println(log.value()); // 输出注解中'value'属性设置的日志信息
}
1
2
3
4
5
6
7
8
9
10
11

这段代码检查一个方法是否有XinLinLog注解,并打印出注解中value参数的值。

# 二、 java自定义注解的定义、描述

注解是一种能被添加到java源代码中的元数据,方法、类、参数和包都可以用注解来修饰。注解可以看作是一种特殊的标记,可以用在方法、类、参数和包上,程序在编译或者运行时可以检测到这些标记而进行一些特殊的处理。

# 1. 创建一个注解的基本元素

修饰符 访问修饰符必须为public,不写默认为pubic; 关键字 关键字为@interface; 注解名称 注解名称为自定义注解的名称,例如上面的XinLinLog 就是注解名称 注解类型元素 注解类型元素是注解中内容,根据需要标志参数,例如上面的注解的value;

# 2. 元注解(@Target、@Retention、@Inherited、@Documented)

我们上面的创建的注解XinLinLog上面还有几个注解(@Target、@Retention、@Inherited、@Documented),这四个注解就是元注解,元注解的作用就是负责注解其他注解。Java5.0定义了4个标准的元注解类型,它们被用来提供对其它 注解类型作标志操作(可以理解为最小的注解,基础注解)

  • @Target:用于描述注解的使用范围,该注解可以使用在什么地方
Target类型 描述
ElementType.TYPE 应用于类、接口(包括注解类型)、枚举
ElementType.FIELD 应用于属性(包括枚举中的常量)
ElementType.METHOD 应用于方法
ElementType.PARAMETER 应用于方法的形参
ElementType.CONSTRUCTOR 应用于构造函数
ElementType.LOCAL_VARIABLE 应用于局部变量
ElementType.ANNOTATION_TYPE 应用于注解类型
ElementType.PACKAGE 应用于包

备注:例如@Target(ElementType.METHOD),标志的注解使用在方法上,但是我们在这个注解标志在类上,就会报错

  • @Retention:表明该注解的生命周期
生命周期类型 描述
RetentionPolicy.SOURCE 编译时被丢弃,不包含在类文件中
RetentionPolicy.CLASS JVM加载时被丢弃,包含在类文件中,默认值
RetentionPolicy.RUNTIME 由JVM 加载,包含在类文件中,在运行时可以被获取到
  • @Inherited:是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
  • @Documented:表明该注解标记的元素可以被Javadoc 或类似的工具文档化

# 三、自定义注解的使用DEMO

上面总结的注解的定义,但是创建这样一个注解,仅仅是一个标志,装饰类、方法、属性的,并没有功能,要想实现功能,需要我们通过拦截器、AOP切面这些地方获取注解标志,然后实现我们的功能。

java自定义注解的使用范围 一般我们可以通过注解来实现一些重复的逻辑,就像封装了的一个方法,可以用在一些权限校验、字段校验、字段属性注入、保存日志、缓存

# 1.权限校验注解(校验token)

有些项目进入到接口后调用公用方法来校验token,这样看起来代码就有点不优雅,我们可以写自定义注解来进行校验token。 例如有个项目,前端是把token放到json里面传到后端(也有一些项目放到请求头的header里面,方式一样),没用注解之前,我们可能是通过调用公共的方法去校验token,如validateToken(token),然后每个接口都有这一段代码,我们用注解的模式替换 在这里插入图片描述

# 1) 首先我们创建一个注解,标志那些类需要校验token
/**
 * 自定义注解 AppAuthenticationValidate 用于标记需要进行应用级认证的方法。
 * 该注解用于校验方法执行前用户的权限,以及其他安全相关的校验。
 */
@Target(ElementType.METHOD)  // 指定注解使用的位置是方法。这意味着此注解只能用于方法上。
@Retention(RetentionPolicy.RUNTIME)  // 指定注解的保留策略为运行时。这使得可以在运行时通过反射访问该注解的信息。
public @interface AppAuthenticationValidate {
    /**
     * requestParams 元素用于指定需要校验的请求参数名。
     * 这些参数名将在使用此注解的方法中被检查,以确保它们存在且有效,通常用于校验请求中必须包含的关键字段。
     *
     * @return 返回一个字符串数组,每个字符串是一个请求参数的名称。
     */
    String[] requestParams() default {};  // 默认为空数组,用户可以在使用注解时指定具体需要校验的参数。
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 2) 然后再创建一个AOP切面类来拦截这个注解

拦截使用这个注解的方法,同时获取注解上面的requestParams参数,校验json里面必填的属性是否存在

@Aspect  // 表明这是一个切面类
@Component  // 表明该类是Spring容器管理的一个Bean
@Slf4j  // Lombok注解,为类提供一个属性名为log的log4j日志对象
public class AppAuthenticationValidateAspect {

    @Reference(check = false, timeout = 18000)  // Dubbo服务引用,关闭启动时检查,设置超时为18000毫秒
    private CommonUserService commonUserService;  // 用于执行用户服务相关操作的服务对象

    @Before("@annotation(cn.com.bluemoon.admin.web.common.aspect.AppAuthenticationValidate)")  // 前置通知,拦截标有AppAuthenticationValidate注解的方法
    public void repeatSumbitIntercept(JoinPoint joinPoint) throws Throwable {

        // 获取调用方法的参数
        Object[] o = joinPoint.getArgs();
        JSONObject jsonObject = null;
        String source = null;
        String[] parameterNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();  // 获取方法的参数名数组

        // 循环处理所有参数,查找名为"source"和"jsonObject"的参数
        for (int i = 0; i < parameterNames.length; i++) {
            String paramName = parameterNames[i];
            if ("source".equals(paramName)) {
                source = (String) o[i];  // 获取token来源
            }
            if ("jsonObject".equals(paramName)) {
                jsonObject = (JSONObject) o[i];  // 获取JSON对象参数
            }
        }

        // 如果JSON对象为空,抛出异常
        if (jsonObject == null) {
            throw new WebException(ResponseConstant.ILLEGAL_PARAM_CODE, ResponseConstant.ILLEGAL_PARAM_MSG);
        }

        // 从JSON对象中获取token
        String token = jsonObject.getString("token");
        if (StringUtils.isBlank(token)) {
            throw new WebException(ResponseConstant.TOKEN_EXPIRED_CODE, "登录超时,请重新登录");
        }

        // 获取当前方法签名和方法对象
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        // 获取方法上的AppAuthenticationValidate注解对象
        AppAuthenticationValidate annotation = method.getAnnotation(AppAuthenticationValidate.class);
        String[] requestParams = annotation.requestParams();  // 获取注解中指定的必填参数

        // 校验JSON对象中的必填参数
        ParamsValidateUtil.isNotBlank(jsonObject, requestParams);

        // 根据source判断调用的token校验方法
        ResponseBean<String> response;
        if (StringUtils.isBlank(source)) {
            response = commonUserService.checkAppToken(token);  // 检查token
        } else {
            response = commonUserService.checkAppTokenByAppType(token, source);  // 根据应用类型检查token
        }

        // 根据响应结果处理
        if (response.getIsSuccess() && ResponseConstant.REQUEST_SUCCESS_CODE == response.getResponseCode()) {
            String empCode = response.getData();  // 获取员工编码
            log.info("---token ={}, empCode={}---", token, empCode);
            jsonObject.put(ProcessParamConstant.APP_EMP_CODE, empCode);  // 将员工编码存入JSON对象
        } else {
            log.info("---token验证不通过,token ={}---", token);
            throw new WebException(ResponseConstant.TOKEN_EXPIRED_CODE, "登录超时,请重新登录");
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# 3)把注解加在需要校验的接口方法上

这个注解同时校验了必填字段,校验完token后同时会把token的用户信息加在json对象里面 在这里插入图片描述

备注:有些项目会把token放到请求头header中,处理方式类似

# 2.角色校验注解(springsecurity中的角色校验)

我们在使用springsecurity有一个注解@PreAuthorize可以作用在类或方法上,用来校验是否有权限访问,我们可以模仿这个注解,写一个我们自定义注解来实现同样的功能

# 1)创建一个自定义注解
/**
 * 该注解用于授权和角色检查。
 * 它可以应用于类或方法上,用于指定哪些角色可以访问特定的类或方法。
 */
@Target({ElementType.METHOD, ElementType.TYPE})  // 指定注解可以应用于方法和类上。
@Retention(RetentionPolicy.RUNTIME)  // 指定注解在运行时依然有效,允许通过反射动态访问。
@Inherited  // 允许子类继承父类中的此注解,即如果一个类使用了这个注解,它的子类也将自动被此注解应用。
@Documented  // 指定此注解将被包含在javadoc中。
public @interface RoleAuthorize {
    /**
     * 定义角色数组,用于指定允许访问的角色列表。
     * 默认为空数组,表示没有角色限制。
     *
     * @return 允许访问的角色列表。
     */
    String[] value() default {};  // 注解的参数,用于存储角色名称的数组。
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 2)创建一个拦截器

这个拦截器拦截所有访问路径的url,如果访问方法上带有我们创建的自定义注解RoleAuthorize ,则获取这个注解上限定的访问角色,方法没有注解再获取这个类是否有这个注解,如果这个类也没有注解,则这个类的访问没有角色限制,放行,如果有则校验当前用户的springsecurity是否有这个角色,有则放行,没有则抛出和springsecurity一样的异常AccessDeniedException,全局异常捕获这个异常,返回状态码403(表示没有权限访问)

@Component
public class RoleInterceptor extends HandlerInterceptorAdapter{

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod)handler;

        //在方法上寻找注解
        RoleAuthorize permission = handlerMethod.getMethodAnnotation(RoleAuthorize.class);
        if (permission == null) {
            //方法不存在则在类上寻找注解则在类上寻找注解
            permission = handlerMethod.getBeanType().getAnnotation(RoleAuthorize.class);
        }

        //如果没有添加权限注解则直接跳过允许访问
        if (permission == null) {
            return true;
        }

        //获取注解中的值
        String[] validateRoles = permission.value();
        //校验是否含有对应的角色
        for(String role : validateRoles){
            //从springsecurity的上下文获取用户角色是否存在当前的角色名称
            if(AuthUserUtils.hasRole("ROLE_"+role)){
                return true;
            }
        }
        throw  new AccessDeniedException("没有权限访问当前接口");
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 3)配置拦截器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private RoleInterceptor roleInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加拦截器,并指定拦截所有路径
        registry.addInterceptor(roleInterceptor).addPathPatterns("/**");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • WebMvcConfigurer: 接口,用于配置Spring MVC。实现此接口允许自定义MVC配置。
  • addInterceptors: 方法用于向Spring MVC注册自定义拦截器。

备注

  • WebMvcConfigurerAdapter: 在Spring 5.0及更高版本中已废弃。现在推荐直接实现WebMvcConfigurer接口。
  • WebMvcConfigurationSupport vs. WebMvcConfigurer: 继承WebMvcConfigurationSupport将覆盖Spring Boot的自动配置。通常建议实现WebMvcConfigurer接口而不是扩展WebMvcConfigurationSupport,以保留Spring Boot的自动配置特性。
# 4)把注解加到接口的类或方法上验证

可以看到接口会返回无权限访问 在这里插入图片描述 在这里插入图片描述

# 3.字段属性注入注解

还有一种场景,我们校验token通过后,还需要通过token去换取用户信息,如果通过接口方法里面去调token换用户信息,好像不太优雅,我们用自定义注解把用户信息直接注入我们接口上的属性参数里面

# 1)创建自定义注解
/**
 * 注解用于自动注入当前登录用户的信息。
 * 当应用此注解到方法的参数上时,框架将自动填充这个参数
 * 与当前登录用户的信息。这通常用于Web应用中,避免了
 * 手动从会话中获取用户信息的重复代码。
 */
@Target(ElementType.PARAMETER)  // 指明这个注解只能被用于方法的参数上。
@Retention(RetentionPolicy.RUNTIME)  // 指明这个注解在运行时是有效的,使其能通过反射被读取。
public @interface LoginUserInfo {
    // 目前这个注解内没有定义任何方法(注解的属性),它仅用作一个标记。
}
1
2
3
4
5
6
7
8
9
10
11
# 2)需要写一个实现类实现HandlerMethodArgumentResolver接口

Spring也向我们提供了多种解析器Resolver,HandlerMethodArgumentResolver是用来处理方法参数的解析器,包含以下2个方法:

  • supportsParameter(满足某种要求,返回true,方可进入resolveArgument做参数处理)
  • resolveArgument(解析返回对象注入我们该注解上的属性)
import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.bind.support.WebDataBinderFactory;

/**
 * 参数解析器,用于处理带有 @LoginUserInfo 注解的参数。
 */
public class UserInfoArgumentResolver implements HandlerMethodArgumentResolver {

    /**
     * 检查给定的方法参数是否由这个解析器支持。
     * 
     * @param methodParameter 方法的参数
     * @return 如果该参数类型是 LoginUser 并且带有 @LoginUserInfo 注解,则返回 true
     */
    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        // 检查参数类型是否为 LoginUser 并且该参数是否被 @LoginUserInfo 注解标注
        return methodParameter.getParameterType().isAssignableFrom(LoginUser.class)
                && methodParameter.hasParameterAnnotation(LoginUserInfo.class);
    }

    /**
     * 解析方法参数的实现逻辑。
     *
     * @param methodParameter 包含方法参数信息
     * @param modelAndViewContainer 模型和视图容器
     * @param nativeWebRequest 表示当前请求
     * @param webDataBinderFactory 创建数据绑定器工厂
     * @return 从请求中提取或计算出的参数值
     * @throws Exception 如果解析过程中发生错误
     */
    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
                                  NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        // 从请求中获取名为 Constants.USER_INFO 的属性,该属性应该在之前的某个阶段被设置
        // SCOPE_REQUEST 表示该属性存储在请求范围内
        return nativeWebRequest.getAttribute(Constants.USER_INFO, RequestAttributes.SCOPE_REQUEST);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 3)创建拦截器

这个拦截器会通过请求头header拿到token,然后再通过token去从缓存或数据库中获取用户信息,最后把用户信息写进request里面

@Component
public class AuthenticationInterceptor extends HandlerInterceptorAdapter {

    @Autowired  // 自动注入UserService
    UserService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 打印请求URL
        StringBuffer requestURL = request.getRequestURL();
        System.out.println("前置拦截器1 preHandle: 请求的uri为:" + requestURL.toString());

        // 从请求头中获取token
        String token = request.getHeader("token");

        // 根据token从缓存或数据库中获取用户信息
        LoginUser user = userService.findUserByToken(token);

        // 如果用户不存在,抛出异常
        if (user == null) {
            throw new WebException(ResultStatusCode.INVALID_TOKEN);
        }

        // 将用户信息添加到请求属性中,方便后续操作使用
        request.setAttribute(Constants.USER_INFO, user);
        return true;
    }
}

//配置拦截器
@Configuration  // 标记为配置类
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired  // 注入自定义的拦截器
    AuthenticationInterceptor authenticationInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加拦截器,并指定拦截的路径
        registry.addInterceptor(authenticationInterceptor).addPathPatterns("/user/check/**");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 4)把注解加到接口的参数上

在这里插入图片描述 执行测试我们就可以知道只要token正确,loginUser就已经注入用户信息了

# 4.对象的属性校验注解

完整描述查看 springboot中参数验证自定义注解,@Valid总结 (opens new window)

validation-api包里面还有一个@Constraint注解,我们的自定义注解里面加上这个注解就能实现自定义验证

# 1)创建一个自定义验证注解

我们这个自定义注解实现一个简单的功能: 根据type参数进行校验,校验为空、年龄范围、手机号码

@Constraint(validatedBy = {CheckValidator.class})//指向实现验证的类

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义注解,用于字段和参数的校验。
 */
@Target({ElementType.FIELD, ElementType.PARAMETER})  // 指定注解可以应用于字段和方法参数。
@Retention(RetentionPolicy.RUNTIME)  // 注解在运行时保留,以便运行时的校验框架可以读取。
@Constraint(validatedBy = {CheckValidator.class})  // 指定负责实现校验逻辑的类。
public @interface Check {

    /**
     * 校验类型,枚举定义了不同的校验策略。
     */
    CheckValidatorEnum type() default CheckValidatorEnum.Null;

    /**
     * 最小值,用于数值校验,例如年龄或数量。
     */
    long min() default 1;

    /**
     * 最大值,同样用于数值校验。
     */
    long max() default 1;

    /**
     * 校验失败时的默认错误消息。
     */
    String message() default "参数异常";

    /**
     * 指定哪些校验组这个约束条件属于。
     */
    Class<?>[] groups() default {};

    /**
     * 校验过程中负载可以传递的对象,比如校验的严重程度或分组信息。
     */
    Class<? extends Payload>[] payload() default {};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

校验枚举类 CheckValidatorEnum

/**
 * 枚举类定义不同的校验类型。
 */
public enum CheckValidatorEnum {
    Null(1, "为空校验"),  // 用于检查字段或参数是否为空
    AGE(2, "年龄校验"),  // 用于年龄范围校验
    Phone(3, "手机校验");  // 用于手机号码格式校验

    private int code;  // 代码,用于内部处理
    private String desc;  // 描述,用于错误消息或日志

    CheckValidatorEnum(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public int getCode() {
        return code;
    }

    public String getDesc() {
        return desc;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 2)实现验证的类

我们这个实现自定义验证的类需要实现ConstrainValidator接口

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/**
 * 校验器实现类,用于执行由 @Check 注解定义的校验逻辑。
 */
public class CheckValidator implements ConstraintValidator<Check, Object> {
    
    private Check check;
    private CheckValidatorEnum checkValidatorEnum;

    /**
     * 初始化方法,获取注解实例及其属性。
     * 
     * @param CheckAnnotation 注解实例,提供对注解属性的访问。
     */
    @Override
    public void initialize(Check CheckAnnotation){
        this.check = CheckAnnotation;
        this.checkValidatorEnum = CheckAnnotation.type();  // 获取注解中指定的校验类型
    }

    /**
     * 校验方法,根据注解参数判断对象的有效性。
     * 
     * @param o 被校验的对象。
     * @param constraintValidatorContext 校验时的上下文环境。
     * @return boolean 表示是否校验通过。
     */
    @Override
    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
        
        // 根据不同的校验类型执行校验逻辑
        if (checkValidatorEnum.getCode() == CheckValidatorEnum.Null.getCode()) {
            // 非空校验
            return o != null && !o.toString().isEmpty();
        } else if (checkValidatorEnum.getCode() == CheckValidatorEnum.AGE.getCode()) {
            // 年龄校验
            if (o == null) {
                return true;  // 如果对象为空则默认通过校验(视业务逻辑而定)
            }
            long min = this.check.min();
            long max = this.check.max();
            Integer age = Integer.parseInt(o.toString());
            return age >= min && age <= max;  // 确认年龄是否在指定范围内
        } else if (checkValidatorEnum.getCode() == CheckValidatorEnum.Phone.getCode()) {
            // 手机号码校验
            if (o == null) {
                return true;  // 如果对象为空则默认通过校验(视业务逻辑而定)
            }
            return CommonUtil.isMobile(o.toString());  // 调用工具类方法检查手机号格式
        }
        
        return false;  // 如果不符合任何校验规则,返回false
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 3)使用@Valid注解加在类上校验

如下 @Valid User user,同时在User里面的属性加入自定义注解

控制器方法

/**
 * 添加用户的控制器方法。
 * 使用@Valid注解来触发Spring的自动校验机制,检查传入的User对象是否符合设定的校验规则。
 *
 * @param user 从请求体中自动绑定的User对象
 * @return 返回响应对象
 * @throws IOException 可能抛出的异常
 */
public ResponseObject addUser(@Valid @RequestBody User user) throws IOException {
    // 添加用户的逻辑
    // 如果User对象校验失败,将自动抛出异常,这通常被全局异常处理器捕获并处理
    return new ResponseObject("OK", "用户添加成功", user);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

User 实体类

import javax.validation.constraints.NotEmpty;

/**
 * 用户实体类,包含用户的各种信息。
 */
@Data
public class User {
    private int id;

    @Check(type = CheckValidatorEnum.Null)  // 确保名字字段不为空
    private String name;

    @Check(type = CheckValidatorEnum.AGE, min = 18, max = 30, message = "年龄不在18-30范围内")  // 确保年龄在18到30之间
    private Integer age;

    @Check(type = CheckValidatorEnum.Phone, message = "手机号码不正确")  // 确保电话号码符合预定格式
    private String tel;
    
    @Valid  // 确保cityInfo子对象也进行校验
    private UserCityInfo cityInfo;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

UserCityInfo 嵌套对象的校验

/**
 * 用户城市信息实体类,为User的嵌套属性。
 */
@Data
public class UserCityInfo {

    @Check(type = CheckValidatorEnum.Null, message = "城市编码不能为空")  // 确保城市编码不为空
    private Integer cityCode;

    @NotEmpty(message = "城市名称不能为空")  // 使用标准的@NotEmpty注解确保城市名称不为空
    private String cityName;
}
1
2
3
4
5
6
7
8
9
10
11
12

备注: 可能需要校验的对象的属性还是对象,然后我们需要校验对象里面的对象,这种嵌套校验,我们只需要在对象的属性对象上再加一个@Valid即可实现嵌套校验

# 4)在全局异常里捕获校验异常
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.method.annotation.MethodArgumentNotValidException;
import lombok.extern.slf4j.Slf4j;
import java.util.List;

/**
 * 全局异常处理器,用于捕获和处理在控制器层抛出的异常。
 */
@Slf4j  // Lombok 注解,为这个类提供一个名为 'log' 的日志对象
@RestControllerAdvice  // 标记为控制器的全局配置类,可以用来配置异常处理器、绑定数据处理器等
public class GlobalExceptionHandle {

    /**
     * 捕获并处理因 @Valid 注解导致的校验失败异常。
     *
     * @param e 抛出的异常对象
     * @return 返回自定义的响应对象,包含错误码和错误信息
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)  // 指定这个方法用于处理 MethodArgumentNotValidException 异常
    public ResponseObject handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error(e.getMessage(), e);  // 记录异常信息
        BindingResult ex = e.getBindingResult();  // 获取异常中的绑定结果
        List<ObjectError> allErrors = ex.getAllErrors();  // 获取所有错误对象
        ObjectError error = allErrors.get(0);  // 通常情况下我们只获取第一个错误
        String defaultMessage = error.getDefaultMessage();  // 获取默认的错误消息

        // 创建并返回错误响应对象
        ResponseObject responseObject = new ResponseObject(ResultStatusCode.VALIDATE_FAIL.getStatuscode(), defaultMessage, null);
        return responseObject;
    }

    /**
     * 捕获并处理所有的异常。
     *
     * @param e 抛出的异常对象
     * @return 返回包含系统错误码和错误信息的响应对象
     */
    @ExceptionHandler(Exception.class)  // 指定这个方法用于处理所有的 Exception 类型的异常
    public ResponseObject handleException(Exception e) {
        log.error(e.getMessage(), e);  // 记录异常信息

        // 创建并返回系统错误响应对象
        ResponseObject responseObject = new ResponseObject(ResultStatusCode.SYSTEM_ERROR.getStatuscode(), ResultStatusCode.SYSTEM_ERROR.getStatusmsg(), null);
        return responseObject;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

然后我们就实现了在注解里校验,不需要在接口代码里再判断在这里插入图片描述

# 5.接口的操作日志注解

记录接口的操作日志注解

# 1)创建自定义注解
/**
 * 自定义注解,用于标记方法并记录操作日志。
 * 这个注解可以应用于任何方法上,以记录该方法执行时的关键操作信息。
 */
@Target(ElementType.METHOD)  // 指定注解用途为方法,表明只能应用于方法上
@Retention(RetentionPolicy.RUNTIME)  // 指定注解在运行时保持有效,允许通过反射读取
@Inherited  // 允许注解被子类继承
@Documented  // 指定注解信息将出现在JavaDoc中
public @interface OperationLog {

    /**
     * 描述操作的详细信息,默认为空。
     * 可以通过此属性提供具体操作的描述,如"创建用户"、"修改密码"等。
     *
     * @return 操作描述字符串
     */
    String value() default "";

    /**
     * 操作类型枚举,默认为 OTHER。
     * 可以指定操作的类型,如添加、删除、更新等,帮助更精确地分类操作日志。
     *
     * @return 操作类型枚举值
     */
    OperationTypeEnum operationTypeEnum() default OperationTypeEnum.OTHER;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 2)通过AOP切面实现注解功能
/**
 * 切面类,用于记录操作日志。
 */
@Aspect  // 表示这是一个切面类
@Component  // 表示这是一个Spring管理的bean
public class LogAspect {

    @Autowired  // 自动注入系统日志服务
    private ISysLogInfoService sysLogInfoService;

    /**
     * 定义切点。所有带有@OperationLog注解的方法将触发这个切点。
     */
    @Pointcut("@annotation(OperationLog)")
    public void logPointCut() {
    }

    /**
     * 在方法执行完毕后执行的通知,用于记录日志。
     *
     * @param joinPoint 连接点提供了对当前环境的深入了解,包括方法签名和方法参数等。
     */
    @AfterReturning("logPointCut()")
    public void saveSysLog(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        OperationLog operation = method.getAnnotation(OperationLog.class);
        if (operation != null) {
            String value = operation.value();
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = method.getName();
            String methodStr = className + "." + methodName;

            // 构建参数JSON对象
            JSONObject allParams = new JSONObject();
            for (Object arg : joinPoint.getArgs()) {
                if (arg instanceof JSONObject) {
                    allParams.putAll((JSONObject) arg);
                }
            }

            // 获取用户IP地址
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String url = request.getRequestURI();
            String ip = IpAddressUtils.getIpAdrress(request);

            String params = allParams.isEmpty() ? "" : allParams.toJSONString();
            if (params.length() > 1000) {
                params = params.substring(0, 997).concat("...");
            }

            SysLogInfo sysLog = new SysLogInfo();
            sysLog.setLogDesc(value);
            sysLog.setOpIp(ip);
            sysLog.setBizParam(params);
            sysLog.setOpMethod(methodStr);
            
            // 获取当前用户信息
            AuthUser user = AuthUserUtils.getCurrentUser();
            sysLog.setBizType(operation.operationTypeEnum().getCode());
            sysLog.setOpUser(user.getName());
            sysLog.setOpUserNo(user.getEmpNo());
            sysLog.setOpTime(new Date());

            sysLogInfoService.insertSelective(sysLog);  // 保存日志
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# 3)把注解加在接口方法上

把注解加在接口的方法上就可以保存进入这个接口请求IP、类型、方法名、入参、用户信息

# 6.缓存注解

spring里面有一个@Cacheable注解,使用这个注解的方法,如果key在缓存中已有,则不再进入方法,直接从缓存获取数据,缓存没有值则进入并且把值放到缓存里面,我们写一个类似的,简单的注解

备注:使用了这个注解,如果数据有修改,记得清除缓存

# 1)创建自定义注解
/**
 * 自定义注解,用于配置方法级别的缓存。
 * 通过指定缓存的前缀和键来定义缓存的存储和查询策略。
 */
@Target(ElementType.METHOD)  // 表明该注解仅适用于方法
@Retention(RetentionPolicy.RUNTIME)  // 指定注解在运行时保持有效,允许通过反射进行访问
public @interface CustomCache {
    /**
     * 缓存前缀,用于区分不同的缓存数据。
     * 前缀可以帮助在缓存存储中组织数据,防止键的冲突。
     *
     * @return 返回定义的缓存前缀,默认为空字符串
     */
    String prefix() default "";

    /**
     * 缓存键,用于标识缓存中的具体数据。
     * 键是用于查询和存储缓存数据的唯一标识。
     *
     * @return 返回定义的缓存键,默认为空字符串
     */
    String key() default "";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 2)然后再创建一个AOP切面类来实现这个注解
/**
 * 切面类,用于实现基于注解的缓存机制。
 */
@Aspect  // 标记这个类为一个切面
@Component  // 将这个类加入到Spring容器中,管理其生命周期
public class CustomCacheAspect {

    // 静态HashMap用作简单的缓存实现
    private static HashMap<String, Object> cacheMap = new HashMap<>();

    /**
     * 定义切点,拦截所有标有 @CustomCache 注解的方法。
     */
    @Pointcut("@annotation(CustomCache)")
    public void cache() {
    }

    /**
     * 环绕通知处理缓存逻辑,如果缓存中存在数据,则返回缓存数据;否则执行目标方法并缓存其结果。
     *
     * @param joinPoint 提供了方法执行的上下文。
     * @return 方法执行的结果,可能来自缓存或新执行的结果。
     */
    @Around("cache()")
    public Object manageCache(ProceedingJoinPoint joinPoint) {

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();  // 获取被拦截的方法
        CustomCache customCache = method.getAnnotation(CustomCache.class);  // 获取方法上的CustomCache注解

        // 处理缓存键的前缀
        String prefix = customCache.prefix();
        if (prefix == null || prefix.isEmpty()) {
            String className = joinPoint.getTarget().getClass().getName();  // 获取类名
            String methodName = method.getName();  // 获取方法名
            prefix = className + "-" + methodName;  // 默认前缀为类名和方法名的组合
        }

        // 处理缓存键
        String key = customCache.key();
        if (key == null || key.isEmpty()) {
            Object[] args = joinPoint.getArgs();  // 获取方法参数
            String[] parameterNames = signature.getParameterNames();  // 获取参数名称
            for (int i = 0; i < parameterNames.length; i++) {
                if ("id".equals(parameterNames[i])) {  // 默认使用名为id的参数作为key
                    key = args[i].toString();
                    break;
                }
            }
        }

        String cacheKey = prefix + key;  // 组合前缀和键形成完整的缓存键
        Object result = cacheMap.get(cacheKey);  // 尝试从缓存中获取数据
        if (result != null) {
            return result;  // 如果缓存中有数据,直接返回缓存数据
        }

        try {
            result = joinPoint.proceed();  // 没有缓存时执行原方法
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }

        cacheMap.put(cacheKey, result);  // 将新获取的结果存入缓存
        return result;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# 3)把注解加在查询用户信息的Service上
/**
 * 通过注解 @CustomCache 增强的方法,用于查找用户。
 * 这个注解的使用意味着该方法的结果可能会被缓存,以加快后续同样参数的调用速度。
 */
@Service
public class UserService {

    @Autowired
    private UserMapper baseMapper;  // 自动注入数据访问层的映射器

    /**
     * 查找用户的方法,应用了 @CustomCache 注解来启用缓存。
     * 默认情况下,如果没有特别指定 prefix 和 key,缓存键将由框架根据方法签名和参数自动生成。
     *
     * @param id 用户的ID
     * @return 返回从数据库查询到的用户对象
     */
    @Override
    @CustomCache()  // 使用自定义的缓存注解,此处未指定 prefix 和 key
    public User findUser(Integer id) {
        return baseMapper.selectById(id);  // 从数据库查询用户
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

测试可以看到只有首次才会进入这个方法查询用户信息,查询出用户信息后再调用这个方法只是从缓存中获取

# 7.防刷新注解

有一些场景,例如申请提交之后几秒内需要防止用户重复提交,我们后端通过注解实现这一功能

# 1)创建自定义注解
/**
 * 自定义注解,用于防止在指定时间内重复提交。
 * 可以应用于任何方法,以确保在特定的时间间隔内不会重复执行。
 */
@Target(ElementType.METHOD)  // 表明此注解只能应用于方法
@Retention(RetentionPolicy.RUNTIME)  // 表明此注解在运行时可用,因此可以通过反射读取
public @interface AvoidRepeatSubmit {

    /**
     * 定义在多少秒内方法不可重复执行。
     * 这个时间限制用于防止例如表单的重复提交,或者重复的方法调用。
     * 
     * @return 返回定义的超时时间(秒)
     */
    long timeout() default 3;  // 默认值为3秒
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2)再创建一个AOP切面类来实现这个注解
/**
 * 切面类,用于实现防止重复提交功能。
 * 利用Redis作为存储来记录方法调用的信息,并检查是否在限制的时间内重复调用。
 */
@Aspect  // 标记为切面
@Component  // 注册为Spring管理的Bean
@Slf4j  // Lombok注解,自动注入日志对象
public class AvoidRepeatSubmitAspect {
    @Autowired  // 自动注入Redis操作类
    private RedisRepository redisRepository;

    /**
     * 切入点之前执行的操作,用于拦截带有@AvoidRepeatSubmit注解的方法。
     * @param joinPoint 连接点,提供方法执行时的上下文信息
     * @throws WebException 当方法在指定时间内重复调用时抛出此异常
     */
    @Before("@annotation(cn.com.bluemoon.admin.web.common.aspect.AvoidRepeatSubmit)")
    public void repeatSubmitIntercept(JoinPoint joinPoint) {
        // 获取请求对象
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String ip = IpAddressUtils.getIpAdrress(request);  // 获取请求者的IP地址

        String className = joinPoint.getTarget().getClass().getName();  // 获取被调用的类名

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();  // 获取被调用的方法
        String methodName = method.getName();  // 获取方法名

        // 获取注解中配置的超时时间
        AvoidRepeatSubmit annotation = method.getAnnotation(AvoidRepeatSubmit.class);
        long timeout = annotation.timeout();  // 超时时间,单位秒

        // 构造Redis的key,以区分不同的方法调用
        StringBuilder builder = new StringBuilder();
        builder.append(ip).append(",").append(className).append(",").append(methodName).append(",").append(timeout).append("s");
        String key = builder.toString();
        log.info(" --- >> 防重提交:key -- {}", key);

        // 判断Redis中是否存在该key,若存在则表示在超时时间内重复提交
        String value = redisRepository.get(key);
        if (StringUtils.isNotBlank(value)) {
            String message = MessageFormat.format("请勿在{0}s内重复提交", timeout);
            throw new WebException(message);
        }

        // 将key存入Redis并设置过期时间
        redisRepository.setExpire(key, key, timeout);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# 8.动态切换数据源注解

原理:Spring提供了AbstractRoutingDataSource用于动态路由数据源,继承AbstractRoutingDataSource类并覆写其protected abstract Object determineCurrentLookupKey()即可

完整描述查看:springboot动态多数据源配置和使用(二) (opens new window)

# 1)创建自定义注解
/**
 * 自定义注解,用于指定使用的数据源。
 * 可以应用于类或方法级别,根据注解的值切换不同的数据源。
 * 主要用于多数据源环境,通过指定数据源名称来控制数据访问的路由。
 */
@Target({ElementType.METHOD, ElementType.TYPE})  // 注解可以用于方法和类上
@Retention(RetentionPolicy.RUNTIME)  // 注解在运行时有效,可通过反射访问
@Documented  // 注解将被包括在Javadoc中
@Inherited  // 注解可以被子类继承
public @interface DataSource {
    /**
     * 指定数据源的名称。
     * 默认值为空字符串,通常在实际使用时需要明确指定数据源的标识符。
     *
     * @return 数据源的名称
     */
    String value() default "";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 2)再创建一个AOP切面类来实现这个注解

下面的代码很简单,可以看出,这个切面类先判断注解是在方法上还是类上,如果方法上有注解优先使用方法上的,获取注解的value属性的值,把这个值作为数据源的key。通过 DynamicContextHolder.push(value)来设置数据源的key(这里改变后, determineCurrentLookupKey()重写的方法返回的key也就改变了,从而切换了数据源)

/**
 * AOP切面处理类,用于实现多数据源的动态切换。
 * 切面拦截所有被 @DataSource 注解的类和方法,以便动态切换到指定的数据源。
 */
@Aspect  // 标记这个类为切面
@Component  // 将切面类定义为Spring管理的Bean
@Order(Ordered.HIGHEST_PRECEDENCE)  // 设置切面的优先级,最高优先级,保证数据源切换在事务之前执行
public class DataSourceAspect {

    protected Logger logger = LoggerFactory.getLogger(getClass());  // 日志记录器

    /**
     * 定义切点,匹配所有被 @DataSource 注解标记的类或方法。
     */
    @Pointcut("@annotation(io.renren.datasource.annotation.DataSource) " +
              "|| @within(io.renren.datasource.annotation.DataSource)")
    public void dataSourcePointCut() {
    }

    /**
     * 环绕通知,用于在方法执行前后进行数据源的切换和恢复。
     *
     * @param point 连接点,提供了方法执行的上下文。
     * @return 方法执行的结果。
     * @throws Throwable 如果被调用的方法抛出异常。
     */
    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Class<?> targetClass = point.getTarget().getClass();
        Method method = signature.getMethod();

        // 尝试获取类和方法上的@DataSource注解
        DataSource targetDataSource = targetClass.getAnnotation(DataSource.class);
        DataSource methodDataSource = method.getAnnotation(DataSource.class);

        // 判断使用类上的注解还是方法上的注解
        if (targetDataSource != null || methodDataSource != null) {
            String value;
            if (methodDataSource != null) {
                value = methodDataSource.value();  // 优先使用方法上的数据源
            } else {
                value = targetDataSource.value();  // 没有方法注解则使用类注解
            }

            DynamicContextHolder.push(value);  // 切换数据源
            logger.debug("set datasource is {}", value);
        }

        try {
            return point.proceed();  // 执行原方法
        } finally {
            DynamicContextHolder.poll();  // 方法执行完毕后恢复之前的数据源
            logger.debug("clean datasource");
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# 3)把这个注解加在需要使用多数据源的service方法或类上

从下面的三个方法逐个调用测试,可以看到操作了三个不同的数据源的数据

/**
 * 服务层类,演示使用不同的数据源进行数据库操作。
 */
@Service  // 标记为Spring管理的服务类
//@DataSource("slave1")  // 可以在类级别指定数据源,影响所有方法,这里被注释,不起作用
public class DynamicDataSourceTestService {

    @Autowired  // 自动注入数据访问对象
    private SysUserDao sysUserDao;

    /**
     * 更新用户信息,使用默认数据源。
     * @param id 用户ID
     */
    @Transactional  // 声明事务,确保数据库操作的完整性
    public void updateUser(Long id) {
        SysUserEntity user = new SysUserEntity();
        user.setUserId(id);
        user.setMobile("13500000000");
        sysUserDao.updateById(user);  // 执行更新操作
    }

    /**
     * 更新用户信息,显式使用 "slave1" 数据源。
     * @param id 用户ID
     */
    @Transactional
    @DataSource("slave1")  // 指定使用 "slave1" 数据源
    public void updateUserBySlave1(Long id) {
        SysUserEntity user = new SysUserEntity();
        user.setUserId(id);
        user.setMobile("13500000001");
        sysUserDao.updateById(user);  // 执行更新操作
    }

    /**
     * 更新用户信息,显式使用 "slave2" 数据源。
     * @param id 用户ID
     */
    @DataSource("slave2")  // 指定使用 "slave2" 数据源
    @Transactional
    public void updateUserBySlave2(Long id) {
        SysUserEntity user = new SysUserEntity();
        user.setUserId(id);
        user.setMobile("13500000002");
        sysUserDao.updateById(user);  // 执行更新操作
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
编辑此页 (opens new window)
上次更新: 2024/12/28, 18:32:08
Spring6 - AOP
Spring6 - Junit

← Spring6 - AOP Spring6 - Junit→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式