AOP面向切面编程
静态代理和动态代理
静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。
动态代理
将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现,JDK本身就支持动态代理,这是反射技术的一部分。
public class TestStaticProxy {
@Test
public void testProxy(){
//1. 创建一个CalculatorPureImpl对象:被代理者对象
Calculator calculator = new CalculatorPureImpl();
//2. 创建代理者对象:使用动态代理技术创建
//动态代理技术分为两种:
//1. JDK内置的动态代理技术:要求被代理者必须实现接口,它的底层其实就是使用反射创建接口的实现类对象
//2. CGLIB的动态代理技术(需要引入依赖):被代理者可以不实现接口,它的底层其实是创建被代理类的子类对象
//我们现在使用JDK的动态代理技术
Class<? extends Calculator> clazz = calculator.getClass();
//参数一:用于定义代理对象的类加载器:和被代理者的类加载器是同一个类加载器
ClassLoader classLoader = clazz.getClassLoader();
//参数二:表示要被代理的接口的集合
//获取参数二的方式一: 获取被代理者实现的所有接口
Class<?>[] interfaces = clazz.getInterfaces();
//获取参数二的方式二: 自己编写一个数组,将你想要代理的接口放里面new Class[]{Calculator.class}
//参数三:InvocationHandler接口的实现类对象或者是匿名内部类对象,在它里面真正编写代理逻辑
Calculator calculatorProxy = (Calculator) Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//这个方法中就编写具体的代理逻辑
//invoke()方法什么时候会执行:当代理对象调用任何方法的时候都会执行invoke()
//proxy参数:就是调用方法的代理对象
//method参数:代理对象调用的那个方法
//args参数:代理对象调用的那个方法的参数
//目标:在被代理者的add、sub、mul、div方法前后添加日志打印
//1. 判断方法名是否是add、sub、mul、div
String methodName = method.getName();
if (methodName.equals("add") || methodName.equals("sub") || methodName.equals("mul") || methodName.equals("div")) {
//先执行前置打印日志
System.out.println("[日志] "+methodName+" 方法开始了,参数是:" + args[0] + "," + args[1]);
//执行被代理者的核心逻辑
Object result = method.invoke(calculator, args);
//执行后置打印日志
System.out.println("[日志] "+methodName+" 方法结束了,结果是:" + result);
return result;
}
//返回值是返回给代理对象调用的那个方法
//对于不需要进行代理的方法,就执行被代理者原本的方法
return method.invoke(calculator,args);
}
});
int result = calculatorProxy.sub(2, 3);
System.out.println("在外面打印代理对象执行运算的结果:" + result);
}
}
使用代理模式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KroIFJ3I-1640004690352)(./img/003.jpg)]
相关术语
①代理:又称之为代理者,用于将非核心luoji剥离出来,封装这些非核心逻辑类、方法、对象
②目标:用于核心逻辑,并把这些代理者非核心逻辑用在类、对象方法上
AOP
简化代码:把固定重复的代码抽取出来,让其方法更专注于自己的核心功能,提高内聚性
代码增强:抽取出来的方法,放在切面类里面,看哪里需要就往哪里套,被套用的切面楼价就被切面类增强了
AOP的核心思路
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hRh3PdmJ-1640004690360)(./img/004.jpg)]
相关术语
横切关注点
从每个方法中抽取同一类非核心业务
通知
每个横向切点都需写一个方法来实现
- 前置通知:在被代理的目标方法前执行
- 返回通知:在被代理的目标方法成功结束后执行(寿终正寝)
- 异常通知:在被代理的目标方法异常结束后执行(死于非命)
- 后置通知:在被代理的目标方法最终结束后执行(盖棺定论)
- 环绕通知:使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wDBspPJD-1640004690363)(.\img\005.png)]
切面
封装通知的方法类
目标
被代理目标对象
代理
向目标对象应用通知之后创建的代理对象
连接点
各个方法中可以被增强或修改的点
切入点
方法中真正要去配置增强或者配置修改的地方
注解方式配置AOP
1、加入依赖
<dependencies>
<!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.1</version>
</dependency>
<!-- junit测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>
<!--spring整合Junit-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.1</version>
</dependency>
</dependencies>
2、准备接口
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
3、接口的实现类
import org.springframework.stereotype.Component;
@Component
public class CalculatorPureImpl implements Calculator {
@Override
public int add(int i, int j) {
int result = i + j;
//int num = 10 / 0;
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
return result;
}
}
4、创建切面类
/**
* Aspect注解:指定一个切面类
* Component注解: 对这个切面类进行IOC
*
* 注解AOP的关键点:
* 1. 一定要在配置文件中加上<aop:aspectj-autoproxy />表示允许自动代理
* 2. 切面类一定要加上Aspect注解,并且切面类一定要进行IOC
* 3. 其它的类该进行IOC和依赖注入的就一定要进行IOC和依赖注入
* 4. 通知上一定要指定切入点(怎么使用切入点表达式描述切入点又是一个难点)
*/
@Aspect
@Component
public class LogAspect {
@Before("execution(int com.wwb.component.CalculatorPureImpl.*(int,int))")
public void printLogBeforeCore(){
System.out.println("[前置通知]在方法执行之前打印日志...");
}
@AfterReturning("execution(int com.wwb.component.CalculatorPureImpl.*(int,int))")
public void printLogAfterReturning(){
System.out.println("[返回通知]在方法执行成功之后打印日志...");
}
@AfterThrowing("execution(int com.wwb.component.CalculatorPureImpl.*(int,int))")
public void printLogAfterThrowing(){
System.out.println("[AOP异常通知]在方法抛出异常之后打印日志...");
}
@After("execution(int com.wwb.component.CalculatorPureImpl.*(int,int))")
public void printLogFinallyEnd(){
System.out.println("[AOP后置通知]在方法最终结束之后打印日志...");
}
}
5、Spring的配置文件
<!--包扫描-->
<context:component-scan base-package="com.wwb"/>
<!--允许注解AOP-->
<aop:aspectj-autoproxy />
6、测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-application.xml")
//@ContextConfiguration这个注解通常与@RunWith(SpringJUnit4ClassRunner.class)联合使用用来测试
public class TestAop {
@Autowired
private Calculator calculator;
@Test
public void testAdd(){
//调用CalculatorPureImpl对象的add()方法
System.out.println("返回值是:"+calculator.add(1, 2));
}
}
[AOP前置通知] 方法开始了 方法内部 result = 12 [AOP返回通知] 方法成功返回了 [AOP后置通知] 方法最终结束了 方法外部 add = 12
在通知内部获取细节信息
JoinPoint接口
-
要点1:JoinPoint接口通过getSignature()方法获取目标方法的签名
-
要点2:通过目标方法签名对象获取方法名
-
要点3:通过JoinPoint对象获取外界调用目标方法时传入的实参列表组成的数组
// 1.通过JoinPoint对象获取目标方法签名对象 // 方法的签名:一个方法的全部声明信息 Signature signature = joinPoint.getSignature(); // 2.通过方法的签名对象获取目标方法的详细信息 String methodName = signature.getName(); System.out.println("methodName = " + methodName); // 3.通过JoinPoint对象获取外界调用目标方法时传入的实参列表 Object[] args = joinPoint.getArgs(); // 4.由于数组直接打印看不到具体数据,所以转换为List集合 List<Object> argList = Arrays.asList(args);
获取目标方法的方法返回值
只有在AfterReturning返回通知中才能够获取目标方法的返回值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M4wzQwaw-1640004690366)(./img/006.jpg)]
切入点
声明切入点
易于维护,一处修改,处处生效。
@Pointcut("execution(int com.wwb.component.CalculatorPureImpl.*(int,int))")
public void calculatorPointCut(){
}
同一个类内部引用切入点
通过方法名引入
@Before("calculatorPointCut()")
public void printLogBeforeCore(JoinPoint joinPoint){
其它类中引用切入点
@Before("com.wwb.pointcut.wwbPointCut.calculatorPointCut()")
public void printLogBeforeCore(JoinPoint joinPoint){}
对项目中的所有切入点进行统一管理
public class wwbPointCut {
@Pointcut("execution(int com.wwb.component.CalculatorPureImpl.*(int,int))")
public void calculatorPointCut(){
}
}
总结
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IqEevW1E-1640004690368)(.\img\007.jpg)]
环绕通知
环绕通知对应整个try…catch…finally结构,可以在目标方法的各个部位进行套用代理逻辑,它能够真正介入并改变目标方法的执行
基于XML方式配置AOP
<!--包扫描-->
<context:component-scan base-package="com.wwb"/>
<!--
使用xml方式配置AOP:
1. 切面: 封装非核心逻辑的那个类,非核心逻辑就是封装在切面的方法中
2. 通知: 将非核心逻辑套在核心逻辑上进行执行
3. 切入点: 核心逻辑
-->
<aop:config>
<!--
1. 切面: ref属性就是指定作为切面的那个对象的id,order属性表示切面的优先级
-->
<aop:aspect id="myAspect" ref="logAspect">
<!--2. 通知-->
<!--配置前置通知-->
<aop:before method="printLogBeforeCore" pointcut-ref="calculatorPoint"/>
<!--配置返回通知-->
<aop:after-returning method="printLogAfterReturning" pointcut-ref="calculatorPoint" returning="result"/>
<!--配置异常通知-->
<aop:after-throwing method="printLogAfterThrowing" pointcut-ref="calculatorPoint" throwing="throwable"/>
<!--配置后置通知-->
<aop:after method="printLogFinallyEnd" pointcut-ref="calculatorPoint"/>
<!--配置环绕通知-->
<aop:around method="printLogAround" pointcut-ref="calculatorPoint"/>
<!--3. 切入点-->
<aop:pointcut id="calculatorPoint"
expression="execution(* com.wwb.component.CalculatorPureImpl.*(..))"/>
</aop:aspect>
</aop:config>
测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-application.xml")
public class TestAop {
@Autowired
private Calculator calculator;
@Test
public void testAdd(){
//调用CalculatorPureImpl对象的add()方法
System.out.println("调用完目标方法之后获取返回值是:"+calculator.sub(5, 3));
}
}