AOP前奏
计算器案例
package com.mytest.aopbefore;
public interface Calc {
//加法
public int add(int i,int j);
//减法
public int sub(int i,int j);
//乘法
public int mul(int i,int j);
//除法
public int div(int i,int j);
}
package com.mytest.aopbefore;
public class CalcImpl implements Calc{
/**
* 为加减乘除方法,添加如下功能
1.记录日志
计算之前:参数
计算之后:结果
2.验证功能
3.
...
*/
@Override
public int add(int i, int j) {
// System.out.println("==>add()加法执行之前,记录参数:i:"+i+",j:"+j);
//验证i和j合法性
//MyLogging.methodBefore("add",i,j);
int rs = i + j; //核心计算代码
// MyLogging.methodAfter("add",rs);
// System.out.println("==>add()加法执行之后,记录结果:"+rs);
return rs;
}
@Override
public int sub(int i, int j) {
// System.out.println("==>sub()加法执行之前,记录参数:i:"+i+",j:"+j);
MyLogging.methodBefore("sub",i,j);
int rs = i - j;
MyLogging.methodAfter("sub",rs);
// System.out.println("==>sub()加法执行之后,记录结果:"+rs);
return rs;
}
@Override
public int mul(int i, int j) {
int rs = i * j;
return rs;
}
@Override
public int div(int i, int j) {
int rs = i / j;
return rs;
}
}
问题发现及解决方案
-
发现问题
- 在核心业务代码(计算操作)中,直接书写非核心业务代码(日志,验证等)
- 导致代码分散及代码混乱问题
- 代码分散:每个计算方法都书写相同代码
- 代码混乱:核心代码与非核心代码耦合(书写在一处)
- 高内聚,低耦合
-
解决方案
- 解决代码分散:将非核心业务代码,提取到工具类中(切面类)
- 解决代码混乱:将非核心业务代码先横向提取到工具类,再动态织入回核心代码
静态代理与动态代理
代理模式
- 生活中代理
- 各种中介(房屋中介)
- 你需要找房子,但不能亲力亲为
- 程序中代理
- 静态代理:代理对象固定
- 动态代理:代理对象非固定
静态代理
package com.mytest.aopbefore;
public class MyStaticProxy implements Calc {
//目标对象
private CalcImpl calcImpl;
public MyStaticProxy(CalcImpl calcImpl) {
this.calcImpl = calcImpl;
}
@Override
public int add(int i, int j) {
//执行方法之前,记录日志
MyLogging.methodBefore("add",i,j);
//执行目标对象的加法
int rs = calcImpl.add(i, j);
//执行方法之后,记录日志
MyLogging.methodAfter("add",rs);
return rs;
}
@Override
public int sub(int i, int j) {
MyLogging.methodBefore("sub",i,j);
//执行目标对象的减法
int rs = calcImpl.sub(i, j);
MyLogging.methodAfter("sub",rs);
return rs;
}
@Override
public int mul(int i, int j) {
return 0;
}
@Override
public int div(int i, int j) {
return 0;
}
}
@Test
void contextLoads() {
//创建目标对象
CalcImpl calcImpl = new CalcImpl();
//(为目标对象)创建代理对象
Calc calc = new MyStaticProxy(calcImpl);
//错误的,代理对象与目标对象是兄弟关系,不能相互转换
// CalcImpl calc = new MyStaticProxy(calcImpl);
int rs = calc.add(1, 2);
System.out.println("rs = " + rs);
}
动态代理
package com.mytest.aopbefore;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class MyDynimacProxy {
//目标对象
private Object target;
public MyDynimacProxy(Object target) {
this.target = target;
}
public Object getProxy(){
//代理对象
Object proxy = null;
//获取目标对象的类加载器
ClassLoader classLoader = target.getClass().getClassLoader();
//获取目标对象的接口
Class<?>[] interfaces = target.getClass().getInterfaces();
proxy = Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//获取方法名称
String methodName = method.getName();
//执行之前,日志功能
MyLogging.methodBefore(methodName,args);
//执行目标对象的方法
Object rs = method.invoke(target, args);
//执行之后,日志功能
MyLogging.methodAfter(methodName,rs);
return rs;
}
});
return proxy;
}
}
//测试动态代理
@Test
void testDynamicProxy() {
//目标对象
CalcImpl calcImpl = new CalcImpl();
//创建工具类型
MyDynimacProxy proxy = new MyDynimacProxy(calcImpl);
//获取代理对象
Calc calc = (Calc) proxy.getProxy();
calc.add(5,6);
//错误的,代理对象与目标对象是兄弟关系,不能相互转换
//java.lang.ClassCastException: class jdk.proxy2.$Proxy98 cannot be cast to class com.mytest.aopbefore.CalcImpl
// CalcImpl calc = (CalcImpl) proxy.getProxy();
// calc.add(5,6);
}
小结
Spring中支持两种代理实现方式
- JDK动态代理
- 目标对象与代理对象均实现同一个接口
- 目标对象与代理对象是兄弟关系,兄弟关系不能相互转换,如转换会报错:ClassCastException
- CGLIB动态代理
- 代理对象"继承"目标对象
- 代理对象与目标对象是"父子"关系,可以相互转换
Spring框架默认使用JDK动态代理,SpringBoot环境中默认代理模式CGLIB
基于注解实现AOP
AOP概念
AOP概念
- AOP:Aspect Oriented Programming(面向切面编程设计)
- 横向扩展机制
- OOP:Object Oriented Programming(面向对象编程设计)
- 纵向继承机制
AOP相关术语
-
横切关注点:非核心业务代码(提取类之前)
- 如:日志代码
-
通知:将非核心业务代码提取到类中后,称之为通知(提取类之后)
-
连接点:非核心业务代码织入到核心业务代码的位置(通知之前)
- 日志功能在加减乘除方法中书写位置
-
切入点:非核心业务代码织入到核心业务代码的位置(通知之后)
-
切面类(Aspect):将非核心业务代码提取到类中,这个类称之为切面类
- 将日志功能提取MyLoggin,MyLogging就是切面类
-
目标对象:被代理的对象称之为目标对象
- 如:CalcImpl是目标对象
-
代理对象:通过代理类中getProxy()方法获取的对象,称之为代理对象
使用AspectJ基于注解实现AOP
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
定义切面类(通知)
package com.mytest.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component //将组件装配到IOC容器
@Aspect //标识当前类是一个切面类
public class MyLogging {
//前置通知:日志(方法前)通知
@Before("execution(public int com.mytest.aop.CalcImpl.add(int,int))")
public void methodBefore(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("==>"+methodName+"()正在执行,参数:"+ Arrays.toString(args));
}
}
开启AspectJ动态代理(SpringBoot环境中可省略)
package com.mytest.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan(basePackages = "com.mytest.aop")
//开启AspectJ动态代理
@EnableAspectJAutoProxy
public class SpringConfigAop {
}
测试
package com.mytest;
import com.mytest.aop.Calc;
import com.mytest.aop.CalcImpl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
//@SpringBootTest注解,自动整合Junit
@SpringBootTest
public class TestAOP {
@Autowired
private CalcImpl calc;
// private Calc calc;
@Test
void testAOP(){
System.out.println("calc.getClass().getName() = " + calc.getClass().getName());
calc.add(1,2);
}
}
切入点表达式详解
切入点表达式语法
语法:execution(public int com.mytest.aop.CalcImpl.add(int,int))
通配符
..
可以代表任意参数类型及参数数量
*
可以代表如下细节:
- *可以代表任意访问修饰符及返回值类型
- *可以代表任意包名及类名
- *可以代表任意方法名
重用切入点表达式
-
提取可重用切入点表达式
//提取可重用,切入点表达式 @Pointcut("execution(* com.mytest.aop.CalcImpl.*(..))") public void myPointCut(){}
-
引用切入点表达式
@Before("myPointCut()") public void methodBefore(JoinPoint joinPoint){}
JoinPoint接口
joinPoint.getSignature():获取方法签名
- 方法签名=方法名称+形参列表
joinPoint.getSignature().getName():获取方法名称
joinPoint.getArgs():获取参数列表
AOP的五大通知(通知)
-
前置通知:@Before
-
执行时机:在目标方法执行之前执行
-
注意:如目标方法有异常,前置通知会执行
@Before("myJoinPoint()") public void methodBefore(JoinPoint joinPoint){}
-
-
后置通知:@After
-
执行时机:在目标方法执行之后执行(最后)
-
注意:如目标方法有异常,后置通知执行
@After("myJoinPoint()") public void methodAfter(JoinPoint joinPoint){}
-
-
返回通知:@AfterReturning
-
执行时机:在目标方法返回结果执行
-
注意:与目标方法有无异常没有关系(一般有异常时不返回结果,所以返回通知不执行)
@AfterReturning(value = "myJoinPoint()",returning = "rs") public void methodAfterRetuning(JoinPoint joinPoint,Object rs){ String methodName = joinPoint.getSignature().getName(); System.out.println("返回通知==>"+methodName+"()方法返回结果rs:"+rs); }
-
-
异常通知:@AfterThrowing
-
执行时机:在目标方法抛出异常时执行
-
注意:有异常执行,无异常不执行,且throwing = "ex"与形参列表中的ex一致
@AfterThrowing(value = "myJoinPoint()",throwing = "ex") public void methodAfterThrowing(JoinPoint joinPoint,Exception ex){ String methodName = joinPoint.getSignature().getName(); System.out.println("异常通知==>"+methodName+"()方法的异常ex:"+ex); }
-
-
环绕通知:
-
环绕通知:整合以上四个通知(前置&后置&返回&异常)
-
注意
-
环绕通知参数必须使用ProceedingJoinPoint,
- 使用ProceedingJoinPoint的joinPoint.proceed();可以手动触发目标对象的相应方法
-
环绕通知必须将目标方法的返回值返回,如不设置返回值,会报如下错:
org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type for: public abstract int com.mytest.aop.Calc.add(int,int)
-
-
示例代码
//环绕通知(四合一) @Around("myJoinPoint()") public Object methodAround(ProceedingJoinPoint pjp){ String methodName = pjp.getSignature().getName(); Object[] args = pjp.getArgs(); Object rs = null; try { //前置通知 System.out.println("前置通知==>"+methodName+"()正在执行,参数:"+ Arrays.toString(args)); // 执行目标方法,如:add() sub() mul() div() rs = pjp.proceed(); //返回通知 System.out.println("返回通知==>"+methodName+"()方法返回结果rs:"+rs); } catch (Throwable e) { //异常通知 System.out.println("异常通知==>"+methodName+"()方法的异常ex:"+e); throw new RuntimeException(e); } finally { //后置通知 System.out.println("后置通知==>"+methodName+"()执行完毕!"); } return rs; }
-
-
小结
- 通知执行顺序
- 有异常:前置通知 -> 异常通知 -> 后置通知
- 无异常:前置通知 -> 返回通知 -> 后置通知
- 通知执行顺序
定义切面优先级
-
语法:@Order(index)
- index数值越小优先级越高,但一般推荐正整数
-
源码
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) @Documented public @interface Order { int value() default Integer.MAX_VALUE; }
AOP实现方式(动态代理实现方式)
-
JDK支持动态代理,底层目标对象实现接口方式**(代理对象目标对象关系:兄弟)**
- springboot设置jdk动态代理方式:spring.aop.proxy-target-class=false
- jdk.proxy2.$Proxy98
-
CGLIB(cglib)默认方式,底层目标对象不实现接口方式(认干爹:代理对象与目标对象关系:父子)
- com.mytest.aop.CalcImpl0