一、前言
二、效果演示
2.1 访问接口
2.2 控制台日志输出
三、如何设计一个注解
3.1 概念
知其然,要知其所以然,所以我们先来康康官方对注解的描述是什么:
翻译过来的大意是:
综上来说,注解其实相当于 Java 的一种特殊的数据类型,也可以把它当做一个可以自定义的标记去理解,和类、接口、枚举类似,可以使用在很多不同的地方并且对原有的操作代码没有任何影响,仅做中间收集和处理。
3.2 小试牛刀
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Excel {
public String name();
int sort() default Integer.MAX_VALUE;
... ...
}
说明(从上往下):
- 使用该注解在程序运行时被 JVM 保留,并且被编译器记录到 class 文件中,所以能够通过 Java 反射机制读取到注解中的属性等
- 该注解仅能使用在字段上,不能用在类、方法、变量上
- 该注解有两个属性,一个是 name 另一个是 sort 属性,属性?你可能就会问了后边不是带一对圆括号嘛,不应该是方法吗?看似接口中定义的抽象方法,实则看没看到 default 关键字,官方管定义在注解内的是 注解类型元素 ,不过我习惯管它们叫属性,因为在使用注解时,总是以键值对的形式传参
- 访问修饰符必须为 public,不写默认为 public
- 圆括号不是定义方法参数的地方,也不能在括号中定义任何参数,这仅仅是一个特殊的写法罢了
- default 表示未设置该属性时的默认值,值需和类型保持一致
- 如果没有 default 默认值,表示该类型元素必须在后续赋值
3.3 注解注解的注解类
此外,在 JDK 中提供了 4 个标准的用来对注解类型进行注解的注解类(元注解),分别是:
- @Target 是专门用来限定某个自定义注解能够被应用在哪些 Java 元素上
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
}
- @Retention 该注解有 “保留”、“保持” 之义,用来定义注解的留存策略,可指定的留存策略只有 3 个:
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 就够了。
- @Documented 是被用来指定自定义注解是否能随着被定义的 Java 文件生成到 JavaDoc 文档当中
- @Inherited 是指定某个自定义注解如果写在了父类的声明部分,那么子类的声明部分也能自动拥有该注解
注意:@Inherited 注解只对那些 @Target 被定义为 ElementType.TYPE 的自定义注解起作用。
3.4 使用流程
四、代码实现
4.1 第一步
编写设计注解:
/**
* @description: TODO
* @author: HUALEI
* @date: 2021-11-19
* @time: 15:50
*/
@Retention(RetentionPolicy.RUNTIME)
/* 注解用在方法上 */
@Target(ElementType.METHOD)
public @interface MyAnnotation {
/**
* 接口方法描述
*/
public String description() default "默认描述";
}
这步没什么好讲的,上面的概念理解掌握了,轻轻松松写出这个注解应该是没有什么问题!
4.2 第二步
使用切面注解进行标记,因为是对请求相关的日志打印,所以我们随便写一个控制层接口方法进行测试:
/**
* @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;
}
}
4.3 第三步
最后一步也是最关键的一步,在运行时解析注解执行切面操作,所以对应地写一个切面类:
新建切面类后,考虑到日志的打印,这段代码必不可少:
/**
* @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 的子接口,可以获取当前切入的方法的参数、代理类等信息,因此可以记录一些信息、验证一些信息等,它有两个重要的方法:
- Object proceed() throws Throwable 执行目标方法
- Object proceed(Object[] var1) throws Throwable 传入的新的参数去执行目标方法
整个代码都有注解,这里就不赘述代码逻辑了!
4.4 扩展
除了上面用到的 @PointCut 和 @Around 注解,还有另外 4 个使用 AOP 常用的注解:
- @Before :前置增强,在切点之前织入相关代码
- @After :final 增强,不管是抛出异常或者正常退出都会执行
- @AfterReturning :后置增强,方法正常退出时执行
- @AfterThrowing :异常抛出增强,切点方法抛出异常时执行
执行顺序:@Around => @Before => 执行接口方法中的代码 => @After => @AfterReturning
有兴趣的同学,可以环绕增强中的代码拆分到前置和后置增强中,以便更好地理解这四个常用注解使用场景! (๑•̀ㅂ•́)و✧