JavaWeb学习笔记(Day13)

一、事务管理

1. Spring事务管理
(1) 案例
① 步骤
  • 根据ID删除部门数据

  • 根据部门ID删除该部门下的员工

② 代码实现

DeptServiceImpl

java 复制代码
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptMapper deptMapper;

    @Autowired
    private EmpMapper empMapper;

    
    @Override
    @Transactional  //当前方法添加了事务管理
    public void delete(Integer id){
        //根据部门id删除部门信息
        deptMapper.deleteById(id);
        
        //模拟:异常发生
        int i = 1/0;

        //删除部门下的所有员工信息
        empMapper.deleteByDeptId(id);   
    }
}

DeptMapper

java 复制代码
@Mapper
public interface DeptMapper {
    /**
     * 根据id删除部门信息
     * @param id   部门id
     */
    @Delete("delete from dept where id = #{id}")
    void deleteById(Integer id);
}

EmpMapper

java 复制代码
@Mapper
public interface EmpMapper {

    //根据部门id删除部门下所有员工
    @Delete("delete from emp where dept_id=#{deptId}")
    public int deleteByDeptId(Integer deptId);
    
}
(2) Transactional注解

**① 作用:**在当前这个方法执行开始之前来开启事务,方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常,就会进行事务的回滚操作。

② 注解: 我们一般会在业务层当中来控制事务,因为在业务层当中,一个业务功能可能会包含多个数据访问的操作。在业务层来控制事务,我们就可以将多个数据访问操作控制在一个事务范围内。

2. 事务进阶
(1) rollbackFor

假如我们想让所有的异常都回滚,需要来配置@Transactional注解当中的rollbackFor属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。

java 复制代码
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptMapper deptMapper;

    @Autowired
    private EmpMapper empMapper;

    
    @Override
    @Transactional(rollbackFor=Exception.class)
    public void delete(Integer id){
        //根据部门id删除部门信息
        deptMapper.deleteById(id);
        
        //模拟:异常发生
        int num = id/0;

        //删除部门下的所有员工信息
        empMapper.deleteByDeptId(id);   
    }
}
(2) propagation

① 常见的事务传播行为:

属性值 含义
REQUIRED 【默认值】需要事务,有则加入,无则创建新事务
REQUIRES_NEW 需要新事务,无论有无,总是创建新事务
SUPPORTS 支持事务,有则加入,无则在无事务状态中运行
NOT_SUPPORTED 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务
MANDATORY 必须有事务,否则抛异常
NEVER 必须没事务,否则抛异常

对于这些事务传播行为,我们只需要关注以下两个就可以了:

  • REQUIRED(默认值):大部分情况下都是用该传播行为即可。

  • REQUIRES_NEW:当我们不希望事务之间相互影响时,可以使用该传播行为。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。

② 代码实现

java 复制代码
@Slf4j
@Service
//@Transactional //当前业务实现类中的所有的方法,都添加了spring事务管理机制
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptMapper deptMapper;
    
    @Autowired
    private EmpMapper empMapper;

    @Autowired
    private DeptLogService deptLogService;


    //根据部门id,删除部门信息及部门下的所有员工
    @Override
    @Log
    @Transactional(rollbackFor = Exception.class) 
    public void delete(Integer id) throws Exception {
        try {
            //根据部门id删除部门信息
            deptMapper.deleteById(id);
            //模拟:异常
            if(true){
                throw new Exception("出现异常了~~~");
            }
            //删除部门下的所有员工信息
            empMapper.deleteByDeptId(id);
        }finally {
            //不论是否有异常,最终都要执行的代码:记录日志
            DeptLog deptLog = new DeptLog();
            deptLog.setCreateTime(LocalDateTime.now());
            deptLog.setDescription("执行了解散部门的操作,此时解散的是"+id+"号部门");
            //调用其他业务类中的方法
            deptLogService.insert(deptLog);
        }
    }
    
    //省略其他代码...
}

二、AOP基础

1. AOP快速入门
(1) 实现步骤
  • 导入依赖:在pom.xml中导入AOP的依赖

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

(2) 代码实现

pom.xml

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

AOP程序:TimeAspect

java 复制代码
@Component
@Aspect //当前类为切面类
@Slf4j
public class TimeAspect {

    @Around("execution(* com.itheima.service.*.*(..))") 
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        //记录方法执行开始时间
        long begin = System.currentTimeMillis();

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

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

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

        return result;
    }
}

核心拆解:execution(* com.itheima.service.*.*(..)) 切入点表达式

① 语法格式:

execution( [访问修饰符] 返回值类型 包路径.类名.方法名(参数列表) )

② 各部分含义逐一拆解(按从左到右顺序):

表达式片段 具体含义 补充说明
* 方法返回值类型为「任意类型」 若指定返回值(如String),则仅匹配返回值为该类型的方法
com.itheima.service 目标方法所在的固定包路径 仅匹配这个包下的类中的方法,子包下的类不会被匹配
第一个* 匹配该包下的任意类(包括普通类、实现类等) 若指定类名(如UserService),则仅匹配该类中的方法
第二个* 匹配类中的任意方法名 若指定方法名(如findById),则仅匹配该方法
(..) 匹配方法的任意参数列表 无参数、1个参数、多个参数都能匹配;若指定参数(如(Long)),则仅匹配该参数类型的方法
2. AOP核心概念
(1) 连接点:JoinPoint

可以被AOP控制的方法(所有方法都可以算作是连接点,暗含方法执行时的相关信息)

连接点指的是可以被aop控制的方法。例如:入门程序当中所有的业务方法都是可以被aop控制的方法。

(2) 通知:Advice

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

(3) 切入点:PointCut

匹配连接点的条件,通知仅会在切入点方法执行时被应用**(实际被AOP控制的方法)**

在通知当中,我们所定义的共性功能到底要应用在哪些方法上?此时就涉及到了切入点pointcut概念。切入点指的是匹配连接点的条件。通知仅会在切入点方法运行时才会被应用。

(4) 切面:Aspect

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

当通知和切入点结合在一起,就形成了一个切面。通过切面就能够描述当前aop程序需要针对于哪个原始方法,在什么时候执行什么样的操作。

(5) 目标对象:Target

通知所应用的对象

目标对象指的就是通知所应用的对象,我们就称之为目标对象。

三、AOP进阶

1. 通知类型
(1) Spring中AOP的通知类型:
  • @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行

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

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

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

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

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

    }

    //环绕通知
    @Around("execution(* com.itheima.service.*.*(..))")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("around before ...");

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

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

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

    //异常通知(程序在出现异常的情况下,执行的后置通知)
    @AfterThrowing("execution(* com.itheima.service.*.*(..))")
    public void afterThrowing(JoinPoint joinPoint){
        log.info("afterThrowing ...");
    }
}
(3) 解决切入点表达式重复的问题
① 解决方案

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

② 代码实现
java 复制代码
@Slf4j
@Component
@Aspect
public class MyAspect1 {

    //切入点方法(公共的切入点表达式)
    @Pointcut("execution(* com.itheima.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("com.itheima.aspect.MyAspect1.pt()")
    public void before(){
        log.info("MyAspect2 -> before ...");
    }
}
2. 通知顺序
(1) 修改执行顺序的方式

使用Spring提供的@Order注解

(2) 代码示例
java 复制代码
@Slf4j
@Component
@Aspect
@Order(1) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect4 {
    //前置通知
    @Before("execution(* com.itheima.service.*.*(..))")
    public void before(){
        log.info("MyAspect4 -> before ...");
    }

    //后置通知
    @After("execution(* com.itheima.service.*.*(..))")
    public void after(){
        log.info("MyAspect4 -> after ...");
    }
}

通知的执行顺序大家主要知道两点即可:

  • 不同的切面类当中,默认情况下通知的执行顺序是与切面类的类名字母排序是有关系的

  • 可以在切面类上面加上@Order注解,来控制不同的切面类通知的执行顺序

3. 切入点表达式
(1) execution
① 语法格式:

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

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

其中带?的表示可以省略的部分(第一个、第三个一般会省略)

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

  • 包名.类名: 可省略(一般不省略)

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

② 示例:
java 复制代码
@Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
③ 可以使用通配符描述切入点
  • * :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分

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

④ 切入点表达式的语法规则:
  1. 方法的访问修饰符可以省略

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

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

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

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

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

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

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

⑤ 切入点表达式示例
  • 省略方法的修饰符号
java 复制代码
execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))
  • 使用*代替返回值类型
java 复制代码
execution(* com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))
  • 使用 * 代替参数
java 复制代码
execution(* com.itheima.service.impl.DeptServiceImpl.delete(*))
  • 使用..省略参数
java 复制代码
execution(* com..*.*(..))
(2) @annotation
① 实现步骤
  1. 编写自定义注解

  2. 在业务类要做为连接点的方法上添加自定义注解

② 代码实现

自定义注解:MyLog

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

业务类:DeptServiceImpl

java 复制代码
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptMapper deptMapper;

    @Override
    @MyLog //自定义注解(表示:当前方法属于目标方法)
    public List<Dept> list() {
        List<Dept> deptList = deptMapper.list();
        //模拟异常
        //int num = 10/0;
        return deptList;
    }

    @Override
    @MyLog  //自定义注解(表示:当前方法属于目标方法)
    public void delete(Integer id) {
        //1. 删除部门
        deptMapper.delete(id);
    }


    @Override
    public void save(Dept dept) {
        dept.setCreateTime(LocalDateTime.now());
        dept.setUpdateTime(LocalDateTime.now());
        deptMapper.save(dept);
    }

    @Override
    public Dept getById(Integer id) {
        return deptMapper.getById(id);
    }

    @Override
    public void update(Dept dept) {
        dept.setUpdateTime(LocalDateTime.now());
        deptMapper.update(dept);
    }
}

切面类

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

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

    //后置通知
    @After("@annotation(com.itheima.anno.MyLog)")
    public void after(){
        log.info("MyAspect6 -> after ...");
    }
}
4. 连接点
(1) 定义

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

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

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

(2) 代码实现
java 复制代码
@Slf4j
@Component
@Aspect
public class MyAspect7 {

    @Pointcut("@annotation(com.itheima.anno.MyLog)")
    private void pt(){}
   
    //前置通知
    @Before("pt()")
    public void before(JoinPoint joinPoint){
        log.info(joinPoint.getSignature().getName() + " MyAspect7 -> before ...");
    }
    
    //后置通知
    @Before("pt()")
    public void after(JoinPoint joinPoint){
        log.info(joinPoint.getSignature().getName() + " MyAspect7 -> after ...");
    }

    //环绕通知
    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        //获取目标类名
        String name = pjp.getTarget().getClass().getName();
        log.info("目标类名:{}",name);

        //目标方法名
        String methodName = pjp.getSignature().getName();
        log.info("目标方法名:{}",methodName);

        //获取方法执行时需要的参数
        Object[] args = pjp.getArgs();
        log.info("目标方法参数:{}", Arrays.toString(args));

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

        return returnValue;
    }
}

四、AOP案例

1. 需求

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

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

(1) 既然要基于AOP面向切面编程的方式来完成的功能,那么我们要使用 AOP五种通知类型当中的哪种通知类型?

  • 答案:环绕通知

方法返回值,是在原始方法执行后才能获取到的。

方法的运行时长,需要原始方法运行之前记录开始时间,原始方法运行之后记录结束时间。通过计算获得方法的执行耗时。

(2) 切入点表达式我们该怎么写?

  • 答案:使用annotation来描述表达式

要匹配业务接口当中所有的增删改的方法,而增删改方法在命名上没有共同的前缀或后缀。此时如果使用execution切入点表达式也可以,但是会比较繁琐。 当遇到增删改的方法名没有规律时,就可以使用 annotation切入点表达式

3. 步骤
(1) 准备工作:
  • 引入AOP的起步依赖

  • 导入资料中准备好的数据库表结构,并引入对应的实体类

(2) 编码实现:
  • 自定义注解@Log

  • 定义切面类,完成记录操作日志的逻辑

4. 代码实现
(1) 实体类
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; //操作耗时
}
(2) 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});")
    public void insert(OperateLog log);

}
(3) 自定义注解@Log
java 复制代码
/**
 * 自定义Log注解
 */
@Target({ElementType.METHOD})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}
(4) 修改业务实现类,在增删改业务方法上添加@Log注解
java 复制代码
@Slf4j
@Service
public class EmpServiceImpl implements EmpService {
    @Autowired
    private EmpMapper empMapper;

    @Override
    @Log
    public void update(Emp emp) {
        emp.setUpdateTime(LocalDateTime.now()); //更新修改时间为当前时间

        empMapper.update(emp);
    }

    @Override
    @Log
    public void save(Emp emp) {
        //补全数据
        emp.setCreateTime(LocalDateTime.now());
        emp.setUpdateTime(LocalDateTime.now());
        //调用添加方法
        empMapper.insert(emp);
    }

    @Override
    @Log
    public void delete(List<Integer> ids) {
        empMapper.delete(ids);
    }

    //省略其他代码...
}
(5) 定义切面类,完成记录操作日志的逻辑
java 复制代码
@Slf4j
@Component
@Aspect //切面类
public class LogAspect {

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private OperateLogMapper operateLogMapper;

    @Around("@annotation(com.itheima.anno.Log)")
    public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
        //操作人ID - 当前登录员工ID
        //获取请求头中的jwt令牌, 解析令牌
        String jwt = request.getHeader("token");
        Claims claims = JwtUtils.parseJWT(jwt);
        Integer operateUser = (Integer) claims.get("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();

        //方法返回值
        String returnValue = JSONObject.toJSONString(result);

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


        //记录操作日志
        OperateLog operateLog = new OperateLog(null,operateUser,operateTime,className,methodName,methodParams,returnValue,costTime);
        operateLogMapper.insert(operateLog);

        log.info("AOP记录操作日志: {}" , operateLog);

        return result;
    }

}

核心逻辑:

① 为什么 HttpServletRequest 能通过 @Autowired 直接注入?

  • Spring Web 环境中,每个 HTTP 请求都会分配一个独立的线程处理,Spring 会把当前请求的 HttpServletRequest 绑定到这个线程的 ThreadLocal(线程局部变量)中
  • @Autowired 注入的 HttpServletRequest 其实是 Spring 提供的一个 "代理对象",它会从当前线程的 ThreadLocal 中获取真正的、对应本次请求的 HttpServletRequest 实例,因此能正确获取到本次请求的 token。

② 为什么要把目标方法的返回值(result)通过 JSONObject.toJSONString() 转换成 JSON 字符串。

  • 你的操作日志表(operate_log)里,记录返回值的字段(比如 return_value),在数据库中定义的类型大概率是 VARCHARTEXT(文本类型)------这种字段只能存字符串,不能直接存 Java 对象、集合、数字等复杂类型

举个例子:假设目标方法返回的是一个 User 对象(包含 id=1、name="张三"、age=20),如果不转换,直接把这个 User 对象存到数据库的 return_value 字段里,会发生什么?

  • Java 会默认把对象转成 "内存地址字符串"(比如 com.itheima.pojo.User@7852e922),存到数据库里完全看不懂;
  • 更严重的是,如果返回值是 List<User> 集合、Map 字典,直接存还可能触发数据库插入异常(因为类型不匹配)。

而通过 JSONObject.toJSONString(result) 转换后:User 对象会变成 {"id":1,"name":"张三","age":20}(JSON 字符串),List<User> 会变成 [{"id":1,"name":"张三"},{"id":2,"name":"李四"}]------ 这些都是标准字符串,既能正常存入数据库,又能直观看到内容。

相关推荐
ZH154558913112 小时前
Flutter for OpenHarmony Python学习助手实战:机器学习算法实现的实现
python·学习·flutter
承渊政道13 小时前
Linux系统学习【Linux基础开发工具】
linux·运维·笔记·学习·centos·编辑器
承渊政道13 小时前
C++学习之旅【C++中模板进阶内容介绍】
c语言·c++·笔记·学习·visual studio
浅念-13 小时前
C语言——动态内存管理
c语言·开发语言·c++·笔记·学习
ASKED_20191 天前
Langchain学习笔记一 -基础模块以及架构概览
笔记·学习·langchain
Lois_Luo1 天前
Obsidian + Picgo + Aliyun OSS 实现笔记图片自动上传图床
笔记·oss·图床
(❁´◡`❁)Jimmy(❁´◡`❁)1 天前
Exgcd 学习笔记
笔记·学习·算法
傻小胖1 天前
21.ETH-权益证明-北大肖臻老师客堂笔记
笔记·区块链
云小逸1 天前
【nmap源码学习】 Nmap网络扫描工具深度解析:从基础参数到核心扫描逻辑
网络·数据库·学习