在我们实际的开发过程当中,可能会用到一些自定义注解去实现一些功能,自定义注解可以注解在接口类的方法上,也可以注解在接口实现类的方法上,这样这个自定义注解运用起来就会更加的灵活,其实想要在SpringBoot中达到这样的效果是一件非常简单的事。
以下的实现方式借鉴了 keetone 大佬的(原创) spring aop 无法拦截接口上的注解文章,中间做了一些修改,如果想要更为详细的了解可以去看看他的这篇文章
好了费话不多说,直接上代码(中间有很多我个人的理解的描述,可能不正确,勿喷, 但功能是能用的):
pom 引入 aspectjweaver 依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
自定义一个注解类,例如:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Ax {
String value() default "";
}
Target({ElementType.METHOD})
由这里定义只能注解在方法上面,如果有其他的需求,可以去看看 java.lang.annotation.ElementType
这个枚举类的定义定义一个方法匹配切入点顾问类,例如:
public class AxMethodPointcutAdvisor extends StaticMethodMatcherPointcutAdvisor {
@Override
public boolean matches(@NotNull Method method, @NotNull Class<?> targetClass) {
return AnnotatedElementUtils.hasAnnotation(method, Ax.class);
}
}
定义这个类的目的是把所有接口类及接口实现类中被我们上面定义的Ax
注解的所有方法都过滤出来,让spring给我们自动生成CGLIB代理(其实是自动生成SpringProxy代理)
定义一个方法拦截器
这个是作为过滤出来的方法的切面处理,我们对于自己定义的注解要实现功能的处理逻辑就写在这个里面
public class AxInterceptor implements MethodInterceptor {
@Override
public Object invoke(@NotNull MethodInvocation invocation) throws Throwable {
String name = invocation.getMethod().getName();
Ax ax = invocation.getMethod().getAnnotation(Ax.class);
Object ret;
if (Objects.nonNull(ax)) {
// 在这里,拦截的有可能是我们的接口实现类的方法,也有可能是Spring为我们自动创建的SpringProxy动态代理的方法
// 在接口实现类和动态代理类中,方法上面的注解源信息都可以拿到
// 当我们在接口类的方法上添加我们自定义的注解时,Spring为我们创建的动态代理的方法上也会有该注解,且含有我们在接口类中设置的注解源信息,比如Ax的value值
// 调用该方法之前的处理逻辑... 这里只是打印了一下信息,自定义的注解想要实现的功能逻辑就从这里开始写,调用方法前后或者不调用方法大家各自发挥
System.out.println("before -- " + ax.value() + " --->> " + name);
ret = invocation.proceed();
// 调用该方法之后的处理逻辑... 这里也只是打印了一下信息
System.out.println("after -- " + ax.value() + " ---->> " + name);
} else {
// 该地方是的接口类中被注解的方法的拦截,但是在这里我们拿不到自定义注解源信息:ax都是null,更别说获取ax的value
// 故让方法调用继续往下,下面就有可能是实现类的方法调用,也有可能是Spring为我们创建的动态代理类的方法调用
ret = invocation.proceed();
}
return ret;
}
}
让Spring把我们上面定义的类粘合起来
@Configuration
public class AxConfig implements BeanPostProcessor {
/**
* 切面处理类注册为Spring的Bean
* 这个里面是我们自定义注解需想要实现功能的核心
*
* @return AxInterceptor
*/
@Bean
public AxInterceptor axInterceptor() {
return new AxInterceptor();
}
/**
* 方法匹配切入点顾问
* (我个人觉得更像是ApplicationContext中的Bean的扫描过滤器,过滤出需要创建动态代理的方法)
*
* @return AxMethodPointcutAdvisor
*/
@Bean
public AxMethodPointcutAdvisor axMethodPointcutAdvisor() {
AxMethodPointcutAdvisor advisor = new AxMethodPointcutAdvisor();
// 设置切面处理
advisor.setAdvice(axInterceptor());
return advisor;
}
/**
* 该Bean是让Spring自动创建代理的核心
* 可以不添加这个Bean,如果不添加,那么接口类的方法注解就拦截不到了,只能拦截到接口实现类中被注解的方法
* 它和上面申明的AxMethodPointcutAdvisor一起协同工作,它要创建的代理由AxMethodPointcutAdvisor中matches方法决定
*
* @return DefaultAdvisorAutoProxyCreator
*/
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
return new DefaultAdvisorAutoProxyCreator();
}
}
需要注意的是该类为BeanPostProcessor
的实现,也可以不实现BeanPostProcessor接口,不影响使用,如果不实现BeanPostProcessor接口,那么在Spring启动的时候会出现一行像是错误的提示(Info级别的日志):
Bean 'axConfig' of type [com.proxyclient.advisor.AxConfig$$EnhancerBySpringCGLIB$$60a28775] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
这个提示出现的原因是我们在这个类里申明了DefaultAdvisorAutoProxyCreator
这个Bean,如果不创建DefaultAdvisorAutoProxyCreator这个Bean,就可以不用实现BeanPostProcessor
接口,启动时不会出现上面那个日志。
这里需要说明一下:我们在申明DefaultAdvisorAutoProxyCreator
这个Bean之后,可能会产生一些"副使用",比如我自己这个Demo中的SpringRetry实例,启动会报错,需要在Retryable的实例上添加@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
这个注解才能正常启动和工作,这只是我个人遇到了这个问题及解决的办法,所以大家在以这种方式去实现接口类和实现类的自定义注解拦截时需要注意由DefaultAdvisorAutoProxyCreator这个Bean带来的副作用测试一下
测试接口类。
public interface AxTestService {
@Ax(value = "interface a method")
String a();
@Ax(value = "interface b method")
String b();
String c();
String d();
String e();
}
测试实现类
@Service
public class AxTestServiceImpl implements AxTestService {
@Override
public String a() {
System.out.println("impl a method");
return "a method\n";
}
@Override
public String b() {
System.out.println("impl b method");
return "b method\n";
}
@Ax(value = "impl c method")
@Override
public String c() {
System.out.println("impl c method");
return "c method\n";
}
@Ax(value = "impl d method")
@Override
public String d() {
System.out.println("impl d method");
return "d method\n";
}
@Override
public String e() {
System.out.println("impl e method");
return "e method\n";
}
}
测试WEB入口类
@RestController
@RequestMapping("/ax")
public class AxTestController {
@Resource
private AxTestService axTestService;
@GetMapping
public void a() {
System.out.println("controller-->> " + axTestService.a());
System.out.println("controller-->> " + axTestService.b());
System.out.println("controller-->> " + axTestService.c());
System.out.println("controller-->> " + axTestService.d());
System.out.println("controller-->> " + axTestService.e());
}
}
直接浏览器请求:http://host:port/ax时,打印日志为:
before -- interface a method --->> a
impl a method
after -- interface a method ---->> a
controller-->> a method
before -- interface b method --->> b
impl b method
after -- interface b method ---->> b
controller-->> b method
before -- impl c method --->> c
impl c method
after -- impl c method ---->> c
controller-->> c method
before -- impl d method --->> d
impl d method
after -- impl d method ---->> d
controller-->> d method
impl e method
controller-->> e method
通过上面的日志可以看到,不论是在接口类中还是实现类中的方法上添加了Ax
注解的都拦截到了,没有被Ax
注解的方法就不会被拦截,这已经达到了我们想要的效果
必须要注意的是接口类必须要有实现类
必须要注意的是接口类必须要有实现类
必须要注意的是接口类必须要有实现类
重要的事情说三遍,除非你自己为这些接口创建动态代理类,不然Spring启动直接报错!
不会有人还要问:你上面不是由Spring自动创建代理了吗?
评论区