@Configuration与@Component类中的自调用行为 [英] Self-invocation behaviour in @Configuration vs. @Component classes

查看:115
本文介绍了@Configuration与@Component类中的自调用行为的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的问题是在内部方法调用的情况下有关AOP Spring的行为.

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

    @Transactional
    public void method2() {}
}
 

如果我们从外部调用method1(),则method1()将在事务模式下执行,但是由于它在内部调用method2(),因此method2()内部的代码将不会在事务模式下执行.

并行地,对于Configuration类,通常我们应该具有相同的行为:

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

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

通常,如果我理解得很好,代理对象不应拦截从bean2()对bean1()方法的调用,因此,如果我们多次调用bean1(),则每次都应获得不同的对象./p>

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

解决方案

常规Spring @Component s

有关一般情况下正常的Spring(AOP)代理或动态代理(JDK,CGLIB)如何工作的说明,请参见我的其他答案和示例代码.首先阅读该书,您将了解为什么无法通过Spring AOP拦截这些类型的代理的自调用.

@Configuration

对于@Configuration类,它们的工作方式不同.为了避免仅由于在外部或内部再次调用其@Bean工厂方法而再次创建已创建的Spring 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
 

运行应用程序时,即使myTestBean()内部有多个调用,也不会多次调用方法myInterfaceWithDefaultMethod().为什么?

如果在myTestBean()中的myInterfaceWithDefaultMethod()调用之一上放置一个断点并让调试器停在那里,您将学到更多.然后,您可以通过评估代码来检查情况:

 System.out.println(this);

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

因此config类确实是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是包作用域的,因此我们必须在@Service class Service { @Transactional public void method1() { method1(); } @Transactional public void method2() {} }

If we call method1() from outside, method1() will be executed in a transaction mode, but as it calls internally method2(), the code inside method2() will not be executed in a transaction mode.

In parallel, for a Configuration class, normally we should have the same behaviour:

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

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

Normally if i understood well, the call call to bean1() method from bean2() should not be intercepted by the proxy object and hence, if we call bean1() many times, we should get different object every time.

Firstly, could you explains technically why the inner calls are not intercepted by the proxy object, and secondly to check if my understanding of the second example is correct.

解决方案

Regular Spring @Components

For an explanation of how normal Spring (AOP) proxies or dynamic proxies (JDK, CGLIB) in general work, see my other answer with illustrative sample code. Read that first and you will understand why self-invocation cannot be intercepted for these types of proxies via Spring AOP.

@Configuration classes

As for @Configuration classes, they work differently. In order to avoid Spring beans which have already been created from being created again just because their @Bean factory methods are being called again ex- or internally, Spring creates special CGLIB proxies for them.

One of my config classes looks like this:

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

The corresponding application looks like this:

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

This prints (edited to remove stuff we don't want to see):

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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

When running the application, method myInterfaceWithDefaultMethod() is not called multiple times even though there are multiple calls from within myTestBean(). Why?

You learn more if you put a breakpoint onto one of the myInterfaceWithDefaultMethod() calls within myTestBean() and let the debugger stop there. Then you can inspect the situation by evaluating code:

System.out.println(this);

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

So the config class is indeed a CGLIB proxy. But what methods does it have?

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()

This looks kinda messy, let's just print the method names:

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

Does that proxy implement any interfaces?

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

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

Okay, interesting. Let us read some Javadoc. Actually class ConfigurationClassEnhancer is package-scoped, so we have to read the Javadoc right inside the source code:

Enhances Configuration classes by generating a CGLIB subclass which interacts with the Spring container to respect bean scoping semantics for @Bean methods. Each such @Bean method will be overridden in the generated subclass, only delegating to the actual @Bean method implementation if the container actually requests the construction of a new instance. Otherwise, a call to such an @Bean method serves as a reference back to the container, obtaining the corresponding bean by name.

The inner interface EnhancedConfiguration is actually public, but still the Javadoc is again only in the source code:

Marker interface to be implemented by all @Configuration CGLIB subclasses. Facilitates idempotent behavior for enhance through checking to see if candidate classes are already assignable to it, e.g. have already been enhanced. Also extends BeanFactoryAware, as all enhanced @Configuration classes require access to the BeanFactory that created them.

Note that this interface is intended for framework-internal use only, however must remain public in order to allow access to subclasses generated from other packages (i.e. user code).

Now what do we see if we step into the myInterfaceWithDefaultMethod() call? The generated proxy method calls method ConfigurationClassEnhancer.BeanMethodInterceptor.intercept(..) and that method's Javadoc says:

Enhance a @Bean method to check the supplied BeanFactory for the existence of this bean object.

There you can see the rest of the magic happening, but the description would really be out of scope of this already lengthy answer.

I hope this helps.

这篇关于@Configuration与@Component类中的自调用行为的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆