一、AOP
1、什么是AOP
1.1、概述
- AOP(Aspect-Oriented Programming):面向切面编程,即把一些业务逻辑中的相同代码抽取出来,让业务逻辑更加简练清爽
- 如果要CRUD写一堆业务,可如何实现业务代码前后进行打印日志和参数的校验?
可以把日志记录和数据校验可重用的功能模块分离出来,然后在程序合适的位置动态地植入这些代码并执行,如此,让业务逻辑只包含核心的业务代码,而没有通用逻辑的代码,使业务模块更简洁,实现了业务逻辑和通用逻辑的代码分离,便于维护和升级,降低了业务逻辑和通用逻辑的耦合性
- AOP可以将遍布应用的功能分离出来形成可重用的组件,在编译期间、装载期间或运行期间实现给原程序动态添加功能(在不修改源代码的情况下),从而实现对业务逻辑的隔离,提高代码的模块化能力
- AOP的核心是动态代理,如果实现了接口,就使用JDK的动态代理,不然就使用CGLIB代理,主要应用于处理具有横切性质的系统级功能,如日志收集、事务管理、安全检查、缓存、对象池管理等
1.2、AOP的核心概念
- 目标对象(Target):代理的目标对象
- 切面(Aspect):类是对物体特征的抽象,切面就是对横切关注点的抽象,在Spring中,通过
@Aspect
注解声明当前类为切面,一般要在切面定义切入点和通知 - 连接点(JoinPoint):被拦截的点,由于Spring只支持方法类型的连接点,所以在Spring中,连接点指的是被拦截到的方法,实际上连接点还可以是字段或者构造器
- 切点(PointCut):带有通知的连接点,在程序中主要体现为书写切入点表达式
// 以自定义注解 @CustomLog为切点
@Pointcut("@annotation(com.example.interviewStudy.annotation.CustomLog)")
public void logPcut() {
}
- 通知(Advice):拦截到连接点之后要执行的代码,也称作增强
- 织入(Weave):将切面/ 切面类和目标类动态接入
编译器织入:切面在目标类编译时织入
类加载期织入:切面在目标类加载到JVM时织入,需要特殊的类加载器,可以在目标类被引入应用之前增强该目标类的字节码,AspectJ采用编译期织入和类加载器织入
运行期织入:切面在应用运行的某时刻被织入,一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象,这也是SpringAOP织入切面的方式 - 增强器(advisor):筛选类中的哪些方法是连接点(哪些方法需要被拦截)
- 引介(introduction):⼀种特殊的增强,可以动态地为类添加⼀些属性和方法
1.3、AOP的环绕方式
AOP有五种通知的方式:
- 前置通知 (@Before):在切入点方法执行之前执行
- 环绕通知 (@Around):手动调用切入点方法并对其进行增强的通知方式
- 后置通知 (@After):在切入点方法执行之后执行,无论切入点方法内部是否出现异常,后置通知都会执行
- 异常通知 (@AfterThrowing):在切入点方法执行之后执行,只有当切入点方法内部出现异常之后才执行
- 返回通知 (@AfterReturning):在切入点方法执行之后执行,如果切入点方法内部出现异常将不会执行
当有多个切面的情况下,可以通过 @Order指定先后顺序,数字越小,优先级越高
2、AOP在项目中的运用
2.1、日志输出
- 在SpringBoot项目中,使用AOP 打印接口的入参和出参日志,以及执行时间
1)引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.8</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 2)自定义注解 作为切入点
import java.lang.annotation.*;
@Target({ElementType.METHOD}) // 指定注解使用在方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomLog {
String info();
}
- 3)配置AOP切面
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
@Aspect // 标识当前类为切面
@Component
public class CustomLogAspect {
// getLogger(Class<?> clazz)
public static final Logger logger = LoggerFactory.getLogger(CustomLogAspect.class);
// 以自定义注解 @CustomLog为切点
@Pointcut("@annotation(com.example.interviewStudy.annotation.CustomLog)")
public void logPcut() {
}
// 前置通知: 在切点之前织入
@Before("logPcut()")
public void doBefore(JoinPoint joinPoint) throws JsonProcessingException {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
logger.info("========== 开始打印请求参数 ===========");
logger.info("URL: {}", request.getRequestURL().toString());
logger.info("HTTP Method: {}", request.getMethod());
logger.info("Controller的全路径 和 执行方法: {} , {}方法", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
logger.info("请求入参:{}", new ObjectMapper().writeValueAsString(joinPoint.getArgs()));
}
// 后置通知,在切入点之后织入
@After("logPcut()")
public void doAfter() {
logger.info("======== 请求日志输出完毕 ========");
}
/**
* 环绕通知: ProceedingJoinPoint对象调用proceed方法,实现 原本目标方法的调用
* ProceedingJoinPoint 只支持环绕通知,如果其他通知也采用ProceedingJoinPoint作为连接点,就会出现异常
* ==> Caused by: java.lang.IllegalArgumentException: ProceedingJoinPoint is only supported for around advice
* */
@Around("logPcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
logger.info("请求结果: {}", new ObjectMapper().writeValueAsString(result));
logger.info("请求处理耗时: {} ms", (end - start));
return result;
}
}
- 4)在接口上添加自定义注解
@RestController
@RequestMapping("/aop")
public class CustomAspectController {
@GetMapping("/hello")
@CustomLog(info = "hello,使用AOP实现请求日志输出")
public String hello(String uname) {
return "Hello,welcome to studing AOP, your name is " + uname;
}
}
执行结果:
3、JDK和CGLIB的动态代理
- 动态代理主要有JDK动态代理和CGLIB的动态代理
1)JDK动态代理
- Interface:对于JDK动态代理,目标类需要实现一个Interface
- InvocationHandler:通过实现InvocationHandler接口,定义横切逻辑,再通过反射机制(invoke)调用目标类的方法,在此过程,可能包装逻辑,对目标方法进行前置/ 后置处理
- Proxy:利用InvocationHandler动态创建一个符合目标类实现接口的实例,生成目标类的代理对象
我们来看⼀个常见的⼩场景,客服中转,解决⽤户问题:
代码实现:
- 接口
public interface ISolver {
public String solve();
}
- 目标类:需要实现对应接口
public class ProblemSolver implements ISolver{
@Override
public String solve() {
System.out.println("ProblemSolver,solve方法 ==> 问题正在解决中...");
return "OKK";
}
}
- 动态代理工厂:ProxyFactory,直接用反射生成一个目标对象的代理对象,如下是用匿名内部类的方式重写了InvocationHandler的方法
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyFactory {
// 维护一个目标对象
private Object target;
public ProxyFactory(Object target){
this.target = target;
}
// 为目标对象生成代理对象
public Object getProxyInstance(){
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("请描述您的问题:");
// 通过反射机制 调用目标对象方法
Object result = method.invoke(target, args);
System.out.println("问题已经得到解决!!");
// 返回 目标对象方法的返回值
return result;
}
});
}
}
- 客户端:Client,生成一个代理对象实例,通过代理对象 调用目标对象的方法
public class Client {
public static void main(String[] args) {
ISolver developer = new ProblemSolver();
// 创建代理对象实例
ISolver instance = (ISolver) new ProxyFactory(developer).getProxyInstance();
// 代理对象调用目标对象方法,得到目标方法的返回值并输出
String res = instance.solve();
System.out.println(res);
}
}
执行结果:
2)CGLIB动态代理
- 目标类(不需要像JDK动态代理一样实现接口):
public class CglibSolver {
public String solve(){
System.out.println("Testing implement proxy by cglib");
return "CglibSolver ==> solve方法";
}
}
- 动态代理工厂:
import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class ProxyFactory implements MethodInterceptor, Callback {
private Object target;
public ProxyFactory(Object target){
this.target = target;
}
public Object getProxyInstance(){
Enhancer enhancer = new Enhancer();
// 设置父类
enhancer.setSuperclass(target.getClass());
// 设置回调函数,用于监听当前事件
enhancer.setCallback(this);
// 创建子类对象代理
return enhancer.create();
}
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("请问有什么可以帮到您?");
// 调用目标对象的方法
Object result = method.invoke(target, args);
System.out.println("问题得到解决啦哈!");
return result;
}
}
- 客户端:Client
public class CgClient {
public static void main(String[] args) {
CglibSolver solver = new CglibSolver();
// 创建代理对象
CglibSolver proxy = (CglibSolver) new ProxyFactory(solver).getProxyInstance();
// 通过代理对象实例调用目标对象方法
String result = proxy.solve();
System.out.println("result : " + result);
}
}
执行结果(代理对象替目标对象执行调用方法):
4、Spring AOP和AspectJ AOP的区别
1) Spring AOP
Spring AOP属于运行时增强,主要具有如下特点:
- 基于动态代理来实现,默认如果使用接口的方式来实现,则使用JDK提供的动态代理;如果是方法,则使用CGLIB来实现
- Spring AOP需要依赖IOC容器来管理,并且只能作用于Spring容器,使用纯Java代码实现
- 在性能上,由于Spring AOP是基于动态代理来实现的,在容器启动时需要生成代理实例,在方法调用上也会增加栈的深度,使得Spring AOP的性能不如Aspect好
2)AspectJ
-
AspectJ是功能强大的AOP框架,属于
编译时增强
,可以单独使用,也可以整合到其他框架中,是AOP编程的完全解决方案 -
AspectJ属于静态织入,通过修改代码来实现,在实际运行之前就完成了织入,生成的类没有额外运行时开销,可织入时机如下:
A、编译期织入(Compile-time weaving):如 A类使用AspectJ添加了某属性,B类引用了A类,该场景就需要编译期进行织入,否则没法编译B类
B、编译后织入(Post-compile weaving):在已生成了字节码/ class文件,或已经打包成jar包后,该情况需要增强,就需要使用到编译后织入
C、类加载后织入(Load-time weaving):在加载类时进行织入 -
两者整体对比如下:
事务
1、Spring事务的种类
Spring支持编程式事务
和声明式事务
管理两种方式:
1)编程式事务管理:使用TransactionTemplate,需要显示地执行事务
2)声明式事务管理:建立在AOP之上,其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,即在目标方法开始之前启动事务,在执行完目标方法之后根据执行情况 进行提交或回滚事务
- 优点:不需要在业务逻辑代码中掺杂事务管理的代码,只需要在配置文件中进行相关的事务规则声明或通过 @Transactional注解声明事务(以及在启动类上添加@EnableTransactionManagement注解开启事务管理),将事务规则应用到业务逻辑中,减少业务代码的侵入
- 缺点:最细粒度只能作用到方法级别,无法做到像编程式事务那样作用到代码块级别
2、声明式事务的失效情况
3、声明式事务的实现原理
4、Spring事务的隔离级别
- Spring的接口TransactionDefinition定义了表示隔离级别的常量,主要是对应后端数据库的事务隔离级别:
- ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,MySQL默认可重复读,Oracle默认读已提交
- ISOLATION_READ_UNCOMMITTED:读未提交
- ISOLATION_READ_COMMITTED:读已提交
- ISOLATION_REPEATABLE_READ:可重复读
- ISOLATION_SERIALIZABLE:串行化
5、Spring事务的传播机制
未完待续…