Spring 使用增强类来定义横切逻辑,同时由于 Spring 只支持方法连接点,而增强还包括在方法的哪一点上加入横切代码,所以增强类既包括横切逻辑,又包括部分连接点的信息。
1 增强类型
AOP 联盟为增强定义了 org.aopalliance.aop.Advice
接口:
带红点标志的是 AOP 联盟所定义的接口,其它的是 Spring 定义的扩展增强接口。
按照增强在目标类方法连接点的位置,可以将增强划分为以下五类:
类型 | 类名 | 说明 |
---|---|---|
前置增强 | org.springframework.aop.BeforeAdvice |
在目标方法执行前来实施增强。 |
后置增强 | org.springframework.aop.AfterReturningAdvice |
在目标方法执行后来实施增强。 |
环绕增强 | org.aopalliance.intercept.MethodInterceptor |
在目标方法执行前后同时实施增强。 |
异常抛出增强 | org.springframework.aop.ThrowsAdvice |
在目标方法抛出异常后来实施增强。 |
引介增强 | org.springframework.aop.introductioninterceptor |
在目标类中添加一些新的方法和属性。 |
通过实现(加入横切逻辑)这些增强接口的方法,就可以将它们织入目标类方法的相应连接点位置 。
2 前置增强
2.1 示例
假设,我们需要开发一个充电器共享的应用,既然是共享充电器,自然需要提供租借服务。
租借服务接口:
public interface RentService {
boolean rent(String userId);
}
租借服务类:
public class RentServiceImpl implements RentService {
/**
* 租赁
* @param userId 用户 ID
* @return
*/
public boolean rent(String userId) {
System.out.println("租赁成功");
return true;
}
}
这时,我们希望在日志中记录租赁用户的 ID。这个需求可以通过前置增强来实现。
RentBeforeAdvice:
public class RentBeforeAdvice implements MethodBeforeAdvice {
public void before(Method method, Object[] args, Object o) throws Throwable {
System.out.println("准备租赁的用户 ID:" + args[0]);
}
}
MethodBeforeAdvice 接口定义了一个方法:
void before(Method method, Object[] args, Object target) throws Throwable;
参数 | 说明 |
---|---|
method | 目标类的方法。 |
args | 目标类方法的入参。 |
target | 目标类实例。 |
当调用这个方法发生异常时,将会不会执行目标类方法。
单元测试:
RentService rentService = new RentServiceImpl();
RentBeforeAdvice advice = new RentBeforeAdvice();
//创建代理工厂
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(rentService);//设置代理目标
proxyFactory.addAdvice(advice);//设置增强
//生成代理类
RentService proxy = (RentService) proxyFactory.getProxy();
final String userId = "001";
proxy.rent(userId);
输出结果:
准备租赁的用户 ID:001
租赁成功
2.2 剖析 ProxyFactory
我们使用 org.springframework.aop.framework.ProxyFactory
通过编码的方式将 RentBeforeAdvice 的增强织入目标类 RentService 中。
Spring 定义了 AopProxy 接口,并提供了两种创建代理实现类:
CglibAopProxy 使用的是 CGLib 代理技术,而 JdkDynamicAopProxy 使用的是 JDK 技术。如果通过 ProxyFactory 的 setInterfaces(Class[] interfaces)
方法指定了目标接口进行代理,则 ProxyFactory 会使用 JdkDynamicAopProxy。此外,还可以通过 ProxyFactory 的 setOptimize(true)
方法让 ProxyFactory 启动优化代理方式,这样,针对接口的代理也会使用 CglibAopProxy。
注意: 因为 Spring 本身集成了 CGLib 库,所以可以直接使用 CGLib 代理技术。
ProxyFactory 通过 addAdvice()
来增加一个增强 。 所以我们可以使用这个方法来增加多个增强,通过增强形成一个增强链,它们的调用顺序和添加顺序是一致的,也可以通过 addAdvisor(int pos, Advisor advisor)
把特定的增强添加到增强链的某个具体位置(起始位置为 0)。
2.3 Spring 配置
<bean id="rentBeforeAdvice" class="net.deniro.spring4.aop.RentBeforeAdvice"/>
<bean id="rentService" class="net.deniro.spring4.aop.RentServiceImpl"/>
<bean id="rentService2" class="org.springframework.aop.framework.ProxyFactoryBean"
p:proxyInterfaces="net.deniro.spring4.aop.RentService"
p:interceptorNames="rentBeforeAdvice"
p:target-ref="rentService"
>
</bean>
ProxyFactoryBean 负责为其它 Bean 创建代理实例。它有这些属性:
属性 | 说明 |
---|---|
target | 需要代理的目标对象。 |
proxyInterfaces | 代理所要实现的接口,可以是多个接口。 |
interceptorNames | 需要织入的目标对象的增强 Bean 列表。这些 Bean 必须实现 Advice 或者 MethodInterceptor,配置的顺序就是调用顺序。 |
singleton | 确定返回的代理是否为单实例,默认为单例。 |
optimize | 当值为 true 时,强制使用 CGLib 代理 。代理为 singleton,推荐使用 CGLib 代理。其它类型的作用域,推荐使用 JDK 代理 。 因为 CGLib 创建代理速度较慢,但创建出的代理对象运行效率较高;JDK 代理则相反 。 |
proxyTargetClass | 是否对类进行代理。当值为 true 时,使用 CGLib 代理。 |
注意:将 proxyTargetClass 设置为 true 后,无需再设置 proxyInterfaces ,即使设置了也会被忽略。
单元测试:
RentService rentService=(RentService)context.getBean("rentService2");
rentService.rent("003");
输出结果:
准备租赁的用户 ID:003
租赁成功
3 后置增强
假设在租赁服务调用后,需要记录一些日志,那么我们可以使用后置增强:
public class RentAfterAdvice implements AfterReturningAdvice {
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("租赁服务调用结束:"+new Date());
}
}
AfterReturningAdvice 定义了一个方法:
void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable;
属性 | 说明 |
---|---|
returnValue | 目标实例方法返回的结果。 |
method | 目标类方法。 |
args | 目标实例方法的入参。 |
obj | 目标类实例。 |
如果在后置增强中抛出了异常,这个异常如果是目标方法中所声明的异常,那么这个异常会被归入目标方法;如果不是,那么将会被转为运行时异常被抛出。
Spring 配置:
<!-- 后置增强-->
<bean id="rentAfterAdvice" class="net.deniro.spring4.aop.RentAfterAdvice"/>
<!-- 增强后的服务 -->
<bean id="rentService2" class="org.springframework.aop.framework.ProxyFactoryBean"
p:proxyInterfaces="net.deniro.spring4.aop.RentService"
p:interceptorNames="rentBeforeAdvice,rentAfterAdvice"
p:target-ref="rentService"
>
</bean>
注意:interceptorNames 为 String[] 类型,它接受的是增强 Bean 的名称。因为 ProxyFactoryBean 需要使用增强 Bean 的类来生成代理类。
对于这种属性为 String[] 类型并且数组元素为 Bean 名称的配置项,建议使用 <idref bean="xxx">
进行配置,因为这样的配置在 IDE 环境下,会马上发现配置错误并予以预警,形如:
<property name="interceptorNames">
<list>
<idref bean="rentBeforeAdvice"/>
<idref bean="rentAfterAdvice"/>
</list>
</property>
输出结果:
准备租赁的用户 ID:005
租赁成功
租赁服务调用结束:Tue Jun 05 xx:26:11 CST 2018
4 环绕增强
既然租赁服务实现了前、后增强,那何不干脆直接使用环绕增强:
public class RentInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
Object[] args = invocation.getArguments();
System.out.println("准备租赁的用户 ID:" + args[0]);
//调用目标方法
Object obj = invocation.proceed();
System.out.println("租赁服务调用结束:" + new Date());
return obj;
}
}
Spring 直接使用 AOP 联盟所定义的 MethodInterceptor 作为环绕增强的接口,该接口拥有唯一的接口方法:
Object invoke(MethodInvocation invocation) throws Throwable;
MethodInvocation 不但封装了目标方法及其入参数组,还封装了目标方法所在的实例对象 。 通过 MethodInvocation 的 getArguments()
方法可以获取到目标方法的入参数组,通过 proceed()
方法反射调用目标实例相应的方法 。
配置:
<!-- 环绕增强-->
<bean id="rentInterceptor" class="net.deniro.spring4.aop.RentInterceptor"/>
<!-- 增强后的服务(环绕增强) -->
<bean id="rentService5" class="org.springframework.aop.framework.ProxyFactoryBean"
p:proxyInterfaces="net.deniro.spring4.aop.RentService"
p:interceptorNames="rentInterceptor"
p:target-ref="rentService"
/>
5 异常抛出增强
异常抛出增强指的是在目标方法抛出异常后实施增强,它最适合的场景是事务管理,比如当参与事务的某个方法抛出异常时必须回滚事务 。
public class TransactionManager implements ThrowsAdvice {
public void afterThrowing(Method method, Object[] args, Object target, Exception ex)
throws Throwable {
System.out.println(method.getName() + " 方法抛出异常:" + ex.getMessage()+"。");
System.out.println("成功回滚事务。");
}
}
ThrowsAdvice 接口只是一个标签接口,它没有定义任何的方法 。
在运行期 Spring 会采用反射的机制进行判断 。 所以我们必须采用以下的形式来定义异常抛出的方法:
void afterThrowing([Method method, Object[] args, Object target], Throwable);
注意:
- 方法名必须为 afterThrowing。
- 方法入参中的前三个入参为一组可选,即要么同时存在,要么都不存在。
- 最后一个入参是 Throwable 及其子类,必须定义 。
可以在同一个异常抛出增强中定义多个 afterThrowing() 方法,当目标类抛出异常,Spring 会自动调用最合适的增强方法。
假设在增强中定义了两个方法:
- afterThrowing(Exception e)
- afterThrowing(SQLException e)
当目标方法抛出 SQLException 时,将调用 afterThrowing(SQLException e)
。因为在类继承树上,两个类距离越近,这两个类的相似度就会越高。当目标方法抛出异常时,将优先调用相似度最高的方法。
注意:标签接口是没有任何方法和属性的接口,它仅表明它的实现类属于一个特定的类型。它有这些用途:
- 通过标签接口来标识同一类型的类,这些类本身可能具有不同的方法,如 Advice 接口。
- 通过标签接口让程序或 JVM 进行一些特殊处理,如 Serializable(表明这个对象可以序列号)。
Spring 配置:
<!-- 异常增强-->
<bean id="transactionManager" class="net.deniro.spring4.aop.TransactionManager"/>
<!-- 增强后的服务(异常增强) -->
<bean id="rentService6" class="org.springframework.aop.framework.ProxyFactoryBean"
p:proxyInterfaces="net.deniro.spring4.aop.RentService"
p:interceptorNames="transactionManager"
p:target-ref="rentService"
/>
输出结果:
replay 方法抛出异常:归还失败。
成功回滚事务。
6 引介增强
引介增强会为目标类创建新的方法和新的属性,所以它的连接点是类级别的 。通过引介增强,我们可以为目标类添加一个接口的实现(原来目标类未实现该接口) , 引介增强会为目标类创建实现某接口的代理 。
Spring 定义了引介增强的标签接口 IntroductionInterceptor,Spring 为该接口提供了 DelegatingIntroductionInterceptor 实现类,一般情况下,通过扩展该实现类就可以自定义引介增强类 。
假设,我们需要做一个带可控开关的性能监控器。
性能记录类:
public class PerformanceRecord {
private final String methodName;//方法名称
private final long begin;//开始时间
public PerformanceRecord(String method) {
this.methodName = method;
this.begin = System.currentTimeMillis();
}
/**
* 打印性能信息
*/
public void print() {
long end = System.currentTimeMillis();
long elapse = end - begin;
System.out.println(methodName + " 耗费时间:" + elapse + " 毫秒");
}
}
性能监视器类:
public class PerformanceMonitor {
//通过 ThreadLocal,保存与调用线程相关的性能监视信息
private static ThreadLocal<PerformanceRecord> record=new
ThreadLocal<PerformanceRecord>();
/**
* 开启监视
* @param method 需要监视的方法
*/
public static void begin(String method) {
System.out.println("开启监视...");
record.set(new PerformanceRecord(method));
}
/**
* 结束监视
*/
public static void end() {
System.out.println("结束监视...");
record.get().print();
}
}
现在定义一个用于标识目标类是否支持性能监控的接口:
public interface Monitorable {
void setActive(boolean active);
}
这里定义了一个接口方法,作为性能监控功能的开关。
接着,通过扩展 DelegatingIntroductionInterceptor,为目标类引入性能监控功能:
public class ControllablePerformanceMonitor extends DelegatingIntroductionInterceptor
implements Monitorable {
//保存性能监控功能的开关,通过 ThreadLocal,会让每一个线程都能够单独使用一个状态
private ThreadLocal<Boolean> monitorStatuses = new ThreadLocal<Boolean>();
public void setActive(boolean active) {
monitorStatuses.set(active);
}
public Object invoke(MethodInvocation invocation) throws Throwable {
Object obj = null;
if (monitorStatuses.get() != null && monitorStatuses.get()) {//开启性能监控
PerformanceMonitor.begin(invocation.getClass().getName() + "." + invocation
.getMethod().getName());
obj = super.invoke(invocation);
PerformanceMonitor.end();
} else {
obj = super.invoke(invocation);
}
return obj;
}
}
配置引介增强:
<!-- 性能监控器-->
<bean id="performanceMonitor"
class="net.deniro.spring4.aop.ControllablePerformanceMonitor"/>
<!-- 引介增强-->
<bean id="rentService7" class="org.springframework.aop.framework.ProxyFactoryBean"
p:interfaces="net.deniro.spring4.aop.Monitorable"
p:target-ref="rentService"
p:interceptorNames="performanceMonitor"
p:proxyTargetClass="true"
/>
- p:interfaces 指定引介增强所要实现的接口。
- 由于只能通过为目标类创建子类的方式来生成引介增强代理,所以必须将
p:proxyTargetClass=”true”
。
如果没有对 ControllablePerformaceMonitor 进行线程安全的处理,那么必须将 singleton 属性设置为 false, 让 ProxyFactoryBean 产生 prototype 的作用域类型的代理 。 但这样做会带来了一个严重的性能问题。因为 CGLib 动态创建代理的性能很低,而每次 getBean() 方法从容器中获取作用域为 prototype 的 Bean 时,都会返回一个新的代理实例,所以这种影响是巨大的,因此我们在这里通过 ThreadLocal 对 ControllablePerformaceMonitor 的开关进行线程安全化处理 。 通过线程安全处理后,就可以使用默认的 singleton Bean 作用域,这样创建代理的动作仅发生一次,就不会发生性能问题啦 O(∩_∩)O哈哈~
单元测试:
RentService rentService=(RentService)context.getBean("rentService7");
System.out.println("-------- 未开启监控 ------");
rentService.rent("007");//默认未开启监控
//开启监控
Monitorable monitorable=(Monitorable)rentService;
monitorable.setActive(true);
System.out.println("-------- 开启监控 ------");
rentService.rent("008");
输出结果:
definitions from class path resource [spring7-3.xml]]
-------- 未开启监控 ------
租赁成功
-------- 开启监控 ------
开启监视...
租赁成功
结束监视...
org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.rent 耗费时间:0 毫秒
注意:在 Spring4 之前的版本中,基于 CGLib 的类代理需要目标类必须具有无参的构造函数,Spring4 中已经取消(通过 objenesis 类库实现)这一限制啦 O(∩_∩)O哈哈~,我们甚至可以通过构造函数的注入方式来增强目标 Bean 。