目录
3.4.1.5. AspectJ与Spring AOP的关系
1. 场景模拟
搭建子模块:spring6-aop
1.1. 声明接口
声明计算器接口Calculator,包含加减乘除的抽象方法
package com.sakurapaid.spring6.aop.demo;
/**
* 计算器接口,定义了基本的四则运算方法。
*/
public interface Calculator {
/**
* 加法操作。
*
* @param i 第一个操作数。
* @param j 第二个操作数。
* @return 两个操作数相加的结果。
*/
int add(int i, int j);
/**
* 减法操作。
*
* @param i 第一个操作数。
* @param j 第二个操作数。
* @return 两个操作数相减的结果。
*/
int sub(int i, int j);
/**
* 乘法操作。
*
* @param i 第一个操作数。
* @param j 第二个操作数。
* @return 两个操作数相乘的结果。
*/
int mul(int i, int j);
/**
* 除法操作。
*
* @param i 第一个操作数,作为被除数。
* @param j 第二个操作数,作为除数。
* @return 两个操作数相除的结果。
*/
int div(int i, int j);
}
1.2. 创建实现类
package com.sakurapaid.spring6.aop.demo;
/**
* 实现Calculator接口的计算类,提供基本的四则运算功能。
*/
public class CalculatorImpl implements Calculator {
/**
* 实现加法操作。
*
* @param i 第一个操作数。
* @param j 第二个操作数。
* @return 两个操作数相加的结果。
*/
@Override
public int add(int i, int j) {
int result = i + j; // 计算加法结果
System.out.println("方法内部 result = " + result); // 打印结果
return result; // 返回结果
}
/**
* 实现减法操作。
*
* @param i 第一个操作数。
* @param j 第二个操作数。
* @return 两个操作数相减的结果。
*/
@Override
public int sub(int i, int j) {
int result = i - j; // 计算减法结果
System.out.println("方法内部 result = " + result); // 打印结果
return result; // 返回结果
}
/**
* 实现乘法操作。
*
* @param i 第一个操作数。
* @param j 第二个操作数。
* @return 两个操作数相乘的结果。
*/
@Override
public int mul(int i, int j) {
int result = i * j; // 计算乘法结果
System.out.println("方法内部 result = " + result); // 打印结果
return result; // 返回结果
}
/**
* 实现除法操作。
*
* @param i 第一个操作数。
* @param j 第二个操作数。
* @return 两个操作数相除的结果。
*/
@Override
public int div(int i, int j) {
int result = i / j; // 计算除法结果
System.out.println("方法内部 result = " + result); // 打印结果
return result; // 返回结果
}
}
1.3. 创建带日志功能的实现类
package com.sakurapaid.spring6.aop.demo;
/**
* 实现Calculator接口的计算类,提供基本的四则运算功能,并通过日志记录方法的调用过程。
*/
public class CalculatorLogImpl implements Calculator {
@Override
/**
* 实现加法运算。
* @param i 第一个加数。
* @param j 第二个加数。
* @return 两个数的和。
*/
public int add(int i, int j) {
System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
int result = i + j; // 计算结果
System.out.println("方法内部 result = " + result);
System.out.println("[日志] add 方法结束了,结果是:" + result);
return result;
}
@Override
/**
* 实现减法运算。
* @param i 被减数。
* @param j 减数。
* @return 两个数的差。
*/
public int sub(int i, int j) {
System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j);
int result = i - j; // 计算结果
System.out.println("方法内部 result = " + result);
System.out.println("[日志] sub 方法结束了,结果是:" + result);
return result;
}
@Override
/**
* 实现乘法运算。
* @param i 第一个乘数。
* @param j 第二个乘数。
* @return 两个数的积。
*/
public int mul(int i, int j) {
System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j);
int result = i * j; // 计算结果
System.out.println("方法内部 result = " + result);
System.out.println("[日志] mul 方法结束了,结果是:" + result);
return result;
}
@Override
/**
* 实现除法运算。
* @param i 被除数。
* @param j 除数。
* @return 两个数的商。
*/
public int div(int i, int j) {
System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j);
int result = i / j; // 计算结果
System.out.println("方法内部 result = " + result);
System.out.println("[日志] div 方法结束了,结果是:" + result);
return result;
}
}
1.4. 提出问题
- 现有代码缺陷
针对带日志功能的实现类,我们发现有如下缺陷:
- 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
- 附加功能分散在各个业务功能方法中,不利于统一维护
- 解决思路
解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。
- 困难
解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术。
2. 代理模式
2.1. 概述
代理模式的核心思想是为某个对象(称为目标对象或真实对象)创建一个替代品(称为代理对象)。当客户端请求与目标对象交互时,实际上是与代理对象打交道。代理对象可以透明地转发请求到目标对象,也可以在转发请求前或后添加额外的操作,如权限检查、日志记录、缓存、延迟加载、数据预处理等。这些附加功能通常不属于目标对象的核心业务逻辑,但对系统的整体功能或性能有重要影响。
代理模式的运作机制:
- 代理对象与目标对象实现了相同的接口或继承自同一基类,这意味着它们对外部世界(客户端)呈现出相同的接口,即客户端可以以相同的方式与两者交互。
- 当客户端调用代理对象的方法时,代理对象可以选择直接转发该调用给目标对象,或者在转发前/后执行一些额外的操作。这些操作可以包括但不限于:
-
- 访问控制:如检查调用者的权限,决定是否允许访问目标对象的方法。
- 功能增强:如在方法调用前后添加日志记录、统计计数、性能监控等。
- 资源管理:如缓存目标对象的结果以提高性能,或者延迟加载目标对象直到真正需要时。
- 复杂逻辑封装:将与目标对象交互的复杂流程(如网络通信、事务管理)封装在代理内,简化客户端代码。
通俗例子说明:
- 广告商找大明星拍广告需要经过经纪人:在这个场景中,大明星是目标对象,拥有实际拍摄广告的能力。经纪人则是代理对象,他代表明星与广告商进行谈判、签订合同等工作。广告商并不直接与明星交流,而是通过经纪人这个代理来间接访问明星的服务。经纪人可能还会负责处理非核心演艺事务,如协调档期、管理酬劳等,这些都是明星业务逻辑之外的辅助工作。
- 合作伙伴找大老板谈合作要约见面时间需要经过秘书:大老板是目标对象,拥有决策权和进行商业洽谈的能力。秘书作为代理对象,负责过滤和安排合作伙伴的会面请求。合作伙伴不直接联系大老板,而是通过秘书预约时间,秘书可能会先进行初步筛选,确保会面符合大老板的日程安排和其他要求。
- 房产中介是买卖双方的代理:卖方和买方分别是各自交易过程中的目标对象,他们拥有房源或购房需求。房产中介作为代理,帮助双方寻找合适的交易对象,处理看房、议价、签约、过户等复杂流程。买卖双方无需直接对接所有细节,而是通过中介这一代理进行沟通和交易,中介在此过程中提供了诸如市场分析、法律咨询、手续代办等附加服务。
相关术语解释:
- 代理:在代理模式中,代理是一个类或对象,它代表或封装了对目标对象的访问。代理负责接收客户端的请求,并根据需要执行额外的操作,最终将请求转发给目标对象或返回结果。代理可以增加、改变或限制对目标对象的访问。
- 目标(或真实对象):目标对象是代理所代表的实际对象,它包含了代理所要提供服务的核心业务逻辑。客户端对目标对象的访问通常是由代理对象间接提供的,目标对象本身并不知晓代理的存在,专注于执行其核心职责。
总结起来,代理模式通过引入代理对象来控制对目标对象的访问,实现了职责分离和功能增强,有助于提高代码的可维护性和系统的灵活性。代理对象封装了非核心逻辑,使得这些逻辑可以独立于目标对象进行管理和扩展,同时保持了对客户端的透明性,即客户端无需关心是直接与目标对象交互还是通过代理进行交互。
2.2. 静态代理
静态代理是指在编写代码时,手动创建一个代理类来实现对目标对象的代理。这个代理类通常会遵循以下特点:
- 明确指定代理对象:静态代理类需要提前知道它要代理的是哪个具体的目标类,因此代理类通常会与某个已知接口(或抽象类)关联,确保代理类和目标类具有相同的接口。
- 手动编码代理逻辑:代理类中的每个方法都会包含两部分逻辑:一部分是针对该方法的额外功能(如日志记录、权限检查等),另一部分是调用目标对象相应方法以执行核心业务逻辑。
- 硬编码限制:由于代理类是在编译时就已经确定的,它的行为和功能也是固定的。如果需要为不同的目标类添加类似的代理逻辑,或者调整代理逻辑,就需要为每个目标类编写对应的代理类。这种情况下,代码的重复性高,难以灵活应对变化。
创建静态代理类:
package com.sakurapaid.spring6.aop.demo;
public class CalculatorStaticProxy implements Calculator {
// 将被代理的目标对象声明为成员变量
private final Calculator target;
public CalculatorStaticProxy(Calculator target) {
this.target = target;
}
@Override
public int add(int i, int j) {
// 附加功能由代理类中的代理方法来实现
System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
// 通过目标对象来实现核心业务逻辑
int addResult = target.add(i, j);
System.out.println("[日志] add 方法结束了,结果是:" + addResult);
return addResult;
}
@Override
public int sub(int i, int j) {
return 0;
}
@Override
public int mul(int i, int j) {
return 0;
}
@Override
public int div(int i, int j) {
return 0;
}
}
静态代理虽然实现了解耦,即将附加功能(如日志)与核心业务逻辑分离,但它存在明显的局限性:
- 缺乏灵活性:对于不同的目标类或同一目标类的不同方法,如果都需要添加类似日志这样的附加功能,必须为每个类或方法分别编写代理类和代理方法。这会导致代码重复,且随着需求变化,需要不断修改或新增代理类。
- 无法集中管理:如果需要在系统中全局地应用某种通用的代理逻辑(如日志),静态代理方式会让这些逻辑分散在各个代理类中,不易于统一管理和维护。
基于上述问题,提出了使用动态代理技术的需求。动态代理允许在运行时根据需要动态地创建代理对象,其优势在于:
- 无需手动编写代理类:动态代理通常通过编程框架(如 Java 的 JDK 动态代理或 CGLIB 等第三方库)在运行时自动为给定的目标对象生成代理类及其代理方法,无需手动编写大量代理类代码。
- 通用性与集中管理:动态代理可以为任意符合一定条件(如实现特定接口)的目标对象提供代理,且附加功能(如日志)可以在一个集中位置定义和配置,适用于多种场景和对象,大大减少了代码重复。
- 易于扩展和调整:由于代理逻辑是在运行时动态生成的,可以根据运行时条件或配置动态调整代理行为,使得系统更易于适应需求变化。
举个通俗易懂的例子就是:
静态代理
想象一下,你正在经营一家小店,雇了一位员工(目标对象)专门负责算账。为了监督员工的工作,你找了一个朋友(代理对象)帮忙。朋友的任务是:
- 记录日志:每当员工开始计算或结束计算时,朋友都要记下时间和计算的详情。
- 传递工作:朋友不亲自计算,而是让员工去做实际的计算,然后把结果告诉客户。
在这个例子中,你的朋友就是静态代理。编写了一个名为 CalculatorStaticProxy
的类,就像你朋友的角色一样。这个类实现了 Calculator
接口(就像你的朋友知道如何“算账”),并包含一个指向员工(target
)的引用。代理类的方法(如 add()
)包含了两部分任务:记录日志(附加功能)和调用员工的对应方法(核心业务逻辑)。
局限性
现在,问题来了:
- 不够灵活:假如你开了更多的分店,每家店都需要监督员工算账,你得为每家店找一个朋友做代理。这意味着你需要为每个分店编写一个单独的代理类(就像为每个店找不同的朋友)。如果以后要更改日志格式或添加其他监控功能(如记录计算耗时),你得逐一修改所有代理类的代码,工作量大且容易出错。
- 难以集中管理:每个朋友(代理类)有自己的日志记录方式,没有统一的标准。如果你想查看所有分店的日志,得分别向每个朋友要,不方便且效率低。如果要更改日志处理规则,比如统一上传到云端,你需要逐个通知每个朋友,非常麻烦。
动态代理
为了解决这些问题,你决定雇佣一个专业的审计团队(动态代理)。他们承诺:
- 自动化代理:不论你有多少家分店,只要告诉他们哪位员工需要监督,他们会立即安排一个“虚拟朋友”去工作,无需你手动找人或编写代理类。
- 集中处理日志:所有的“虚拟朋友”都按照审计团队的标准流程记录日志,然后统一上报到审计团队的系统中,便于你集中查看和管理。
- 灵活调整:如果你需要更改日志规则或添加新功能,只需告诉审计团队一次,他们会自动更新所有“虚拟朋友”的行为,无需你一个个通知。
在编程世界里,动态代理就像这个审计团队。它通过编程框架(如 Java 的 JDK 动态代理)在运行时自动为你生成代理对象。你只需要指定目标对象(员工),框架会自动生成一个代理对象(虚拟朋友),这个代理对象能执行附加功能(日志记录)并调用目标对象的方法(算账)。这样,无论有多少个目标对象(分店员工),都可以用同一个动态代理机制来处理,日志逻辑也集中在一个地方管理,大大提高了代码的复用性和可维护性。
总结来说,静态代理就像手动找朋友帮忙监督员工,虽然实现了功能,但扩展困难、管理分散。而动态代理就像雇佣专业团队,自动、集中且灵活地处理代理任务,更适合应对复杂多变的需求。
2.3. 动态代理
要实现JDK动态代理,需要遵循以下步骤:
- 创建代理接口
首先确保您的目标对象实现了某个接口(如本例中的Calculator接口)。这是JDK动态代理的基本要求,因为JDK动态代理只能针对接口进行代理。
- 定义InvocationHandler
创建一个实现了java.lang.reflect.InvocationHandler接口的类,它负责处理代理对象上所有方法的调用。在这个类中实现invoke()方法,该方法接收三个参数:
Object proxy: 代理对象本身。
Method method: 被调用的方法对象。
Object[ ] args: 调用方法时传入的实际参数数组。
在invoke()方法中,您可以添加额外的日志记录或其他横切关注点逻辑,然后调用目标对象的实际方法。
- 生成代理对象
使用java.lang.reflect.Proxy类的newProxyInstance()静态方法创建代理对象。此方法需要以下三个参数:
ClassLoader loader: 目标对象的类加载器。
Class<?>[] interfaces: 目标对象所实现的接口数组。
InvocationHandler h: 实现了InvocationHandler接口的对象,即第二步中创建的类实例。
生产代理对象的工厂类:
package com.sakurapaid.spring6.aop.demo;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
/**
* 动态代理工厂类,用于创建目标对象的动态代理实例。
*/
public class ProxyFactory {
private Object target;
/**
* 构造函数,初始化目标对象。
* @param target 目标对象,将为此对象创建动态代理。
*/
public ProxyFactory(Object target) {
this.target = target;
}
/**
* 获取目标对象的动态代理实例。
* @return 返回代理对象,该对象实现了目标对象的所有接口。
*/
public Object getProxy(){
// 获取目标对象的类加载器和实现的接口数组
ClassLoader classLoader = target.getClass().getClassLoader(); // 获取目标对象的类加载器
Class<?>[] interfaces = target.getClass().getInterfaces(); // 获取目标对象的实现的接口数组
// 设置代理对象的行为,即在调用任何方法时执行的逻辑
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 在方法执行前后打印日志
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.println("[动态代理][日志] "+method.getName()+",异常:"+e.getMessage());
} finally {
System.out.println("[动态代理][日志] "+method.getName()+",方法执行完毕");
}
return result;
}
};
// 创建并返回代理实例
return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
}
}
测试输出
package com.sakurapaid.spring6.aop.demo;
import org.junit.jupiter.api.Test;
/**
* 计算器测试类,用于测试动态代理功能。
*/
public class CaculatorTest {
/**
* 测试使用动态代理进行方法拦截。
* 该测试方法不接受参数,也不返回任何值。
* 主要流程是创建一个代理工厂,针对CalculatorLogImpl实例生成一个动态代理对象,
* 然后通过该代理对象调用add方法,以验证动态代理的拦截功能是否正常。
*/
@Test
public void testDynamicProxy(){
// 创建代理工厂,并设置目标对象为CalculatorLogImpl的实例
ProxyFactory factory = new ProxyFactory(new CalculatorLogImpl());
// 通过代理工厂获取代理对象
Calculator proxy = (Calculator) factory.getProxy();
// 使用代理对象调用add方法,验证动态代理是否生效
proxy.add(1,2);
}
}
3. AOP切面编程
3.1. 概述
AOP是什么?
想象你开了一家餐厅,餐厅里有很多事情要做:厨师负责烹饪美食(这是主业务,就像程序中的核心业务逻辑),服务员接待客人、点菜、上菜(这也是业务的一部分),还有清洁工打扫卫生、收银员结账等。除此之外,还有一些贯穿整个运营过程的“边缘任务”,比如:
- 记账:每次卖出一道菜,都需要记录销售额和成本,以便月底算账。
- 消毒:每接待一批客人后,都要对桌面和餐具进行消毒,保证食品安全。
- 提醒:当某种食材快要用完时,需要及时提醒采购员补货。
这些“边缘任务”(我们称它们为“切面”)虽然不是直接做菜或服务客人,但对餐厅的正常运营至关重要。而且,它们会涉及到餐厅的各个环节,不是只针对某一特定角色或环节。
传统的做法
以前,你可能让厨师、服务员、清洁工等人在忙完自己的本职工作后,额外花时间去记账、消毒、提醒采购。这样做的问题是:
- 分散注意力:每个人都得操心额外的事情,可能会影响他们做好本职工作。
- 重复劳动:比如每道菜售出时,每个服务员都得记一笔账,很浪费时间。
- 难于管理:记账、消毒、提醒采购的规则可能经常变动,要通知所有人并确保他们按新规则执行,很麻烦。
AOP的做法
AOP就像是请了一个全能助手,专门负责这些“边缘任务”。助手站在一旁观察餐厅的运营情况,按照设定好的规则自动执行:
- 记账助手:每道菜卖出时,助手自动记账,月底直接给你报表。
- 消毒助手:客人离开后,助手立刻去清理桌面、消毒餐具,无需服务员操心。
- 提醒助手:食材快用完时,助手直接通知采购员,无需厨师分心。
这样做的好处:
- 专注主业:厨师、服务员可以专心烹饪和服务,不用分心做其他事。
- 高效执行:记账、消毒等任务由专人专职处理,速度快、准确率高。
- 易于管理:修改记账规则、消毒频率、提醒条件时,只需告诉助手一个人即可,省时省力。
映射到编程领域
在软件开发中,AOP就是那个全能助手。它能在程序运行时,自动在合适的地方插入额外的代码(比如记录日志、开启事务、权限检查等),而不用修改原来的业务代码。这样:
- 业务代码更纯粹:程序员只需关注业务逻辑本身,不用在代码中混杂各种“边缘任务”。
- 功能复用:比如日志记录逻辑,可以统一定义一次,然后在需要的地方自动应用,无需重复编写。
- 易于维护:如果需要调整某个“边缘任务”(如日志格式),只需改动一处,不影响业务代码。
总结来说,AOP是一种编程思想,它让程序能够自动在执行主业务逻辑的同时,处理好那些与业务逻辑交织但又相对独立的“边缘任务”,使得代码更清晰、更易维护,同时也提高了开发效率。在Spring框架中,AOP提供了具体的工具和技术实现这一思想。
AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现,在不修改源代码的情况下,给程序动态统一添加额外功能的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
3.2. 相关术语
3.2.1. 横切关注点
比喻: 想象你经营一家餐厅,每天有许多日常事务要处理。其中有一些事务是贯穿整个餐厅运营的,就像一根根“红线”贯穿于各个部门之间。例如:
- 食品安全检查:无论是厨房烹饪、服务员上菜还是收银结账,都需要确保食物安全。
- 员工考勤:所有员工上班时都需要打卡,下班时也需要记录工时。
- 顾客满意度调查:在顾客用餐结束后,可能需要让他们填写问卷,收集反馈。
这些贯穿于餐厅各个角落、各部门都需处理的共同问题,就像“红线”一样横跨各个业务模块,我们称之为“横切关注点”。
解释: 在软件开发中,横切关注点指的是那些在不同业务模块中反复出现、与业务逻辑本身关系不大,但又不可或缺的处理逻辑。比如用户验证、日志记录、事务管理、数据缓存等。它们并非某一个具体功能的核心部分,而是与整个系统的行为规范、运行环境、管理需求等相关。
3.2.2. 通知(增强)
比喻: 假设你想提升餐厅的服务质量,于是决定给餐厅添加一些“增强功能”。比如:
- 餐前欢迎词:顾客入座后,服务员送上一句温馨的欢迎词。
- 餐后感谢语:顾客结账离开时,收银员表达一声真诚的感谢。
- 限时优惠提醒:在特定时段,主动告知顾客当前正在进行的优惠活动。
这些额外的互动环节就像给餐厅原有的服务流程加上了“增强功能”,让顾客体验更佳。
解释: 在AOP中,通知(增强)就是指那些在目标方法执行前后(或过程中)插入的额外操作,用来实现横切关注点所需的功能。比如:
- 前置通知:在厨师开始炒菜(目标方法)之前,先检查食材新鲜度(附加功能)。
- 返回通知:顾客成功付款(目标方法执行完毕)后,系统自动发送电子发票(附加功能)。
- 异常通知:若结账时发生支付失败(目标方法异常),则触发退款流程(附加功能)。
- 后置通知:无论结账是否成功,都会记录本次交易信息到数据库(附加功能)。
- 环绕通知:整个点餐到结账的过程,如同包裹在try...catch...finally结构中,确保无论是否发生异常,都能执行必要的清理工作(如释放资源、关闭连接等)。
3.2.3. 切面
比喻: 为了让餐厅更好地运营,你专门设立了一个“服务质量提升小组”,这个小组负责策划并实施所有的“增强功能”,如欢迎词、感谢语、优惠提醒等。这个小组就是所有“增强功能”的集合体。
解释: 在AOP中,切面就是一个封装了多个通知(增强)的类,它定义了哪些地方需要执行哪些增强逻辑。切面就像一个“服务质量提升小组”,集中管理与特定横切关注点相关的所有通知。
3.2.4. 目标
比喻: 在餐厅里,厨师专心炒菜、服务员热情待客,他们是各自岗位上的“目标”,他们的主要任务是完成本职工作。
解释: 在AOP中,目标就是指那些被增强的对象或方法,即我们想要在其周围添加通知的原始业务逻辑。比如某个处理订单的类或方法,它们专注于处理订单的核心业务,是AOP要进行增强的“目标”。
3.2.5. 代理
比喻: 如果你聘请了一位餐厅经理,他并不直接炒菜或服务顾客,但他会指导厨师和服务员如何更好地工作,并在必要时介入处理一些特殊情况。这位经理就像是厨师和服务员的“代理”,他们对外展示的是经理协调下的综合服务能力。
解释: 在AOP中,代理就是指为目标对象生成的一个替代者,它在保留原对象功能的基础上,包含了切面中定义的通知逻辑。客户端代码实际上与代理对象交互,代理对象在调用目标方法时会适时执行相应的通知。
3.2.6. 连接点
比喻: 在餐厅运营过程中,任何一个可以插入“增强功能”的时机都可以看作一个“连接点”。比如顾客刚坐下时、点完菜后、结账时、离开时等都是可以插入欢迎词、感谢语、优惠提醒等“增强功能”的“连接点”。
解释: 在AOP中,连接点是一个抽象概念,它表示在程序执行过程中能够插入通知的所有可能位置。具体来说,通常是某个方法的执行前、执行后、抛出异常时、正常返回后等时刻。连接点就像一张餐厅运营流程的时间线图,图上标注了所有可以插入“增强功能”的时间节点。
3.2.7. 切入点
比喻: 如果你决定只在顾客入座时和结账时进行“增强功能”(欢迎词、感谢语),那么这两个特定的“连接点”就是你要瞄准的“切入点”。
解释: 在AOP中,切入点就是用来精确指定应该在哪些连接点上应用通知的表达式或规则。它就像一个过滤器,从众多连接点中选出真正需要插入通知的具体位置。比如,可能只选择所有以“save
”开头的方法作为切入点,对这些方法执行事务管理的增强。切入点就像一份详细的“增强功能”实施计划,指定了在哪些具体场景(方法调用)中启用哪些“增强功能”。
3.3. 作用
- 简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。
- 代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就被切面给增强了。
3.4. 基于注解的AOP
3.4.1. 技术说明
3.4.1.1. AOP(面向切面编程)
想象您经营一家咖啡馆,每天有很多顾客来点咖啡。在制作咖啡的过程中,除了实际的冲泡工作(主业务逻辑),还有一些附加的操作是每次都需要做的,比如:
- 记录订单:每次有顾客点咖啡时,都要在记账本上登记一笔订单。
- 检查库存:确认所需的咖啡豆、糖、牛奶等材料充足。
- 清洗器具:使用过的咖啡杯和器具要及时清洗。
这些附加的操作(记录订单、检查库存、清洗器具)就像是“横切关注点”,它们贯穿于咖啡冲泡流程的各个环节,但并非咖啡制作的核心逻辑。AOP就像是一个聪明的经理,他设计了一个系统,让这些附加操作能够自动完成,而不干扰咖啡师专心制作咖啡。
3.4.1.2. 注解在AOP中的应用
注解(Annotations)就像经理给咖啡师发了一份清单,上面标注了哪些步骤前后需要做什么。例如,经理可能用不同的标签(注解)来标记:
@BeforeMakeCoffee
:表示在开始冲泡前,要先检查库存和清洗器具。@AfterMakeCoffee
:表示咖啡制作完成后,要立即记录订单。
咖啡师只要按照清单上的标注(注解)执行相应操作即可,不用关心具体细节。
在Java中,主要存在两种动态代理实现方式:
JDK动态代理和CGLIB(Code Generation Library)动态代理。
3.4.1.3. JDK动态代理
简单来说,JDK动态代理就像有两个对象(代理对象和目标对象)共同遵守一个“约定(接口),代理对象作为目标对象的“代理人”,在对外提供服务时,会先做一些额外的工作,然后才调用目标对象的实际方法。这里的“约定”就是接口,即目标对象必须实现一个或多个接口。JDK动态代理利用java.lang.reflect.Proxy
类和InvocationHandler
接口来创建代理对象。
具体过程如下:
- 代理对象和目标对象实现同样的接口:这意味着目标类已经声明了它将提供哪些方法(通过接口定义),代理对象也必须具备这些方法,以便在调用时可以替代目标对象。
- JDK动态生成代理类:当你使用JDK动态代理创建代理对象时,JVM会在运行时自动生成一个代理类(通常在
com.sun.proxy
包下,类名如$proxy1
),这个代理类实现了与目标类相同的接口。 - 代理类的方法调用:当你通过代理对象调用一个方法时,实际上调用的是代理类的方法。代理类的方法内部会调用
InvocationHandler
接口的invoke()
方法,该方法接收你的方法调用请求,并在此处添加你想要的额外逻辑(如前置处理、后置处理等)。最后,invoke()
方法再调用目标对象的实际方法来完成原始功能。
总结:JDK动态代理适用于那些目标类已经实现了接口的情况。代理对象通过实现相同的接口并与目标对象形成“兄弟关系”,在对外提供服务时扮演中间人的角色,通过InvocationHandler
接口添加额外功能。
3.4.1.4. CGLIB动态代理
CGLIB动态代理则采取了不同的策略,它不是基于接口而是通过继承被代理的目标类来创建代理对象。这意味着即使目标类没有实现任何接口,只要它是非final
的,CGLIB也可以为其创建代理。
具体过程如下:
- 代理对象继承目标类:CGLIB通过生成一个继承自目标类的子类作为代理对象。这个子类在编译期间或运行时动态生成,包含与目标类相同的方法以及可能添加的额外方法。
- 方法拦截与增强:CGLIB使用字节码技术(如ASM库)修改子类的方法实现,使其在调用时先执行代理逻辑,然后再调用父类(即目标类)的相应方法。这样,当通过代理对象调用方法时,实际上会触发CGLIB预设的拦截器(MethodInterceptor)进行增强处理,随后再执行实际的业务逻辑。
总结:CGLIB动态代理适用于那些目标类没有实现接口或者你不希望依赖接口的情况。代理对象通过继承目标类成为其“子类”,并在方法调用时通过方法拦截器插入额外功能,实现对目标类行为的扩展或修改。
综上所述,JDK动态代理基于接口,通过实现相同接口的代理类来提供代理功能;而CGLIB动态代理则是通过继承目标类并修改其实现,达到无接口情况下的代理目的。两种方式各有适用场景,根据实际需求选择合适的代理技术。在Spring框架中,AOP(面向切面编程)会根据目标类是否实现接口自动选择使用JDK动态代理还是CGLIB动态代理。
3.4.1.5. AspectJ与Spring AOP的关系
AspectJ 是一种实现了 AOP(面向切面编程)思想的框架,尽管它的机制和之前提到的动态代理(如JDK动态代理和CGLIB动态代理)有所不同,但它们都服务于同一个目标:在不修改原始代码的情况下,对程序的某些横切关注点(如日志、事务、安全检查等)进行统一管理和模块化。
- 静态代理:
虽然 AspectJ 实现了AOP,但与我们前面讨论的动态代理不同,它采用的是静态代理的方式。这意味着代理逻辑不是在运行时动态生成,而是在编译阶段就将代理逻辑“织入”到目标类的字节码文件中。这种提前编织(或称为“编织”、“织入”)的方式使得代理行为在程序执行时看起来就像是直接集成在原始类中一样,因此虽然实现过程是静态的,但最终效果呈现出动态特性。
- 织入(Weaving):
织入是 AspectJ 中的关键过程,指的是将切面(Aspect)所定义的横切关注点(如切点表达式、通知方法等)合并到原始类的字节码中。织入有多种时机:
-
- 编译期织入(Compile-time weaving, CTW):使用专门的AspectJ编译器(ajc)编译源代码时,直接将切面逻辑织入到目标类的字节码中,生成已增强的类文件。
- 加载期织入(Load-time weaving, LTW):在类加载到 JVM 时动态地修改类的字节码,这通常需要特殊的类加载器和配置文件支持。
- 编译器插件织入:对于支持插件的编译器(如Eclipse中的AJDT插件),可在编译过程中直接进行织入。
织入器(Weaver)是负责执行上述织入过程的组件,它解析切面定义,识别切点,并按照指定的连接点(Join point)将切面逻辑插入到目标类的方法调用、字段访问等合适的位置。
- 切面(Aspect):
切面是 AspectJ 中的核心概念,它封装了跨越多个类或对象的关注点。一个切面通常包括:
-
- 切点表达式(Pointcut):用于定义在何时何处应用切面逻辑,比如指定哪些方法调用、字段访问等连接点应触发切面的行为。
- 通知(Advice):定义了在切点处要执行的代理逻辑,如前置通知(Before)、后置通知(After Returning)、环绕通知(Around)、异常抛出通知(After Throwing)等。
在 Spring 框架中,虽然它内置了基于动态代理的AOP(JDK、CGLIB)支持,但也可以选择与 AspectJ 集成,利用 AspectJ 的强大切面定义能力和更广泛的织入点支持来编写更复杂的切面。Spring 通过引入 @Aspect
注解和相关的配置,使得开发者可以使用类似Spring AOP的风格编写切面,但底层实际上是借助AspectJ的编译器或织入器来实现切面逻辑的织入。
总结:AspectJ 实现了AOP思想,它采用静态代理的方式,在编译阶段(或加载阶段)将代理逻辑“织入”到目标类的字节码中。这种方式提供了更强大的切面定义能力和更广泛的织入点支持,使得开发者能够在不影响原有业务代码结构的前提下,灵活、集中地管理横切关注点。Spring框架可以与AspectJ集成,借用其注解和强大的切面功能来增强Spring应用的AOP能力。
总结来说,基于注解的AOP就像是给咖啡师发了一份带有标注的操作清单,让他们在制作咖啡的同时自动完成附加任务。
- 动态代理则是通过创建代理对象(手机或智能背心)来指导目标对象(咖啡师)执行附加任务
- JDK动态代理需要目标对象实现接口(咖啡师使用标准化服务流程)
- 而CGLIB动态代理则通过继承目标类来实现(咖啡师没有使用标准化服务流程)。
- AspectJ则更进一步,直接修改目标对象的工作手册(字节码),使其自带附加任务执行能力。
Spring AOP可以根据需要灵活选用这些技术,实现系统的自动化管理。
3.4.2. 准备工作
操作步骤:
第1步:引入aop相关依赖
第2步:创建目标资源,接口、实现类
第3步:创建切面点,切入点、通知类型
- 添加依赖
在IOC所需依赖基础上再加入下面依赖即可:
<dependencies>
<!--spring context依赖-->
<!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.2</version>
</dependency>
<!--spring aop依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.0.2</version>
</dependency>
<!--spring aspects依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.2</version>
</dependency>
<!--junit5测试-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.3.1</version>
</dependency>
<!--log4j2的依赖-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.19.0</version>
</dependency>
</dependencies>
- 准备被代理的目标资源
接口:
package com.sakurapaid.spring6.aop.annoxml;
/**
* 计算器接口,定义了基本的四则运算方法。
*/
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.4.3. 基于JDK动态代理
JDK动态代理适用于那些目标类已经实现了接口的情况。代理对象通过实现相同的接口并与目标对象形成“兄弟关系”,在对外提供服务时扮演中间人的角色,通过InvocationHandler
接口添加额外功能。
创建接口的实现类:
package com.sakurapaid.spring6.aop.annoxml;
import org.springframework.stereotype.Component;
/**
* 实现Calculator接口的计算类,提供基本的四则运算功能。
*/
@Component // 将CalculatorImpl注册为Spring组件,交给Spring容器管理
public class CalculatorImpl implements Calculator{
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("方法内部 result = " + result);
return result;
}
}
创建切面类并配置
@对应注解 (value = "切入点表达式配置切入点")
具体切入点表达式怎么写,后面会讲到
package com.sakurapaid.spring6.aop.annoxml;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Aspect //声明这是一个切面类
@Component //将切面类注册为Spring组件
public class LogAspect {
/**
* 方法执行前的通知。记录方法名和参数。
*
* @param joinPoint 切入点,用于获取方法名和参数等信息。
*/
@Before(value = "execution(public int com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.add(..))")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName(); //获取方法名
String args = Arrays.toString(joinPoint.getArgs()); //获取参数
System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}
}
测试输出
package com.sakurapaid.spring6.aop.annoxml;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* 计算器测试类,用于测试动态代理功能。
*/
public class CaculatorTest {
@Test
public void testAdd(){
ApplicationContext ac = new ClassPathXmlApplicationContext("SpringAop.xml");
Calculator calculator = ac.getBean(Calculator.class);
calculator.add(1,2);
}
}
刚才在切面类只设置一个前置通知,这会全部通知都加上
配置切面类
根据切面表达式,我对所有的方法都进行了通知
package com.sakurapaid.spring6.aop.annoxml;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Aspect //声明这是一个切面类
@Component //将切面类注册为Spring组件
public class LogAspect {
@Before("execution(public int com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}
@After("execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->后置通知,方法名:"+methodName);
}
@AfterReturning(value = "execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result);
}
@AfterThrowing(value = "execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex);
}
@Around("execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
Object result = null;
try {
System.out.println("环绕通知-->目标对象方法执行之前");
//目标对象(连接点)方法的执行
result = joinPoint.proceed();
System.out.println("环绕通知-->目标对象方法返回值之后");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("环绕通知-->目标对象方法出现异常时");
} finally {
System.out.println("环绕通知-->目标对象方法执行完毕");
}
return result;
}
}
再重新测试输出
package com.sakurapaid.spring6.aop.annoxml;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* 计算器测试类,用于测试动态代理功能。
*/
public class CaculatorTest {
@Test
public void testAdd(){
ApplicationContext ac = new ClassPathXmlApplicationContext("SpringAop.xml");
Calculator calculator = ac.getBean(Calculator.class);
System.out.println("1+2");
calculator.add(1,2);
System.out.println("-----------");
System.out.println("1-2");
calculator.sub(1,2);
System.out.println("-----------");
System.out.println("1*2");
calculator.mul(1,2);
System.out.println("-----------");
System.out.println("1/2");
calculator.div(1,2);
}
}
3.4.4. 各种通知
3.4.4.1. 各种通知类型
- 前置通知(
@Before
):这是一个在目标方法被调用前执行的通知。它可以用于设置上下文、验证参数、开启事务等预处理操作。 - 返回通知(
@AfterReturning
):当目标方法成功执行并返回时触发。此通知可以用来执行清理工作、关闭资源、发布事件或基于方法返回值进行进一步处理。 - 异常通知(
@AfterThrowing
):如果目标方法抛出异常,则会执行异常通知。它通常用于记录错误日志、回滚事务、通知监控系统等与异常处理相关的行为。 - 后置通知(
@After
):无论目标方法是否正常完成(无论有无返回值、是否抛出异常),后置通知都会在方法执行完毕后执行。它适用于执行那些无论方法执行结果如何都需要做的清理工作,如释放通用资源、关闭数据库连接等。 - 环绕通知(
@Around
):环绕通知最为灵活,它能够完全控制目标方法的执行流程。它以try...catch...finally
结构包裹目标方法,允许在方法调用前后插入自定义行为,并且可以根据需要决定是否继续执行目标方法、何时执行以及如何处理返回值或抛出的异常。
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。通知方法在AOP(面向切面编程)中扮演着关键角色,它们负责在特定的程序执行点(如方法调用前、后、返回结果或抛出异常时)执行相应的横切逻辑。具体来说,常见的五种通知方法包括:
- 前置通知(@Before) :在被代理的目标方法执行前执行。此时,目标方法尚未开始运行,前置通知可以用于进行权限校验、资源初始化、日志记录等准备工作。例如,记录方法调用的时间戳、操作者信息,或者检查用户是否有执行该操作的权限。
@Before("execution(public int com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))")
public void beforeMethod(JoinPoint joinPoint){
// 记录方法名和参数
}
- 返回通知(@AfterReturning) :在被代理的目标方法成功执行并返回结果后执行。当目标方法正常结束且没有抛出任何异常时,返回通知会被触发。它常用于处理方法执行结果的后续操作,如数据校验、缓存更新、统计分析等。
@AfterReturning(value = "execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result){
// 记录方法名和返回结果
}
- 异常通知(@AfterThrowing) :在被代理的目标方法执行过程中抛出异常后执行。无论异常是否被捕获,只要目标方法抛出了异常,异常通知就会被触发。它可以用于记录异常信息、执行回滚操作、发送错误通知等。
@AfterThrowing(value = "execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
// 记录方法名和异常信息
}
- 后置通知(@After) :在被代理的目标方法最终完成(无论是否抛出异常)后执行。无论方法执行是否成功,也不管有没有返回值,后置通知都会被执行。这类通知通常用于释放资源、清理环境、关闭数据库连接等收尾工作。
@After("execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))")
public void afterMethod(JoinPoint joinPoint){
// 记录方法名
}
- 环绕通知(@Around) :使用try...catch...finally结构围绕整个被代理的目标方法,涵盖了上述四种通知对应的所有位置。环绕通知提供了最大的灵活性,它可以在方法调用前后插入自定义逻辑,并且可以选择是否继续执行目标方法。通常用于实现事务管理、性能监控、日志记录等功能。
@Around("execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint){
// 在方法执行前后插入自定义逻辑,控制方法执行
}
综上所述,这五种通知方法分别对应了目标方法执行生命周期中的不同阶段,通过编写这些通知方法,我们可以将与业务逻辑无关的横切关注点(如日志、安全、事务等)从业务代码中剥离出来,实现代码的解耦和复用,提升系统的可维护性和扩展性。
针对每个通知方法中的传入参数的简要解释:
- 前置通知(@Before) :
参数:JoinPoint joinPoint
-
JoinPoint
: 这是AOP框架提供的一个接口,代表程序执行过程中的一个具体点(如方法调用)。在前置通知中,joinPoint
参数包含了关于当前被拦截方法的各种信息,如方法名、方法签名(包含参数类型和返回类型)、目标对象、方法参数值等。通过这个参数,您可以在通知方法中获取到目标方法的相关上下文,以便进行诸如日志记录、权限校验等操作。例如,您可以使用joinPoint.getSignature().getName()
获取方法名,或者joinPoint.getArgs()
获取方法参数值。
- 返回通知(@AfterReturning) :
参数:JoinPoint joinPoint
,Object result
-
JoinPoint joinPoint
: 同前置通知中的joinPoint
,提供了关于目标方法的详细信息。Object result
: 这个参数代表目标方法成功执行后返回的结果。在返回通知中,您可以访问到这个结果对象,进行后续的数据校验、缓存更新、统计分析等操作。例如,如果目标方法返回一个计算结果,您可以通过result
变量获取并记录这个结果。
- 异常通知(@AfterThrowing) :
参数:JoinPoint joinPoint
,Throwable ex
-
JoinPoint joinPoint
: 同前置通知中的joinPoint
,提供了关于目标方法的详细信息。Throwable ex
: 这个参数代表目标方法执行过程中抛出的异常实例。在异常通知中,您可以访问到这个异常对象,记录异常信息、执行回滚操作、发送错误通知等。例如,您可以使用ex.getMessage()
获取异常消息,或者ex.printStackTrace()
打印异常堆栈跟踪。
- 后置通知(@After) :
参数:JoinPoint joinPoint
-
JoinPoint joinPoint
: 同前置通知中的joinPoint
,提供了关于目标方法的详细信息。在后置通知中,您可以使用这个参数进行资源释放、环境清理等与方法执行结果无关的收尾工作。
- 环绕通知(@Around) :
参数:ProceedingJoinPoint joinPoint
-
ProceedingJoinPoint joinPoint
: 它继承自JoinPoint
接口,除了包含普通JoinPoint
的所有信息外,还提供了额外的方法proceed()
,允许您控制目标方法的执行。在环绕通知中,您可以编写自定义逻辑包围joinPoint.proceed()
调用,实现对目标方法调用前后及过程中的干预。例如,您可以在proceed()
前后添加事务管理代码、性能监控逻辑或日志记录语句。
总结:每个通知方法中的参数主要是为了提供目标方法的相关上下文信息(如方法名、参数、返回值、抛出的异常等)以及控制目标方法执行的能力(如环绕通知中的ProceedingJoinPoint
)。通过这些参数,您可以根据需要编写相应的横切逻辑,将关注点与业务代码分离,提高代码的可维护性和可扩展性。作为初学者,理解这些参数如何帮助您定位和操作目标方法是掌握AOP的关键。
代码示例就是下面这样的:
package com.sakurapaid.spring6.aop.annoxml;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* 日志切面类,用于通过注解方式定义切面逻辑,包括方法执行前、后、返回结果后、抛出异常后的日志记录。
*/
@Aspect //声明这是一个切面类
@Component //将切面类注册为Spring组件
public class LogAspect {
/**
* 方法执行前的通知。记录方法名和参数。
*
* @param joinPoint 切入点,用于获取方法名和参数等信息。
*/
@Before("execution(public int com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}
/**
* 方法执行后的通知。仅记录方法名。
*
* @param joinPoint 切入点,用于获取方法名。
*/
@After("execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->后置通知,方法名:"+methodName);
}
/**
* 方法返回结果后的通知。记录方法名和返回结果。
*
* @param joinPoint 切入点,用于获取方法名。
* @param result 方法的返回结果。
*/
@AfterReturning(value = "execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result);
}
/**
* 方法抛出异常后的通知。记录方法名和异常信息。
*
* @param joinPoint 切入点,用于获取方法名。
* @param ex 方法抛出的异常。
*/
@AfterThrowing(value = "execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex);
}
/**
* 环绕通知。在方法执行前后都可以插入自定义逻辑,还可以控制是否执行方法本身。
*
* @param joinPoint 切入点,用于获取方法名、参数,以及执行方法本身。
* @return 方法的返回结果。
* @throws Throwable 如果执行方法过程中出现异常。
*/
@Around("execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
Object result = null;
try {
System.out.println("环绕通知-->目标对象方法执行之前");
// 执行目标方法
result = joinPoint.proceed();
System.out.println("环绕通知-->目标对象方法返回值之后");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("环绕通知-->目标对象方法出现异常时");
} finally {
System.out.println("环绕通知-->目标对象方法执行完毕");
}
return result;
}
}
在Spring的配置文件中配置:
- 将目标对象和切面交给IOC容器管理(注解+扫描)
- 开启AspectJ的自动代理,为目标对象自动生成代理
- 将切面类通过注解@Aspect标识
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--
基于注解的AOP的实现:
1、将目标对象和切面交给IOC容器管理(注解+扫描)
2、开启AspectJ的自动代理,为目标对象自动生成代理
3、将切面类通过注解@Aspect标识
-->
<!-- 通过扫描指定包下所有组件并加入IOC容器,来实现对注解的自动识别和处理 -->
<context:component-scan base-package="com.sakurapaid.spring6.aop.annoxml">
</context:component-scan>
<!-- 开启AspectJ的自动代理功能,它会为目标对象自动生成代理,以实现切面的织入 -->
<aop:aspectj-autoproxy />
</beans>
3.4.4.2. 通知执行顺序
Spring版本5.3.x以前:
- 前置通知(
@Before
) - 目标操作(即被代理的方法本身)
- 后置通知(
@After
) - 返回通知或异常通知(
@AfterReturning
或@AfterThrowing
),根据目标方法实际执行情况选择执行其中一个
Spring版本5.3.x以后:
- 前置通知(
@Before
) - 目标操作(即被代理的方法本身)
- 返回通知或异常通知(
@AfterReturning
或@AfterThrowing
),根据目标方法实际执行情况选择执行其中一个 - 后置通知(
@After
)
3.4.5. 切入点表达式语法
切入点表达式是用来描述你希望在何处应用AOP(面向切面编程)的特定行为(如日志记录、事务管理等)。它是AOP框架识别和定位要增强的方法的一种规则。以下是对各部分语法元素的简单解释:
- 权限修饰符(可选):
-
- 使用
*
表示任何权限修饰符(如public
、private
、protected
、默认无修饰符)均可。
- 使用
- 返回值类型(必填):
-
- 使用
*
表示返回值类型不受限制,可以是任何类型。 - 如果要指定特定返回值类型,如
int
,则需同时指定权限修饰符。
- 使用
- 包名:
-
*.Hello
表示匹配直接在指定包下的类,如com.Hello
,但不匹配子包内的类,如com.atguigu.Hello
。*..Hello
表示匹配任何包层级下的Hello
类,不论包的深度如何。
- 类名:
-
*
表示类名可以是任意名称。*Service
表示匹配所有类名以Service
结尾的类或接口,如MyService
、UserService
。
- 方法名:
-
*
表示方法名可以是任意名称。*Operation
表示匹配所有方法名以Operation
结尾的方法,如doOperation
、performOperation
。
- 方法参数列表:
-
(..)
表示方法可以接受任意数量、任意类型的参数。(int, ..)
表示方法的第一个参数必须是int
类型,后面可以有任意数量、任意类型的其他参数。
- 注意点:
-
- 基本数据类型与包装类型区分:在切入点表达式中使用
int
与实际方法中使用Integer
是不匹配的。如果你指定了int
作为参数类型,那么只有接收int
参数的方法才会被匹配。 - 返回值类型与权限修饰符:若要指定一个具体的返回值类型,必须同时提供权限修饰符。例如:
execution(public int ..Service.*(.., int))
是正确的,而只写execution(int ..Service.(.., int))
则是错误的。
- 基本数据类型与包装类型区分:在切入点表达式中使用
综合示例:
execution(* com.example.service.*Service.*Operation(..))
这个表达式表示:
- 对于
com.example.service
包及其所有子包下的所有类(类名以Service
结尾), - 找到它们所有方法名以
Operation
结尾的方法, - 不管这些方法的权限修饰符、返回值类型以及具体参数列表(只要参数数量和类型匹配),
- 应用相应的切面逻辑。
总之,切入点表达式通过组合这些元素来精确地定位到需要增强的方法,使你能在指定的代码执行点插入额外的行为,实现业务逻辑与横切关注点的分离。
3.4.6. 重用切入点表达式
在AOP(面向切面编程)中,当我们在编写切面(Aspect)时,可能会发现针对同一类或一组类的方法,我们需要添加多种类型的切面通知(如前置通知、后置通知、返回通知、异常通知等),并且这些通知往往使用的是相同的切入点表达式。为了减少代码冗余,提高可维护性,我们可以采用重用切入点表达式的方法。
比如下面这个类中的切面表达式(我从上面扒下来的)
package com.sakurapaid.spring6.aop.annoxml;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* 日志切面类,用于通过注解方式定义切面逻辑,包括方法执行前、后、返回结果后、抛出异常后的日志记录。
*/
@Aspect
@Component
public class LogAspect {
/**
* 方法执行前的通知。记录方法名和参数。
*
* @param joinPoint 切入点,用于获取方法名和参数等信息。
*/
@Before("execution(public int com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}
/**
* 方法执行后的通知。仅记录方法名。
*
* @param joinPoint 切入点,用于获取方法名。
*/
@After("execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->后置通知,方法名:"+methodName);
}
/**
* 方法返回结果后的通知。记录方法名和返回结果。
*
* @param joinPoint 切入点,用于获取方法名。
* @param result 方法的返回结果。
*/
@AfterReturning(value = "execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result);
}
/**
* 方法抛出异常后的通知。记录方法名和异常信息。
*
* @param joinPoint 切入点,用于获取方法名。
* @param ex 方法抛出的异常。
*/
@AfterThrowing(value = "execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex);
}
/**
* 环绕通知。在方法执行前后都可以插入自定义逻辑,还可以控制是否执行方法本身。
*
* @param joinPoint 切入点,用于获取方法名、参数,以及执行方法本身。
* @return 方法的返回结果。
* @throws Throwable 如果执行方法过程中出现异常。
*/
@Around("execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
Object result = null;
try {
System.out.println("环绕通知-->目标对象方法执行之前");
// 执行目标方法
result = joinPoint.proceed();
System.out.println("环绕通知-->目标对象方法返回值之后");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("环绕通知-->目标对象方法出现异常时");
} finally {
System.out.println("环绕通知-->目标对象方法执行完毕");
}
return result;
}
}
它包含了多个通知方法,分别用于在CalculatorImpl类中各个方法的执行前、后、返回结果后和抛出异常后进行日志记录。注意到这些通知方法的@Before、@After、@AfterReturning、@AfterThrowing和@Around注解中,切入点表达式都是重复的:
为了解决上述重复问题,我们可以按照如下步骤进行重用切入点表达式的重构:
① 声明一个公共的切入点方法:
@Pointcut("execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))")
public void calculatorMethods() {
}
在这个方法上使用@Pointcut注解,将重复的切入点表达式定义为一个方法(这里命名为calculatorMethods),该方法本身不包含任何实现代码。
② 在各个通知方法中引用这个公共切入点:
然后,在各个通知方法的注解中,不再直接写切入点表达式,而是引用刚刚声明的calculatorMethods方法名。例如:
@Before("calculatorMethods()")
public void beforeMethod(JoinPoint joinPoint) {...}
@After("calculatorMethods()")
public void afterMethod(JoinPoint joinPoint) {...}
@AfterReturning(value = "calculatorMethods()", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result) {...}
@AfterThrowing(value = "calculatorMethods()", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {...}
@Around("calculatorMethods()")
public Object aroundMethod(ProceedingJoinPoint joinPoint) {...}
在不同切面的重用切入表达式的使用
上述方式是在相同的切面进行重用切入表达式,在不同切面中使用已声明的切入点表达式,实际上是与在同一切面中使用的原理相同,只是切入点方法所在的类不同。当需要在多个切面类中共享同一个切入点表达式时,可以将这个公共切入点方法放在一个单独的类中,然后在各个切面类中通过全限定名引用这个公共切入点方法。
假设我们有一个名为CommonPointcuts
的类,专门用来存放公共的切入点方法:
package com.sakurapaid.spring6.aop.common;
import org.aspectj.lang.annotation.Pointcut;
public class CommonPointcuts {
@Pointcut("execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))")
public void calculatorMethods() {}
}
现在,如果我们有一个新的切面类AnotherLogAspect
,也需要对CalculatorImpl
类中的方法应用相同的切面逻辑,可以这样引用公共切入点:
package com.sakurapaid.spring6.aop.another;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import com.sakurapaid.spring6.aop.common.CommonPointcuts;
@Aspect
public class AnotherLogAspect {
@Before("com.sakurapaid.spring6.aop.common.CommonPointcuts.calculatorMethods()")
public void anotherBeforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Another Logger-->前置通知,方法名:" + methodName + ",参数:" + args);
}
// 其他通知方法...
}
在这个AnotherLogAspect
类中,@Before
注解中的切入点表达式不再是直接写的,而是引用了CommonPointcuts
类中声明的calculatorMethods
方法。这样,即使在不同的切面类中,我们依然可以通过全限定名引用同一个已声明的切入点,实现了跨切面的切入点表达式重用。
总结来说,要在不同切面中重用切入点表达式,只需将公共切入点方法放在一个独立的类中,然后在各个切面类中通过全限定名引用这个公共切入点方法即可。这样既能保持代码的简洁性和一致性,又便于维护。
3.4.7. 切面的优先级
在面向切面编程(AOP)中,当针对相同的目标方法应用了多个切面时,这些切面可能会按照特定的顺序执行其通知(如前置通知、后置通知等)。这个顺序就是所谓的“切面优先级”。理解切面优先级对于控制切面行为的嵌套顺序至关重要,特别是在涉及多个切面协同工作或有特定执行顺序需求的场景。
切面优先级的基本概念:
- 外面 vs 里面:当多个切面作用于同一个目标方法时,优先级较高的切面被视为“外面”,其通知将在优先级较低的切面通知“里面”之前执行。换句话说,优先级高的切面会先包围目标方法,形成一个外层的“切面包围圈”,而优先级低的切面则在其内部再形成一层包围圈。
- 影响通知执行顺序:优先级决定了各个切面中相同类型通知(如前置通知、后置通知等)的执行顺序。例如,如果有两个切面A和B,A的优先级高于B,那么在目标方法执行前,A的前置通知会先于B的前置通知执行;在目标方法执行后,A的后置通知会后于B的后置通知执行。
控制切面优先级:
Spring AOP提供了@Order
注解来显式指定切面的优先级。这个注解可以放在切面类上,其值是一个整数,数值越小,优先级越高。
- @Order(较小的数):标注了较小数值的切面具有更高的优先级,其通知将在其他切面之前执行。
- @Order(较大的数):标注了较大数值的切面具有较低的优先级,其通知将在其他切面之后执行。
示例说明:
假设我们有两个切面类,LoggingAspect
负责日志记录,TransactionAspect
负责事务管理,它们都需要应用于某个服务方法。由于事务管理通常要求在日志记录之前启动,而在日志记录之后提交/回滚,因此TransactionAspect
的优先级应高于LoggingAspect
。
// 优先级较高,负责事务管理
@Aspect
@Order(1) // 数值较小,优先级高
public class TransactionAspect {
// 事务相关的通知方法...
}
// 优先级较低,负责日志记录
@Aspect
@Order(2) // 数值较大,优先级低
public class LoggingAspect {
// 日志记录相关的通知方法...
}
在上述例子中,TransactionAspect
的优先级设置为1,LoggingAspect
的优先级设置为2。因此,在目标方法执行时,会先执行TransactionAspect
的前置通知(如开启事务),接着执行目标方法,然后执行LoggingAspect
的后置通知(如记录日志),最后执行TransactionAspect
的后置通知(如提交/回滚事务)。这种顺序确保了事务管理逻辑在外层,日志记录逻辑在内层,符合预期的业务需求。
总结:
切面的优先级决定了多个切面作用于相同目标方法时,其通知执行的内外嵌套顺序。通过使用@Order
注解并指定一个整数值,可以轻松控制切面的优先级。数值越小,优先级越高,切面通知越“靠外”执行;数值越大,优先级越低,切面通知越“靠内”执行。合理设置切面优先级有助于确保切面行为按预期顺序执行,满足特定的业务需求或技术约束。
3.5. 基于XML的AOP
3.5.1. 准备工作
参考基于注解的AOP环境
接口:
package com.sakurapaid.spring6.aop.annoxml;
/**
* 计算器接口,定义了基本的四则运算方法。
*/
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);
}
创建接口的实现类:
package com.sakurapaid.spring6.aop.annoxml;
import org.springframework.stereotype.Component;
/**
* 实现Calculator接口的计算类,提供基本的四则运算功能。
*/
@Component // 将CalculatorImpl注册为Spring组件,交给Spring容器管理
public class CalculatorImpl implements Calculator{
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("方法内部 result = " + result);
return result;
}
}
配置切面类
因为是基于XML配置的,所以这里是不写@Aspect注解
并且把每个方法上面的注解都去掉
package com.sakurapaid.spring6.aop.annoxml;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
//@Aspect //声明这是一个切面类
@Component //将切面类注册为Spring组件
public class LogAspect {
// @Before("execution(public int com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}
// @After("execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->后置通知,方法名:"+methodName);
}
// @AfterReturning(value = "execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result);
}
// @AfterThrowing(value = "execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex);
}
// @Around("execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
Object result = null;
try {
System.out.println("环绕通知-->目标对象方法执行之前");
//目标对象(连接点)方法的执行
result = joinPoint.proceed();
System.out.println("环绕通知-->目标对象方法返回值之后");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("环绕通知-->目标对象方法出现异常时");
} finally {
System.out.println("环绕通知-->目标对象方法执行完毕");
}
return result;
}
}
3.5.2. 实现
配置spring文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 扫描指定包下所有组件并加入IOC容器,实现对注解的自动识别和处理 -->
<context:component-scan base-package="com.sakurapaid.spring6.aop.annoxml"/>
<!-- 开启AspectJ的自动代理功能,自动为目标对象生成代理以实现切面的织入 -->
<aop:aspectj-autoproxy/>
<aop:config>
<!-- 配置切面,包括切面类、切点、通知等 -->
<aop:aspect ref="logAspect">
<!-- 定义切点,这里指定了要拦截的方法执行 -->
<aop:pointcut id="pointCut"
expression="execution(* com.sakurapaid.spring6.aop.annoxml.CalculatorImpl.*(..))"/>
<!-- 前置通知,在目标方法执行前执行 -->
<aop:before method="beforeMethod" pointcut-ref="pointCut"/>
<!-- 后置通知,在目标方法执行后执行,无论方法执行是否成功 -->
<aop:after method="afterMethod" pointcut-ref="pointCut"/>
<!-- 返回后通知,在目标方法返回结果后执行 -->
<aop:after-returning method="afterReturningMethod" returning="result" pointcut-ref="pointCut"/>
<!-- 异常抛出后通知,在目标方法抛出异常后执行 -->
<aop:after-throwing method="afterThrowingMethod" throwing="ex" pointcut-ref="pointCut"/>
<!-- 环绕通知,可以在方法执行的任何时刻干预方法的执行 -->
<aop:around method="aroundMethod" pointcut-ref="pointCut"/>
</aop:aspect>
</aop:config>
</beans>
实现的效果是一样的