《Spring实战》-第四章:面向切面编程(AOP)

慢慢来比较快,虚心学技术

前言:DI (依赖注入)有助于应用对象之间的解耦,而 AOP(面向切面编程) 可以实现横切关注点与它们所影响的对象之间的解耦

一、什么是面向切面编程

Ⅰ、横切关注点:在软件开发中,散布于应用中多处的功能被称为横切关注点( cross-cutting concern )【比如说日志,安全和事务管理等】。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑之中)。把这些横切关注点与业务逻辑相分离正是面向切面编程( AOP )所要解决的问题。

Ⅱ、切面:横切关注点可以被模块化为特殊的类,这些类被称为切面( aspect )。

Ⅲ、AOP术语

通知( Advice ):切面的工作被称为通知。通知描述切面的工作,同时决定切面何时工作【定义了切面工作做什么,什么时候做】

  • 前置通知( Before ):在目标方法被调用之前调用通知功能;
  • 后置通知( After ):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
  • 返回通知( After-returning ):在目标方法成功执行之后调用通知;
  • 异常通知( After-throwing ):在目标方法抛出异常后调用通知;
  • 环绕通知( Around ):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为

连接点( Join point ):触发切面工作的点,比如方法执行,异常抛出等行为

切点( Poincut ):决定切面工作的地方,比如某个方法等【定义了切面工作在哪里做】

切面( Aspect ):通知和切点的结合【面】

引入( Introduction ):允许我们向现有的类添加新方法或属性

织入( Weaving ):把切面应用到目标对象并创建新的代理对象的过程【切面在指定的连接点被织入到目标对象中】

通知包含了需要用于多个应用对象的横切行为;连接点是程序执行过程中能够应用通知的所有点;切点定义了通知被应用的具体位置(在哪些连接点)。其中关键的概念是切点定义了哪些连接点会得到通知

Ⅳ、Spring对AOP的支持【 Spring AOP 构建在动态代理基础之上,因此, Spring 对 AOP 的支持局限于方法拦截。】

  1. 基于代理的经典 Spring AOP ;
  2. 纯 POJO 切面;
  3. @AspectJ 注解驱动的切面;
  4. 注入式 AspectJ 切面(适用于 Spring 各版本)。

二、面向切面编程实现

1、定义切点:

Spring支持通过AspectJ的切点表达式语言来定义 Spring 切面,同时增加通过bean的id指定bean的写法。

如:execution( com.my.spring.bean..(..))* 指定com.my.spring.bean包下所有类的所有方法作为切点

其结构解析如下:


AspectJ切点表达式的指示器不只有execution:

  • arg() :限制连接点匹配参数为指定类型的执行方法
  • execution() :用于匹配是连接点的执行方法
  • this() :限制连接点匹配 AOP 代理的 bean 引用为指定类型的类
  • target :限制连接点匹配目标对象为指定类型的类
  • within() :限制连接点匹配指定的类型

各指示器之间可以通过&&(与),||(或),!(非)连接符进行连接实现多条件查询定义节点

如:execution(* com.my.spring.bean.* . *(..))&&arg(java.lang.Integer)

2.示例Demo

Spring AOP的实现依赖于spring-aop包和aspectjweaver包,需在pom文件引入:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>${org.springframework.version}</version>
</dependency>

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.13</version>
</dependency>

通过实例demo引入概念:

①定义一个基础接口类BaseInterface和实现类BaseBean

public interface BaseInterface {

    /**
     * 新增歌曲
     *
     * @param author 作者
     * @param songTitle 歌曲名
     *
     * @return java.lang.Integer 返回当前歌曲总数
     *
     * @author xxx 2019/3/4
     * @version 1.0
     **/
    Integer addSong(String author,String songTitle);

    /**
     * 删除歌曲
     *
     * @param author 作者
     * @param songTitle 歌曲名
     *
     * @return java.lang.Integer 返回当前歌曲总数
     *
     * @author xxx 2019/3/4
     * @version 1.0
     **/
    Integer delSong(String author,String songTitle);
}

@Component
public class BaseBean implements  BaseInterface{

    private String author;
    private String songTitle;
    private Integer count=0;

    @Override
    public Integer addSong(String author,String songTitle){
        this.author = author;
        this.songTitle = songTitle;
        System.out.println("新增了一首歌:"+author+"-"+songTitle);
        count++;
        return count;
    }

    @Override
    public Integer delSong(String author,String songTitle){
        this.author = author;
        this.songTitle = songTitle;
        System.out.println("删除了一首歌:"+author+"-"+songTitle);
        count--;
        return count;
    }
}

②创建一个切面类

@Aspect
@Component
public class BaseBeanAspect {

    private Logger logger = LoggerFactory.getLogger(BaseBeanAspect.class);

     /**
      * 方法执行前的通知
      */
    @Before("execution(* com.my.spring.bean.*.*(..))")
    public void beforeInvoke(){
        logger.debug("方法执行前");
    }

    /**
     * 方法执行后的通知
     */
    @After("execution(* com.my.spring.bean.*.*(..))")
    public void afterInvoke(){
        logger.debug("方法执行后");
    }

    /**
     * 方法执行返回后的通知
     */
    @AfterReturning("execution(* com.my.spring.bean.*.*(..))")
    public void afterReturning(){
        logger.debug("==================方法执行完成");
    }

    /**
     * 方法抛出异常的通知
     */
    @AfterThrowing("execution(* com.my.spring.bean.*.*(..))")
    public void afterThrowing(){

        logger.debug("==================方法执行报错");
    }
}

③创建自动化装配的配置类

@Configuration
@EnableAspectJAutoProxy//开启自动代理开关,启用切面
@ComponentScan(basePackages = {"com.my.spring"})
public class ComponentConfig {
}

④测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {ComponentConfig.class})
public class AppTest {

    @Autowired
    private BaseInterface baseInterface;

    @Test
    public void testBean(){
        baseInterface.addSong("myBean","mySong");
        baseInterface.delSong("myBean","mySong");
    }
}

⑤测试结果

2019-03-04 14:32:55.019 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法执行前
新增了一首歌:myBean-mySong
2019-03-04 14:32:55.019 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法执行后
2019-03-04 14:32:55.019 DEBUG com.my.spring.aspect.BaseBeanAspect - ==================方法执行完成
2019-03-04 14:32:55.019 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法执行前
删除了一首歌:myBean-mySong
2019-03-04 14:32:55.019 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法执行后
2019-03-04 14:32:55.019 DEBUG com.my.spring.aspect.BaseBeanAspect - ==================方法执行完成

3、注解分析

前代码是使用注解实现SpringAOP的简单示例,我们了解以下其中用到的注解和实现:

Ⅰ、定义切面:@Aspect------------标识当前类是一个切面类(仅仅只是标识,我们可以看到该注解源码并没有使用@Component注解,所以使用该注解的bean依旧只是一个普通的POJO,使用时依旧需要显式或自动装配)

Ⅱ、定义切点:@PointCut

我们看到上述代码中在多个地方使用切点使用的是重复性的表达式,其实通过@PointCut注解定义切点,同时通过指定空方法名引入切点到各个通知即可

@Pointcut("execution(* com.my.spring.bean.*.*(..))")
public void pointCut(){//被用于标识的空方法
}

@Before("pointCut()")//以切点方法名引入
public void beforeInvoke(){
    logger.debug("方法执行前");
}

Ⅱ、定义通知:

@Beafore(切点)-----------------切点方法执行前的通知

@After(切点)-------------------------切点方法执行后的通知

@AfterReturning(切点)-----------切点方法执行返回后的通知

@AfterThrowing(切点)------------切点方法抛异常后的通知

Ⅲ、开启自动代理:@EnableAspectJAutoProxy--------在配置类中使用,如果不启用的话,编写的切面将不生效

Ⅳ、定义环绕通知:@Around("pointCut()")

环绕通知是从方法执行前一直包裹直到方法执行完成后的一个通知,用的比较多,其中被定义的方法需要引入参数ProceedingJoinPoint,ProceedingJoinPoint对象封装了当前运行对象的具体信息,简单实现如下:

 @Around("pointCut()")
public void aroundInvoke(ProceedingJoinPoint jp){
    try {
        logger.debug("=====================环绕执行方法开始");
        Signature signature = jp.getSignature();
        String methodName = signature.getName();
        MethodSignature methodSignature = (MethodSignature) signature;
        logger.debug("方法名:{}",methodName);
        List<Object> args = Arrays.asList(jp.getArgs());
       logger.debug("参数列表:{}",args);
       Class<?> returnType = methodSignature.getMethod().getReturnType();
       logger.debug("方法返回类型:{}",returnType);
       Object proceed = jp.proceed();
       logger.debug("======================环绕执行方法结束,方法执行结果:{}",proceed);
     } catch (Throwable throwable) {
         throwable.printStackTrace();
     }
}

其中,ProceedingJoinPoint对象源码分析如下:

JoinPoint 

     java.lang.Object[] getArgs()//获取连接点方法运行时的入参列表; 

     Signature getSignature() //获取连接点的方法签名对象; 

     java.lang.Object getTarget() //获取连接点所在的目标对象; 

     java.lang.Object getThis() //获取代理对象本身; 

ProceedingJoinPoint继承JoinPoint子接口,它新增了两个用于执行连接点方法的方法

      java.lang.Object proceed() throws java.lang.Throwable:通过反射执行目标对象的连接点处的方法;

      java.lang.Object proceed(java.lang.Object[] args) throws java.lang.Throwable:通过反射执行目标对象连接点处的方法,不过使用新的入参替换原来的入参。 

4、XML实现切面编程

我们知道使用注解实现切面编程是很方便直接的,但是有时候我们并不一定拥有通知类的源码,也就无法给对应的方法添加注解,这时候就需要使用XML配置实现了。XML配置实现与注解实现十分类似,我们可以看一下基本的实现节点:

定义切面:<aop:aspect ref="切面类在xml文件中对应bean的id">

定义切点:<aop:pointcut id="切点id" expression="切点表达式"/>

定义通知:

<aop:beafore method="通知方法名" pointcut-ref="切点id"/>-------------定义方法执行前的通知

<aop:after method="通知方法名" pointcut-ref="切点id"/>-------------定义方法执行后的通知

<aop:afterReturning method="通知方法名" pointcut-ref="切点id"/>-------------定义方法执行返回后的通知

<aop:afterThrowing method="通知方法名" pointcut-ref="切点id"/>-------------定义方法执行抛出异常后的通知

<aop:around method="通知方法名" pointcut-ref="切点id"/>-------------定义方法环绕通知

④开启自动代理:<aop:aspectj-autoproxy/>

⑤表示aop配置:<aop:config></aop:config>

除了开启自动代理,aop的所有节点都需要包含在<aop:config></aop:config>节点中,如下demo演示:

applicaiton.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: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/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--开启自动代理-->
    <aop:aspectj-autoproxy/>

    <!--装配基本类-->
    <bean class="com.my.spring.bean.BaseBean" id="baseBean" name="baseBean"/>

    <!--装配切面类-->
    <bean class="com.my.spring.aspect.BaseBeanAspect" id="baseBeanAspect"/>

    <!--aop配置-->
    <aop:config>
        <!--配置切面-->
        <aop:aspect ref="baseBeanAspect">
            <!--定义切点-->
            <aop:pointcut id="pointCut" expression="execution(* com.my.spring.bean.*.*(..))"/>
            <!--定义前置通知-->
            <aop:before method="beforeInvoke" pointcut-ref="pointCut"/>
            <!--定义后置通知-->
            <aop:after method="afterInvoke" pointcut-ref="pointCut"/>
            <!--定义方法执行返回后通知-->
            <aop:after-returning method="afterReturning" pointcut-ref="pointCut"/>
            <!--定义方法异常后通知-->
            <aop:after-throwing method="afterThrowing" pointcut-ref="pointCut"/>
            <!--定义方法环绕通知通知-->
            <aop:around method="aroundInvoke" pointcut-ref="pointCut"/>
        </aop:aspect>
    </aop:config>
</beans>

将BaseBean和BaseBeanAspect类中的注解去除,编写测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:application.xml"})//将配置文件作为装配环境
public class AppXMLTest {

    @Autowired
    private BaseInterface baseInterface;

    @Test
    public void testBean(){
        baseInterface.addSong("Mr D","The World!!");
        baseInterface.delSong("Mr D","The World!!");
    }
}

测试结果:

2019-03-04 22:07:37.901 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法执行前
2019-03-04 22:07:37.903 DEBUG com.my.spring.aspect.BaseBeanAspect - =====================环绕执行方法开始
2019-03-04 22:07:37.907 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法名:addSong
2019-03-04 22:07:37.910 DEBUG com.my.spring.aspect.BaseBeanAspect - 参数列表:[Mr D, The World!!]
2019-03-04 22:07:37.910 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法返回类型:class java.lang.Integer
2019-03-04 22:07:37.910 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法执行前
新增了一首歌:Mr D-The World!!
2019-03-04 22:07:37.911 DEBUG com.my.spring.aspect.BaseBeanAspect - ==================方法执行完成
2019-03-04 22:07:37.911 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法执行后
2019-03-04 22:07:37.911 DEBUG com.my.spring.aspect.BaseBeanAspect - ======================环绕执行方法结束,方法执行结果:1
2019-03-04 22:07:37.912 DEBUG com.my.spring.aspect.BaseBeanAspect - ==================方法执行完成
2019-03-04 22:07:37.912 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法执行后
2019-03-04 22:07:37.915 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法执行前
2019-03-04 22:07:37.915 DEBUG com.my.spring.aspect.BaseBeanAspect - =====================环绕执行方法开始
2019-03-04 22:07:37.916 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法名:delSong
2019-03-04 22:07:37.917 DEBUG com.my.spring.aspect.BaseBeanAspect - 参数列表:[Mr D, The World!!]
2019-03-04 22:07:37.917 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法返回类型:class java.lang.Integer
2019-03-04 22:07:37.917 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法执行前
删除了一首歌:Mr D-The World!!
2019-03-04 22:07:37.917 DEBUG com.my.spring.aspect.BaseBeanAspect - ==================方法执行完成
2019-03-04 22:07:37.917 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法执行后
2019-03-04 22:07:37.917 DEBUG com.my.spring.aspect.BaseBeanAspect - ======================环绕执行方法结束,方法执行结果:0
2019-03-04 22:07:37.917 DEBUG com.my.spring.aspect.BaseBeanAspect - ==================方法执行完成
2019-03-04 22:07:37.917 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法执行后
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容