一、AOP
1、概述
AOP为 Aspect Oriented Programming 的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
Spring AOP 是基于 AOP 编程模式的一个框架,它能够有效的减少系统间的重复代码,达到松耦合的目的。Spring AOP 使用纯 Java 实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类植入增强的代码。有两种实现方式:基于接口的 JDK 动态代理和基于继承的 CGLIB 动态代理。
AspectJ 是一个基于 Java 语言的 AOP 框架,从 Spring 2.0 开始,Spring AOP 引入了对 AspectJ 的支持。AspectJ 扩展了 Java 语言,提供了一个专门的编译器,在编译时提供横向代码的植入。
2、AOP术语
为了更好地理解 AOP,我们需要了解一些它的相关术语。这些专业术语并不是 Spring 特有的,有些也同样适用于其它 AOP 框架,如 AspectJ。它们的含义如下:
通知(Advice):指拦截到 Joinpoint 之后要做的事情,即对切入点增强的内容。其中包括了“around”、“before”和“after”等不同类型的通知(通知的类型将在后面部分进行说明);前置通知、后置通知、环绕通知、最终通知、异常通知。环绕单独讲。
连接点(Joinpoint):指那些可以被拦截到的点,在 Spring 中,指可以被动态代理拦截目标类的方法(和方法有关的前前后后(抛出异常),都是连接点);
切入点(Pointcut):指要对哪些 Joinpoint 进行拦截,即被拦截的连接点、被拦截类中的方法(筛选连接点,选中那几个你想要的方法);
切面(Aspect):Aspect 由切入点(Pointcut)和 通知(Advice)组成, 它既包含了增强逻辑的定义, 也包括了切入点的定义,即两者的结合;
引入(introduction):允许我们向现有的类添加新方法属性(就是把切面(也就是新方法属性:通知定义的)用到目标类中);
目标(Target):代理的目标对象,即被增强的对象(引入中所提到的目标类,也就是要被通知的对象);
代理(Proxy):指生成的代理对象;
织入(Weaving):指把增强代码应用到目标上,生成代理对象的过程;
Spring AOP就是负责实施切面的框架, 它将切面所定义的增强逻辑织入到切面所指定的切入点中。
AOP 是 Spring 的核心之一,在 Spring 中经常会使用 AOP 来简化编程。在 Spring 框架中使用 AOP 主要有以下优势:
-
提供声明式企业服务,特别是作为 EJB 声明式服务的替代品。最重要的是,这种服务是声明式事务管理;
-
允许用户实现自定义切面。在某些不适合用 OOP 编程的场景中,采用 AOP 来补充;
-
可以对业务逻辑的各个部分进行隔离,从而使业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时也提高了开发效率。
3、入门案例
-
创建maven工程,修改pom.xml加入spring-context、spring-test、aspectjweaver、lombok依赖
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <!-- 引入spring框架相关jar,包含了aop相关的支持--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.10.RELEASE</version> </dependency> <!-- spring单元测试支持的jar--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.2.10.RELEASE</version> </dependency> <!-- spring aop中解析切入点表达式,以及用到其中的注解--> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.6</version> </dependency> <!-- @DATA等注解支持--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> </dependency> </dependencies>
-
UserService
public interface UserService { void add(); void del(); void update(); void select(); }
-
UserServiceImpl
@Service public class UserServiceImpl implements UserService { @Override public void add() { System.out.println("add"); } @Override public void del() { System.out.println("del"); } @Override public void update() { System.out.println("update"); } @Override public void select() { System.out.println("select"); } }
-
编写通知类Logger,这里模拟日志记录功能
public class Logger { public void printLog(){ System.out.println("日志记录了....."); } }
-
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: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/aop https://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 配置目标类的bean--> <bean id="userService" class="com.hqyj.cl.service.impl.UserServiceImpl"/> <!-- 配置通知类的bean--> <bean id="logger" class="com.hqyj.cl.utils.Logger"/> <!-- 配置AOP--> <aop:config> <!--切入点 expression: 表达式 execution(* com.hqyj.cl.pojo.UserServiceImpl.*(..)) 第一个 *: 返回值类型 第二个参数: 切入的位置 第三个参数 *: 切入的方法 (..): 表示方法参数随便是多少个 --> <aop:aspect id="aspect" ref="logger"> <!-- 配置前置通知--> <aop:before method="printLog" pointcut="execution(* com.hqyj.cl.service.impl.UserServiceImpl.*(..))"/> </aop:aspect> </aop:config> </beans>
-
UserServiceImplTest
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {"classpath:applicationContext.xml"}) public class UserServiceImplTest { @Autowired private UserService userService; @Test public void add(){ userService.add(); } }
测试结果,会在调用add方法之前,输出一句日志记录了.....
4、通知类型
Advice 直译为通知,也有的资料翻译为“增强处理”,共有 5 种类型,如下表所示:
通知 | 说明 |
---|---|
before(前置通知) | 通知方法在目标方法调用之前执行 |
after-returning(后置通知) | 通知方法会在目标方法返回后调用 |
after-throwing(异常通知) | 通知方法会在目标方法抛出异常后调用 |
after(最终通知) | 通知方法在目标方法返回或异常后调用 |
around(环绕通知) | 通知方法会将目标方法封装起来 |
4.1 示例代码
-
Logger通知类
public class Logger { public void printLogBefore(){ System.out.println("日志记录了...前置通知"); } public void printLogRound(){ System.out.println("日志记录了...环绕通知"); } public void printLogAfter(){ System.out.println("日志记录了...后置通知"); } public void printLogException(){ System.out.println("日志记录了...异常通知"); } public void printLogFinally(){ System.out.println("日志记录了...最终通知"); } }
-
UserService
public interface UserService { void add(); void del(); void update(); void select(); }
-
UserServiceImpl
@Service public class UserServiceImpl implements UserService { @Override public void add() { // int i = 1/0; System.out.println("add"); } @Override public void del() { System.out.println("del"); } @Override public void update() { System.out.println("update"); } @Override public void select() { System.out.println("select"); } }
-
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: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/aop https://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 配置目标类的bean--> <bean id="userService" class="com.hqyj.cl.service.impl.UserServiceImpl"/> <!-- 配置通知类的bean--> <bean id="logger" class="com.hqyj.cl.utils.Logger"/> <!-- 配置AOP--> <aop:config> <!--切入点 expression: 表达式 execution(* com.hqyj.cl.pojo.UserServiceImpl.*(..)) 第一个 *: 返回值类型 第二个参数: 切入的位置 第三个参数 *: 切入的方法 (..): 表示方法参数随便是多少个 --> <aop:aspect id="aspect" ref="logger"> <!-- aop:pointcut标签单独配置切入点表达式,方便各通知标签引用 --> <aop:pointcut id="pointcut" expression="execution(* com.hqyj.cl.service.*.*(..))"/> <!-- 配置前置通知--> <aop:before method="printLogBefore" pointcut-ref="pointcut"/> <!-- 配置后置通知--> <aop:after-returning method="printLogAfter" pointcut-ref="pointcut"/> <!-- 异常通知配置/后置通知和异常通知这两类不会同时通知--> <aop:after-throwing method="printLogException" pointcut-ref="pointcut"/> <!-- 最终通知配置--> <aop:after method="printLogFinally" pointcut-ref="pointcut"/> </aop:aspect> </aop:config> </beans>
-
测试类
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {"classpath:applicationContext.xml"}) public class UserServiceImplTest { @Test public void add(){ ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); UserService userService = ac.getBean("userService", UserService.class); userService.add(); } }
当没有异常发生时,输出内容为
日志记录了...前置通知 add 日志记录了...后置通知 日志记录了...最终通知
当异常发生时,输出内容为
日志记录了...前置通知 日志记录了...异常通知 日志记录了...最终通知
4.2 环绕通知
-
在目标方法执行之前和之后都可以执行额外代码的通知;
-
在环绕通知中必须显式的调用目标方法,目标方法才会执行,这个显式调用时通过ProceedingJoinPoint来实现的,可以在环绕通知中接收一个此类型的形参,spring容器会自动将该对象传入,注意这个参数必须处在环绕通知的第一个形参位置;
-
只有环绕通知可以接收ProceedingJoinPoint,而其他通知只能接收JoinPoint;
-
环绕通知需要返回返回值,否则真正调用者将拿不到返回值,只能得到一个null;
-
环绕通知有控制目标方法是否执行、有控制是否返回值、有改变返回值的能力;
-
环绕通知虽然有这样的能力,但一定要慎用,不是技术上不可行,而是要小心不要破坏了软件分层的“高内聚 低耦合”的目标。
-
Logger通知类
public class Logger { public void printLogBefore(){ System.out.println("日志记录了...前置通知"); } public Object printLogAround(ProceedingJoinPoint pjp){ System.out.println("日志记录了...环绕通知"); Object result = null; try{ // 模拟执行前置通知 printLogBefore(); // 得到方法执行所需的参数 Object[] args = pjp.getArgs(); // 明确调用业务层方法(切入点方法) result = pjp.proceed(args); // 模拟执行后置通知 printLogAfter(); return result; }catch (Throwable t){ // 模拟执行异常通知 printLogException(); // //如果真实对象调用方法时发生异常,将异常抛给虚拟机 throw new RuntimeException(t); }finally { // 模拟执行最终通知 printLogFinally(); } } public void printLogAfter(){ System.out.println("日志记录了...后置通知"); } public void printLogException(){ System.out.println("日志记录了...异常通知"); } public void printLogFinally(){ System.out.println("日志记录了...最终通知"); } }
-
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: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/aop https://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 配置目标类的bean--> <bean id="userService" class="com.hqyj.cl.service.impl.UserServiceImpl"/> <!-- 配置通知类的bean--> <bean id="logger" class="com.hqyj.cl.utils.Logger"/> <!-- 配置AOP--> <aop:config> <!--切入点 expression: 表达式 execution(* com.hqyj.cl.pojo.UserServiceImpl.*(..)) 第一个 *: 返回值类型 第二个参数: 切入的位置 第三个参数 *: 切入的方法 (..): 表示方法参数随便是多少个 --> <aop:aspect id="aspect" ref="logger"> <!-- aop:pointcut标签单独配置切入点表达式,方便各通知标签引用 --> <aop:pointcut id="pointcut" expression="execution(* com.hqyj.cl.service.*.*(..))"/> <!-- 环绕通知--> <aop:around method="printLogAround" pointcut-ref="pointcut"/> </aop:aspect> </aop:config> </beans>
-
测试类
@Test public void add(){ ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); UserService userService = ac.getBean("userService", UserService.class); userService.add(); }
4.3 JoinPoint
任何通知方法可以将第一个参数定义为org.aspectj.lang.JoinPoint类型(环绕通知需要定义第一个参数为ProceedingJoinPoint类型,它是 JoinPoint 的一个子类)。JoinPoint接口提供了一系列有用的方法,比如 getArgs()(返回方法参数)、getSignature()(返回正在被通知的方法相关信息)
二、基于注解实现AOP(了解)
1、注解
通过全注解方式实现AOP,主要是用到以下注解:
// 标明这是一个配置文件,代替了之前的Spring-config.xml配置文件 @Configuration // 相当于Spring-config.xml配置文件<context:component-scan base-package="com.hqyj.cl"/>,配置自动扫描的包 @ComponentScan("com.hqyj.cl.aop") // 相当于Spring-config.xml配置文件<aop:aspectj-autoproxy/>,自动为切面方法中匹配的方法所在的类生成代理对象 @EnableAspectJAutoProxy // 标注这个类是一个切面 @Aspect // Pointcut是植入Advice的触发条件 @Pointcut // 前置通知 @Before() // 后置通知 @AfterReturning // 最终通知 @After // 异常通知 @AfterThrowing // 环绕通知 @Around
2、示例代码
-
SpringConfiguration配置类
@Configuration @ComponentScan("com.hqyj.cl.aop") @EnableAspectJAutoProxy public class SpringConfiguration { @Bean public UserService userService(){ return new UserServiceImpl(); } }
-
AnnotationLogger通知类
@Aspect // 标注这个类是一个切面 @Component // 标注该类会创建一个bean public class AnnotationLogger { @Pointcut("execution(* com.hqyj.cl.service.*.*(..))") public void pointCut(){ } // 前置通知:目标方法执行之前执行以下方法体的内容 @Before("pointCut()") public void beforeMethod(){ System.out.println("前置通知"); } // 后置通知:目标方法正常执行完毕时执行以下代码 @AfterReturning(value = "pointCut()") public void afterReturningMethod(){ System.out.println("后置通知"); } // 最终通知:目标方法执行之后执行以下方法体的内容,不管是否发生异常。 @After("pointCut()") public void afterMethod(){ System.out.println("最终通知"); } // 异常通知:目标方法发生异常的时候执行以下代码 @AfterThrowing("pointCut()") public void afterThrowing(){ System.out.println("异常通知"); } }
-
UserService和UserServiceImpl同上
-
测试类
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {"classpath:applicationContext.xml"}) public class AnnotationLoggerTest { @Test public void annotationLoggerTest(){ ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class); UserService userService = ac.getBean("userService", UserService.class); userService.add(); } }
3、环绕通知
自行测试
三、动态代理过程
先配置切面,然后在切面中配置通知。要把通知明确的指向某个地方,要给某个方法或者某些方法配置通知,要先指定该方法在哪里,这个时候就需要配置一个切入点,整个过程是一个动态代理的过程。
代理模式:静态代理模式,例如,张三想要租房,但是找不到房子,得找中介,典型的代理模式。中介作为代理对象,而张三则是被代理的对象。静态代理有个局限性,例如张三想要租房子,只能找租房中介,但是想要结婚,就得去找婚庆中介,一个类就需要一个代理对象。动态代理模式,(需实现InvocationHandler接口),代理对象是在运行的过程中生成的,无论你是什么对象,可以封装成类,通过ioc的反射就可以获取到该对象,那么不论你是租房还是婚庆,都可以根据需求动态的生成代理对象。