Spring AOP源码解析之通知初始化

  这段时间的项目里使用到了Spring aop的相关功能,使用起来还是比较简单的。也知道Spring aop是通过java和cglib建立动态代理的方式实现切面的相关功能。但是实现动态代理的具体过程还是不太清除,为了能够更进一步的了解aop的原理,决定一读aop的源码。
  目前使用Spring aop时大多都是通过注解的方式,那么Spring中就一定有相应的解析器去解析这个注解,在Spring的AopNamespaceHandler类中存在这样一个方法。

@Override
public void init() {
   // In 2.0 XSD as well as in 2.1 XSD.
   registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser());
   registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser());
   registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator());
   // Only in 2.0 XSD: moved to context namespace as of 2.1
   registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
}

总体流程

  init方法将aspectj-autoproxy注解注册到了AspectJAutoProxyBeanDefinitionParser解析器进行解析,在此解析器的parse方法里注册了AnnotationAwareAspectJAutoProxyCreator去完成aop的代理工作,它可以根据@Point注解去自动代理相匹配的bean。下面是AnnotationAwareAspectJAutoProxyCreator的类继承图。
在这里插入图片描述
  从类图中可以看到AnnotationAwareAspectJAutoProxyCreator实现了BeanPostProcessor接口,实现BeanPostProcessor的接口的类当Spring加载这个Bean时会在实例化前调用其postProcessBeforeInitialization方法。AnnotationAwareAspectJAutoProxyCreator的父类AbstractAutoProxyCreator中,这个方法的实现如下

@Override
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;
}
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
   //如果已经处理过直接返回
   if (beanName != null && this.targetSourcedBeans.contains(beanName)) {
      return bean;
   }    
   //如果无需增强直接返回
   if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
      return bean;
   }
   //如果bean是一个基础设施类或者在配置中配置过此bean无需代理,则无需代理直接返回。
   //基础设施类:
   if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
      this.advisedBeans.put(cacheKey, Boolean.FALSE);
      return bean;
   }
   // Create proxy if we have advice. 
   //如果存在通知则创建代理
   Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
   if (specificInterceptors != DO_NOT_PROXY) {
      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;
}

  在wrapIfNecessary方法中,经过一系列的判断,真正创建代理的过程从getAdvicesAndAdvisorsForBean方法开始,下面主要做了两件事情。

  1. 获取通知
  2. 根据通知创建代理

获取通知

getAdvicesAndAdvisorsForBean

  首先看获取通知的方法getAdvicesAndAdvisorsForBean。

@Override
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) {
   //找到所有的候选通知
   List<Advisor> candidateAdvisors = findCandidateAdvisors();
   //在候选通知中找到能应用到这个bean的通知
   List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
   //扩展通知
   extendAdvisors(eligibleAdvisors);
   if (!eligibleAdvisors.isEmpty()) {
      //对通知进行排序
      eligibleAdvisors = sortAdvisors(eligibleAdvisors);
   }
   return eligibleAdvisors;
}

  getAdvicesAndAdvisorsForBean方法主要调用了findEligibleAdvisors方法,findEligibleAdvisors主要分为四步:

  1. 获取所有的候选通知
  2. 从候选通知中找出匹配的通知
  3. 扩展通知
  4. 对通知进行排序
获取所有的候选通知
@Override
protected List<Advisor> findCandidateAdvisors() {
   // 调用父类添加通知,此处只要是添加xml配置的通知,而不是通过注解的通知.
   List<Advisor> advisors = super.findCandidateAdvisors();
   // 添加aspectj注解形式的通知
   advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
   return advisors;
}
public List<Advisor> buildAspectJAdvisors() {
   List<String> aspectNames = this.aspectBeanNames;
   if (aspectNames == null) {
      synchronized (this) {
         aspectNames = this.aspectBeanNames;
         //双重检查降低并发成本,这是一种值得学习的技巧
         if (aspectNames == null) {
            List<Advisor> advisors = new LinkedList<Advisor>();
            aspectNames = new LinkedList<String>();
            //获取Object类型的所有bean的名称
            String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
                  this.beanFactory, Object.class, true, false);
            for (String beanName : beanNames) {
               //校验bean是否符合条件
               if (!isEligibleBean(beanName)) {
                  continue;
               }
               // We must be careful not to instantiate beans eagerly as in this case they
               // would be cached by the Spring container but would not have been weaved.
               //获取对应bean的类型
               Class<?> beanType = this.beanFactory.getType(beanName);
               if (beanType == null) {
                  continue;
               }
               //检查bean是否被Aspectj注解
               if (this.advisorFactory.isAspect(beanType)) {
                  aspectNames.add(beanName);
                  AspectMetadata amd = new AspectMetadata(beanType, beanName);
                  //如果切面实例模型为单例(可配置)
                  if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
                     MetadataAwareAspectInstanceFactory factory =
                           new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
                     //创建通知
                     List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
                     if (this.beanFactory.isSingleton(beanName)) {
                        this.advisorsCache.put(beanName, classAdvisors);
                     }
                     else {
                        this.aspectFactoryCache.put(beanName, factory);
                     }
                     advisors.addAll(classAdvisors);
                  }
                  else {
                     // Per target or per this.
                     if (this.beanFactory.isSingleton(beanName)) {
                        throw new IllegalArgumentException("Bean with name '" + beanName +
                              "' is a singleton, but aspect instantiation model is not singleton");
                     }
                     MetadataAwareAspectInstanceFactory factory =
                           new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
                     this.aspectFactoryCache.put(beanName, factory);
                     advisors.addAll(this.advisorFactory.getAdvisors(factory));
                  }
               }
            }
            this.aspectBeanNames = aspectNames;
            return advisors;
         }
      }
   }
   if (aspectNames.isEmpty()) {
      return Collections.emptyList();
   }
   List<Advisor> advisors = new LinkedList<Advisor>();
   for (String aspectName : aspectNames) {
      List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);
      if (cachedAdvisors != null) {
         advisors.addAll(cachedAdvisors);
      }
      else {
         MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
         advisors.addAll(this.advisorFactory.getAdvisors(factory));
      }
   }
   return advisors;
}

  获取通知的流程可总结如下:

  1. 获取所有在beanFactory中注册的bean的beanName
  2. 遍历所有beanName,找出其中注解有Aspectj的类,进行下一步处理
  3. 进行通知的创建
  4. 将通知加入缓存

  在这四步中最复杂也是最终要的一步就是通知的创建:

@Override
public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) {
   //获取标记为AspectJ的类信息
   Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
   //获取标记为AspectJ的name
   String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName();
   //验证类信息
   validate(aspectClass);
   // We need to wrap the MetadataAwareAspectInstanceFactory with a decorator
   // so that it will only instantiate once.
   MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory =
         new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory);

   List<Advisor> advisors = new LinkedList<Advisor>();
   //注解有AspectJ的类中可能有不止一个通知
   for (Method method : getAdvisorMethods(aspectClass)) {
      //获取通知
      Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName);
      if (advisor != null) {
         advisors.add(advisor);
      }
   }

   // If it's a per target aspect, emit the dummy instantiating aspect.
   if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
      Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory);
      advisors.add(0, instantiationAdvisor);
   }
   // Find introduction fields.
   for (Field field : aspectClass.getDeclaredFields()) {
      Advisor advisor = getDeclareParentsAdvisor(field);
      if (advisor != null) {
         advisors.add(advisor);
      }
   }
   return advisors;
}

  在上述代码中主要干了这么几件事,首先获取了切面类的类信息和类名,对类信息进行了验证,验证其是否是一个切面。之后将aspectInstanceFactory包装为一个LazySingletonAspectInstanceFactoryDecorator以确保通知只会被实例化一次。下面针对切面类除了注解了@Pointcut的方法进行创建通知。

@Override
public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory,
      int declarationOrderInAspect, String aspectName) {
   //对切面类进行验证
   validate(aspectInstanceFactory.getAspectMetadata().getAspectClass());
   //获取切点
   AspectJExpressionPointcut expressionPointcut = getPointcut(
         candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass());
   if (expressionPointcut == null) {
      return null;
   }

   return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod,
         this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
}

  通知的创建主要由getAdvisor方法实现,其中主要包括对切点注解信息的获取以及根据注解信息创建通知。
  首先介绍切点注解信息的获取。getPointcut方法将切点注解信息封装到AspectJExpressionPointcut类型中,当然如果并没有关于切点表达式(Before、 Around、After、 AfterReturning、AfterThrowing、Pointcut)的注解那么直接返回null。

private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class<?> candidateAspectClass) {
   //获取切点注解,即切点表达式
   AspectJAnnotation<?> aspectJAnnotation =
         AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
   if (aspectJAnnotation == null) {
      return null;
   }
   //封装切点为
   AspectJExpressionPointcut ajexp =
         new AspectJExpressionPointcut(candidateAspectClass, new String[0], new Class<?>[0]);
   //提取注解中的切点表达式,并将其传入封装好的切点中
   ajexp.setExpression(aspectJAnnotation.getPointcutExpression());
   ajexp.setBeanFactory(this.beanFactory);
   return ajexp;
}
protected static AspectJAnnotation<?> findAspectJAnnotationOnMethod(Method method) {
   //切点表达式的类型,如果不含有如下几种类型的注解直接返回空
   Class<?>[] classesToLookFor = new Class<?>[] {
         Before.class, Around.class, After.class, AfterReturning.class, AfterThrowing.class, Pointcut.class};
   for (Class<?> c : classesToLookFor) {
      AspectJAnnotation<?> foundAnnotation = findAnnotation(method, (Class<Annotation>) c);
      if (foundAnnotation != null) {
         return foundAnnotation;
      }
   }
   return null;
}

  获取切点注解信息并将其封装之后便是根据切点信息创建通知,通知类型为InstantiationModelAwarePointcutAdvisorImpl。

public InstantiationModelAwarePointcutAdvisorImpl(AspectJExpressionPointcut declaredPointcut,
      Method aspectJAdviceMethod, AspectJAdvisorFactory aspectJAdvisorFactory,
      MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) {

   this.declaredPointcut = declaredPointcut;
   this.declaringClass = aspectJAdviceMethod.getDeclaringClass();
   this.methodName = aspectJAdviceMethod.getName();
   this.parameterTypes = aspectJAdviceMethod.getParameterTypes();
   this.aspectJAdviceMethod = aspectJAdviceMethod;
   this.aspectJAdvisorFactory = aspectJAdvisorFactory;
   this.aspectInstanceFactory = aspectInstanceFactory;
   this.declarationOrder = declarationOrder;
   this.aspectName = aspectName;

   if (aspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
      // Static part of the pointcut is a lazy type.
      Pointcut preInstantiationPointcut = Pointcuts.union(
            aspectInstanceFactory.getAspectMetadata().getPerClausePointcut(), this.declaredPointcut);

      // Make it dynamic: must mutate from pre-instantiation to post-instantiation state.
      // If it's not a dynamic pointcut, it may be optimized out
      // by the Spring AOP infrastructure after the first evaluation.
      this.pointcut = new PerTargetInstantiationModelPointcut(
            this.declaredPointcut, preInstantiationPointcut, aspectInstanceFactory);
      this.lazy = true;
   }
   else {
      // A singleton aspect.
      this.pointcut = this.declaredPointcut;
      this.lazy = false;
      this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut);
   }
}

  在构造函数中,只是简单的将相关信息复制到类的属性中,并完成了对通知的初始化,因为不同的通知是有区别的,比如Before和Around表达式的不同便代表了通知的位置不同,因此对不同的通知进行不同的初始化。

private Advice instantiateAdvice(AspectJExpressionPointcut pcut) {
   return this.aspectJAdvisorFactory.getAdvice(this.aspectJAdviceMethod, pcut,
         this.aspectInstanceFactory, this.declarationOrder, this.aspectName);
}
@Override
public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut,
      MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) {
   Class<?> candidateAspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
   validate(candidateAspectClass);
   AspectJAnnotation<?> aspectJAnnotation =
         AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
   if (aspectJAnnotation == null) {
      return null;
   }
   // If we get here, we know we have an AspectJ method.
   // Check that it's an AspectJ-annotated class
   if (!isAspect(candidateAspectClass)) {
      throw new AopConfigException("Advice must be declared inside an aspect type: " +
            "Offending method '" + candidateAdviceMethod + "' in class [" +
            candidateAspectClass.getName() + "]");
   }
   if (logger.isDebugEnabled()) {
      logger.debug("Found AspectJ method: " + candidateAdviceMethod);
   }
   AbstractAspectJAdvice springAdvice;
   switch (aspectJAnnotation.getAnnotationType()) {
      case AtBefore:
         springAdvice = new AspectJMethodBeforeAdvice(
               candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
         break;
      case AtAfter:
         springAdvice = new AspectJAfterAdvice(
               candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
         break;
      case AtAfterReturning:
         springAdvice = new AspectJAfterReturningAdvice(
               candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
         AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation();
         if (StringUtils.hasText(afterReturningAnnotation.returning())) {
            springAdvice.setReturningName(afterReturningAnnotation.returning());
         }
         break;
      case AtAfterThrowing:
         springAdvice = new AspectJAfterThrowingAdvice(
               candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
         AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation();
         if (StringUtils.hasText(afterThrowingAnnotation.throwing())) {
            springAdvice.setThrowingName(afterThrowingAnnotation.throwing());
         }
         break;
      case AtAround:
         springAdvice = new AspectJAroundAdvice(
               candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
         break;
      case AtPointcut:
         if (logger.isDebugEnabled()) {
            logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'");
         }
         return null;
      default:
         throw new UnsupportedOperationException(
               "Unsupported advice type on method: " + candidateAdviceMethod);
   }
   // Now to configure the advice...
   springAdvice.setAspectName(aspectName);
   springAdvice.setDeclarationOrder(declarationOrder);
   String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod);
   if (argNames != null) {
      springAdvice.setArgumentNamesFromStringArray(argNames);
   }
   springAdvice.calculateArgumentBindings();
   return springAdvice;
}

  从代码中可以看出,Spring会根据不同的注解生成不同的通知器,例如AtBefore便对应AspectJMethodBeforeAdvice,其中封装了前置通知的逻辑。
  重新回到getAdvisors方法中,自此已经获取到了切面类中注解有切面表达式的通知。下面如果获取到的通知不为空并且配置了延迟初始化,那么就需要在通知列表头添加同步实例化通知(不是很理解)。最后是获取DeclareParents注解,DeclareParents主要用于往代理类中添加目标方法通知,其具体实现和普通通知很类似,只是使用DeclareparentsAdvisor对其进行封装。

寻找匹配的通知

  findCandidateAdvisors方法获取到了所有的通知,但并不是所有的通知都需要织入到当前的bean,还需要根据切点表达式挑选出合适的通知。其具体实现在findAdvisorsThatCanApply方法中。

protected List<Advisor> findAdvisorsThatCanApply(
      List<Advisor> candidateAdvisors, Class<?> beanClass, String beanName) {
   ProxyCreationContext.setCurrentProxiedBeanName(beanName);
   try {
      return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);
   }
   finally {
      ProxyCreationContext.setCurrentProxiedBeanName(null);
   }
}
public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {
   if (candidateAdvisors.isEmpty()) {
      return candidateAdvisors;
   }
   List<Advisor> eligibleAdvisors = new LinkedList<Advisor>();
   for (Advisor candidate : candidateAdvisors) {
      //首先引入引介通知
      if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) {
         eligibleAdvisors.add(candidate);
      }
   }
   boolean hasIntroductions = !eligibleAdvisors.isEmpty();
   for (Advisor candidate : candidateAdvisors) {
      //引介通知已经添加了,此处跳过
      if (candidate instanceof IntroductionAdvisor) {
         // already processed
         continue;
      }
      //添加普通通知
      if (canApply(candidate, clazz, hasIntroductions)) {
         eligibleAdvisors.add(candidate);
      }
   }
   return eligibleAdvisors;
}

  findCandidateAdvisors方法会将引介通知和普通通知分开处理,在之前获取所有通知时引介通知和普通通知也是分开获取的,毕竟引介通知和普通通知的处理是不同的。真正将bean和通知进行匹配的地方是canApply方法。

public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) {
   //首先判断是不是引介通知。
   if (advisor instanceof IntroductionAdvisor) {
      return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass);
   }
   //如果是PointcutAdvisor,则将advisor转为PointcutAdvisor,再进行匹配
   else if (advisor instanceof PointcutAdvisor) {
      PointcutAdvisor pca = (PointcutAdvisor) advisor;
      return canApply(pca.getPointcut(), targetClass, hasIntroductions);
   }
   else {
      // It doesn't have a pointcut so we assume it applies.
      return true;
   }
}
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;
   }
   //获取当前bean所有实现的接口以及当前类自己的类信息
   Set<Class<?>> classes = new LinkedHashSet<Class<?>>(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
   classes.add(targetClass);
   for (Class<?> clazz : classes) {
      //获取类/接口的所有方法
      Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
      for (Method method : methods) {
         //判断方法和注解切点表达式是否匹配,只要有匹配的就返回true,说明此通知和当前bean是匹配的
         if ((introductionAwareMethodMatcher != null &&
               introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) ||
               methodMatcher.matches(method, targetClass)) {
            return true;
         }
      }
   }
   return false;
}

  在canApply方法中,会根据注解切点表达式解析出一个MethodMatcher,接着获取当前bean实现的接口以及自身的类信息,只要这些接口/类中有方法和切点表达式相匹配,则返回true。

扩展通知
@Override
protected void extendAdvisors(List<Advisor> candidateAdvisors) {
   AspectJProxyUtils.makeAdvisorChainAspectJCapableIfNecessary(candidateAdvisors);
}
public static boolean makeAdvisorChainAspectJCapableIfNecessary(List<Advisor> advisors) {
   // Don't add advisors to an empty list; may indicate that proxying is just not required
   if (!advisors.isEmpty()) {
      boolean foundAspectJAdvice = false;
      //遍历通知
      for (Advisor advisor : advisors) {
         // Be careful not to get the Advice without a guard, as
         // this might eagerly instantiate a non-singleton AspectJ aspect
         //是否有Aspectj形式的通知
         if (isAspectJAdvice(advisor)) {
            foundAspectJAdvice = true;
         }
      }
      //如果有Aspectj形式的通知则在通知列表头加上ExposeInvocationInterceptor
      if (foundAspectJAdvice && !advisors.contains(ExposeInvocationInterceptor.ADVISOR)) {
         advisors.add(0, ExposeInvocationInterceptor.ADVISOR);
         return true;
      }
   }
   return false;
}

  扩展通知的逻辑比较简单,就是如果有Aspectj形式的通知,就在通知列表头加上一个ExposeInvocationInterceptor。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值