1. Spring AOP
1.1 底层实现就是动态代理,AOP即对动态代理的优化
1.2 AOP介绍
-
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
-
知识扩展:
- 面向过程变成 C语言
- 半面向对象程序设计 C++
- 面向对象编程技术 Java
-
编程方式:
- 面向接口编程
- 面向切面编程
-
总结:AOP(面向切面编程)主要的目的是利用动态代理模式,降低程序耦合度,同时扩展业务功能方法。
1.3 关于AOP专业术语(难点)
1.3.1 连接点:用户可以被扩展的方法
1.3.2 切入点:用户实际扩展的方法
1.3.3 通知:扩展方法的具体实现
1.3.4 切面:将通知应用到切入点的过程
1.4 通知的类型(必会)
1.4.1 before:在目标方法执行之前执行
1.4.2 afterReturning:在目标方法执行之后,返回时执行
1.4.3 afterThrowing:在目标方法执行之后,抛出异常时执行
1.4.4 after:无论程序是否执行成功,都要最后执行的通知
1.4.5 around:在目标方法执行前后,都要执行的通知(完美的体现了动态代理的模式),功能最为强大,只有环绕通知可以控制目标方法的执行
1.4.6 关于通知方法总结:
- 环绕通知是处理业务的首选(控制方法是否执行),可以修改程序的执行轨迹。
- 另外的四大通知一般用来做程序的监控(监控系统),只做记录。
1.5 切入点表达式
1.5.1 概念:当程序满足切入点表达式,才能进入切面,执行通知方法
- bean(“bean的ID”):被Spring容器管理的对象,根据bean的id进行拦截,只能匹配一个
- within(“包名.类名”)可以使用通配符(*和?),能匹配多个
- 粒度:上述的切入点表达式,粒度是类级别的,即粗粒度
- execution(返回值类型 包名.类名.方法名(参数列表))
- 粒度:控制的是方法参数级别,所以粒度较细 最常用的
- @annotation(包名.注解名):只拦截注解
- 粒度:注解是一种标记,根据规则标识某个方法/属性/类,细粒度
1.6 AOP入门案例
1.6.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>org.example</groupId>
<artifactId>spring_demo_9_aop</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!--Spring核心包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.6</version>
</dependency>
<!--引入SpringBean-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.3.6</version>
</dependency>
<!--引入context包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.6</version>
</dependency>
<!--引入表达式jar包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.3.6</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>5.3.6</version>
</dependency>
<!--切面包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.6</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.6</version>
</dependency>
<!--当下所有的jar包都需要手动添加,并且需要确定依赖的关系,对初学者不友好-->
</dependencies>
</project>
1.6.2 配置切面类
-
切面 = 切入点表达式 + 通知方法
// AOP需要被Spring容器管理 @Component // 标识该类为AOP的切面,切面注解 Spring容器默认不能识别切面的注解,需要手动配置 @Aspect public class SpringAOP { // 定义before通知 @Before("bean(deptServiceImpl)") public void before(){ System.out.println("我是before通知"); } }
-
编辑配置类
@Configuration @ComponentScan("cn.lz") // 启动AOP注解 实质:创建代理对象 // 默认启用JDK动态代理,目标对象没有实现接口时启用CGLIB代理 /** * JDK代理创建速度快,运行时稍慢 * CGLIB代理创建速度较慢,运行时更快 */ @EnableAspectJAutoProxy // @EnableAspectJAutoProxy(proxyTargetClass = true) // 强制使用CGLIB代理 public class SpringConfig { }
-
编辑测试代码
@Test public void test01(){ ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class); DeptService deptService = context.getBean(DeptService.class); deptService.addDept(); }
-
关于表达式的写法
// AOP需要被Spring容器管理 @Component // 标识该类为AOP的切面,切面注解 Spring容器默认不能识别切面的注解,需要手动配置 @Aspect public class SpringAOP { /** * 切入点表达式练习: * within: * 1.within(cn.lz.*.DeptServiceImpl) 一级包下的类 * 2.within(cn.lz..*.DeptServiceImpl) ..代表多级包下的类 * 3.within(cn.lz..*) 包下的所有类 * * execution(返回值类型 包名.类名.方法名(参数列表)): * 1.execution(* cn.lz..*.DeptServiceImpl.update*()) * 返回值类型是任意的,cn.lz下的所有包中的DeptServiceImpl的以add开头的方法,并且没有参数 * 2.execution(* cn.lz..*.*.*(..)) → execution(* cn.lz..*.*(..)) → execution(* cn.lz..*(..)) * 返回值类型任意,cn.lz包下的所有包的所有类的所有方法 任意参数 * 3.execution(int cn.lz..*.*(int)) * 4.execution(Integer cn.lz..*.*(Integer)) * 强调:在Spring表达式中没有任意的拆装箱功能,即int与Integer不同,注意参数类型 * * 注解形式:@annotation(包名.注解名) * @annotation(cn.lz.anno.Cache) * 只拦截特定注解的内容 */ // 定义before通知 // @Before("bean(deptServiceImpl)") // @Before("within(cn.lz..*)") // @Before("execution(* cn.lz..*.*(..))") @Before("@annotation(cn.lz.anno.Cache)") public void before(){ System.out.println("我是before通知"); } }
-
自定义注解
// 控制注解的生命周期 @Retention(RetentionPolicy.RUNTIME) // 注解的作用对象 方法有效 类有效 属性有效 @Target({ElementType.METHOD,ElementType.TYPE,ElementType.FIELD}) public @interface Cache { }
1.6.3 关于通知方法测试
-
抽取通知方法
// 定义切入点表达式 // @Pointcut("@annotation(cn.lz.anno.Cache)") // @Pointcut("bean(deptServiceImpl)") // @Pointcut("within(cn.lz..*)") // @Pointcut("execution(* cn.lz..*.add*())") @Pointcut("execution(* cn.lz..*.*())") public void pointcut(){ }
-
@Before作用
- 说明:前置通知,在目标方法执行之前执行
- 用途:如果需要记录程序在方法执行之前的状态,则使用前置通知
- 需求:
-
获取目标对象的类型
-
获取目标方法的名称
-
获取目标方法的参数
@Before("pointcut()") /** * JoinPoint 连接点信息 * Spring为了AOP中动态获取对象及方法中的数据,则通过JoinPoint对象进行数据的传递 * getSignature():方法签名,获取方法参数 */ 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通知"); }
-
-
@AfterReturning
-
说明:AfterReturning在目标方法执行之后执行
-
用途:用来监控方法的返回值,用来日志记录
-
在接口中添加方法
public interface DeptService { void addDept(); void updateDept(); // AOP中的测试方法 String after(Integer id); }
-
实现目标方法
@Cache public String after(Integer id) { return "Spring通知测试"; }
-
编辑AOP方法测试
/** * 记录方法的返回值 * pointcut:关联的切入点表达式 * returning:将方法的返回值,通过形参result进行传递 * @AfterReturning(pointcut = "pointcut()",returning = "result") */ @AfterReturning(pointcut = "pointcut()",returning = "result") public void afterReturning(JoinPoint joinPoint,Object result){ System.out.println("我是afterReturning通知"); System.out.println("用户的返回值结果:" + result); }
-
-
@AfterThrowing
-
作用:当目标方法执行时,抛出异常,可以使用AfterThrowing进行记录
-
编辑业务接口
public interface DeptService { void addDept(); void updateDept(); // AOP中的afterReturning测试方法 String after(Integer id); // AOP中的afterThrowing测试方法 void afterThrow(); }
-
编辑业务接口实现类
@Cache public void afterThrow() { System.out.println("用户执行目标方法"); // 手动抛出算数异常 int a = 1/0; }
-
编辑异常通知类型
@AfterThrowing(pointcut = "pointcut()",throwing = "e") public void afterThrowing(Exception e){ System.out.println("获取异常信息:" + e.getMessage()); System.out.println("获取异常的类型:" + e.getClass()); System.out.println("我是异常通知"); }
-
编辑测试方法
// 测试异常通知 @Test public void testThrow(){ ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class); DeptService deptService = context.getBean(DeptService.class); deptService.afterThrow(); }
-
-
@Around
-
规则:在目标方法执行前后都要执行
-
实际作用:可以控制目标方法是否执行
-
环绕通知学习
/** * 关于环绕通知的说明 * 作用:可以控制目标方法是否执行 * @param proceedingJoinPoint 通过proceed()方法控制目标方法执行 * 注意事项: ProceedingJoinPoint is only supported for around advice * @return */ @Around("pointcut()") public Object around(ProceedingJoinPoint proceedingJoinPoint) { Object result = null; System.out.println("环绕通知开始"); /** * proceed() * 1. 执行下一个通知 * 2. 执行目标该方法 * 3. 接收返回值 */ try { Long start = System.currentTimeMillis(); result = proceedingJoinPoint.proceed(); Thread.sleep(100); Long end = System.currentTimeMillis(); System.out.println("耗时:" + (end - start)); } catch (Throwable throwable) { throwable.printStackTrace(); } System.out.println("环绕通知结束"); System.out.println(); return result; }
-
1.6.4 关于Spring中AOP流程介绍
-
AOP名词解释
- 连接点:永和可以被扩展的方法 JoinPoint
- 切入点:用户实际扩展的方法(判断方法能否进入切面) pointcut
- 通知:扩展方法的具体实现 @Before/AfterReturning…
- 切面:将通知应用到切入点的过程,方法功能得到全部扩展的全部配置(切面 = 切入点表达式 + 通知方法)
-
关于知识点讲解
-
关于注解的说明
// 自定义注解 包括元注解 // 注解的作用: (配合AOP进行注解类型案例的训练)满足某些业务条件的处理 即标识 // 控制注解的生命周期 @Retention(RetentionPolicy.RUNTIME) // 注解的作用对象 方法有效 类有效 属性有效 @Target({ElementType.METHOD,ElementType.TYPE,ElementType.FIELD}) public @interface Cache { }
-
注解的使用
@Cache // 标识该方法要执行切面 public String after(Integer id) { return "Spring通知测试"; }
-
pointcut
-
说明:判断方法是否能进入切面,即IF判断
-
注解说明:@Pointcut 内部编辑切入点表达式
-
判断的依据
// 定义before通知 // @Before("bean(deptServiceImpl)") // @Before("within(cn.lz..*)") // @Before("execution(* cn.lz..*.*(..))") // @Before("@annotation(cn.lz.anno.Cache)")
-
-
1.6.5 关于通知方法的执行顺序
- 执行@Around通知开始
- 执行@Before通知
- 执行目标方法
- 执行@AfterReturning通知
- 执行@AfterThrowinging通知
- 执行@After通知
- 执行@Before通知
- 执行@Before通知结束
1.6.6 关于@Order注解说明
-
说明:如果是多个切面,如何控制切面执行顺序
-
定义切面a
@Component @Aspect @Order(2) public class Before1 { @Before("@annotation(cn.lz.anno.Cache)") public void before(){ System.out.println("我是切面a执行"); } }
-
定义切面b
@Component @Aspect /** * @Order * 通过数字控制程序的执行顺序 * 值越小,越先执行 */ @Order(1) public class Before2 { @Before("@annotation(cn.lz.anno.Cache)") public void before(){ System.out.println("我是切面b执行"); } }