1.概述
Aop(Aspect Oriented Programming),即面向切面编程,这是面向对象思想的一种补充,面向切面编程就是在程序运行时,在不改变程序源码的情况下,动态的增强方法的功能。
Aop的使用场景非常多,例如:日志 、事物等等
2.概念
Aop的世界有很多概念,其中重要的有切面、通知、连接点、切点、织入。
切面(Aspect)
横切关注点可以被模块化为特殊的类,这些类被称为切面
一个切面包括切点和通知
通知(Advice)
一个切面的工作被称为通知,它定义了切面要增强的具体内容
Spring切面可以有5种类型的通知:
-
前置通知(Before)
通知方法在目标方法被调用前执行 -
后置通知/最终通知(After)
通知方法在目标方法返回或者抛出异常后执行
注意
⚠️:
1.不管目标方法的返回值类型是否为void
2.不管目标方法有没有返回确切的值还是null
-
返回通知(After-returning)
通知方法在目标方法返回后执行
注意
⚠️:
1.目标方法的返回值类型不能为void
,否则无此通知
2.不管目标方法有没有返回确切的值还是null
-
异常通知(After-throwing)
通知方法在目标方法抛出异常后执行 -
环绕通知(Around)
通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
实际上就像在一个通知方法中同时编写前置通知和后置通知
通俗的理解:通知定义了切面“干什么”、“什么时候干”的问题
连接点(Join point)
我们的应用程序可能有各种时机应用通知,这些时机被称为连接点
切点(Pointcut)
一个切面并不是需要通知应用中的所有连接点,我们的某个切面真正通知的那个连接点被称为切点
通俗的理解:切点定义了切面“在哪里干”的问题
织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程
切面在指定的连接点织入到目标对象中
在目标对象的生命周期里有多个点可以进行织入:
-
编译期
切面在目标类编译时被织入
这种方式需要特殊的编译器,AspectJ的织入编译器就是以这种方式织入切面的 -
类加载期
切面在目标类加载到JVM时被织入
这种方式需要特殊的类加载器(ClassLoader),它可以 在目标类被引入应用之前增强该目标类的字节码,AspectJ5的加载时织入就支持以这种方式织入切面 -
运行期
切面在应用运行的某个时刻被织入
一般情况下,在织入切面时,容器会为目标对象动态的创建一个代理对象
Spring AOP就是以这种方式织入切面的
3.Spring对AOP的支持
并不是所有的AOP框架都是相同的,它们在连接点模型上可能有强弱之分
有些框架允许在字段修饰符级别应用通知,而有一些框架只支持方法调用级别的连接点。除此之外,它们织入切面的方式和时机也所有不同。
Spring和AspectJ项目之间有大量的协作,而且Spring对AOP的支持也在很多方面借鉴了AspectJ项目。
Spring提供了4种类型的AOP支持:
- 基于代理的经典Spring AOP
- 纯POJO切面
- @AspectJ注解驱动的切面
- 注入式AspectJ切面(适用于Spring各个版本)
SpringAOP构建在动态代理的基础上,因此Spring对AOP的支持局限于方法拦截
。
Spring的通知都是Java编写的,定义通知所应用的切点通常会使用注解或者在Spring的配置文件里采用XML来编写
Spring在运行期把切面织入到Spring管理的bean中,实际上Spring是基于动态代理实现的,代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean,当代理拦截到方法调用是,在调用目标bean方法之前会执行切面逻辑。
4.基于注解声明切面
在Spring AOP中,要使用AspectJ的切点表达式语言来定义切点,但是Spring仅支持AspectJ切点指示器的一个子集
SpirngAOP支持的AspectJ切点指示器有:
AspectJ指示器 | 描述 |
---|---|
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的bean引用为指定类型的类 |
target | 限制连接点匹配目标对象为指定类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解 |
within() | 限制连接点匹配的指定类型 |
@withinn() | 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义由指定的注解所标注的类里) |
@annotation | 限定匹配带有指定注解的连接点 |
在Spring中尝试使用AspectJ其他指示器,将会抛出IllegalArgumentException异常
如上表格中所展示的Spring支持的指示器,注意只有execution
指示器是实际执行匹配的,其他指示器都是用来限制匹配的(execution指示器是我们在编写切点定义时最主要使用的指示器)
4.1编写切点
为了演示Spring的AOP,我们定义了如下一个接口:
public interface IUserService {
/**
* 根据ID获取用户
*
* @param id 主键ID
* @return 用户信息
*/
User getById(Integer id);
/**
* 保存用户信息
*
* @param user 用户信息
* @return 影响的行数
*/
int saveUser(User user);
}
假设我们希望在getById()方法触发通知的调用,那么切点表达式写法如下:
4.2使用注解创建切面
使用注解创建切面是AspectJ 5所引入的关键特性。
Spring的切面是一个纯Java类,@Aspect注解能够表明一个一个类不仅仅是一个POJO,还是一个切面:
package com.tp.aop;
import com.tp.models.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* FileName: UserServiceOperationAspect
* Author: TP
* Description:SpringAop-Service层切面测试
*/
@Slf4j
@Aspect
@Component
public class UserServiceOperationAspect {
/**
* 切点定义
*/
@Pointcut("execution(* com.tp.service.IUserService.getById(..))")
public void serviceGetById() {
}
/**
* 测试service前置通知
*
* @param id 用户主键ID
*/
@Before("serviceGetById() && args(id)")
public void getUserBefore(Integer id) {
log.info(">>>>AOP-Service:前置通知,请求参数id为{}", id);
}
/**
* 测试service后置通知
*
* @param id 请求参数
*/
@After("serviceGetById() && args(id)")
public void getUserAfter(Integer id) {
log.info(">>>>AOP-Service:后置通知,请求参数id为{}", id);
}
/**
* 测试service返回通知
*
* @param id 主键ID
* @param user 用户信息
*/
@AfterReturning(pointcut = "serviceGetById() && args(id)", returning = "user")
public void getUserAfterReturning(Integer id, User user) {
log.info(">>>>AOP-Service:返回通知,请求参数id为{},响应结果User信息为:{}", id, user);
}
/**
* 测试service异常通知
*
* @param id 请求参数
*/
@AfterThrowing(pointcut = "serviceGetById() && args(id)", throwing = "e")
public void getUserAfterTrowing(Integer id, Exception e) {
log.info(">>>>AOP-Service:异常通知,请求参数id为{},异常信息:{}", id, e);
}
/**
* 测试service环绕通知
* 需要注意的是,对于环绕通知必须给一个ProceedingJoinPoint参数,并且放在参数的第一位
* 方法体中必须调用proceedingJoinPoint.proceed()方法,否则会阻塞目标方法的执行
*
* @param proceedingJoinPoint proceedingJoinPoint
* @param id 请求参数id
* @return 返回值,这里要注意:Around通知返回值不要写成void(除非目标方法返回值为void),否则最终返回结果为void
*/
@Around("serviceGetById() && args(id)")
public Object getUserAround(ProceedingJoinPoint proceedingJoinPoint, Integer id) throws Throwable{
log.info(">>>>AOP-Service:环绕通知-开始,请求参数id为{}", id);
Object result = proceedingJoinPoint.proceed();
log.info(">>>>AOP-Service:环绕通知-结束,请求参数id为{}", id);
return result;
}
}
我们在serviceGetById()方法上添加了@Pointcut注解,并且为@Pointcut注解设置的值是一个切点表达式,serviceGetById()方法的实现并不重要,这个方法本身只是一个标识,供@Pointcut依附。
同时我们需要将UserServiceOperationAspect声明个一个Spring管理的bean,所以我们增加了注解@Component。
如果你就此止步的话,UserServiceOperationAspect只会是Spring容器中的一个bean,即使使用了@Aspect对它进行了标注,但是它还没有真正成为一个可用的切面,这些注解不会解析也不会创建将其转换为切面的代理,我们还需要做的一件事是:启用AspectJ自动代理功能
如果你使用JavaConfigg的话,可以在配置类的类级别上通过使用@EnableAspectJAutoProxy注解启动自动代理功能:
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class AspectJConfig {
//声明bean
//.....
}
如果你使用Spring的xml装配bean,那么需要使用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: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 https://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:aspectj-autoproxy />
<!--bean声明略-->
</beans>
无论你使用JavaConfig还是XML,AspectJ自动代理都会为使用@Aspect注解的bean创建一个代理,这个代理会围绕着所有该切面的切点所匹配的bean。
值得注意的是,对于环绕通知,它是最为强大的通知类型,它能够让你编写的逻辑将被通知的目标方法完全包装起来,实际上就像在一个通知方法中同时编写前置通知和后置通知。
另外,在使用环绕通知的时候,我们一般都会在通知的形参上增加一个ProceedingJoinPoint参数,并且保证这个参数在第一个位置上,我们需要在通知中通过它来调用被通知的方法,通知方法中我们可以做任何事情作为增强逻辑,当要将控制权交给被通知的方法时,需要调用ProceedingJoinPoint的peoceed()方法,如果不调用proceed()方法,会阻塞对被通知方法的访问。
4.3运行测试观察效果
我们启动服务,让IUserService的getById()方法得以执行,观看效果:
我们将IUserService的实现类中增加一个int i = 2/0;
让程序出现异常,观察异常通知效果:
@Override
public User getById(Integer id) {
if (null != id && id > 0) {
int i = 2/0;
return userMapper.getById(id);
}
return null;
}
效果:
5.使用XML声明切面
当然我们也可以使用Xml声明切面,我们再写一个切面类XmlUserServiceOperationAspect,这次我们不使用注解的开发模式,具体代码如下:
package com.tp.aop;
import com.tp.models.entity.User;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
/**
* FileName: XmlUserServiceOperationAspect
* Author: TP
* Description:基于XML形式声明切面,切面测试
*/
public class XmlUserServiceOperationAspect {
private static final Logger log = LoggerFactory.getLogger(XmlUserServiceOperationAspect.class);
/**
* 测试service前置通知
*
* @param joinPoint 连接点信息
*/
public void getUserBefore(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
String methodName = joinPoint.getSignature().getName();
log.info(">>>>AOP-Service:前置通知,方法名:{},请求参数:{}", methodName, Arrays.toString(args));
}
/**
* 测试service后置通知
*
* @param joinPoint 连接点信息
*/
public void getUserAfter(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
String methodName = joinPoint.getSignature().getName();
log.info(">>>>AOP-Service:后置通知,方法名:{},请求参数:{}", methodName, Arrays.toString(args));
}
/**
* 测试service返回通知
*
* @param joinPoint 连接点信息
* @param user 返回值
*/
public void getUserAfterReturning(JoinPoint joinPoint, User user) {
Object[] args = joinPoint.getArgs();
String methodName = joinPoint.getSignature().getName();
log.info(">>>>AOP-Service:返回通知,方法名:{},请求参数id为{},响应结果User信息为:{}", methodName, Arrays.toString(args), user);
}
/**
* 测试service异常通知
*
* @param joinPoint 连接点信息
* @param e 异常信息
*/
public void getUserAfterTrowing(JoinPoint joinPoint, Exception e) {
Object[] args = joinPoint.getArgs();
String methodName = joinPoint.getSignature().getName();
log.info(">>>>AOP-Service:异常通知,方法名:{},请求参数id为{},异常信息:{}", methodName, Arrays.toString(args), e);
}
/**
* 测试service环绕通知
* 需要注意的是,对于环绕通知必须给一个ProceedingJoinPoint参数,并且放在参数的第一位
* 方法体中必须调用proceedingJoinPoint.proceed()方法,否则会阻塞目标方法的执行
*
* @param proceedingJoinPoint proceedingJoinPoint
* @return 返回值,这里要注意:Around通知返回值不要写成void(除非目标方法返回值为void),否则最终返回结果为void
*/
public Object getUserAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object[] args = proceedingJoinPoint.getArgs();
String methodName = proceedingJoinPoint.getSignature().getName();
log.info(">>>>AOP-Service:环绕通知-开始,方法名:{},请求参数id为{}", methodName, Arrays.toString(args));
Object result = proceedingJoinPoint.proceed();
log.info(">>>>AOP-Service:环绕通知-结束,方法名:{},请求参数id为{},返回结果为{}", methodName, Arrays.toString(args), result);
return result;
}
}
这时候我们就可以在Spring的XML配置文件中配置和声明AOP:
<aop:aspectj-autoproxy />
<bean id="xmlUserServiceOperationAspect" class="com.tp.aop.XmlUserServiceOperationAspect"/>
<aop:config>
<!--声明切点-->
<aop:pointcut id="pointCut" expression="execution(* com.tp.service.IUserService.getById(..))"/>
<!--声明切面-->
<aop:aspect ref="xmlUserServiceOperationAspect">
<aop:before method="getUserBefore" pointcut-ref="pointCut"/>
<aop:after method="getUserAfter" pointcut-ref="pointCut"/>
<aop:after-returning method="getUserAfterReturning" pointcut-ref="pointCut" returning="user"/>
<aop:after-throwing method="getUserAfterTrowing" pointcut-ref="pointCut" throwing="e"/>
<aop:around method="getUserAround" pointcut-ref="pointCut"/>
</aop:aspect>
</aop:config>
编写一个测试类:
public class AopMain {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("config/applicationContext.xml");
IUserService userService = (UserServiceImpl) ctx.getBean("userServiceImpl");
System.out.println(userService);
System.out.println("========================================================");
userService.getById(2);
}
}
执行main函数效果如下:
6.如何在通知中获取目标方法信息
在日常开发中我们很可能会在通知的逻辑里需要知道目标方法的信息,比如方法名、参数信息、方法的返回值等等...
访问目标方法最简单的做法是定义增强处理方法时,将第一个参数
定义为JoinPoint类型,当该增强处理方法被调用时,该JoinPoint参数就代表了织入增强处理的连接点。
JoinPoint里包含了如下几个常用的方法:
- Object[] getArgs:返回目标方法的参数
- Signature getSignature:返回目标方法的签名
- Object getTarget:返回被织入增强处理的目标对象
- Object getThis:返回AOP框架为目标对象生成的代理对象
注意⚠️:当使用@Around处理时,我们需要将第一个参数定义为ProceedingJoinPoint类型,该类是JoinPoint的子类。
具体用法参见上面的XmlUserServiceOperationAspect.java
中的通知处理逻辑
另外如果只想访问目标方法的参数,Spring还提供了一种更加简洁的获取参数的方法:我们可以在程序中使用args来绑定目标方法的参数。如果在一个args表达式中指定了一个或多个参数,该切入点将只匹配具有对应形参的方法,且目标方法的参数值将被传入增强处理方法。
例如:
@Aspect
public class AccessArgAdviceTest {
@AfterReturning(
pointcut="execution(* com.tp.service.*.access*(..)) && args(time, name)",
returning="returnValue")
public void access(Date time, String name, Object returnValue) {
System.out.println("目标方法中的参数String = " + name);
System.out.println("目标方法中的参数Date = " + time);
System.out.println("目标方法的返回结果returnValue = " + returnValue);
}
}
上面的程序中,定义pointcut时,表达式中增加了args(time, name)部分,意味着可以在增强处理方法(即access方法)中定义time和name两个属性——这两个形参的类型可以随意指定,但一旦指定了这两个参数的类型,则这两个形参类型将用于限制该切入点只匹配第一个参数类型为Date,第二个参数类型为name的方法(方法参数个数和类型若有不同均不匹配)。
注意,在定义returning的时候,这个值(即上面的returning="returnValue"中的returnValue)作为增强处理方法的形参时,位置可以随意,即:如果上面access方法的签名可以为:
public void access(Date time, Object returnValue, String name)
也可以为:
public void access(Object returnValue, Date time, String name)
还可以为:
public void access(Date time, String name, Object returnValue)
只需要满足另外的参数名的顺序和pointcut中args(param1, param2)的顺序相同即可。
除此之外,使用args表达式时,还可以使用如下形式:args(param1, param2, ..)
注意args参数中后面的两个点,它表示可以匹配更多参数。
在例子args(param1, param2, ..)中,表示目标方法只需匹配前面param1和param2的类型即可
我们上面的基于注解创建切面的例子UserServiceOperationAspect.java
就是使用这种方式获取目标方法参数的
7.通知及切面的执行顺序
5种通知的执行顺序
不同切面同一个切点时的执行顺序
Spring AOP采用和AspectJ一样的有限顺序来织入增强处理:
- 在“进入”连接点时,最高优先级的增强处理将先被织入(所以给定的两个Before增强处理中,优先级高的那个会先执行)
- 在“退出”连接点时,最高优先级的增强处理会最后被织入(所以给定的两个After增强处理中,优先级高的那个会后执行)。
当不同的切面中的多个增强处理需要在同一个连接点被织入时,Spring AOP将以随机的顺序来织入这些增强处理。如果应用需要指定不同切面类里的增强处理的优先级,Spring提供了如下两种解决方案:
- 让切面类实现org.springframework.core.Ordered接口:实现该接口只需要实现一个int getOrder()方法,该方法返回值越小,优先级越高
- 直接使用@Order注解来修饰一个切面类:使用这个注解时可以配置一个int类型的value属性,该属性值越小,优先级越高
例如xml中:
<aop:aspect ref="xmlUserServiceOperationAspect" order="2">
<!--声明通知-->
<aop:before method="getUserBefore" pointcut-ref="pointCut"/>
<aop:after method="getUserAfter" pointcut-ref="pointCut"/>
<aop:after-returning method="getUserAfterReturning" pointcut-ref="pointCut" returning="user"/>
<aop:after-throwing method="getUserAfterTrowing" pointcut-ref="pointCut" throwing="e"/>
<aop:around method="getUserAround" pointcut-ref="pointCut"/>
</aop:aspect>
基于注解的切面:
@Aspect
@Order(1)
@Component
public class UserServiceOperationAspect {
....
}
将2种切面同时生效,运行测试类后效果如下: