一、前言
在之前的学习中我们介绍了注解实际上起到的是标记和注释的作用,其本身并不提供任何的逻辑处理能力。也就是说如果想让注解能够实现预期的作用,就必须给注解搭配一个能够读取并处理该注解的方法,这里为了方便描述我将这样一个方法定义为注解处理器。基于这样一个认知,我们学习的重心就需要放在理解Spring中不同注解的注解处理器是如何生效的以及Spring提供了哪些自定义注解处理器实现的扩展点上面。
二、基本概念
首先,让我们先了解一下Spring注解的基本情况,具体我们需要了解以下几个方面:
在Spring当中注解可以分为两类,一类是Spring本身提供的原生注解(包含JDK提供的注解),一类是由开发人员自行开发的自定义注解。由于不同的注解需要特定的注解处理器才能够识别和处理,这也就意味着注解处理器也会分为原生注解处理器和自定义注解处理器两种。
那么如何在Spring中实现一个注解处理器呢?为了实现这一个需求,我们需要关注两个方面:
基于Spring框架
识别和处理目标注解
在之前的文章中我们讲解了在Java程序中实现注解处理器的两种方式,分别是基于反射机制的方式和使用插入式注解处理器的方式。而在加了需要基于Spring框架来实现这样一个前提之后,使用插入式注解处理器的方式就不是非常合适,因为这种方式依赖的是JDK提供的用于在编译阶段进行注解识别和处理的API而并非是依赖Spring框架提供的能力。
注意,这里并不是说不可以使用插入式注解,只是我们讨论的前提是基于Spring提供的能力来实现注解处理器。
如此一来,我们能够实现注解处理器的方式就少了一种,但正所谓:失之东隅,收之桑榆。虽然我们不能使用插入式注解,但Spring框架提供了另外一种方式来作为替代——基于BeanDefinition的方式。让我们重新回顾一下注解处理器的作用:识别并处理注解。可以看到,这里的第一步就是识别注解。所谓识别注解,实际上是识别被注解标识的类、属性或者方法,而这些信息都保存在类型信息当中。这也是为什么我们可以使用反射机制来去识别注解的原因,因为我们的目标就是去获取这些信息。让我们将目光重新转移到Spring框架中,此时我们会发现这些信息Spring已经收集并保存在BeanDefinition当中了。由此我们得到两种在Spring框架中实现注解处理器的方式:
基于BeanDefinition方式
:通过ApplicationContext的getBeansWithAnnotation
方法获取被注解的Bean;基于反射机制方式
:通过JDK提供的反射相关API获取类、属性以及方法维度的信息;
那么应该如何使用这两种方式呢?其实在我们实际的开发过程中,这两种方式的使用并不是完全独立的,两者既可以分开使用,也可以搭配使用,具体的使用方式还需要结合Spring本身提供的扩展点进行具体讨论。
除此以外我们还需要根据实际的执行时机判断如何实现注解处理器的执行逻辑,基于Spring应用的生命周期可以分为以下三种执行时机:
初始化阶段
:在应用初始化阶段执行的注解执行器一般只能够执行一次,一般情况是为了在Spring的通用初始化流程之外对特定Bean进行一些额外初始化操作,具体可以参考@Service
、@Resource
等原生注解处理器的执行逻辑,有兴趣的同学也可以阅读笔者之前的一篇文章关于Spring的两三事:万物之始—BeanDefinition。应用运行阶段
:在应用运行阶段执行的注解处理器可以针对每一次请求进行拦截执行。应用销毁阶段
:和第一种执行时机一样,在应用销毁阶段执行的注解执行器也执行执行一次。
三、自定义注解的几个小例子
由于目前已经有太多的文章来讲解Spring原生注解以及注解处理器的相关原理,笔者就不做过多的阐述,想要了解的同学可以自行百度或者翻阅源码。这里笔者想要用几个比较常见的例子来说明一下如何实现一个自定义注解及其注解处理器。
测试注解类
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
}
@Inherited
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LogMonitor {
/**
* 方法名称
*
* @return 方法名称
*/
String methodName() default "";
/**
* 日志事件
*
* @return 日志事件
*/
String logEvent() default "";
}
复制代码
1. 基于ApplicationContextAware和InitializingBean接口
@Component
public class TestApplicationAware implements ApplicationContextAware, InitializingBean {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void afterPropertiesSet() throws Exception {
// 在所有的属性被设置之后进行一次处理
this.handleClass();
this.handleMethod();
}
private void handleMethod() {
Map<String, Object> beans = applicationContext.getBeansWithAnnotation(RestController.class);
for (Object bean : beans.values()) {
Class<?> clazz = bean.getClass();
System.out.println(clazz.getName());
for (Method method : clazz.getDeclaredMethods()) {
// 这里需要思考一个问题:如果从应用上下文中取出的代理对象会发生什么?
LogMonitor logMonitor = method.getAnnotation(LogMonitor.class);
if (Objects.nonNull(logMonitor)) {
System.out.println("被注解的方法: " + method.getName());
}
TestAnnotation testAnnotation = method.getAnnotation(TestAnnotation.class);
if (Objects.nonNull(testAnnotation)) {
System.out.println("被测试注解注解的方法: " + method.getName());
}
}
}
}
private void handleClass() {
Map<String, Object> beans = applicationContext.getBeansWithAnnotation(LogMonitor.class);
System.out.println(beans.keySet());
}
}
复制代码
通过ApplicationContextAware
接口我们可以获取到应用上下文applicationConetext
对象,而通过InitalizingBean
接口提供的afterPropertiesSet
方法,我们可以将注解处理逻辑嵌入到Bean初始化流程当中。从上面的代码可以看到,我们既使用了基于BeanDefinition的方式,也使用了反射机制来获取方法维度的信息。
但是在这里我们需要注意一个问题,即如果从应用上下中获取到的Bean是一个代理对象,那么上面关于方法级别的处理逻辑还能够生效吗?这里的答案是否定的,因为Spring中使用CGlib生成的代理对象实际是原有对象的子类,而在子类重写父类方法的同时标记在方法维度的注解时不会被继承的,这也就导致了上面关于方法级别的处理逻辑会出现失效的情况。除了方法维度的处理逻辑会遇到这样的问题,类的成员变量维度也会遇到相同的问题。至于如何解决这个问题,笔者的建议是没事就不要混着使用了,还是遵循着Java语法规则来编写代码吧。
2. 基于BeanPostProcessor接口
@Component
public class TestBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(@NotNull Object bean, @NotNull String beanName) throws BeansException {
Class<?> clazz = bean.getClass();
Annotation[] annotations = clazz.getAnnotations();
handleAnnotation(annotations, beanName);
// 同样,在遇到代理对象的时候这里也会出现同样的问题
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method method : declaredMethods) {
handleAnnotation(method.getAnnotations(), method.getName());
}
// 同样,在遇到代理对象的时候这里也会出现同样的问题
Field[] declaredFields = clazz.getDeclaredFields();
for (Field field : declaredFields) {
handleAnnotation(field.getAnnotations(), field.getName());
}
return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
}
private void handleAnnotation(Annotation[] annotations, String name) {
if (null == annotations) {
return;
}
for (Annotation annotation : annotations) {
if (annotation.annotationType().equals(LogMonitor.class)) {
System.out.println("拥有指定注解的测试Bean或方法名称: " + name);
}
}
}
}
复制代码
通过BeanPostProcessor
接口我们可以在Bean的前置/后置处理中进行注解的处理。从上面的代码实现中可以发现,这里只能通过反射机制来完成对应信息的识别和处理。同样,使用这种方式实现的注解处理器也会面临第一种方式遇到的问题。
3. 基于Spring AOP
@Slf4j
@Aspect
@Component
public class LogAspect {
/**
* 限制时间
*/
private static final long LIMIT_TIME = 5000;
/**
* 进行日志切面打印
*
* @param joinPoint 切入点
* @return 处理完成对象
*/
@Around("@annotation(com.brucebat.hey.push.core.common.annotations.LogMonitor) || @within(com.brucebat.hey.push.core.common.annotations.LogMonitor)")
public Object process(ProceedingJoinPoint joinPoint) throws Throwable {
// 针对方法进行拦截
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
LogMonitor logMonitor = method.getAnnotation(LogMonitor.class);
if (Objects.isNull(logMonitor)) {
logMonitor = joinPoint.getTarget().getClass().getAnnotation(LogMonitor.class);
}
if (Objects.isNull(logMonitor)) {
return joinPoint.proceed();
}
String methodName = logMonitor.methodName();
if (StringUtils.isBlank(methodName)) {
String[] classNameArray = method.getDeclaringClass().getName().split("\.");
methodName = classNameArray[classNameArray.length - 1] + "." + method.getName();
}
StopWatch stopWatch = new StopWatch();
try {
stopWatch.start(methodName);
log.info("日志切面打印, method = {}, request = {}", methodName, Objects.isNull(joinPoint.getArgs()) ? "null" : GsonUtils.toJsonString(joinPoint.getArgs()));
Object object = joinPoint.proceed();
log.info("日志切面打印, method = {}, response = {}", methodName, Objects.isNull(object) ? "null" : GsonUtils.toJsonString(object));
return object;
} finally {
stopWatch.stop();
if (stopWatch.getTotalTimeMillis() > LIMIT_TIME) {
log.info("method = {} 耗时超过限制", methodName);
}
log.info("method = {} 耗时 {} 毫秒", methodName, stopWatch.getTotalTimeMillis());
}
}
}
复制代码
最后一种方式通过Spring AOP切面编程的方式来完成注解处理。区别于前两种方式是在Spring应用初始化阶段执行并且只能执行一次,使用这种方式实现的注解处理器可以在每次请求执行到被注解标记的类对象或者方法时都会出现处理逻辑。想必这里会有不少同学会问,为什么在上面代码中从方法级别获取注解信息又是可以获取的呢?其实在这里获取到的方法信息是来源于被代理的原始类型信息,不同于上面两种方法从代理对象中直接获取类型信息。原始的类型信息当中一定会包含对应的注解信息,这也是为什么上面关于方法维度的注解识别和处理逻辑是可以生效的。
四、总结
在本篇文章中,笔者并没有花费笔墨去介绍Spring框架中具体某个原生注解的处理逻辑,而是将目光聚焦于介绍Spring内部实现注解处理的可能方案以及如何依赖Spring提供的能力去实现一个自定义注解处理器。从上面的介绍我们不难发现,实现一个注解处理器的关键就在于抓住如何识别和怎样触发,当你明白这两个问题该如何处理之后,即使后面不再使用Spring框架而是转投其他框架的怀抱,也依然能够在对应框架中实现一个符合预期的自定义注解处理器。
最后,疫情仍在继续,希望大家注意防护,身体健康!