SpringMVC干货系列:从零搭建SpringMVC+mybatis(四):Spring两大核心之AOP学习

前言

上一篇我们介绍了Spring的核心概念DI,DI有助与应用对象之间的解耦。今天我们就来介绍下另一个非常核心的概念,面向切面编程AOP。

正文

在软件开发中,散布于应用中多处的功能被称为横切关注点(cross-cutting concern)。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的。比如:日志、声明式事物、安全和缓存。这些东西都不是我们平时写代码的核心功能,但许多地方都要用到。

把这些横切关注点与业务相分离正是面向切面编程(AOP)索要解决的问题。

简单的说就是把这些许多地方都要用到,但又不是核心业务的功能,单独剥离出来封装,通过配置指定要切入到指定的方法中去。

什么是面向切面编程


如上图所示,这就是横切关注点的概念,水平的是核心业务,这些切入的箭头就是我们的横切关注点。
横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。这样做有两个好处:

  • 首先,现在每个关注点都集中于一个地方,而不是分割到多处代码中
  • 其次,服务模块更简洁,因为它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被转移到切面中了。

定义AOP术语

为了理解AOP,我们必须先了解AOP的相关术语,很简单不难:

通知(Advice)
在AOP中,切面的工作被称为通知。通知定义了切面“是什么”以及“何时”使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。

Spring切面可以应用5种类型的通知:

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

连接点(Join point)
连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加行为。

切点(Pointcut):
如果说通知定义了切面“是什么”和“何时”的话,那么切点就定义了“何处”。比如我想把日志引入到某个具体的方法中,这个方法就是所谓的切点。

切面(Aspect)
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容———他是什么,在何时和何处完成其功能。

引入(Introduction)
引入允许我们向现有的类添加新的方法和属性(Spring提供了一个方法注入的功能)。

织入(Weaving)
把切面应用到目标对象来创建新的代理对象的过程,织入一般发生在如下几个时机:

  • 编译时:当一个类文件被编译时进行织入,这需要特殊的编译器才可以做的到,例如AspectJ的织入编译器
  • 类加载时:使用特殊的ClassLoader在目标类被加载到程序之前增强类的字节代码
  • 运行时:切面在运行的某个时刻被织入,SpringAOP就是以这种方式织入切面的,原理应该是使用了JDK的动态代理技术

Spring对AOP的支持

创建切入点来定义切面所织入的连接点是AOP框架的基本功能。
Spring提供了4种类型的AOP支持:

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

前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。

这里我不准备介绍经典Spring AOP,因为引入了简单的声明式AOP和基于直接的AOP后,Spring经典的AOP看起来就显得非常笨重和过于复杂。

对于新手入门来说,我们不需要知道这么多,在这里我也只介绍2,3两种方式,简单的说就是一个基于xml配置,一个基于注解。

下面就直接开始举两个例子分别来介绍下这两种AOP方式,我们就拿简单的日志来说明。

基于注解的方式

首先基于注解的方式需要引入这些包,对用的pom.xml如下:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>4.1.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.8</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.8</version>
</dependency>

我们还是举前面用到的UserController来说明,下面方法很简单,执行进入这个方法的时候会打印“进来了”信息,现在我打算给这个方法加日志,在执行该方法前打印“进来前”,在执行完方法后执行“进来后”。

package com.tengj.demo.controller;

@Controller
@RequestMapping(value="/test")
public class UserController {
    @Autowired
    UserService userService;
    
    @RequestMapping(value="/view",method = RequestMethod.GET)
    public String index(){
        userService.sayHello("tengj");
        return "index";
    }
}

servie层代码:

package com.tengj.demo.service
public interface UserService {
    public void sayHello(String name);
}

servie实现类代码:

package com.tengj.demo.service.impl;
@Service("userService")
public class UserServiceImpl implements UserService{
    @Override
    public void sayHello(String name) {
        System.out.println("hello,"+name);
    }
}

上面方法index()其实就是我们之前定义的切点,表示在哪里切入AOP。



如图所示,我们使用execution()指示器选择UserServiceImpl的sayHello方法。方法表达式以“*”号开始,表明了我们不关心方法返回值的类型。然后,我们指定了全限定类名和方法名。对于方法参数列表,我们使用两个点号(..)表明切点要选择任意的sayHello()方法,无论该方法的入参是什么。

接下来我们要定义个切面,也就是所谓的日志功能的类。

package com.tengj.demo.aspect;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component //注入依赖
@Aspect //该注解标示该类为切面类
public class LogAspect {
    @Pointcut("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
    public void logAop(){}

    @Before("logAop() && args(name)")
    public void logBefore(String name){
        System.out.println(name+"前置通知Before");
    }

    @AfterReturning("logAop()")
    public void logAfterReturning(){
        System.out.println("返回通知AfterReturning");
    }

    @After("logAop() && args(name)")
    public void logAfter(String name){
        System.out.println(name+"后置通知After");
    }

    @AfterThrowing("logAop()")
    public void logAfterThrow(){
        System.out.println("异常通知AfterThrowing");
    }
}

上面就是切面类的代码,很简单,这里用到了前面提的通知的几种类型。
这样就能实现切入功能了

@Pointcut("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
public void logAop(){}

这里的@Pointcut注解是为了定义切面内重用的切点,也就是说把公共的东西抽出来,定义了任意的方法名称logAop,这样下面用到的各种类型通知就只要写成

@Before("logAop() && args(name)")
@AfterReturning("logAop()")
@AfterThrowing("logAop()")

这样既可,否则就要写成

@Before("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
@AfterReturning("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
@AfterThrowing("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")

大家是否注意到了@Before("logAop() && args(name)")这里多出来个&& args(name),这个是用来传递参数的,定义只要跟sayHello参数名称一样就可以。

如果就此止步的话,LogAspect只会是Spring容器中的一个Bean,即便使用了AspectJ注解,但它并不会被视为切面,这些注解不会解析,也不会创建将其转换为切面的代理。

所以需要在XML里面配置一下,需要使用Spring aop命名空间中的<aop:aspectj-autoproxy/>元素,简单如下:

<?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:mvc="http://www.springframework.org/schema/mvc"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context-4.1.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd"
       default-lazy-init="true">
    <context:component-scan base-package="com.tengj.demo"/>
    <mvc:resources location="/WEB-INF/pages/" mapping="/pages/**"/>
    <!-- 默认的注解映射的支持 -->
    <mvc:annotation-driven/>
    <!--启用AspectJ自动代理-->
    <aop:aspectj-autoproxy/>
    <!-- 视图解析器 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/pages/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>

接着就可以启动工程,访问index这个方法,http://localhost:8080/SpringMVCMybatis/test/view
执行结果:

tengj前置通知Before
hello,tengj
tengj后置通知After
返回通知AfterReturning

根据前面学的我们知道,除了上面提到的通知外,还有一个更强大通知类型,就是环绕通知。可以自定义我们需要切入的位置,可以替代上面提到的所有通知。看例子:

@Around("logAop()")
public void logAround(ProceedingJoinPoint jp){
    try {
        System.out.println("自定义前置通知Before");
        jp.proceed();//将控制权交给被通知的方法,也就是执行sayHello方法
        System.out.println("自定义后置通知After");
    } catch (Throwable throwable) {
        System.out.println("异常处理~");
        throwable.printStackTrace();
    }
}

执行结果:

自定义前置通知Before
hello,tengj
自定义后置通知After

这里主要是通过ProceedingJoinPoint这个参数。其中里面的proceed()方法就是将控制权交给被通知的方法。如果你忘记调用这个方法,那么你的通知实际上会阻塞对被通知方法的调用。

有意思的是,你可以不调用proceed()方法,从而阻塞堆被通知方法的访问,与之类似,你也可以在通知中对它进行多次调用。要这样做的一个场景就是实现重试逻辑,也就是在被通知方法失败后,进行重复尝试。

基于XML配置的方式

这里介绍使用XML配置的方式来实现,在Spring的aop命名空间中,提供了多个元素用来在XML中声明切面。

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

我们已经看过了<aop:aspectj-autoproxy/>元素,它能够自动代理AspectJ注解的通知类。aop命名空间的其他元素能够让我们直接在Spring配置中声明切面,而不需要使用注解。
所以,我们重新来看看一下这个LogAspect类,这次我们将它所有的AspectJ注解全部移除掉:

package com.tengj.demo.aspect;

public class LogAspect {
    public void logBefore(String name){
        System.out.println(name+"前置通知Before");
    }

    public void logAfterReturning(String name){
        System.out.println("返回通知AfterReturning");
    }

    public void logAfter(String name){
        System.out.println(name+"后置通知After");
    }

    public void logAfterThrow(String name){
        System.out.println("异常通知AfterThrowing");
    }
}

然后在xml配置文件中使用Spring aop命名空间中的一些元素,详细基本配置参考上面注解方式中的xml配置,这里是贴出来关键的代码:

<bean id="logAspect" class="com.tengj.demo.aspect.LogAspect" />
<aop:config>
        <aop:aspect id="log"  ref="logAspect">
            <aop:pointcut id="logAop" expression="execution(* com.tengj.demo.service.impl.UserServiceImpl.sayHello(..)) and args(name)"/>
            <aop:before method="logBefore" pointcut-ref="logAop"/>
            <aop:after method="logAfter"  pointcut-ref="logAop"/>
            <aop:after-returning method="logAfterReturning"  pointcut-ref="logAop"/>
            <aop:after-throwing method="logAfterThrow" pointcut-ref="logAop"/>
            <!--<aop:around method="logAfterThrow"  pointcut-ref="logAop"/>-->
        </aop:aspect>
</aop:config>

配置也 很好理解

  • xml里面配置aop,都是放在<aop:config>里面
  • 然后使用<aop:aspect>一个切面,指向具体的bean类。
  • 使用<aop:pointcut>定义切点,基本跟注解的很像,其中要注意的是xml配置里面如果要带参数的,用的不再是&&,要使用and关键字才行(因为在XML中,“&”符号会被解析为实体的开始)
  • 然后就是使用各种通知标签了,简单。

执行效果如下:

tengj前置通知Before
hello,tengj
tengj后置通知After
返回通知AfterReturning

环绕通知也很简单,直接贴代码:
xml配置:

<aop:around method="logAround"  pointcut-ref="logAop"/>

切面方法:

public void logAround(ProceedingJoinPoint jp,String name){
    try {
        System.out.println(name+"自定义前置通知Before");
        jp.proceed();
        System.out.println(name+"自定义后置通知After");
    } catch (Throwable throwable) {
        System.out.println("异常处理~");
        throwable.printStackTrace();
    }
}

执行结果:

tengj自定义前置通知Before
hello,tengj
tengj自定义后置通知After

总结

Spring AOP是Spring学习中最关键的,我总结的这2种写法也是开发中最常用的。也不知道大家能不能理解~看得时候如果有不懂的地方可以提出来,我好修改一下,让更多的人理解并掌握AOP,希望对你有所帮助。


一直觉得自己写的不是技术,而是情怀,一篇篇文章是自己这一路走来的痕迹。靠专业技能的成功是最具可复制性的,希望我的这条路能让你少走弯路,希望我能帮你抹去知识的蒙尘,希望我能帮你理清知识的脉络,希望未来技术之巅上有你也有我。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • 本章内容: 面向切面编程的基本原理 通过POJO创建切面 使用@AspectJ注解 为AspectJ切面注入依赖 ...
    谢随安阅读 3,132评论 0 9
  • AOP实现可分为两类(按AOP框架修改源代码的时机): 静态AOP实现:AOP框架在编译阶段对程序进行修改,即实现...
    数独题阅读 2,310评论 0 22
  • 以我的观念来看,人总是有颗争强好胜的心,或多或少,时强时弱。 当你的付出与回报不成正比的时候,当你求而不得...
    Tawnie涵阅读 248评论 0 0