如果你为你的业务对象使用 Spring IoC 容器(ApplicationContext 或 BeanFactory)(你应该这样做!),你要使用 Spring 的 AOP FactoryBean 实现之一。(记住,工厂 Bean 引入了一层中介,让它创建不同类型的对象)。

:::info Spring 的 AOP 支持也使用 factory bean。 :::

在 Spring 中创建 AOP 代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean。这样就可以完全控制 pointcut、任何适用的 advice 以及它们的排序。然而,如果你不需要这样的控制,也有一些更简单的选项是更好的。

基础知识

ProxyFactoryBean 和其他 Spring FactoryBean 的实现一样,引入了一定程度的间接性。如果你定义了一个名为 foo 的 ProxyFactoryBean,那么引用 foo 的对象看到的不是 ProxyFactoryBean 实例本身,而是一个由 ProxyFactoryBean 中 getObject()方法的实现所创建的对象。这个方法创建了一个 AOP 代理,包裹了一个目标对象。

使用 ProxyFactoryBean 或其他 IoC 感知类来创建 AOP 代理的一个最重要的好处是,advice 和 pointcuts 也可以由 IoC 管理。这是一个强大的功能,可以实现其他 AOP 框架难以实现的某些方法。例如,advice 本身可以引用应用对象(除了目标,任何 AOP 框架都应该有),受益于依赖注入提供的所有可插拔性。

JavaBean 属性

与 Spring 提供的大多数 FactoryBean 实现一样,ProxyFactoryBean 类本身就是一个 JavaBean。它的属性被用来:

一些关键属性是从 org.springframework.aop.framework.ProxyConfig(Spring 中所有 AOP 代理工厂的超类)继承的。这些关键属性包括以下内容:

  • proxyTargetClass:如果要代理的是目标类,而不是目标类的接口,则为 true。如果这个属性值被设置为 true,那么就会创建 CGLIB代理(但也请看基于 JDK 和 CGLIB 的代理)。
  • optimize:控制是否对通过 CGLIB 创建的代理进行积极的优化。你不应该轻率地使用这个设置,除非你完全了解相关的 AOP 代理如何处理优化。目前这只用于 CGLIB 代理。它对 JDK 动态代理没有影响。
  • frozen:如果一个代理配置被冻结,就不再允许对配置进行更改。这既是一种轻微的优化,也适用于那些不希望调用者在代理创建后能够操作代理(通过 Advised 接口)的情况。这个属性的默认值是 false,所以允许改变(比如添加额外的 advice)。
  • exposeProxy:确定当前代理是否应在 ThreadLocal 中暴露,以便它能被目标访问。如果目标需要获得代理,并且 exposeProxy 属性被设置为 true,那么目标可以使用 AopContext.currentProxy()方法。

ProxyFactoryBean 的其他特定属性包括如下:

  • proxyInterfaces: 一个字符串接口名称的数组。如果不提供这个,就会使用目标类的 CGLIB 代理(但也请看基于 JDK 和 CGLIB 的代理)。
  • interceptorNames:

一个由顾问、拦截器或其他 advice 名称组成的字符串数组,以便应用。排序是很重要的,以先来后到为原则。也就是说,列表中的第一个拦截器是第一个能够拦截调用的。
这些名字是当前工厂的 Bean 名称,包括来自祖先工厂的 Bean 名称。你不能在这里提到 Bean 引用,因为这样做会导致 ProxyFactoryBean 忽略 advice 的单例设置。
你可以在拦截器的名字后面加上星号(*)。这样做的结果是应用所有顾问 bean,其名称以星号前的部分开始,将被应用。你可以在 使用全局顾问 中找到使用这一功能的例子。

  • singleton:无论 getObject()方法被调用多少次,工厂是否应该返回一个单一对象。一些 FactoryBean 的实现提供了这样的方法。默认值是 true。如果你想使用有状态的 advice —例如,对于有状态的混合器—请使用单例 advice,同时使用 false 的 singleton 值。

    基于 JDK 和 CGLIB 的代理

本节是关于 ProxyFactoryBean 如何选择为特定目标对象(要代理的对象)创建基于 JDK 的代理或基于 CGLIB 的代理的权威文档。

:::info ProxyFactoryBean 在创建基于 JDK 或 CGLIB 的代理方面的行为在 Spring 的 1.2.x 和 2.0 版本之间有所改变。现在 ProxyFactoryBean 在自动检测接口方面表现出与 TransactionProxyFactoryBean 类类似的语义。 :::

如果要代理的目标对象的类(以下简称目标类)没有实现任何接口,就会创建一个基于 CGLIB 的代理。这是最简单的情况,因为 JDK 代理是基于接口的,而没有接口意味着 JDK 代理甚至不可能。你可以插入目标 Bean,并通过设置 interceptorNames属性指定拦截器列表。注意,即使 ProxyFactoryBean 的 proxyTargetClass 属性被设置为 false,也会创建基于 CGLIB 的代理。(这样做没有意义,最好从 Bean 定义中删除,因为它最好是多余的,最糟糕的是会引起混淆)。

如果目标类实现了一个(或多个)接口,创建的代理类型取决于 ProxyFactoryBean 的配置。

如果 ProxyFactoryBean 的 proxyTargetClass 属性被设置为 true,就会创建一个基于 CGLIB 的代理。这是有道理的,并且符合最小惊喜的原则。即使 ProxyFactoryBean 的 proxyInterfaces 属性被设置为一个或多个完全合格的接口名称,proxyTargetClass 属性被设置为 true 这一事实也会导致基于 CGLIB 的代理的生效。

如果 ProxyFactoryBean 的 proxyInterfaces 属性被设置为一个或多个完全合格的接口名称,就会创建一个基于 JDK 的代理。创建的代理实现了在 proxyInterfaces 属性中指定的所有接口。如果目标类恰好实现了比 proxyInterfaces 属性中指定的更多的接口,那就好办了,但这些额外的接口不会被返回的代理所实现。

如果 ProxyFactoryBean 的 proxyInterfaces 属性没有被设置,但目标类确实实现了一个(或多个)接口,ProxyFactoryBean 就会自动检测目标类确实实现了至少一个接口这一事实,并创建一个基于 JDK 的代理。实际上被代理的接口是目标类实现的所有接口。实际上,这与向 proxyInterfaces 属性提供目标类实现的每一个接口的列表是一样的。然而,这大大减少了工作量,也不容易出现排版错误。

代理接口

考虑一下 ProxyFactoryBean 在行动中的一个简单例子。这个例子涉及到:

  • 一个被代理的目标 Bean。这就是例子中的 personTarget Bean 定义。
  • 一个顾问和一个拦截器,用于提供 advice。
  • 一个 AOP 代理 Bean 定义,用来指定目标对象(personTarget Bean),代理的接口,以及应用的 advice。
  1. <bean id="personTarget" class="com.mycompany.PersonImpl">
  2. <property name="name" value="Tony"/>
  3. <property name="age" value="51"/>
  4. </bean>
  5. <bean id="myAdvisor" class="com.mycompany.MyAdvisor">
  6. <property name="someProperty" value="Custom string property value"/>
  7. </bean>
  8. <bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor">
  9. </bean>
  10. <bean id="person"
  11. class="org.springframework.aop.framework.ProxyFactoryBean">
  12. <property name="proxyInterfaces" value="com.mycompany.Person"/>
  13. <property name="target" ref="personTarget"/>
  14. <property name="interceptorNames">
  15. <list>
  16. <value>myAdvisor</value>
  17. <value>debugInterceptor</value>
  18. </list>
  19. </property>
  20. </bean>

请注意,interceptorNames 属性接收一个 String 列表,其中保存了当前工厂中的拦截器或顾问的 bean 名称。你可以使用顾问、拦截器、之前、之后返回和抛出的 advice 对象。顾问的排序是很重要的。

:::info 你可能想知道为什么列表中没有保留 Bean 的引用。原因是,如果 ProxyFactoryBean 的 singleton 属性被设置为 false,它必须能够返回独立的代理实例。如果任何一个顾问本身就是一个 多例,那么就需要返回一个独立的实例,所以必须能够从工厂获得一个 多例 的实例。持
有一个引用是不够的。 :::

前面显示的 person bean 定义可以用来代替 Person 的实现,如下所示:

  1. Person person = (Person) factory.getBean("person");

同一 IoC 上下文中的其他 Bean 可以表达对它的强类型依赖,就像对普通 Java 对象一样。下面的例子展示了如何做到这一点:

  1. <bean id="personUser" class="com.mycompany.PersonUser">
  2. <property name="person"><ref bean="person"/></property>
  3. </bean>

本例中的 PersonUser 类暴露了一个 Person 类型的属性。就它而言,AOP 代理可以透明地用来代替 「真正的」Persion 的实现。然而,它的类将是一个动态代理类。有可能把它投到 Advised 接口(后面讨论)。

你可以通过使用一个匿名的内部 Bean 来掩盖目标和代理之间的区别。只有 ProxyFactoryBean 的定义是不同的。包括这个 advice 只是为了完整。下面的例子展示了如何使用一个匿名的内部 Bean:

  1. <bean id="myAdvisor" class="com.mycompany.MyAdvisor">
  2. <property name="someProperty" value="Custom string property value"/>
  3. </bean>
  4. <bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/>
  5. <bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
  6. <property name="proxyInterfaces" value="com.mycompany.Person"/>
  7. <!-- Use inner bean, not local reference to target -->
  8. <property name="target">
  9. <bean class="com.mycompany.PersonImpl">
  10. <property name="name" value="Tony"/>
  11. <property name="age" value="51"/>
  12. </bean>
  13. </property>
  14. <property name="interceptorNames">
  15. <list>
  16. <value>myAdvisor</value>
  17. <value>debugInterceptor</value>
  18. </list>
  19. </property>
  20. </bean>

使用匿名内部 Bean 的好处是只有一个 Person 类型的对象。如果我们想防止应用程序上下文的用户获得对未被 advice 的对象的引用,或者需要避免与 Spring IoC autowiring 的任何歧义,这就很有用。可以说,还有一个好处是 ProxyFactoryBean 的定义是自成一体的。然而,有时能够从工厂中获得未被 advice 的目标实际上是一种优势(例如,在某些测试场景中)。

例子

这是一个用编程方式写的上面的例子,因为上面文档首次看的话,看完后,不一定知道如何写代码,下面就是一个例子

定义目标类的接口

  1. package cn.mrcode.study.springdocsread.aspect.persion;
  2. /**
  3. * @author mrcode
  4. */
  5. public interface Person {
  6. String getName();
  7. void setName(String name);
  8. Integer getAge();
  9. void setAge(Integer age);
  10. }

编写一个目标类,并实现这个接口

  1. package cn.mrcode.study.springdocsread.aspect.persion;
  2. /**
  3. * @author mrcode
  4. */
  5. public class PersonImpl implements Person {
  6. private String name;
  7. private Integer age;
  8. public String getName() {
  9. return name;
  10. }
  11. public void setName(String name) {
  12. this.name = name;
  13. }
  14. public Integer getAge() {
  15. return age;
  16. }
  17. public void setAge(Integer age) {
  18. this.age = age;
  19. }
  20. }

编写一个 advisor,这里继承了 DefaultPointcutAdvisor 这个顾问类,它的默认构造就是拦截所有的方法

  1. package cn.mrcode.study.springdocsread.aspect.persion;
  2. import org.springframework.aop.support.DefaultPointcutAdvisor;
  3. /**
  4. * 顾问:包含 切入点 与 切面的类
  5. *
  6. * @author mrcode
  7. */
  8. public class MyAdvisor extends DefaultPointcutAdvisor {
  9. private String someProperty;
  10. public String getSomeProperty() {
  11. return someProperty;
  12. }
  13. public void setSomeProperty(String someProperty) {
  14. this.someProperty = someProperty;
  15. }
  16. }

进行 bean 定义

  1. package cn.mrcode.study.springdocsread.web;
  2. import org.aopalliance.intercept.MethodInterceptor;
  3. import org.aopalliance.intercept.MethodInvocation;
  4. import org.springframework.aop.framework.ProxyFactoryBean;
  5. import org.springframework.aop.interceptor.DebugInterceptor;
  6. import org.springframework.context.annotation.Bean;
  7. import org.springframework.context.annotation.Configuration;
  8. import cn.mrcode.study.springdocsread.aspect.persion.MyAdvisor;
  9. import cn.mrcode.study.springdocsread.aspect.persion.Person;
  10. import cn.mrcode.study.springdocsread.aspect.persion.PersonImpl;
  11. /**
  12. * @author mrcode
  13. */
  14. @Configuration
  15. public class AppConfig {
  16. /**
  17. * 配置 ProxyFactoryBean
  18. *
  19. * @return
  20. * @throws ClassNotFoundException
  21. */
  22. @Bean
  23. public ProxyFactoryBean person() throws ClassNotFoundException {
  24. final ProxyFactoryBean factoryBean = new ProxyFactoryBean();
  25. // 配置目标类接口,也就是说生成的代理会对这个接口中的方法进行代理
  26. factoryBean.setProxyInterfaces(new Class<?>[]{Person.class});
  27. // 设置目标类的 bean 引用
  28. factoryBean.setTargetName("personTarget");
  29. // 设置拦截器的引用
  30. factoryBean.setInterceptorNames("myAdvisor", "debugInterceptor");
  31. return factoryBean;
  32. }
  33. /**
  34. * 目标类
  35. *
  36. * @return
  37. */
  38. @Bean
  39. public PersonImpl personTarget() {
  40. final PersonImpl person = new PersonImpl();
  41. person.setName("Tony");
  42. person.setAge(51);
  43. return person;
  44. }
  45. /**
  46. * 配置一个顾问
  47. *
  48. * @return
  49. */
  50. @Bean
  51. public MyAdvisor myAdvisor() {
  52. // 该顾问没有设置 pointcut,默认就是拦截所有的方法
  53. final MyAdvisor myAdvisor = new MyAdvisor();
  54. myAdvisor.setSomeProperty("Custom string property value");
  55. // 给该顾问配置一个拦截器,这里使用了方法拦截器,每个方法被调用的时候都会进入这个 advice
  56. myAdvisor.setAdvice(new MethodInterceptor() {
  57. @Override
  58. public Object invoke(MethodInvocation invocation) throws Throwable {
  59. System.out.println("自定义方法拦截");
  60. return invocation.proceed();
  61. }
  62. });
  63. return myAdvisor;
  64. }
  65. /**
  66. * 一个显示方法被调用了多少次的拦截器,它其实是一个 MethodInterceptor 实现
  67. *
  68. * @return
  69. */
  70. @Bean
  71. public DebugInterceptor debugInterceptor() {
  72. return new DebugInterceptor();
  73. }
  74. }

启动容器进行测试

  1. package cn.mrcode.study.springdocsread;
  2. import org.springframework.aop.framework.ProxyFactoryBean;
  3. import org.springframework.aop.interceptor.DebugInterceptor;
  4. import org.springframework.context.annotation.AnnotationConfigApplicationContext;
  5. import cn.mrcode.study.springdocsread.aspect.persion.Person;
  6. import cn.mrcode.study.springdocsread.aspect.persion.PersonImpl;
  7. import cn.mrcode.study.springdocsread.web.AppConfig;
  8. /**
  9. * @author zhuqiang
  10. * @date 2022/2/10 11:29
  11. */
  12. public class TestDemo {
  13. public static void main(String[] args) throws InterruptedException {
  14. final AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
  15. // 获取 person bean,这个获取到的是一个 jdk 动态代理对象
  16. Person person = (Person) ctx.getBean("person");
  17. person.getName();
  18. person.setAge(1);
  19. System.out.println("getBean(\"person\"):" + person);
  20. // 不能这样写,因为容器中有 2 个实现,一个是上面的代理实现,一个是 personTarget
  21. // final Person bean1 = ctx.getBean(Person.class);
  22. // System.out.println("getBean(Person.class):" + bean1);
  23. // 这样获取的话,不是代理对象,而是原始的普通对象
  24. // 因为:上面配置的代理 bean,使用的是 JDK 动态对象,只对 person 接口进行实现,然后对 PersonImpl 这个目标类进行增强处理
  25. final Person bean1 = ctx.getBean(PersonImpl.class);
  26. System.out.println("getBean(PersonImpl.class):" + bean1);
  27. // 获取到这个 ProxyFactoryBean 本身的对象,而不是 bean 对象
  28. final ProxyFactoryBean proxyFactoryBean = (ProxyFactoryBean) ctx.getBean("&person");
  29. System.out.println("getBean(\"&person\"):" + proxyFactoryBean);
  30. // 获取拦截器的对象
  31. final DebugInterceptor bean = ctx.getBean(DebugInterceptor.class);
  32. // 打印方法被调用的次数
  33. System.out.println("目标类的方法被调用了几次:" + bean.getCount());
  34. }
  35. }

测试输出

  1. 自定义方法拦截
  2. 自定义方法拦截
  3. 自定义方法拦截
  4. getBean("person"):cn.mrcode.study.springdocsread.aspect.persion.PersonImpl@16150369
  5. getBean(PersonImpl.class):cn.mrcode.study.springdocsread.aspect.persion.PersonImpl@16150369
  6. getBean("&person"):org.springframework.aop.framework.ProxyFactoryBean: 1 interfaces [cn.mrcode.study.springdocsread.aspect.persion.Person]; 2 advisors [cn.mrcode.study.springdocsread.aspect.persion.MyAdvisor: pointcut [Pointcut.TRUE]; advice [cn.mrcode.study.springdocsread.web.AppConfig$1@821330f], org.springframework.aop.support.DefaultPointcutAdvisor: pointcut [Pointcut.TRUE]; advice [org.springframework.aop.interceptor.DebugInterceptor@6f43c82]]; targetSource [SingletonTargetSource for target object [cn.mrcode.study.springdocsread.aspect.persion.PersonImpl@16150369]]; proxyTargetClass=false; optimize=false; opaque=false; exposeProxy=false; frozen=false
  7. 目标类的方法被调用了几次:3

为什么方法被拦截了 3 次?明明只调用了两次(person.getName()person.setAge(1)), 其实还有一次就是,在控制台输出的时候,调用了一次 toString 方法;

上面看着输出的地址都是 PersonImpl@16150369,其实他们不是同一个对象
image.png
image.png
打印出来的是 Object 的 toString 里面定义的类名和 hashCode 地址,不是内存地址,至于为什么代理里面能打印出目标类里面一样的 hashCode 我这里就搞不明白了

  1. getClass().getName() + "@" + Integer.toHexString(hashCode());

代理类

如果你需要代理一个类,而不是一个或多个接口怎么办?

想象一下,在我们之前的例子中,如果并没有 Person 接口。我们需要 advice 一个名为 Person 的类,它没有实现任何业务接口。在这种情况下,你可以将 Spring 配置为使用 CGLIB 代理而不是动态代理。要做到这一点,将前面所示的 ProxyFactoryBean 上的 proxyTargetClass 属性设置为 true。虽然最好是对接口而不是类进行编程,但在处理遗留代码时,为没有实现接口的类提供 advice 的能力还是很有用的。(一般来说,Spring 不是规定性的。虽然它使应用良好的实践变得很容易,但它避免了强迫一种特定的方法)。

  1. @Bean
  2. public ProxyFactoryBean person() throws ClassNotFoundException {
  3. final ProxyFactoryBean factoryBean = new ProxyFactoryBean();
  4. // 配置目标类接口,也就是说生成的代理会对这个接口中的方法进行代理
  5. factoryBean.setProxyInterfaces(new Class<?>[]{Person.class});
  6. // 设置目标类的 bean 引用
  7. factoryBean.setTargetName("personTarget");
  8. // 强制创建 CGLIB 代理
  9. factoryBean.setProxyTargetClass(true);
  10. // 设置拦截器的引用
  11. factoryBean.setInterceptorNames("myAdvisor", "debugInterceptor");
  12. return factoryBean;
  13. }

image.png
可以看到,强制使用 CGLIB 代理之后,返回的代理类是可以强转为 PersonImpl 的。

如果你愿意,你可以在任何情况下强制使用 CGLIB,即使你确实有接口。

CGLIB 代理的工作方式是在 运行时生成一个目标类的子类。Spring 对这个生成的子类进行配置,将方法调用委托给原始目标。该子类被用来实现 Decorator 模式,在 advice 中编织。

CGLIB 代理通常对用户来说是透明的。然而,有一些问题需要考虑:

  • final的方法不能被 advice ,因为它们不能被重写。
  • 没有必要将 CGLIB 添加到你的 classpath 中。从 Spring 3.2 开始,CGLIB 被重新打包并包含在 spring-core JAR 中。换句话说,基于 CGLIB 的 AOP 「开箱即用」,正如 JDK 动态代理一样。

CGLIB 代理和动态代理之间的性能差异很小。在这种情况下,性能不应该是一个决定性的考虑。

Using「Global」Advisors / 使用全局顾问

通过在拦截器名称上附加星号,所有 bean 名称与星号前的部分相匹配的顾问都会被添加到顾问链中。如果你需要添加一套标准的 「全局」顾问,这就很方便了。下面的例子定义了两个全局顾问:

  1. <bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
  2. <property name="target" ref="service"/>
  3. <property name="interceptorNames">
  4. <list>
  5. <value>global*</value>
  6. </list>
  7. </property>
  8. </bean>
  9. <bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>
  10. <bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/>