26

自定义注解的魅力你到底懂不懂

 3 years ago
source link: https://leishen6.github.io/2020/11/15/understand_annotations_charm/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

你知道自定义注解的魅力所在吗?

你知道自定义注解该怎么使用吗?

本文一开始的这两个问题,需要您仔细思考下,然后结合这两个问题来阅读下面的内容;如果您在阅读完文章后对这两个问题有了比较清晰的,请动动您发财的小手,点赞留言呀!

FzArEvE.jpg!mobile

本文主线:

  • 注解是什么;

  • 实现一个自定义注解;

  • 自定义注解的实战应用场景;

注意:本文在介绍自定义注解实战应用场景时,需要结合拦截器、AOP进行使用,所以本文也会简单聊下AOP相关知识点,如果对于AOP的相关内容不太清楚的可以参考此 细说Spring——AOP详解 文章进行了解。

注解

注解是什么?

①、引用自维基百科的内容:

Java注解又称Java标注,是JDK5.0版本开始支持加入源代码的特殊语法 元数据

Java语言中的类、方法、变量、参数和包等都可以被标注。和Javadoc不同,Java标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java虚拟机可以保留标注内容,在运行时可以获取到标注内容。 当然它也支持自定义Java标注。

②、引用自网络的内容:

Java 注解是在 JDK5 时引入的新特性,注解(也被称为 元数据 )为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便地使用这些数据。

元注解是什么?

元注解的作用就是负责注解其他注解。Java5.0定义了4个标准的meta-annotation(元注解)类型,它们被用来提供对其它 annotation类型作说明。

标准的元注解:

  • @Target

  • @Retention

  • @Documented

  • @Inherited

在详细说这四个元数据的含义之前,先来看一个在工作中会经常使用到的 @Autowired 注解,进入这个注解里面瞧瞧: 此注解中使用到了@Target、@Retention、@Documented 这三个元注解 。

@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
boolean required() default true;
}

@Target元注解:

@Target注解,是专门用来限定某个自定义注解能够被应用在哪些Java元素上面的,标明作用范围;取值在 java.lang.annotation.ElementType 进行定义的。

public enum ElementType {
/** 类,接口(包括注解类型)或枚举的声明 */
TYPE,

/** 属性的声明 */
FIELD,

/** 方法的声明 */
METHOD,

/** 方法形式参数声明 */
PARAMETER,

/** 构造方法的声明 */
CONSTRUCTOR,

/** 局部变量声明 */
LOCAL_VARIABLE,

/** 注解类型声明 */
ANNOTATION_TYPE,

/** 包的声明 */
PACKAGE
}

根据此处可以知道 @Autowired 注解的作用范围:

// 可以作用在 构造方法、方法、方法形参、属性、注解类型 上
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})

@Retention元注解:

@Retention注解,翻译为持久力、保持力。即用来修饰自定义注解的生命周期。

注解的生命周期有三个阶段:

  • Java源文件阶段;

  • 编译到class文件阶段;

  • 运行期阶段;

同样使用了RetentionPolicy 枚举类型对这三个阶段进行了定义:

public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
* (注解将被编译器忽略掉)
*/

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文件中,但在运行时不会被虚拟机保留,这是一个默认的行为)
*/

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.
* (注解将被编译器记录在class文件中,而且在运行时会被虚拟机保留,因此它们能通过反射被读取到)
* @see java.lang.reflect.AnnotatedElement
*/

RUNTIME
}

再详细描述下这三个阶段:

①、如果被定义为 RetentionPolicy.SOURCE,则它将被限定在Java源文件中,那么这个注解即不会参与编译也不会在运行期起任何作用,这个注解就和一个注释是一样的效果,只能被阅读Java文件的人看到;

②、如果被定义为 RetentionPolicy.CLASS,则它将被编译到Class文件中,那么编译器可以在编译时根据注解做一些处理动作,但是运行时JVM(Java虚拟机)会忽略它,并且在运行期也不能读取到;

③、如果被定义为 RetentionPolicy.RUNTIME,那么这个注解可以在运行期的加载阶段被加载到Class对象中。那么在程序运行阶段,可以通过反射得到这个注解,并通过判断是否有这个注解或这个注解中属性的值,从而执行不同的程序代码段。

注意:实际开发中的自定义注解几乎都是使用的 RetentionPolicy.RUNTIME 。

@Documented元注解:

@Documented注解,是被用来指定自定义注解是否能随着被定义的java文件生成到JavaDoc文档当中。

@Inherited元注解:

@Inherited注解,是指定某个自定义注解如果写在了父类的声明部分,那么子类的声明部分也能自动拥有该注解。
@Inherited注解只对那些@Target被定义为 ElementType.TYPE 的自定义注解起作用。

自定义注解实现:

在了解了上面的内容后,我们来尝试实现一个自定义注解:

Fzaymyv.png!mobile

根据上面自定义注解中使用到的元注解得知:

①、此注解的作用范围,可以使用在类(接口、枚举)、方法上;

②、此注解的生命周期,被编译器保存在class文件中,而且在运行时会被JVM保留,可以通过反射读取;

自定义注解的简单使用:

上面已经创建了一个自定义的注解,那该怎么使用呢?下面首先描述下它简单的用法,后面将会使用其结合拦截器和AOP切面编程进行实战应用;

FBj222b.png!mobile

应用场景实现

在了解了上面注解的知识后,我们乘胜追击,看看它的实际应用场景是肿么样的,以此加深下我们的理解;

实现的 Demo 项目是以 SpringBoot 实现的,项目工程结构图如下:

Nra6Nfa.png!mobile

场景一:自定义注解 + 拦截器 = 实现接口响应的包装

使用自定义注解 结合 拦截器 优雅的实现对API接口响应的包装。

在介绍自定义实现的方式之前,先简单介绍下普遍的实现方式,通过两者的对比,才能更加明显的发现谁最优雅。

普通的接口响应包装方式:

现在项目绝大部分都采用的前后端分离方式,所以需要前端和后端通过接口进行交互;目前在接口交互中使用最多的数据格式是 json,然后后端返回给前端的最为常见的响应格式如下:

{
#返回状态码
code:integer,
#返回信息描述
message:string,
#返回数据值
data:object
}

项目中经常使用枚举类定义状态码和消息,代码如下:

/**
* @author 【 木子雷 】 公众号
* @Title: ResponseCode
* @Description: 使用枚举类封装好的响应状态码及对应的响应消息
* @date: 2019年8月23日 下午7:12:50
*/

public enum ResponseCode {

SUCCESS(1200, "请求成功"),

ERROR(1400, "请求失败");


private Integer code;

private String message;

private ResponseCode(Integer code, String message) {
this.code = code;
this.message = message;
}

public Integer code() {
return this.code;
}

public String message() {
return this.message;
}

}

同时项目中也会设计一个返回响应包装类,代码如下:

import com.alibaba.fastjson.JSONObject;
import java.io.Serializable;

/**
* @author 【 木子雷 】 公众号
* @Title: Response
* @Description: 封装的统一的响应返回类
* @date: 2019年8月23日 下午7:07:13
*/

@SuppressWarnings("serial")
public class Response<T> implements Serializable {

/**
* 响应数据
*/

private T date;

/**
* 响应状态码
*/

private Integer code;

/**
* 响应描述信息
*/

private String message;

public Response(T date, Integer code, String message) {
super();
this.date = date;
this.code = code;
this.message = message;
}


public T getDate() {
return date;
}

public void setDate(T date) {
this.date = date;
}

public Integer getCode() {
return code;
}

public void setCode(Integer code) {
this.code = code;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}


@Override
public String toString() {
return JSONObject.toJSONString(this);
}
}

最后就是使用响应包装类和状态码枚举类 来实现返回响应的包装了:

@GetMapping("/user/findAllUser")
public Response<List<User>> findAllUser() {
logger.info("开始查询所有数据...");

List<User> findAllUser = new ArrayList<>();
findAllUser.add(new User("木子雷", 26));
findAllUser.add(new User("公众号", 28));

// 返回响应进行包装
Response response = new Response(findAllUser, ResponseCode.SUCCESS.code(), ResponseCode.SUCCESS.message());

logger.info("response: {} \n", response.toString());
return response;
}

在浏览器中输入网址: http://127.0.0.1:8080/v1/api/user/findAllUser 然后点击回车,得到如下数据:

{
"code": 1200,
"date": [
{
"age": 26,
"name": "木子雷"
},
{
"age": 28,
"name": "公众号"
}
],
"message": "请求成功"
}

通过看这中实现响应包装的方式,我们能发现什么问题吗?

答:代码很冗余,需要在每个接口方法中都进行响应的包装;使得接口方法包含了很多非业务逻辑代码;

有没有版本进行优化下呢? en en 思考中。。。。。 啊,自定义注解 + 拦截器可以实现呀!

自定义注解实现接口响应包装:

①、首先创建一个进行响应包装的自定义注解:

/**
* @author 【 木子雷 】 公众号
* @PACKAGE_NAME: com.lyl.annotation
* @ClassName: ResponseResult
* @Description: 标记方法返回值需要进行包装的 自定义注解
* @Date: 2020-11-10 10:38
**/

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseResult {

}

②、创建一个拦截器,实现对请求的拦截,看看请求的方法或类上是否使用了自定义的注解:

/**
* @author 【 木子雷 】 公众号
* @PACKAGE_NAME: com.lyl.interceptor
* @ClassName: ResponseResultInterceptor
* @Description: 拦截器:拦截请求,判断请求的方法或类上是否使用了自定义的@ResponseResult注解,
* 并在请求内设置是否使用了自定义注解的标志位属性;
* @Date: 2020-11-10 10:50
**/

@Component
public class ResponseResultInterceptor implements HandlerInterceptor {

/**
* 标记位,标记请求的controller类或方法上使用了到了自定义注解,返回数据需要被包装
*/

public static final String RESPONSE_ANNOTATION = "RESPONSE_ANNOTATION";

/**
* 请求预处理,判断是否使用了自定义注解
*/

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception
{
// 请求的接口方法
if (handler instanceof HandlerMethod) {
final HandlerMethod handlerMethod = (HandlerMethod) handler;
final Class<?> clazz = handlerMethod.getBeanType();
final Method method = handlerMethod.getMethod();
// 判断是否在类对象上加了注解
if (clazz.isAnnotationPresent(ResponseResult.class)) {
// 在请求中设置需要进行响应包装的属性标志,在下面的ResponseBodyAdvice增强中进行处理
request.setAttribute(RESPONSE_ANNOTATION, clazz.getAnnotation(ResponseResult.class));
} else if (method.isAnnotationPresent(ResponseResult.class)) {
// 在请求中设置需要进行响应包装的属性标志,在下面的ResponseBodyAdvice增强中进行处理
request.setAttribute(RESPONSE_ANNOTATION, method.getAnnotation(ResponseResult.class));
}
}
return true;
}
}

③、创建一个增强Controller,实现对返回响应进行包装的增强处理:

/**
* @author 【 木子雷 】 公众号
* @PACKAGE_NAME: com.lyl.interceptor
* @ClassName: ResponseResultHandler
* @Description: 对 返回响应 进行包装 的增强处理
* @Date: 2020-11-10 13:49
**/

@ControllerAdvice
public class ResponseResultHandler implements ResponseBodyAdvice<Object> {

private final Logger logger = LoggerFactory.getLogger(this.getClass());

/**
* 标记位,标记请求的controller类或方法上使用了到了自定义注解,返回数据需要被包装
*/

public static final String RESPONSE_ANNOTATION = "RESPONSE_ANNOTATION";

/**
* 请求中是否包含了 响应需要被包装的标记,如果没有,则直接返回,不需要重写返回体
*
* @param methodParameter
* @param aClass
* @return
*/

@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
ServletRequestAttributes ra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest sr = (HttpServletRequest) ra.getRequest();
// 查询是否需要进行响应包装的标志
ResponseResult responseResult = (ResponseResult) sr.getAttribute(RESPONSE_ANNOTATION);
return responseResult == null ? false : true;
}


/**
* 对 响应体 进行包装; 除此之外还可以对响应体进行统一的加密、签名等
*
* @param responseBody 请求的接口方法执行后得到返回值(返回响应)
*/

@Override
public Object beforeBodyWrite(Object responseBody, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
logger.info("返回响应 包装进行中。。。");
Response response;
// boolean类型时判断一些数据库新增、更新、删除的操作是否成功
if (responseBody instanceof Boolean) {
if ((Boolean) responseBody) {
response = new Response(responseBody, ResponseCode.SUCCESS.code(), ResponseCode.SUCCESS.message());
} else {
response = new Response(responseBody, ResponseCode.ERROR.code(), ResponseCode.ERROR.message());
}
} else {
// 判断像查询一些返回数据的情况,查询不到数据返回 null;
if (null != responseBody) {
response = new Response(responseBody, ResponseCode.SUCCESS.code(), ResponseCode.SUCCESS.message());
} else {
response = new Response(responseBody, ResponseCode.ERROR.code(), ResponseCode.ERROR.message());
}
}
return response;
}
}

④、最后在 Controller 中使用上我们的自定义注解;在 Controller 类上或者 方法上使用@ResponseResult自定义注解即可; 在浏览器中输入网址: http://127.0.0.1:8080/v1/api/user/findAllUserByAnnotation 进行查看:

// 自定义注解用在了方法上
@ResponseResult
@GetMapping("/user/findAllUserByAnnotation")
public List<User> findAllUserByAnnotation() {
logger.info("开始查询所有数据...");

List<User> findAllUser = new ArrayList<>();
findAllUser.add(new User("木子雷", 26));
findAllUser.add(new User("公众号", 28));

logger.info("使用 @ResponseResult 自定义注解进行响应的包装,使controller代码更加简介");
return findAllUser;
}

至此我们的接口返回响应包装自定义注解实现设计完成,看看代码是不是又简洁,又优雅呢。

总结:本文针对此方案只是进行了简单的实现,如果有兴趣的朋友可以进行更好的优化。

场景二:自定义注解 + AOP = 实现优雅的使用分布式锁

分布式锁的最常见的使用流程:

zqARJbr.png!mobile

先看看最为常见的分布式锁使用方式的实现,然后再聊聊自定义注解怎么优雅的实现分布式锁的使用。

普通的分布式锁使用方式:

YFBN7rQ.png!mobile

通过上面的代码可以得到一个信息:如果有很多方法中需要使用分布式锁,那么每个方法中都必须有获取分布式锁和释放分布式锁的代码,这样一来就会出现代码冗余;

那有什么好的解决方案吗? 自定义注解使代码变得更加简洁、优雅;

自定义注解优雅的使用分布式锁:

①、首先实现一个标记分布式锁使用的自定义注解:

/**
* @author 【 木子雷 】 公众号
* @PACKAGE_NAME: com.lyl.annotation
* @ClassName: GetDistributedLock
* @Description: 获取redis分布式锁 注解
* @Date: 2020-11-10 16:24
**/

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GetDistributedLock {

// 分布式锁 key
String lockKey();

// 分布式锁 value,默认为 lockValue
String lockValue() default "lockValue";

// 过期时间,默认为 300秒
int expireTime() default 300;

}

②、定义一个切面,在切面中对使用了 @GetDistributedLock 自定义注解的方法进行环绕增强通知:

/**
* @author: 【 木子雷 】 公众号
* @PACKAGE_NAME: com.lyl.aop
* @ClassName: DistributedLockAspect
* @Description: 自定义注解结合AOP切面编程优雅的使用分布式锁
* @Date: 2020-11-10 16:52
**/

@Component
@Aspect
public class DistributedLockAspect {

private final Logger logger = LoggerFactory.getLogger(this.getClass());

@Autowired
RedisService redisService;


/**
* Around 环绕增强通知
*
* @param joinPoint 连接点,所有方法都属于连接点;但是当某些方法上使用了@GetDistributedLock自定义注解时,
* 则其将连接点变为了切点;然后在切点上织入额外的增强处理;切点和其相应的增强处理构成了切面Aspect 。
*/

@Around(value = "@annotation(com.lyl.annotation.GetDistributedLock)")
public Boolean handlerDistributedLock(ProceedingJoinPoint joinPoint) {
// 通过反射获取自定义注解对象
GetDistributedLock getDistributedLock = ((MethodSignature) joinPoint.getSignature())
.getMethod().getAnnotation(GetDistributedLock.class);

// 获取自定义注解对象中的属性值
String lockKey = getDistributedLock.lockKey();
String LockValue = getDistributedLock.lockValue();
int expireTime = getDistributedLock.expireTime();

if (redisService.tryGetDistributedLock(lockKey, LockValue, expireTime)) {
// 获取分布式锁成功后,继续执行业务逻辑
try {
return (boolean) joinPoint.proceed();
} catch (Throwable throwable) {
logger.error("业务逻辑执行失败。", throwable);
} finally {
// 最终保证分布式锁的释放
redisService.releaseDistributedLock(lockKey, LockValue);
}
}
return false;
}

}

③、最后,在 Controller 中的方法上使用 @GetDistributedLock 自定义注解即可;当某个方法上使用了 自定义注解,那么这个方法就相当于一个切点,那么就会对这个方法做环绕(方法执行前和方法执行后)增强处理;

在浏览器中输入网址: http://127.0.0.1:8080/v1/api/user/getDistributedLock 回车后触发方法执行:

// 自定义注解的使用
@GetDistributedLock(lockKey = "userLock")
@GetMapping("/user/getDistributedLock")
public boolean getUserDistributedLock() {
logger.info("获取分布式锁...");
// 写具体的业务逻辑

return true;
}

通过自定义注解的方式,可以看到代码变得更加简洁、优雅。

场景三:自定义注解 + AOP = 实现日志的打印

先看看最为常见的日志打印的方式,然后再聊聊自定义注解怎么优雅的实现日志的打印。

普通日志的打印方式:

IzyARf.png!mobile

通过看上面的代码可以知道,如果每个方法都需要打印下日志,那将会存在大量的冗余代码;

自定义注解实现日志打印:

①、首先创建一个标记日志打印的自定义注解:

/**
* @Author: 【 木子雷 】 公众号
* @PACKAGE_NAME: com.lyl.annotation
* @ClassName: PrintLog
* @Description: 自定义注解实现日志打印
* @Date: 2020-11-10 18:05
**/

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PrintLog {

}

②、定义一个切面,在切面中对使用了 @PrintLog 自定义注解的方法进行环绕增强通知:

/**
* @author: 【 木子雷 】 公众号
* @PACKAGE_NAME: com.lyl.aop
* @ClassName: PrintLogAspect
* @Description: 自定义注解结合AOP切面编程优雅的实现日志打印
* @Date: 2020-11-10 18:11
**/

@Component
@Aspect
public class PrintLogAspect {

private final Logger logger = LoggerFactory.getLogger(this.getClass());

/**
* Around 环绕增强通知
*
* @param joinPoint 连接点,所有方法都属于连接点;但是当某些方法上使用了@PrintLog自定义注解时,
* 则其将连接点变为了切点;然后在切点上织入额外的增强处理;切点和其相应的增强处理构成了切面Aspect 。
*/

@Around(value = "@annotation(com.lyl.annotation.PrintLog)")
public Object handlerPrintLog(ProceedingJoinPoint joinPoint) {
// 获取方法的名称
String methodName = joinPoint.getSignature().getName();
// 获取方法入参
Object[] param = joinPoint.getArgs();

StringBuilder sb = new StringBuilder();
for (Object o : param) {
sb.append(o + "; ");
}
logger.info("进入《{}》方法, 参数为: {}", methodName, sb.toString());

Object object = null;
// 继续执行方法
try {
object = joinPoint.proceed();

} catch (Throwable throwable) {
logger.error("打印日志处理error。。", throwable);
}
logger.info("{} 方法执行结束。。", methodName);
return object;
}

}

③、最后,在 Controller 中的方法上使用 @PrintLog 自定义注解即可;当某个方法上使用了 自定义注解,那么这个方法就相当于一个切点,那么就会对这个方法做环绕(方法执行前和方法执行后)增强处理;

@PrintLog
@GetMapping(value = "/user/findUserNameById/{id}", produces = "application/json;charset=utf-8")
public String findUserNameById(@PathVariable("id") int id) {
// 模拟根据id查询用户名
String userName = "木子雷 公众号";
return userName;
}

④、在浏览器中输入网址: http://127.0.0.1:8080/v1/api/user/findUserNameById/66 回车后触发方法执行,发现控制台打印了日志:

进入《findUserNameById》方法, 参数为: 66; 
findUserNameById 方法执行结束。。

使用自定义注解实现是多优雅,代码看起来简介干净,越瞅越喜欢;赶快去你的项目中使用吧, 嘿嘿。。。

end 。。。 自定义注解介绍到这本文也就结束了,期待我们的下次见面。

最后,想问下文章开头的那两个问题大家心里是不是已经有了答案呢!嘿嘿。。

❤关注 + 点赞 + 收藏 + 评论 哟

如果本文对您有帮助的话,请挥动下您爱发财的小手点下赞呀,您的支持就是我不断创作的动力;谢谢!

如果想要 Demo 源码的话,请您 VX搜索【木子雷】公众号,回复 “ 注解 ” 获取; 再次感谢您阅读本文!

参考资料

①、 自定义注解详细介绍

②、 Java 自定义注解及使用场景

③、 想自己写框架?不会写Java注解可不行

④、 看看人家那后端 API 接口写得,那叫一个优雅!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK