【问题标题】:Self-invocation behaviour in @Configuration vs. @Component classes@Configuration 与 @Component 类中的自调用行为
【发布时间】:2019-11-16 22:32:56
【问题描述】:

我的问题是关于内部方法调用时的 AOP Spring 行为。

@Service
class Service {
    @Transactional
    public void method1() {
        method1();
    }

    @Transactional
    public void method2() {}
}

如果我们从外部调用method1(),method1()会以事务模式执行,但由于它在内部调用method2(),所以method2()内部的代码不会以事务模式执行。

同时,对于配置类,通常我们应该有相同的行为:

@Configuration
class MyConfiguration{
    @Bean
    public Object1 bean1() {
        return new Object1();
    }

    @Bean
    public Object1 bean2() { 
        Object1 b1 = bean1();
        return new Object2(b1);
    }
}

通常如果我理解得很好,从 bean2() 对 bean1() 方法的调用不应该被代理对象拦截,因此,如果我们多次调用 bean1(),我们应该每次都得到不同的对象。

首先,您能否从技术上解释为什么内部调用没有被代理对象拦截,其次检查我对第二个示例的理解是否正确。

【问题讨论】:

  • 您好,有什么反馈意见吗?我花了很多时间为您调试 Spring 并记录我的发现。
  • 抱歉,没有回复您。在你的时间里比你先。其次,我认为我会花时间阅读您的回复,因为您的回答者还有其他讨论要阅读。

标签: spring spring-aop spring-transactions cglib dynamic-proxy


【解决方案1】:

常规弹簧@Components

有关一般 Spring (AOP) 代理或动态代理(JDK、CGLIB)如何正常工作的说明,请参阅my other answer 以及说明性示例代码。先读一下,你就会明白为什么不能通过 Spring AOP 拦截这些类型的代理的自调用。

@Configuration

对于@Configuration 类,它们的工作方式不同。为了避免已经创建的 Spring bean 仅仅因为它们的 @Bean 工厂方法在外部或内部被再次调用,Spring 为它们创建了特殊的 CGLIB 代理。

我的一个配置类如下所示:

package spring.aop;

import org.springframework.context.annotation.*;

@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ApplicationConfig {
  @Bean(name = "myInterfaceWDM")
  public MyInterfaceWithDefaultMethod myInterfaceWithDefaultMethod() {
    MyClassImplementingInterfaceWithDefaultMethod myBean = new MyClassImplementingInterfaceWithDefaultMethod();
    System.out.println("Creating bean: " + myBean);
    return myBean;
  }

  @Bean(name = "myTestBean")
  public Object myTestBean() {
    System.out.println(this);
    myInterfaceWithDefaultMethod();
    myInterfaceWithDefaultMethod();
    return myInterfaceWithDefaultMethod();
  }
}

对应的应用程序如下所示:

package spring.aop;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext appContext = SpringApplication.run(DemoApplication.class, args);
        MyInterfaceWithDefaultMethod myInterfaceWithDefaultMethod =
          (MyInterfaceWithDefaultMethod) appContext.getBean("myInterfaceWDM");
        System.out.println(appContext.getBean("myTestBean"));
    }
}

这个打印(编辑删除我们不想看到的东西):

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.2.RELEASE)

2019-07-07 08:37:55.750  INFO 22656 --- [           main] spring.aop.DemoApplication               : Starting DemoApplication on (...)
(...)
Creating bean: spring.aop.MyClassImplementingInterfaceWithDefaultMethod@7173ae5b
spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a@72456279

在运行应用程序时,方法myInterfaceWithDefaultMethod() 不会被多次调用,即使myTestBean() 中有多次调用。为什么?

如果在myTestBean() 内的myInterfaceWithDefaultMethod() 调用之一上设置断点并让调试器停在那里,您将了解更多信息。然后你可以通过评估代码来检查情况:

System.out.println(this);

spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a@72456279

所以配置类确实是一个 CGLIB 代理。但是它有什么方法呢?

for (Method method: this.getClass().getDeclaredMethods()) {
  System.out.println(method);
}

public final java.lang.Object spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.myTestBean()
public final spring.aop.MyInterfaceWithDefaultMethod spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.myInterfaceWithDefaultMethod()
public final void spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.setBeanFactory(org.springframework.beans.factory.BeanFactory) throws org.springframework.beans.BeansException
final spring.aop.MyInterfaceWithDefaultMethod spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.CGLIB$myInterfaceWithDefaultMethod$1()
public static void spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.CGLIB$SET_THREAD_CALLBACKS(org.springframework.cglib.proxy.Callback[])
public static void spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.CGLIB$SET_STATIC_CALLBACKS(org.springframework.cglib.proxy.Callback[])
public static org.springframework.cglib.proxy.MethodProxy spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.CGLIB$findMethodProxy(org.springframework.cglib.core.Signature)
final void spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.CGLIB$setBeanFactory$6(org.springframework.beans.factory.BeanFactory) throws org.springframework.beans.BeansException
static void spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.CGLIB$STATICHOOK4()
private static final void spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.CGLIB$BIND_CALLBACKS(java.lang.Object)
final java.lang.Object spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.CGLIB$myTestBean$0()
static void spring.aop.ApplicationConfig$$EnhancerBySpringCGLIB$$8b4ed8a.CGLIB$STATICHOOK3()

这看起来有点乱,让我们打印方法名称:

for (Method method: this.getClass().getDeclaredMethods()) {
  System.out.println(method.name);
}

myTestBean
myInterfaceWithDefaultMethod
setBeanFactory
CGLIB$myInterfaceWithDefaultMethod$1
CGLIB$SET_THREAD_CALLBACKS
CGLIB$SET_STATIC_CALLBACKS
CGLIB$findMethodProxy
CGLIB$setBeanFactory$6
CGLIB$STATICHOOK4
CGLIB$BIND_CALLBACKS
CGLIB$myTestBean$0
CGLIB$STATICHOOK3

该代理是否实现了任何接口?

for (Class<?> implementedInterface : this.getClass().getInterfaces()) {
  System.out.println(implementedInterface);
}

interface org.springframework.context.annotation.ConfigurationClassEnhancer$EnhancedConfiguration

好的,有趣。让我们阅读一些 Javadoc。实际上 ConfigurationClassEnhancer 类是包作用域的,所以我们必须阅读 source code 内的 Javadoc:

通过生成 CGLIB 子类来增强配置类,该子类与 Spring 容器交互以尊重 @Bean 方法的 bean 范围语义。 每个这样的@Bean 方法都将在生成的子类中被覆盖,只有在容器实际请求构造一个新实例时才委派给实际的@Bean 方法实现。 否则,调用这样的@Bean方法作为对容器的引用,通过名称获取对应的bean。

内部接口EnhancedConfiguration实际上是公开的,但Javadoc仍然只在source code中:

由所有@Configuration CGLIB 子类实现的标记接口。通过检查候选类是否已经分配给它来促进幂等行为以增强,例如已经增强了。 还扩展了 BeanFactoryAware,因为所有增强的 @Configuration 类都需要访问创建它们的 BeanFactory。

请注意,此接口仅供框架内部使用,但必须保持公开,以允许访问从其他包(即用户代码)生成的子类。

现在,如果我们进入myInterfaceWithDefaultMethod() 电话,我们会看到什么?生成的代理方法调用方法ConfigurationClassEnhancer.BeanMethodInterceptor.intercept(..),该方法的Javadoc 表示:

增强 @Bean 方法以检查提供的 BeanFactory 是否存在此 bean 对象。

在那里你可以看到其余的魔法正在发生,但描述真的超出了这个已经很长的答案的范围。

【讨论】:

  • 在您的第一次讨论中,当您解释 Jdk 代理的工作原理以及为什么我们不能进行内部调用时,在您提供的文档部分中,他们提到可以通过依赖项来改变这种行为在 AopContext 上获取对代理对象的引用。实际上,对于 @Configuration 类,它们以其他方式改变了这种行为。对吗?
  • Spring 手册本身称将您的应用程序类与 Spring AOP 耦合是“可怕的”,因为它确实如此。 AOP 的整体思想是应用程序代码不知道任何增强其行为的方面。我看不到与@Configuration 的联系,因为 Spring 框架在那里做了一些非常特殊的事情,因此我指向了源代码。这是内部行为,普通用户不得使用,因此未公开记录详细信息。我从 Javadoc + 源代码中引用的提示包含了事实。
  • Spring 手册中解释 Spring AOP 的自调用问题的同一部分还告诉您,您可以使用完整的 AspectJ 来避免该问题。 AspectJ 不使用代理,因此没有它们的问题或限制。
猜你喜欢
  • 2019-10-08
  • 1970-01-01
  • 2012-08-07
  • 2020-10-28
  • 1970-01-01
  • 1970-01-01
  • 2011-03-19
  • 1970-01-01
相关资源
最近更新 更多