项目中的自定义注解
1、实际开发场景
在项目的实际开发中,我们肯定会需要使用自定义注解来做到某些我们想要达到的效果
2、代码开发
2.1、自定义注解的使用
2.1.1、controller层
//swagger中的注解,用于构建Api文档
@ApiOperation(value = "新增用户")
//自定义注解-日志
@WebLog(info = "新增用户")
//自定义注解-加解密
@SecurityParameter
@PostMapping("/addUser")
public ResponseEntity<String> addUser(@RequestBody @Validated({User.AddAction.class}) User user) {
.
.
.
.
.
.
return "新增成功";
}
2.1.2、对象
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
//开启链式编程
@Accessors(chain = true)
//现实实体类型和数据库中的表实现映射
@TableName(value = "user")
public class User extends BasicEntity implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
@NotNull(groups = {UpdateAction.class}, message = "userId不能为空")
private Long id;
@ApiModelProperty(value = "分组名称")
@NotBlank(groups = {AddAction.class, UpdateAction.class}, message = "分组名称不能为空")
private String roleName;
@ApiModelProperty(value = "性别")
@NotNull(groups = {AddAction.class, UpdateAction.class}, message = "性别不能为空")
@EnumCheck(groups = {AddAction.class, UpdateAction.class}, clazz = Sex.class, message = "性别类型不合法")
private Sex sex;
@TableField(fill = FieldFill.INSERT)
@TableLogic(value = "0", delval = "1")
private Byte isDel;
public interface AddAction {
}
public interface UpdateAction {
}
}
2.2、自定义注解 - 校验字段的枚举值类型是否合法
2.2.1、注解
//将注解包含在javadoc中
@Documented
/**
* Constraint 详细信息
* @Null 被注释的元素必须为 null
* @NotNull 被注释的元素必须不为 null
* @AssertTrue 被注释的元素必须为 true
* @AssertFalse 被注释的元素必须为 false
* @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
* @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
* @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
* @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
* @Size(max, min) 被注释的元素的大小必须在指定的范围内
* @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
* @Past 被注释的元素必须是一个过去的日期
* @Future 被注释的元素必须是一个将来的日期
* @Pattern(value) 被注释的元素必须符合指定的正则表达式
*/
//标明由哪个类执行校验逻辑
@Constraint(validatedBy = EnumConstraintValidator.class)
/**
* 接口、类、枚举、注解
* @Target(ElementType.TYPE)
* 字段、枚举的常量
* @Target(ElementType.FIELD)
* 方法
* @Target(ElementType.METHOD)
* 方法参数
* @Target(ElementType.PARAMETER)
* 构造函数
* @Target(ElementType.CONSTRUCTOR)
* 局部变量
* @Target(ElementType.LOCAL_VARIABLE)
* 注解
* @Target(ElementType.ANNOTATION_TYPE)
* 包
* @Target(ElementType.PACKAGE)
*/
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
/**
* 注解仅存在于源码中,在class字节码文件中不包含
* @Retention(RetentionPolicy.SOURCE)
* 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得
* @Retention(RetentionPolicy.CLASS)
* 注解会在class字节码文件中存在,在运行时可以通过反射获取到
* @Retention(RetentionPolicy.RUNTIME)
*/
@Retention(RetentionPolicy.RUNTIME)
//可以在注解的地方重复标注注解
@Repeatable(EnumCheck.List.class)
public @interface EnumCheck {
String message() default "{javax.validation.constraints.EnumCheck.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* 枚举类
*/
Class<? extends EnumValidator> clazz();
/**
* 调用的方法名称
*/
String method() default "getValue";
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface List {
EnumCheck[] value();
}
}
2.2.2、校验类
//需要继承ConstraintValidator<注解名,校验的Object>
public class EnumConstraintValidator implements ConstraintValidator<EnumCheck, Object> {
/**
* 注解对象
*/
private EnumCheck annotation;
/**
* 初始化方法
*
* @param constraintAnnotation 注解对象
*/
//方法initialize()不需要一定有,使用可用来对注解定义的参数进行初始化给isValid()方法进行使用
@Override
public void initialize(EnumCheck constraintAnnotation) {
this.annotation = constraintAnnotation;
}
//方法isValid()必须实现,是校验逻辑所在的位置
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (Objects.isNull(value)) {
return true;
}
Object[] enumConstants = annotation.clazz().getEnumConstants();
try {
Method method = annotation.clazz().getMethod(annotation.method());
for (Object enumConstant : enumConstants) {
if (method.invoke(value).equals(method.invoke(enumConstant))) {
return true;
}
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
return false;
}
return false;
}
}
2.2.3、枚举接口
public interface EnumValidator {
Object getValue();
}
2.2.4、枚举对象
public enum Sex implements EnumValidator {
MAN(0, "男"),
WOMAN(1, "女");
@EnumValue
@JsonValue
private Integer type;
private String desc;
Sex(Integer type, String desc) {
this.type = type;
this.desc = desc;
}
public Integer getType() {
return type;
}
public String getDesc() {
return desc;
}
public static Sex getByType(int type) {
for (Sex sex : values()) {
if (sex.type == type) {
return sex;
}
}
return null;
}
@Override
public Object getValue() {
return type;
}
}
2.3、自定义注解 - 日志
2.3.1、注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface WebLog {
/**
* 日志方法描述信息
*/
String info() default "";
}
2.3.2、切面
//@Aspect:作用是把当前类标识为一个切面供容器读取
@Aspect
@Component
@Slf4j
//切面加载顺序
@Order(1)
public class RequestAspect {
private static ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
//@Pointcut:Pointcut是植入Advice的触发条件。每个Pointcut的定义包括2部分,一是表达式,二是方法签名。方法签名必须是 public及void型。可以将Pointcut中的方法看作是一个被Advice引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为此表达式命名。因此Pointcut中的方法只需要方法签名,而不需要在方法体内编写实际代码。
@Pointcut("@annotation(xxx.xxx.xxx.WebLog)")
public void webLog() {
}
//@Before:标识一个前置增强方法,相当于BeforeAdvice的功能
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
String ip = request.getRemoteAddr();
String httpMethod = request.getMethod();
String url = request.getRequestURL().toString();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
WebLog webLog = signature.getMethod().getAnnotation(WebLog.class);
String apiInfo = webLog.info();
log.info("~~~Start~~~收到来自 {} 的 {} 请求,URL: {},开始执行后台方法:{},全路径({}.{}),入参为:{}~~~",
ip, httpMethod, url, apiInfo,
joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName(),
JSONObject.toJSONString(getFieldsName(joinPoint)));
}
//@Around:环绕增强,相当于MethodInterceptor
@Around("webLog()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
WebLog webLog = signature.getMethod().getAnnotation(WebLog.class);
String apiInfo = webLog.info();
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
log.info("~~~End~~~方法 {}.{} ({}) 执行结束,耗时:{}秒,返回值为:{}~~~",
proceedingJoinPoint.getSignature().getDeclaringTypeName(),
proceedingJoinPoint.getSignature().getName(),
apiInfo,
(float) (System.currentTimeMillis() - startTime) / 1000,
JSONObject.toJSONString(result));
return result;
}
/**
* 获取参数列表
*/
private static Map<String, Object> getFieldsName(JoinPoint joinPoint) {
// 参数值
Object[] args = joinPoint.getArgs();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String[] parameterNames = pnd.getParameterNames(method);
if (parameterNames != null) {
Map<String, Object> paramMap = new HashMap<>(parameterNames.length);
for (int i = 0; i < parameterNames.length; i++) {
paramMap.put(parameterNames[i], args[i]);
}
return paramMap;
}
return Collections.emptyMap();
}
}
2.4、自定义注解 - 加解密
2.4.1、注解
/**
* 请求数据解密
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Mapping
@Documented
public @interface SecurityParameter {
/**
* 入参是否解密,默认解密
*/
boolean inDecode() default true;
/**
* 出参是否加密,默认加密
*/
boolean outEncode() default true;
}
2.4.2、请求数据解密
/**
* 请求数据解密
*/
//范围 - 在括号内的包下生效
@ControllerAdvice(basePackages = "xxx.xxx.controller")
public class DecodeRequestBodyAdvice implements RequestBodyAdvice {
private static final Logger logger = LoggerFactory.getLogger(DecodeRequestBodyAdvice.class);
@Override
public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return body;
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
try {
boolean encode = false;
if (methodParameter.getMethod().isAnnotationPresent(SecurityParameter.class)) {
//获取注解配置的包含和去除字段
SecurityParameter serializedField = methodParameter.getMethodAnnotation(SecurityParameter.class);
//入参是否需要解密
encode = serializedField.inDecode();
}
if (encode) {
logger.info("对方法method :【" + methodParameter.getMethod().getName() + "】返回数据进行解密");
return new MyHttpInputMessage(inputMessage);
} else {
return inputMessage;
}
} catch (Exception e) {
e.printStackTrace();
logger.error("对方法method :【" + methodParameter.getMethod().getName() + "】返回数据进行解密出现异常:" + e.getMessage());
return inputMessage;
}
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return body;
}
class MyHttpInputMessage implements HttpInputMessage {
private HttpHeaders headers;
private InputStream body;
public MyHttpInputMessage(HttpInputMessage inputMessage) throws Exception {
this.headers = inputMessage.getHeaders();
this.body = IOUtils.toInputStream(AesEncryptUtils.decryptAES(IOUtils.toString(inputMessage.getBody(), "UTF-8")));
}
@Override
public InputStream getBody() throws IOException {
return body;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
}
}
2.4.3、返回数据加密
/**
* 返回数据加密
*/
@ControllerAdvice(basePackages = "xxx.xxx.controller")
public class EncodeResponseBodyAdvice implements ResponseBodyAdvice {
private final static Logger logger = LoggerFactory.getLogger(EncodeResponseBodyAdvice.class);
@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
boolean encode = false;
if (methodParameter.getMethod().isAnnotationPresent(SecurityParameter.class)) {
//获取注解配置的包含和去除字段
SecurityParameter serializedField = methodParameter.getMethodAnnotation(SecurityParameter.class);
//出参是否需要加密
encode = serializedField.outEncode();
}
if (encode) {
logger.info("对方法method :【" + methodParameter.getMethod().getName() + "】返回数据进行加密");
ObjectMapper objectMapper = new ObjectMapper();
try {
String result = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(body);
return AesEncryptUtils.encryptAES(result);
} catch (Exception e) {
e.printStackTrace();
logger.error("对方法method :【" + methodParameter.getMethod().getName() + "】返回数据进行解密出现异常:" + e.getMessage());
}
}
return body;
}
}
2.4.4、前后端数据传输加密工具类
/**
* 前后端数据传输加密工具类
*
* @author monkey
*/
@Slf4j
public class AesEncryptUtils {
/*
* 加密用的Key 可以用26个字母和数字组成 例如使用AES-128-CBC加密模式,key需要为16位。
*/
private static final String KEY = "xxxxxxxxxxxxxxxx";
private static final String IV = "xxxxxxxxxxxxxxxx";
//参数分别代表 算法名称/加密模式/数据填充方式
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS7Padding";
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* AES算法加密明文
*
* @param data 明文
* @return 密文
*/
public static String encryptAES(String data) {
try {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
SecretKeySpec keyspec = new SecretKeySpec(KEY.getBytes("UTF-8"), "AES");
// CBC模式,需要一个向量iv,可增加加密算法的强度
IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes("UTF-8"));
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
byte[] encrypted = cipher.doFinal(data.getBytes("UTF-8"));
// BASE64做转码。
return AesEncryptUtils.encode(encrypted).trim();
} catch (Exception e) {
log.error("{} 加密失败", data, e);
return null;
}
}
/**
* AES算法解密密文
*
* @param data 密文
* @return 明文
*/
public static String decryptAES(String data) {
try {
if (StringUtils.isEmpty(data)) {
return null;
}
//先用base64解密
byte[] encrypted = AesEncryptUtils.decode(data);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
SecretKeySpec keyspec = new SecretKeySpec(KEY.getBytes("UTF-8"), "AES");
IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes("UTF-8"));
cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec);
byte[] original = cipher.doFinal(encrypted);
String originalString = new String(original);
return originalString.trim();
} catch (Exception e) {
log.error("{} 解密失败", data, e);
return data;
}
}
/**
* 编码
*
* @param byteArray
* @return
*/
public static String encode(byte[] byteArray) {
return new String(new Base64().encode(byteArray));
}
/**
* 解码
*
* @param base64EncodedString
* @return
*/
public static byte[] decode(String base64EncodedString) {
return new Base64().decode(base64EncodedString);
}
/**
* 用于对GET请求中的查询字符串进行解密
*
* @param params GET请求中的查询字符串
*/
public static Map<String, Object> decryptQueryParams(Map<String, String> params) {
Map<String, Object> doAfter = new HashMap<>();
for (Map.Entry<String, String> entry : params.entrySet()) {
if (!StringUtils.isEmpty(entry.getValue())) {
doAfter.put(entry.getKey(), decryptAES(entry.getValue()));
}
}
return doAfter;
}
}