写点什么

使用 SpringAop 对方法进行增强

发布于: 2 小时前
使用SpringAop对方法进行增强

一、需求

前段时间项目中有一个需求需要生成编码,编码规则中有一个 4 位数字需要顺序生成,从 1 开始,当增加到 10000 的时候回复成 1。考虑到这个是顺序生成的,同时适用于多线程环境,所以就想到使用 redis 存储数据,同时使用 Ression 的 Lock 来实现多线程顺序增加。考虑到代码的可读性以及业务无关性,所以想到使用 Aop 把这个逻辑代码抽出去,利用注解的特点结合业务代码可插拔使用。过程很顺利,但是最终运行发现切面 Aspect 未生效,排查的时候发现是由于忘记了 spring 中代理类的生效规则,所以记录此贴,总结错误以便后面防止出现这些问题。

二、测试代码

2.1 测试的业务逻辑代码

@Componentpublic class MyService{
@MyAnnotation public void serviceA(){ System.out.println("serviceA"); }}
复制代码

2.2 测试的注解

通过这个注解标识该方法是否需要被拦截

@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@Componentpublic @interface MyAnnotation{}
复制代码

2.3 切面

@Component@Aspectpublic class MyAspect{    //定义拦截点,由于我是通过注解标识该方法是否需要被拦截的,所以这里拦截点是方法时候被标注注解MyAnnotation    @Pointcut("@annotation(com.example.demo.MyAnnotation)")    public void pointcut(){    }     //拦截后执行的环绕通知    @Around(value = "pointcut()")    public Object sout(ProceedingJoinPoint point) throws Throwable {        System.out.println("run into aspect!");        //注意这里一定要执行被拦截的方法,否则就会导致只执行了拦截而没有执行原本的业务方法        return point.proceed();    }}
复制代码

2..4 测试方法

@SpringBootApplication@EnableAspectJAutoProxypublic class DemoApplication implements ApplicationContextAware {
static ApplicationContext ctx;
public static void main(String[] args) throws IOException { SpringApplication.run(DemoApplication.class, args); ServiceAinf = (ServiceA) ctx.getBean("serviceA "); inf.ServiceA(); }
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.ctx = applicationContext; }}
复制代码

2.5 测试结果如下

2021-07-27 14:25:02.623  INFO 23356 --- [  restartedMain] com.example.demo.DemoApplication         : Starting DemoApplication using Java 1.8.0_161 on DESKTOP-APRM3G5 with PID 23356 (D:\IDEA-WROKPLACE\demo2\target\classes started by ColpoCharlie in D:\IDEA-WROKPLACE\demo2)2021-07-27 14:25:02.625  INFO 23356 --- [  restartedMain] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default2021-07-27 14:25:02.652  INFO 23356 --- [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable2021-07-27 14:25:03.068  INFO 23356 --- [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 357292021-07-27 14:25:03.079  INFO 23356 --- [  restartedMain] com.example.demo.DemoApplication         : Started DemoApplication in 0.701 seconds (JVM running for 1.239)run into aspect!serviceA
复制代码

2.6 结果分析

可以看到方法确实被拦截下来了,而且执行完切面的 around 方法后,继续执行了原本的方法逻辑,同时也可以很明显的看到这个 ServiceA 的对象 inf 确实是采用了 CGLIB 生成了代理类的子类

三、原理探索

考虑到这是在原本的功能上动态添加了增强功能,所以基本上是采用了动态代理的方式,结合 Spring 的默认使用了 CGLIB 的代理方式,猜测应该是内部使用 CGLIB 生成了一个代理类,之后当所有方法调用的时候就是调用了代理类的增强后的方法

3.1 容器中的代理类是哪一步生成的

1、通过 debug 进入 getBean 的方法内部

//AbstractApplicationContext  //---------------------------------------------------------------------  // Implementation of BeanFactory interface  //---------------------------------------------------------------------  @Override  public Object getBean(String name) throws BeansException {        //判断BeanFactory是否可用    assertBeanFactoryActive();       //通过BeanFactory调用getBean方法获取Bean    return getBeanFactory().getBean(name);  }
复制代码


2、进一步追踪到 AbstractAutowireCapableBeanFactory 的 createBean 方法,这里我们主要看到 resolveBeforeInstantiation 方法和 doCreateBean 方法,这个方法提供一个通过 BeanPostProcessors 的遍历调用生成代理类的途径,本次测试的是通过 doCreateBean 方法创建代理类的。

//AbstractAutowireCapableBeanFactory  //---------------------------------------------------------------------  // Implementation of relevant AbstractBeanFactory template methods  //---------------------------------------------------------------------
/** * Central method of this class: creates a bean instance, * populates the bean instance, applies post-processors, etc. * @see #doCreateBean * 用于创建一个Bean的实例,然后填充这个实例的属性 */ @Override protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
if (logger.isTraceEnabled()) { logger.trace("Creating instance of bean '" + beanName + "'"); } //传递进来的beandefinition,这里是ServiceA对应的beandefinition RootBeanDefinition mbdToUse = mbd;
// Make sure bean class is actually resolved at this point, and // clone the bean definition in case of a dynamically resolved Class // which cannot be stored in the shared merged bean definition. Class<?> resolvedClass = resolveBeanClass(mbd, beanName); if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) { mbdToUse = new RootBeanDefinition(mbd); mbdToUse.setBeanClass(resolvedClass); }
// Prepare method overrides. try { mbdToUse.prepareMethodOverrides(); } catch (BeanDefinitionValidationException ex) { throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(), beanName, "Validation of method overrides failed", ex); }
try { // Give BeanPostProcessors a chance to return a proxy instead of the target bean instance. //提供了通过调用这些BeanPostProcessor的方法,生成目标类的代理类 Object bean = resolveBeforeInstantiation(beanName, mbdToUse); if (bean != null) { return bean; } } catch (Throwable ex) { throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName, "BeanPostProcessor before instantiation of bean failed", ex); }
try { //本次测试的主要生成代理类的方法 Object beanInstance = doCreateBean(beanName, mbdToUse, args); if (logger.isTraceEnabled()) { logger.trace("Finished creating instance of bean '" + beanName + "'"); } return beanInstance; } catch (BeanCreationException | ImplicitlyAppearedSingletonException ex) { // A previously detected exception with proper bean creation context already, // or illegal singleton state to be communicated up to DefaultSingletonBeanRegistry. throw ex; } catch (Throwable ex) { throw new BeanCreationException( mbdToUse.getResourceDescription(), beanName, "Unexpected exception during bean creation", ex); } }
复制代码


3、我们看 resolveBeforeInstantiation 方法的逻辑定义在哪里

//AbstractAutowireCapableBeanFactory/**   * Apply before-instantiation post-processors, resolving whether there is a   * before-instantiation shortcut for the specified bean.   * @param beanName the name of the bean   * @param mbd the bean definition for the bean   * @return the shortcut-determined bean instance, or {@code null} if none   */  @Nullable  protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {    Object bean = null;    if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {      // Make sure bean class is actually resolved at this point.      if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {        Class<?> targetType = determineTargetType(beanName, mbd);        if (targetType != null) {          bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);          if (bean != null) {            bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);          }        }      }      mbd.beforeInstantiationResolved = (bean != null);    }    return bean;  }
复制代码


4、doCreateBean 的内部逻辑是

//AbstractAutowireCapableBeanFactory/**   * Actually create the specified bean. Pre-creation processing has already happened   * at this point, e.g. checking {@code postProcessBeforeInstantiation} callbacks.   * <p>Differentiates between default bean instantiation, use of a   * factory method, and autowiring a constructor.   * @param beanName the name of the bean   * @param mbd the merged bean definition for the bean   * @param args explicit arguments to use for constructor or factory method invocation   * @return a new instance of the bean   * @throws BeanCreationException if the bean could not be created   * @see #instantiateBean   * @see #instantiateUsingFactoryMethod   * @see #autowireConstructor   */  protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)      throws BeanCreationException {
// Instantiate the bean. BeanWrapper instanceWrapper = null; //判断这个的beandifination是不是单例模式的 if (mbd.isSingleton()) { //单例就从factoryBeanInstanceCache中移除 instanceWrapper = this.factoryBeanInstanceCache.remove(beanName); } //假如为空则说明不是单例的或者缓存中不存在 if (instanceWrapper == null) { //创建bean实例 instanceWrapper = createBeanInstance(beanName, mbd, args); } //从生成的包装类中获取被包装的对象 Object bean = instanceWrapper.getWrappedInstance(); //从生成的包装类中获取被包装的对象的类型 Class<?> beanType = instanceWrapper.getWrappedClass(); if (beanType != NullBean.class) { mbd.resolvedTargetType = beanType; }
// Allow post-processors to modify the merged bean definition. //允许BeanDefinitionPostProcessor对BeanDefinition进行修改但是BeanDefinition只能被修改一次 synchronized (mbd.postProcessingLock) { if (!mbd.postProcessed) { try { applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName); } catch (Throwable ex) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Post-processing of merged bean definition failed", ex); } mbd.postProcessed = true; } }
// Eagerly cache singletons to be able to resolve circular references // even when triggered by lifecycle interfaces like BeanFactoryAware. boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { if (logger.isTraceEnabled()) { logger.trace("Eagerly caching bean '" + beanName + "' to allow for resolving potential circular references"); } addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); }
// Initialize the bean instance. Object exposedObject = bean; try { //填充Beandefinition populateBean(beanName, mbd, instanceWrapper); //初始化给定的Bean对象 exposedObject = initializeBean(beanName, exposedObject, mbd); } catch (Throwable ex) { if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) { throw (BeanCreationException) ex; } else { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex); } }
if (earlySingletonExposure) { Object earlySingletonReference = getSingleton(beanName, false); if (earlySingletonReference != null) { if (exposedObject == bean) { exposedObject = earlySingletonReference; } else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { String[] dependentBeans = getDependentBeans(beanName); Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length); for (String dependentBean : dependentBeans) { if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { actualDependentBeans.add(dependentBean); } } if (!actualDependentBeans.isEmpty()) { throw new BeanCurrentlyInCreationException(beanName, "Bean with name '" + beanName + "' has been injected into other beans [" + StringUtils.collectionToCommaDelimitedString(actualDependentBeans) + "] in its raw version as part of a circular reference, but has eventually been " + "wrapped. This means that said other beans do not use the final version of the " + "bean. This is often the result of over-eager type matching - consider using " + "'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example."); } } } }
复制代码


5、初始化方法

//AbstractAutowireCapableBeanFactory  /**   * Initialize the given bean instance, applying factory callbacks   * as well as init methods and bean post processors.   * <p>Called from {@link #createBean} for traditionally defined beans,   * and from {@link #initializeBean} for existing bean instances.     * 这个方法是用于出初始化给定的Bean对象,这个方法会被factory在createBean方法或者initializeBean方法调用   * @param beanName the bean name in the factory (for debugging purposes)   * @param bean the new bean instance we may need to initialize   * @param mbd the bean definition that the bean was created with   * (can be null}, if given an existing bean instance)   * @return the initialized bean instance (potentially wrapped)     *  返回被包装的Bean对象   */  protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {    if (System.getSecurityManager() != null) {      AccessController.doPrivileged((PrivilegedAction<Object>) () -> {        invokeAwareMethods(beanName, bean);        return null;      }, getAccessControlContext());    }    else {      invokeAwareMethods(beanName, bean);    }
Object wrappedBean = bean; if (mbd == null || !mbd.isSynthetic()) { //调用BeanPostProcessor的前置处理器对这个Bean对象进行初始化 wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); }
try { //调用这个Bean对象的构造器方法进行初始化 invokeInitMethods(beanName, wrappedBean, mbd); } catch (Throwable ex) { throw new BeanCreationException( (mbd != null ? mbd.getResourceDescription() : null), beanName, "Invocation of init method failed", ex); } if (mbd == null || !mbd.isSynthetic()) { //调用BeanPostProcessor的后置处理器对这个Bean对象进行初始化 wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName); }
return wrappedBean; }
复制代码

从上方的几个初始化方法的调用顺序可以知道,我们之前记忆的 Bean 的创建流程中显示前置处理器-》初始化方法-》后置处理器这样的而初始化逻辑是这里确定的。而本次生成代理类是根据后置处理器生成的


6、applyBeanPostProcessorsAfterInitialization

//AbstractAutowireCapableBeanFactory
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) throws BeansException {
Object result = existingBean; //可以看到是获取所有的后置处理器然后遍历每个后置处理器,调用后置处理器初始化对象 for (BeanPostProcessor processor : getBeanPostProcessors()) { Object current = processor.postProcessAfterInitialization(result, beanName); if (current == null) { return result; } result = current; } return result; }
复制代码


每一个后置处理器都有自己的后置处理方法,根据 BeanPostProcessor 的实现类的不同分别调用这些后置处理器的方法。当一个后置处理器不重写覆盖默认的 BeanPostProcessor 的后置处理方法 postProcessAfterInitialization,那么这个方法内部是直接返回传入对象的。


这里的生成代理类的后置处理器是 AnnotationAwareAspectJAutoProxyCreator,它的父类是 AbstractAutoProxyCreator,这个类定义了后置处理方法

//AbstractAutoProxyCreator  /**   * Create a proxy with the configured interceptors if the bean is   * identified as one to proxy by the subclass.   * @see #getAdvicesAndAdvisorsForBean   */  @Override  public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {    if (bean != null) {      Object cacheKey = getCacheKey(bean.getClass(), beanName);      if (this.earlyProxyReferences.remove(cacheKey) != bean) {        return wrapIfNecessary(bean, beanName, cacheKey);      }    }    return bean;  }
复制代码


  /**   * Wrap the given bean if necessary, i.e. if it is eligible for being proxied.   * @param bean the raw bean instance   * @param beanName the name of the bean   * @param cacheKey the cache key for metadata access   * 这里有一个疑问这个类的名称是驼峰命名的,也就是serivceA,这样不会在不同包下面有相同的key吗   * @return a proxy wrapping the bean, or the raw bean instance as-is   */  protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {    if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {      return bean;    }    if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {      return 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); //如果这个类有关联到切面,则为这个传进的Bean生成一个代理类并返回 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 方法,该内部有一个 createProxy 方法用于创建代理类后并返回。我们看看我们是否能够拿到这个代理类。

可以看到已经能够获得代理类了,而且尝试通过代理类调用方法已经能够被拦截下来的。

2021-08-03 16:22:58.329  INFO 29984 --- [  restartedMain] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default2021-08-03 16:22:58.354  INFO 29984 --- [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable2021-08-03 16:22:59.046  INFO 29984 --- [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 357292021-08-03 16:22:59.064  INFO 29984 --- [  restartedMain] com.example.demo.DemoApplication         : Started DemoApplication in 1.013 seconds (JVM running for 1.485)run into aspect!ServiceADisconnected from the target VM, address: '127.0.0.1:52801', transport: 'socket'
复制代码


四、容易踩坑的点

4.1、嵌套方法的增强

由于业务的复杂性,可能是多个方法一起互相调用的,所以存在很多方法嵌套的情况,例如事务的嵌套。对于这种方法的嵌套,假如说我希望这些互相调用的方法都需要被拦截到,对于不熟悉代理的人来说大部分人的代码是这样写的。

@Componentpublic class ServiceA {
@MyAnnotation public void ServiceA(){ System.out.println("ServiceA"); ServiceB(); } public void ServiceB(){ System.out.println("ServiceB"); }}
复制代码

或者是这样写的

@Componentpublic class ServiceA {
@MyAnnotation public void ServiceA(){ System.out.println("ServiceA"); ServiceB(); }
@MyAnnotation public void ServiceB(){ System.out.println("ServiceB"); }}
复制代码

我们来看一下这样写是否会有效果的,运行结果如下

2021-08-03 15:15:17.847  INFO 22684 --- [  restartedMain] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default2021-08-03 15:15:17.873  INFO 22684 --- [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable2021-08-03 15:15:18.373  INFO 22684 --- [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 357292021-08-03 15:15:18.384  INFO 22684 --- [  restartedMain] com.example.demo.DemoApplication         : Started DemoApplication in 0.794 seconds (JVM running for 1.252)inf = class com.example.demo.ServiceA$$EnhancerBySpringCGLIB$$b121f309run into aspect!ServiceAServiceBDisconnected from the target VM, address: '127.0.0.1:58960', transport: 'socket'
复制代码

可以看到对于嵌套的方法 SericeB 并没有被拦截到的,所以对于嵌套的那个方法 ServiceB 的调用就没有被增强。

4.2、代理失效原因

我们先来想一下无论是 JDK 的代理还是 CGLIB 的代理过程,都是为被代理的对象生成一个代理对象,而且这个代理对象还持有被代理对象的引用。当通过代理对象去调用方法的时候,基本都是先执行被增强的逻辑代码,然后再调用持有的被代理的对象的应用,通过这个引用去反射调用被代理对象的对应方法的,所以对于这个

inf.ServiceA()
复制代码

调用的而动作来说,实际上在执行完增强逻辑后,还是使用了 ServiceA 对象来调用方法 ServiceA()的,而不是 ServiceAProxy 来调用 ServiceA()的。这就会导致一个问题。我们使用的是 ServiceA.ServiceA(),也就是对于 ServiceA()方法内部调用 ServiceB()方法来说的话,就是 ServiceA.ServiceB(),所以是无法被代理增强的。我们来看看实际上是不是这样的。


1、再进入方法调用前我们可以看到 ServiceA 对象经过 Spring 的处理已经变成了代理对象的,然后通过代理对象调用方法

由于我们的切面是定义了 Before 切面的,所以在进入方法前就应该执行了增强。

2021-08-03 15:27:01.083  INFO 22588 --- [  restartedMain] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default2021-08-03 15:27:01.109  INFO 22588 --- [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable2021-08-03 15:27:01.807  INFO 22588 --- [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 357292021-08-03 15:27:01.826  INFO 22588 --- [  restartedMain] com.example.demo.DemoApplication         : Started DemoApplication in 1.128 seconds (JVM running for 1.623)inf = class com.example.demo.ServiceA$$EnhancerBySpringCGLIB$$87ee8547run into aspect!ServiceA
复制代码

可以看到确实是执行了增强,然后再进入了实际的 ServiceA 方法的调用,我们再仔细看一下这个方法调用的栈帧。



可以看到这里的栈帧是 ServiceA 的栈帧,所以这里的存在于 ServiceA 方法中的方法调用 ServiceB()实际上调用的是 ServiceA 对象的 ServiceB 方法,也就是不是经过代理类调用的,无增强逻辑的方法,所以可以看到即使 ServiceB 方法的上方标注了注解,但实际上没有生效。

4.3、针对以上无法代理的修改方法

既然知道了为啥方法之间的调用代理不生效(由于方法的嵌套方法不是经过代理对象调用的),那么我们只要想办法让嵌套方法也是经过代理对象调用就可以增强嵌套方法的执行。


这里有两方法可以有效避免。


①注入 ServiceA 类对象,通过这个对象来调用嵌套方法的 ServiceB 就可以了

这里的原理是因为在生成 ServiceA 对象的时候,也就是调用 getBean 的方法的时候 Spring 发现这个类是需要被代理的 Java 类,那么内部就会默认(未指定 Spring 代理方式的话)使用 CGLIB 生成代理对象然后把这个对象以 (ServiceA 的对应的 key:ServiceA 对象的代理对象)的形式存储在 BeanFactory 中,当我们使用自动注入 ServiceA 的对象的时候,这里注入的就是 ServiceA 的代理对象的。



②通过 AopContext 类获取存储在 AopContext 中的代理对象

这个方法需要在启动配置类上标注的 @EnableAspectJAutoProxy(exposeProxy = true)注解里面设置 exposeProxy 属性设置为 true,当这里设置为 true 的时候,在生成代理类的时候会把这个代理类存储在 AopContext 中,默认 exposeProxy 是 false,只有设置为真才会把代理类暴露出来的。


可以看到以上两种方法都能够实现嵌套方法的代理。

#五、总结

通过大概的代理对象的生成步骤大概了解了嵌套方法的代理失效的原因,还了解在 Spring 中如何获得一个类的代理对象

1、通过注入获得代理对象

2、通过 AopContext 获得代理对象


存在的疑惑

1、对于代理类的生成步骤不是很熟悉

2、对于代理类如何做方法增强的逻辑不明确

发布于: 2 小时前阅读数: 7
用户头像

还未添加个人签名 2021.04.27 加入

还未添加个人简介

评论

发布
暂无评论
使用SpringAop对方法进行增强