一.AOP是什么
简介:
面向切面编程(Aspect-Oriented Programming)是一种编程范式,它的主要目的是通过预编译和运行期动态代理实现程序功能的横切(cross-cutting)特性,如日志记录、性能统计、事务监控等。它可以帮助开发者将这些原本分散在各个方法或类中的业务逻辑抽象出来,提高代码复用性,降低耦合度
AOP(Aspect-Oriented Programming)是Spring框架的一个重要特性,它通过将横切关注点(cross-cutting concerns)从核心业务逻辑中分离出来,以模块化的方式在整个应用程序中重复使用。以下是关于AOP的简介及其特点:
AOP是一种编程范式,它通过将横切关注点切割出来,将其模块化,并将其应用于多个类和模块,以提高代码的重用性和可维护性。
横切关注点是指与核心业务逻辑无关但存在于多个类或模块中的非功能性需求,例如日志记录、性能监控、事务管理等。
特点:
模块化:AOP允许将横切关注点从核心业务逻辑中提取出来,形成独立的切面(Aspect),使得关注点的逻辑可以独立于各个模块。
解耦:AOP通过解耦横切关注点与核心业务逻辑,使得它们可以独立演化和变化,提高了模块之间的松耦合程度。
重用性:AOP允许将切面应用于多个类和模块,从而实现了关注点的重用,避免了代码的重复编写。
可维护性:将横切关注点抽象为切面后,使得代码结构更清晰,易于理解和维护。
动态性:AOP可以在运行时动态地将切面应用到目标对象上,而不需要修改目标对象的源代码,增强了系统的灵活性和可扩展性。
多样性:Spring框架支持不同类型的切面编程,包括基于代理的AOP和基于字节码增强的AOP。这样可以选择最适合应用程序需求的AOP实现方式。
在Spring框架中,AOP的实现采用了代理模式和动态代理技术。Spring提供了多种AOP的实现方式,包括基于XML配置的AOP、基于注解的AOP和基于纯Java配置的AOP(JavaConfig)等,开发者可以根据具体需求选择适合的方式来配置和使用AOP。
面向切面:
1.专业术语
①目标对象:
专业解释:被通知(被代理)的对象
通俗理解:在书店中,商品就是目标。每个商品都有自己的属性(比如价格、名称、库存等)和行为(比如计算促销价格、更新库存等)。收银员通过扫描商品的条形码来与商品进行交互,调用商品的方法来获取商品信息以及执行一些操作。商品本身即代表了目标
②连接点:
专业解释:程序执行过程中明确的点,如方法的调用,或者异常的抛出
通俗理解:在书店中,我们可以将顾客结账的行为看作一个连接点
③通知:
专业解释:在某个特定的连接点上执行的动作,同时Advice也是程序代码的具体实现,例如一个实现日志记录的代码(通知有些书上也称为处理)
通俗理解:
前置通知(Before Advice):在切入点前执行的代码,在读者购买图书之前,我们可以记录读者购买的图书信息
后置通知(After Advice):在切入点后执行的代码,在读者购买图书之后,我们可以更新图书库存
环绕通知(Around Advice):在切入点前后都执行的代码,我们可以对读者进行额外的安全检查和记录日志
异常通知(After-Throwing Advice):异常通知是在切入点发生异常时执行的额外功能代码。假设当顾客购买商品的数量大于库存数量时,就会发生异常。我们希望在顾客购买商品时检查库存,并在发生异常时执行异常通知,向顾客显示错误信息并处理异常情况
过滤通知(After-Returning Advice):过滤通知是在切入点成功执行后执行的额外功能代码。假设我们有一个特殊会员组,他们在购买商品时可以获得额外的积分。我们可以使用过滤通知来筛选出这些特殊会员,并在成功购买后给他们添加积分
④代理:
专业解释:将通知应用到目标对象后创建的对象(代理=目标+通知)
通俗理解:在书店中,收银员是一个代理角色。他们既代表顾客与商品交互,又代表书店执行一些额外的任务。当顾客带着商品到收银台时,收银员会扫描每个商品的条形码,获取商品信息并计算总价。这里,收银员即充当了顾客与商品之间的代理角色,也充当了超市执行计算总价等额外任务的代理角色
⑤切入点:
专业解释:
多个连接点的集合,定义了通知应该应用到那些连接点 (也将Pointcut理解成一个条件 ,此条件决定了容器在什么情况下将通知和目标组合成代理返回给外部程序)
通俗理解:在书店场景中,我们可能希望在计算折扣方法之前或之后记录日志和进行库存管理。这些切入点决定了我们在代码中操作的位置
⑥适配器:
专业解释:适配器是一个中间组件,用于将面向切面编程框架与原始的业务逻辑代码连接起来(适配器=通知(Advice)+切入点(Pointcut))
通俗理解:在书店场景中,适配器可以将代理对象与书店的购买图书业务逻辑连接起来,使得代理对象能够在购买图书的过程中添加额外的功能
2.代码演示
在上面场景模拟的代码中,我们能够发现记录日志的代码基本相同,那么有没有可能将这部分的代码抽取出来进行封装,统一进行维护呢?同时也可以将日志代码和业务代码完全分离,解耦合
那么我们便可以将业务方法中的非业务核心代码(日志记录)抽离出来形成一个横切面,并且将这个横切面封装成一个对象,将所有的记录日志的代码写到这个对象中,以实现与业务代码的分离,这便是面向切面编程的思想
2.1将记录日志的代码进行封装
三.案例演示
1.前置通知
1.1 先准备接口
package com.lya.aop.biz; public interface IBookBiz { // 购书 public boolean buy(String userName, String bookName, Double price); // 发表书评 public void comment(String userName, String comments); }
1.2然后再准备好实现类
package com.lya.aop.biz.impl; import com.YU.aop.biz.IBookBiz; import com.YU.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); } }
1.3对我们的目标对象进行JavaBean配置
<!--目标对象-->
<bean class="com.lya.aop.biz.impl.BookBizImpl" id="bookBiz"></bean>
1.4 编写前置系统日志通知
package com.lya.aop.advice; import java.lang.reflect.Method; import java.util.Arrays; import org.springframework.aop.MethodBeforeAdvice; /** * 买书、评论前加系统日志 * @author YU * */ 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+")被调用了"); } }
1.5配置系统通知XML中的JavaBean
<!--通知-->
<bean class="com.lya.aop.advice.MyMethodBeforeAdvice" id="myMethodBeforeAdvice"></bean>
1.6 配置代理XML中的JavaBean
<!-- 代理--> <bean class="org.springframework.aop.framework.ProxyFactoryBean" id="bookProxy"> <!-- 配置目标对象 --> <property name="target" ref="bookBiz"></property> <!-- 配置代理接口,目标对象的接口 --> <property name="proxyInterfaces"> <value>com.YU.aop.biz.IBookBiz</value> </property> <property name="interceptorNames"> <list> <value>myMethodBeforeAdvice</value> </list> </property> </bean>
1.7 测试代码开始测试
package com.lya.util; import com.lya.biz.IBookBiz; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * @author 程序猿-小李哥 * @site www.xiaolige.com * @company 猪八戒有限集团 * @create 2023-08-17-15:34 */ public class Demo { public static void main(String[] args) { // 今天所学: // 1.AOP的介绍:专心做事 // 2专业术语 // 1.连接点 // 2.通知:前,后,环绕 // 3.目标 // 4.代理 // 代理=目标+通知 // 3配置xml初始化Spring容器IOC ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml"); // 演示一:目标对象 // BookBizImpl bookBiz = context.getBean("bookTarget",BookBizImpl.class); // bookBiz.buy("晓东","欠你一夜",2000d); // bookBiz.comment("晓东","不看亏了,看了真爽啊!"); // 演示二:前置通知 // 错误:类型强转, Object proxy1 = context.getBean("proxy"); System.out.println(proxy1.getClass()+"代理的类型"); // com.sun.proxy.$Proxy5代理的类型 // 这里proxy==new bookbizimpl // BookBizImpl proxy = context.getBean("proxy",BookBizImpl.class); // proxy.buy("晓东","欠你一夜",2000d); // proxy.comment("晓东","不看亏了,看了真爽啊!"); // 使用接口接收代理对象!!!因为代理对象实现了接口在xml中 IBookBiz proxy = context.getBean("proxy",IBookBiz.class); proxy.buy("晓东","欠你一夜",2000d); proxy.comment("晓东","不看亏了,看了真爽啊!"); } }
注意这里有一个报错问题!!!
因为proxy代理已经实现了接口可以看作为一个实现类
使用接口接收代理对象!!!因为代理对象实现了接口在xml中
测试结果:
由测试结果可得知,不仅获取到了我们的参数,同时根据方法获取到了我们的系统日志,也就是前置通知
2. 后置通知
2.1 先准备好后置通知的系统日志
package com.zking.aop.advice; import java.lang.reflect.Method; import java.util.Arrays; import org.springframework.aop.AfterReturningAdvice; /** * 买书返利 * @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); } }
2.2 配置后置系统通知的XML的JavaBean
<!--后置通知-->
<bean class="com.YU.aop.advice.MyAfterReturningAdvice" id="myAfterReturningAdvice"></bean>
并在前面已经配置好的代理接口中添加一个value值
2.3 测试结果
由测试结果我们可以得知,后置通知永远都在方法执行后才会显示通知,与前置通知不同的是每次前面的方法调用后都会返回一个参数
3.环绕通知
3.1 环绕通知就是前置通知和后置通知的结合,在实际应用开发中,我们一般不会单独编写前置通知和后置通知,单独使用前置通知或者后置通知时,我们会使用环绕通知,将里面前置(后置)通知的功能注释,以达到单独使用的目的
3.2 环绕通知的系统日志
package com.lya.advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import java.util.Arrays; /** * @author 程序猿-小李哥 * @site www.xiaolige.com * @company 猪八戒有限集团 * @create 2023-08-17-18:45 * * 环绕通知 */ public class AroundAdvice implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { //获取目标对象的执行方法 String methodName=invocation.getMethod().getName(); //获取目标对象执行方法的参数 Object[] params=invocation.getArguments(); //获取目标对象 Object target = invocation.getThis(); System.out.println("[环绕通知] "+target.getClass().getName()+"."+methodName+"," + "执行的参数:"+ Arrays.toString(params)); Object returnValue = invocation.proceed(); //放行操作 System.out.println("[环绕通知] 返回参数等于:"+returnValue); return returnValue; } }
3.3 配置环绕通知的XML的JavaBean与前置通知和后置通知一致
3.4 测试结果
由测试结果得知,环绕通知就是前置通知和后置通知的结合,优点就是不需要再多次去进行配置及编码,所以就像我们前面所说在实际开发应用中我们一般都会选择使用环绕通知
4.异常通知
4.1 异常通知的系统日志和其他系统日志不同的是,方法名为固定的afterThrowing,不能修改
package com.lya.advice; /** * @author 程序猿-小李哥 * @site www.xiaolige.com * @company 猪八戒有限集团 * @create 2023-08-17-18:56 */ import org.springframework.aop.ThrowsAdvice; /** * 异常通知 */ public class ExceptionAdvice implements ThrowsAdvice { public void afterThrowing(PriceException e) { System.out.println("[异常通知] 价格异常,撤销订单!"); } }
价格异常
package com.lya.advice; /** * @author 程序猿-小李哥 * @site www.xiaolige.com * @company 猪八戒有限集团 * @create 2023-08-17-19:04 * * 价格异常通知 */ 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); } }
4.2 在我们正常程序出问题没有去配置异常通知时会出现报错,并且不会执行后面的后置通知,如以下情况
4.3 异常处理配置和前面的配置相同
4.4 当我们配置好异常通知模块时,程序出现异常时会上报日志进行提示
5.过滤通知
5.1 直接在XML中配置JavaBean
<!--过滤通知-->
<bean class="org.springframework.aop.support.RegexpMethodPointcutAdvisor" id="regexpMethodPointcutAdvisor">
<property name="advice" ref="myAfterReturningAdvice"></property>
<property name="pattern" value=".*buy"></property>
</bean>
将图中指出部分替换成过滤通知
测试结果:
对比框中内容,在调用过buy方法后进行过滤,第二次调用时不再buy方法而是comment方法
四.总结
aop是面向切面编程,普通程序由上而下正常执行,aop的程序执行是先执行到目标对象的目标方法中,如果连接点上由前置通知,则先执行前置通知再执行目标方法,最后如果目标方法有后置通知则最后执行后置通知代码,不管是前置通知,后置通知,环绕通知,异常通知,过滤通知,代码都是非业务核心代码,如日志、事务的管理(开启、提交、回滚)