前言
AOP是一种思想,并不是Spring独有的,所有符合AOP思想都可以看作AOP的实现。在进入Spring AOP之前,先对它的一些术语做一个了解,它们是构成Spring AOP的基本组成部分。
名称 | 说明 |
切面(Aspect) | 切面是对象操作过程中的截面,实际上"切面"是一段程序代码,这段代码将被"植入"到程序本身的流程中去,所谓的"面向切面编程"正是指这个。 |
连接点(join point) | 连接点是指对象操作过程中的某个阶段点,因为Spring只支持方法,所以被拦截的对象往往就是指特定的方法。 |
切入点(point cut) | 切入点是连接点(join point)的集合。有时候,我们的切面不单单应用于单个方法,也有可能是多个类的不同方法,这时,可以通过正则表达式和指示器的规则去定义,从而适配连接点(join point)。 |
通知(Advice) | 通知是某个切入点被横切后,所采取的处理逻辑。分为前置通知(before advice)、后置通知(after advice)、环绕通知(around advice)、事后返回通知(afterReturning advice)和异常通知(afterThrowing advice)。 |
目标对象(target) | 所有被通知的对象(也可以理解为所有被代理对象)都是目标对象。目标对象被AOP关注,它的属性被改变会被关注,它的行为(方法)的调用会被关注,它的方法传参的变化也会被关注。 |
织入(Weaving) | 织入是将切面功能应用到目标对象的过程。由代理工厂创建一个代理对象,这个代理可以为目标对象执行切面功能。AOP的思想中有3种织入方式:编译期织入、类加载期织入和执行期织入,Spring AOP一般多见于执行期织入。 |
引入(Introduction) | 对一个已编译完类(class),在运行期,动态地向这个类中加载属性和方法。 |
环境搭建
pom文件(这里只使用AOP,所以不用引入Spring的其它组件,这篇文章讨论的是实现AOP的xml和注解形式,所以直接引入最高版本)
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
项目结构,需要在配置文件中开启aop和bean的命名空间(namespaces)
Spring AOP的简单实现过程
public class Target {
//程序执行的方法
public void execute(String name) {
System.out.println("程序开始执行 : " + name);
}
}
---------------------分割线---------
public class AspectExecute implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
before();
invocation.proceed();//执行目标对象
return null;
}
public void before() {
System.out.println("执行前置通知...");
}
}
-----------------------分割线----------------
public class Test {
public static void main(String[] args) {
ProxyFactory proxyFactory = new ProxyFactory();//创建代理工厂
proxyFactory.addAdvice(new AspectExecute());
proxyFactory.setTarget(new Target());
Target target = (Target) proxyFactory.getProxy();
target.execute("目标方法的形参");
}
}
---------------------执行结果-------------
执行前置通知...
程序开始执行 : 目标方法的形参
利用XML实现AOP
1、前置通知、后置通知、事后返回通知、异常通知,环绕通知稍特殊,后面单独示例
a、创建一个User实体类和服务层对象UserService(目标对象):
public class User {
private String name;
private String gender;
//省略set/get方法和构造方法
}
-------------分割线------------
public class UserService {
public void buildUser() {
System.out.println("我的名字是zepal");
System.out.println("我是男性");
}
//重载一下buildUser()方法
public void buildUser(User user) {
System.out.println("我的名字是" + user.getName());
System.out.println("我的性別是" + user.getGender());
}
public User getUser() {
User user = new User("張三丰", "男");
return user;
}
}
b.创建切面对象
public class UserAspect {
public void beforeMethod() {
System.out.println("执行前置通知方法...");
}
public void afterMethod() {
System.out.println("执行后置通知方法....");
}
public void afterReturningMethod() {
System.out.println("执行返回通知方法....");
}
public void afterThrowingMethod() {
System.out.println("执行异常通知方法......");
}
}
c、Spring配置文件
<bean id="userService" class="com.zepal.aop.target.UserService"/>
<bean id="userAspectBean" class="com.zepal.aop.aspect.UserAspect"/>
<aop:config>
<aop:aspect id="userAspect" ref="userAspectBean">
<!-- 注意*后面有个空格 -->
<aop:before method="beforeMethod" pointcut="execution(* com.zepal.aop.target.UserService.buildUser(..))"/>
<aop:after method="afterMethod" pointcut="execution(* com.zepal.aop.target.UserService.buildUser(..))"/>
<aop:after-returning method="afterReturningMethod" pointcut="execution(* com.zepal.aop.target.UserService.buildUser(..))"/>
<aop:after-throwing method="afterThrowingMethod" pointcut="execution(* com.zepal.aop.target.UserService.buildUser(..))"/>
</aop:aspect>
</aop:config>
配置文件说明:<aop:config>是配置切面的顶级标签,<aop:aspect>标签是指定切面对象的Bean交给Spring管理,它下面的子标签就是配置具体的通知,除了上面的通知,<aop:around/>环绕通知。method属性是指定具体的切面方法名称,没有(),pointcut指定具体的切入点,以正则表达式去匹配。
execution(* com.zepal.aop.target.UserService.buildUser(..))
注意*后面有个空格
-execution 表示在执行的时候,拦截里面的正则匹配方法;
-* 表示任意返回类型
-com.zepal.aop.target.UserService 以全路径指定目标对象
-buildUser 指定目标方法
-(..) 表示任意形参,注意UserService目标对象中的重载方法
这样,spring就可以通过这个正则表达式知道你要对哪个目标对象进行AOP增强了。
对于这个正则表达式而言,我们还可以使用AspectJ指示器(AspectJ是spring2.0以后的新特性,AspectJ只是增强了切入点解析和匹配的功能,AOP在运行时还是纯粹的Spring AOP)。具体描述如下:
项目类型 | 描述 |
arg() | 限定连接点方法参数 |
@args() | 通过连接点方法参数上的注解进行限定 |
execution() | 用于匹配连接点的执行方法 |
this() | 限制连接点匹配AOP代理Bean引用唯指定的类型 |
target | 目标对象(即被代理对象) |
@target() | 限制目标对象的配置了指定的注解 |
within | 限制连接点匹配指定的类型 |
@within() | 限制连接点带有匹配注解类型 |
@annotation() | 限制带有指定注解的连接点 |
例如,我们目标对象所在包的所有对象都有buildUser()方法,但是我们只想要被AOP代理的目标Bean。
execution(* com.zepal.aop.target.*.buildUser(..) && this('userService'))
表达式中,&&是并且的意思,this()中定义的字符串就是被AOP代理、且由Spring管理的Bean。
d、执行AOP测试用例
public class Test {
public static void main(String[] args) {
ApplicationContext context = new FileSystemXmlApplicationContext("classpath:applicationContext.xml");
UserService userService = context.getBean(UserService.class);
userService.buildUser();
System.out.println("-------分割线--------------");
userService.buildUser(new User("zepal", "男"));
}
}
执行前置通知方法...
我的名字是zepal
我是男性
执行后置通知方法....
执行返回通知方法....
-------分割线--------------
执行前置通知方法...
我的名字是zepal
我的性別是男
执行后置通知方法....
执行返回通知方法....
很明显,我们的目标对象UserService中,被重载的方法都被匹配上了。但是我们在切面对象UserApsect中定义了前置通知、后置通知、返回通知和异常通知,这里的结果没有异常通知,且返回通知一定在后置通知之后吗?而且在切面通知中都是使用的单个连接点,
接下来,我们将示例改造一下,使用切入点,并将正则表达式改造为匹配目标对象UserService的所有方法。
//目标对象的 buildUser()方法增加一个判断
public void buildUser(User user) {
if(user == null) {
throw new RuntimeException("user is null...");
}
System.out.println("我的名字是" + user.getName());
System.out.println("我的性別是" + user.getGender());
}
-----------
<bean id="userService" class="com.zepal.aop.target.UserService"/>
<bean id="userAspectBean" class="com.zepal.aop.aspect.UserAspect"/>
<aop:config>
<!-- 注意*后面有个空格,后面的*表示匹配UserService下所有方法 -->
<aop:pointcut id="userPointcut" expression="execution(* com.zepal.aop.target.UserService.*(..))" />
<aop:aspect id="userAspect" ref="userAspectBean">
<aop:before method="beforeMethod" pointcut-ref="userPointcut"/>
<aop:after method="afterMethod" pointcut-ref="userPointcut"/>
<aop:after-returning method="afterReturningMethod" pointcut-ref="userPointcut"/>
<aop:after-throwing method="afterThrowingMethod" pointcut-ref="userPointcut"/>
</aop:aspect>
</aop:config>
ApplicationContext context = new FileSystemXmlApplicationContext("classpath:applicationContext.xml");
UserService userService = context.getBean(UserService.class);
userService.buildUser();//无参
System.out.println("-------分割线1--------------");
userService.buildUser(new User("zepal", "男"));//有参
System.out.println("--------分割线2---------");
User user = userService.getUser();//有返回值
System.out.println(user.toString());
System.out.println("----------分割线3-------");
userService.buildUser(null);//参数为null,会抛异常
-----------------------执行结果-----------------
执行前置通知方法...
我的名字是zepal
我是男性
执行后置通知方法....
执行返回通知方法....
-------分割线1--------------
执行前置通知方法...
我的名字是zepal
我的性別是男
执行后置通知方法....
执行返回通知方法....
--------分割线2---------
执行前置通知方法...
执行后置通知方法....
执行返回通知方法....
User [name=張三丰, gender=男]
----------分割线3-------
执行前置通知方法...
执行后置通知方法....
执行异常通知方法...... //这里出现异常通知了
Exception in thread "main" java.lang.RuntimeException: user is null...
可以看到返回通知(afterReturning advice)总是在后置通知(after advice)之后执行(注意,但是会被环绕通知影响,看下面的示例),前置通知(beforeAdevice)和后置通知(after advice)总会执行,但是一旦目标方法出现异常,不能产生返回值,则返回通知(afterReturning advice)不会执行,但是异常通知(afterThrowing advice)会被触发。
2、环绕通知
环绕通知(Around Advice)是所用通知(Advice)中最为强大的通知,强大到难以控制。
在切面对象UserAspect和配置文件中增减环绕通知
public void aroundMethod() {
System.out.println("执行环绕通知......");
}
--------配置文件---------
<aop:around method="aroundMethod" pointcut-ref="userPointcut"/>
ApplicationContext context = new FileSystemXmlApplicationContext("classpath:applicationContext.xml");
UserService userService = context.getBean(UserService.class);
userService.buildUser();
System.out.println("-------分割线1---------");
userService.buildUser(new User("赵敏", "女"));
System.out.println("-------分割线2---------");
userService.buildUser(null);
-----------------------执行结果-------------------------
执行前置通知方法...
执行环绕通知......
执行返回通知方法....
执行后置通知方法....
-------分割线1---------
执行前置通知方法...
执行环绕通知......
执行返回通知方法....
执行后置通知方法....
-------分割线2---------
执行前置通知方法...
执行环绕通知......
执行返回通知方法....
执行后置通知方法....
在上面的执行过程中,你会发现返回通知在后置通知之前执行了,然后任意更改<aop:config></aop:config>中环绕通知的配置顺序,你会发现,后置通知和返回通知顺序又不一样,而且很重要的一点,目标对象的方法没了。接下来处理这个问题。
3、ProceedingJoinPoint和JoinPoint接口
它是一个被Spring封装过的对象,可以实现对目标函数的操作。它有一个父类接口JoinPoint,它们的使用就像Servlet的内置对象一样,声明即可。根据继承的约定,子类可以调用父类的方法,父类不能调用子类的,所以一般直接使用ProceedingJoinPoint接口即可。常用方法有(以下),
方法 | 描述 |
proceed() | 实现回调目标函数,返回目标函数的返回值,ProceedingJoinPoint接口的方法 |
proceed(Object args) | 实现回调目标函数,并且替换掉目标函数的传递形参,返回目标函数的返回值,ProceedingJoinPoint接口的方法 |
getArgs() | 获取目标函数传递的形参,返回Object[],JoinPoint接口的方法 |
getSignature() | 返回目标方法的方法签名接口。可以通过签名接口Signature获取目标方法的注解、方法名之类的。JoinPoint接口的方法 |
针对上面的问题,在环绕通知的切面方法中作以下更改:
public void aroundMethod(ProceedingJoinPoint pj) throws Throwable {
System.out.println("执行环绕通知......");
pj.proceed();//回调目标函数
}
----------------------执行上面相同的测试代码---------
执行前置通知方法...
执行环绕通知......
我的名字是zepal
我是男性
执行返回通知方法....
执行后置通知方法....
-------分割线1---------
执行前置通知方法...
执行环绕通知......
我的名字是赵敏
我的性別是女
执行返回通知方法....
执行后置通知方法....
-------分割线2---------
执行前置通知方法...
执行环绕通知......
执行异常通知方法......
执行后置通知方法....
Exception in thread "main" java.lang.RuntimeException: user is null...
所以说,一般而言,使用环绕通知的场景是在你需要大幅度修改目标方法的逻辑的时候使用。假如把目标方法看作一个整体,环绕通知可以任意的在这个整体上切入任意的逻辑代码,就像环绕着目标方法一样,所以使用环绕通知一定要回调目标方法。另外,有了环绕通知,就要避免前置通知和后置通知的切入。
4、多个切面
上面的示例是基于一个切面在运行,而事实是Spring是支持多个切面的运行,类似于拦截器链的形式。
假如我们的目标方法targetMethod有A、B、C三个切面,我要可以定义它们的执行顺序,通过<aop:aspect>标签中的order属性,像这样:
<aop:aspect order="1" id="userAspectA" ref="userAspectABean">
<!-- ..... -->
</aop:aspect>
<aop:aspect order="2" id="userAspectB" ref="userAspectBBean">
<!-- ..... -->
</aop:aspect>
<aop:aspect order="3" id="userAspectC" ref="userAspectCBean">
<!-- ..... -->
</aop:aspect>
它们的运行顺序就是按order属性值,由小到大依次执行。
利用注解实现AOP
1、沿用XML形式的项目,不需要增加其它任何东西。
目标对象
public class CigaretteService {
public void buildCigarette() {
System.out.println("香烟有过滤嘴");
System.out.println("香烟有烟丝");
}
}
切面对象(利用@Aspect声明当前类是一个切面类)
@Aspect
public class CigaretteAspect {
@Before("execution(* com.zepal.aop.target.CigaretteService.buildCigarette(..))")
public void beforeMethod() {
System.out.println("前置通知-->烟丝由人种植");
}
@After("execution(* com.zepal.aop.target.CigaretteService.buildCigarette(..))")
public void afterMethod() {
System.out.println("后置通知-->香烟由烟草公司销售");
}
}
配置类如下。注意:@EnableAspectJAutoProxy注解一定不要掉了,否则AOP会无效。这个注解等价于
<aop:aspectj-autoproxy />,为了让spring能识别@Aspect进行自动切入。有两个属性,都是Boolean类型,默认都是false。proxyTargetClass为true 的话使用cglib,为false的话使用java的Proxy。
exposeProxy参数控制代理的暴露方式,解决内部调用不能使用代理的场景.
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
@Bean(name="cigaretteService")
public CigaretteService initCigaretteService() {
return new CigaretteService();
}
@Bean(name="cigaretteAspect")
public CigaretteAspect initCigaretteAspect() {
return new CigaretteAspect();
}
}
测试和结果
public class Test {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
CigaretteService cigaretteService = context.getBean(CigaretteService.class);
cigaretteService.buildCigarette();
}
}
------------------------执行结果--------------------
前置通知-->烟丝由人种植
香烟有过滤嘴
香烟有烟丝
后置通知-->香烟由烟草公司销售
说明:@EnableAspectJAutoProxy注解一定不要漏掉,否则AOP无效。
2、在切面类CigaretteAspect中,上面的示例只有前置通知和后置通知,其它通知和用切点示例如下,这里就不执行测试了,和XML的逻辑一模一样,:
@Pointcut("execution(* com.zepal.aop.target.CigaretteService.buildCigarette(..))")
public void pointCut() {
//切点
}
@AfterReturning(pointcut="pointCut()")
public void afterReturningMethod() {
//时候返回通知
}
@AfterThrowing(pointcut="pointCut()")
public void afterThrowingMethod() {
//异常通知
}
@Around("pointCut()")
public void aroundMethod(ProceedingJoinPoint pj) {
//环绕通知,不要忘了回调目标方法
}
3、同一个目标对象有多个切面,类似于拦截器链的方式,需要@order注解指定切面的执行顺序,在xml形式中是order作为<aop:aspect>标签的属性存在,按@Order属性值从小到大依次执行。如下:
@Aspect
@Order(1)
public class CigaretteAspectA {
//切面1
}
-------------------
@Aspect
@Order(2)
public class CigaretteAspectB {
//切面2
}
------------------
@Aspect
@Order(3)
public class CigaretteAspectC {
//切面3
}
此篇完结