元注解
要自定义开发注解首先要了解的便是元注解,元注解可以简单的理解为修饰注解的注解,主要有以下四个:
@Documented
@Documented
是一个标记注解,没有成员。被此注解修饰的注解会被 javadoc
工具记录。默认情况下使用 javadoc
命令生成文档是不包括注解相关信息的。
@Target
@Target
注解用来说明那些被它所修饰的注解可以使用的范围,它的取值范围定义在 ElementType
枚举中,具体的可以参考枚举类中的注解:
public enum ElementType {
TYPE, // 类、接口、枚举类
FIELD, // 成员变量(包括:枚举常量)
METHOD, // 成员方法
PARAMETER, // 方法参数
CONSTRUCTOR, // 构造方法
LOCAL_VARIABLE, // 局部变量
ANNOTATION_TYPE, // 注解类 - 有点类似于元注解的作用范围
PACKAGE, // 可用于修饰:包
TYPE_PARAMETER, // 类型参数,JDK 1.8 新增
TYPE_USE // 使用类型的任何地方,JDK 1.8 新增
}
复制代码
@Retention 注解
@Reteniton
注解用来限定那些被它所修饰注解类的生命周期,一共有三种策略,定义在 RetentionPolicy
枚举中。
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
*/
RUNTIME
// 这3个生命周期分别对应于:Java源文件(.java文件) ---> .class文件 ---> 内存中的字节码。
}
复制代码
@Inherited
注解
@Inherited
注解的作用为:使被它修饰的注解具有继承性,如果某个类使用了被 @Inherited
修饰的注解,则其子类将继承该注解。
自定义 @Valid 相关的注解
参考 @Valid 注解
spring-boot-starter 2.3.0
以下是会包含这个依赖的,在新的版本就需要单独引入了。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
复制代码
代码示例
// ==== 创建一个User类 =====
@Data
public class User {
private String username;
@NotBlank(message = "密码不能为空")
private String password;
private Integer id;
@Past(message = "出生日期不能在未来")
private Date birthday;
}
//====== 验证接收到user对象的属性 =======
@RestController
@Slf4j
public class UserController {
@PostMapping("/user")
public Boolean postUser(@Valid @RequestBody User user, BindingResult bindingResult){
// 验证不通过 打印错误信息
if(bindingResult.hasErrors()){
//打印错误
log.error("error={}",bindingResult.getFieldError().getDefaultMessage());
}
return bindingResult.hasErrors();
}
}
// ==== 传入的参数 =======
{
"birthday": "2025-09-13T01:24:22.148Z",//大于当前日期
"id": 0,
"password": "",//密码为空
"username": "string"
}
// ===== 打印的日志 ======
2020-09-10 09:24:49.517 ERROR 21968 --- [nio-7777-exec-9] c.a.controller.CreateDataController: error=出生日期不能在未来
复制代码
从示例代码中可以看出 @Valid
注解可以与 BindingResult
对象联合使用来校验传入对象的参数。与它功能类似的注解还有 @Validated
,它们两个的主要区别为前者不支持分组验证,后者支持分组验证。如有疑问可以参考:https://www.cnblogs.com/xiaoqi/p/spring-valid.html
。
自定义 @Valid 相关的注解
@Constraint
限定自定义注解,指定自定义注解的处理逻辑。
自定义 PhoneNumber 注解
package com.example.annotation;
import org.springframework.util.StringUtils;
import javax.validation.*;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.regex.Pattern;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 自定义 validate 相关注解
* @author WT
* @date 2022/01/02 13:26:42
*/
//可以被 javadoc 记录
@Documented
// 指定自定义注解处理逻辑的类
@Constraint(validatedBy = { PhoneNumber.PhoneNumberValidator.class })
//生命周期到 runtime
@Retention(RUNTIME)
//可以用于什么地方
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
public @interface PhoneNumber {
//当处理逻辑指定的类的 isValid 方法返回 false 时, 返回的失败信息
String message() default "手机号校验失败";
// 约束分组的
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
// 校验号码的逻辑类 返回false的话就会抛出异常 第一个参数是注解的名称,第二个参数是注解校验的参数
class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, CharSequence> {
/**
* 移动电话
*/
private final static String PHONE_NUMBER_REGEX = "1[3456789]\d{9}";
@Override
public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
if (!StringUtils.hasText(value)) {
return false;
}
return Pattern.compile(PHONE_NUMBER_REGEX).matcher(value).matches();
}
}
}
复制代码
使用自定义注解
// === 实体类 ===
package com.example.pojo;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.example.annotation.PhoneNumber;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
/**
* 测试分库分表 以及 @Validate
* @author WT
* @date 2022/01/02 12:53:41
*/
@Data
@TableName("b_coin_account_alarm_data_month_yyyymm")
public class TestTable {
@TableId
@NotNull(message = "id 不能为空")
private Long id;
private String accountId;
private String strategyId;
@PhoneNumber
private String month;
private String mobile;
private String tag;
private String measurement;
private BigDecimal data;
}
// === controller 中使用
@PostMapping ("/test/valid")
public void testValid (@Valid @RequestBody TestTable testTable, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
//打印错误
log.error("== error: [{}]",bindingResult.getFieldError().getDefaultMessage());
}
}
复制代码
自定义注解
上一小节介绍了自定义 Validate
相关的注解,但是使用场景局限性过大,主要是利用上面那个例子来简单介绍一下自定义注解。下面我们来自定义一个常规注解并且介绍一般情况下自定义注解的使用场景。
场景描述
一个开发完成的系统,想要添加鉴权信息,当别的服务调用本服务时,需要进行验签操作。
需要满足以下几个要求:
- 不改变原有代码的任何逻辑
- 可以指定那些客户端可以访问
- 对有权限的访问的客户端要进一步验签
解决方案
采用 AOP
结合自定义注解的方式来完成这个需求。
Maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
复制代码
yaml 配置
auth:
# 客户端 1 配置
client1:
# 客户端的秘钥 - 为 client1-secret 加盐进行 md5 - 示例: Md5Crypt.apr1Crypt("client1-secret", "salt1")
appSecret: $apr1$salt1$EMKCT8e1YK6HkS0lR2Onv/
# md5 加密使用的盐
salt: salt1
# 客户端 2 配置
client2:
# 客户端的秘钥 - 为 client2-secret 加盐进行 md5
appSecret: $apr1$salt2$JnMmVb96AOcSz2kObzLc90
# md5 加密使用的盐
salt: salt2
复制代码
配置类
package com.example.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 获取配置文件中 auth 的相关信息
*
* @author WT
* @date 2022/01/02 15:10:59
*/
@Data
@Configuration
@ConfigurationProperties("auth")
public class AuthConfig {
private ClientConfig client1;
private ClientConfig client2;
@Data
public static class ClientConfig {
private String appSecret;
private String salt;
}
}
复制代码
自定义注解
package com.example.annotation;
import java.lang.annotation.*;
/**
* 自定义注解,当被此注解标识,需要认证才能访问对应的接口
* 认证规则:在 注解中指定 appKeyArray 表明哪些客户端可以访问此接口,
* 在 header 头中传递 appKey 和 secret,使用 aop 校验
* 请求头中的 secret 是否正确,正确的话允许访问
*
* @author WT
* @date 2022/01/02 13:45:29
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface EnableAuth {
// 指定那些客户端拥有访问被修饰接口的权限
String[] appKeyArray() default "";
}
复制代码
AOP 处理逻辑类
package com.example.aop;
import com.example.annotation.EnableAuth;
import com.example.aop.exception.AuthException;
import com.example.config.AuthConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.Md5Crypt;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
/**
* 指定切面类
*
* @author WT
* @date 2022/01/02 14:34:25
*/
@Slf4j
@Aspect
@Component
public class AuthAspect {
private final AuthConfig authConfig;
public AuthAspect (AuthConfig authConfig) {
this.authConfig = authConfig;
}
/**
* 指定切入点,定义 Advice 要发生的地方为被 @EnableAuth 修饰的方法
*/
@Pointcut("@annotation(com.example.annotation.EnableAuth)")
public void auth() {
}
/**
* 指定前置 advice 增强
* @param enableAuth 标记需要增强方法的注解
*/
@Before("@annotation(enableAuth)")
public void beforeAdvice(EnableAuth enableAuth) {
log.info("进入 beforeAdvice 方法,开始进行 auth 操作");
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
throw new AuthException("非 httpServletRequest 请求");
}
HttpServletRequest request = requestAttributes.getRequest();
String[] appKeyArray = enableAuth.appKeyArray();
if (appKeyArray == null || appKeyArray.length == 0) {
throw new AuthException("appKeyArray 不能为空");
}
String headerAppKey = request.getHeader("appKey");
String headerSecret = request.getHeader("secret");
if (!StringUtils.hasText(headerAppKey) || !StringUtils.hasText(headerSecret)) {
throw new AuthException("header 头中 headerAppKey 或者 headerSecret 为空");
}
boolean flag;
// 校验请求此接口的客户端是否具有访问权限
if (!Arrays.asList(appKeyArray).contains(headerAppKey)) {
log.error("appKey为: {} 的 客户端没有访问此接口的权限", headerAppKey);
throw new AuthException("appKey为: " + headerAppKey + " 的客户端没有访问此接口的权限");
}
// 校验 appSecret
switch (headerAppKey) {
case "client1": {
AuthConfig.ClientConfig client1 = authConfig.getClient1();
flag = validateSecret(headerSecret, authConfig.getClient1().getSalt(), authConfig.getClient1().getAppSecret());
break;
}
case "client2": {
flag = validateSecret(headerSecret, authConfig.getClient1().getSalt(), authConfig.getClient2().getAppSecret());
break;
}
default: flag = false;
}
if (!flag) {
log.error("校验 {} 的 headerSecret 失败", headerAppKey);
throw new AuthException("校验 " + headerAppKey + " 的 headerSecret 失败");
}
log.info("== 校验 {} 的 headerSecret 成功", headerAppKey);
}
private static boolean validateSecret (String secret, String salt, String appSecret) {
return appSecret.equals(Md5Crypt.apr1Crypt(secret, salt));
}
}
复制代码
使用自定义注解实现权限控制
@EnableAuth(appKeyArray = {"client1", "client2"})
@GetMapping("/test/auth")
public void testAuth () {
log.info("== 成功调用到 testAuth 方法");
}
复制代码