接口方法上的注解无法被@Aspect声明的切面拦截的原因分析

SpringBoot AOP 注解失效
部署运行你感兴趣的模型镜像

点击上方“Java知音”,选择“置顶公众号”

技术文章第一时间送达!


作者:光闪

my.oschina.net/guangshan/blog/1808373

(点击即可跳转阅读)

1. SpringBoot内容聚合

2. 面试题内容聚合

3. 设计模式内容聚合

4. Mybatis内容聚合

5. 多线程内容聚合

前言

在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<Map<String, Object>> selectAll() {        return demoMapper.selectAll();    }}/** * mapper类 */@Mapperpublic interface DemoMapper {  @Select("SELECT * FROM demo")  @Demo  List<Map<String, Object>> 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<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);    }}

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

// AopProxyUtils.javapublic 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;    }}

基本定位了问题点,看下最终调用的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,这里两个版本都为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;}

2.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<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;}

调试信息图

640?wx_fmt=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 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)));    }}

这段代码在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版本修复了这个问题。

影响范围

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

  • Bean是接口动态代理对象时,且该动态代理对象不是Spring体系生成的,接口中的切面注解无法被拦截

  • Bean是CGLIB动态代理对象时,该动态代理对象不是Spring体系生成的,原始类方法上的切面注解无法被拦截。

  • 可能也影响基于类名和方法名的拦截体系,因为生成的动态代理类路径和类名是不同的。

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

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

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

// AbstractFallbackCacheOperationSource.computeCacheOperationsif (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;    }}

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

可以参考后面的内容,有提到Spring相关的issue

解决方案

如何解决这个问题呢?答案是在Spring Boot 1.X中没有解决方案。。因为这个类太基础了,除非切换版本。

使用其他Aspect表达式也可以解决此问题,使用注解方式在1.X版本是无解的。

表达式参考如下链接:

https://blog.youkuaiyun.com/zhengchao1991/article/details/53391244
https://blog.youkuaiyun.com/lang_niu/article/details/51559994

本来以为在注解Demo中加入@Inherited可解决的,结果发现不行,因为这个@Inherited只在类注解有效,在接口中或者方法上,都是不能被子类或者实现类继承的,看这个@Inherited上面的注释

/** * 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 {}

扩展阅读

问题及可能的影响范围已经详细分析完了,下面我们好奇一下,这个核心问题类AopUtils.java的提交记录中,作者有写什么吗

AopUtils.java类GitHub页面

https://github.com/spring-projects/spring-framework/blob/master/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java

查看这个类的历史记录,注意Commits on Apr 3, 2018这个日期的提交,其中提到:

Consistent treatment of proxy classes and interfaces for introspectionIssue: SPR-16675Issue: SPR-16677

针对proxy classes做了内省配置,相关issue是SPR-16677,我们看下这个issue。

https://jira.spring.io/browse/SPR-16677

这个issue详细描述了这次提交的原因及目的。

读者感兴趣的话可以详细的阅读。

注意AopUtils.java的最新提交,又做了一些优化,可以研究一下。

扩展知识

上面的示例代码依赖于数据库,现做一个模拟Mapper类的改进,可以直接无任何依赖的重现该问题:

已知Mybatis的Mapper接口是通过JDK动态代理生成的逻辑,而Mapper接口相关的Bean生成,是通过AutoConfiguredMapperScannerRegistrar自动注册到BeanFactory中的,注册进去的是MapperFactoryBean这个工厂Bean类型。

而MapperFactoryBean的getObject方法,则是通过getSqlSession().getMapper(this.mapperInterface)生成的,mapperInterfact是mapper接口。

底层是通过Configuration.getMapper生成的,再底层是mapperRegistry.getMapper方法,代码如下

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);}

可以看到底层是通过JDK动态代理Proxy生成的,InvocationHandler是MapperProxy类。

清楚原理之后,我们对上面的实例做下改造,把Mybatis的引用简化。

@Configurationpublic 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;            }        };    }}

上面的代码可达到与Mapper同样的效果,大家可以本地随便玩哈。

觉得不错?欢迎转发分享给更多人


640?


您可能感兴趣的与本文相关的镜像

TensorFlow-v2.9

TensorFlow-v2.9

TensorFlow

TensorFlow 是由Google Brain 团队开发的开源机器学习框架,广泛应用于深度学习研究和生产环境。 它提供了一个灵活的平台,用于构建和训练各种机器学习模型

<think>我们正在解决一个具体问题:在Spring AOP中,当自定义注解标注在接口方法上时,无法被AOP拦截。根据提供的引用信息,我们可以总结如下: 问题原因: - Spring AOP默认使用JDK动态代理(当目标对象实现了接口)或CGLIB代理(当目标对象没有实现接口)。 - 当注解标注在接口方法上,而AOP拦截的是实现类的方法时,由于JDK动态代理是基于接口的,它不会继承接口上的注解(除非实现类方法上也标注了注解)[^1][^4]。 - 另外,Spring AOP的切入点表达式默认只针对目标类的方法,而不是接口方法。 解决方案: 1. **将注解移到实现类的方法上**:这是最直接的方法,但可能不符合设计(例如为了接口规范而将注解放在接口上)[^4]。 2. **使用CGLIB代理**:强制Spring AOP使用CGLIB代理,这样代理类会继承目标类(实现类),因此可以获取到接口方法上的注解(通过继承的方法)?但注意,CGLIB代理创建的是目标类的子类,因此只能获取到目标类方法上的注解,而接口上的注解仍然不会被直接继承。 3. **在实现类方法上再次添加注解**:这样虽然可以,但需要重复工作。 4. **通过反射获取接口方法上的注解**:在AOP通知方法中,我们可以通过反射获取目标方法对应的接口方法,然后从接口方法上获取注解。 具体实现方案(推荐第4种): - 在AOP的拦截器中,我们可以通过`Method`对象获取其声明类(即实现类),然后通过该实现类获取其实现的接口,再找到对应接口方法,从而获取注解。 步骤: 1. 在AOP切面中,定义环绕通知(或其他通知)方法。 2. 在通知方法中,通过`MethodInvocation`(或`JoinPoint`)获取目标方法(即实现类的方法)。 3. 通过目标方法获取其声明类(实现类),然后获取该类实现的接口数组。 4. 遍历接口数组,找到与目标方法同名且参数相同的方法(即接口方法)。 5. 从接口方法上获取自定义注解。 注意:如果目标类实现了多个接口,可能需要更精确的匹配。 代码示例(基于引用[3]的Kotlin代码,改为Java): ```java @Aspect @Component public class MyAspect { @Around("@annotation(com.example.MyAnnotation)") // 这里可以改为扫描接口上的注解?但注意,默认情况下这里扫描的是实现类方法上的注解。所以我们可以先尝试从实现类方法上获取,如果获取不到,再尝试从接口方法上获取。 public Object around(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); // 先尝试从目标方法(实现类方法)上获取注解 MyAnnotation annotation = method.getAnnotation(MyAnnotation.class); if (annotation == null) { // 如果目标方法上没有,尝试从接口方法上获取 Class<?> targetClass = joinPoint.getTarget().getClass(); Class<?>[] interfaces = targetClass.getInterfaces(); for (Class<?> intf : interfaces) { try { Method intfMethod = intf.getMethod(method.getName(), method.getParameterTypes()); annotation = intfMethod.getAnnotation(MyAnnotation.class); if (annotation != null) { break; } } catch (NoSuchMethodException e) { // 忽略,继续下一个接口 } } } if (annotation != null) { // 执行切面逻辑 // ... } return joinPoint.proceed(); } } ``` 配置: - 确保Spring配置了AOP自动代理,并且使用CGLIB代理(因为即使接口上有注解,我们也要通过目标类来获取接口,所以使用CGLIB代理可以覆盖更多情况)。在Spring Boot中,可以通过`@EnableAspectJAutoProxy(proxyTargetClass = true)`来强制使用CGLIB代理。 注意:使用CGLIB代理后,即使目标类实现了接口,也会使用CGLIB代理,这样代理对象就是目标类的子类。这样,我们在切面中通过`joinPoint.getTarget().getClass()`获取的是目标类的实际类型(而不是接口),从而可以获取到目标类实现的接口。 其他注意事项: - 如果接口方法上的注解是`@Inherited`元注解标注的,那么实现类方法会自动继承该注解吗?答案是不会,因为`@Inherited`只对类上的注解有效,对方法上的注解无效。 因此,通过上述方式,我们可以解决接口方法注解无法被直接拦截的问题。 相关问题: 1. 如何强制Spring AOP使用CGLIB代理? 2. Spring AOP中如何获取目标方法接口方法? 3. 除了在切面中手动获取接口方法上的注解,还有哪些方法可以解决这个问题?</think>### Spring AOP 无法拦截接口方法注解的问题及解决方案 #### 问题原因分析 1. **代理机制限制** Spring AOP 默认对**实现类**方法进行拦截: - 若目标对象实现了接口 → 使用 JDK 动态代理(基于接口)[^2] - 若目标对象无接口 → 使用 CGLIB 代理(基于子类) **关键问题**:JDK 动态代理仅拦截实现类方法无法直接获取接口方法上的注解[^4]。 2. **注解继承机制缺失** 方法注解不支持 `@Inherited` 元注解,导致实现类方法**不会自动继承**接口方法上的注解[^1]。 3. **切入点表达式作用域** 标准切入点表达式(如 `@annotation(com.example.CustomAnnotation)`)默认扫描**实现类方法**,不会向上查找接口方法[^3]。 --- #### 解决方案(附代码) ##### 方案1:强制使用 CGLIB 代理 ```java @Configuration @EnableAspectJAutoProxy(proxyTargetClass = true) // 强制使用 CGLIB public class AppConfig {} ``` **优点**: - 代理目标为具体类而非接口 - 可扫描类层级结构中的注解 **限制**: - 无法代理 `final` 类或 `private` 方法[^2] ##### 方案2:在切面中动态查找接口注解 ```java @Aspect @Component public class CustomAspect { @Around("execution(* com.example.service.*.*(..))") public Object checkInterfaceAnnotation(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); // 1. 先在实现类方法查找注解 CustomAnnotation ann = method.getAnnotation(CustomAnnotation.class); // 2. 若未找到,扫描接口方法 if (ann == null) { Class<?>[] interfaces = joinPoint.getTarget().getClass().getInterfaces(); for (Class<?> intf : interfaces) { try { Method intfMethod = intf.getMethod(method.getName(), method.getParameterTypes()); ann = intfMethod.getAnnotation(CustomAnnotation.class); if (ann != null) break; } catch (NoSuchMethodException ignored) {} } } // 3. 执行切面逻辑 if (ann != null) { System.out.println("拦截注解: " + ann.value()); // 执行数据源切换等操作 } return joinPoint.proceed(); } } ``` ##### 方案3:注解下沉到实现类(推荐) ```java // 接口 public interface UserMapper { @DataSource("slave_db") // 接口注解(可能失效) User getById(Long id); } // 实现类 @Repository public class UserMapperImpl implements UserMapper { @Override @DataSource("slave_db") // 在实现类重复添加注解 public User getById(Long id) { ... } } ``` **最佳实践**: - 在接口上使用注解仅作为**文档标记** - 在实现类方法上添加**实际生效的注解**[^1] --- #### 技术原理图 ```mermaid graph TD A[调用接口方法] --> B{代理类型} B -->|JDK 代理| C[仅拦截实现类方法] B -->|CGLIB 代理| D[拦截类层级结构] D --> E[可扫描接口注解] C --> F[需手动查找接口] ``` --- #### 扩展优化建议 1. **组合注解技术** 创建元注解自动继承接口配置: ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface InheritedDataSource { @AliasFor(annotation = DataSource.class) String value(); } ``` 2. **AspectJ 编译时织入** 使用 `aspectj-maven-plugin` 实现编译期织入,绕过动态代理限制。 3. **注解预处理器** 在编译期通过注解处理器 (`AbstractProcessor`) 自动复制接口注解到实现类。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值