SSM 框架学习: Spring AOP 面向切面编程 Published on Mar 1, 2024 in 日常 with 0 comment Java SpringFramework ### 1. 场景设定和问题复现 #### 1.1 声明一个接口 ```java public interface Calculator { // 定义运算的标准接口 int add(int i, int j); int sub(int i, int j); int mul(int i, int j); int div(int i, int j); } ``` #### 1.2 编写接口的实现类 ```java public class CalculatorImpl implements Calculator{ @Override public int add(int i, int j) { return i + j; } @Override public int sub(int i, int j) { // System.out.println("i=" + i); return i - j; } @Override public int mul(int i, int j) { return i * j; } @Override public int div(int i, int j) { return i / j; } } ``` 这个时候,我们发现一个问题。如果我希望在每次运算结束和开始之前,输出某些日志内容,那我就需要在上面的代码段中,编写若干 `System.out.println(...)`语句,非常繁琐。 #### 1.3 代码问题分析 a、代码缺陷 - 对核心业务功能有干扰,导致在开发核心业务时分散了精力。 - 附加功能代码重复,分散在各个业务功能方法中,冗余而且不方便进行统一维护。 b、解决思路 - 核心就是解耦,我们需要把附加功能从业务功能代码中抽取出来。然后,将重复的代码统一提取,并且动态插入到每个业务方法中。 c、解决问题的困境 - 提取重复的附加功能到一个类中,我们可以实现。但是如何将代码插入到方法中,则需要引用新的技术。 ### 2. 解决方法代理模式 代理模式是 23 种设计模式中的一种,属于结构型模式。它的作用是通过一提供一个代理类,让我们在调用目标方法的时候,不再直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来实现解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一期,有利于后续的统一维护。 #### 2.1 静态代理技术 主动创建代理类: ```java public class CalculatorStaticPorxy implements Calculator{ private Calculator target; public CalculatorStaticPorxy(Calculator target){ this.target = target; } @Override public int add(int i, int j) { System.out.println("i="+i+"j="+j); int addResult = target.add(i,j); System.out.println("result=" + addResult); System.out.println(addResult); return addResult; } @Override public int sub(int i, int j) { System.out.println("i="+i+"j="+j); int subResult = target.sub(i,j); System.out.println("result=" + subResult); System.out.println(subResult); return subResult; } @Override public int mul(int i, int j) { System.out.println("i="+i+"j="+j); int mulResult = target.add(i,j); System.out.println("result=" + mulResult); System.out.println(mulResult); return mulResult; } @Override public int div(int i, int j) { System.out.println("i="+i+"j="+j); int divResult = target.add(i,j); System.out.println("result=" + divResult); System.out.println(divResult); return divResult; } } ``` 使用代理,调用目标方法需要首先经过代理,代理对象调用目标方法,然后目标方法把返回值返回给代理对象,最后代理对象把返回值返回给最初的调用者: ```java public class UseAop { public static void main(String[] args){ Calculator target = new CalculatorImpl(); // proxy Calculator proxy = new CalculatorStaticPorxy(target); proxy.add(2,3); } } ``` #### 2.2 动态代理技术 动态代理的技术分类: - JDK 动态代理:JDK 原生的实现方式,需要被代理的目标类必须实现接口。它会根据目标类的接口动态生成一个代理对象,代理对象和目标对象有相同的接口(原生)。 - cglib:通过继承被代理的目标类实现代理,所以不需要目标类实现接口(第三方,已经融入 spring )。 但是,无论使用何种方式,编程的工作都会比较繁琐。因而,在开发中,我们可以直接使用 spring AOP 框架,简化代理的实现。 ### 3. 面向切面编程思维 AOP #### 3.1 简要介绍 AOP 可以说是 OOP (面向对象编程)的补充和完善。OOP 允许开发者定义纵向的关系,但不适合横向的关系。而 AOP 技术恰恰相反,它采用了一种称为 “横切”的技术,剖开封装对象的内部,并将那些影响了多个类的公共行为封装到一个可重用的模块,并命名为 Aspect,即切面。简单来说,就是把那些与业务无关,却为业务模块所共同调用的逻辑和责任封装起来,便于减少系统的重复代码,降低耦合度,有利于未来的可操作性和可维护性。 主要的应用场景: - 日志记录。 - 事务处理,在数据库操作中使用事务可以保证数据的一致性。 - 安全控制:在系统中某些需要安全控制的操作,如登录、修改密码、授权时使用 AOP 进行控制。 - 性能监控。 - 异常处理:系统可能出现各类异常情况,如空指针异常等,在方法执行过程中,可以用 AOP 处理,如记录日志,发送邮件等。 - 缓存控制,实现访问速度。 AOP 的术语名词介绍: (1)横切关注点:从每个方法中抽取出来的同一类的非核心业务。 (2)通知(增强):每个横切关注点上要做的事情都要写一个方法来实现,这样的方法叫通知方法。 (3)连接点 joinpoint : 指那些被拦截到的点。 (4)切入点:定位连接点的方式,可以理解为被选中的连接点。 (5) 切面:切入点和通知的结合,是一个类。 (6)目标:被代理的目标对象。 (7)代理:向目标对象应用通知之后创建的代理对象。 (8)织入:指把通知应用到目标上,生成代理对象的过程,spring 采用运行期织入的模式。 #### 3.2 Spring AOP 基于注解方式的实现和细节 目标:横向插入增强代码。 需求:给计算的业务类,添加日志。 ##### 3.2.1 Spring AOP 初步实现 1、加入依赖 ```xml org.springframework spring-aop 5.3.1 org.springframework spring-aspects 5.3.1 ``` 2、准备接口和实现类 ```java public interface Calculator { // 定义运算的标准接口 int add(int i, int j); int sub(int i, int j); int mul(int i, int j); int div(int i, int j); } @Component public class CalculatorImpl implements Calculator{ @Override public int add(int i, int j) { return i + j; } @Override public int sub(int i, int j) { // System.out.println("i=" + i); return i - j; } @Override public int mul(int i, int j) { return i * j; } @Override public int div(int i, int j) { return i / j; } ``` 3、创建配置文件 ```xml ``` 4、测试一下看看效果 ```java @SpringJUnitConfig(locations = "classpath:spring.xml") public class SpringAoPTest { @Autowired private Calculator calculator; @Test public void test(){ System.out.println(calculator.add(1,1)); } } ```  5、定义增强类 ```java package org.alen.advice; // 增强类的内部存储增强代码,具体定义几个方法,根据插入的位置决定。 // 使用注释配置,指定插入目标方法的位置 import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.stereotype.Component; // 配置切点表达式 [选中插入的方法 切点] // 补全注解,加入 IoC 容器,配置切面 @Aspect = 切点 + 增强 // 最后,开启 aspect 注解的支持 @EnableAspectJAutoProxy /** * 前置 @Before * 后置 @AfterReturning * 异常 @AfterThrowing * 最后 @After * 环绕 @Around * */ @Component @Aspect @EnableAspectJAutoProxy public class LogAdvice { @Before("execution(* org.alen.*.*(..))") public void start(){ System.out.println("方法开始"); } @After("execution(* org.alen.*.*(..))") public void after(){ System.out.println("方法结束"); } @AfterThrowing("execution(* org.alen.*.*(..))") public void error(){ System.out.println("方法报错"); } } ``` 6、重新测试一下,发现在方法执行过程中自动补全了相应日志内容的输出。  ##### 3.2.2 获取切点的详细信息 概括来说,一般切点的配置大概分为以下几个步骤: 1. 定义方法; 2. 使用注解指定相应的位置; 3. 配置切点表达式选中方法; 4. 切面和IoC配置; 5. 开启aspectj注解的配置; 但是如果想要获取切点的详细信息,如修饰符类型、返回值等,则可以参考以下的代码实现: ```java import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.stereotype.Component; import java.lang.reflect.Modifier; // 定义方法 // 使用注解指定相应的位置 // 配置切点表达式选中方法 // 切面和IOC配置 // 开启aspectj注解的配置 /* * TODO : 增强方法中获取目标方法信息 * 1. 全部增强方法中,获取目标方法的信息(方法名、参数、访问修饰符等) * JoinPoint joinPoint * 2. 返回的结果 - 仅在 AfterReturning 有效 * 形参里加入 Object result 并修改注解 * 3. 获取异常的信息 - @AfterThrowing * 形参里加入 Throwable 并修改注解 * */ @Component @Aspect @EnableAspectJAutoProxy public class MyAdvice { @Before("execution(* org.alen.*.*(..))") public void before(JoinPoint joinPoint){ // 1、获取方法所取的类的信息 String simpleName = joinPoint.getTarget().getClass().getSimpleName(); // 2、获取方法名称 String name = joinPoint.getSignature().getName(); // 3、获取参数列表 Object[] args = joinPoint.getArgs(); // 4、获取修饰符类型 int modifiers = joinPoint.getSignature().getModifiers(); String s = Modifier.toString(modifiers); } // 获取返回结果 @AfterReturning(value = "execution(* org.alen.*.*(..))", returning = "result") public void afterReturning(JoinPoint joinPoint, Object result){ } @After("execution(* org.alen.*.*(..))") public void after(JoinPoint joinPoint){ } @AfterThrowing(value = "execution(* org.alen.*.*(..))", throwing = "throwable") public void afterThrowing(JoinPoint joinPoint, Throwable throwable){ } } ``` ##### 3.2.3 切点表达式语法 1、切点表达式的作用 AOP 切点表达式是一种用于指定切点的语言,它可以通过定义匹配规则,来选择需要被切入的目标对象。 2、切点表达式语法 - 固定语法 execution (切点表达式 1 2 3.4.5(6)) - 1、访问修饰符 public / private - 2、方法的返回参数类型 String / void / int...如果不考虑访问修饰符和返回值,那么这两位整合成一起写成一个 * 即可。注意,不能出现一个考虑一个不考虑的情况。 - 3、包的位置,具体包例如 com.alen.service.impl 或者单层模糊:com.alen.service.* 或者多层模糊:com..impl,值得注意的是 .. 不能作为开头,但是可以携程 *..impl。 - 4、类的名称,如 CalcImpl 模糊写法为 *,也可以部分模糊,例如*Impl。 - 5、方法名称,语法和类名一样。 - 6、(6)内表示参数列表,表示没有参数()、有具体参数的例如 (String)、模糊参数(..),这部分适应于重载的情况。 ##### 3.2.4 统一切点管理 切点表达式的提取,便与统一管理,具体实现可以参考以下代码实现。 ```java /* * TODO 切点表达式的提取 * 1、当前类中提取,首先定义一个空方法,注解 @Pointcut 然后在增强注解中引用切点表达式的方法 * */ public class MyAdvice { @Pointcut("execution(* org.alen.*.*(..))") public void pc(){} @Before("pc()") public void before(JoinPoint joinPoint){} ``` ```java * TODO 切点表达式的提取 * 2、创建一个存取切点的类,单独维护切点表达式 * */ import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; // 存放切点 @Component public class MyPointCut { @Pointcut("execution(* org.alen.*.*(..))") public void pc(){} } @Component @Aspect @EnableAspectJAutoProxy public class LogAdvice { @Before("org.alen.MyPointCut.pc()") public void start(){ System.out.println("方法开始"); } } ``` ##### 3.2.5 切面优先级设定 相同的目标方法上面存在多个切面时,可以通过切面的优先级控制。 方法:通过` @Order `注解控制切面的优先级。 - `@Order(较小的数)`:优先级高。 - `@Order(较大的数)`:优先级低。 本文由 Alen 创作,采用 知识共享署名4.0 国际许可协议进行许可本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名最后编辑时间为: Mar 19, 2024 at 05:38 pm