Spring之面向切面AOP

AOP概念

AOP的定义:AOP,Aspect Oriented Programming的缩写,意为面向切面编程,是通过预编译或运行期动态代理实现程序功能处理的统一维护的一种技术。

系统中往往存在非常多的Controller匹配各种请求。现在,如果我们想在每一个Controller的RequestMapping请求识别到的时候做个记录。正常的处理逻辑是在每个Request请求到的时候写个Log。然后执行相应的方法。但是这样的话要把所有的RequestMapping方法都加上这个Log,显然非常麻烦。面向切面就是希望只写一遍Log就让所有的请求都能够执行。横向的将代码嵌入到各个请求中。如下图那样,并不影响原本程序的原有逻辑。

image.png

接下来看一下几个名词

切面(Aspect):横切关注点可以被模块化为特殊的类被称为切面(aspect)。

通知(Advice):通知定义了切面是什么以及合适使用。通常有五种类型:

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

连接点(Join point):通知执行的时机。例如高速路上有非常多的出口,这些出口都相当于连接点。

切入点(Poincut):通知所要织入的具体位置。还是以高速路为例子,众多出口(连接点)中,我们只需要找到一个出口,这个出口就是切入点。

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

织入(Weaving):把切面用到目标对象并创建新的代理对象的过程。

下图为《Spring实战》中关于各个名词结合的图。

image.png

Spring对AOP的支持

  • 基于代理的Spring AOP
  • 纯POJO切面
  • @AspectJ注解驱动的切面
  • 注入式AspectJ切面(适用于Spring各版本)

注:Spring只支持方法级别的连接点

通过切点选择连接点

Spring支持Aspectj的指示器中只有execution指示器是实际执行匹配的。
首先,我们有一个发送短信的接口

public interface MessageService {

    void sendMessage(String msg);

}

我们希望在MessageService进行sendService方法时候触发通知。就有了如下的表达式。

execution(* com.dqzhou.spring.service.MessageService.sendMessage(..))
  • 表示可以返回任意类型,接下来是全限定类名,方法。方法中的参数为 .. 表明可以使用任意参数。

使用注解创建切面

我们定义一个切面记录方法的开始执行的时间与执行结束的时间,以及失败的时间,此外还有@AfterRetrun和@Around

@Aspect
public class TimeLogging {

    @Before("execution(* com.dqzhou.spring.service.MessageService.sendMessage(..))")
    public void recordBeforeExecute() {
        System.out.println("start to send message at " + LocalDateTime.now());
    }

    @After("execution(* com.dqzhou.spring.service.MessageService.sendMessage(..))")
    public void recordAfterExecute() {
        System.out.println("message has sent seccess at " + LocalDateTime.now());
    }

    @AfterThrowing("execution(* com.dqzhou.spring.service.MessageService.sendMessage(..))")
    public void recordFailure() {
        System.out.println("fail to send message at " + LocalDateTime.now());
    }

}

显然,上面同一个切入点表达式写了三遍非常难看。因此,可以使用@Pointcut注解定义命名的切点

@Aspect
public class TimeLogging {

    // 定义命名的切点
    @Pointcut("execution(* com.dqzhou.spring.service.MessageService.sendMessage(..))")
    public void sendMessage() {}

    @Before("sendMessage()")
    public void recordBeforeExecute() {
        System.out.println("start to send message at " + LocalDateTime.now());
    }

    @After("sendMessage()")
    public void recordAfterExecute() {
        System.out.println("message has sent seccess at " + LocalDateTime.now());
    }

    @AfterThrowing("sendMessage()")
    public void recordFailure() {
        System.out.println("fail to send message at " + LocalDateTime.now());
    }

}

接下来需要配置Bean及启用AspectJ注解的自动代理,JavaConfig可以使用@EnableAspectJ-AutoProxy注解启用;Xml可以使用Spring aop命名空间中的<aop:aspectj-autoproxy>元素。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">

    <bean id="messageService" class="com.dqzhou.spring.service.impl.MessageServiceImpl"/>
    <bean id="timeLogging" class="com.dqzhou.spring.aspectj.TimeLogging"/>

    <aop:aspectj-autoproxy/>
</beans>

最后,测试一下调用messageService的sendMessage方法

public class SpringApplicationContext {
    
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        MessageService messageService = (MessageService) applicationContext.getBean("messageService");
        messageService.sendMessage("hello world!");
    }

}

结果如下,Spring通过代理的方式增强方法

start to send message at 2019-10-28T00:16:11.746
hello world!
message has sent seccess at 2019-10-28T00:16:11.748
创建环绕通知

环绕通知相当于在通知方法中同时编写前置和后置通知。
更改切面类如下,ProceedingJoinPoint作为参数。通过它能够调用被通知的方法。显然,环绕通知可以轻易实现其它几个注解所实现的功能,但是如果proceed()方法没有调用,被通知的方法将无法访问。不过可以通过多次调用达到重试场景。

@Aspect
public class TimeLogging {

    // 定义命名的切点
    @Pointcut("execution(* com.dqzhou.spring.service.MessageService.sendMessage(..))")
    public void sendMessage() {}

    @Around("sendMessage()")
    public void watchMessage(ProceedingJoinPoint joinPoint) {
        try {
            System.out.println("start to send message at " + LocalDateTime.now());
            joinPoint.proceed();
            System.out.println("message has sent seccess at " + LocalDateTime.now());
        } catch (Throwable throwable) {
            System.out.println("fail to send message at " + LocalDateTime.now());
        }
    }

}
通过切面引入新功能

之前,Spring AOP通过代理已经能够对被通知的方法进行增强。那么,有没有可能在不破坏,没有嵌入原有类的结构上增加新的方法。
@DeclareParents注解,做个标记,暂不展开

使用XML声明切面

Spring的aop命名空间

  • <aop:advisor>:定义AOP通知器
  • <aop:after>:定义AOP后置通知(不管被通知的方法是否执行成功)
  • <aop:after-returning>
  • <aop:after-throwing>
  • <aop:around>
  • <aop:aspect>:定义一个切面
  • <aop:aspectj-autoproxy>:启用@AspectJ注解驱动的切面
  • <aop:before>
  • <aop:config>:顶层的AOP配置元素。大多数的<aop:*>元素必须包含
    在<aop:config>元素内
  • <aop:declareparents>:以透明的方式为被通知的对象引入额外的接口
  • <aop:pointcut>:定义一个切点

创建一个切面类,不加任何注解

public class LogAspect {
    
    public void logBeforeExecute() {
        System.out.println("method ready to execute at" + LocalDateTime.now());
    }

    public void logAfterExecute() {
        System.out.println("method has executed at" + LocalDateTime.now());
    }

    public void logFailure() {
        System.out.println("execute fail at " + LocalDateTime.now());
    }
    
    public void logAroundMessage(ProceedingJoinPoint joinPoint) {
        try {
            System.out.println("method ready to execute at" + LocalDateTime.now());
            joinPoint.proceed();
            System.out.println("method has executed at" + LocalDateTime.now());
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
}

配置xml

    <bean id="logAspect" class="com.dqzhou.spring.aop.aspectj.LogAspect"/>
    <aop:config>
        <aop:aspect ref="logAspect">
            <aop:before pointcut="execution(* com.dqzhou.spring.aop.service.MessageService.sendMessage(..))" method="logBeforeExecute"/>
            <aop:after pointcut="execution(* com.dqzhou.spring.aop.service.MessageService.sendMessage(..))" method="logAfterExecute"/>
        </aop:aspect>
    </aop:config>

大多数aop配置必须在<aop:config>元素的上下文使用。然后<aop:config>可以声明多个通知。同样的,写多遍切点表达式很麻烦,可以使用<aop:pointcut>指定切点,然后通知通过pointcut-ref属性来引用命名切点,如下:

    <aop:config>
        <aop:aspect ref="logAspect">
            <aop:pointcut expression="execution(* com.dqzhou.spring.aop.service.MessageService.sendMessage(..))" id="logMessage"/>
            <aop:before pointcut-ref="logMessage" method="logBeforeExecute"/>
            <aop:after pointcut-ref="logMessage" method="logAfterExecute"/>
        </aop:aspect>
    </aop:config>

如果使用环绕通知,xml配置如下:

    <aop:config>
        <aop:aspect ref="logAspect">
            <aop:pointcut expression="execution(* com.dqzhou.spring.aop.service.MessageService.sendMessage(..))" id="logMessage"/>
            <aop:around pointcut-ref="logMessage" method="logAroundMessage"/>
        </aop:aspect>
    </aop:config>

总结

面向对象编程通过AOP把应用各处的行为放入可重用的模块中。减少了代码的冗余。Spring提供了AOP的框架,可以通过XML或注解的方式快速进行配置。但是如果SpringAOP不能满足需求的时候,需要专项更为强大的AspectJ。

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

推荐阅读更多精彩内容