Spring之AOP
时间:2022-09-16 10:30:00
一、AOP概述
1、什么是AOP
OOP(Object-Oriented Programming),面向对象编程。
AOP(Aspect-Oriented Programming),面向切面编程。
在AOP切面是最重要的概念。能否理解切面的概念,决定了能否掌握AOP技术。
2.生活中的面向切面案例
1)案例一 丰巢快递柜
丰巢智能快递柜起源于2015年,是所有快递公司和电子商务物流的24小时自助开放平台。最初的出现是为快递行业提供最后一公里的解决方案服务。
整个物品的寄送过程大致如下:
【寄件】->【集散】->【跨省】->【分拣】->【快递员】->【送货上门】
【寄件】->【集散】->【跨省】->【分拣】->【快递员】->【送货上门】
【寄件】->【集散】->【跨省】->【分拣】->【快递员】->【送货上门】
【寄件】->【集散】->【跨省】->【分拣】->【快递员】->【送货上门】
【寄件】->【集散】->【跨省】->【分拣】->【快递员】->【送货上门】
【寄件】->【集散】->【跨省】->【分拣】->【快递员】->【送货上门】
在上述过程中,前几步几乎都是程式化的。只有最后一个环节最容易出现问题。例如,社区禁止快递员进入,收货人不在家,收货人无法打电话……而且,这种现象并非个案,但所有的快递公司和街道上的快递员都会面临这些问题。
丰巢很好地解决了这个问题。在社区门口设置智能快递柜,对所有快递公司开放。旧金山快递、圆通、云达、中通等快递均可存放在丰巢,用户可统一到丰巢取快递。
不同的快递公司有不同的业务流程。例如,旧金山快递选择长途运输,而其他公司选择陆路运输。或者,从同一地址发送到同一目的地,旧金山快递可能会转移三次,圆通会转移四次,云达会转移两次……然而,丰巢并不在乎。它所关心的只是所有快递公司都会面临的问题——最后一公里,送货上门。
最后一公里相当于整个快递行业的一个切面。丰巢是针对这一切面存在的问题提出的解决方案。
我们可以理解生活中面向切面的问题,用于解决某一环节不同领域或相似领域的相同问题。
2)银行排号系统
张三到工行办理开户手续:
前往工行-> 排队-> 复印身份证-> 填表-> 柜台办理-> 签字-> 离开银行
李四到农行办理存款:
前往农行-> 排队-> 提供卡和钱-> 柜台存入-> 回执单签字-> 离开银行
王五到建行办理预留手机号变更:
前往建行-> 排队-> 填表-> 柜台办理-> 验证码-> 确认-> 离开银行
无论去哪家银行,办理什么业务,都要排队。在早期的银行中,人们必须站成一个长队,通过队列中的位置来决定业务的顺序。所以这种方式给人们带来了极大的不便。排队是一件很无聊的事情。每个人站久了都会腰酸腿痛。而且排队的时候不能离队。如果有人辛辛苦苦排了一个小时的队,突然内心焦虑,回来后要站到队尾再排队。……
针对这种情况,银行排号系统出现在人们的视线中。每个人都可以先去银行取号纸,然后在银行设立的等候区休息,等待系统广播依次打电话。
在这种情况下,排队就像我们从所有银行业务中抽离出来的一个切面。每家银行的业务流程略有不同,客户到银行需要处理的业务因人而异。但在整个过程中,排队业务是常见的。然后我们可以把它拉出来,作为一个切面单独处理。
3)食堂、快餐、外卖、菜鸟驿站、自动售货机……
A公司员工-> 上班-> 讲课-> 吃饭-> 买饮料-> 回办公室-> 继续上班
B公司员工 -> 上班-> 研究销售机会-> 吃饭 -> 买饮料 -> 回办公室 ->上班
二、程序中的面向切面
程序中也存在许多切面问题,即与完整的业务逻辑执行过程无关。
问题只是出现了多个类似的过程的同一个环节中。
例如,在传统的三层结构中,我们需要Service在执行开始和执行结束时行结束时。
浏览器 >> 登录 >> LoginServlet >> CustomerService >> CustomerDao >> 数据库
浏览器 >> 注册 >> RegisterServlet >> CustomerService >> CustomerDao >> 数据库
浏览器 >> 查询书籍 >> ViewBookServlet >> BookService >> BookDao >> 数据库
想要实现Service植入日志的记录很简单,直接在每一个service方法的开始和结束可以调用日志模块。然而,这种做法会带来许多问题:大量代码重复、系统需求和业务需求耦合等……
三、为什么要用?AOP
1、AOP是OOP思想的补充和完善
传统的OOP(面向对象编程)编程在解决实际问题时仍存在一些不足和困难。
而AOP正是为了解决这些问题。
2、AOP能做什么
在OOP正是由于分散在各地且与业务相关的代码(横切代码)的存在,模块。
AOP则将封装好的对象剖开,找出其中对多个对象产生影响的公共行为,并将其封装为一个可重用的模块,这个模块被命名为“切面”(Aspect)。该截面提取并包装了与业务无关但由业务模块共同调用的逻辑,减少了系统中的重复代码,减少了模块之间的耦合,提高了系统的可维护性。
四、Spring AOP和AOP的关系
注意:AOP只是一种设计理念,不是特定的技术,更不用说Spring专属技术。
针对AOP许多公司开发自己的想法AOP实现框架的方式也不同。
例如:AspectJ、JBoss AOP、Nanning Aspects、Spring AOP……
Spring AOP只是其中之一,也是最好的。
五、代理模式
学习Spring AOP先了解代理模式。Spring中的AOP技术,就是通过动态代理来实现的。
1.生活中的代理案例
-
请明星唱歌、跳舞、拍戏,找经纪人谈价格
-
通过中介租房
-
找律师打官司
-
通过销售点购买火车票和机票
代理思想的原则:
-
目标对象被隐藏
代理模式是隐藏委托对象的控制权,改为访问代理对象。
-
代理和本尊具有相同的功能
刘德华技能:唱歌、跳舞、拍摄
刘德华的经纪人:唱歌、跳舞、拍戏
-
代理只做一些预处理和善后工作,但真正的业务还是要靠原对象来完成。
我们邀请刘德华通过经纪人唱歌,但这首歌仍然由刘德华自己唱。
我们通过中介租房,但房子最终还是要自己住。
我们委托律师帮忙提起诉讼,庭审还是要自己出庭。
如果诉讼输了,你必须自己坐牢,而不是律师帮你坐牢。
2.程序中的代理模式
代理模式是OOAD在不修改原始代码的基础上,设计模式之一可以扩展程序的功能。
例如,一个类需要在现有的基础上扩展一些功能,但直接在自己的类中写代码并不方便,所以你可以找到一个类来做它的代理类。
使代理对象完成需要扩展的功能,而核心功能仍然依赖于原始对象的实现。
代理模式的一般做法是为某一对象提供代理对象,并由代理对象控制引用原对象。
需要扩展功能的类别:委托类——委托他人为自己工作的人——刘德华
- 委托对象也称为[目标对象]target
帮助委托扩展功能的类别:代理-代理他人处理事情的人-刘德华的经纪人
- 代理对象也称为[代理对象]proxy
3.代理模式分类
静态代理:在程序运行前,代理.clas文件就已经存在。
动态代理:在程序运行时,代理类是运用了反射技术或字节码技术动态创建而成的。
静态代理需要程序员手动地为每一个委托类编写对应的代理类代码,仅仅为了方便理解动态代理,不可能在生产中使用。
4、静态代理
业务逻辑层接口:
public interface CustomerService {
// 用户登录方法
public void login(String username, String password);
// 用户注册方法
public void register(String username, String password, String gender, int age);
}
业务逻辑层实现类:
public class CustomerServiceImpl implements CustomerService {
@Override
public void login(String username, String password) {
// 处理登录业务逻辑的代码
System.out.println("处理用户登录的代码...");
}
@Override
public void register(String username, String password, String gender, int age) {
// 处理注册业务逻辑的代码
System.out.println("处理用户注册的代码...");
}
}
日志类:
public class Logger {
public void info(String msg) {
System.out.println("[日志输出]" + msg);
}
}
业务逻辑层代理类:
public class CustomerServiceProxy implements CustomerService {
// 目标对象
private CustomerService target;
// 日志对象
Logger logger = new Logger();
// 目标对象通过构造器传入
public CustomerServiceProxy(CustomerService target) {
this.target = target;
}
@Override
public void login(String username, String password) {
// 日志输出
logger.info("login方法被调用了...");
// 让目标对象执行它真正处理业务逻辑的方法
target.login(username, password);
// 日志输出
logger.info("login方法调用结束了...");
}
@Override
public void register(String username,String password,
String gender,int age){
// 日志输出
logger.info("register方法被调用了...");
// 让目标对象执行它真正处理业务逻辑的方法
target.register(username, password, gender, age);
// 日志输出
logger.info("register方法调用结束了...");
}
}
测试类:
public class Test {
public static void main(String[] args) {
// 创建目标对象
CustomerService service = new CustomerServiceImpl();
// 创建代理对象,注入目标对象
CustomerServiceProxy proxy = new CustomerServiceProxy(service);
// 访问代理对象方法
proxy.login("tom", "123");
proxy.register("tom","123","男",23);
}
}
上述案例为我们展示了静态代理的基本实现过程,具有如下特点:
1)委托类对象(目标对象)被“隐藏”
代理模式的主要方式就是要将目标对象隐藏起来,控制外界对它的直接访问。而改为访问代理对象。
2)委托类和代理类实现相同的接口
目的是让代理类具有和委托类相同的行为。
刘德华:唱歌、跳舞、拍戏…
刘德华经纪人:接唱歌的活儿、接跳舞的活儿、接拍戏的活儿…
3)代理类中包含委托类对象(目标对象)
代理对象只是做预先工作和善后工作,真正的功能还要靠目标对象自己实现。
通过上述案例,我们可以发现,代理模式很大程度上降低了登录代码和日志代码的耦合。
首先,在业务逻辑层中看不到任何日志代码的存在。如果将来日志类发生了改变,对业务逻辑层却没有任何影响。
这样一来,降低了项目后期维护的难度,也易于进行功能扩展。而且不同分工的程序员就更专注地开发自己的组件,提升代码质量。
但是这样的静态代理也存在一定的缺点,程序员不得不为每一个有需求的类编写一个代理类出来。不仅费时费力,也不利于修改,更是使项目变得十分臃肿。
我们可以通过使用动态代理来解决这一问题。
5、动态代理
动态代理的特点是:在编译期间,不需要手动编写代理类。而是在程序运行期间动态生成一个代理类对象。
不仅简化了编程工作,而且提高了软件系统的可扩展性,因为使用它可以生成任意类型的动态代理类。
动态代理有两种实现方式:
-
JDK动态代理(要求掌握)
通过Java反射机制动态创建代理对象。
-
CGLib代理(稍作了解)
通过ASM字节码技术创建代理对象。
1)JDK动态代理案例
日志类、业务逻辑层接口、业务逻辑层实现类和静态代理案例中相同:
class Logger、interface CustomerService、class CustomerServiceImpl
使用JDK动态代理,首先需要提供一个InvocationHandler接口的实现类。
这个接口的实现类,类似我们后面在AOP中要提到的“切面类”概念。
简而言之,我们要对委托类进行扩展的功能,都在这个类中定义。
也就是说,在静态代理案例中,我们在代理类方法中执行的日志输出代码,现在全部都要定义在这个InvocationHandler的实现类中。
InvocationHandler实现类:
public class MyHandler implements InvocationHandler {
// 目标对象
Object target;
// 日志对象
Logger logger = new Logger();
// 构造器(传入目标对象)
public MyHandler(Object target) {
this.target = target;
}
// 参数:
// 1.proxy 将来要生成的代理对象
// 2.method 目标对象中的目标方法
// 3.args 调用代理方法时传入的参数列表
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 目标方法被调用前执行的操作
logger.info(method.getName() + "方法被调用了...");
// 调用目标对象的目标方法,并获取返回值
Object returnValue = method.invoke(target, args);
// 目标方法调用结束之后执行的操作
logger.info(method.getName() + "方法调用结束了...");
// 将返回值返回
return returnValue;
}
}
测试类:
public class Test {
public static void main(String[] args) throws Exception {
// 获取委托类镜像对象
Class cls = Class.forName("com.briup.CustomerServiceImpl");
// 获取委托类的类加载器
ClassLoader loader = cls.getClassLoader();
// 获取委托类实现的所有接口
// 返回值类型是装有所有实现接口镜像对象的Class[]数组
Class[] interfaces = cls.getInterfaces();
// 创建目标对象
CustomerService target = new CustomerServiceImpl();
// 创建InvocationHandler接口的实现类
InvocationHandler h = new MyHandler(target);
// 动态代理的核心方法,返回值为动态生成的代理对象。
// 参数:
// 1.loader:委托类的类加载器
// 2.interfaces:委托类实现的所有接口
// 3.h:invocation实现类对象,指定代理对象要做的事情
CustomerService proxy = (CustomerService) Proxy.newProxyInstance(
loader, interfaces, h);
// 调用代理对象的方法
proxy.login("username", "password");
proxy.register("username", "password","男",24);
}
}
这样的代码看起来十分复杂,类、接口和方法一时难以理解。
但是这样做的好处也是显而易见的:
-
可扩展性强
如果需要扩展业务逻辑功能,那么只需要在service接口添加方法,并在实现类给出具体实现即可。
测试代码则无需改动,执行时新添加的方法也会被代理。而在静态代理中,不仅要修改service接口和实现类,每一个受到影响的proxy类也要手动修改。
-
代码复用率高
在静态代理中,一个代理类只能服务于一个委托类。假如将来项目中有十个类需要进行日志输出,那么委托类也要编写十个。
在动态代理中,我们编写的MyHandler(InvocationHandler接口的实现类),可以为任何一个需要代理的service服务。假设系统中所有的service都需要进行日志输出,那么也只需要编写一个MyHandler即可。
2)CGLib代理
JDK动态代理要求目标类实现接口,才能对其进行代理。
CGLib的好处就是在于对于没有实现接口的类,也可以对其进行动态代理。
CGLib采用了非常底层的ASM字节码技术,其原理是通过字节码技术为目标类创建一个子类对象,并在子类对象中拦截所有父类方法的调用。然后在方法调用前后调用后都可以加入自己想要执行的代码。
在CGLib代理中,我们同样需要将“横切代码”定义在某个类中,这个类是MethodInterceptor接口的子类(类似JDK动态代理中InvocationHandler接口的实现类),我们可以把它称之为“拦截器类”。将来目标方法被调用之前,就会先一步被拦截,调用到拦截器中预先定义好的横切代码。
MethodInterceptor实现类:
public class MyInterceptor implements MethodInterceptor {
// 日志对象
Logger logger = new Logger();
// 参数:
// 1.proxyObj:将来生成的代理对象
// 2.method:代理对象中被调用的方法
// 3.args:调用方法传入的参数列表
// 4.mProxy:该参数可以调用父类中的方法
@Override
public Object intercept(Object proxyObj, Method method,
Object[] args, MethodProxy mProxy) throws Throwable {
// 日志输出
logger.info(method.getName()+"方法被调用了...");
// 子类对象调用父类方法
Object returnValue = mProxy.invokeSuper(proxyObj, args);
// 日志输出
logger.info(method.getName()+"方法调用结束了...");
// 将目标方法调用结果返回
return returnValue;
}
}
测试类:
public class Test {
public static void main(String[] args) {
// 创建拦截器对象
MethodInterceptor ic = new MyInterceptor();
// 创建Enhancer对象,用来生成某个类的子类对象
Enhancer enhancer = new Enhancer();
// 指定父类(要生成哪个类的子类)
enhancer.setSuperclass(CustomerService.class);
// 指定调用哪个拦截器
enhancer.setCallback(ic);
// 生成子类对象
CustomerService proxy = (CustomerService) enhancer.create();
// 访问代理对象
proxy.login("tom", "123");
}
}
【面试题】1、请比较JDK动态代理和cglib?
1)JDK动态代理所需要使用的类和接口全部来自于原生API(java.lang.reflect)
cglib属于第三方技术,需要额外引入依赖(也可以直接从spring框架中使用它)。
2)底层的实现方式不同:
JDK动态代理的思路是使用委托类的镜像、类加载器,通过运行期间内存中的活动,动态生成一个代理对象。该代理对象和目标对象具有相同的行为(通过实现同一个接口实现)。代理对象和目标对象的关系是包含关系(一个类中维护另外一个类的引用),在设计上这种关系称之为弱耦合的聚合或组合关系。
cglib的实现思路是创建一个目标对象的子类对象,通过继承来实现和它用有相同的特性。在设计上这种关系可以看做强耦合的关系。
3)JDK动态代理要求被代理的类必须要实现接口,而cglib不做要求,实现也行不实现也行。
2、Spring中的AOP功能是通过哪种技术实现的?
默认是使用JDK动态代理实现,但是可以通过配置修改,改为使用cglib。
六、Spring中AOP的实现
1、AOP中的基本名词和概念
名词 | 解释 |
---|---|
AOP | 面向切面编程 |
Aspect | 切面/切面类,比如调用日志类、日志模块、权限验证的代码 |
JoinPoint | 连接点,即被拦截的方法。在Spring AOP中连接点就是java中的方法。 |
PointCut | 切入点/切点,一组连接点的集合。 |
Advice | 通知/拦截器,指定切面类代码织入到切入点之前、之后…… 类似动态代理中的InvocationHandler接口的实现类、MethodInterceptor接口的实现类 |
Advisor | 增强器,指定类中的哪些方法需要被拦截 |
Target | 目标对象、委托类对象,被代理的对象,本尊对象 |
Proxy | 代理对象 |
Wave | 织入 |
2、Advice通知类型
Advice通知/拦截器用来指定将切面类中的代码织入到连接点的什么位置。
选择不同的通知类型,可以决定像日志输出这样的操作(切面代码)是发生在执行目标方法之前,还是之后,还是前后都有……具体有下列四种通知类型:
类型 | 描述 |
---|---|
前置通知 (Before advice) |
在某些连接点之前执行的通知 |
返回后通知 (After returning advice) |
在某些连接点正常完成后执行的通知(方法正常结束,没有异常) |
抛出异常后通知 (After throwing advice) |
在某些连接点抛出异常退出时执行的通知 |
环绕通知 (Around Advice) |
包围一个连接点的通知。例如事务的处理,就需要这样的通知。 因为事务需要在方法前开启,在方法后提交,以及方法抛出异常时候回滚。 |
注:在spring中,连接点(join point)指的就是类中的方法
在Spring中,通知是以类的形式体现的。而这个类属于哪一种通知,就要看它具体实现了哪一个接口,对应关系如下:
通知类型 | 实现的接口 |
---|---|
前置通知 | MethodBeforeAdvice |
返回后通知 | AfterReturningAdvice |
抛出异常后通知 | ThrowsAdvice |
环绕通知 | MethodInterceptor |
3、五种通知类型案例
Spring AOP中配置通知有两种方式:
xml配置文件配置通知过程较为繁琐,实际项目中多使用aop:config的方式配置通知。但是xml配置的方式的使用能够帮助理解aop:config标签的原理和大致过程。
1)xml配置文件案例
注意:在如下案例中同时配置了多种通知,每种通知都可以单独配置和使用。
实体类
Account.java
public class Account {
private int id;
private String name;
private double balance;// 余额
// getters&setters...
}
Dao层接口
AccountDao.java
public interface AccountDao {
// 取款 账号减去多少钱
void withdraw(Account account, double amount);
// 存款 账号加上多少钱
void deposit(Account account, double amount);
}
Dao层实现类
AccountDaoImpl.java
public class AccountDaoImpl implements AccountDao {
@Override
public void withdraw(Account account, double amount) {
System.out.println("[AccountDao]" + account.getName() + "取款" + amount + "元。");
}
@Override
public void deposit(Account account, double amount) {
System.out.println("[AccountDao]" + account.getName()+"存款" + amount + "元。");
}
}
Service层接口
AccountService.java
public interface AccountService {
// 模拟某个业务
void bankAction(Account account);
}
Service层实现类
AccountServiceImpl.java
public class AccountServiceImpl implements AccountService {
// Dao层对象
private AccountDao accountDao;
@Override
public void bankAction(Account account) {
// 存取一百元
accountDao.deposit(account, 100);
// 模拟异常抛出,测试异常抛出类通知
// 测试前置、后置、环绕通知时,请将异常抛出代码删除或注释
int a = 0;
if (a==0)
throw new RuntimeException("测试异常!");
accountDao.withdraw(account, 100);
}
public AccountDao getAccountDao() {
return accountDao;
}
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
}
日志类
Logger.java
public class Logger {
// 简单的日志输出方法
public void info(String msg) {
System.out.println("[日志输出]"+msg);
}
}
前置通知
Before.java
public class Before implements MethodBeforeAdvice {
Logger logger; // 日志对象
@Override
public void before(Method method, Object[] args, Object obj) throws Throwable {
// 注意,这里不要手动调用method.invoke()
// Spring会自动调用一次目标方法
// 我们只需要在这里调用目标方法前要做什么事情即可
// 如果在这里调用了method.invoke(),会导致目标方法被调用两次
logger.info("[前置通知]" + method.getName() + "方法开始执行了...");
// 另外,这里不需要返回值,因为执行到前置通知时,
// 目标方法还未被真正调用,所以不存在返回值
}
// 用于依赖注入
public void setLogger(Logger logger) {
this.logger = logger;
}
}
返回后通知
AfterReturn.java
public class AfterReturn implements AfterReturningAdvice {
Logger logger;
@Override
public void afterReturning(Object returnValue, Method method,
Object[] args, Object target) throws Throwable {
// 不要手动调用method.invoke(),原因同上
logger.info("[后置通知]" + method.getName() + "方法执行结束...");
}
public void setLogger(Logger logger) {
this.logger = logger;
}
}
环绕通知
Around.java
public class Around implements MethodInterceptor { Logger logger; @Override public Object invoke(MethodInvocation invocation) throws Throwable { logger.info("[环绕前通知]"