如果你为你的业务对象使用 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。它的属性被用来:
- 指定你要代理的目标。
- 指定是否使用 CGLIB(稍后描述,另见基于 JDK 和 CGLIB 的代理)。
一些关键属性是从 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。
<bean id="personTarget" class="com.mycompany.PersonImpl">
<property name="name" value="Tony"/>
<property name="age" value="51"/>
</bean>
<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
<property name="someProperty" value="Custom string property value"/>
</bean>
<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor">
</bean>
<bean id="person"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="com.mycompany.Person"/>
<property name="target" ref="personTarget"/>
<property name="interceptorNames">
<list>
<value>myAdvisor</value>
<value>debugInterceptor</value>
</list>
</property>
</bean>
请注意,interceptorNames 属性接收一个 String 列表,其中保存了当前工厂中的拦截器或顾问的 bean 名称。你可以使用顾问、拦截器、之前、之后返回和抛出的 advice 对象。顾问的排序是很重要的。
:::info
你可能想知道为什么列表中没有保留 Bean 的引用。原因是,如果 ProxyFactoryBean 的 singleton 属性被设置为 false,它必须能够返回独立的代理实例。如果任何一个顾问本身就是一个 多例,那么就需要返回一个独立的实例,所以必须能够从工厂获得一个 多例 的实例。持
有一个引用是不够的。
:::
前面显示的 person bean 定义可以用来代替 Person 的实现,如下所示:
Person person = (Person) factory.getBean("person");
同一 IoC 上下文中的其他 Bean 可以表达对它的强类型依赖,就像对普通 Java 对象一样。下面的例子展示了如何做到这一点:
<bean id="personUser" class="com.mycompany.PersonUser">
<property name="person"><ref bean="person"/></property>
</bean>
本例中的 PersonUser 类暴露了一个 Person 类型的属性。就它而言,AOP 代理可以透明地用来代替 「真正的」Persion 的实现。然而,它的类将是一个动态代理类。有可能把它投到 Advised 接口(后面讨论)。
你可以通过使用一个匿名的内部 Bean 来掩盖目标和代理之间的区别。只有 ProxyFactoryBean 的定义是不同的。包括这个 advice 只是为了完整。下面的例子展示了如何使用一个匿名的内部 Bean:
<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
<property name="someProperty" value="Custom string property value"/>
</bean>
<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="com.mycompany.Person"/>
<!-- Use inner bean, not local reference to target -->
<property name="target">
<bean class="com.mycompany.PersonImpl">
<property name="name" value="Tony"/>
<property name="age" value="51"/>
</bean>
</property>
<property name="interceptorNames">
<list>
<value>myAdvisor</value>
<value>debugInterceptor</value>
</list>
</property>
</bean>
使用匿名内部 Bean 的好处是只有一个 Person 类型的对象。如果我们想防止应用程序上下文的用户获得对未被 advice 的对象的引用,或者需要避免与 Spring IoC autowiring 的任何歧义,这就很有用。可以说,还有一个好处是 ProxyFactoryBean 的定义是自成一体的。然而,有时能够从工厂中获得未被 advice 的目标实际上是一种优势(例如,在某些测试场景中)。
例子
这是一个用编程方式写的上面的例子,因为上面文档首次看的话,看完后,不一定知道如何写代码,下面就是一个例子
定义目标类的接口
package cn.mrcode.study.springdocsread.aspect.persion;
/**
* @author mrcode
*/
public interface Person {
String getName();
void setName(String name);
Integer getAge();
void setAge(Integer age);
}
编写一个目标类,并实现这个接口
package cn.mrcode.study.springdocsread.aspect.persion;
/**
* @author mrcode
*/
public class PersonImpl implements Person {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
编写一个 advisor,这里继承了 DefaultPointcutAdvisor 这个顾问类,它的默认构造就是拦截所有的方法
package cn.mrcode.study.springdocsread.aspect.persion;
import org.springframework.aop.support.DefaultPointcutAdvisor;
/**
* 顾问:包含 切入点 与 切面的类
*
* @author mrcode
*/
public class MyAdvisor extends DefaultPointcutAdvisor {
private String someProperty;
public String getSomeProperty() {
return someProperty;
}
public void setSomeProperty(String someProperty) {
this.someProperty = someProperty;
}
}
进行 bean 定义
package cn.mrcode.study.springdocsread.web;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.aop.interceptor.DebugInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import cn.mrcode.study.springdocsread.aspect.persion.MyAdvisor;
import cn.mrcode.study.springdocsread.aspect.persion.Person;
import cn.mrcode.study.springdocsread.aspect.persion.PersonImpl;
/**
* @author mrcode
*/
@Configuration
public class AppConfig {
/**
* 配置 ProxyFactoryBean
*
* @return
* @throws ClassNotFoundException
*/
@Bean
public ProxyFactoryBean person() throws ClassNotFoundException {
final ProxyFactoryBean factoryBean = new ProxyFactoryBean();
// 配置目标类接口,也就是说生成的代理会对这个接口中的方法进行代理
factoryBean.setProxyInterfaces(new Class<?>[]{Person.class});
// 设置目标类的 bean 引用
factoryBean.setTargetName("personTarget");
// 设置拦截器的引用
factoryBean.setInterceptorNames("myAdvisor", "debugInterceptor");
return factoryBean;
}
/**
* 目标类
*
* @return
*/
@Bean
public PersonImpl personTarget() {
final PersonImpl person = new PersonImpl();
person.setName("Tony");
person.setAge(51);
return person;
}
/**
* 配置一个顾问
*
* @return
*/
@Bean
public MyAdvisor myAdvisor() {
// 该顾问没有设置 pointcut,默认就是拦截所有的方法
final MyAdvisor myAdvisor = new MyAdvisor();
myAdvisor.setSomeProperty("Custom string property value");
// 给该顾问配置一个拦截器,这里使用了方法拦截器,每个方法被调用的时候都会进入这个 advice
myAdvisor.setAdvice(new MethodInterceptor() {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("自定义方法拦截");
return invocation.proceed();
}
});
return myAdvisor;
}
/**
* 一个显示方法被调用了多少次的拦截器,它其实是一个 MethodInterceptor 实现
*
* @return
*/
@Bean
public DebugInterceptor debugInterceptor() {
return new DebugInterceptor();
}
}
启动容器进行测试
package cn.mrcode.study.springdocsread;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.aop.interceptor.DebugInterceptor;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import cn.mrcode.study.springdocsread.aspect.persion.Person;
import cn.mrcode.study.springdocsread.aspect.persion.PersonImpl;
import cn.mrcode.study.springdocsread.web.AppConfig;
/**
* @author zhuqiang
* @date 2022/2/10 11:29
*/
public class TestDemo {
public static void main(String[] args) throws InterruptedException {
final AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
// 获取 person bean,这个获取到的是一个 jdk 动态代理对象
Person person = (Person) ctx.getBean("person");
person.getName();
person.setAge(1);
System.out.println("getBean(\"person\"):" + person);
// 不能这样写,因为容器中有 2 个实现,一个是上面的代理实现,一个是 personTarget
// final Person bean1 = ctx.getBean(Person.class);
// System.out.println("getBean(Person.class):" + bean1);
// 这样获取的话,不是代理对象,而是原始的普通对象
// 因为:上面配置的代理 bean,使用的是 JDK 动态对象,只对 person 接口进行实现,然后对 PersonImpl 这个目标类进行增强处理
final Person bean1 = ctx.getBean(PersonImpl.class);
System.out.println("getBean(PersonImpl.class):" + bean1);
// 获取到这个 ProxyFactoryBean 本身的对象,而不是 bean 对象
final ProxyFactoryBean proxyFactoryBean = (ProxyFactoryBean) ctx.getBean("&person");
System.out.println("getBean(\"&person\"):" + proxyFactoryBean);
// 获取拦截器的对象
final DebugInterceptor bean = ctx.getBean(DebugInterceptor.class);
// 打印方法被调用的次数
System.out.println("目标类的方法被调用了几次:" + bean.getCount());
}
}
测试输出
自定义方法拦截
自定义方法拦截
自定义方法拦截
getBean("person"):cn.mrcode.study.springdocsread.aspect.persion.PersonImpl@16150369
getBean(PersonImpl.class):cn.mrcode.study.springdocsread.aspect.persion.PersonImpl@16150369
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
目标类的方法被调用了几次:3
为什么方法被拦截了 3 次?明明只调用了两次(person.getName()
、person.setAge(1)
), 其实还有一次就是,在控制台输出的时候,调用了一次 toString
方法;
上面看着输出的地址都是 PersonImpl@16150369,其实他们不是同一个对象
打印出来的是 Object 的 toString 里面定义的类名和 hashCode 地址,不是内存地址,至于为什么代理里面能打印出目标类里面一样的 hashCode 我这里就搞不明白了
getClass().getName() + "@" + Integer.toHexString(hashCode());
代理类
如果你需要代理一个类,而不是一个或多个接口怎么办?
想象一下,在我们之前的例子中,如果并没有 Person 接口。我们需要 advice 一个名为 Person 的类,它没有实现任何业务接口。在这种情况下,你可以将 Spring 配置为使用 CGLIB 代理而不是动态代理。要做到这一点,将前面所示的 ProxyFactoryBean 上的 proxyTargetClass 属性设置为 true。虽然最好是对接口而不是类进行编程,但在处理遗留代码时,为没有实现接口的类提供 advice 的能力还是很有用的。(一般来说,Spring 不是规定性的。虽然它使应用良好的实践变得很容易,但它避免了强迫一种特定的方法)。
@Bean
public ProxyFactoryBean person() throws ClassNotFoundException {
final ProxyFactoryBean factoryBean = new ProxyFactoryBean();
// 配置目标类接口,也就是说生成的代理会对这个接口中的方法进行代理
factoryBean.setProxyInterfaces(new Class<?>[]{Person.class});
// 设置目标类的 bean 引用
factoryBean.setTargetName("personTarget");
// 强制创建 CGLIB 代理
factoryBean.setProxyTargetClass(true);
// 设置拦截器的引用
factoryBean.setInterceptorNames("myAdvisor", "debugInterceptor");
return factoryBean;
}
可以看到,强制使用 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 名称与星号前的部分相匹配的顾问都会被添加到顾问链中。如果你需要添加一套标准的 「全局」顾问,这就很方便了。下面的例子定义了两个全局顾问:
<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="service"/>
<property name="interceptorNames">
<list>
<value>global*</value>
</list>
</property>
</bean>
<bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/>