一、理解AOP
AOP,即面向切面编程,在不改变原有业务逻辑方法前提下,进行增强(如AOP系统日志);能够解耦,有利于维护代码。
二、AOP原理
动态代理设计模式,在bean“初始化后”(AbstractAutoProxyCreator -> postProcessAfterInitialization())进行操作,对目标对象进行增强处理;默认使用JDK动态代理,若没有出现接口 或者 开启动态代理时设置属性,则转而使用CGLIB代理。
三、JDK动态代理
配置类、接口和实现接口类
@ComponentScan(basePackages = "com.igeek.ch01.jdk")
public class MyConfig {
}
public interface ICount {
public int add(int a , int b);
public int div(int a , int b);
}
/**
* TODO
*
* @author ding
* @since 2024/5/22
*
* 需求:计算器实现 + 日志实现
*
* 代理设计模式:对原有的方法,无侵入性的进行增强
* 1.JDK动态代理 涉及接口
* 2.CGLIB代理 无需接口
*/
@Component
public class CountImpl implements ICount{
@Override
public int add(int a, int b) {
int c = a+b;
System.out.println("a+b = "+c);
return c ;
}
@Override
public int div(int a, int b) {
int c = a/b;
System.out.println("a/b = "+c);
return c ;
}
}
获取代理对象
1.直接获取代理对象
public class CountProxyImpl {
//目标对象
private ICount target;
public CountProxyImpl(ICount target){
this.target = target;
}
//获取代理对象的方法
public ICount getProxy(){
//代理对象
//第一个参数:目标对象的类加载器 第二个参数:目标对象所有实现的接口列表 第三个参数:执行目标方法的处理器
ICount proxy = (ICount)Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
/**
* @param proxy 代理对象,一般不使用
* @param method 执行目标方法对象
* @param args 执行目标方法中的参数列表
* return 目标方法的执行结果返回,若目标方法没有返回值则返回null
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//获取执行目标方法的名称
String methodName = method.getName();
System.out.println("日志追踪 The method "+methodName+" begin with "+ Arrays.toString(args));
//执行目标方法
Object result = method.invoke(target, args);
System.out.println("日志追踪 The method "+methodName+" end with "+result);
return result;
}
}
);
return proxy;
}
}
2.在bean“初始化后”获取代理对象,实现增强对象
@Component
public class MyAware implements BeanPostProcessor {
/**
* 初始化后
* @param target 实例bean
* @param beanName 唯一标识
* @return 经过处理后的实例bean
* @throws BeansException
*/
@Override
public Object postProcessAfterInitialization(Object target, String beanName) throws BeansException {
//只针对计算器ICount进行方法业务逻辑的增强
if(target instanceof ICount){
//代理对象
ICount proxy = (ICount)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("MyAware 日志追踪 The method "+method.getName()+" begin with "+ Arrays.toString(args));
//执行目标方法
Object result = null;
try {
result = method.invoke(target, args);
//返回通知
System.out.println("MyAware 日志追踪 The method "+method.getName()+" end with "+ result);
}catch (Exception e){
//异常通知
System.out.println("MyAware 日志追踪 The method "+method.getName()+" 发生异常,"+ e.getMessage());
e.printStackTrace();
}
//后置通知
System.out.println("MyAware 日志追踪 The method "+method.getName()+" end with ");
return result;
}
}
);
//返回代理对象
return proxy;
}
//返回目标对象
return target;
}
}
MainTest
public class MainTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig.class);
/*//目标对象
CountImpl target = ac.getBean(CountImpl.class);
//com.igeek.ch01.jdk.CountImpl
System.out.println("target = "+target.getClass().getName());
//代理对象
ICount proxy = new CountProxyImpl(target).getProxy();
//com.sun.proxy.$Proxy7
System.out.println("proxy = "+proxy.getClass().getName());*/
//Spring IOC 后
ICount proxy = ac.getBean(ICount.class);
//com.sun.proxy.$Proxy8
System.out.println("proxy = "+proxy.getClass().getName());
proxy.add(10 , 20);
proxy.div(50 , 20);
}
}
四、AOP 注解字 + 配置类 版本
1.修改 pom.xml 文件
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.13</version>
</dependency>
<!-- 引入aspectj依赖 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
2.新建Spring的核心配置类
Maven项目要在Spring IOC容器中启用AsppectJ注解支持;若为SpringBoot项目,则在启动类上添加
//开启AspectJ功能 proxyTargetClass = true
//1.@EnableAspectJAutoProxy 默认使用的是JDK动态代理,必须要有实现接口
//2.若出现以下情况则将会使用CGLIB动态代理
//2.1 若未出现实现接口,则直接使用CGLIB动态代理
//2.2 若设置proxyTargetClass = true,则直接使用CGLIB动态代理
//当前类是一个配置类
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "com.igeek.ch02")
public class MyConfig {
}
//接口类
public interface ICount {
public int add(int a , int b);
public int sub(int a , int b);
public int mul(int a , int b);
public int div(int a , int b);
}
//实现类
@Component
public class CountImpl /*implements ICount*/ {
//@Override
public int add(int a, int b) {
int c = a+b;
System.out.println("a+b = "+c);
return c ;
}
//@Override
public int sub(int a, int b) {
int c = a-b;
System.out.println("a-b = "+c);
return c ;
}
//@Override
public int mul(int a, int b) {
int c = a*b;
System.out.println("a*b = "+c);
return c ;
}
//@Override
public int div(int a, int b) {
int c = a/b;
System.out.println("a/b = "+c);
return c ;
}
}
3.@Aspect
在AspectJ注解中,切面只是一个带有@Aspect注解的Java类。通知是标注有某种注解的简单的Java方法。
4.通知
AsectJ支持五种类型的通知注解
4.1 前置通知
@Before:前置通知,在方法执行之前执行
@Component
@Aspect
@Order(1) //可以存在多个切面类,通过@Order来设置切面类的先后执行顺序,数字越小越先执行
public class LogAspect {
/**
* 前置通知 @Before("execution(切入点)")
*
* 切点表达式
* execution(访问权限 返回值 包名.类名.方法名(形参列表)) 表达式
* 例如:execution(public int com.igeek.ch02.log.ICount.add(int , int))
* 简化:execution(* ICount.*(..))
* 第一个*:代表任意访问权限和任意返回值类型
* 第二个*:代表任意方法
* 第三个..:代表任意的形参列表
*
* 连接点 JoinPoint
* 通过连接点获取目标方法及其参数列表
*/
@Before("execution(* ICount.*(..))")
public void beforeAdvice(JoinPoint joinPoint){
//获取目标方法名
String name = joinPoint.getSignature().getName();
//获取目标方法形参列表
Object[] args = joinPoint.getArgs();
System.out.println("LogAspect 日志追踪 The method "+name+" begin with "+ Arrays.toString(args));
}
}
4.2 后置通知
@After:后置通知,在方法之后执行
@Component
@Aspect
public class LogAspect {
@After("execution(* ICount.*(..))")
public void afterAdvice(JoinPoint joinPoint){
System.out.println("LogAspect 日志追踪 The method "+joinPoint.getSignature().getName()+" end");
}
}
4.3 返回通知
@AfterReturning:返回通知,在方法返回结果之后执行
@Component
@Aspect
public class LogAspect {
/**
* 返回通知 @AfterReturning
* 1.value属性:即pointcut属性,声明切点表达式
* 2.returning属性:记录方法执行的结果,若目标方法没有返回值则为null
*
* @param joinPoint 连接点
* @param result 形参名称,必须与注解中的returning的值一致
*/
@AfterReturning(value = "execution(* ICount.*(..))" , returning = "result")
public void returnAdvice(JoinPoint joinPoint , Object result){
System.out.println("LogAspect 日志追踪 The method "+joinPoint.getSignature().getName()+" end with "+result);
}
}
4.4 异常通知
@AfterThrowing:异常通知,在方法抛出异常之后
@Component
@Aspect
public class LogAspect {
/**
* 异常通知 @AfterThrowing
* 1.value属性:即pointcut属性,声明切点表达式
* 2.throwing属性:接收方法运行时抛出的异常信息
*
* @param joinPoint 连接点
* @param ex 形参名称,必须与注解中的throwing的值一致
*/
//@AfterThrowing(pointcut = "execution(* ICount.*(..))" , throwing = "ex")
@AfterThrowing(pointcut = "p()" , throwing = "ex")
public void throwAdvice(JoinPoint joinPoint , Throwable ex){
System.out.println("LogAspect 日志追踪 The method "+joinPoint.getSignature().getName()+" 发生异常 "+ex.getMessage());
}
}
public class MainTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig.class);
//ICount count = ac.getBean(ICount.class);
CountImpl count = ac.getBean(CountImpl.class);
//com.sun.proxy.$Proxy16 JDK动态代理
//com.igeek.ch02.log.CountImpl$$EnhancerBySpringCGLIB$$3f6dec88 CGLIB动态代理
System.out.println(count.getClass().getName());
int divResult = count.div(10, 0);
System.out.println("divResult = "+divResult);
}
}
4.5 环绕通知
环绕通知@Around是所有通知类型中功能最为强大的(前面四种通知都能够实现),能够全面地控制连接点,甚至可以控制是否执行连接点。
注:环绕通知的方法需要返回目标方法执行之后的结果,即调用ProceedingJoinPoint的proceed()方法来调用原始方法的返回值,否则会出现空指正异常。
@Component
@Aspect
public class LogAspect {
//环绕通知
@Around(value = "execution(* ICount.*(..))")
public Object aroundAdvice(ProceedingJoinPoint pjp){
String name = pjp.getSignature().getName();
Object[] args = pjp.getArgs();
System.out.println("@Around 日志追踪 The method "+name+" begin with "+ Arrays.toString(args));
Object result = null;
try {
//执行目标方法,若目标方法没有返回值则返回null
result = pjp.proceed(args);
System.out.println("@Around 日志追踪 The method "+name+" end with "+result);
} catch (Throwable e) {
System.out.println("@Around 日志追踪 The method "+name+" 发生异常 "+e.getMessage());
throw new RuntimeException(e);
}
System.out.println("@Around 日志追踪 The method "+name+" end");
return result;
}
}
五、切点表达式
1.execution(切入点)
execution(访问权限 返回值 包名.类名.方法名(形参列表))。
例如:execution(public int com.igeek.ch02.log.ICount.add(int , int))
或者可以写 重入切点表达式
/**
* 重入切点表达式
* 注解@Pointcut("execution(* ICount.*(..))")
* 第一个*:代表任意访问权限和任意返回值类型;第二个*:代表任意方法;第三个..:代表任意的形参列表
*
* 公共的切入点如何使用? @Before("p()")
* 1.本类 方法名()
* 2.同包下不同类 类名.方法名()
* 3.不同包下的类 包名.类名.方法名()
*
* 声明重入切点表达式有没有要求?
* 1.访问权限修饰符,代表了当前切入点的可见性
* 2.不能有返回值void,不可以写形参列表,不可以有方法体
*/
//@Pointcut("execution(* ICount.*(..))")
@Pointcut("execution(* CountImpl.*(..))")
public void p(){}
则其他通知切入点直接使用方法名称即可,如 @Before("p()")