一、什么是Spring AOP
AOP(Aspect Oriented Programming):面向切面编程,是与OOP(Object Oriented Programming)面向对象编程并列的编程思想。
AOP可以拦截指定的方法并且对方法增强,而且无序侵入到业务代码中,使业务与非业务处理逻辑分离,比如Spring的事务,通过事务的注解配置,Spring会自动在业务方法中开启、提交事务,并且在业务处理时,执行相应的回滚策略。
二、AOP的作用
AOP采取横向抽取机制(动态代理),取代了传统纵向继承机制的重复性代码,其应用主要体现在事务处理、日志管理、权限控制、异常处理等方面。
主要作用是保证开发者在不修改源代码的前提下,为系统中的业务组件添加某种通用功能,使开发人员可以集中处理某一个关注点或者横切逻辑,减少对业务代码的侵入,增强代码的可读性和可维护性。
三、AOP的应用场景
- 日志管理
- 事务管理
- 权限验证
- 性能检测
四、AOP的组成
(1)通知(Advice)
切面的工作被称为通知。
通知:规定了AOP执行的时机和执行的方法
Spring切面可以应用五种类型的通知:
- 前置通知(@Before):在目标方法调用之前调用通知功能。
- 后置功能(@After):通知方法会在目标方法返回或者抛出异常后调用
- 返回通知(@AfterReturning):在目标方法成功执行之后调用通知。
- 异常通知(@AfterThrowing):在目标方法抛出异常后调用通知。
- 环绕通知(@Around):通知包裹了被通知方法,在被通知方法调用之前和之后执行自定义行为。
(2)连接点(join point)
连接点事应用程序执行过程中能插入切面的一个点。这个点可以是调用时,抛出异常时,甚至是修改一个字段时。切面代码可以利用这些点插入到应用的正常流程中,并添加新的行为。
(3)切点(pointcut)
切点的作用就是提供一组规则来匹配连接点,给满足规则的连接点添加通知,简单老说,定义AOP拦截的规则,切点相当于保存了众多连接点的一个集合。
(4)切面(Aspect)
定义AOP是针对某个统一功能的,这个功能就叫做一个切面,比如用户的登录功能或方法的统计日志,它们就各是一个切面。切面是由切点和通知组成的。
通过定义切面,可以让一些业务无关的代码,与业务代码相解耦。比如要在业务方法执行前后打印日志时,我们只需定义一个切面,而不用把打印日志的代码加到业务方法中,这样就将日志打印代码和业务代码相解耦了。
(5)引入(Introduction)
引入允许我们向现有类添加新方法或属性。
(6)织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定连接点被织入到目标对象中,在目标对象的生命周期中,有多个点可以进行织入。
- 编译期:切面在目标类编译时被织入。AspectJ的织入编译器就是以这种方式织入切面的。
- 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特使的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5的加载时织入就支持这种方式织入切面。
- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象。String AOP就是以这种方式织入切面。
五、Spring AOP底层原理
Spring AOP即面向切面编程,是运行时织入的,通过代理对象实现织入。代理对象可以分为静态代理和动态代理。
静态代理:由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。
动态代理:在程序运行时,运用反射机制动态创建而成。
静态代理的每一个代理类只能为一个接口服务,这样一来程序开发中必然会产生过多的代理,而且,所有的代理操作除了调用的方法不一样之外,其他的操作都一样,则此时肯定是重复代码。解决这一问题最好的做法是可以通过一个代理类完成全部的代理功能,那么此时就必须使用动态代理完成。
动态代理与静态代理对照的是动态代理类,动态代理类的字节码在程序运行时由Java反射机制动态生成,无需程序员手工编写它的源代码。动态代理不仅简化了编程工作,而且提高了软件系统的可扩展性,因为Java反射机制可以生成任意类型的动态代理类。java.lang.reflect包中的Proxy类和InvocationHandler接口提供了生成动态代理类的能力。
Spring AOP使用动态代理技术在运行期间织入增强的代码,主要有两种代理机制:基于JDK的动态代理;基于cglib的动态代理。JDK本身只提供接口的代理,而不支持类的代理。
六、Spring实现AOP操作
在Spring中进行AOP操作,需要进行aspectJ实现
方式一:基于aspectJ实现xml配置方式
一、步骤
1、添加依赖
2、创建核心配置文件,导入AOP约束
3、准备增强类和被增强类(指定切入点和通知)
4、使用表达式配置切入点
二、常用的表达式(使用表达式目的是配置切入点,完成方法的增强)
1、访问修饰符:可以是public或者private 通常情况可以指定*,代表是任意的访问修饰符
2、方法的全路径:是增强方法的全路径,表示对这个路径下的类中的方法做增强,可以在类的后面加上*,表示对所有方法做增强
eg:execution(* com.hpe.aop.Book.* (…))
三、代码演示
1、添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.apesource</groupId>
<artifactId>Demo06_AOP_01</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- spring坐标 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.29</version>
</dependency>
<!-- aspectJ依赖,用于解析切入点表达式aop -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
</project>
2、通过Spring API实现,编写业务接口和实习类
package com.apesource.service;
/**
* @author 张荣晁
* @version 1.0
* @since 2024/1/2
*/
public interface IAccountService {
// 增加
public void add(int i);
// 修改
public void update();
// 删除
public int delete();
}
package com.apesource.service;
import org.springframework.stereotype.Service;
/**
* @author 张荣晁
* @version 1.0
* @since 2024/1/2
*/
@Service
public class IAccountServiceImp implements IAccountService{
@Override
public void add(int i) {
System.out.println("新增方法");
}
@Override
public void update() {
System.out.println("修改的方法");
}
@Override
public int delete() {
System.out.println("删除的方法");
// int i = 10/0; (异常测试)
return 0;
}
}
3、编写日志记录代码
package com.apesource.util;
import org.aspectj.lang.ProceedingJoinPoint;
import java.util.Properties;
/**
* @author 张荣晁
* @version 1.0
* @since 2024/1/2
*/
public class Logger {
/* 前置通知 */
public void afterLogger(){
System.out.println("前置通知-日志类logger中调用printLogger方法进行日志记录");
}
/* 返回通知 */
public void returnLogger(){
System.out.println("返回通知-日志类logger中调用printLogger方法进行日志记录");
}
/* 异常通知*/
public void exceptionLogger(){
System.out.println("异常通知-日志类logger中调用printLogger方法进行日志记录");
}
/* 后置通知 */
public void beforeLogger(){
System.out.println("后置通知-日志类logger中调用printLogger方法进行日志记录");
}
/* 环绕通知 */
public Object errayLogger(ProceedingJoinPoint pjp){
Object object = null;
try {
/* 前置通知 */
System.out.println("环绕通知--前置通知");
/* 切点方法 */
Object[] obj = pjp.getArgs();
object = pjp.proceed(obj);
/* 返回通知 */
System.out.println("环绕通知--返回通知");
} catch (Throwable throwable) {
/* 异常通知 */
System.out.println("环绕通知--异常通知");
} finally {
/* 后置通知 */
System.out.println("环绕通知--后置通知");
}
return object;
}
}
4、编写配置文件,导入aop约束
<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">
<!--注入业务层-->
<bean id="accountServiceImp" class="com.apesource.service.IAccountServiceImp"></bean>
<!--注入日志记录层(通知)-->
<bean id="logger" class="com.apesource.util.Logger"></bean>
<!--
spring中基于XML的AOP配置
1.使用aop:config标签表明开始AOP的配置
2.使用aop:aspect标签表明配置切面
id属性:是给切面提供一个唯一标识
ref属性:是指定通知类bean的Id。
3.在aop:aspect标签的内部使用对应标签来配置通知的类型
aop:before:表示配置前置通知
method属性:用于指定Logger类中哪个方法是前置通知
pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
切入点表达式的写法:
关键字:execution(表达式)
表达式:
访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
标准的表达式写法:
public void com.apesource.service.AccountServiceImp.update()
访问修饰符可以省略
void com.apesource.service.AccountServiceImp.update()
返回值可以使用通配符,表示任意返回值
* com.apesource.service.AccountServiceImp.update()
包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*.
* *.*.*.AccountServiceImp.update()
包名可以使用..表示当前包及其子包
* *..AccountServiceImp.update()
类名和方法名都可以使用*来实现通配
* *..*.*()
参数列表:
可以直接写数据类型:
基本类型直接写名称 int
引用类型写包名.类名的方式 java.lang.String
可以使用通配符表示任意类型,但是必须有参数
可以使用..表示有无参数均可,有参数可以是任意类型
全通配写法:
* *..*.*(..)
实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有方法
* com.apesource.service.*.*(..)
-->
<!--配置AOP-->
<aop:config>
<!--配置切面 -->
<aop:aspect id="aopAspect" ref="logger">
<!--前置通知-->
<aop:before method="afterLogger" pointcut-ref="dian"></aop:before>
<!-- 返回通知 -->
<aop:after-returning method="returnLogger" pointcut-ref="dian"></aop:after-returning>
<!-- 异常通知 -->
<aop:after-throwing method="exceptionLogger" pointcut-ref="dian"></aop:after-throwing>
<!-- 后置通知 -->
<aop:after method="beforeLogger" pointcut-ref="dian"></aop:after>
<!-- 环绕通知 -->
<aop:around method="errayLogger" pointcut-ref="dian"></aop:around>
<!--切点-->
<aop:pointcut id="dian" expression="execution(* com.apesource.service.*.*(..))"/>
</aop:aspect>
</aop:config>
</beans>
5、测试类测试
package com.apesource.test;
import com.apesource.service.IAccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author 张荣晁
* @version 1.0
* @since 2024/1/2
*/
// Junit使用Spring测试运行器来测试
@RunWith(SpringJUnit4ClassRunner.class)
// 指定加载的Spring配置文件的位置
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class Test01 {
@Autowired
public IAccountService service;
@Test
public void ceshi(){
service.add(2);
service.delete();
service.update();
}
}
运行结果:
方式二:使用注解来实现AOP
1、添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.apesource</groupId>
<artifactId>Demo06_AOP_01</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- spring坐标 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.29</version>
</dependency>
<!-- aspectJ依赖,用于解析切入点表达式aop -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
2、通过Spring API实现,编写业务接口和实习类
package com.apesource.service;
/**
* @author 张荣晁
* @version 1.0
* @since 2024/1/2
*/
public interface IAccountService {
// 增加
public void add(int i);
// 修改
public void update();
// 删除
public int delete();
}
package com.apesource.service;
import org.springframework.stereotype.Service;
/**
* @author 张荣晁
* @version 1.0
* @since 2024/1/2
*/
@Service
public class IAccountServiceImp implements IAccountService{
@Override
public void add(int i) {
System.out.println("新增方法");
}
@Override
public void update() {
System.out.println("修改的方法");
}
@Override
public int delete() {
System.out.println("删除的方法");
// int i = 10/0; (异常测试)
return 0;
}
}
3、编写日志记录代码
package com.apesource.util;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* @author 张荣晁
* @version 1.0
* @since 2024/1/2
*/
@Component
@Aspect // 标注此类是一个切面
public class Logger {
// 定义切入点的位置和条件,匹配com.apesource.service包及其子包中的所有类的所有方法
@Pointcut("execution(* com.apesource.service.*.*(..))")
public void dian(){}
/* 前置通知 */
@Before("dian()")
public void beforeLogger(){
System.out.println("前置通知-日志类logger中调用printLogger方法进行日志记录");
}
/* 返回通知 */
@AfterReturning("dian()")
public void afterReturnLogger(){
System.out.println("前置通知-日志类logger中调用printLogger方法进行日志记录");
}
/* 异常通知 */
@AfterThrowing("dian()")
public void throwLogger(){
System.out.println("前置通知-日志类logger中调用printLogger方法进行日志记录");
}
/* 后置通知 */
@After("dian()")
public void after(){
System.out.println("前置通知-日志类logger中调用printLogger方法进行日志记录");
}
/* 环绕通知 */
@Around("dian()")
public Object errayLogger(ProceedingJoinPoint pjp){
Object object = null;
try {
/* 前置通知 */
System.out.println("环绕通知--前置通知");
/* 切点方法 */
Object[] obj = pjp.getArgs();
object = pjp.proceed(obj);
/* 返回通知 */
System.out.println("环绕通知--返回通知");
} catch (Throwable throwable) {
/* 异常通知 */
System.out.println("环绕通知--异常通知");
} finally {
/* 后置通知 */
System.out.println("环绕通知--后置通知");
}
return object;
}
}
4、编写配置文件
<?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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 自动扫描base-package对应的路径或者该路径的子包下面的java文件 -->
<context:component-scan base-package="com.apesource"></context:component-scan>
<!-- aop注解驱动 @EnableAspectJAutoProxy -->
<aop:aspectj-autoproxy/>
</beans>
5、测试类测试
package com.apesource.test;
import com.apesource.service.IAccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author 张荣晁
* @version 1.0
* @since 2024/1/2
*/
// Junit使用Spring测试运行器来测试
@RunWith(SpringJUnit4ClassRunner.class)
// 指定加载的Spring配置文件的位置
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class Test01 {
@Autowired
public IAccountService service;
@Test
public void ceshi(){
service.add(2);
System.out.println("=============");
service.delete();
System.out.println("=============");
service.update();
}
}
运行结果:
在业务层模拟了出现异常时报错的场景,可以看到异常通知这时执行了,可以用来定位错误,但返回通知不会执行,而后置通知依然执行。
package com.apesource.service;
import org.springframework.stereotype.Service;
/**
* @author 张荣晁
* @version 1.0
* @since 2024/1/2
*/
@Service
public class IAccountServiceImp implements IAccountService{
@Override
public void add(int i) {
System.out.println("新增方法");
}
@Override
public void update() {
System.out.println("修改的方法");
}
@Override
public int delete() {
System.out.println("删除的方法");
int i = 10/0; // (异常测试)
return 0;
}
}