AOP(面向切面编程)
概念
AOP((Aspect Oriented Programming)面向切面编程
- 是一种思想,目的是在不修改源代码的基础上,对原有功能进行增强
- 通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术
- AOP是一种编程范式,是OOP的延续,在OOP基础之上进行横向开发。
- AOP研究的不是每层内部如何开发,而是同一层面上各个模块之间的共性功能。比如:事务、日志、统计
Spring AOP 是对 AOP 思想的一种实现,将那些与业务无关,却为业务模块所共用的逻辑封装起来,减少系统的重复代码,降低模块间的耦合度。另外,AOP还能解决一些系统层面上的问题,比如日志、事务、权限等。
Spring 底层是通过动态代理的方式实现的 AOP。同时支持 jdk 和 cglib 动态代理,Spring会根据被代理的类是否有接口自动选择代理方式:
- 如果有接口,就采用 JDK 动态代理(也可以强制使用 CGLIB 动态代理)
- 没有接口就采用用 CGLIB 动态代理的方式
Spring AOP工作流程:
开发阶段分别开发,运行阶段组装运行
开发阶段(开发者完成)
- 开发共性功能,制作成增强
- 开发非共性功能,制作成切点
- 在配置文件中,声明切点与增强之间的关系,即切面
运行阶段/容器启动阶段(AOP完成)
- Spring读取配置文件中的切面信息,根据切面中的描述,将 增强功能 增加在 目标对象 的 切点方法 上,动态创建代理对象
- 最后将经过代理之后对象放入容器中(注意:存入容器的是动态代理对象!)
术语及说明
- 目标对象(target ):需要被增强的对象,即切入点方法所在对象
- 连接点(jointPoint):被代理对象中的方法
- 切点(pointCut):按照 AOP 的规则去切(匹配)连接点,匹配出来的就叫切点,即 需要被增强的方法
切点表达式的:定义一组规则,用于在连接点(所有方法)中挑选切入点(被增强方法)
常用方式切点表达式:execution(方法的修饰符 返回值类型 包名.类名.方法名(参数))
增强(通知)(advice):一个具体的增强功能。增强方法在切点方法的什么位置上执行
Spring AOP 通知(增强)分为5种类型:
- 前置通知(before):在切点运行之前执行
- 后置通知(after-returning):在切点正常运行结束之后执行
- 异常通知(after-throwing):在切点发生异常的时候执行
- 最终通知(after):在切点的最终执行
环绕通知(around):一种特殊的通知,在一个方法中定义多个增强逻辑(和手动定义动态代理类似)
环绕通知的自定义方法:
- 参数 ProceedingJoinPoint:被代理对象的方法
- 返回值:被增强方法的返回值
- 代理对象(proxy ):目标对象被增强后成为代理对象
- 切面(aspect):是一个设计概念,包含了Advice 和 Pointcut。切面 = 切入点 + 增强
Advice定义了Aspect的任务和什么时候执行,Pointcut定义在哪里切入
即 Aspect定义了一个什么样的增强功能,切入到哪个核心方法的哪个位置
- 织入(Weaving):一个动作。将增强代码加入到核心代码的过程就叫织入
切点标志符(表达式)
在定义 SpringAOP 的切点时候,比如使用 @Pointcut
注解标记切点时,需要填写切入通知的连接点的特征,即连接点的匹配规则或表达式,这些表达式是通过被称之为切点指示符的符号进行编写的。
通配符
在使用切点指示符进行匹配表达式编写时,几乎都需要使用到通配符进行模糊匹配,常用的通配符有三个:..
、+
和 *
..
:表示匹配方法中的任意数量和类型的参数,或者匹配类的任意包路径+
:表示匹配给定类的任意子类*
:表示匹配一个或多个数量的字符
注:
- 方法修饰符可以省略
- 方法返回值可以通过 * 标识任意返回值类型
- 包名可以通过 * 标识任意包,一般会指定到一个具体的包路径
- 类名可以通过 * 标识任意类
- 方法名可以通过 * 标识任意方法
- 参数可以通过 .. 标识任意参数
类型指示符(within)
within 指示符用于匹配类型(包括:接口、类和包),其语法格式如下:
within(<type name>)
示例:
// 匹配 com.example.dao 包下的所有类的所有方法,但不包括子包的类
@Pointcut("within(com.example.dao.*)")
// 匹配 com.example.dao 包及其子包中所有类中的所有方法
@Pointcut("within(com.example.dao..*)")
// 匹配 包路径前缀为com.example + 任意包路径 + dao 包及其子包中所有类中的所有方法
@Pointcut("within(com.example..dao..*)")
// 匹配 com.example.dao 包下的 UserDaoImpl 类的所有方法
@Pointcut("within(com.example.dao.UserDaoImpl)")
// 匹配当前包下的 UserDaoImpl 类的所有方法
@Pointcut("within(UserDaoImpl)")
// 匹配所有实现 com.example.dao 包下的 UserDao 接口的类的所有方法
@Pointcut("within(com.example.dao.UserDao+)")
注: 示例中的 UserDao 为接口,UserDaoImpl 为其一个实现类。
方法指示符(execution)
execution 指示符根据方法签名进行匹配,其语法格式如下:
execution(<scope> <return-type> <fully-qualified-class-name>.*(parameters))
其中:
- scope 表示方法作用域(如 public、private)
- return-type 表示返回值类型
- fully-qualified-class-name 表示方法所在类的完全限定名称
- parameter 表示方法参数
示例:
// 匹配 com.example.dao 包中所有类中的所有方法(常用)
@Pointcut("execution(* com.example.dao.*.*(..))")
// 匹配 UserDaoImpl 类中的所有方法
@Pointcut("execution(* com.example.dao.UserDaoImpl.*(..))")
// 匹配 UserDaoImpl 类中的所有公共方法
@Pointcut("execution(public * com.example.dao.UserDaoImpl.*(..))")
// 匹配 UserDaoImpl 类中的所有返回值为 int 类型的公共方法
@Pointcut("execution(public int com.example.dao.UserDaoImpl.*(..))")
// 匹配 UserDaoImpl 类中第一个参数为 int 类型的所有公共方法
@Pointcut("execution(public * com.example.dao.UserDaoImpl.*(int , ..))")
名称指示符(bean)
bean 指示符用于匹配特定名称的 Bean 对象的方法,是 SpringAOP 扩展的指示符,AspectJ 中,我有对应的指示符。
示例如下:
// 匹配名称中带有后缀 Service 的 Bean 的所有方法
@Pointcut("bean(*Service)")
对象指示符(this 、target)
对象指示符共有两个 this 和 target,两者的区别如下:
- this:用于匹配当前 AOP 代理对象类型的方法;
- target:用于匹配当前目标对象类型的方法。
示例如下:
// 匹配任意实现了 UserDao 接口的代理对象的方法
@Pointcut("this(com.example.springAop.dao.UserDao)")
// 匹配任意实现了 UserDao 接口的目标对象的方法
@Pointcut("target(com.example.springAop.dao.UserDao)")
注解指示符(@within、@annotation)
注解指示符用于匹配使用了特定注解的类或方法,包括@within和 @annotation:
- @within:表示匹配使用了特定注解的类的所有方法(注意和 within 的区别)
- @annotation:表示匹配使用了特定注解的方法
示例如下:
// 匹配使用了 MyAnnotation 注解的类(注意是类)的所有方法
@Pointcut("@within(com.example.annotation.MyAnnotation)")
// 匹配使用了 MyAnnotation 注解的方法(注意是方法)
@Pointcut("@annotation(com.example.annotation.MyAnnotation)")
切点指示符的组合使用
所有的切点指示符都可以通过逻辑运算符进行组合使用,比如 &&
、||
、!
,示例如下:
// 匹配了任意实现了 UserDao 接口的目标对象的方法并且该对象不在 com.zejian.dao 包及其子包下
@Pointcut("target(com.example.dao.UserDao) !within(com.example.dao..*)")
// 匹配名字以 Service 结尾, 并且在 com.example.service 包中的 bean
@Pointcut("bean(*Service) && within(com.example.service.*)")
注: 在组合指示符中,第一个指示符为主匹配表达式,而第二个指示符只是对第一个指示符的匹配结果进行过滤。
配置 Spring AOP(xml)
xml文件 配置切入点
<aop:pointcut
- id:当前切点的唯一标志
- expression:切入点表达式
xml文件 配置切面
<aop:aspect :配置一个切面
- id:当前切面的唯一标志
- ref:指定当前切面使用哪个通知
xml文件 配置通知类型
<aop:before :指定通知在切入点方法中执行的位置
- method : 切面类中的增强方法名
- pointcut-ref:切入点的id
xml文件 配置AOP示例
<!--声明AOP配置-->
<aop:config>
<!-- 配置切入点(被增强的方法) -->
<aop:pointcut id="pt" expression="execution(* cn.test.dao.impl.*.*(..))"/>
<!--配置切面-->
<aop:aspect ref="logger">
<!-- 配置通知类型 -->
<!-- 前置通知 -->
<aop:before method="before" pointcut-ref="pt"></aop:before>
<!-- 后置通知 -->
<aop:after-returning method="afterReturning" pointcut-ref="pt"></aop:after-returning>
<!-- 异常通知 -->
<aop:after-throwing method="afterThrowing" pointcut-ref="pt"></aop:after-throwing>
<!-- 最终通知 -->
<aop:after method="after" pointcut-ref="pt"></aop:after>-->
<!-- 环绕通知 -->
<aop:around method="around" pointcut-ref="pt"></aop:around>
</aop:aspect>
</aop:config>
import org.aspectj.lang.ProceedingJoinPoint;
/**
* 切面类:此类中具有所有的增强代码逻辑
*/
public class Logger {
/**
* 前置通知:执行被代理对象方法之前执行
* 方法:无参数,无返回值
*/
public void before() {
System.out.println("执行前置通知");
}
/**
* 后置后置:正常执行被代理对象方法获取返回值之后执行
*/
public void afterReturning() {
System.out.println("执行后置通知");
}
/**
* 异常通知:执行代码抛出异常的时候执行
*/
public void afterThrowing() {
System.out.println("执行异常通知");
}
/**
* 最终通知:finally代码块中执行的逻辑
*/
public void after() {
System.out.println("执行最终通知");
}
/**
* 环绕通知:在一个方法中定义多个增强逻辑
*/
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object obj = null;
try {
System.out.println("执行前置通知");
//执行被代理对象方法
obj = pjp.proceed();
System.out.println("执行后置通知");
}catch (Exception e){
System.out.println("执行异常通知");
}finally {
System.out.println("执行最终通知");
}
return obj;
}
配置 Spring AOP(注解)
AOP注解版有两种:
- 基于 XML 结合注解的配置方式
- 基于纯注解的配置方式
开启 AOP 注解支持(xml 方式)
xml配置文件
开启IOC注解的支持,包扫描
- 自定义的对象,通过IOC注解进行对象创建和依赖注入
- 第三方的对象,通过XML配置对象创建和依赖注入
- 开启AOP注解的支持,自动代理
<?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">
<!--开启包扫描-->
<context:component-scan base-package="cn.test"></context:component-scan>
<!--开启对AOP注解的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<!--在切面类中通过注解完成AOP配置-->
</beans>
开启 AOP 注解支持(配置类方式)
- @EnableAspectJAutoProxy:标注在配置类上,开启 aop 注解支持
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* 配置类
*/
@Configuration
@ComponentScan(basePackages = "cn.test")
@EnableAspectJAutoProxy
public class SpringConfig {
}
切面类(注解)
- @Aspect:标注在自定义类上,声明切面类
注意:该切面类同时需要标注 IOC注解(@Component),交给 Spring 容器管理
在切面类中的通知(增强)方法上通过注解配置通知类型:
@Before:前置通知
- @AfterReturning: 后置通知
- @AfterThrowing:异常通知
- @After:最终通知
- @Around:环绕通知
通知注解的属性:
value / argNames 属性:切入点表达式 或 被 @Pointcut 标注的方法名()
@Around("pt()") public Object around(ProceedingJoinPoint pjp)
@Pointcut:标注在切面类中的空的方法上,抽取公共的切入点表达式
- value / argNames 属性:切入点表达式
通知注解配置 方法名() 即可引入公共的切入点表达式
@Pointcut(value="execution(* cn.test.service.impl.*.*(..))") public void pt() {}
切面类示例
/**
* 切面类:此类中具有所有的增强代码逻辑
*/
@Component
@Aspect
public class Logger {
/**
* 前置通知:执行被代理对象方法之前执行
* 方法:无参数,无返回值
*/
//@Before(value="execution( * cn.test.dao.impl.*.*(..) )")
public void before() {
System.out.println("执行前置通知");
}
/**
* 后置通知:正常执行被代理对象方法获取返回值之后执行
*/
//@AfterReturning(value="execution( * cn.test.dao.impl.*.*(..) )")
public void afterReturning() {
System.out.println("执行后置通知");
}
/**
* 异常通知:执行代码抛出异常的时候执行
*/
// @AfterThrowing("execution( * cn.test.dao.impl.*.*(..) )")
public void afterThrowing() {
System.out.println("执行异常通知");
}
/**
* 最终通知:finally代码块中执行的逻辑
*/
//@After("execution( * cn.test.dao.impl.*.*(..) )")
public void after() {
System.out.println("执行最终通知");
}
// @Pointcut:抽取公共的切入点表达式
@Pointcut(value="execution(* cn.test.service.impl.*.*(..))")
public void pt() {}
/**
* 环绕通知:在一个方法中定义多个增强逻辑
*/
//@Around("execution( * cn.test.dao.impl.*.*(..) )")
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object obj = null;
try {
System.out.println("1执行前置通知");
//执行被代理对象方法
obj = pjp.proceed();
System.out.println("2执行后置通知");
}catch (Exception e){
System.out.println("3执行异常通知");
}finally {
System.out.println("4执行最终通知");
}
return obj;
}
}
四大通知的执行顺序问题
参考:https://blog.csdn.net/qq_45193304/article/details/109430545
SpringAOP四大通知(前置、后置、异常、最终通知)的执行顺序非常复杂,跟Spring版本、配置先后顺序、配置方式(xml | 注解)都有关,故建议使用环绕通知!
xml方式正确顺序配置aop的执行顺序:
try {
// 前置通知(before) : 在切点运行之前执行
// 切点执行,被代理对象方法调用
// 后置通知(after-returning): 在切点正常运行结束之后执行
}catch (Exception e){
// 异常通知(after-throwing): 在切点发生异常的时候执行
}finally {
// 最终通知(after): 在切点的最终执行
}
注解方式正确顺序配置aop的执行顺序:
try{
try{
//@Before -- 首先执行前置通知
method.invoke(..); -- 然后执行切入点方法
}finally{
//@After -- 再而肯定会执行最终通知 --- 注解配置的注意点
}
//@AfterReturning -- 如果没有异常,则继续执行后置通知
return; -- 返回结果
}catch(){
//@AfterThrowing -- 如果有异常,则执行异常通知
}
切面方法的可传参数
JoinPoint 对象
JoinPoint 对象封装了 SpringAop 中切面方法的信息,在切面方法中添加 JoinPoint 参数,就可以获取到封装了该方法信息的JoinPoint 对象。
常用 API:
- Signature getSignature() :获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的 Class 等信息
joinPoint.getSignature().getName()) :目标方法名
joinPoint.getSignature().getDeclaringType().getSimpleName()) :目标方法所属类的简单类名
joinPoint.getSignature().getDeclaringTypeName()) :目标方法所属类的类名
Modifier.toString(joinPoint.getSignature().getModifiers())) :目标方法声明类型
- Object[] getArgs() :获取传入目标方法的参数对象
- Object getTarget() :获取被代理的对象
- Object getThis() :获取代理对象
使用示例:
@Component
@Aspect
@Slf4j
public class Logger {
@Before(value="execution( * cn.test.dao.impl.*.*(..) )")
public void before(JoinPoint joinPoint) {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
StringBuffer uri = request.getRequestURL();
Object[] args = joinPoint.getArgs();
if (Objects.nonNull(args) && args.length > 0 && args[0] instanceof WebDataBinder){
// 不打印initBinder方法的请求入参,否则会报错
return;
}
List<Object> logArgs = Arrays.stream(args)
.filter(arg -> (!(arg instanceof ServletRequest)
&& !(arg instanceof ServletResponse)
&& !(arg instanceof MultipartFile)
&& !(arg instanceof HttpSession)))
.collect(Collectors.toList());
log.info("执行前置通知, uri: {}, args: {}", uri, logArgs);
}
}
ProceedingJoinPoint 对象
ProceedingJoinPoint 对象是 JoinPoint 的子接口,该对象只用在 @Around 的切面方法中
添加了 两个方法
- Object proceed() throws Throwable :执行目标方法
- Object proceed(Object[] var1) throws Throwable :传入的新的参数去执行目标方法
使用示例详解案例 MethodAnnotationLogAspect
案例
HttpLogAspect
打印 controller 方法日志的请求和返回的切面类
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Slf4j
@Aspect
@Component
public class HttpLogAspect {
@Pointcut("execution(* com.test.ssmtest..controller..*(..))")
public void pt() {}
@Before(value = "pt()")
public void testBefore(JoinPoint joinPoint) {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
StringBuffer uri = request.getRequestURL();
Object[] args = joinPoint.getArgs();
if (Objects.nonNull(args) && args.length > 0 && args[0] instanceof WebDataBinder){
// 不打印initBinder方法的请求入参,否则会报错
return;
}
List<Object> logArgs = Arrays.stream(args)
.filter(arg -> (!(arg instanceof ServletRequest)
&& !(arg instanceof ServletResponse)
&& !(arg instanceof MultipartFile)
&& !(arg instanceof HttpSession)))
.collect(Collectors.toList());
log.info("request, uri: {}, args: {}", uri, JSON.toJSONString(logArgs));
}
@AfterReturning(value = "pt()", returning = "rt")
public void afterReturning(Object rt) {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
StringBuffer uri = request.getRequestURL();
log.info("response, uri: {}, return: {}", uri, JSON.toJSONString(rt));
}
}
MethodAnnotationLogAspect
打印使用了自定义注解的方法的调用时间
自定义注解
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogAnnotation {
String methodDesc();
}
切面类
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Slf4j
@Aspect
@Component
public class MethodAnnotationLogAspect {
@Around("@annotation(com.test.ssmtest.aspect.LogAnnotation)")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 获取方法上的注解
LogAnnotation annotation = method.getAnnotation(LogAnnotation.class);
String methodDesc = annotation == null ? "" : annotation.methodDesc();
long startTime = System.currentTimeMillis();
log.info("开始时间:{},进入方法:{}", DateFormatUtils.format(startTime, "yyyy-MM-dd HH:mm:ss:SSS"), methodDesc);
// 执行方法
Object proceedReturn = joinPoint.proceed();
long endTime = System.currentTimeMillis();
long time = endTime - startTime;
log.info("用时:{} ms,离开方法:{}", time, methodDesc);
return proceedReturn;
}
}