一、AOP的相关概念
1.1AOP概述
1.1.1什么是AOP
- AOP:全称是Aspect Oriented Programming,即面向切面编程,通过预编译的方式和运行期动态代理实现程序功能的统一维护的一种技术。简单来说就是把程序的重复代码抽取出来,在需要执行的时候,使用动态代理技术,在不修改源码的基础上,对我们已有的方法进行增强。
1.1.2 AOP的使用及优势
- 问题:在传统的业务处理代码中,通常会进行事务处理、日志记录等操作。虽然使用OOP可以通过组合或者继承的方式来达到代码的重用,但是如果要实现某个功能(如日志记录),同样的代码会分散到各个方法中。这样,如果想要关闭某个功能,或者对其进行修改,就必须修改所有的相关方法,这不但增加了开发人员的工作量,而且提高了代码的出错率。
- 解决方案:AOP采取横向抽取的机制,将分散在各个方法中的重复代码提取出来,然后在程序编译或者运行时,再将这些提取出来的代码应用到需要执行的地方。这种采用横向抽取的机制,采用传统的OOP思想显然是无法办到的,因为OOP只能实现父子关系的纵向重用。虽然AOP是一种新的编程思想,但却不是OOP的替代品,它只是OOP的延伸和补充。
- 作用:在程序运行期间,不修改源码的基础上对方法进行增强。
- 优势:减少重复代码;提高开发效率;维护方便
1.1.3 类与切面的关系以及AOP术语
从图中可以看到,通过Aspect(切面)分别在Class1和Class2的方法中加入了事务、日志
、权限以及异常等功能。AOP的使用使得开发人员在编写业务逻辑的时候可以专心于核心业务,而不用过多地关注于其他的逻辑实现,这样不但提高了开发效率,而且增加了可维护性。下面看看AOP中的一些术语:
- Target(目标对象):代理的目标对象,即所有被通知的对象,也被称之为增强对象。
- Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类
- JointPoint(连接点):所谓连接点是值那些被拦截到的点,它实际上是对象的一个操作,例如方法的调用或者抛出异常。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点。
- Pointcut(切入点):切入点是指我们要对哪些JoinPoint进行拦截的定义。通知的类型:前置通知、后置通知、异常通知、最终通知、环绕通知。
- Advice(通知/增强):所谓通知是拦截到JoinPoint之后所要做的事情
- Aspect(切面):在实际应用中,切面通常是指封装的用于横向插入系统功能的类,如上面的事务类、日志类以及异常类等。在上面的配置中,如果这个类要被Spring容器识别为切面,需要在配置文件中通过元素指定。
- Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织入而AspectJ采用编译期织入和类装载期织入。
- Introduction(引介):引介是一种特殊的通知,在不修改类代码的前提下,Introduction可以在运行期为类动态地添加一些方法或者Field。
1.1.4 AOP开发明确的事项
1.1.4.1 需要编写的内容
- 编写核心业务代码(目标类的目标方法)
- 编写切面类,切面类中有通知(增强功能方法)
- 在配置文件中,配置织入关系,即将哪些通知和哪些连接点进行结合
1.1.4.2AOP技术实现的内容
Spring框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用动态代理机制,动态的创建目标对象的代理对象,根据通知类别,在代理对象的对应位置上,将通知对应的功能织入,完成完整的代码逻辑运行。
1.2 基于XML的AOP配置
1.2.1导入坐标
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.7</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
</dependencies>
1.2.2在src目录下,创建一个com.itheima.utils包,在该包中创建切面类Logger
package com.itheima.utils;
import org.aspectj.lang.ProceedingJoinPoint;
public class Logger {
/*
* 前置通知
* */
public void beforePrintLog(){
System.out.println("beforePrintLog");
}
/*
* 后置通知
* */
public void afterReturningPrintLog(){
System.out.println("afterReturningPrintLog");
}
/*
* 异常通知
* */
public void afterThrowingPrintLog(){
System.out.println("afterThrowingPrintLog");
}
/*
* 最终通知
* */
public void aftergPrintLog(){
System.out.println("aftergPrintLog");
}
/*
* 环绕通知
* 问题:
* */
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
Object[] args = pjp.getArgs();//得到方法所需要的参数
System.out.println("aroundPrintLog开始记录了。。。前置");
rtValue = pjp.proceed(args);//明确调用业务层的方法(切入点方法)
System.out.println("aroundPrintLog开始记录了。。。后置");
return rtValue;
} catch (Throwable throwable) {
System.out.println("aroundPrintLog开始记录了。。。异常");
throw new RuntimeException(throwable);
}finally {
System.out.println("aroundPrintLog开始记录了。。。最终");
}
}
}
1.2.3 创建配置文件,指定代理对象
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置Spring的IoC,把Service对象配置进来-->
<bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl"></bean>
<!--配置Logger类-->
<bean id="logger" class="com.itheima.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切入点表达式 id属性用于指定表达式的唯一标识。expression属性用于指定表达式内容
此标签写在aop:aspect标签内部知恩感当前切面的标签使用。
它还可以写在aop:aspect外面,此时就变成了所有标签可以使用
-->
<aop:pointcut id="pt1" expression="execution(* com.itheima.dao.impl.*.*(..))"/>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置前置通知:在切入点方法执行之前指定-->
<aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>
<!--配置后置通知:在切入点方法正常执行之后执行,它和异常通知永远只有一个-->
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>
<!--配置异常通知-->
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>
<!--配置最终通知-->
<aop:after method="aftergPrintLog" pointcut-ref="pt1"></aop:after>
<!--配置环绕通知-->
<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
</beans>
首先通过元素定义了目标类和切面,然后使用ProxyFactoryBean类定义代理类和切面对象。在定义的代理对象中,分别通过< property >子元素指定了代理实现的接口、代理的目标对象、需要织入目标类的通知以及代理方式。
1.3AspectJ开发
AspectJ是一个基于Java语言的AOP框架,它提供了强大的AOP功能。新版本的Spring框架使用AspectJ来开发AOP。使用AspectJ实现AOP有两种方式:一种是基于XML的声明式AspectJ,另一种是基于注解的声明式AspectJ。
1.3.1基于XML的声明式AspectJ
基于XML的声明式AspectJ是指通过XML文件来定义切面、切入点以及通知,所有的切面、切入点和通知都必须定义在< aop:config >元素。
在上图中,Spring配置文件中的< beans >元素下面可以包含多个元素,一个元素下面又可以包含属性和子元素,其子元素包括aop:pointcut、aop:advisor和aop:aspect。在配置的时候,三个子元素的顺序必须按照这个顺序来定义。在aop:aspect元素下面,同样包含多个子元素,通过使用aop:aspect元素及其子元素就可以在XML文件中配置切面、切入点以及通知。上面元素配置的代码如下:
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置Spring的IoC,把Service对象配置进来-->
<bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl"></bean>
<!--配置Logger类-->
<bean id="logger" class="com.itheima.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切入点表达式 id属性用于指定表达式的唯一标识。expression属性用于指定表达式内容
此标签写在aop:aspect标签内部知恩感当前切面的标签使用。
它还可以写在aop:aspect外面,此时就变成了所有标签可以使用
-->
<aop:pointcut id="pt1" expression="execution(* com.itheima.dao.impl.*.*(..))"/>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置前置通知:在切入点方法执行之前指定-->
<aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>
<!--配置后置通知:在切入点方法正常执行之后执行,它和异常通知永远只有一个-->
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>
<!--配置异常通知-->
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>
<!--配置最终通知-->
<aop:after method="aftergPrintLog" pointcut-ref="pt1"></aop:after>
<!--配置环绕通知-->
<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
</beans>
aop:config用于声明开始aop的配置。
<aop:config>
<!--配置的代码都写在这里-->
</aop:config>
1.3.2配置切面
在Spring的配置文件中,配置切面使用的是aop:aspect元素,该元素会将一个已经定义好的Spring Bean转换成切面Bean,所以要在配置文件中先定义一个普通的Spring Bean(如上述代码中定义的Logger),定义完成后,通过aop:aspect元素的ref属性即可引用该Bean。配置aop:aspect元素的时候,通常会指定id和ref两个属性,其中id用于定义该切面的唯一标识名称,ref用于引用普通的Spring Bean。
<!--配置Logger类-->
<bean id="logger" class="com.itheima.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<aop:aspect id="logAdvice" ref="logger"></aop:aspect>
</aop:config>
1.3.3切入点配置
在Spring的配置文件中,切入点是通过aop:pointcut元素来定义的。当aop:pointcut元素作为aop:config元素的子元素定义时,表示该切入点是全局的切入点,它可以被多个切面所共享;当aop:pointcut元素作为aop:aspect元素的子元素时,表示该切入点只对当前切面有效。在定义aop:pointcut元素的时候,通常会指定id和expression两个属性,其中id用于指定切入点的唯一标识名称,expression用于指定切入点关联的切入点表达式。
- < aop:pointcut>作用:用于配置切入点表达式。就是指定哪些方法进行增强
- < aop:pointcut>属性:expression用于定义切入点表达式。id用于给切入点表达式提供一个唯一的标识。切入点表达式声明如下:
execution:匹配方法的执行(常用)
execution(表达式)
表达式语法:execution([修饰符] 返回值类型 包名.类名.方法名(参数))
写法说明:
全匹配方式:pulbic void com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
返回值类型可以用*号,表示任意返回值:* com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
包名可以使用*号,表示任意包,但是有几级包,就需要写几个*:* *.*.*.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
使用..来表示当前包及其子包:* *..AccountServiceImpl.saveAccount(com.itheima.domain.Account)
类名可以使用*号表示任意类:* *..*.saveAccount(com.itheima.domain.Account)
方法名可以使用*号,表示任意方法:* *..*.*(com.itheima.domain.Account)
参数列表可以使用*号表示参数可以是任意数据类型,但是必须有参数:* *..*.*(*)
参数列表可以使用..表示有无参数均可,有参数可以是任意类型:* *..*.*(..)
全通配方式:* *..*.*(..)
通常情况下,我们都是对业务层的方法进行增强,所以切入点表达式都是切到业务层的实现类。
execution(* com.itheima.service.impl*.*(..))
1.3.4配置消息通知的类型
1.3.4.1 aop:before
- 作用:aop:before用于配置前置通知,指定增强的方法在切入点方法之前执行;
- 属性:method用于指定通知类中增强方法的名称;pointcut-ref用于指定切入点表达式的引用;pointcut用于指定切入点表达式;
- 执行时间点:在切入点方法执行之前执行。
<aop:before method="beginTransaction" pointcut-ref="pt1"/>
1.3.4.2 aop:after-returning
- 作用:用于配置后置通知
- 属性:method用于指定通知中的方法名称;pointcut用于定义切入点表达式;pointcut-ref:指定切入点表达式的引用
- 执行时间点:切入点方法正常执行之后,它和异常通知只有一个能够执行。
aop:after-returning method="commit" pointcut-ref="pt1"/>
1.3.4.3 aop:after-throwing
- 作用:用于配置异常通知
- 属性:method用于指定通知中的方法名称;pointcut用于定义切入点表达式;pointcut-ref:指定切入点表达式的引用
- 执行时间点:切入点方法执行产生异常之后执行,它和后置通知只能执行一个
<aop:after-throwing method="rollback" pointcut-ref="pt1"/>
1.3.4.4 aop:after
- 作用:用于配置最终通知
- 属性:method用于指定通知中的方法名称;pointcut用于定义切入点表达式;pointcut-ref:指定切入点表达式的引用
- 执行时间点:无论切入点方法是否有异常,它都会在其后面执行
< aop:after method=“release” pointcut-ref=“pt1”/>
1.3.4.5 aop:around
- 作用:把当前方法看成是环绕通知。
- 属性:method指定通知中方法的名称;pointcut定义切入点表达式;pointcut-ref指定切入点表达式的引用。
- 说明:它是spring框架为我们提供的一种可以在代码中手动控制增强代码什么时候执行的方式。
- 通常环绕通知都是独立使用的。
public Object aroundPrintLog(ProceedingJoinPoint pjp){
//定义返回值
Object rtValue = null;
try {
//获取方法所需要的参数
Object[] args = pjp.getArgs();//得到方法所需要的参数
//前置通知
System.out.println("aroundPrintLog开始记录了。。。前置");
rtValue = pjp.proceed(args);//明确调用业务层的方法(切入点方法)
//后置通知
System.out.println("aroundPrintLog开始记录了。。。后置");
return rtValue;
} catch (Throwable throwable) {
//异常通知
System.out.println("aroundPrintLog开始记录了。。。异常");
throw new RuntimeException(throwable);
}finally {
//最终通知
System.out.println("aroundPrintLog开始记录了。。。最终");
}
}