1.AOP简介
Spring AOP(Aspect-Oriented Programming)是Spring框架中的一个关键特性,它允许开发者实现跨应用程序模块的横切关注点(cross-cutting concerns)的功能。横切关注点是指那些在应用程序的多个模块中重复出现的功能,如日志记录、性能监测、事务管理等。Spring AOP通过将这些横切关注点与主要应用程序逻辑分离开来,提供了更好的模块化和代码复用性。
在Spring AOP中,开发者可以使用通知(advice)来定义横切关注点的行为。通知有四种类型:
- 前置通知(Before Advice):在目标方法执行之前执行的通知。
- 后置通知(After Returning Advice):在目标方法成功执行后执行的通知。
- 异常通知(After Throwing Advice):在目标方法抛出异常后执行的通知。
- 最终通知(After Advice):无论目标方法的执行结果如何,都会执行的通知。
除了以上四种通知类型,Spring AOP还支持环绕通知(Around Advice),环绕通知可以在目标方法的前后都执行自定义的逻辑。
Spring AOP的核心概念是切面(Aspect)。一个切面由切点(Pointcut)和通知组成。切点定义了在应用程序中哪些类、哪些方法应该被应用切面的通知。通知定义了切面所提供的横切关注点的具体行为。
Spring AOP可以通过两种方式来实现:基于代理的模式和基于字节码增强的模式。基于代理的模式是通过生成代理对象来织入切面逻辑,而基于字节码增强的模式则是直接修改目标类的字节码来织入切面逻辑。
总的来说,Spring AOP提供了一种方便的方式来实现横切关注点的功能,并可以与Spring的IOC容器无缝集成,使得开发者能够更加灵活和高效地开发应用程序。
2.AOP特点
Spring AOP具有以下特点:
- 非侵入性:Spring AOP以编程的方式实现横切关注点,而不需要修改原有的业务逻辑代码。开发者只需在配置文件或注解中定义切面和通知,然后让Spring框架自动完成织入操作。
- 模块化:Spring AOP可以将横切关注点的逻辑抽取到切面中,实现代码的模块化和复用。开发者可以通过定义多个切面来关注不同的横切关注点,而不需要在每个模块中都重复编写相同的代码。
- 松耦合:Spring AOP让开发者将横切关注点的逻辑与核心业务逻辑分开,实现了关注点与业务逻辑的解耦。这样一来,当横切关注点需要修改或扩展时,不会影响到核心业务代码的修改。
- 支持多种切点表达式:Spring AOP提供了丰富的切点表达式,开发者可以根据需要定义不同的切点,灵活地选择要织入切面的方法或类。
- 支持多种通知类型:Spring AOP支持多种类型的通知,如前置通知、后置通知、异常通知和最终通知。开发者可以根据不同的需求选择合适的通知类型来实现横切关注点的行为。
- 可扩展性:Spring AOP是一个可扩展的框架,可以与其他的AOP框架集成使用,如AspectJ。开发者可以利用AspectJ提供的更强大的切点表达式和通知类型来扩展Spring AOP的功能。
总的来说,Spring AOP通过提供非侵入性、模块化、松耦合的方式实现横切关注点的功能,为开发者提供了一种简便、灵活和可扩展的AOP解决方案。
2.1思考
AOP能解决什么问题?
参考于之前我们写的代码,假设我们需要做一个网上书城项目,那我们就必备一个书籍管理模块(BookAction,BookService),如下:
模拟场景:
场景一:客户使用系统,上架了一本zf明令禁止售卖的书籍
场景二:对于平台方而言,客户已经下单并付款,工作人员出于私心,从中牟利。
伪代码如下:
BookAction
BookService
//正常增删改查上架下架功能
add(book book){
bookdao.add
}
del(book book){
bookdao.del
}
edit(book book){
bookdao.edit
}
list(book book){
bookdao.list
}
up(book book){
//每个系统都会添加日志功能,也就是在每个业务模块操作板块,添加日志记录。
//什么时间点谁调用了什么类的什么方法传了什么参数以及最终方法的返回值是什么
datetime current = ...
String username = req.getsession.getattribute("user").getUserid;
String classNameMethodName = this.getClass().getMethod...
String params = arrays.toString(...);
String methodreturn = ...
bookdao.up
logDao.add(current,username,classNameMethodName,params,methodreturn);//日志
}
down(book book){
bookdao.down
logDao.add(current,username,classNameMethodName,params,methodreturn);//日志
}
那么上述代码有何不足之处呢?
如上所说,每个增删改上架下架功能都添加非业务 核心功能的日志功能代码,会重复冗余。
所以回到提问,AOP能解决什么问题?如下:
- 横切关注点的代码重复:在应用程序中,某些功能(如日志记录、异常处理、安全检查等)可能会在多个模块中重复出现。使用AOP可以将这些功能的实现抽取到切面中,避免代码的重复编写,提高代码的可维护性。
- 核心业务逻辑与非核心逻辑的耦合:在传统的编程模式中,核心业务逻辑与非核心逻辑(如事务管理、性能监测)往往混杂在一起,使得代码难以理解和修改。通过使用AOP,可以将非核心逻辑抽离出来,与核心业务逻辑解耦,使得代码更加清晰和易于维护。
- 跨层级的功能扩展:有时候需要在应用程序的多个层级(如控制层、服务层、持久层)中添加某些功能,例如权限验证、缓存管理等。使用AOP可以通过切面的方式统一织入这些功能,而不需要在每个层级中重复添加代码,提高代码的复用性和可扩展性。
- 业务逻辑的优化和统计:通过AOP可以方便地在应用程序中插入性能监测、统计分析等代码,实现对业务逻辑的优化和监控。例如,可以使用AOP在方法执行前记录开始时间,在方法执行后记录结束时间,计算方法的执行时间等。
- 异常处理和错误处理:AOP可以帮助处理应用程序中的异常情况。通过定义异常通知,可以在方法抛出异常时执行相应的处理逻辑,如记录日志、发送通知等。这样可以使得异常处理的逻辑与核心业务逻辑分离开来,提高代码的可读性和可维护性。
总的来说,AOP可以帮助提高代码的模块化和复用性,解耦核心业务逻辑与非核心逻辑,实现功能的统一扩展和优化,以及完成异常处理和错误处理等任务。它提供了一种灵活的编程方式,使得开发者能够更加高效地开发和维护应用程序。
3.Spring的AOP专用术语
- 连接点(Joinpoint):程序执行过程中明确的点,如方法的调用,或者异常的抛出.
- 目标(Target):被通知(被代理)的对象。注1:完成具体的业务逻辑
- 通知(Advice):在某个特定的连接点上执行的动作,同时Advice也是程序代码的具体实现,例如一个实现日志记录的代码(通知有些书上也称为处理) 注2:完成切面编程
- 代理(Proxy):将通知应用到目标对象后创建的对象(代理=目标+通知),例子:外科医生+护士注3:只有代理对象才有AOP功能,而AOP的代码是写在通知的方法里面的。
3.1目标 连接点 通知 代理
我们依照第二点的伪代码来进行罗列详细讲解,如下:
面向切面编程与原有代码运行模式的区别:
之前我们实现功能都是代码从上至下执行实现,但是现在加入了切面,也就是从上至下执行代码,执行到切面时停止了,先要找到通知,找到通知之后,再走接下来的代码。完整的功能是指,既执行了目标代码,也执行了通知,才是一个完整的功能(代理)。
往简单了讲,假设目标是一位明星 ,通知是经纪人,假设明星要拍一部电影,经纪人需要去接这部电影,但是经纪人不负责拍戏,真正做事的人是明星(目标)。但是完整的流程走下来,缺一不可。真正做事的是目标对象,缺了通知整个功能也不完整。就像网上书城系统,上架功能做完了,并不完整。应客户要求可能还需要一个监管功能。
完整的业务链:
AOP面向切面编程,改变了我们原有的代码运行模式,原来是从上至下一路往下走,而面向切面编程时原有的代码走到连接点这个位置时,如果还有前置通知,先执行前置通知再执行目标对象。如果没有前置通知,就会执行目标对象,再执行后置通知,如有后置通知就会执行后置通知。
3.2切入点与适配器
- 切入点(Pointcut):多个连接点的集合,定义了通知应该应用到那些连接点。(也将Pointcut理解成一个条件 ,此条件决定了容器在什么情况下将通知和目标组合成代理返回给外部程序)
- 适配器(Advisor):适配器=通知(Advice)+切入点(Pointcut)
切入点是连接点的集合,比如我既要给书籍管理的新增功能要加监管日志功能,又要给订单管理添加日志功能。要给两个连接点加,那么我们应该写一个类似于以下格式的正则表达式:
com.kissship..controller.*.*Add(..))'
去匹配上那两个连接点。即:
切入点是连接点的集合。
适配器就是通知+切入点。找到切入点是为了干嘛,不就是为了添加日志的功能(以上面实例来说),日志即通知。
4.Spring的前置通知
在讲解前,我们需要做一些准备工作,目录结构如下:
IBookBiz:
package com.kissship.aop.biz; public interface IBookBiz { // 购书 public boolean buy(String userName, String bookName, Double price); // 发表书评 public void comment(String userName, String comments); }
BookBizImpl:
package com.kissship.aop.biz.impl; import com.kissship.aop.biz.IBookBiz; import com.kissship.aop.exception.PriceException; public class BookBizImpl implements IBookBiz { public BookBizImpl() { super(); } public boolean buy(String userName, String bookName, Double price) { // 通过控制台的输出方式模拟购书 if (null == price || price <= 0) { throw new PriceException("book price exception"); } System.out.println(userName + " buy " + bookName + ", spend " + price); return true; } public void comment(String userName, String comments) { // 通过控制台的输出方式模拟发表书评 System.out.println(userName + " say:" + comments); } }
异常类PriceException:
package com.kissship.aop.exception; public class PriceException extends RuntimeException { public PriceException() { super(); } public PriceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { // super(message, cause, enableSuppression, writableStackTrace); } public PriceException(String message, Throwable cause) { super(message, cause); } public PriceException(String message) { super(message); } public PriceException(Throwable cause) { super(cause); } }
配置spring-context.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" default-autowire="byType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- ioc的javabean--> <!-- 凡是在Spring配置文件spring-context.xml中配置,那么该类javabean就交给了Spring容器管理--> <bean class="com.kissship.ioc.web.UserAction" id="userAction"> <property name="userService" ref="userService"></property> <!-- <constructor-arg name="uname" value="扎克" ></constructor-arg>--> <!-- <constructor-arg name="age" value="18" ></constructor-arg>--> <!-- <constructor-arg name="hobby">--> <!-- <list>--> <!-- <value>唱,跳</value>--> <!-- <value>Rap</value>--> <!-- <value>篮球</value>--> <!-- </list>--> <!-- </constructor-arg>--> </bean> <bean class="com.kissship.ioc.web.GoodsAction" id="goodsAction"> <property name="userService" ref="userServiceImpl1"></property> <!-- <property name="gname" value="小文"></property>--> <!-- <property name="age" value="19"></property>--> <!-- <property name="peoples">--> <!-- <list>--> <!-- <value>印度飞饼</value>--> <!-- <value>意大利炮</value>--> <!-- <value>北京烤鸭</value>--> <!-- <value>墨西哥卷</value>--> <!-- </list>--> <!-- </property>--> </bean> <bean class="com.kissship.ioc.service.impl.UserServiceImpl2" id="userService"></bean> <bean class="com.kissship.ioc.service.impl.UserServiceImpl1" id="userServiceImpl1"></bean> <!-- aop相关的Javabean--> <!--目标对象--> <bean class="com.kissship.aop.biz.impl.BookBizImpl" id="bookBiz"></bean> </beans>
最后建一个测试类Demo1进行测试:
package com.kissship.aop.demo; import com.kissship.aop.biz.IBookBiz; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * @author Kissship * @site www.Kissship.com * @company xxx公司 * @create 2023-08-17-17:08 */ public class Demo1 { public static void main(String[] args) { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-context.xml"); IBookBiz bookBiz = (IBookBiz) context.getBean("bookBiz"); bookBiz.buy("GG Bone","美丽的小姐",9.9d); bookBiz.comment("GG Bone","我叫Bone,GG Bone!"); } }
测试结果如下:
这是没有前置通知的演示效果,我们添加前置通知试试:
MyMethodBeforeAdvice:
package com.kissship.aop.advice; import org.springframework.aop.MethodBeforeAdvice; import java.lang.reflect.Method; import java.util.Arrays; /** * 买书、评论前加系统日志 * @author Administrator * */ public class MyMethodBeforeAdvice implements MethodBeforeAdvice { @Override public void before(Method arg0, Object[] arg1, Object arg2) throws Throwable { // 在这里,可以获取到目标类的全路径及方法及方法参数,然后就可以将他们写到日志表里去 String target = arg2.getClass().getName(); String methodName = arg0.getName(); String args = Arrays.toString(arg1); System.out.println("【前置通知:系统日志】:"+target+"."+methodName+"("+args+")被调用了"); } }
然后配置spring-context.xml,如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" default-autowire="byType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- ioc的javabean--> <!-- 凡是在Spring配置文件spring-context.xml中配置,那么该类javabean就交给了Spring容器管理--> <bean class="com.kissship.ioc.web.UserAction" id="userAction"> <property name="userService" ref="userService"></property> <!-- <constructor-arg name="uname" value="扎克" ></constructor-arg>--> <!-- <constructor-arg name="age" value="18" ></constructor-arg>--> <!-- <constructor-arg name="hobby">--> <!-- <list>--> <!-- <value>唱,跳</value>--> <!-- <value>Rap</value>--> <!-- <value>篮球</value>--> <!-- </list>--> <!-- </constructor-arg>--> </bean> <bean class="com.kissship.ioc.web.GoodsAction" id="goodsAction"> <property name="userService" ref="userServiceImpl1"></property> <!-- <property name="gname" value="小文"></property>--> <!-- <property name="age" value="19"></property>--> <!-- <property name="peoples">--> <!-- <list>--> <!-- <value>印度飞饼</value>--> <!-- <value>意大利炮</value>--> <!-- <value>北京烤鸭</value>--> <!-- <value>墨西哥卷</value>--> <!-- </list>--> <!-- </property>--> </bean> <bean class="com.kissship.ioc.service.impl.UserServiceImpl2" id="userService"></bean> <bean class="com.kissship.ioc.service.impl.UserServiceImpl1" id="userServiceImpl1"></bean> <!-- aop相关的Javabean--> <!--目标对象--> <bean class="com.kissship.aop.biz.impl.BookBizImpl" id="bookBiz"></bean> <!--通知--> <bean class="com.kissship.aop.advice.MyMethodBeforeAdvice" id="methodBeforeAdvice"></bean> <!--代理--> <bean class="org.springframework.aop.framework.ProxyFactoryBean" id="bookProxy"> <!-- 配置目标对象--> <property name="target" ref="bookBiz"></property> <!-- 配置代理接口,目标对象的接口--> <property name="proxyInterfaces"> <list> <value>com.kissship.aop.biz.IBookBiz</value> </list> </property> <!-- 配置通知--> <property name="interceptorNames"> <list> <value>methodBeforeAdvice</value> </list> </property> </bean> </beans>
修改Demo1测试类进行测试:
package com.kissship.aop.demo; import com.kissship.aop.biz.IBookBiz; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * @author Kissship * @site www.Kissship.com * @company xxx公司 * @create 2023-08-17-17:08 */ public class Demo1 { public static void main(String[] args) { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-context.xml"); // IBookBiz bookBiz = (IBookBiz) context.getBean("bookBiz"); IBookBiz bookBiz = (IBookBiz) context.getBean("bookProxy"); System.out.println(bookBiz); bookBiz.buy("GG Bone","美丽的小姐",9.9d); bookBiz.comment("GG Bone","我叫Bone,GG Bone!"); } }
演示结果如下:
5.Spring的后置通知
新建一个MyAfterReturningAdvice通知类,如下:
package com.kissship.aop.advice; import org.springframework.aop.AfterReturningAdvice; import java.lang.reflect.Method; import java.util.Arrays; /** * 买书返利 * @author Administrator * */ public class MyAfterReturningAdvice implements AfterReturningAdvice { @Override public void afterReturning(Object arg0, Method arg1, Object[] arg2, Object arg3) throws Throwable { String target = arg3.getClass().getName(); String methodName = arg1.getName(); String args = Arrays.toString(arg2); System.out.println("【后置通知:买书返利】:"+target+"."+methodName+"("+args+")被调用了,"+"该方法被调用后的返回值为:"+arg0); } }
然后进行spring-context.xml:
往原本的XML代码中,通知注释下加入以下代码:
<bean class="com.kissship.aop.advice.MyAfterReturningAdvice" id="myAfterReturningAdvice"></bean>
然后配置通知,加入:
<value>myAfterReturningAdvice</value>
完整xml配置文件如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" default-autowire="byType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- ioc的javabean--> <!-- 凡是在Spring配置文件spring-context.xml中配置,那么该类javabean就交给了Spring容器管理--> <bean class="com.kissship.ioc.web.UserAction" id="userAction"> <property name="userService" ref="userService"></property> <!-- <constructor-arg name="uname" value="扎克" ></constructor-arg>--> <!-- <constructor-arg name="age" value="18" ></constructor-arg>--> <!-- <constructor-arg name="hobby">--> <!-- <list>--> <!-- <value>唱,跳</value>--> <!-- <value>Rap</value>--> <!-- <value>篮球</value>--> <!-- </list>--> <!-- </constructor-arg>--> </bean> <bean class="com.kissship.ioc.web.GoodsAction" id="goodsAction"> <property name="userService" ref="userServiceImpl1"></property> <!-- <property name="gname" value="小文"></property>--> <!-- <property name="age" value="19"></property>--> <!-- <property name="peoples">--> <!-- <list>--> <!-- <value>印度飞饼</value>--> <!-- <value>意大利炮</value>--> <!-- <value>北京烤鸭</value>--> <!-- <value>墨西哥卷</value>--> <!-- </list>--> <!-- </property>--> </bean> <bean class="com.kissship.ioc.service.impl.UserServiceImpl2" id="userService"></bean> <bean class="com.kissship.ioc.service.impl.UserServiceImpl1" id="userServiceImpl1"></bean> <!-- aop相关的Javabean--> <!--目标对象--> <bean class="com.kissship.aop.biz.impl.BookBizImpl" id="bookBiz"></bean> <!--通知--> <bean class="com.kissship.aop.advice.MyMethodBeforeAdvice" id="methodBeforeAdvice"></bean> <bean class="com.kissship.aop.advice.MyAfterReturningAdvice" id="myAfterReturningAdvice"></bean> <!--代理--> <bean class="org.springframework.aop.framework.ProxyFactoryBean" id="bookProxy"> <!-- 配置目标对象--> <property name="target" ref="bookBiz"></property> <!-- 配置代理接口,目标对象的接口--> <property name="proxyInterfaces"> <list> <value>com.kissship.aop.biz.IBookBiz</value> </list> </property> <!-- 配置通知--> <property name="interceptorNames"> <list> <value>methodBeforeAdvice</value> <value>myAfterReturningAdvice</value> </list> </property> </bean> </beans>
然后我们接着再来测试一下,测试结果如下:
6.Spring的环绕通知
新建一个MyMethodInterceptor,如下:
package com.kissship.aop.advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import java.util.Arrays; /** * 环绕通知 * 包含了前置和后置通知 * * @author Administrator * */ public class MyMethodInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation arg0) throws Throwable { String target = arg0.getThis().getClass().getName(); String methodName = arg0.getMethod().getName(); String args = Arrays.toString(arg0.getArguments()); System.out.println("【环绕通知调用前:】:"+target+"."+methodName+"("+args+")被调用了"); // arg0.proceed()就是目标对象的方法 Object proceed = arg0.proceed(); System.out.println("【环绕通知调用后:】:该方法被调用后的返回值为:"+proceed); return proceed; } }
然后跟前后置一样配置xml:
加入以下xml到固定位置,如下:
<!--通知--> <bean class="com.kissship.aop.advice.MyMethodInterceptor" id="myMethodInterceptor"></bean> <!-- 配置通知--> <value>myMethodInterceptor</value>
然后回到Demo1进行测试,结果如下:
7.Spring的异常通知
把书的价格改为负数,控制台报错,在没加异常通知前,效果如下:
加入异常通知,新建一个MyThrowsAdvice通知类,如下:
package com.kissship.aop.advice; import org.springframework.aop.ThrowsAdvice; import com.kissship.aop.exception.PriceException; /** * 出现异常执行系统提示,然后进行处理。价格异常为例 * @author Administrator * */ public class MyThrowsAdvice implements ThrowsAdvice { public void afterThrowing(PriceException ex) { System.out.println("【异常通知】:当价格发生异常,那么执行此处代码块!!!"); } }
配置spring-context,xml:
<!--通知--> <bean class="com.kissship.aop.advice.MyThrowsAdvice" id="myThrowsAdvice"></bean> <!-- 配置通知--> <value>myThrowsAdvice</value>
添加异常通知后的效果如下:
8.Spring的过滤通知
把价格改回正常之后,我们执行会发现一个问题,如下:
我们通过过滤通知试着解决一下,如下(注:需把原有的后置通知注释):
<!--通知--> <bean class="org.springframework.aop.support.RegexpMethodPointcutAdvisor" id="regexpMethodPointcutAdvisor"> <property name="advice" ref="myAfterReturningAdvice"></property> <property name="pattern" value=".*buy"></property> </bean> <!-- 配置通知--> <property name="interceptorNames"> <list> <value>methodBeforeAdvice</value> <!-- <value>myAfterReturningAdvice</value>--> <value>regexpMethodPointcutAdvisor</value> <value>myMethodInterceptor</value> <value>myThrowsAdvice</value> </list> </property>
测试结果如下:
可以看到在评论之后并没有买书返利的后置通知了。
最后Spring之AOP篇就到这里,祝大家在敲代码的路上一路通畅!