JavaWeb(后端进阶)

AOP 概述:

AOP:Aspect Oriented Programming (面向切面编程、面向方法编程),是一种思想

优点:减少重复代码,代码无侵入,提高开发效率,维护方便

AOP 入门:

需求:统计部门管理各个业务层方法执行耗时

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

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

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

java 复制代码
@Component
@Aspect//当前类为切面类
@Slf4j
public class RecordTimeAspect{
    @Around("execution(* org.example.service.impl.DeptServiceImpl.*(..))")
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable{
        //记录方法执行开始时间
        long begin = System.currentTimeMillis();

        //执行原始方法
        Object result = pjp.proceed();

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

        //计算方法执行耗时
        log.info("方法执行耗时: {}毫秒",end-begin);
        return result;
    }
}

AOP 详解:

连接点:JoinPoint

指的是可以被 AOP 控制的方法

在 SpringAOP 提供的 JoinPoint 当中,封装了连接点方法在执行时的相关信息

通知:Advice

指的是重复的逻辑,也就是共性的功能

切入点:PointCut

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

切面:Aspect

描述通知与切入点的对应关系

切面所在的类,为切面类(被 @Aspect 注解标识的类)

目标对象:Target

指的是通知所应用的对象

通知类型:

|-----------------|--------------------------------------|
| Spring AOP 通知类型 ||
| @Around | 环绕通知,此注解标注的通知方法在目标方法前、后都被执行 |
| @Before | 前置通知,此注解标注的通知方法在目标方法前被执行 |
| @After | 后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行 |
| @AfterReturning | 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行 |
| @AfterThrowing | 异常后通知,此注解标注的通知方法发生异常后执行 |

代码测试:

java 复制代码
@Slf4j
@Component
@Aspect
public class MyAspect1{
    //前置通知
    @Before("execution(* org.example.service.*.*(..))")
    public void before(JoinPoint joinPoint){
        log.info("before ...");

    }

    //环绕通知
    @Around("execution(* org.example.service.*.*(..))")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
        log.info("around before ...");
        //调用目标对象的原始方法执行
        Object result = proceedingJoinPoint.proceed();
        //原始方法如果执行时有异常,环绕通知中的后置代码将不会执行
        log.info("around after ...");
        return result;
    }

    //后置通知
    @After("execution(* org.example.service.*.*(..))")
    public void after(JoinPoint joinPoint){
        log.info("after ...");
    }

    //返回后通知(程序在正常执行的情况下,会执行的后置通知)
    @AfterReturning("execution(* org.example.service.*.*(..))")
    public void afterReturning(JoinPoint joinPoint){
        log.info("afterReturning ...");
    }

    //异常通知(程序在出现异常的情况下,执行的后置通知)
    @AfterThrowing("execution(* org.example.service.*.*(..))")
    public void afterThrowing(JoinPoint joinPoint){
        log.info("afterThrowing ...");
    }
}

程序发生异常的情况下:

@AfterReturning 标识的通知方法不会执行,@AfterThrowing 标识的通知方法执行

@Around 环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑将不会执行(原始方法调用已经出现异常)

在使用通知时的注意事项:

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

@Around 环绕通知方法的返回值,必须指定为 Object,来接收原始方法的返回值,否则原始方法执行完毕,将获取不到返回值

抽取:

Spring 提供了 @Pointcut 注解,该注解的作用是将公共的切入点表达式抽取出来,需要用到时引用该切入点表达式即可

java 复制代码
@Slf4j
@Component
@Aspect
public class MyAspect1{
    //切入点方法(公共的切入点表达式)
    @Pointcut("execution(* org.example.service.*.*(..))")
    private void pt(){}

    //前置通知
    @Before("pt()")
    public void before(JoinPoint joinPoint){
        log.info("before ...");
    }

    //环绕通知
    @Around("pt()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
        log.info("around before ...");
        //调用目标对象的原始方法执行
        Object result = proceedingJoinPoint.proceed();
        //原始方法如果执行时有异常,环绕通知中的后置代码将不会执行
        log.info("around after ...");
        return result;
    }

    //后置通知
    @After("pt()")
    public void after(JoinPoint joinPoint){
        log.info("after ...");
    }

    //返回后通知(程序在正常执行的情况下,会执行的后置通知)
    @AfterReturning("pt()")
    public void afterReturning(JoinPoint joinPoint){
        log.info("afterReturning ...");
    }

    //异常通知(程序在出现异常的情况下,执行的后置通知)
    @AfterThrowing("pt()")
    public void afterThrowing(JoinPoint joinPoint){
        log.info("afterThrowing ...");
    }
}

注意:当切入点方法使用 private 修饰时,仅能在当前切面类中引用该表达式, 当外部其他切面类中也要引用当前类中的切入点表达式,就需要把 private 改为 public,而在引用的时候,具体的语法为:

java 复制代码
@Slf4j
@Component
@Aspect
public class MyAspect2{
    //引用 MyAspect1 切面类中的切入点表达式
    @Before("org.example.aop.MyAspect1.pt()")
    public void before(){
        log.info("MyAspect2 -> before ...");
    }
}

通知顺序:

当在项目开发当中定义了多个切面类,而多个切面类中多个切入点都匹配到了同一个目标方法,此时当目标方法在运行的时候,这多个切面类当中的这些通知方法都会运行

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

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

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

控制通知的执行顺序:修改切面类的类名 或 使用 Spring 提供的 @Order 注解(推荐)

切入点表达式:

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

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

常见形式:

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

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

execution:

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

java 复制代码
execution([访问修饰符]  返回值  [包名.类名.]方法名(方法参数) [throws 异常])

使用通配符描述切入点:

* :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分

.. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数

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

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

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

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

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

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

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

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

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

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

java 复制代码
execution(* org.example.service.DeptService.findAll(..)) || execution(* org.example.service.DeptService.deleteById(..))

切入点表达式书写建议:

所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配(如:findXxx,updateXxx)

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

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

@annotation:

基于注解的方式来匹配切入点方法

自定义注解:LogOperation

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation{
}

DetpServiceImpl:

java 复制代码
@Service
public class DeptServiceImpl implements DeptService{
    @Autowired
    private DeptMapper deptMapper;
    @Override
    @LogOperation//自定义注解:表示当前方法属于目标方法
    public List<Dept> findAll(){
        return deptMapper.findAll();
    }
    @Override
    @LogOperation
    public void deleteById(Integer id){
        deptMapper.deleteById(id);
    }
    /*
        ...
    */
}

切面类:

java 复制代码
@Slf4j
@Component
@Aspect
public class MyAspect{

    //前置通知
    @Before("@annotation(org.example.anno.LogOperation)")
    public void before(){
        log.info("MyAspect -> before ...");
    }
    
    //后置通知
    @After("@annotation(org.example.anno.LogOperation)")
    public void after(){
        log.info("MyAspect -> after ...");
    }
}

AOP 案例:

需求:

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

操作日志信息包含:

操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长

代码实现:

准备工作:

在 pom.xml 中引入 AOP 的依赖:

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

创建数据库表结构:

sql 复制代码
-- 操作日志表
create table operate_log(
    id int unsigned primary key auto_increment comment 'ID',
    operate_emp_id int unsigned comment '操作人ID',
    operate_time datetime comment '操作时间',
    class_name varchar(100) comment '操作的类名',
    method_name varchar(100) comment '操作的方法名',
    method_params varchar(1000) comment '方法参数',
    return_value varchar(2000) comment '返回值, 存储json格式',
    cost_time int comment '方法执行耗时, 单位:ms'
) comment '操作日志表';

创建实体类 OperateLog:

java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog{
    private Integer id; //ID
    private Integer operateEmpId; //操作人ID
    private LocalDateTime operateTime; //操作时间
    private String className; //操作类名
    private String methodName; //操作方法名
    private String methodParams; //操作方法参数
    private String returnValue; //操作方法返回值
    private Long costTime; //操作耗时
}

创建日志操作 Mapper 接口:

java 复制代码
@Mapper
public interface OperateLogMapper{
    //插入日志数据
    @Insert("insert into operate_log (operate_emp_id, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
            "values (#{operateEmpId}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
    void insert(OperateLog log); 
}

自定义注解 @LogOperation:

java 复制代码
//自定义注解,用于标识哪些方法需要记录日志
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation{
}

定义 AOP 记录日志的切面类:

java 复制代码
@Aspect
@Component
public class OperationLogAspect{
    @Autowired
    private OperateLogMapper operateLogMapper;
    //环绕通知
    @Around("@annotation(log)")
    public Object around(ProceedingJoinPoint joinPoint, LogOperation log) throws Throwable{
        //记录开始时间
        long startTime = System.currentTimeMillis();
        //执行方法
        Object result = joinPoint.proceed();
        //当前时间
        long endTime = System.currentTimeMillis();
        //耗时
        long costTime = endTime - startTime;

        //构建日志对象
        OperateLog operateLog = new OperateLog();
        operateLog.setOperateEmpId(getCurrentUserId());//需要实现 getCurrentUserId 方法
        operateLog.setOperateTime(LocalDateTime.now());
        operateLog.setClassName(joinPoint.getTarget().getClass().getName());
        operateLog.setMethodName(joinPoint.getSignature().getName());
        operateLog.setMethodParams(Arrays.toString(joinPoint.getArgs()));
        operateLog.setReturnValue(result.toString());
        operateLog.setCostTime(costTime);

        //插入日志
        operateLogMapper.insert(operateLog);
        return result;
    }
    //获取当前用户ID
    private int getCurrentUserId(){
        return 1;
    }
}

在 Controller 层需要记录日志的方法上加上注解 @LogOperation:

java 复制代码
@Slf4j
@RequestMapping("/depts")
@RestController
public class DeptController{
    @Autowired
    private DeptService deptService;
    //查询部门列表
    @GetMapping
    public Result list(){
        log.info("查询部门列表");
        List<Dept> deptList = deptService.findAll();
        return Result.success(deptList);
    }
    //根据ID删除部门
    @LogOperation
    @DeleteMapping
    public Result delete(Integer id){
        log.info("根据Id删除部门, id: {}" , id);
        deptService.deleteById(id);
        return Result.success();
    }
    //新增部门
    @LogOperation
    @PostMapping
    public Result add(@RequestBody Dept dept){
        log.info("新增部门, dept: {}" , dept);
        deptService.add(dept);
        return Result.success();
    }
    //根据ID查询部门
    @GetMapping("/{id}")
    public Result getById(@PathVariable Integer id){
        log.info("根据ID查询部门, id: {}" , id);
        Dept dept = deptService.getById(id);
        return Result.success(dept);
    }
    //修改部门
    @LogOperation
    @PutMapping
    public Result update(@RequestBody Dept dept){
        log.info("修改部门, dept: {}" , dept);
        deptService.update(dept);
        return Result.success();
    }
}
java 复制代码
@Slf4j//在类中自动生成日志对象
@RequestMapping("/clazzs")
@RestController//标识 RESTful 风格的控制器类
public class ClazzController{
    @Autowired
    private ClazzService clazzService;
    //条件分页查询班级
    @GetMapping//绑定 HTTP GET 请求
    public Result page(String name,
                       @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,//请求参数绑定
                       @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end,
                       @RequestParam(defaultValue = "1") Integer page,//接收前端传递的 URL 查询参数
                       @RequestParam(defaultValue = "10") Integer pageSize){
        PageResult pageResult = clazzService.page(name,begin,end,page,pageSize);
        return Result.success(pageResult);
    }
    //查询全部班级
    @GetMapping("/list")
    public Result findAll(){
        List<Clazz> clazzList = clazzService.findAll();
        return Result.success(clazzList);
    }
    //新增班级
    @LogOperation
    @PostMapping
    public Result add(@RequestBody Clazz clazz){//将请求体中的json数据自动转换为指定的对象
        clazzService.add(clazz);
        return Result.success();
    }
    //根据ID查询班级
    @GetMapping("/{id}")
    public Result getInfo(@PathVariable Integer id){
        Clazz clazz = clazzService.getInfo(id);
        return Result.success(clazz);
    }
    //修改班级
    @LogOperation
    @PutMapping
    public Result update(@RequestBody Clazz clazz){
        clazzService.update(clazz);
        return Result.success();
    }
    //删除班级
    @LogOperation
    @DeleteMapping("/{id}")
    public Result delete(@PathVariable Integer id){
        clazzService.deleteById(id);
        return Result.success();
    }
}

测试操作日志记录功能:

连接点:

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

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

java 复制代码
@Around("execution(* org.example.service.DeptService.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
    String className = joinPoint.getTarget().getClass().getName();//获取目标类名
    Signature signature = joinPoint.getSignature();//获取目标方法签名
    String methodName = joinPoint.getSignature().getName();//获取目标方法名
    Object[] args = joinPoint.getArgs();//获取目标方法运行参数
    Object res = joinPoint.proceed();//执行原始方法,获取返回值(环绕通知)
    return res;
}

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

java 复制代码
@Before("execution(* org.example.service.DeptService.*(..))")
public void before(JoinPoint joinPoint) {
    String className = joinPoint.getTarget().getClass().getName();//获取目标类名
    Signature signature = joinPoint.getSignature();//获取目标方法签名
    String methodName = joinPoint.getSignature().getName();//获取目标方法名
    Object[] args = joinPoint.getArgs();//获取目标方法运行参数
}

获取当前登录员工:

ThreadLocal:

ThreadLocal并不是一个 Thread,而是 Thread 的局部变量

ThreadLocal 为每个线程提供一份单独的存储空间,具有线程隔离的效果,不同的线程之间不会相互干扰

常见方法:

public void set(T value)`` 设置当前线程的线程局部变量的值

public T get()`` 返回当前线程所对应的线程局部变量的值

public void remove()`` 移除当前线程的线程局部变量

记录当前登录员工:

定义 ThreadLocal 操作的工具类,用于操作当前登录员工 ID:

java 复制代码
package org.example.utils;

public class CurrentHolder{
    private static final ThreadLocal<Integer> CURRENT_lOCAL = new ThreadLocal<>();

    public static void setCurrentId(Integer employeeId){
        CURRENT_lOCAL.set(employeeId);
    }
    public static Integer getCurrentId(){
        return CURRENT_lOCAL.get();
    }
    public static void remove(){
        CURRENT_lOCAL.remove();
    }
}

在 TokenFilter 中,解析完当前登录员工 ID,将其存入 ThreadLocal (用完后需将其删除)

java 复制代码
//令牌校验过滤器
@Slf4j
//@WebFilter(urlPatterns = "/*")
public class TokenFilter implements Filter{
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;//转换为HTTP请求对象
        HttpServletResponse response = (HttpServletResponse) resp;//转换为HTTP响应对象
        //获取请求url
        String url = request.getRequestURL().toString();

        //判断是否是登录请求
        if(url.contains("login")){//登录请求
            log.info("登录请求 , 放行");
            chain.doFilter(request, response);
            return;
        }

        //获取请求头中的令牌(token)
        String jwt = request.getHeader("token");

        //判断令牌是否存在
        if(!StringUtils.hasLength(jwt)){//jwt为空
            log.info("获取到jwt令牌为空, 返回错误结果");
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);//设置响应状态码为401
            return;
        }

        //解析token
        try{
            Claims claims = JwtUtils.parseJWT(jwt);
            Integer empId = Integer.valueOf(claims.get("id").toString());
            CurrentHolder.setCurrentId(empId);
        }catch(Exception e){//解析失败
            e.printStackTrace();
            log.info("解析令牌失败, 返回错误结果");
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);//设置响应状态码为401
            return;
        }

        //放行
        log.info("令牌合法, 放行");
        chain.doFilter(request , response);//允许请求继续访问目标接口

        //清空当前线程绑定的ID
        CurrentHolder.remove();
    }
}

在 AOP 程序中,从 ThreadLocal 中获取当前登录员工的 ID:

java 复制代码
@Aspect
@Component
public class OperationLogAspect{
    @Autowired
    private OperateLogMapper operateLogMapper;
    //环绕通知
    @Around("@annotation(log)")
    public Object around(ProceedingJoinPoint joinPoint, LogOperation log) throws Throwable{
        //记录开始时间
        long startTime = System.currentTimeMillis();
        //执行方法
        Object result = joinPoint.proceed();
        //当前时间
        long endTime = System.currentTimeMillis();
        //耗时
        long costTime = endTime - startTime;

        //构建日志对象
        OperateLog operateLog = new OperateLog();
        operateLog.setOperateEmpId(getCurrentUserId());//需要实现 getCurrentUserId 方法
        operateLog.setOperateTime(LocalDateTime.now());
        operateLog.setClassName(joinPoint.getTarget().getClass().getName());
        operateLog.setMethodName(joinPoint.getSignature().getName());
        operateLog.setMethodParams(Arrays.toString(joinPoint.getArgs()));
        operateLog.setReturnValue(result.toString());
        operateLog.setCostTime(costTime);

        //插入日志
        operateLogMapper.insert(operateLog);
        return result;
    }
    //获取当前用户ID
    private int getCurrentUserId(){
        return CurrentHolder.getCurrentId();
    }
}

在同一个线程/同一个请求中,进行数据共享就可以使用 ThreadLocal

相关推荐
IT_陈寒2 小时前
5个Python 3.12新特性让你的代码效率提升50%,第3个太实用了!
前端·人工智能·后端
Victor3562 小时前
Redis(109)Redis的Pipeline如何使用?
后端
NPE~2 小时前
[手写系列]Go手写db — — 第七版(实现Disk存储引擎、Docker化支持)
数据库·后端·docker·golang·教程·手写数据库
Victor3562 小时前
Redis(108)Redis的事务机制如何实现?
后端
JaguarJack2 小时前
PHP 开发中 你可能不知道的非常好用 PhpStorm 插件
后端·php
9ilk2 小时前
【基于one-loop-per-thread的高并发服务器】--- 前置技术
运维·服务器·c++·笔记·后端·中间件
编程火箭车2 小时前
【Java SE 基础学习打卡】02 计算机硬件与软件
java·电脑选购·计算机基础·编程入门·计算机硬件·软件系统·编程学习路线
Felix_XXXXL3 小时前
IDEA + Spring Boot 的三种热加载方案
java·后端