Analysis of the reasons why annotations on interface methods cannot be intercepted by the aspects declared by @Aspect

foreword

When using the Mapper interface of MyBatis in Spring to automatically generate, mark the method of the Mapper interface with a custom annotation, and then use @Aspect to define an aspect, and intercept this annotation to record the log or execution time. But I am surprised to find that after doing this, it does not take effect in Spring Boot 1.X (Spring Framework 4.x), but it can take effect in Spring Boot 2.X (Spring Framework 5.X).

What is this for? What updates have Spring made to produce such a change? This article will lead you to explore this secret.

case

core code

@SpringBootApplication
public class Starter {
  public static void main(String[] args) {
    SpringApplication.run(DynamicApplication.class, args);
  }
}

@Service
public class DemoService {

    @Autowired
    DemoMapper demoMapper;

    public List<Map<String, Object>> selectAll() {
        return demoMapper.selectAll();
    }
}

/**
 * mapper类
 */
@Mapper
public interface DemoMapper {

  @Select("SELECT * FROM demo")
  @Demo
  List<Map<String, Object>> selectAll();

}

/**
 * 切入的注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Demo {
  String value() default "";
}

/**
 * aspect切面,用于测试是否成功切入
 */
@Aspect
@Order(-10)
@Component
public 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");
  }

}

test class

@RunWith(SpringRunner.class) 
@SpringBootTest(classes = Starter.class)
public class BaseTest {

    @Autowired
    DemoService demoService;

    @Test
    public void testDemo() {
        demoService.selectAll();
    }

}

In Spring Boot 1.X, the two printlns in @Aspect are not printed normally, but in Spring Boot 2.X, both are printed.

Debug research

The interceptor declared by the known @Aspect annotation will automatically cut into the beans that meet its interception conditions. This function is enabled and configured through the @EnableAspectJAutoProxy annotation (enabled by default, through AopAutoConfiguration), as can be seen from @Import(AspectJAutoProxyRegistrar.class) in @EnableAspectJAutoProxy, the dependency of @Aspect-related annotation automatic cut-in is the BeanPostProcessor of AnnotationAwareAspectJAutoProxyCreator. Put a conditional breakpoint in the postProcessAfterInitialization method of this class: 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;
}

In the wrapIfNecessary method, there is the logic to automatically wrap the 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;
}

Debugging found that the specificInterceptors return in Spring Boot 1.X is empty, but not in Spring Boot 2.X, so here is the core point of the problem, check the source code:

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

The core problem here lies in the AopUtils.findAdvisorsThatCanApply method. The return here is different in the two versions. Because there is too much code here, it will not be posted. The core problem code is this:

// AopProxyUtils.java
public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> 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;
	}
}

Basically locate the problem point, look at the canApply method that is finally called, the code here is different between Spring Boot 1.X and 2.X

  1. The source code in Spring Boot 1.X, that is, the source code in 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,这里两个版本都为true
	if (!pc.getClassFilter().matches(targetClass)) {
		return false;
	}
    
	MethodMatcher methodMatcher = pc.getMethodMatcher();
	// 如果method是固定true,即拦截所有method,则返回true。这里当然为false
	if (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<Class<?>> classes = new LinkedHashSet<Class<?>>(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
	// 再把目标calss加入遍历列表
	classes.add(targetClass);
	for (Class<?> clazz : classes) {
		Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
		// 遍历每个类的每个方法,尝试判断是否match
		for (Method method : methods) {
			if ((introductionAwareMethodMatcher != null &&
					introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) ||
					methodMatcher.matches(method, targetClass)) {
				return true;
			}
		}
	}

	return false;
}
  1. Source code in Spring Boot 2.X, that is, source code in 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<Class<?>> 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;
}

Debug infographicdebugging information

The execution results of the above code are different, but the difference is that there are fewer classes generated by dynamic agents to traverse. Why is the result of one less traversal content true? There must be changes in the logic of introductionAwareMethodMatcher or methodMatcher, where methodMatcher and introductionAwareMethodMatcher are the same object, and the two methods have the same logic. Look at the code:

/** 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 case
		if (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)));
	}
}

This code is basically the same in Spring Boot 1.X and 2.X, but in the execution result of AopUtils.getMostSpecificMethod(method, targetClass); the two are different, 1.X returns dynamic The method in the overridden interface in the class generated by the proxy, 2.X returns the method in the original interface.

In the interface method rewritten in the class generated by the dynamic proxy, the annotation information in the interface will not be included, so the conditional annotation in Aspect cannot get the matching information here, so false is returned.

In 2.X, because the method of the original interface is returned, it can be successfully matched.

The problem lies in the logic of AopUtils.getMostSpecificMethod(method, targetClass):

// 1.X
public 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.X
public 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);
}

So far, the reason is completely clear, and Spring has fixed this problem in the 5.X version of AOP.

Sphere of influence

The reason has been identified, then according to the reason, we can calculate the scope of influence

  1. When the Bean is an interface dynamic proxy object, and the dynamic proxy object is not generated by the Spring system, the aspect annotations in the interface cannot be intercepted
  2. When the bean is a CGLIB dynamic proxy object, the dynamic proxy object is not generated by the Spring system, and the aspect annotation on the original class method cannot be intercepted.
  3. May also affect the interception system based on class name and method name, because the generated dynamic proxy classpath and class name are different.

If it is generated by the Spring system, all the real classes or interfaces are obtained before, and only after the dynamic proxy is generated, it is a new class. So when you create a dynamic proxy, you get the real class.

Interface dynamic proxy is more common in ORM framework's Mapper, RPC framework's SPI, etc., so be careful when using annotations in these two cases.

Some students are more concerned about whether the @Cacheable annotation takes effect in Mapper. The answer is to take effect, because the @Cacheable annotation uses not the PointCut of @Aspect, but the CacheOperationSourcePointcut. Although getMostSpecificMethod is also used to obtain the method, in the end, it actually tries to obtain the annotation from the original method:

// AbstractFallbackCacheOperationSource.computeCacheOperations
if (specificMethod != method) {
	//  Fallback is to look at the original method
	opDef = 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;
	}
}

It seems that it is not affected, but it is actually compatible.

You can refer to the following content, there are issues related to Spring mentioned

solution

How to solve this problem? The answer is that there is no solution in Spring Boot 1.X. . Because this class is too basic, unless you switch versions.

This problem can also be solved by using other Aspect expressions. The annotation method is unsolvable in version 1.X.

Expressions refer to the following links:

  1. A detailed explanation of Spring's AOP AspectJ pointcut syntax (the most comprehensive and detailed.)
  2. Execution expression of Spring Aspect

I thought that adding @Inherited to the annotation Demo could solve it, but I found out that it is not possible, because this @Inherited is only valid for class annotations, and it cannot be inherited by subclasses or implementation classes in interfaces or methods. See this @Inherited above Notes

/**
 * Indicates that an annotation type is automatically inherited.  If
 * an Inherited meta-annotation is present on an annotation type
 * declaration, and the user queries the annotation type on a class
 * declaration, and the class declaration has no annotation for this type,
 * then the class's superclass will automatically be queried for the
 * annotation type.  This process will be repeated until an annotation for this
 * type is found, or the top of the class hierarchy (Object)
 * is reached.  If no superclass has an annotation for this type, then
 * the query will indicate that the class in question has no such annotation.
 *
 * <p>Note that this meta-annotation type has no effect if the annotated
 * type is used to annotate anything other than a class.  Note also
 * that this meta-annotation only causes annotations to be inherited
 * from superclasses; annotations on implemented interfaces have no
 * effect.
 * 上面这句话说明了只在父类上的注解可被继承,接口上的都是无效的
 *
 * @author  Joshua Bloch
 * @since 1.5
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}

Extended reading

The problem and the possible scope of influence have been analyzed in detail. Let's be curious. Did the author write anything in the submission record of the core problem class AopUtils.java?

AopUtils.java class GitHub page

Looking at the history of this class, notice the commit on the date Commits on Apr 3, 2018 , which mentions:

Consistent treatment of proxy classes and interfaces for introspection

Issue: SPR-16675
Issue: SPR-16677

The introspection configuration is done for proxy classes. The related issue is SPR-16677. Let's take a look at this issue.

Spring Framework/SPR-16677

This issue details the reason and purpose of this commit.

Readers can read in detail if they are interested.

Pay attention to the latest submission of AopUtils.java, and make some optimizations, you can study it.

expand knowledge

The above sample code depends on the database, and now an improvement is made to simulate the Mapper class, which can reproduce the problem directly without any dependencies:

It is known that the Mapper interface of Mybatis is the logic generated by the JDK dynamic proxy, and the Bean generation related to the Mapper interface is automatically registered in the BeanFactory through the AutoConfiguredMapperScannerRegistrar, and the factory Bean type MapperFactoryBean is registered.

The getObject method of MapperFactoryBean is generated by getSqlSession().getMapper(this.mapperInterface), and mapperInterfact is the mapper interface.

The bottom layer is generated by Configuration.getMapper, and then the bottom layer is the mapperRegistry.getMapper method, the code is as follows

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
        // 调用下面的方法生成代理实例
        return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
        throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
}
public T newInstance(SqlSession sqlSession) {
    // 创建MapperProxy这个InvocationHandler实例
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
    // 调用jdk动态代理生成实例,代理的InvocationHandler是MapperProxy
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

It can be seen that the bottom layer is generated by the JDK dynamic proxy Proxy, and the InvocationHandler is the MapperProxy class.

After clarifying the principle, we make a modification to the above example to simplify the reference of Mybatis.

@Configuration
public class DemoConfiguraion {
    
    @Bean
    public FactoryBean<DemoMapper> getDemoMapper() {
        return new FactoryBean<DemoMapper>() {
            @Override
            public DemoMapper getObject() throws Exception {
                InvocationHandler invocationHandler = (proxy, method, args) -> {
                    System.out.println("调用动态代理方法" + method.getName());
                    return Collections.singletonList(new HashMap<String, Object>());
                };
                return (DemoMapper) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[] {DemoMapper.class}, invocationHandler);
            }
            @Override
            public Class<?> getObjectType() {
                return DemoMapper.class;
            }
            @Override
            public boolean isSingleton() {
                return true;
            }
        };
    }
}

The above code can achieve the same effect as Mapper, and you can play it locally.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325966998&siteId=291194637