Spring6 - 自定义注解
# 一、前言
初学spring的时候使用注解总觉得使用注解很神奇,加一个注解就能实现想要的功能,很好奇,也想自己根据需要写一些自己实现的自定义注解。问题来了,自定义注解到底是什么?肯定会有人和我一样有这个疑惑,我根据自己的理解总结一下。看完下面的几个使用自定义注解的实战demo,小伙伴大概就懂怎么用了。
其实注解一点也不神奇,注解就是一种标志,单独使用注解,就相当于在类、方法、参数和包上加上一个装饰,什么功能也没有,仅仅是一个标志,然后这个标志可以加上一些自己定义的参数。就像下面这样,创建一个@interface的注解,然后就可以使用这个注解了,加在我们需要装饰的方法上,但是什么功能也没有
@Target(ElementType.METHOD) // 注解应用于方法
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时可用,使得可以通过反射读取
@Inherited // 允许子类继承父类的注解
@Documented // 注解将被包含在JavaDoc中
public @interface XinLinLog {
String value() default ""; // 注解的参数,默认值为空字符串
}
2
3
4
5
6
7
注解中可以定义多种类型的参数,这些参数定义了注解在使用时可以接受的配置信息:
- 基本数据类型(int、long、float等)
- String
- Class类型
- 枚举类型
- 注解类型
- 以上类型的数组
参数在注解内部通过无参方法的方式声明。例如:
// 这看起来像是一个方法,但实际上是一个注解的参数定义。
String value() default ""; // 默认值为空字符串
2
这里value()
方法定义了一个名为value
的参数,它的类型为String
,并指定了一个默认值为空字符串。虽然语法上看起来像是方法,但在使用时这些元素实际上代表了注解的配置属性。
参数使用示例
定义好注解及其参数后,注解可以被附加到代码的相应部分,并在使用时通过参数传递特定的值或使用定义的默认值。例如,在一个方法上使用XinLinLog
注解:
@XinLinLog(value = "This is a log message.")
public void someMethod() {
// 方法体
}
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'属性设置的日志信息
}
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 {}; // 默认为空数组,用户可以在使用注解时指定具体需要校验的参数。
}
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, "登录超时,请重新登录");
}
}
}
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 {}; // 注解的参数,用于存储角色名称的数组。
}
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("没有权限访问当前接口");
}
}
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("/**");
}
}
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 {
// 目前这个注解内没有定义任何方法(注解的属性),它仅用作一个标记。
}
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);
}
}
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/**");
}
}
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 {};
}
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;
}
}
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
}
}
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);
}
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;
}
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;
}
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;
}
}
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;
}
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); // 保存日志
}
}
}
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 "";
}
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;
}
}
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); // 从数据库查询用户
}
}
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秒
}
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);
}
}
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 "";
}
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");
}
}
}
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); // 执行更新操作
}
}
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