Spring AOP
[TOC]
Spring AOP 介绍
什么是 AOP ?
AOP(Aspect Oriented Programming)意为:
面向切面编程,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
AOP 编程思想:
- 问题一:业务方法日后会很多,会有很多重复的代码。
- 问题二:已经存在很多的方法,并没有考虑到事务的问题,现在要求加上。
AOP 作用
AOP 是 OOP(面向对象编程)的延续,是软件开发中的一个热点,也是 Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。
利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
- AOP 采取横向抽取机制,取代了传统纵向继承体系的重复性代码。
- 经典应用:事务管理、性能监视、安全检查、缓存、日志等。
小结:
- 作用:在程序运行期间,在不修改源码的情况下对方法进行功能增强。
- 优势:减少重复代码,提高开发效率,且便于维护。
AOP 底层实现
AOP 底层采用代理机制进行实现。
静态代理
Proxy Pattern(代理模式),是 23 种常用的面向对象软件的设计模式之一。
代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
优点:
-
职责清晰。
真实的角色就是实现实际的业务逻辑,不用关心其他非本职责的事务,通过后期的代理完成一件完成事务,附带的结果就是编程简洁清晰。
- 代理对象可以在客户端和目标对象之间起到中介的作用,这样起到了的作用和保护了目标对象的作用。
- 高扩展性。
结构:一个是真正的你要访问的对象(目标类),另一个是代理对象,真正对象与代理对象实现同一个接口,先访问代理类再访问真正要访问的对象。
静态代理
装饰者模式(Decorator Pattern):在不惊动原始设计的基础上,为其添加功能
public class UserServiceDecorator implements UserService{ private UserService userService; public UserServiceDecorator(UserService userService) { this.userService = userService; } public void save() { //原始调用 userService.save(); //增强功能(后置) System.out.println("刮大白"); } }
动态代理
- 动态代理它可以直接给某一个目标对象生成一个代理对象,而不需要代理类存在。
- 动态代理与代理模式原理是一样的,只是它没有具体的代理类,直接通过反射生成了一个代理对象。
JDK 动态代理
JDK 动态代理是针对对象做代理,要求原始对象具有接口实现,并对接口方法进行增强。
public class UserServiceJDKProxy { public UserService createUserServiceJDKProxy(final UserService userService){ // 获取被代理对象的类加载器 ClassLoader classLoader = userService.getClass().getClassLoader(); // 获取被代理对象实现的接口 Class[] classes = userService.getClass().getInterfaces(); // 对原始方法执行进行拦截并增强 InvocationHandler ih = new InvocationHandler() { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 前置增强内容 Object ret = method.invoke(userService, args); // 后置增强内容 System.out.println("刮大白2"); return ret; } }; // 使用原始被代理对象创建新的代理对象 UserService proxy = (UserService) Proxy.newProxyInstance(classLoader,classes,ih); return proxy; } }
CGLIB 动态代理
CGLIB(Code Generation Library):Code 生成类库
CGLIB 动态代理不限定是否具有接口,可以对任意操作进行增强。
CGLIB 动态代理无需要原始被代理对象,动态创建出新的代理对象。
public class UserServiceImplCglibProxy { public static UserServiceImpl createUserServiceCglibProxy(Class clazz){ // 创建Enhancer对象(可以理解为内存中动态创建了一个类的字节码) Enhancer enhancer = new Enhancer(); // 设置Enhancer对象的父类是指定类型UserServerImpl enhancer.setSuperclass(clazz); Callback cb = new MethodInterceptor() { public Object intercept(Object o, Method m, Object[] a, MethodProxy mp) throws Throwable { Object ret = mp.invokeSuper(o, a); if(m.getName().equals("save")) { System.out.println("刮大白"); } return ret; } }; // 设置回调方法 enhancer.setCallback(cb); // 使用Enhancer对象创建对应的对象 return (UserServiceImpl)enhancer.create(); } }
Spring 代理模式选择
- Spring AOP 使用纯 Java 实现,不需要专门的编译过程和类加载器,在运行期通过代理方式向目标类织入增强代码。
- AspectJ 是一个基于 Java 语言的 AOP 框架。从 Spring2.0 开始,Spring AOP 引入对 Aspect 的支持,AspectJ 扩展了 Java 语言,提供了一个专门的编译器,在编译时提供横向代码的织入。
Spirng 可以通过配置的形式控制使用的代理形式,默认使用 JDKProxy,通过配置可以修改为使用 cglib 。
- XML 配置:
<!--XMP配置AOP--> <aop:config proxy-target-class="false"></aop:config>
- XML 注解支持:
<!--注解配置AOP--> <aop:aspectj-autoproxy proxy-target-class="false"/>
- 注解驱动:
//注解驱动 @EnableAspectJAutoProxy(proxyTargetClass = true)
织入时机
AOP 术语
target
:目标类,即需要被代理的类。例如:UserServiceproxy
:一个类被 AOP 织入增强后,就成为一个代理类。Joinpoint(连接点)
:所谓连接点是指那些可能被拦截到的方法。例如:所有的方法PointCut(切入点)
:要被增强的连接点。例如:addUser()advice(通知/增强)
:即增强的代码内容。例如:after()、before()Weaving(织入)
:是指把增强(advice)应用到目标对象(target)来创建新的代理对象(proxy)的过程。Aspect(切面)
:是切入点(pointcut)和通知(advice)的结合。-
一个线是一个特殊的面。
- 一个切入点和一个通知,组成一个特殊的面。
AOP 开发明确事项
需要编写的内容:
- 编写核心业务代码(目标类的目标方法)。
- 编写切面类,切面类中有通知(增强功能方法)。
- 在配置文件中,配置织入关系,即将哪些通知与哪些连接点进行结合。
AOP 技术实现的内容:
- Spring 框架监控切入点方法的执行:一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行
AOP 底层使用哪种代理方式:
- 在 Spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。
小结
AOP:面向切面编程
AOP 底层实现:
- 基于 JDK 的动态代理
- 基于 Cglib 的动态代理
AOP 重点概念:
- Pointcut(切入点):被增强的方法
- Advice(通知/增强)封装增强业务逻辑的方法 Aspect(切面):切点+通知
- Weaving(织入):将切点与通知结合的过程
开发明确事项:
- 谁是切点(切点表达式配置)
- 谁是通知(切面类中的增强方法)
- 将切点和通知进行织入配置
AOP 配置开发
AOP 配置开发步骤:
- 导入 AOP 相关坐标;
- 创建目标接口和目标类(内部有切点);
- 创建切面类(内部有增强方法);
- 将目标类和切面类的对象创建权交给 Spring ;
- 在 applicationContext.xml 中配置织入关系;
- 测试代码。
切点表达式
切点表达式的配置语法:
execution([修饰符] 返回值类型 包名.类名.方法名(参数))
- 访问修饰符可以省略
- 返回值类型、包名、类名、方法名可以使用星号 * 表示任意。
- 包名与类名之间一个点代表当前包下的类,两个点..表示当前包及其子包下的类。
- 参数列表可以使用两个点表示任意个数,任意类型的参数列表。
示例:
execution(public void com.aop.Target.method()) execution(void com.aop.Target.*(..)) exeaution(* com.aop.*.*(..)) // 常用 exeaution(* com.aop..*.*(..)) execution(* *..*.*(..))
通知类型
通知的配置语法:
<aop:通知类型 method="切面类中的方法名" pointcut="切点表达式"></aop:通知类型>
名称 | 标签 | 说明 |
---|---|---|
前置通知 | https://www.cnblogs.com/juno3550/p/aop:before | 用于配置前置通知。指定增强的方法在切入点方法之前执行 |
后置通知 | https://www.cnblogs.com/juno3550/p/https://www.cnblogs.com/juno3550/p/aop:after-returning | 用于配置后置通知。指定增强的方法在切入点方法之后执行 |
环绕通知 | https://www.cnblogs.com/juno3550/p/aop:around | 用于配置环绕通知。指定增强的方法在切入点方法之前和之后都会执行 |
异常抛出通知 | https://www.cnblogs.com/juno3550/p/aop:throwing | 用于配置异常执出通知。指定增强的方法在出现异常时执行 |
最终通知 | https://www.cnblogs.com/juno3550/p/aop:after | 用于配置最终通知。无论增强方式执行是否有异常都会执行 |
切点表达式的抽取
<aop:config> <!--配置公共切入点--> <aop:pointcut id="pt1" expression="execution(* *(..))"/> <aop:aspect ref="myAdvice"> <!--配置局部切入点--> <aop:pointcut id="pt2" expression="execution(* *(..))"/> <!--引用公共切入点--> <https://www.cnblogs.com/juno3550/p/aop:before method="logAdvice" pointcut-ref="pt1"/> <!--引用局部切入点--> <https://www.cnblogs.com/juno3550/p/aop:before method="logAdvice" pointcut-ref="pt2"/> <!--直接配置切入点--> <https://www.cnblogs.com/juno3550/p/aop:before method="logAdvice" pointcut="execution(* *(..))"/> </aop:aspect> </aop:config>
案例
目标类
- 目标接口:
package com.aop; public interface Target { public void targetSave(); }
- 目标实现类:
package com.aop; public class TargetImpl implements Target { @Override public void targetSave() { System.out.println("target save..."); int i = 1/0; } }
切面类
package com.aop; import org.aspectj.lang.ProceedingJoinPoint; public class MyAspect { public void before() { System.out.println("MyAspect before ..."); } public void afterReturn() { System.out.println("MyAspect afterReturn ..."); } public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { // ProceedingJoinPoint:正在执行的连接点=切点 System.out.println("MyAspect around before ..."); // 切点方法 Object proceed = proceedingJoinPoint.proceed(); System.out.println("MyAspect around after ..."); return proceed; } public void afterException() { System.out.println("MyAspect afterException ..."); } public void after() { System.out.println("MyAspect afterFinal ..."); } }
Spring 配置
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" 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 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd "> <!-- 目标对象 --> <bean id="target" class="com.aop.TargetImpl"></bean> <!-- 切面对象 --> <bean id="myAspect" class="com.aop.MyAspect"></bean> <!-- 配置织入:告诉spring框架 哪些方法(切点)需要进行哪些增强(前置、后置等...) --> <!-- 一个beans标签中可以配置多个aop:config标签 --> <aop:config> <!-- 声明切面 --> <!-- 一个aop:config标签中可以配置多个aop:aspect标签,且该标签可以配置在aop:aspect标签内 --> <aop:aspect ref="myAspect"> <!-- 抽取切点表达式 --> <aop:pointcut id="myPointCut" expression="execution(* com.aop.*.*(..))"/> <!-- 切面=切点+通知 --> <!-- 前置增强功能在myAspect的before方法中实现的 --> <!-- <https://www.cnblogs.com/juno3550/p/aop:before method="before" pointcut="execution(public void com.aop.*.*(..))"></https://www.cnblogs.com/juno3550/p/aop:before>--> <!-- <https://www.cnblogs.com/juno3550/p/https://www.cnblogs.com/juno3550/p/aop:after-returning method="afterReturn" pointcut="execution(* com.aop.*.*(..))"></https://www.cnblogs.com/juno3550/p/https://www.cnblogs.com/juno3550/p/aop:after-returning>--> <!-- <https://www.cnblogs.com/juno3550/p/aop:around method="around" pointcut="execution(* com.aop.*.*(..))"></https://www.cnblogs.com/juno3550/p/aop:around>--> <!-- <https://www.cnblogs.com/juno3550/p/aop:after-throwing method="afterException" pointcut="execution(* com.aop.*.*(..))"></https://www.cnblogs.com/juno3550/p/aop:after-throwing>--> <!-- <https://www.cnblogs.com/juno3550/p/aop:after method="after" pointcut="execution(* com.aop.*.*(..))"></https://www.cnblogs.com/juno3550/p/aop:after>--> <https://www.cnblogs.com/juno3550/p/aop:around method="around" pointcut-ref="myPointCut"></https://www.cnblogs.com/juno3550/p/aop:around> <https://www.cnblogs.com/juno3550/p/aop:after-throwing method="afterException" pointcut-ref="myPointCut"></https://www.cnblogs.com/juno3550/p/aop:after-throwing> <https://www.cnblogs.com/juno3550/p/aop:after method="after" pointcut-ref="myPointCut"></https://www.cnblogs.com/juno3550/p/aop:after> </aop:aspect> </aop:config> </beans>
测试
package com.aop; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @ExtendWith(SpringExtension.class) @ContextConfiguration("classpath:applicationContext.xml") public class AspectTest { @Autowired private Target target; @Test public void testAspect() { target.targetSave(); /* 运行结果: MyAspect before ... target save... */ } }
运行结果:
MyAspect around before ... target save... MyAspect afterException ... MyAspect afterFinal ...
AOP 注解开发
基于注解的 AOP 开发步骤:
- 创建目标接口和目标类(内部有切点);
- 创建切面类(内部有增强方法);
- 将目标类和切面类的对象创建权交给 spring;
- 在切面类中使用注解配置织入关系;
- 在配置文件中开启组件扫描和 AOP 的自动代理测试(也可以在 Spring 注解配置类中开启 AOP 注解驱动)。
常用注解
名称 | 注解 | 说明 |
---|---|---|
切面 | @Aspect | 标注切面类 |
AOP 自动代理 | @EnableAspectJAutoProxy | 设置 Spring 注解配置类开启 AOP 注解驱动的支持,加载AOP注解 |
前置通知 | @Before | 用于配置前置通知。指定增强的方法在切入点方法之前执行 |
后置通知 | @AfterReturning | 用于配置后置通知。指定增强的方法在切入点方法之后执行 |
环绕通知 | @Around | 用于配置环绕通知。指定增强的方法在切入点方法之前和之后都执行 |
异常抛出通知 | @AfterThrowing | 用于配置异常抛出通知。指定增强的方法在出现异常时执行 |
最终通知 | @After | 用于配置最终通知。无论增强方式执行是否有异常都会执行 |
切点表达式抽取 | @Pointcut | 可以引用已抽取的切点表达式 |
案例
目标类
- 目标接口:
package com.aop; public interface Target { public void targetSave(); }
- 目标实现类:
package com.aop.anno; import org.springframework.stereotype.Component; @Component("target") public class TargetImpl implements Target { @Override public void targetSave() { System.out.println("target save..."); int i = 1/0; } }
切面类
package com.aop.anno; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Component("myAspect") @Aspect // 标注是一个切面类 public class MyAspect { // 配置前置通知 @Before("execution(* com.aop.anno.*.*(..))") public void before() { System.out.println("MyAspect before ..."); } @AfterReturning("execution(* com.aop.anno.*.*(..))") public void afterReturn() { System.out.println("MyAspect afterReturn ..."); } // @Around("execution(* com.aop.anno.*.*(..))") @Around("pointCut()") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { // ProceedingJoinPoint:正在执行的连接点=切点 System.out.println("MyAspect around before ..."); // 切点方法 Object proceed = proceedingJoinPoint.proceed(); System.out.println("MyAspect around after ..."); return proceed; } // @AfterThrowing("execution(* com.aop.anno.*.*(..))") public void afterException() { System.out.println("MyAspect afterException ..."); } // @After("execution(* com.aop.anno.*.*(..))") // 切面类中定义的切入点只能在当前类中使用,如果想引用其他类中定义的切入点使用“类名.方法名()”引用 @After("MyAspect.pointCut()") public void after() { System.out.println("MyAspect afterFinal ..."); } // 切入点最终体现为一个方法,无参无返回值,无实际方法体内容,但不能是抽象方法 @Pointcut("execution(* com.aop.anno.*.*(..))") public void pointCut() { } }
Spring 配置
配置类 or 配置文件(二选一)
- 配置类:
package com.aop.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @ComponentScan("com.aop.anno") @EnableAspectJAutoProxy public class SpringConfig { }
- 配置文件:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" 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 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd "> <!-- 组件扫描 --> <context:component-scan base-package="com.aop.anno"/> <!-- AOP 自动代理 --> <aop:aspectj-autoproxy/> </beans>
测试
import com.aop.anno.Target; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @ExtendWith(SpringExtension.class) //@ContextConfiguration("classpath:applicationContext-anno.xml") @ContextConfiguration(classes=com.aop.config.SpringConfig.class) public class AspectTest { @Autowired private Target target; @Test public void testAspect() { target.targetSave(); } }
运行结果:
MyAspect around before ... MyAspect before ... target save... MyAspect around after ... MyAspect afterFinal ... MyAspect afterReturn ...
- Spring的AOP的总结
- Spring:IOC与AOP的个人理解
- SpringBoot自定义注解、AOP打印日志
- 浅析Spring.net 中的Aop使用
- spring中自定义注解(annotation)与AOP中获取注解
- spring-7-aop-4-jdk
- Spring从菜鸟到高手(一)实现AOP的基本原理
- Spring AOP原理
- Spring-AOP-学习笔记
- 理解Spring AOP 原理(一)Spring AOP的Demo应用和相关概念
- Spring 3 AOP 概念介绍
- spring的IOC,DI,AOP理解
- spring中使用配置文件实现AOP
- Spring AOP 实现原理
- Spring事务管理—aop:pointcut expression解析
- Spring AOP 基本概念
- 使用Spring注解AOP(基于自定义注解和包下拦截方法)
- Spring AOP 代理机制
- Spring4学习笔记-AOP(基于注解的方式)
- Spring、AOP详解