说说在 Spring 中如何创建增强类(AOP)

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

注意:

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

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

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

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

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

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

  1. 通过标签接口来标识同一类型的类,这些类本身可能具有不同的方法,如 Advice 接口。
  2. 通过标签接口让程序或 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 。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,579评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,717评论 6 342
  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong阅读 22,295评论 1 92
  • 有关AOP相关概念以及Spring AOP相关概念和Spring AOP的使用不再重复。关于AOP在Spring中...
    加大装益达阅读 983评论 0 2
  • 眼前十米路, 全被雾当住 驾车一二档, 不敢超五步 行人少无数, 都把嘴捂住 到底是人间, 还是在仙处 半城白雾半...
    享受福袋阅读 135评论 0 0