如果Spring可以成功地拦截@Configuration类中的类内函数调用,为什么它在常规bean中不支持它呢?

2022-09-02 01:44:47

我最近注意到Spring成功地拦截了@Configuration类中的类内函数调用,而不是常规bean中的函数调用。

像这样的电话

@Repository
public class CustomerDAO {  
    @Transactional(value=TxType.REQUIRED)
    public void saveCustomer() {
        // some DB stuff here...
        saveCustomer2();
    }
    @Transactional(value=TxType.REQUIRES_NEW)
    public void saveCustomer2() {
        // more DB stuff here
    }
}

无法启动新事务,因为当 saveCustomer() 的代码在 CustomerDAO 代理中执行时,saveCustomer2() 的代码在未包装的 CustomerDAO 类中执行,正如我通过在调试器中查看 “this” 所看到的那样,因此 Spring 没有机会拦截对 saveCustomer2 的调用。

但是,在下面的示例中,当 transactionManager() 调用 createDataSource() 时,它被正确截获并调用代理的 createDataSource(),而不是未包装类的 createDataSource(),如在调试器中查看“this”所证明的那样。

@Configuration
public class PersistenceJPAConfig {
    @Bean
    public DriverManagerDataSource createDataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        //dataSource.set ... DB stuff here
        return dataSource;
    }

   @Bean 
       public PlatformTransactionManager transactionManager(   ){
           DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(createDataSource());
           return transactionManager;
       }
}

所以我的问题是,为什么Spring可以在第二个示例中正确拦截类内函数调用,而不是在第一个示例中。它是否使用不同类型的动态代理?

编辑:从这里的答案和其他来源,我现在理解以下内容:@Transactional是使用Spring AOP实现的,其中代理模式是通过用户类的包装/组合来执行的。AOP代理足够通用,因此许多方面可以链接在一起,并且可以是CGLib代理或Java动态代理。

在@Configuration类中,Spring还使用CGLib创建一个增强类,该类继承自用户@Configuration类,并用在调用用户/超级函数之前执行一些额外工作的函数覆盖用户的@Bean函数,例如检查这是否是函数的第一次调用。此类是代理吗?这取决于定义。你可能会说它是一个代理,它使用来自真实对象的继承,而不是使用组合来包装它。

总而言之,从这里给出的答案中,我理解这是两种完全不同的机制。为什么做出这些设计选择是另一个悬而未决的问题。


答案 1

它是否使用不同类型的动态代理?

几乎完全

让我们弄清楚类和AOP代理之间的区别是什么,回答以下问题:@Configuration

  1. 为什么自调用方法没有事务语义,即使Spring能够拦截自调用的方法?@Transactional
  2. 和AOP是如何相关的?@Configuration

为什么自调用方法没有事务语义?@Transactional

简短的回答:

这就是AOP的制作方式。

长答案:

  1. 声明式事务管理依赖于 AOP(对于 Spring AOP 上的大多数 Spring 应用程序)

Spring框架的声明式事务管理通过Spring面向方面的编程(AOP)成为可能。

  1. 它是基于代理的 (§5.8.1.了解 AOP 代理)

Spring AOP是基于代理的。

来自同一段落:SimplePojo.java

public class SimplePojo implements Pojo {

    public void foo() {
        // this next method invocation is a direct call on the 'this' reference
        this.bar();
    }

    public void bar() {
        // some logic...
    }
}

以及代理它的片段:

public class Main {

    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());

        Pojo pojo = (Pojo) factory.getProxy();
        // this is a method call on the proxy!
        pojo.foo();
    }
}

这里要了解的关键是,类方法内的客户端代码具有对代理的引用。main(..)Main

这意味着对该对象引用的方法调用是对代理的调用

因此,代理可以委派给与该特定方法调用相关的所有侦听器(建议)。

但是,一旦调用最终到达目标对象(SimplePojo,在本例中为引用),它可能对自己进行的任何方法调用(例如 this.bar()this.foo())都将针对引用而不是代理进行调用

这具有重要意义。这意味着自调用不会导致与方法调用相关的建议有机会执行。

(强调关键部分。)

您可能会认为aop的工作原理如下:

想象一下,我们有一个想要代理的类:Foo

傅.java

public class Foo {
  public int getInt() {
    return 42;
  }
}

没有什么特别的。只是方法返回getInt42

拦截器:

拦截器.java

public interface Interceptor {
  Object invoke(InterceptingFoo interceptingFoo);
}

LogInterceptor.java(用于演示):

public class LogInterceptor implements Interceptor {
  @Override
  public Object invoke(InterceptingFoo interceptingFoo) {
    System.out.println("log. before");
    try {
      return interceptingFoo.getInt();
    } finally {
      System.out.println("log. after");
    }
  }
}

InvokeTargetInterceptor.java

public class InvokeTargetInterceptor implements Interceptor {
  @Override
  public Object invoke(InterceptingFoo interceptingFoo) {
    try {
      System.out.println("Invoking target");
      Object targetRetVal = interceptingFoo.method.invoke(interceptingFoo.target);
      System.out.println("Target returned " + targetRetVal);
      return targetRetVal;
    } catch (Throwable t) {
      throw new RuntimeException(t);
    } finally {
      System.out.println("Invoked target");
    }
  }
}

最后拦截Foo.java

public class InterceptingFoo extends Foo {
  public Foo target;
  public List<Interceptor> interceptors = new ArrayList<>();
  public int index = 0;
  public Method method;

  @Override
  public int getInt() {
    try {
      Interceptor interceptor = interceptors.get(index++);
      return (Integer) interceptor.invoke(this);
    } finally {
      index--;
    }
  }
}

将所有东西连接在一起:

public static void main(String[] args) throws Throwable {
  Foo target = new Foo();
  InterceptingFoo interceptingFoo = new InterceptingFoo();
  interceptingFoo.method = Foo.class.getDeclaredMethod("getInt");
  interceptingFoo.target = target;
  interceptingFoo.interceptors.add(new LogInterceptor());
  interceptingFoo.interceptors.add(new InvokeTargetInterceptor());

  interceptingFoo.getInt();
  interceptingFoo.getInt();
}

将打印:

log. before
Invoking target
Target returned 42
Invoked target
log. after
log. before
Invoking target
Target returned 42
Invoked target
log. after

现在让我们来看看 ReflectiveMethodInvocation

以下是其方法的一部分:proceed

Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);

++this.currentInterceptorIndex 现在应该看起来很熟悉

您可以尝试在应用程序中引入几个方面,并在调用建议的方法时看到堆栈在profeed方法处增长

最后,一切都在MethodProxy结束。

从它的调用方法javadoc:

在相同类型的不同对象上调用原始方法。

正如我之前提到的文档:

一旦调用最终到达对象,它可能对自己进行的任何方法调用都将针对引用而不是代理进行调用。targetthis

我希望现在或多或少,原因很清楚。

和AOP是如何相关的?@Configuration

答案是它们不相关

所以春天在这里可以自由地做任何它想做的事情。在这里,它不绑定到代理AOP语义。

它使用 ConfigurationClassEnhancer 增强了此类类。

看看:

回到问题

如果Spring可以成功地拦截@Configuration类中的类内函数调用,为什么它在常规bean中不支持它呢?

我希望从技术角度来看,原因很清楚。

现在我从非技术方面的想法:

我认为它没有完成,因为春季AOP在这里的时间足够长了......

自Spring Framework 5以来,Spring WebFlux框架已经推出。

现在Spring团队正在努力增强反应式编程模型

查看一些值得注意的近期博客文章

引入了越来越多的功能,以构建Spring应用程序的代理较少方法。(例如,请参阅此提交

所以我认为,即使有可能做你所描述的事情,它现在还远远不是Spring Team的首要任务。


答案 2

因为 AOP 代理和类具有不同的用途,并且以明显不同的方式实现(即使两者都涉及使用代理)。基本上,AOP使用组合,而@Configuration使用继承@Configuration

AOP 代理

这些工作的方式基本上是它们创建代理,这些代理在将调用委派给原始(代理)对象之前/之后执行相关的建议逻辑。容器注册此代理而不是代理对象本身,因此所有依赖项都设置为此代理,并且从一个 Bean 到另一个 Bean 的所有调用都通过此代理。但是,代理对象本身没有指向代理的指针(它不知道它是代理的,只有代理具有指向目标对象的指针)。因此,该对象中对其他方法的任何调用都不会通过代理。

(我在这里添加这个只是为了与@Configuration形成对比,因为你似乎对这部分有正确的理解。

@Configuration

现在,虽然您通常应用 AOP 代理的对象是应用程序的标准部分,但该类是不同的 - 首先,您可能从未打算直接自己创建该类的任何实例。这个类确实只是编写bean容器配置的一种方式,在Spring之外没有任何意义,你知道Spring将以特殊的方式使用它,并且它具有一些特殊的语义,而不仅仅是普通的Java代码 - 例如-annotated方法实际上定义了Spring beans。@Configuration@Bean

正因为如此,Spring可以对这个类做更激进的事情,而不必担心它会破坏你的代码中的某些东西(记住,你知道你只为Spring提供这个类,你不会直接创建或使用它的实例)。

它实际作用是创建一个代理,该代理是@Configuration类的子类。这样,它可以拦截类的每个(非非)方法的调用,即使在同一个对象中也是如此(因为这些方法实际上都被代理覆盖了,并且Java具有所有方法虚拟)。代理这样做是为了将它识别为(语义上)对Spring Bean的引用的任何方法调用重定向到实际的Bean实例,而不是调用超类方法。finalprivate@Configuration


推荐