博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
说说在 Spring 中如何创建增强类(AOP)
阅读量:1903 次
发布时间:2019-04-26

本文共 8977 字,大约阅读时间需要 29 分钟。

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 配置

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 配置:

**注意:**interceptorNames 为 String[] 类型,它接受的是增强 Bean 的名称。因为 ProxyFactoryBean 需要使用增强 Bean 的类来生成代理类。

对于这种属性为 String[] 类型并且数组元素为 Bean 名称的配置项,建议使用 <idref bean="xxx"> 进行配置,因为这样的配置在 IDE 环境下,会马上发现配置错误并予以预警,形如:

输出结果:

准备租赁的用户 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() 方法反射调用目标实例相应的方法 。

配置:

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

注意:

  1. 方法名必须为 afterThrowing。
  2. 方法入参中的前三个入参为一组可选,即要么同时存在,要么都不存在。
  3. 最后一个入参是 Throwable 及其子类,必须定义 。

可以在同一个异常抛出增强中定义多个 afterThrowing() 方法,当目标类抛出异常,Spring 会自动调用最合适的增强方法。

假设在增强中定义了两个方法:

  • afterThrowing(Exception e)
  • afterThrowing(SQLException e)

当目标方法抛出 SQLException 时,将调用 afterThrowing(SQLException e)。因为在类继承树上,两个类距离越近,这两个类的相似度就会越高。当目标方法抛出异常时,将优先调用相似度最高的方法。

**注意:**标签接口是没有任何方法和属性的接口,它仅表明它的实现类属于一个特定的类型。它有这些用途:

  1. 通过标签接口来标识同一类型的类,这些类本身可能具有不同的方法,如 Advice 接口。
  2. 通过标签接口让程序或 JVM 进行一些特殊处理,如 Serializable(表明这个对象可以序列号)。

Spring 配置:

输出结果:

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
record=new ThreadLocal
(); /** * 开启监视 * @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
monitorStatuses = new ThreadLocal
(); 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; }}

配置引介增强:

  • 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 。

转载地址:http://qedcf.baihongyu.com/

你可能感兴趣的文章
public class ExcelBean implements java.io.Serializable
查看>>
Jsp调用Action的几种方法--做个记录
查看>>
SSM-遇见的一些小坑(五)jstl标签库
查看>>
Mybatis-mapper.xml相关记录
查看>>
insert ignore into--跳坑
查看>>
SecureCRT的安装和破解--亲试可用
查看>>
Leetcode--21. Merge Two Sorted Lists--Java
查看>>
Leetcode--26. Remove Duplicates from Sorted Array--Java
查看>>
Java的向上转型和向下转型
查看>>
Java-IO的几种方式的BIO、NIO、AIO
查看>>
创维电视 内存不足 手工清理
查看>>
Oracle 回滚(ROLLBACK)和撤销(UNDO)
查看>>
oracle undo与redo的区别
查看>>
Oracle的高水位线介绍
查看>>
Oracle 10g DBCA建库四个选项的区别------ 一般用途 事务处理 定制数据库数据仓库
查看>>
ora-01658 :无法为表空间USERS 中的段创建INITIAL区
查看>>
(总结)Linux的chattr与lsattr命令详解
查看>>
chattr和lsattr命令
查看>>
数据块(Data Block)原理深入剖析
查看>>
Oracle体系结构
查看>>