重点看切入点、通知、基于注解的AOP。
AOP:多个方法在相同位置有相同的操作,切一刀,将切面抽象为一个对象,把相同的操作代码写进对象,对对象进行编程,底层使用动态代理机制。
0. 场景模拟:计算器加减乘除
接口Calculate:
package com.circle.aop;
public interface Calculate {
public int add(int num1, int num2);
public int sub(int num1, int num2);
public int mul(int num1, int num2);
public int div(int num1, int num2);
}
实现接口的类CalculateImpl:
package com.circle.aop;
public class CalculateImpl implements Calculate{
@Override
public int add(int num1, int num2) {
System.out.println("add方法的参数为:" + num1 + "、" + num2);
int result = num1 + num2;
System.out.println("add方法的结果为:" + result);
return result;
}
@Override
public int sub(int num1, int num2) {
System.out.println("sub方法的参数为:" + num1 + "、" + num2);
int result = num1 - num2;
System.out.println("sub方法的结果为:" + result);
return result;
}
@Override
public int mul(int num1, int num2) {
System.out.println("mul方法的参数为:" + num1 + "、" + num2);
int result = num1 * num2;
System.out.println("mul方法的结果为:" + result);
return result;
}
@Override
public int div(int num1, int num2) {
System.out.println("div方法的参数为:" + num1 + "、" + num2);
int result = num1 / num2;
System.out.println("div方法的结果为:" + result);
return result;
}
}
可以看出,方法前后的两行输出与核心代码无关,也不好修改,代码维护性、复用性差。
通过AOP,把这两行日志代码抽离出核心业务代码,统一处理,使核心业务代码与非业务代码解耦合。
AOP的优点:
- 可以降低模块之间的耦合性
- 提供代码的复用性
- 提高代码的维护性
- 集中管理非业务代码,便于维护
- 业务代码不受非业务代码影响,逻辑更加清晰
一、代理模型
二十三种设计模式中的一种,属于结构型模式。
它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来,实现解耦。
调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
可以想象成是明星和经纪人的关系。
代理可以在不改变源代码的基础上实现对代码的拓展
常用到的业务场景:
- 事务的处理
- 日志的打印
- 性能监控
- 异常的处理等
代理:非核心内容,将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
目标:核心内容,被代理“套用”了非核心逻辑代码的类、对象、方法。
1. 静态代理
静态代理在编译时就已经确定,代理类是由程序员手动编写的。在静态代理中,代理类和目标类实现相同的接口,代理类持有目标类的引用,并委托目标类来执行实际操作。
利用接口类型对传入的对象做接收 (多态),通过带参构造传入一个接口实现类的对象。
静态代理类:
核心目标:
静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,每个功能每个接口都需要代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。
提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现,这就需要使用动态代理技术了。 而动态代理所有接口都可以用一个代理类。
2. 动态代理
动态代理是在运行时由JDK或第三方库(如CGLIB)生成的代理。Spring AOP通常使用JDK动态代理,也可以使用CGLIB库。
使用场景
- 如果目标对象实现了接口,优先使用JDK动态代理,也可以CGLIB。
- 如果目标对象没有实现接口,或者需要使用继承的方式增强方法,只能使用CGLIB。
JDK动态代理
JDK动态代理基于反射机制,可以在运行时动态创建代理对象。
它要求目标对象和代理对象实现同样的接口(兄弟拜把子)。代理类会持有一个目标对象的引用,并委托给目标对象来执行方法。
优点:
- 代理类的生成是自动的,不需要手动编写。
- 可以代理实现了接口的任意方法,灵活性高。
缺点:
- 只能对实现了接口的类进行代理。
CGLIB库
CGLIB(Code Generation Library)是一个强大的代码生成库,它可以用于创建子类而不是代理。Spring AOP使用CGLIB来为没有实现接口的类创建代理。CGLIB通过继承目标类实现代理(认干爹),并覆盖父类的方法来实现增强。
优点:
- 可以代理没有实现接口的类。
- 生成子类的方式比反射更高效。
缺点:
- 生成的子类会覆盖原有类的实现,可能导致一些复杂的类继承问题。
newProxyInstance获取代理对象,需要三个参数(ClassLoader loader : 类加载器、Class<?>[] interfaces : 接口信息、InvocationHandler h : 调用处理器,其本身也是一个接口,内部声明了抽象方法invoke)
invoke方法写具体的日志部分,需要三个参数(Object proxy代理对象、Method method方法、Object[] args方法中的参数)
JDK动态代理实现:可不看,因为Spring AOP动态代理更好
2.1 写一个生成代理对象的工厂类,这个类可以提供一个Calculate接口的代理对象,类中有一个方法返回代理对象getProxy(),将目标对象作为成员变量(为了通用使用Object类,而不是Calculate类)
target 需要被代理的目标对象
getProxy()方法:通过反射创建目标对象的代理对象
2.2 完善getProxy方法:返回代理对象
//编写一个方法,用于返回代理对象 (用到反射机制)
public Object getProxy() {
/**
(1) ClassLoader loader : 加载动态代理生成类的类加载器
(2) Class<?>[] interfaces : 目标对象实现的所有接口的class类型数组
(3) InvocationHandler h : 调用处理器,其本身也是一个接口,内部声明了抽象方法invoke,设置代理对象实现目标对象方法的过程。
*/
//(1)得到类加载器
ClassLoader classLoader = target.getClass().getClassLoader();
//(2)得到被执行对象的接口信息(因为newProxyInstance方法底层是通过接口来调用的,即接口多态)
Class<?>[] interfaces = target.getClass().getInterfaces();
//(3)得到处理器对象(通过匿名内部类实现,最终返回的是一个匿名内部类对象)
//需注意,处理器对象本身也是newProxyInstance方法的一个形参
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/*
proxy:代理对象、method:需要重写目标对象的方法、args:method方法的参数
将相同的业务逻辑代码,放在处理器对象的invoke对象中,
避免了代码下沉到每个实现类所造成的代码冗余。
*/
Object result = null;
try{
System.out.println("[动态代理][日志]“+method.getName()+",参数:"+Arrays.tostring(args))");
result = method.invoke(target, args); //通过反射调用实现类中的方法
System.out.println("[动态代理][日志]"+method.getName()+",结果:"+
result");
} catch(Exception e) {
e.printStackTrace();
System.out.printin("[动态代理][日志]"+method.getName()+",异常:"+e.getMessage());
} finally {
System.out.print1n("[动态代理][日志]"+method.getName()+“,方法执行完毕”);
}
return result;
}
};
return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
}
2.3 测试:向工厂类中传入目标对象得到一个工厂对象,调用工厂对象的getProxy方法并向下转型得到代理对象
二、AOP
1. 概念及相关术语
AOP(Aspect Oriented Programming)是一种设计思想,面向切面编程,它是面向对象编程的一种补充和完善。它以通过预编译方式和运行期动态代理方式实现,在不修改源代码的情况下,给程序动态统一添加额外功能。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
将那些与业务代码无关,但对多个对象产生影响的公共行为,封装成一个可重用的模块(切面)。
以下相关术语关注通知和切入点:
切入点是用来确定“哪里”和“何时”织入横切关注点,而通知则是“如何”织入横切关注点的具体实现。切入点是通知的上下文,它决定了哪些通知应该在哪些连接点上执行。
1.1 横切关注点:相同的非核心业务
分散在各个模块中解决同样的问题,如用户验证、日志管理、事务处理、数据缓存都属于横切关注点。
从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
这个概念不是语法层面的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。
1.2 通知(增强):实现横切关注点的代码块
增强,通俗说,就是你想要增强的功能,比如 安全,事物,日志等。
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
前置通知:在被代理的目标方法前执行
返回通知:在被代理的目标方法成功结束后执行(寿终正寝)
异常通知:在被代理的目标方法异常结束后执行(死于非命)
后置通知:在被代理的目标方法最终结束后执行(盖棺定论)
环绕通知:使用try..catch..finaly结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
1.3 切面:封装通知方法的类(通知+切入点)
1.4 目标 :被代理、通知的对象
1.5 代理:向目标对象应用通知后创建的代理对象
1.6 连接点:使用通知的地方,程序执行的某个特定位置
把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。通俗说,就是spring允许你使用通知的地方。
1.7 切入点:筛选连接点,实际使用通知的地方
切入点是一个连接点的过滤条件,AOP 通过切点定位到特定的连接点。
每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。
如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL语句,即查询条件。
切点和连接点不是一对一的关系,一个切点匹配多个连接点
切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
为什么要筛选连接点得到切入点?
不是所有的连接点都是业务逻辑中重要的部分,或者都需要横切关注点的干预。通过筛选,我们可以只选择那些对横切关注点有意义的连接点,从而减少不必要的代码干预,保持业务逻辑的清晰和简洁。
横切关注点的织入可能会对应用程序的性能产生影响。通过精确筛选切入点,我们可以避免在不必要的连接点上进行横切,从而减少性能开销。
2. 基于注解的AOP
AspectJ:是AOP思想的一种实现。本质上是静态代理,将代理逻辑“织入"被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了Aspectj中的注解。
2.1 在pom.xml中引入aop、aspects依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.0.2</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.2</version>
</dependency>
2.2 准备被代理的目标资源(接口和目标实现类)
2.3 创建切面类:切入点+通知s
2.4 配置Spring配置文件beans.xml
2.5 测试
3. 基于xml的AOP
现多基于注解,xml可不看
3.1 3.2 与上同
3.3 切面类:去掉注解部分即可,将基于xml实现
3.4 配置文件bean.xml:实现五种通知类型
<aop:aspect ref="切面类名(首字母小写)">
<aop:pointcut id = "切入点id(任意,但后续pointcut-ref要与此保持一致)" expression="切入点表达式">
五种通知类型:<aop:before>。。。