jsonfield注解不生效_接口方法上的注解无法被@Aspect声明的切面拦截的原因分析...

前言

在Spring中使用MyBatis的Mapper接口自动生成时,用一个自定义的注解标记在Mapper接口的方法中,再利用@Aspect定义一个切面,拦截这个注解以记录日志或者执行时长。但是惊奇的发现这样做之后,在Spring Boot 1.X(Spring Framework 4.x)中,并不能生效,而在Spring Boot 2.X(Spring Framework 5.X)中却能生效。

这究竟是为什么呢?Spring做了哪些更新产生了这样的变化?此文将带领你探索这个秘密。

案例

核心代码

@SpringBootApplicationpublic class Starter { public static void main(String[] args) { SpringApplication.run(DynamicApplication.class, args); }}@Servicepublic class DemoService { @Autowired DemoMapper demoMapper; public List> selectAll() { return demoMapper.selectAll(); }}/** * mapper类 */@Mapperpublic interface DemoMapper { @Select("SELECT * FROM demo") @Demo List> selectAll();}/** * 切入的注解 */@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Demo { String value() default "";}/** * aspect切面,用于测试是否成功切入 */@Aspect@Order(-10)@Componentpublic class DemoAspect { @Before("@annotation(demo)") public void beforeDemo(JoinPoint point, Demo demo) { System.out.println("before demo"); } @AfterDemo("@annotation(demo)") public void afterDemo(JoinPoint point, Demo demo) { System.out.println("after demo"); }}

测试类

@RunWith(SpringRunner.class) @SpringBootTest(classes = Starter.class)public class BaseTest { @Autowired DemoService demoService; @Test public void testDemo() { demoService.selectAll(); }}

在Spring Boot 1.X中,@Aspect里的两个println都没有正常打印,而在Spring Boot 2.X中,都打印了出来。

调试研究

已知@Aspect注解声明的拦截器,会自动切入符合其拦截条件的Bean。这个功能是通过@EnableAspectJAutoProxy注解来启用和配置的(默认是启用的,通过AopAutoConfiguration),由@EnableAspectJAutoProxy中的@Import(AspectJAutoProxyRegistrar.class)可知,@Aspect相关注解自动切入的依赖是AnnotationAwareAspectJAutoProxyCreator这个BeanPostProcessor。在这个类的postProcessAfterInitialization方法中打上条件断点:beanName.equals("demoMapper")

public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {if (bean != null) { // 缓存中尝试获取,没有则尝试包装Object cacheKey = getCacheKey(bean.getClass(), beanName);if (!this.earlyProxyReferences.contains(cacheKey)) {return wrapIfNecessary(bean, beanName, cacheKey);}}return bean;}

在wrapIfNecessary方法中,有自动包装Proxy的逻辑:

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { // 如果是声明的需要原始Bean,则直接返回if (beanName != null && this.targetSourcedBeans.contains(beanName)) {return bean;}// 如果不需要代理,则直接返回if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {return bean;}// 如果是Proxy的基础组件如Advice、Pointcut、Advisor、AopInfrastructureBean则跳过if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {this.advisedBeans.put(cacheKey, Boolean.FALSE);return bean;}// Create proxy if we have advice.// 根据相关条件,查找interceptor,包括@Aspect生成的相关Interceptor。// 这里是问题的关键点,Spring Boot 1.X中这里返回为空,而Spring Boot 2.X中,则不是空Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);if (specificInterceptors != DO_NOT_PROXY) { // 返回不是null,则需要代理this.advisedBeans.put(cacheKey, Boolean.TRUE);// 放入缓存Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));// 自动生成代理实例this.proxyTypes.put(cacheKey, proxy.getClass());return proxy;}this.advisedBeans.put(cacheKey, Boolean.FALSE);return bean;}

调试发现,Spring Boot 1.X中specificInterceptors返回为空,而Spring Boot 2.X中则不是空,那么这里就是问题的核心点了,查看源码:

protected Object[] getAdvicesAndAdvisorsForBean(Class> beanClass, String beanName, TargetSource targetSource) {List advisors = findEligibleAdvisors(beanClass, beanName);if (advisors.isEmpty()) { // 如果是空,则不代理return DO_NOT_PROXY;}return advisors.toArray();}protected List findEligibleAdvisors(Class> beanClass, String beanName) { // 找到当前BeanFactory中的AdvisorList candidateAdvisors = findCandidateAdvisors();// 遍历Advisor,根据Advisor中的PointCut判断,返回所有合适的AdvisorList eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);// 扩展advisor列表,这里会默认加入一个ExposeInvocationInterceptor用于暴露动态代理对象,之前文章有解释过extendAdvisors(eligibleAdvisors);if (!eligibleAdvisors.isEmpty()) { // 根据@Order或者接口Ordered排序eligibleAdvisors = sortAdvisors(eligibleAdvisors);}return eligibleAdvisors;}protected List findAdvisorsThatCanApply(List candidateAdvisors, Class> beanClass, String beanName) {ProxyCreationContext.setCurrentProxiedBeanName(beanName);try { // 真正的查找方法return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);}finally {ProxyCreationContext.setCurrentProxiedBeanName(null);}}

这里的核心问题在于AopUtils.findAdvisorsThatCanApply方法,这里的返回在两个版本是不一样的,由于这里代码过多就不贴上来了,说明下核心问题代码是这段:

// AopProxyUtils.javapublic static List findAdvisorsThatCanApply(List candidateAdvisors, Class> clazz) { // ... 省略 for (Advisor candidate : candidateAdvisors) { if (canApply(candidate, clazz, hasIntroductions)) { eligibleAdvisors.add(candidate); } } // ... 省略}public static boolean canApply(Advisor advisor, Class> targetClass, boolean hasIntroductions) {if (advisor instanceof IntroductionAdvisor) {return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass);}else if (advisor instanceof PointcutAdvisor) { // 对于@Aspect的切面,是这段代码在生效PointcutAdvisor pca = (PointcutAdvisor) advisor;return canApply(pca.getPointcut(), targetClass, hasIntroductions);}else {// It doesn't have a pointcut so we assume it applies.return true;}}

基本定位了问题点,看下最终调用的canApply方法,Spring Boot 1.X与2.X这里的代码是不一样的

  1. Spring Boot 1.X中源码,即Spring AOP 4.X中源码
/** * targetClass是com.sun.proxy.$Proxy??即JDK动态代理生成的类 * hasIntroductions是false,先不管 */public static boolean canApply(Pointcut pc, Class> targetClass, boolean hasIntroductions) {Assert.notNull(pc, "Pointcut must not be null");// 先判断class,这里两个版本都为trueif (!pc.getClassFilter().matches(targetClass)) {return false;} MethodMatcher methodMatcher = pc.getMethodMatcher();// 如果method是固定true,即拦截所有method,则返回true。这里当然为falseif (methodMatcher == MethodMatcher.TRUE) {// No need to iterate the methods if we're matching any method anyway...return true;} // 特殊类型,做下转换,Aspect生成的属于这个类型IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;if (methodMatcher instanceof IntroductionAwareMethodMatcher) {introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;} // 取到目标class的所有接口Set> classes = new LinkedHashSet>(ClassUtils.getAllInterfacesForClassAsSet(targetClass));// 再把目标calss加入遍历列表classes.add(targetClass);for (Class> clazz : classes) {Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);// 遍历每个类的每个方法,尝试判断是否matchfor (Method method : methods) {if ((introductionAwareMethodMatcher != null &&introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) ||methodMatcher.matches(method, targetClass)) {return true;}}}return false;}
  1. Spring Boot 2.X中源码,即Spring AOP 5.X中源码
public static boolean canApply(Pointcut pc, Class> targetClass, boolean hasIntroductions) {Assert.notNull(pc, "Pointcut must not be null");if (!pc.getClassFilter().matches(targetClass)) {return false;}MethodMatcher methodMatcher = pc.getMethodMatcher();if (methodMatcher == MethodMatcher.TRUE) {// No need to iterate the methods if we're matching any method anyway...return true;}IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;if (methodMatcher instanceof IntroductionAwareMethodMatcher) {introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;}Set> classes = new LinkedHashSet<>();// 这里与1.X版本不同,使用Jdk动态代理Proxy,先判断是否是Proxy,如果不是则加入用户Class,即被动态代理的class,以便查找真正的Class中是否符合判断条件// 因为动态代理可能只把被代理类的方法实现了,被代理类的注解之类的没有复制到生成的子类中,故要使用原始的类进行判断// JDK动态代理一样不会为动态代理生成类上加入接口的注解// 如果是JDK动态代理,不需要把动态代理生成的类方法遍历列表中,因为实现的接口中真实的被代理接口。if (!Proxy.isProxyClass(targetClass)) {classes.add(ClassUtils.getUserClass(targetClass));}classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));for (Class> clazz : classes) {Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);for (Method method : methods) { // 比1.X版本少遍历了Proxy生成的动态代理类,但是遍历内容都包含了真实的接口,其实是相同的,为什么结果不一样呢?if ((introductionAwareMethodMatcher != null &&introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) ||methodMatcher.matches(method, targetClass)) {return true;}}}return false;}

调试信息图

34c60204b72047d18ac0cb3d294a80a5.png

上面的代码执行结果不同,但是区别只是少个动态代理生成的类进行遍历,为什么少一个遍历内容结果却是true呢?肯定是introductionAwareMethodMatcher或者methodMatcher的逻辑有改动,其中methodMatcher和introductionAwareMethodMatcher是同一个对象,两个方法逻辑相同。看代码:

/** AspectJExpressionPointcut.java * method是上面接口中遍历的方法,targetClass是目标class,即生成的动态代理class */public boolean matches(Method method, @Nullable Class> targetClass, boolean beanHasIntroductions) {obtainPointcutExpression();Method targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);ShadowMatch shadowMatch = getShadowMatch(targetMethod, method);// Special handling for this, target, @this, @target, @annotation// in Spring - we can optimize since we know we have exactly this class,// and there will never be matching subclass at runtime.if (shadowMatch.alwaysMatches()) {return true;}else if (shadowMatch.neverMatches()) {return false;}else {// the maybe caseif (beanHasIntroductions) {return true;}// A match test returned maybe - if there are any subtype sensitive variables// involved in the test (this, target, at_this, at_target, at_annotation) then// we say this is not a match as in Spring there will never be a different// runtime subtype.RuntimeTestWalker walker = getRuntimeTestWalker(shadowMatch);return (!walker.testsSubtypeSensitiveVars() ||(targetClass != null && walker.testTargetInstanceOfResidue(targetClass)));}}

这段代码在Spring Boot 1.X和2.X中基本是相同的,但是在AopUtils.getMostSpecificMethod(method, targetClass);这一句的执行结果上,两者是不同的,1.X返回的是动态代理生成的Class中重写的接口中的方法,2.X返回的是原始接口中的方法。

而在动态代理生成的Class中重写的接口方法里,是不会包含接口中的注解信息的,所以Aspect中条件使用注解在这里是拿不到匹配信息的,所以返回了false。

而在2.X中,因为返回的是原始接口的方法,故可以成功匹配。

问题就在于AopUtils.getMostSpecificMethod(method, targetClass)的逻辑:

// 1.Xpublic static Method getMostSpecificMethod(Method method, Class> targetClass) { // 这里返回了targetClass上的重写的method方法。Method resolvedMethod = ClassUtils.getMostSpecificMethod(method, targetClass);// If we are dealing with method with generic parameters, find the original method.return BridgeMethodResolver.findBridgedMethod(resolvedMethod);}// 2.Xpublic static Method getMostSpecificMethod(Method method, @Nullable Class> targetClass) { // 比1.X多了个逻辑判断,如果是JDK的Proxy,则specificTargetClass为null,否则取被代理的Class。Class> specificTargetClass = (targetClass != null && !Proxy.isProxyClass(targetClass) ?ClassUtils.getUserClass(targetClass) : null);// 如果specificTargetClass为空,直接返回原始method。// 如果不为空,返回被代理的Class上的方法Method resolvedMethod = ClassUtils.getMostSpecificMethod(method, specificTargetClass);// If we are dealing with method with generic parameters, find the original method.// 获取真实桥接的方法,泛型支持return BridgeMethodResolver.findBridgedMethod(resolvedMethod);}

至此原因已经完全明了,Spring在AOP的5.X版本修复了这个问题。

影响范围

原因已经查明,那么根据原因我们推算一下影响范围

  1. Bean是接口动态代理对象时,且该动态代理对象不是Spring体系生成的,接口中的切面注解无法被拦截
  2. Bean是CGLIB动态代理对象时,该动态代理对象不是Spring体系生成的,原始类方法上的切面注解无法被拦截。
  3. 可能也影响基于类名和方法名的拦截体系,因为生成的动态代理类路径和类名是不同的。

如果是Spring体系生成的,之前拿到的都是真实类或者接口,只有在生成动态代理后,才是新的类。所以在创建动态代理时,获取的是真实的类。

接口动态代理多见于ORM框架的Mapper、RPC框架的SPI等,所以在这两种情况下使用注解要尤为小心。

有些同学比较关心@Cacheable注解,放在Mapper中是否生效。答案是生效,因为@Cacheable注解中使用的不是@Aspect的PointCut,而是CacheOperationSourcePointcut,其中虽然也使用了getMostSpecificMethod来获取method,但是最终其实又从原始方法上尝试获取了注解:

// AbstractFallbackCacheOperationSource.computeCacheOperationsif (specificMethod != method) {// Fallback is to look at the original methodopDef = findCacheOperations(method);if (opDef != null) {return opDef;}// Last fallback is the class of the original method.opDef = findCacheOperations(method.getDeclaringClass());if (opDef != null && ClassUtils.isUserLevelMethod(method)) {return opDef;}}

看似不受影响,其实是做了兼容。

62b619e77773358469a1b5ecacd67282.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值