# 前言
上一篇我们介绍了 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,希望对你有所帮助。