1 Spring AOP
1.1 AOP介绍
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率
- 知识扩展:
- 面向过程编程 C语言
- 半面向对象的程序设计 C++
- 面向对象的编程技术 java
- 编程方式:
- 面向接口编程
- 面向切面编程
- 总结:
- AOP(面向切面编程) 主要利用动态代理的模式 降低程序的耦合度,扩展业务功能方法.
1.2 关于AOP名词介绍
- 连接点:
- 用户可以被扩展的方法 joinPoint
- 切入点:
- 用户实际扩展的方法(判断方法能否进入切面) pointcut 切入点表达式
- 通知:
- 扩展方法的具体实现 @before
- 切面:
- 将通知应用到切入点的过程 方法功能得到扩展全部配置(切面=切入点表达式+通知方法)
1.3 通知类型
- before:
- 在目标方法执行之前执行
- afterReturning:
- 在目标方法执行之后返回时执行
- afterThrowing:
- 在目标方法执行之后,抛出异常时执行
- after:
- 无论程序是否执行成功,都要最后执行的通知
- around:
- 在目标方法执行前后 都要执行的通知(完美体现了动态代理模式)
- 功能最为强大 只有环绕通知可以控制目标方法的执行
- 关于通知方法总结:
- 环绕通知是处理业务的首选. 可以修改程序的执行轨迹
- 另外的四大通知一般用来做程序的监控(监控系统) 只做记录
1.4 切入点表达式
当程序满足切入点表达式,才能进入切面,执行通知方法
- bean(“bean的ID”) 根据beanId进行拦截 只能匹配一个
- within(“包名.类名”) 可以使用通配符*? 能匹配多个.
粒度: 上述的切入点表达式 粒度是类级别的. 粗粒度. - execution(返回值类型 包名.类名.方法名(参数列表…))
粒度: 控制的是方法参数级别. 所以粒度较细. 最常用的. - @annotation(包名.注解名) 只拦截注解.
粒度: 注解是一种标记 根据规则标识某个方法/属性/类 细粒度
1.5 AOP入门案例
1.5.1 导入jar包
<?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.jt</groupId>
<artifactId>spring_demo_9_aop</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--Spring核心包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.22</version>
</dependency>
<!--引入SpringBean-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.3.22</version>
</dependency>
<!--引入context包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.22</version>
</dependency>
<!--引入表达式jar包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.3.22</version>
</dependency>
<!--引入日志依赖-->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<!--引入测试包-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!--引入AOP包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.1.2</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.6</version>
</dependency>
</dependencies>
</project>
1.5.2 准备代码
public interface DeptService {
void addDept();
void updateDept();
}
import org.springframework.stereotype.Service;
@Service
public class DeptServiceImpl implements DeptService{
@Override
public void addDept() {
System.out.println("添加部门的信息");
}
@Override
public void updateDept() {
System.out.println("更新部门信息");
}
}
1.5.3 配置切面类
- 切面 = 切入点表达式 + 通知方法
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
//1.AOP需要被Spring容器管理
@Component
//2.标识该类为AOP切面,Spring容器默认不能识别切面注解,需要手动配置
@Aspect
public class SpringAOP {
//1.定义before通知
@Before("bean(deptServiceImpl)")
public void before() {
System.out.println("我是before通知");
}
}
1.5.4 编辑配置类
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan("com.jt")
@EnableAspectJAutoProxy(proxyTargetClass=false) //启动AOP注解 创建代理对象
//默认启用JDK动态代理,
//目标对象没有实现接口时,则采用CGLIB
//强制使用cglib proxyTargetClass=true
//JDK代理创建速度快.运行时稍慢
//CGLIB创建时速度较慢,运行时更快
public class SpringConfig {
}
1.5.5 编辑测试代码
1.6 关于表达式写法
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
//1.AOP需要被Spring容器管理
@Component
//2.标识该类为AOP切面,Spring容器默认不能识别切面注解,需要手动配置
@Aspect
public class SpringAOP {
/*
* 切入点
* within:
* within(com.jt.*.DeptServiceImpl) 一级包下的类
* within(com.jt..*.DeptServiceImpl) ..代表多级包下的类
* within(com.jt..*) 包下的所有的类
* execution(返回值类型 包名.类名.方法名(参数列表))
* execution(* com.jt..*.DeptServiceImpl.add*()) 返回值类型任意的, com.jt下的所有包中的DeptServiceImpl的类的add开头的方法 ,并且没有参数.
* execution(* com.jt..*.*(..)) 返回值类型任意,com.jt包下的所有包的所有类的所有方法 任意参数.
* execution(int com.jt..*.*(int))
* execution(Integer com.jt..*.*(Integer))
* 注意: 在Spring表达式中没有自动拆装箱功能! 注意参数类型
* @annotation(包名.注解名)
* @Before("@annotation(com.jt.anno.Cache)") 只拦截特定注解的内容.
*/
//1.定义before通知
@Before("bean(deptServiceImpl)")
//@Before("within(com.jt.service.DeptServiceImpl)")
//@Before("within(com.jt..*)")
//@Before("execution(* com.jt..*.DeptServiceImpl.add*())")
//@Before("@annotation(com.jt.anno.Cache)")
public void before() {
System.out.println("我是before通知");
}
}
1.6.1 关于@annotation切入点相关代码
- 在测试的时候只有被注解标识的方法才会有通知方法
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)//控制注解的生命周期
@Target({ElementType.METHOD,ElementType.TYPE})//注解的作用对象 注解对方法有效 对类有效
public @interface Cache {
}
@Override
@Cache //被注解标识
public void updateDept() {
System.out.println("更新部门信息");
}
1.7 关于通知方法测试
1.7.1 抽取切入点表达式
如下代码,两个方法都相对注解进行拦截,这样显得代码很冗余
所以我们可以抽取切入点表达式,代码如下:
//定义切入点表达式
@Pointcut("@annotation(com.jt.anno.Cache)")
public void pointcut(){
}
@Before("pointcut()")
public void before() {
System.out.println("我是before通知");
}
@AfterReturning("pointcut()")
public void afterReturning(){
System.out.println("我是afterReturning通知");
}
1.7.2 @Before
- 前置通知,在目标方法执行之前执行
- 一般用来记录程序在方法执行前的状态
//1.定义before通知
/**
* Spring为了AOP动态获取目标对象及方法中的数据,则通过joinPoint对象进行数据的传递.
* getSignature : 方法签名 获取方法的参数
*/
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
System.out.println("获取目标对象的类型:" + joinPoint.getTarget().getClass());
System.out.println("获取目标对象类名:" + joinPoint.getSignature().getDeclaringTypeName());
System.out.println("获取目标对象方法名:" + joinPoint.getSignature().getName());
System.out.println("获取方法参数:" + Arrays.toString(joinPoint.getArgs()));
System.out.println("我是before通知");
}
- 运行结果
1.7.3 @AfterReturning
- 在目标方法执行之后执行
- 一般用来监控方法的返回值,进行日志的记录
- 在DeptService和DeptServiceImpl中添加测试方法
- 编辑AOP配置类
//2.定义afterReturning通知
/**
* pointcut:关联的切入点表达式
* returning:将方法的返回值通过形参result进行传递
* 如果参数中需要添加joinPoint对象时,参数必须位于第一位
* Spring在进行参数赋值时,采用index[0]下标的方式赋值
*/
@AfterReturning(pointcut = "pointcut()",returning = "result")
public void afterReturning(JoinPoint joinPoint,Object result) {
System.out.println("我是afterReturning通知");
System.out.println("用户的返回值结果:"+result);
}
- 运行结果
- 如果参数反了则会报以下错误
1.7.4 @AfterThrowing
- 当目标方法执行时,抛出异常时可以进行记录.
- 在DeptService和DeptServiceImpl中添加测试方法
- 编辑AOP配置类
//3.定义afterReturning通知
/**
* throwing = "e" 动态接受程序运行时的报错信息,利用异常通知进行记录
*/
@AfterThrowing(pointcut = "pointcut()",throwing = "e")
public void afterThrowing(Exception e) {
System.out.println("我是afterReturning异常通知");
System.out.println("获取异常信息"+e.getMessage());
System.out.println("获取异常的类型"+e.getClass());
}
- 测试结果
1.7.5 @After
- 方法执行之后执行
编辑AOP配置类
//4.定义after通知
@After("pointcut()")
public void after() {
System.out.println("我是after通知");
}
- 测试结果
1.7.6 @Around
- 在目标方法执行前后都要执行
- 实际作用可以控制目标方法是否执行.
- 在DeptService和DeptServiceImpl中添加测试方法
- 编辑AOP配置类
//5.定义afterReturning通知
/**
* 关于环绕通知的说明
* 作用: 可以控制目标方法是否执行.
* 参数: ProceedingJoinPoint 通过proceed方法控制目标方法执行.
* 注意事项: ProceedingJoinPoint is only supported for around advice
* @return
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) {
Object result = null;
try {
System.out.println("环绕通知开始");
//1.执行下一个通知 2.执行目标方法 3.接收返回值
Long start = System.currentTimeMillis();
result = joinPoint.proceed();
Long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start));
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("环绕通知结束");
return result;
}
- 测试结果
1.8 关于通知方法的执行顺序
- 执行around开始
- 执行before
- 执行目标方法
- 执行afterReturning
- 执行afterThrowing
- 执行after
- 执行around通知结束
1.9 关于Order注解说明
- 如果是多个切面,可以用于控制切面的执行顺序,该注解只可以添加在类上使用
- 在DeptService和DeptServiceImpl中添加测试方法
- 准备两个切面类
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class Before1 {
@Before("@annotation(com.jt.anno.Cache)")
public void before(){
System.out.println("我是切面A");
}
}
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class Before2 {
@Before("@annotation(com.jt.anno.Cache)")
public void before(){
System.out.println("我是切面B");
}
}
- 编辑切面配置类
- 测试结果
2 Aop案例-缓存控制
2.1 业务说明
- 用户有一个缓存的集合 static Map<k:v> key=id号 value=User对象 根据id将User对象进行了缓存
- 当用户第二次根据Id查询用户时,如果缓存中有数据,则直接返回
2.2 业务分析
1).首选用AOP方式实现
2).通知方法: 使用环绕通知
3).切入点表达式: execution(…)
2.3 代码实现
- User类
public class User {
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
- UserService接口
import com.jt.pojo.User;
public interface UserService {
//根据user对象查询有效信息
void findUser(User user);
}
- UserServiceImpl类
import com.jt.pojo.User;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService{
@Override
public void findUser(User user) {
System.out.println("从数据库中查询数据"+user);
}
}
- SpringConfig配置类
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan("com.jt")
@EnableAspectJAutoProxy//创建代理对象
public class SpringConfig {
}
- SpringAOP配置类
import com.jt.pojo.User;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
@Aspect
public class SpringAOP {
private static Map<Integer, User> map = new HashMap();
/**
* 需求: 用户第一次查询走目标方法
* 用户第二次查询走缓存 不执行目标方法
* 判断依据: 如何判断用户是否为第一次查询?
* 通过map集合进行判断 有数据 证明不是第一次查询
* 执行步骤:
* 1.获取用户查询的参数
* 2.判断map集合中是否有该数据.
* true: 从map集合中get之后返回
* false: 执行目标方法,之后将user对象保存到Map中
*/
//切入点表达式: 拦截service包中的所有方法
@Around("execution(* com.jt.service..*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = null;
//1.获取目标对象的参数
Object[] args = joinPoint.getArgs();
//2.强制类型转化为对象
User user = (User) args[0];
//3.判断map集合中是否有该数据 user的Id是唯一标识
int id = user.getId();
if (map.containsKey(id)) {
//map中有数据
System.out.println("AOP缓存执行");
//将获取的数据返回
return map.get(id);
} else {
//map中没有数据 执行目标方法
result = joinPoint.proceed();
//将user对象保存到Map中
map.put(id, user);
System.out.println("AOP执行目标方法");
}
return result;
}
}
- 测试类
import com.jt.config.SpringConfig;
import com.jt.pojo.User;
import com.jt.service.UserService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class TestSpring {
@Test
public void test01(){
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
UserService userService = context.getBean(UserService.class);
User user = new User();
user.setId(100);
user.setName("缓存");
//1.第一次查询走数据库
userService.findUser(user);
//2.第二次查询走缓存
userService.findUser(user);
}
}
- 运行结果