JavaWeb - ⭐ AOP 面相切面编程原理及用户校验功能实战

一、概述

  1. 定义: AOP (Aspect Oriented Programming 面向切面编程) ,一种面向方法编程的思想

  2. 功能:管理 bean 对象的过程中,通过底层的动态代理机制对特定方法进行功能的增强或改变

  3. 实现方式:动态代理技术,即创建目标对象的代理对象,并对目标对象中的方法的功能进行增强的技术

  4. 优点

    1. 代码无侵入:不修改原有代码的基础上对原有方法的功能进行增强
    2. 减少重复代码:一次性对一类方法进行功能增强
    3. 提高开发效率
    4. 维护方便
  5. AOP 的依赖

    <dependency> 
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
  6. 代理方式

    • 没有切面的对象:不需要代理,Spring 会直接注入目标对象(原始类的实例)
    • **接口类型的目标对象:**Spring 使用 JDK 动态代理 创建代理对象
    • 非接口类型的目标对象 (如纯类):Spring 则使用 CGLIB(子类代理) 创建代理对象

二、核心概念

  1. JoinPoint : 连接点,客观可以被 Proxy 的方法(实际上所有的方法都是 JoinPoint,都可以被代理)
  2. PointCut : 切入点,主观需要被 Proxy 的方法(满足匹配条件的 JoinPoint),实际被 Advice 控制的方法
  3. Advice : 通知,共性的功能 (最终体现为一个方法),是对目标 PointCut 的相同处理逻辑
  4. Aspect : 切面,即 PointCut + Advice ,描述 "通知" 与 "切入点" 对应关系
  5. Target : Advice 作用的目标对象,PointCut 所在的类

Aspect 类 (AOP 类)

  1. 定义 : 切面,即 PointCut + Advice ,描述 "通知" 与 "切入点" 对应关系
  2. 功能:用于实现特定切面功能,动态代理某些目标类或目标方法的切面类
  3. 组成
    1. Aspect 类:通过 @Aspect 注解标注,告知 Spring 这是一个 AOP 类
    2. PointCut 切入点:通过 @PointCut 注解标注,指定代理的目标方法,减少 Advice 类型注解中的代码冗余
    3. Advice 方法:通过 Advice 注解标注(如 @Before、@After、@Around ),告知 Spring 这个 Aspect 方法将代理哪些目标方法

PointCut (切入点)

  1. 定义 : 切入点,主观需要被 Proxy 的方法(满足匹配条件的 JoinPoint),实际被 Advice 控制的方法
  2. 功能:用于指定 Aspect 代理的具体方法,抽取出公共切入点减少代码冗余(实际上也可以不单独声明,直接在 Advice 里面声明)
  3. 实现方式:通过 execution 和 annotation 两种方式声明
  4. 书写建议
    1. 建议抽取公共的 PointCut 进行复用(也可以不声明单独的 PointCut,而是在 Advice 方法上的注解中声明 PointCut)
    2. 尽量缩小切入点范围,(比如 : 尽量不用 .. 进行包名匹配,因为范围越大匹配效率越低)
    3. 基于接口描述切入点,而不是直接描述实现类,增强拓展性

execution 表达式

  1. 格式

    execution ( [访问修饰符] 返回值 [包名.类名.] 方法名(方法形参) [throws 异常] )
    
  2. 使用方法

    1. 将目标方法 (被代理的方法) 写到 execution 表达式中
    2. 在 Aspect 类中定义一个空参空返回值方法,给其加上 @PointCut( "execution ..." ) 注解
  3. 书写格式

    1. * :通配1个或0个单独的任意符号,可以通配返回值、包名、类名、方法名、任意类型的一个参数,或者包名、类名、方法名的一部分
    2. .. :通配多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
    3. || && ! :可以用逻辑运算符组合复杂的 PointCut 表达式
  • 示例

    @Pointcut("execution(* com.tlias.service.impl.DeptServiceImpl.*(..))")
    private void myPointCut();
    
    • 第一个 * :前面为任意类型的返回值
    • 包名: PointCut 所在的包名类名方法名
    • 最后一个 * :为任意方法
    • (..) :为任意形参

annotation 表达式

  1. 格式

    @annotation(包名.类名.注解名)
    
  2. 使用方法

    1. 在目标方法 (被代理的方法) 上加上自定义的注解 @MyPointCut
    2. 在 Aspect 类中定义一个空参空返回值方法,给其加上 @PointCut( "@annotation(com.xxx.aop.MyPointCut)" ) 注解
  • 示例
    1. 目标:自定义 annotation,被这个 annotation 标注的方法都会被 Aspect 类代理

    2. annotation

      @Target(ElementType.METHOD)                  // 设置注解使用的位置,ElementType.METHOD 表示这个注解用来标注方法
      @Retention(RetentionPolicy.RUNTIME)          // 设置注解生效的时间,RetentionPolicy.RUNTIME 表示运行时生效
      public @interface MyPointCut {
      }
      
    3. PointCut

      @Slf4j
      @Component
      @Aspect
      public class MyAspect{
      	@Pointcut("execution(* com.tlias.service.impl.DeptServiceImpl.*(..)) && @annotation(com.tlias.annotation.MyPointCut)")
      	private void myPointCut();
      	
      	@Around("myPointCut()")
      	public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
      		log.info("around before ...");
      		Object result = proceedingJoinPoint.proceed();
      		log.info("around after ...");
      		return result;
      	}
      	
      }
      

Advice (通知)

  1. 定义:通知(Advice)是自定义的处理逻辑,当被 PointCut 选中的方法在指定时机(Before/Around/After)执行时会触发对应的 Advice 方法

  2. 功能 : 明确 PointCut 指定的方法的切面处理逻辑,此方法中会通过 ProceedingJoinPoint 类来代理目标方法

  3. 基本类型

    注解类型 说明
    @Before 前置通知,Advice 方法在目标方法开始前执行
    @Around 环绕通知,Advice 方法在目标方法前后都会执行,期间需要通过 proceed() 方法主动调用目标方法
    @After 后置通知,Advice 方法在目标方法结束后执行,有无异常都会执行
    @AfterReturning 返回后通知,Advice 方法在目标方法后被执行,有异常则不会执行
    @AfterThrowing 异常后通知,Advice 方法在目标方法发生异常后执行
  4. 对比总结

    切面注解 运行时机 可修改目标方法行为 可获取返回值 可获取异常
    @Before 目标方法执行前
    @Around 方法执行前、执行后均可运行 ☑️ ☑️ ☑️
    @After 目标方法执行后(无论成功或失败)
    @AfterReturning 目标方法成功返回后 ☑️
    @AfterThrowing 目标方法抛出异常时 ☑️
  5. 自定义优先级:通过 @Order(x) 给 Advice 指定优先级,注解中 x 越大,则 Before 越先执行,After 越后执行

  • 示例

    @Slf4j
    @Component
    @Aspect
    public class MyAspect{
    
    		// 声明一个 PointCut 切入点, 用于后续 Advice 方法中的切入点复用
    		@Pointcut("execution(* com.tlias.service.impl.DeptServiceImpl.*(..)) && @annotation(com.tlias.annotation.MyPointCut)")
    		private void myPointCut();
    	
    		@Before("myPointCut()")
    		public void before(){
    				log.info("before...");
    		}
    	
    		@Around("myPointCut()")
    		public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
    				log.info("around before ...");
    				Object result = proceedingJoinPoint.proceed();
    				log.info("around after ...");
    				return result;
    		}
    	
    		@After("myPointCut()")
    		public void after(){
    				log.info("after ...");
    		}
    	
    		@AfterReturning("myPointCut()")
    		public void afterReturning(){
    				log.info("afterReturning ...");
    		}
    	
    		@AfterThrowing("myPointCut()")
    		public void afterThrowing(){
    				log.info("afterThrowing ...");
    		}
    
    }
    

JoinPoint(连接点)

  1. 定义

    1. JoinPoint:目标对象中所有可以被增强的方法。这些方法在运行时都可以被 AOP 框架拦截并添加额外的处理逻辑
    2. ProceedingJoinPoint:继承自 JoinPoint 类,专门给环绕通知 (Around) 使用的类,用于代理原有方法的类 (对被拦截方法的一个包装)
  2. 功能:拿到 ProceedingJoinPoint 类相当于拿到了原方法,可以调用 proceed() 方法执行原方法

  3. 常用方法

    方法 说明
    proceed() 执行被代理的方法
    getArgs() 获取传递给目标方法的参数
    getSignature() 获取被拦截方法的信息,如方法名、返回类型等(通过反射获取)
    getTarget() 获取被拦截方法所属的目标对象(Target Object)
    getThis() 获取代理对象(Proxy Object)
  • 示例

    @Around("execution (* com.tlias.service.*.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
    
    	// 获取目标对象类名
    	String className = joinPoint.getTarget().getClass().getName();
    	log.info("目标对象的类名:{}", className);
    
    	// 获取目标方法的方法名
    	String methodName = joinPoint.getSignature().getName();
    	log.info("目标方法的方法名:{}", methodName);
    
    	// 获取目标方法运行时传入的参数
    	Object[] args = joinPoint.getArgs();
    	log.info("目标方法运行时传入的参数:{}", Arrays.toString(args));
    
    	// 获取目标方法运行的返回值
    	Object result = joinPoint.proceed();
    	log.info("目标方法运行的返回值:{}", result);
    
    	// 最后一定要将结果返回回去,因为此时 around 函数代理了 JoinPoint 函数
    	// 不返回的话 controller 层拿不到返回结果
    	return result;
    }
    

三、执行流程

@Around 流程

  1. 声明切入点:Aspect 类声明将会代理的目标方法(根据 @Around 注解配置的 PointCut 信息决定代理哪些方法)
  2. 依赖注入:Spring 通过动态代理技术,注入代理对象 XxxServiceProxy(如果没有配置 Aspect,则直接注入目标对象 XxxService)
  3. 接收请求:服务器接收客户端 "调用了被代理的方法(目标方法)" 的请求
  4. 代理方法:XxxServiceProxy 代理类接收请求,通过 ProceedingJoinPoint 参数获取 PointCut 方法的上下文信息
  5. 执行前置代理逻辑:运行 Aspect 前置代码部分 ( Advice 部分 )
  6. 执行目标方法:通过 joinPoint.proceed() 执行被代理的方法,同时用 Object result 接收被代理方法的返回值
  7. 执行后置代理逻辑:运行 Aspect 后置代码部分 ( Advice 部分 )
  8. 返回结果值

@Before / @After 流程

  1. 声明切入点:Aspect 类声明将会代理的目标方法(根据 @Before、@After 等注解配置的 PointCut 信息决定代理哪些方法)
  2. 依赖注入:Spring 通过动态代理技术,注入代理对象 XxxServiceProxy(如果没有配置 Aspect,则直接注入目标对象 XxxService)
  3. 接收请求:服务器接收客户端 "调用了被代理的方法(目标方法)" 的请求
  4. 代理方法:XxxServiceProxy 接收请求
  5. 运行 Before Advice:运行 @Before 注解的方法逻辑
  6. 执行目标方法:代理对象自动执行目标方法
  7. 运行 After Advice:运行 @After 注解的方法逻辑
  8. 返回结果值

四、使用场景

  1. 记录操作日志
  2. 权限控制
  3. 事务管理
  4. 自动填充数据库字段

五、示例

方法用时统计

  1. 目标 : 统计各个业务层方法执行耗时
  2. 步骤
    1. 导入依赖:在 pom.xml 中导入 AOP 依赖

      <dependency>
      	<groupId>org.springframework.boot</groupId>
      	<artifactId>spring-boot-starter-aop</artifactId>
      </dependency>
      
    2. 创建包和类 : com.tlias.aop.TimeAspect

    3. 编写 AOP 程序

      @Aspect    // 声明这是一个AOP类
      @Component
      public class TimeAspect{
      
      	@PointCut("execution (* com.tlias.service.*.*(..))")
      	private void myPointCut(){}
      
      	@Around("myPointCut()")    // 声明这个Aspect将代理哪些方法
      	public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
      		long begin = System.currentTimeMills();                //1. 记录开始时间
      		Object result = proceedingJoinPoint.prceed();          //2. 调用被代理的方法
      		long end = System.currentTimeMills();                	 //3. 记录结束时间
      		log.info(prceedingJoinPoint.getSignature() + "方法执行共耗时" + end-begin + "毫秒");    		//4. 计算时间差并记录
      		return result;
      	}
      

用户权限校验

  1. 目标 : 保证 @authCheck 注解标注的方法都必须具备特定权限才可调用该方法
  2. 步骤
    1. 导入依赖:在 pom.xml 中导入 AOP 依赖

      <dependency>
      	<groupId>org.springframework.boot</groupId>
      	<artifactId>spring-boot-starter-aop</artifactId>
      </dependency>
      
    2. 编写注解(com.yupi.yudada.annotation.AuthCheck)

      @Target(ElemType.METHOD)
      @Retention(RetentionPolicy.RUNTIME)
      public @interface AuthCheck {
      		
      		/**
      		* 被 AuchCheck 标注就必须具备的用户权限等级
      		* 示例: @AuthCheck("ADMIN") 表示当前方法必须拥有管理员权限才可执行
      		**/
      		String mustRole() default "";
      
    3. 编写 AOP 程序(com.yupi.yudada.aop.AuthInterceptor)

      @Aspect    // 声明这是一个AOP类
      @Component
      public class AuthInterceptor{
      
      		@Resource
      		private UserService userService;
      		
      		@Around("@annotation(authCheck)")
      		public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {
      				String mustRole = authCheck.mustRole();
      				RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
      				HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
      				
      				
      		}
      
      }
      
相关推荐
数据小爬虫@2 小时前
Java爬虫实战:深度解析Lazada商品详情
java·开发语言
咕德猫宁丶2 小时前
探秘Xss:原理、类型与防范全解析
java·网络·xss
songroom2 小时前
Rust: offset祼指针操作
开发语言·算法·rust
code04号2 小时前
C++练习:图论的两种遍历方式
开发语言·c++·图论
煤泥做不到的!3 小时前
挑战一个月基本掌握C++(第十一天)进阶文件,异常处理,动态内存
开发语言·c++
F-2H3 小时前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
苹果酱05673 小时前
「Mysql优化大师一」mysql服务性能剖析工具
java·vue.js·spring boot·mysql·课程设计
_oP_i4 小时前
Pinpoint 是一个开源的分布式追踪系统
java·分布式·开源
mmsx4 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
bryant_meng4 小时前
【python】OpenCV—Image Moments
开发语言·python·opencv·moments·图片矩