深入理解Spring AOP实战(基于注解)

来源:互联网 发布:知乎神回复2016 知乎 编辑:程序博客网 时间:2024/05/18 03:19
一 AOP的概念
AOP(Aspect Oriented Programming),即为面向切面编程。
在软件开发中,散布于应用中多处的功能被称为横切关注点(cross-cutting concern),
通常来说,这些横切关注点从概念上是与应用的业务逻辑分离的。
比如,声明式事务、日志、安全、缓存等等,都与业务逻辑无关,
可以将这些东西抽象成为模块,采用面向切面编程的方式,通过声明方式定义这些功能用于何处,
通过预编译方式和运行期动态代理实现这些模块化横切关注点程序功能进行统一维护,
从而将横切关注点与它们所影响的对象之间分离出来,就是实现解耦。

横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。
这样做有两个有点:
首先,现在每个关注点都集中于一个地方,而不是分散到多处代码中;
其次,服务模块更简洁,因为它们只包含主要的关注点的代码(核心业务逻辑),
而次要关注点的代码(日志,事务,安全等)都被转移到切面中。

二 AOP术语
通知(Advice)
切面类有自己要完成的工作,切面类的工作就称为通知。
通知定义了切面是做什么以及何时使用。
AOP的官腔最不接地气,
用大白话来解释"做什么",即切面类中定义的方法是干什么的,
"何时使用",即5种通知类型,是在目标方法执行前,还是目标方法执行后等等。
下面会分析切点,切点定义的就是”何处做“,即通知定义了做什么,何时使用,
但是不知道用在何处,而切点定义的就是告诉通知应该用在哪个类的哪个目标方法上,
从而完美的完成横切点功能。

spring切面定义了5种类型通知:
(1)前置通知(Before): 在目标方法被调用之前调用通知功能。
(2)后置通知(After): 在目标方法完成之后调用通知,不会关心方法的输出是什么。
(3)返回通知(After-returning): 在目标方法成功执行之后调用通知。
(4)异常通知(After-throwing): 在目标方法抛出异常后调用通知。
(5)环绕通知(Around): 通知包裹了被通知的方法,在被通知的方法
调用之前和之后执行自定义的行为。

连接点(Join point)
在我们的应用程序中有可能有数以万计的时机可以应用通知,而这些时机就被称为连接点。
连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、
甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
用一句话来概括连接点,就是连接点是一个虚概念,可以把连接点看成是切点的集合。
下面我们看看切点是神马鬼?

切点(Poincut)
连接点谈的是一个飘渺的大范围,而切点是一个具体的位置,用于缩小切面所通知的连接点的范围。
咱们前面说过,通知定义的是切面的"要做什么"和"在何时做",是不是没有去哪里做,
而切点就定义了"去何处做"。切点的定义会匹配通知所要织入的一个或多个连接点。
我们通常使用明确的类和方法名称,或者是使用正则表达式定义所匹配的类和方法名称来指定切点。
说白了,切点就是让通知找到"发泄的地方"。

切面(Aspect)
切面是通知和切点的结合,通知和切点共同定义了切面的全部内容。
因为通知定义的是切面的"要做什么"和"在何时做",而切点定义的是切面的"在何地做"。
将两者结合在一起,就可以完美的展现切面在何时,何地,做什么(功能)。

引入(Introduction)
引入这个概念就比较高大尚,引入允许我们向现有的类添加新方法或属性。
主要目的是想在无需修改A的情况下,引入B的行为和状态。

织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程。
切面在指定的连接点被织入到目标对象中。
在目标对象的生命周期里有多个点可以进行织入:
编译期: 
    切面在目标类编译时被织入。需要特殊的编译器,是AspectJ的方式,不是spring的菜。
类加载期: 
    切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器,
    它可以在目标类被引入应用之前增强该目标类的字节码。
     AspectJ5支持这种方式。
运行期:  
     切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,
     AOP容器会为目标对象动态的创建一个代理对象。
     而这正是Spring AOP的织入切面的方式。

三 AOP实战
在家开家庭会议,上学开班会,上班了开工作会议,人死了开追悼会。
人的一生都在开会中出生,然后在开会中死去。每个人都离不开开会。
而上学时候开会,开会前老师都说: 静一下,现在通讯发达了,网络时代,
老师开会估计都说: 把游戏收起来,别他妈玩了!(估计这是现在老师最想大声说的
一句话,开个玩笑!哈哈!)
而工作了,咱们最常听见的是: 把手机调成静音,或者关机。
其实在开会之前的这些都不是开会要将的核心,而开会后搞不好还要写个总结报告,
所有开会前开会后做的这些事情,都与核心业务逻辑开会都是独立开来的,
我们用开会前,开会,开会后来举例分析一下AOP的使用。
AOP的实现可以通过注解方式或XML方式,这里主要分析注解方式,
下一篇有机会再讨论下XML方式。
1. 创建切面类
切面类包含通知和切入点,在创建切面类之前,
我们需要了解下AspectJ的切点表达式,因为我们需要通过切点表达式定义切点,
用于准确的定位应该在什么地方应用切面的通知。直接上一个图,解释下切点表达式的元素:

对切点表达式有所了解后,我们通过@Aspect注解标注创建切面类。
package com.lanhuigu.spring;import org.aspectj.lang.annotation.After;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;/** *  注解@Aspect标识该类为切面类 */// @Component@Aspectpublic class Person {    /**     * 开会之前--找个位置坐下     */    @Before("execution(* com.lanhuigu.spring.ConferenceServiceImpl.conference(..))")    public void takeSeats() {        System.out.println("找位置坐");    }    /**     * 开会之前--手机调成静音     */    @Before("execution(* com.lanhuigu.spring.ConferenceServiceImpl.conference(..))")    public void silenceCellPhones() {        System.out.println("手机调成静音");    }    /**     * 开会之后--写会议总结报告     */    @After("execution(* com.lanhuigu.spring.ConferenceServiceImpl.conference(..))")    public void summary() {        System.out.println("写会议总结报告");    }}
在定义完这个切面类之后,有没有发现3个方法通知类型之后的execution表达式
内容完全一致,如果你有代码强迫症,一定想把他们提成一个公用的,
别的地方只需要引用一下就行,这个地方使用@Pointcut满足你的强迫症。
优化后的切面类:
package com.lanhuigu.spring;import org.aspectj.lang.annotation.After;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;/** *  注解@Aspect标识该类为切面类 */// @Component@Aspectpublic class Person {//    /**//     * 开会之前--找个位置坐下//     *///    @Before("execution(* com.lanhuigu.spring.ConferenceServiceImpl.conference(..))")//    public void takeSeats() {//        System.out.println("找位置坐");//    }////    /**//     * 开会之前--手机调成静音//     *///    @Before("execution(* com.lanhuigu.spring.ConferenceServiceImpl.conference(..))")//    public void silenceCellPhones() {//        System.out.println("手机调成静音");//    }////    /**//     * 开会之后--写会议总结报告//     *///    @After("execution(* com.lanhuigu.spring.ConferenceServiceImpl.conference(..))")//    public void summary() {//        System.out.println("写会议总结报告");//    }    /**     * =========================================================================     * 从上面的执行代码可以看出切点execution表达式内容都是一样,     * 我们可以通过@Pointcut进行优化。     * =========================================================================     */    /**     * 通过注解@Pointcut定义切点,conference()只是一个标识,无所谓是什么,     * 方法中内容本身也是空的,使用该切点的地方直接通过标识conference()引用切点表达式。     */    @Pointcut("execution(* com.lanhuigu.spring.ConferenceServiceImpl.conference(..))")    public void conference() {}    /**     * 开会之前--找个位置坐下     */    @Before("conference()")    public void takeSeats() {        System.out.println("找位置坐");    }    /**     * 开会之前--手机调成静音     */    @Before("conference()")    public void silenceCellPhones() {        System.out.println("手机调成静音");    }    /**     * 开会之后--写会议总结报告     */    @After("conference()")    public void summary() {        System.out.println("写会议总结报告");    }}
咱们解释一下切点表达式的含义:
通过execution指示器,选择ConferenceServiceImpl类中的conference()方法。
方法表达式以“*”号开始,表明了我们不关心方法的返回值是神马鬼。
对于方法参数列表通过两个点表示,表示我们不在乎conference的参数。
在执行表达式的时候,我们可以通过逻辑运算符&&(and) , ||(or) , !(not)
对表达式进行搭配。比如:
execution(* com.lanhuigu.spring.ConferenceServiceImpl.conference(..)
          && within(com.lanhuigu.spring.*))
增加了一个限制就是我们只管com.lanhuigu.spring下的包,这里的&&可以使用and来替代,
同理|| , !都是一样的用法,灵活多变,只能根据实际情况看着办。

2. 创建目标类,定义目标方法
目标类就是我们的核心,开会,开会前和开会后,中间休息等等
地方都是插入切面通知的地方,这些地方的集合就是连接点,
而某一个具体的位置就是切点,连接点就是切点的集合。
我们创建一个目标类,作为切面插入的目标。
创建一个开会的接口:
package com.lanhuigu.spring;public interface ConferenceService {    void conference();}

开会接口的实现:
package com.lanhuigu.spring;import org.springframework.stereotype.Component;@Componentpublic class ConferenceServiceImpl implements ConferenceService {    @Override    public void conference() {        System.out.println("开会......");    }}
3. 编写配置类,启动AOP代理功能
切面类,目标方法都创建完了,但是我们的切面类在启动时也不会被转化为代理的。
现在处于完事具备,只欠东风的状态。需要一阵风,打一场赤壁之战。
古有诸葛亮借东风,今有spring通过@EnableAspectJAutoProxy注解启动AspectJ自动代理。
JavaConfg配置:
package com.lanhuigu.spring;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.EnableAspectJAutoProxy;/** * * Jdk代理:基于接口的代理,一定是基于接口,会生成目标对象的接口的子对象。 * Cglib代理:基于类的代理,不需要基于接口,会生成目标对象的子对象。 * * 1. 注解@EnableAspectJAutoProxy开启代理; * * 2. 如果属性proxyTargetClass默认为false, 表示使用jdk动态代理织入增强; * * 3. 如果属性proxyTargetClass设置为true,表示使用cglib动态代理技术织入增强; * * 4. 如果属性proxyTargetClass设置为false,但是目标类没有声明接口, *    spring aop还是会使用cglib动态代理,也就是说非接口的类要生成代理都用Cglib。 */@Configuration@EnableAspectJAutoProxy(proxyTargetClass = true)@ComponentScanpublic class ConcertConfig {    /**     * 通过@Bean定义,注入到spring容器,也可以通过在Person类上添加@Component,实现同样的效果。     */    @Bean    public Person person() {        return new Person();    }}
4. 测试类
package com.lanhuigu.spring;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(classes = ConcertConfig.class)public class TestAopAnnotation {    @Autowired    private ConferenceServiceImpl conferenceService;    @Test    public void testAop() {        conferenceService.conference();    }}
5. 程序运行结果
手机调成静音
找位置坐

开会......
写会议总结报告

在上面我们通过@EnableAspectJAutoProxy开启的代理,如果使用的不是JavaConfig,
而是xml,我们可以通过xml中的元素<aop:aspectj-autoproxy>开启代理。
spring的xml配置如下:
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:context="http://www.springframework.org/schema/context"       xmlns:aop="http://www.springframework.org/schema/aop"       xsi:schemaLocation="http://www.springframework.org/schema/beans       http://www.springframework.org/schema/beans/spring-beans.xsd       http://www.springframework.org/schema/context       http://www.springframework.org/schema/context/spring-context.xsd       http://www.springframework.org/schema/aop       http://www.springframework.org/schema/aop/spring-aop.xsd">    <!-- 扫描注解 -->    <context:component-scan base-package="com.lanhuigu.*"/>    <!-- 启用AspectJ自动代理 -->    <aop:aspectj-autoproxy proxy-target-class="true"/>    <!--       切面类声明, 如果不想在这里显示声明,可以在该类加上@Component注解,spring在扫描创建bean容器时会自动创建。    -->    <bean class="com.lanhuigu.spring.Person"/></beans>
以上需要引入aop命名空间。
我们写一个测试类试试, 需要注释掉ConcertConfig和TestAopAnnotation,
否则会出现双份横切内容:
package com.lanhuigu.spring;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;/** * 注意: *   测试该类时,注释掉ConcertConfig类和TestAopAnnotation类。 */public class TestAopXml {    @Test    public void testAopXml() {        // 根据配置文件创建spring容器        ApplicationContext context =                new ClassPathXmlApplicationContext("applicationContext.xml");        /*         * 从容器中获取Bean:         * 可以看到在xml中没有配置ConferenceService或ConferenceServiceImpl的内容,         * 为什么读取applicationContext.xml配置文件创建spring容器后根据Bean ID "conferenceServiceImpl"         * 从容器中获取ConferenceServiceImpl对象?         * 因为在xml配置中<context:component-scan base-package="com.lanhuigu.*"/>         * 会扫描所有带有spring注解的类,并纳入到spring容器,而@Component注解注册的Bean ID默认         * 为类首字母小写作为Bean ID,所以ConferenceServiceImpl类在spring容器中的ID为conferenceServiceImpl。         * 从而可以根据Bean ID从容器获取bean对象使用。         */        ConferenceServiceImpl conferenceService = (ConferenceServiceImpl)context.getBean("conferenceServiceImpl");        // 调用Bean方法        conferenceService.conference();    }}
程序输出的结果一样,到此,我们基本上了解了基于注解的AOP实战。
6. 环绕通知
关于通知类型,需要单独分析的是环绕通知,他跟其他通知类型不一样,
环绕通知也是最为强大的一种通知方式,所谓的环绕通知,顾名思义,
它能够让你所编写的逻辑将被通知的目标方法全部包装起来。
实际上就像我们前面写的开会前,开会后干的哪些事情,
对于环绕通知来说,一个方法就搞定了,因为他包围了目标方法,
等同于在一个通知方法中同时编写了前置通知和后置通知,
环绕通知都会为执行开会前,开会后等等逻辑。
把前面的Person类重构为如下内容:
package com.lanhuigu.spring;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;/** *  注解@Aspect标识该类为切面类 */// @Component@Aspectpublic class Person {    /**     * 通过注解@Pointcut定义切点,conference()只是一个标识,无所谓是什么,     * 方法中内容本身也是空的,使用该切点的地方直接通过标识conference()引用切点表达式。     */    @Pointcut("execution(* com.lanhuigu.spring.ConferenceServiceImpl.conference(..))")    public void conference() {}    @Around("conference()")    public void testAround(ProceedingJoinPoint jp) {        try {            System.out.println("1111111111111");            System.out.println("2222222222222");            jp.proceed();            System.out.println("3333333333333");        } catch (Throwable e) {            System.out.println("开会不爽,打起来了");        }    }}

重构完后再运行下TestAopAnnotation测试类,输出如下结果:
11111111111112222222222222开会......3333333333333
是不是很神奇,这里为了看结果明显,
将开会前调静音,找座位用111111111111和22222222222代替,会后写报告用333333333333333来代替。
从实例我们应该能体会到环绕的概念,我们从客观上分析一下环绕。
环绕通知以ProceedingJoinPoint做为参数。这个对象是必须的,
因为我们要在通知中通过它来调用被通知的方法。
通知方法中可以做任何的事情,当要将控制权转交给被通知的方法时,
它需要调用ProceedingJoinPoint类的proceed()方法。
也就是我们在打印完11111111111111和2222222222222的时候
调用了proceed()方法时,将控制权交给了被通知方法conference()
然后打印了"开会......",开会执行完之后,通知又获取了控制权,
继续执行打印33333333333,然后程序运行结束。
换句话来说,如果通知不调用proceed()方法,那么被通知方法会被阻塞,
这个特性可以用于重试逻辑,也就是通知方法失败之后,进行重复尝试。

原创粉丝点击