一、Spring AOP(面向切面编程)概念
1、概述
AOP(Aspect Oriented Programming面向切面编程):说简单点就是我们可以在不修改源代码的情况下,对程序的方法进行增强。说复杂点就是将涉及多业务流程的通用功能抽取并单独封装,形成独立的切面,在合适的时机将这些切面横向切入到业务流程指定的位置中。即系统级的服务从代码中解耦出来。例如:将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来。允许你把遍布应用各处的功能分离出来形成可重用组件。提高程序的可重用性,同时提高了开发的效率。
在面向切面编程的思想里面,把功能分为核心业务功能,和周边功能。
所谓核心业务,工作中做的最多的就是增删改查,增删改查都叫核心业务;
所谓周边功能,比如性能统计,日志记录,事务管理等等。
周边功能在 Spring 的面向切面编程AOP思想里,即被定义为切面。在面向切面编程AOP的思想里面,核心业务功能和切面功能分别独立进行开发,然后把切面功能和核心业务功能 “编织” 在一起,这就叫AOP。
AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
2、AOP的常见术语
- 切面 (Aspect):类是对物体特征的抽象,切面就是对横切关注点的抽象。可以理解成,就是一个特殊的类(包含的都是增强核心业务的代码),切面 = 切入点 + 通知,通俗点就是:在什么时机,什么地方,做什么增强!
- 连接点(join point):指那些被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器。整个系统的所有方法都可以称为连接点。
- 切入点(Pointcut):对连接点进行拦截的定义。就是被选中的连接点,可以通过execution来确定选中的连接点有哪些。
- 通知(Advice):所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类。在方法执行的什么实际(when:方法前/方法后/方法前后)做什么(what:增强的功能),就是切面这个类中的代码块。
- 目标对象:代理的目标对象。
- 引入(Introduction):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段。
- 织入(Weaving) 把切面加入到对象,并创建出代理对象的过程。(由 Spring 来完成)。
3、Spring Aop中的通知类型
- 前置通知(Before Advice)::在目标方法被调用前调用通知功能;相关的类 org.springframework.aop.MethodBeforeAdvice。
- 后置通知(After Advice):在目标方法被调用之后调用通知功能;相关的类 org.springframework.aop.AfterReturningAdvice。
- 返回通知(After-returning):在目标方法成功执行之后调用通知功能。
- 异常通知(After-throwing):在目标方法抛出异常之后调用通知功能;相关的类org.springframework.aop.ThrowsAdvice。
- 环绕通知(Around):把整个目标方法包裹起来,在被调用前和调用之后分别调用通知功能相关的类org.aopalliance.intercept.MethodInterceptor
4、Spring AOP的实现原理
Spring中AOP代理由Spring的IOC容器负责生成、管理,其依赖关系也由IOC容器负责管理。因此,AOP代理可以直接使用容器中的其它bean实例作为目标,这种关系可由IOC容器的依赖注入提供。Spring创建代理的规则为:
(1)JDK动态代理:Spring AOP的首选方法。 每当目标对象实现一个接口时,就会使用JDK动态代理。目标对象必须实现接口。
(2)CGLIB代理:如果目标对象没有实现接口,则可以使用CGLIB代理。
AOP编程其实是很简单的事情,纵观AOP编程,程序员只需要参与三个部分:
- 定义普通业务组件;
- 定义切入点,一个切入点可能横切多个业务组件;
- 定义增强处理,增强处理就是在AOP框架为普通业务组件织入的处理动作。
所以进行AOP编程的关键就是定义切入点和定义增强处理,一旦定义了合适的切入点和增强处理,AOP框架将自动生成AOP代理,即:代理对象的方法=增强处理+被代理对象的方法。
spring aop实现是通过动态代理的方式实现的,动态代理避免了静态代理需要定义冗余的代理类,实现类,动态代理分为两种,第一种就是 jdk 动态代理,第二种就是cglib 动态代理,aop 实现同时采用两种代理模式。
两种动态代理的区别:
- jdk动态代理模式 :采用反射的方式,只能对实现接口的类生成代理,具有加载速度快,执行效率低的特点。
- cglib动态代理模式:采用的asm,通过字节码形式实现,是针对类实现代理,具有加载速度慢,执行效率高的特点。
二、基于Aspectj实现切面
2.1、什么是AspectJ
AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件,AspectJ不是Spring组成部分,独立AOP框架,一般把AspectJ和Spring框架一起使用,进行AOP操作。
2.2、基于AspectJ实现AOP
2.2.1、添加maven依赖
<!-- aspectj支持 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.9</version>
</dependency>
<dependency>
<groupId>org.apache.geronimo.bundles</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.6.8_2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.1.RELEASE</version>
</dependency>
2.2.2、切入点表达式
execution ,匹配方法的执行(常用),让spring知道对哪个类中的哪个方法进行增强。
- execution(表达式)
execution([权限修饰符] [返回值类型] 包名.类名.方法名(参数列表))
- 全匹配方式
public void com.arbor.service.impl.AccountServiceImpl.saveAccount(com.arbor.domain.Account)
- 访问修饰符可以省略
void com.arbor.service.impl.AccountServiceImpl.saveAccount(com.arbor.domain.Account)
- 返回值可以使用*号,表示任意返回值
com.arbor.service.impl.AccountServiceImpl.saveAccount(com.arbor.domain.Account)
- 包名可以使用号,表示任意包,有几级包,需要写几个
*.*.*.*.AccountServiceImpl.saveAccount(com.arbor.domain.Account)
- 使用…来表示当前包,及其子包
com..AccountServiceImpl.saveAccount(com.arbor.domain.Account)
- 类名可以使用*号,表示任意类
com..*.saveAccount(com.arbor.domain.Account)
- 方法名可以使用*号,表示任意方法
com..*.*( com.arbor.domain.Account)
- 参数列表可以使用*,表示参数可以是任意数据类型,但是必须有参数
com..*.*(*)
- 参数列表可以使用…表示有无参数均可,有参数可以是任意类型
com..*.*(..)
- 全通配方式
*..*.*(..)
- 通常情况下,是对业务层的方法进行增强,所以切入点表达式都是切到业务层实现类
execution(* com.arbor.service.impl.*.*(..))
2.2.3、常用注解
- @Aspect:把当前类声明为切面类;
- @Before:把当前方法看成是前置通知;
- @AfterReturning:把当前方法看成是后置通知;
- @AfterThrowing:把当前方法看成是异常通知;
- @After:把当前方法看成是最终通知。
上面四个注解的参数:
value:用于指定切入点表达式,还可以指定切入点表达式的引用。
2.2.4、示例
- 创建一个类和方法,对该类中的方法做增强
import org.springframework.stereotype.Component;
/**
* @author chenqun
* @date 2023/3/14 22:01
*/
@Component
public class QiZeKJService {
public String addQiZeKJ() {
System.out.println("addQiZeKJ...");
return "ok";
}
}
- 创建增强类,在增强类中创建方法,不同的方法代表不同的通知类型,并配置不同类型的通知
/**
* @author chenqun
* @date 2023/3/14 22:02
*/
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect// aop 代理
public class UserProxy {
/**
* 前置通知
*/
@Before("execution(* com.qizekj.proxy.QiZeKJService.addQiZeKJ(..));")
public void before() {
System.out.println("前置通知...");
}
/**
* 后置通知
*/
@After("execution(* com.qizekj.proxy.QiZeKJService.addQiZeKJ(..));")
public void after() {
System.out.println("后置通知...");
}
/**
* 环绕通知
*/
@Around(value = "execution(* com.qizekj.proxy.QiZeKJService.addQiZeKJ(..));")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕通知...");
System.out.println("目标方法之前开始执行...");
Object result = proceedingJoinPoint.proceed();
System.out.println("目标方法之后开始执行...");
return result;
}
//@AfterReturning表达后置通知/返回通知,表达方法返回结果之后执行
@AfterReturning(value = "execution(* com.qizekj.proxy.QiZeKJService.addQiZeKJ(..));")
public void afterReturning() {
System.out.println("afterReturning");
}
//@AfterThrowing表达异常通知
@AfterThrowing(value = "execution(* com.qizekj.proxy.QiZeKJService.addQiZeKJ(..));")
public void afterThrowing() {
System.out.println("afterThrowing");
}
}
- 创建一个Spring的配置文件,开启注解扫描和开启aop
<?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:context="http://www.springframework.org/schema/context"
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/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
">
<!--开启注解扫描,告知 spring,在创建容器时要扫描的包 -->
<context:component-scan base-package="com.qizekj.proxy"></context:component-scan>
<!--开启 spring 对注解 AOP 的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
- 创建测试类
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author chenqun
* @date 2023/3/14 22:05
*/
public class ProxyTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext app =
new ClassPathXmlApplicationContext("proxy.xml");
QiZeKJService qiZeKJService = app.getBean("qiZeKJService", QiZeKJService.class);
qiZeKJService.addQiZeKJ();
}
}
- 运行结果:
2.2.5、不使用XML文件开启AOP注解
定义一个配置类,使用@EnableAspectJAutoProxy注解:
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* @author chenqun
* @date 2023/3/14 22:54
*/
@Configuration
@ComponentScan(basePackages="com.qizekj.proxy")
@EnableAspectJAutoProxy
public class SpringConfiguration {
}
定义一个配置类MyConfigofAOP,将切面类和业务逻辑类(目标方法所在类)都加入到容器中
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* @author chenqun
* @date 2023/3/14 23:03
*/
@EnableAspectJAutoProxy
@Configuration
public class MyConfigofAOP {
//将切面类和业务逻辑类(目标方法所在类)都加入到容器中;
@Bean
public QiZeKJService qiZeKJService(){
return new QiZeKJService();
}
//切面类
@Bean
public UserProxy userProxy(){
return new UserProxy();
}
}
编写测试类:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author chenqun
* @date 2023/3/14 22:05
*/
public class ProxyTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfigofAOP.class);
QiZeKJService qiZeKJService = applicationContext.getBean(QiZeKJService.class);
qiZeKJService.addQiZeKJ();
applicationContext.close();
}
}
运行结果:
2.3、基于XML配置文件方式
2.3.1、常用配置
- aop:config:声明开始aop的配置
- aop:aspect:用于配置切面
参数:
id:给切面提供一个唯一标识
ref:引用配置好的通知类bean的id
- aop:pointcut:配置切入点表达式(指定对哪些类的哪些方法进行增强)
参数:
expression:定义切入点表达式
id:给切入点表达式提供一个唯一标识
- aop:before:配置前置通知(指定增强的方法在切入点方法之前执行)
- aop:after-returning:用于配置后置通知
aop:after-throwing:用于配置异常通知
aop:after:用于配置最终通知
上面四个配置的参数:
method:指定通知类中的增强方法名称
poinitcut:指定切入点表达式
ponitcut-ref:指定切入点的表达式的引用
- 配置文件的约束:
<?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
http://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>
2.3.2、使用示例
- 创建一个类和方法,对该类中的方法做增强
public class Book {
public void buy() {
System.out.println("buy....");
}
}
- 创建增强类,用于增强对应的方法
public class BookProxy {
public void before() {
System.out.println("before....");
}
public void afterReturning() {
System.out.println("afterReturning....");
}
public void afterThrowing() {
System.out.println("afterThrowing....");
}
public void after() {
System.out.println("after....");
}
}
- 创建Spring的配置文件,proxy2.xml
<?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">
<!-- 创建Book对象 -->
<bean id="book" class="com.qizekj.proxy.Book"/>
<bean id="bookProxy" class="com.qizekj.proxy.BookProxy"/>
<!-- 配置AOP的增强 -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut expression="execution(* com.qizekj.proxy.Book.buy(..))" id="pt1"/>
<!-- 配置切面 -->
<aop:aspect ref="bookProxy">
<!--配置通知的类型要写在此处-->
<aop:before method="before" pointcut-ref="pt1"/>
<aop:after method="after" pointcut-ref="pt1"/>
<aop:after-returning method="afterReturning" pointcut-ref="pt1"/>
<aop:after-throwing method="afterThrowing" pointcut-ref="pt1"/>
</aop:aspect>
</aop:config>
</beans>
- 创建测试方法
@Test
public void testProxyXml() {
ApplicationContext context = new ClassPathXmlApplicationContext("proxy2.xml");
Book book = context.getBean("book", Book.class);
book.buy();
}
- 运行结果
2.3.3、环绕通知
- aop:around:用于配置环绕通知
参数:
method:通知中方法的名称
pointct:定义切入点表达式
pointcut-ref:指定切入点表达式的引用
spring提供的一种可以在代码中手动控制增强代码什么时候执行的方式,通常情况下,环绕通知都是独立使用的。
eg:
- 创建环绕通知类BookProxy2:
public class BookProxy2 {
public Object around(ProceedingJoinPoint pjp) {
// 定义返回值,被增强的方法的返回值
Object rtVal = null;
try {
// 获取被增强方法的参数列表
Object[] args = pjp.getArgs();
System.out.println("环绕通知的前置通知....");
// 执行被增强的方法,并传入参数,如果确定没有返回值或者没有参数可以不写
rtVal = pjp.proceed(args);
System.out.println("环绕通知的后置通知....");
} catch (Throwable e) {
System.out.println("环绕通知的异常通知....");
e.printStackTrace();
} finally {
System.out.println("环绕通知的最终通知....");
}
// 将被增强方法的返回值返回
return rtVal;
}
}
- 创建Spring的配置文件,proxy3.xml
<?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">
<!-- 创建Book对象 -->
<bean id="book" class="com.qizekj.proxy.Book"/>
<bean id="bookProxy" class="com.qizekj.proxy.BookProxy2"/>
<!-- 配置AOP的增强 -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut expression="execution(* com.qizekj.proxy.Book.buy(..))" id="pt1"/>
<!-- 配置切面 -->
<aop:aspect ref="bookProxy">
<!--配置通知的类型要写在此处-->
<aop:around method="around" pointcut-ref="pt1"/>
</aop:aspect>
</aop:config>
</beans>
- 创建测试方法
@Test
public void testProxyXml() {
ApplicationContext context = new ClassPathXmlApplicationContext("proxy3.xml");
Book book = context.getBean("book", Book.class);
book.buy();
}
- 运行结果