黑马程序员java web学习笔记--后端进阶(一)AOP

目录

[1 AOP基础](#1 AOP基础)

[1.1 AOP入门程序](#1.1 AOP入门程序)

[1.2 AOP常见场景](#1.2 AOP常见场景)

[1.3 AOP核心概念](#1.3 AOP核心概念)

[2 AOP进阶](#2 AOP进阶)

[2.1 通知类型](#2.1 通知类型)

[2.2 通知顺序](#2.2 通知顺序)

[2.3 切入点表达式](#2.3 切入点表达式)

[2.3.1 execution(主流方法)](#2.3.1 execution(主流方法))

[2.3.2 @annotation(简短)](#2.3.2 @annotation(简短))

[2.4 连接点](#2.4 连接点)

[3 AOP案例](#3 AOP案例)

[3.1 需求分析](#3.1 需求分析)

[3.2 步骤](#3.2 步骤)

[3.3 ThreadLocal](#3.3 ThreadLocal)

[3.4 记录当前登陆员工](#3.4 记录当前登陆员工)

[4 作业--日志信息统计](#4 作业--日志信息统计)

[4.1 完善实体类OperateLog](#4.1 完善实体类OperateLog)

[4.2 SQL语句](#4.2 SQL语句)

[4.3 OperateLogMapper](#4.3 OperateLogMapper)

[4.4 OperateLogController](#4.4 OperateLogController)

[4.5 OperateLogService](#4.5 OperateLogService)

[4.6 OperateLogServiceImpl](#4.6 OperateLogServiceImpl)

[4.7 测试](#4.7 测试)


什么是AOP?

AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,面向切面编程就是面向特定方法编程。

场景:统计当前这个项目当中每一个业务方法的执行耗时。

AOP的优势主要体现在以下四个方面:

  • 减少重复代码:不需要在业务方法中定义大量的重复性的代码,只需要将重复性的代码抽取到AOP程序中即可。

  • 代码无侵入:在基于AOP实现这些业务功能时,对原有的业务代码是没有任何侵入的,不需要修改任何的业务代码。

  • 提高开发效率

  • 维护方便

AOP是一种思想,在Spring框架中对这种思想进行了实现,我们要学习Spring AOP。

1 AOP基础

1.1 AOP入门程序

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

**原始方式:**在原始的实现方式中,我们需要在业务层的也一个方法执行执行,获取方法运行的开始时间; 然后运行原始的方法逻辑; 最后在每一个方法运行结束时,获取方法运行结束时间,计算执行耗时。

SpringAOP实现步骤:

  1. 导入依赖:在 pom.xml 文件中导入 AOP 的依赖。
XML 复制代码
<!-- AOP起步依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  1. 编写AOP程序:针对于特定方法根据业务需要进行编程。
java 复制代码
@Slf4j
@Aspect //标识当前是一个切面类AOP
@Component
public class RecordTimeAspect {

    /**
     * 定义切点
     *
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around("execution(* com.itheima.service.impl.*.*(..))")
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        // 1.记录方法运行的开始时间
        long begin = System.currentTimeMillis();

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

        // 3. 记录方法运行结束的时间,计算方法运行耗时
        long end = System.currentTimeMillis();
        log.info("方法 {} 运行耗时:{} 毫秒", pjp.getSignature(), (end - begin));
        return result;
    }
}

1.2 AOP常见场景

AOP常见的应用场景如下:

  • 记录系统的操作日志。

  • 权限控制。

  • 事务管理:我们前面所讲解的Spring事务管理,底层其实也是通过AOP来实现的,只要添加@Transactional注解之后,AOP程序自动会在原始方法运行前先来开启事务,在原始方法运行完毕之后提交或回滚事务。

1.3 AOP核心概念

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

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

  • 切入点: PointCut ,匹配连接点的条件,通知仅会在切入点方法执行时被应用。
  • 切面:Aspect ,描述通知与切入点的对应关系(通知+切入点)
  • 目标对象:Target ,通知所应用的对象

Spring的AOP底层是基于动态代理技术来实现的,也就是说在程序运行的时候,会自动的基于动态代理技术为目标对象生成一个对应的代理对象。在代理对象当中就会对目标对象当中的原始方法进行功能的增强。

2 AOP进阶

2.1 通知类型

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

在使用@Around环绕通知时的注意事项:

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

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

java 复制代码
@Slf4j
@Aspect
@Component
public class MyAspect1 {
    /**
     * 切入点方法
     */
    @Pointcut("execution(* com.itheima.service.impl.*.*(..))")
    public void pt() {
    }

    /**
     * 前置通知 - 目标方法执行之前执行
     */
    @Before("pt()")
    public void before() {
        log.info("before ...");
    }

    /**
     * 环绕通知 - 目标方法执行之前和之后执行
     */
    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        log.info("around before ...");
        Object result = pjp.proceed();
        log.info("around after ...");
        return result;
    }

    /**
     * 后置通知 - 目标方法执行之后执行, 无论目标方法是否异常都会执行
     */
    @After("pt()")
    public void after() {
        log.info("after ...");
    }

    /**
     * 后置返回通知 - 目标方法正常返回之后执行
     */
    @AfterReturning("pt()")
    public void afterReturning() {
        log.info("afterReturning ...");
    }

    /**
     * 后置异常通知 - 目标方法执行异常之后执行
     */
    @AfterThrowing("pt()")
    public void afterThrowing() {
        log.info("afterThrowing ...");
    }

}

Spring提供了**@PointCut注解:**抽取公共的切入点表达式,提高复用性。

2.2 通知顺序

通过以上程序运行可以看出在不同切面类中,默认按照切面类的类名字母排序:

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

控制通知的执行顺序:@Order

**@Order(2) :**切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)

2.3 切入点表达式

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

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

  • 常见形式:

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

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

2.3.1 execution(主流方法)

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

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

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

  • 包名.类名: 可省略(不建议省略)

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

通配符描述切入点

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

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

完整版:

java 复制代码
@Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")

通配符+省略:

java 复制代码
@Before("execution(* com.itheima.service.impl.*.*(..))")

匹配DeptServiceImpl类中以find开头的方法

java 复制代码
execution(* com.itheima.service.impl.DeptServiceImpl.find*(..))

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

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

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

2.3.2 @annotation(简短)

自定义注解:LogOperation(com.itheima.anno)

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

在需要的地方加上自定义注解

java 复制代码
@LogOperation //自定义注解(表示:当前方法属于目标方法)

切面类

java 复制代码
@Slf4j
@Component
@Aspect
public class MyAspect6 {
    //针对list方法、delete方法进行前置通知和后置通知

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

execution切入点表达式

  • 根据我们所指定的方法的描述信息来匹配切入点方法,也是最为常用的一种方式。
  • 如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过execution切入点表达式描述比较繁琐。

annotation 切入点表达式

  • 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了。

2.4 连接点

连接点可以简单理解为:可以被AOP控制的方法。

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

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

3 AOP案例

3.1 需求分析

将案例(Tlias智能学习辅助系统)中增、删、改相关接口的操作日志记录到数据库表中。

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

问题一:采用哪种通知类型?计算耗时 -> @Around环绕通知。

问题二:切入点表达式怎么写?方法运行时参数、返回值 -> 匹配controller层的方法。匹配增save、删delete、改update,使用execution切入点表达式也可以,但是会比较繁琐,需要 " || " 三个。 此处可以使用 @annotation切入点表达式。

3.2 步骤

自定义注解

java 复制代码
@Target(ElementType.METHOD) // 注解作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
public @interface Log {
}

实现代码

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

    // 注入Mapper对象
    @Autowired
    private OperateLogMapper operateLogMapper;

    @Around("@annotation(com.itheima.anno.Log)")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        // 开始记录日志
        System.out.println("开始记录日志...");
        long startTime = System.currentTimeMillis();

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

        // 记录日志耗时
        long endTime = System.currentTimeMillis();
        long costTime = endTime - startTime;

        // 构建日志对象
        OperateLog olog = new OperateLog();
        olog.setOperateEmpId(1);
        olog.setOperateTime(LocalDateTime.now());
        olog.setClassName(pjp.getTarget().getClass().getName());
        olog.setMethodName(pjp.getSignature().getName());
        olog.setMethodParams(Arrays.toString(pjp.getArgs()));
        olog.setReturnValue(result == null ? "void" : result.toString());
        olog.setCostTime(costTime);

        // 保存日志
        log.info("记录操作日志: {}", olog);

        // 调用Mapper对象保存日志
        operateLogMapper.insert(olog);

        // 返回结果
        System.out.println("结束记录日志...");
        return result;
    }
}

测试

之前员工id是写死的,此处如何实现【获取操作员工id】呢?

获取请求头中传递的jwt 令牌,解析需要的参数。

TokenFilter 中已经解析了令牌的信息,如何传递给AOP程序、Controller、Service呢?ThreadLocal

3.3 ThreadLocal

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

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

  • 常见方法:

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

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

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

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

3.4 记录当前登陆员工

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

在 com.itheima.utils 引入工具类 CurrentHolder

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

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 复制代码
//5.如果token存在,则解析token,如果校验失败,则返回错误信息401
try {
    Claims claims = JwtUtils.parseToken(token);
    Integer empId = (Integer) claims.get("id");
    CurrentHolder.setCurrentId(empId); //存入, 用完后要删除
    log.info("当前用户id: {},将其存入ThreadLocal", empId);
} catch (Exception e) {
    log.info("令牌解析失败,响应401");
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    return;
}

//6.校验通过,则放行
log.info("令牌解析成功,放行");
filterChain.doFilter(request,response);

//7.用完后删除ThreadLocal中的数据
CurrentHolder.remove();

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

java 复制代码
private Integer getCurrentUserId() {
    return CurrentHolder.getCurrentId();
}

测试

4 作业--日志信息统计

4.1 完善实体类OperateLog

java 复制代码
@Data
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; //操作耗时

    //封装非数据库字段:操作人姓名(关联emp表)
    private String operateEmpName;
}

4.2 SQL语句

sql 复制代码
select ol.id, operate_emp_id, operate_time, class_name, method_name, method_params, return_value, cost_time, e.name
from operate_log ol
         left join emp e on ol.operate_emp_id = e.id

4.3 OperateLogMapper

java 复制代码
@Mapper
public interface OperateLogMapper {

    //查询所有日志数据
    @Select("select ol.id, operate_emp_id, operate_time, class_name, method_name, method_params, return_value, cost_time, e.name operate_emp_name " +
            "from operate_log ol left join emp e on ol.operate_emp_id = e.id")
    List<OperateLog> page(OperateLogQueryParam operateLogQueryParam);

}

4.4 OperateLogController

java 复制代码
@Slf4j
@RequestMapping("/log/page")
@RestController
public class OperateLogController {
    @Autowired
    private OperateLogService operateLogService;

    @GetMapping
    public Result page(OperateLogQueryParam operateLogQueryParam) {
        log.info("分页查询:{}", operateLogQueryParam);
        PageResult<OperateLog> operateLogList = operateLogService.page(operateLogQueryParam);
        return Result.success(operateLogList);
    }
}

4.5 OperateLogService

java 复制代码
public interface OperateLogService {

    /**
     * 分页查询操作日志
     *
     * @param operateLogQueryParam
     * @return
     */
    PageResult<OperateLog> page(OperateLogQueryParam operateLogQueryParam);
}

4.6 OperateLogServiceImpl

java 复制代码
@Service
public class OperateLogServiceImpl implements OperateLogService {

    @Autowired
    private OperateLogMapper operateLogMapper;

    @Override
    public PageResult<OperateLog> page(OperateLogQueryParam operateLogQueryParam) {
        // 1. 设置分页参数
        PageHelper.startPage(operateLogQueryParam.getPage(), operateLogQueryParam.getPageSize());
        // 2. 执行查询
        List<OperateLog> operateLogList = operateLogMapper.page(operateLogQueryParam);
        // 3. 解析查询结果并封装
        Page<OperateLog> p = (Page<OperateLog>) operateLogList;
        return new PageResult<OperateLog>(p.getTotal(), p.getResult());
    }
}

4.7 测试

/log/page?page=1&pageSize=10

相关推荐
qq_12498707532 小时前
基于springboot+vue的家乡特色旅游宣传推荐系统(源码+论文+部署+安装)
java·前端·vue.js·spring boot·毕业设计·计算机毕设·计算机毕业设计
霑潇雨2 小时前
Flink转换算子——filter
java·大数据·flink
闻哥2 小时前
从 SQL 执行到优化器内核:MySQL 性能调优核心知识点解析
java·jvm·数据库·spring boot·sql·mysql·面试
毕设源码-钟学长2 小时前
【开题答辩全过程】以 河环院快递服务系统为例,包含答辩的问题和答案
java
weixin199701080162 小时前
B2Bitem_get - 获取商标详情接口对接全攻略:从入门到精通
java·大数据·算法
山北雨夜漫步2 小时前
外卖心得day01
java
NE_STOP2 小时前
springboot--pagehelper整合与日志处理
java
weixin_440784112 小时前
Java线程池工作原理浅析
android·java·开发语言·okhttp·android studio·android runtime
ANGLAL2 小时前
35.登录认证演进及双token机制
java