【Spring】AOP实现原理(一):AOP基础知识
AOP相关概念
在学习AOP实现原理之前,先了解下AOP相关基础知识。
AOP面向切面编程,它可以通过预编译方式或者基于动态代理对我们编写的代码进行拦截(也叫增强处理),在方法执行前后可以做一些操作,一般我们会看到以下几个概念:
连接点(JointPoint): AOP进行切入的位置称为连接点,一般指程序中的某个方法,对该方法进行拦截
通知(Advice): 在某个连接点执行的操作称为通知,也就是被拦截方法执行前后需要执行的操作称为通知,一共有五种
- 前置通知:作用于被拦截方法执行之前
- 后置通知:作用于被拦截方法执行之后进行的操作,无论被拦截方法是否抛出异常都会执行
- 环绕通知:作用于被拦截方法执行之前和执行之后
- 返回通知:作用于被拦截方法正常执行完毕返回时,如果抛出异常将不会执行
- 异常通知:作用于被拦截方法抛出异常时
切点(Pointcut): 切点作用在于让程序知道需要在哪个连接点(方法)上执行通知,所以它也可以是一个表达式,匹配所有需要拦截的方法。
切面(Aspect): 切点和通知共同组成了切面,其中切点定义了需要在哪些连接点上执行通知,通知里面定义了具体需要进行的操作。
织入(Weaving):将切面连接到应用程序类型或者对象上,创建一个被通知的对象(advised object)的过程称为织入,换句话说织入就是将切面应用到目标对象的过程,它可以在编译期时(使用AspectJ)、加载时或者在运行时实现,Spring AOP是在运行时基于动态代理实现的。
Advisor:它是对切面的封装,使用了@AspectJ注解的类会被封装成Advisor。
Spring AOP和AspectJ区别
Spring AOP
Spring AOP是基于动态代理实现拦截功能的,默认使用JDK动态代理实现,当然这需要目标对象实现接口,如果目标对象没有实现接口,则使用CGLIB生成。
AspectJ
AspectJ提供了三种方式实现AOP:
-
编译时织入:在编译期间将代码进行织入到目标类的class文件中。
-
编译后织入:在编译后将代码织入到目标类的class文件中。
-
加载时织入:在JVM加载class文件的时候进行织入。
Spring AOP的应用
了解了AOP相关知识后我们来实现一个需求:
- 自定义一个日志注解MyLogger
- 对使用了MyLogger注解的方法进行拦截,在方法的执行前后分别进行一些操作(环绕通知):
- 方法执行前打印方法传入的参数
- 方法执行后打印方法的返回值
自定义注解
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLogger {
}
定义切面Aspect
这里使用注解@Aspect来标记这是一个切面,切面是切点和通知的集合,分别使用注解@Pointcut和@Around实现。
@Slf4j
@Aspect // 使用注解定义切面
@Component
@EnableAspectJAutoProxy // 开启AOP
public class MyLogAspect {
}
切点Pointcut
使用表达式@annotation(com.demo.mybatis.annotation.MyLogger)
匹配所有使用了@MyLogger注解的方法。
/**
* 定义切点,匹配所有使用了@MyLogger注解的方法
*/
@Pointcut("@annotation(com.example.annotation.MyLogger)") // 这里传入MyLogger的全路径
public void logPoiontcut() {
}
通知Advice
定义一个logAroudAdvice
方法,使用@Around注解标记这是一个环绕通知,logPoiontcut()
引用了切点,表示通知要作用于哪些连接点上,该方法需要传入一个ProceedingJoinPoint类型参数(连接点):
/**
* 通知Advice,这里使用了环绕通知
* @param joinPoint 连接点
* @return
* @throws Throwable
*/
@Around("logPoiontcut()") // 引用切点
public Object logAroudAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
// 方法执行前的日志打印
printBeforeLog(joinPoint);
// 执行方法
Object returnValue = joinPoint.proceed();
// 方法执行后的日志打印
printAfterLog(returnValue);
return returnValue;
}
完整的切面如下:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Slf4j
@Aspect // 使用注解定义切面
@Component
@EnableAspectJAutoProxy
public class MyLogAspect {
/**
* 定义切点,匹配所有使用了@MyLogger注解的方法
*/
@Pointcut("@annotation(com.example.annotation.MyLogger)")
public void logPoiontcut() {
}
/**
* 通知Advice,这里使用了环绕通知
* @param joinPoint 连接点
* @return
* @throws Throwable
*/
@Around("logPoiontcut()") // 引用切点
public Object logAroudAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
// 方法执行前的日志打印
printBeforeLog(joinPoint);
// 执行方法
Object returnValue = joinPoint.proceed();
// 方法执行后的日志打印
printAfterLog(returnValue);
return returnValue;
}
/**
* 方法执行前的日志打印
* @param joinPoint
*/
public void printBeforeLog(ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 获取方法
Method method = methodSignature.getMethod();
log.info("开始执行方法:{}", method.getName());
// 获取参数
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) {
return;
}
// 获取参数名称
String[] parameterNames = methodSignature.getParameterNames();
StringBuilder parameterBuilder = new StringBuilder();
for (int i = 0; i < args.length; i++) {
parameterBuilder.append(parameterNames[i]).append(":").append(args[i]);
if (i < parameterNames.length - 1) {
parameterBuilder.append(",");
}
}
log.info("方法参数【{}】", parameterBuilder.toString());
}
/**
* 方法执行后的日志打印
* @param returnValue
*/
public void printAfterLog(Object returnValue) {
log.info("方法返回值【{}】", returnValue == null ? null : returnValue.toString());
}
}
测试
定义一个用于计算的Service,实现一个两数相加的方法addTwoNum
,并使用@MyLogger
注解,对方法进行拦截,在方法执行前后打印相关日志
@Slf4j
@Service
public class ComputeService {
// 使用自定义日志注解对方法进行拦截
@MyLogger
public Integer addTwoNum(Integer value1, Integer value2) {
log.info("执行addTwoNum方法");
return value1 + value2;
}
}
编写单元测试:
@Autowired
private ComputeService computeService;
@Test
public void testAddTwoNum {
computeService.addTwoNum(1, 2);
}
由于ComputeService没有实现接口,可以看到Spring默认使用了CGLIB生成对象:
日志输出如下,可以看到方法执行前后打印了相关日志:
开始执行方法:addTwoNum
方法参数【value1:1,value2:2】
执行addTwoNum方法
方法返回值【3】
参考