今天就带着大伙梳理一遍注解也就是 @interface 正确的打开方式,除此之外,结合 AOP 切面统一打印出入参日志,对于每个访问注解绑定的接口方法的请求都一目了然,不仅方便接口的调试,还能给你一个优雅、整齐且大方的控制台日志记录。
知其然,要知其所以然,所以我们先来康康官方对注解的描述是什么:
An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.
翻译过来的大意是:
注释是一种元数据的形式,可以添加到Java源代码中。类,方法,变量,参数和包可以注释。注释对他们注释的代码的操作没有直接影响。
综上来说,注解其实相当于 Java 的一种特殊的数据类型,也可以把它当做一个可以自定义的标记去理解,和类、接口、枚举类似,可以使用在很多不同的地方并且对原有的操作代码没有任何影响,仅做中间收集和处理。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Excel {
public String name();
int sort() default Integer.MAX_VALUE;
... ...
}
说明(从上往下):
此外,在 JDK 中提供了 4 个标准的用来对注解类型进行注解的注解类(元注解),分别是:
public enum ElementType {
/** Class, interface (including annotation type), or enum declaration */
/** 类,接口(包括注释类型)或枚举声明 */
TYPE,
/** Field declaration (includes enum constants) */
/** 字段声明(包括枚举常量) */
FIELD,
/** Method declaration */
/** 方法声明 */
METHOD,
/** Formal parameter declaration */
/** 形参声明 */
PARAMETER,
/** Constructor declaration */
/** 构造器声明 */
CONSTRUCTOR,
/** Local variable declaration */
/** 本地变量声明 */
LOCAL_VARIABLE,
/** Annotation type declaration */
/** 注解类型声明 */
ANNOTATION_TYPE,
/** Package declaration */
/** 包声明 */
PACKAGE,
/**
* Type parameter declaration
*
* @since 1.8
*/
/** 类型参数声明 */
TYPE_PARAMETER,
/**
* Use of a type
*
* @since 1.8
*/
/** 使用类型 */
TYPE_USE
}
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.
*
* 注释将由编译器记录在类文件中,并在运行时由虚拟机保留,因此可以通过反射读取。
*
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}
一般使用无特殊需要,使用 RetentionPolicy.RUNTIME 就够了。
注意:@Inherited 注解只对那些 @Target 被定义为 ElementType.TYPE 的自定义注解起作用。
编写设计注解:
/**
* @description: TODO
* @author: HUALEI
* @date: 2021-11-19
* @time: 15:50
*/
@Retention(RetentionPolicy.RUNTIME)
/* 注解用在方法上 */
@Target(ElementType.METHOD)
public @interface MyAnnotation {
/**
* 接口方法描述
*/
public String description() default "默认描述";
}
这步没什么好讲的,上面的概念理解掌握了,轻轻松松写出这个注解应该是没有什么问题!
使用切面注解进行标记,因为是对请求相关的日志打印,所以我们随便写一个控制层接口方法进行测试:
/**
* @description: TODO
* @author: HUALEI
* @date: 2021-11-24
* @time: 13:43
*/
@RestController
@RequestMapping(value = "/test")
public class TestController {
private final static Logger log = LoggerFactory.getLogger(TestController.class);
@GetMapping("/hello/{say}")
@MyAnnotation(description = "测试接口")
public String sayHello(@PathVariable("say") String content) {
log.info("Client is saying:{}", content);
return content;
}
}
最后一步也是最关键的一步,在运行时解析注解执行切面操作,所以对应地写一个切面类:
新建切面类后,考虑到日志的打印,这段代码必不可少:
/**
* @description: TODO
* @author: HUALEI
* @date: 2021-11-19
* @time: 15:56
*/
@Aspect
@Component
public class MyAnnotationAspect {
private static final Logger logger = LoggerFactory.getLogger(MyAnnotationAspect.class);
......
......
}
@Aspect 和 @Component 注解必不可少,@Component 大伙应该在熟悉不过了,将该类注入到 Spring 容器中;而另一个 @Aspect 注解的作用是把当前类标识成一个切面供容器去读取。
注意: 打印日志推荐使用的包是 slf4j.Logger 。
/**
* 配置织入点
*
* 切到所有被 @MyAnnotation 注解修饰的方法
*/
@Pointcut("@annotation(com.xxx.xxx.annotation.MyAnnotation)")
// @annotation(annotationType) 匹配指定注解为切入点的方法,annotationType 为注解的全路径
public void myAnnotationPointCut() {
}
配置织入点,切到所有被 @MyAnnotation 注解修饰的方法,不需要再方法体内编写实际的代码!
/**
* 环绕增强,可自定义目标方法执行的时机
* 实现记录所有被 @MyAnnotation 注解修饰接口请求功能
*
* @param pjp 连接点对象
* @return 目标方法的返回值
* @throws Throwable 异常
*/
@Around("myAnnotationPointCut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 请求开始时间戳
// long begin = System.currentTimeMillis();
TimeInterval timer = DateUtil.timer();
// 通过请求上下文(执行目标方法之前)
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 获取连接点的方法签名对象
Signature signature = pjp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
// 获取接口方法
Method method = methodSignature.getMethod();
// 通过接口方法获取该方法上的 @MyAnnotation 注解对象
MyAnnotation myAnnotation = method.getAnnotation(MyAnnotation.class);
// 通过注解获取接口方法描述信息
String description = myAnnotation.description();
// 请求开始(前置通知)
logger.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 请求开始 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");
// 请求链接
logger.info("请求链接:{}", request.getRequestURL().toString());
// 接口方法描述信息
logger.info("接口描述:{}", description);
// 请求类型
logger.info("请求类型:{}", request.getMethod());
// 请求方法
logger.info("请求方法:{}.{}", signature.getDeclaringTypeName(), signature.getName());
// 请求远程地址
logger.info("请求远程地址:{}", request.getRemoteAddr());
// 请求入参
logger.info("请求入参:{}", JSONUtil.toJsonStr(pjp.getArgs()));
// 请求结束时间戳
// long end = System.currentTimeMillis();
// 请求耗时
logger.info("请求耗时:{}", timer.intervalPretty());
// 请求返回结果(执行目标方法之后)
Object processedResult = pjp.proceed();
// 请求返回
logger.info("请求返回:{}", JSONUtil.toJsonStr(processedResult));
// 请求结束(后置通知)
logger.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 请求结束 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" + System.lineSeparator());
return processedResult;
}
pjp 连接点对象,JoinPoint 的子接口,可以获取当前切入的方法的参数、代理类等信息,因此可以记录一些信息、验证一些信息等,它有两个重要的方法:
整个代码都有注解,这里就不赘述代码逻辑了!
除了上面用到的 @PointCut 和 @Around 注解,还有另外 4 个使用 AOP 常用的注解:
执行顺序:@Around => @Before => 执行接口方法中的代码 => @After => @AfterReturning
有兴趣的同学,可以环绕增强中的代码拆分到前置和后置增强中,以便更好地理解这四个常用注解使用场景! (๑•̀ㅂ•́)و✧
作者:HUALEI
链接:
https://juejin.cn/post/7034406941925474318