Java Web —— 第十天(AOP切面编程)

AOP基础

AOP概述

AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),其实就是面向特定方法编程场景

案例部分功能运行较慢,定位执行耗时较长的业务方法,此时需要统计每一个业务方法的执行耗时

实现:

动态代理面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程

Spring AOP快速入门

步骤: 统计各个业务层方法执行耗时

1.导入依赖:在pom.xml中导入AOP的依赖

XML 复制代码
        <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>

2.编写AOP程序:针对于特定方法根据业务需要进行编程

java 复制代码
package com.example.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
 * @author hyk~
 */

@Slf4j
@Component //当前类交给IOC容器管理
@Aspect //当前类是一个AOP类
public class TimeAspect {
    @Around("execution(* com.example.service.*.*(..))") //切入点表达式
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        //1.记录开始时间
        long begin = System.currentTimeMillis();

        //2.调用原始方法运行
        Object result = joinPoint.proceed();

        //3.记录结束时间,计算方法执行耗时
        long end = System.currentTimeMillis();

        log.info(joinPoint.getSignature()+"方法执行耗时:{}ms",end-begin);

        return result;

    }

}

3.运行测试

场景

记录操作日志

权限控制

事务管理

...

优势

代码无侵入

减少重复代码

提高开发效率

维护方便

AOP核心概念

连接点 :JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)

通知 :Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)

切入点: PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用

切面:Aspect,描述通知与切入点的对应关系 (通知+切入点)

目标对象: Target,通知所应用的对象

AOP执行流程

AOP进阶

通知类型

1.@Around: 环绕通知,此注解标注的通知方法在目标方法前、后都被执行

2.@Before:前置通知,此注解标注的通知方法在目标方法前被执行

3.@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行

4.@AfterReturning :返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执

5.@AfterThrowing :异常后通知,此注解标注的通知方法发生异常后执行

java 复制代码
package com.itheima.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * 使用 Lombok 提供的 @Slf4j 注解,自动为该类生成日志记录器。
 */
@Slf4j
/**
 * 将该类标记为 Spring 管理的组件,使其能够被 Spring 自动检测并注入。
 */
@Component
/**
 * @Aspect 注解将该类声明为一个切面类,用于定义切点和通知。如果解开此注解,MyAspect1 类将作为一个切面生效。
 */
// @Aspect
public class MyAspect1 {

    /**
     * 定义一个切点(Pointcut),表示在哪些方法或类上应用通知。
     * 此处的切点表达式为 execution(* com.itheima.service.impl.DeptServiceImpl.*(..)),
     * 表示切点是 com.itheima.service.impl.DeptServiceImpl 类中的所有方法。
     */
    @Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void pt() {}

    /**
     * 前置通知(Before advice),在目标方法执行之前执行。
     * 当 DeptServiceImpl 类中的任意方法被调用时,会先执行此通知。
     */
    @Before("pt()")
    public void before() {
        log.info("before ...");
    }

    /**
     * 环绕通知(Around advice),在目标方法执行前后分别执行。
     * 该方法可以控制目标方法是否执行,甚至可以在执行前后添加自定义逻辑。
     *
     * @param proceedingJoinPoint 用于控制目标方法的执行
     * @return 目标方法的返回值
     * @throws Throwable 可能抛出的异常
     */
    @Around("pt()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("around before ...");

        // 调用目标对象的原始方法并获取其返回值
        Object result = proceedingJoinPoint.proceed();

        log.info("around after ...");
        return result;
    }

    /**
     * 后置通知(After advice),在目标方法执行完毕后执行,无论方法是否抛出异常。
     */
    @After("pt()")
    public void after() {
        log.info("after ...");
    }

    /**
     * 返回通知(AfterReturning advice),在目标方法成功执行并返回结果后执行。
     * 如果方法抛出异常,则不会执行此通知。
     */
    @AfterReturning("pt()")
    public void afterReturning() {
        log.info("afterReturning ...");
    }

    /**
     * 异常通知(AfterThrowing advice),在目标方法抛出异常后执行。
     */
    @AfterThrowing("pt()")
    public void afterThrowing() {
        log.info("afterThrowing ...");
    }
}

正常运行

有异常时

注意事项

@Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行

@Around环绕通知方法的返回值,必须指定为object,来接收原始方法的返回值

@PointCut

该注解的作用是将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可

通知顺序

当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行

执行顺序

1.不同切面类中,默认按照切面类的类名字母排序:

目标方法前 的通知方法:字母排名靠前的先执行

目标方法后 的通知方法:字母排名靠前的后执行

2.用 **@Order(数字)**加在切面类上来控制顺序

目标方法前的通知方法:数字小的先执行

目标方法后的通知方法:数字小的后执行

切入点表达式

切入点表达式:描述切入点方法的一种表达式

作用:主要用来决定项目中的哪些方法需要加入通知

常见形式

1**.execution(.....)**:根据方法的签名来匹配

2.@annotation(.....) :根据注解匹配

切入点表达式-execution

execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)

其中带?的表示可以省略的部分

访问修饰符:可省略 (比如: public、protected)

包名.类名: 可省略

throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

使用通配符描述切入点

切入点表达式的语法规则:

  1. 方法的访问修饰符可以省略

  2. 返回值可以使用 * 号代替(任意返回值类型)

  3. 包名可以使用 * 号代替,代表任意包(一层包使用一个 * )

  4. 使用 .. 配置包名,标识此包以及此包下的所有子包

  5. 类名可以使用 * 号代替,标识任意类

  6. 方法名可以使用 * 号代替,表示任意方法

  7. 可以使用 * 配置参数,一个任意类型的参数

  8. 可以使用 .. 配置参数,任意个任意类型的参数

同时匹配这两个方法

java 复制代码
 @Pointcut("execution(* com.itheima.service.DeptService.list()) ||"+
    "execution(* com.itheima.service.DeptService.delete(java.lang.Integer))")
    private void pt(){}

    @Before("pt()")
    public void before(){
        log.info("MyAspect6 ... before ...");
    }

注意事项

根据业务需要,可以使用且 (&&)、或()、非(!)来组合比较复杂的切入点表达式

书写建议

所有业务方法名命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是find 开头,更新类方法都是update开头

描述切入点方法通常基于接口描述 ,而不是直接描述实现类,增强拓展性

在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用..,使用*匹配单个包

@annotation

@annotation 切入点表达式,用于匹配标识有特定注解的方法

java 复制代码
@annotation(com.itheima.anno.Log)

实现步骤:

  1. 编写自定义注解

自定义注解:MyLog

java 复制代码
@Retention(RetentionPolicy.RUNTIME) // 该注解在运行时可用
@Target(ElementType.METHOD) // 该注解只能用于方法
public @interface MyLog {
}
  1. 在业务类要做为连接点的方法上添加自定义注解

业务类:DeptServiceImpl

连接点

在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法 名、方法参数等。

对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型

对于其他四种通知,获取连接点信息只能使用JoinPoint ,它是ProceedingJoinPoint的父类型

java 复制代码
package com.itheima.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.util.Arrays;

//切面类
@Slf4j
@Aspect
@Component
public class MyAspect8 {

    @Pointcut("execution(* com.itheima.service.DeptService.*(..))")
    private void pt(){}

    @Before("pt()")
    public void before(JoinPoint joinPoint){
        log.info("MyAspect8 ... before ...");
    }

    @Around("pt()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("MyAspect8 around before ...");

        //1. 获取 目标对象的类名 .
        String name1 = joinPoint.getTarget().getClass().getName();
        log.info("目标对象的类名:{}",name1);

        //2. 获取 目标方法的方法名 .
        String name2 = joinPoint.getSignature().getName();
        log.info("目标方法的方法名:{}",name2);


        //3. 获取 目标方法运行时传入的参数 .
        Object[] args = joinPoint.getArgs();
        log.info("目标方法运行时传入的参数:{}",Arrays.toString(args));


        //4. 放行 目标方法执行 .
        Object result = joinPoint.proceed();


        //5. 获取 目标方法运行的返回值 .
        log.info("目标方法运行的返回值:{}",result);

        log.info("MyAspect8 around after ...");
        return result;

    }
}

AOP案例

案例

将案例中 增、删、改 相关接口的操作日志记录到数据库表中

就是当访问部门管理和员工管理当中的增、删、改相关功能接口时,需要详细的操作日志,并保存 在数据表中,便于后期数据追踪。

操作日志信息包含: 操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长

1. AOP起步依赖
XML 复制代码
<!--AOP起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. 导入准备好的数据库表结构

并引入对应的实体类 数据表

sql 复制代码
-- 操作日志表
create table operate_log(
 id int unsigned primary key auto_increment comment 'ID',
 operate_user int unsigned comment '操作人',
 operate_time datetime comment '操作时间',
 class_name varchar(100) comment '操作的类名',
 method_name varchar(100) comment '操作的方法名',
 method_params varchar(1000) comment '方法参数',
 return_value varchar(2000) comment '返回值',
 cost_time bigint comment '方法执行耗时, 单位:ms'
) comment '操作日志表';
3.实体类
java 复制代码
//操作日志实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
    private Integer id; //主键ID
    private Integer operateUser; //操作人ID
    private LocalDateTime operateTime; //操作时间
    private String className; //操作类名
    private String methodName; //操作方法名
    private String methodParams; //操作方法参数
    private String returnValue; //操作方法返回值
    private Long costTime; //操作耗时
}
4.Mapper接口
java 复制代码
@Mapper
public interface OperateLogMapper {
    @Insert("insert into operate_log (operate_user, operate_time,class_name, method_name, method_params, return_value, cost_time)" +
            " values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime})")
    void insert(OperateLog operateLog);
5.自定义注解@Log
java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log {
}
6.修改业务实现类

在增删改业务方法上添加@Log注解

7.定义切面类,完成记录操作日志的逻辑
java 复制代码
package com.example.aop;

import com.alibaba.fastjson.JSONObject;
import com.example.mapper.OperateLogMapper;
import com.example.pojo.OperateLog;
import com.example.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Arrays;

/**
 * @author hyk~
 */
@Slf4j // 为当前类引入日志功能,方便打印日志信息
@Component // 将当前类作为一个组件管理起来,Spring会自动扫描并管理它
@Aspect // 声明该类为一个切面类,用于定义横切关注点,即对方法执行前后进行拦截和处理
public class LogAspect {

    @Autowired
    private HttpServletRequest request; // 自动注入HttpServletRequest对象,用于获取请求信息

    @Autowired
    private OperateLogMapper operateLogMapper; // 自动注入OperateLogMapper对象,用于操作数据库记录操作日志

    @Around("@annotation(com.example.anno.Log)") // 环绕通知:拦截被@Log注解标记的方法,执行前后插入额外逻辑
    public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取请求头中的JWT令牌并解析,提取操作人的ID
        String jwt = request.getHeader("token"); // 从请求头中获取JWT令牌
        Claims claims = JwtUtils.parseJWT(jwt); // 解析JWT令牌,获取其中的声明
        Integer operateUser = (Integer) claims.get("id"); // 从声明中提取操作人ID

        // 获取操作时间,记录当前时间
        LocalDateTime operateTime = LocalDateTime.now();

        // 获取操作的类名,通过连接点获取目标类的名称
        String className = joinPoint.getTarget().getClass().getName();

        // 获取操作的方法名,通过连接点获取目标方法的名称
        String methodName = joinPoint.getSignature().getName();

        // 获取操作方法的参数,通过连接点获取方法的参数,并转换为字符串
        Object[] args = joinPoint.getArgs();
        String methodParams = Arrays.toString(args);

        // 记录操作开始时间,用于计算操作耗时
        Long begin = System.currentTimeMillis();

        // 调用目标方法,执行被拦截的方法
        Object result = joinPoint.proceed();

        // 记录操作结束时间
        Long end = System.currentTimeMillis();

        // 获取操作方法的返回值,并转换为JSON字符串
        String returnValue = JSONObject.toJSONString(result);

        // 计算操作耗时
        Long costTime = end - begin;

        // 创建操作日志对象,包含操作人、操作时间、类名、方法名、参数、返回值和耗时等信息
        OperateLog operateLog = new OperateLog(
                null, // 日志的主键ID,通常由数据库自动生成
                operateUser, // 操作人ID
                operateTime, // 操作时间
                className, // 操作类名
                methodName, // 操作方法名
                methodParams, // 操作方法参数
                returnValue, // 操作方法返回值
                costTime // 操作耗时
        );

        // 将操作日志插入数据库,持久化存储
        operateLogMapper.insert(operateLog);

        // 打印操作日志,记录到日志文件中
        log.info("AOP操作日志:{}", operateLog);

        // 返回目标方法的执行结果
        return result;
    }
}
解释
  • @Aspect:定义了一个切面,用于在方法执行的前后插入逻辑。
  • @Around:使用环绕通知,拦截指定注解标记的方法,在其执行前后添加逻辑。
  • 日志记录逻辑 :每次有标记了@Log注解的方法被调用时,都会记录操作人的ID、操作的时间、类名、方法名、参数、返回值及耗时,并将这些信息存储到数据库中。
相关推荐
考虑考虑3 小时前
Jpa使用union all
java·spring boot·后端
用户3721574261353 小时前
Java 实现 Excel 与 TXT 文本高效互转
java
浮游本尊4 小时前
Java学习第22天 - 云原生与容器化
java
渣哥6 小时前
原来 Java 里线程安全集合有这么多种
java
间彧6 小时前
Spring Boot集成Spring Security完整指南
java
间彧7 小时前
Spring Secutiy基本原理及工作流程
java
Java水解8 小时前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
洛小豆10 小时前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
前端小张同学10 小时前
服务器上如何搭建jenkins 服务CI/CD😎😎
java·后端
ytadpole10 小时前
Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
java·后端