[Java]SpringBoot业务代码增强

异常处理

在程序开发过程中, 不可避免的会遇到异常现象, 如果不处理异常, 那么程序的异常会层层传递, 直到spring抛出标准错误, 标准错误不符合我们的结果规范

手动处理: 在所有Controller的方法中添加 try/catch 处理错误, 代码臃肿, 所以并不推荐

全局异常处理器: 统一捕获程序中的所有异常, 简单优雅


@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)  //指定捕获所有异常
    public Result ex(Exception ex) {
        // 输出堆栈信息
        ex.printStackTrace();
        return Result.error("对不起,出现错误,请联系管理员");
    }

}
  1. 新建exception包, 新建GlobalExceptionHandler类
  2. 使用 @RestControllerAdvice 注解 注册全局异常处理器
  3. @RestControllerAdvice = @ControllerAdvice + @ResponseBody
  4. 使用 @ExceptionHandler注解 指定需要捕获的异常类型

事务管理

事务是 一组操作的集合, 保证操作同时成功或失败, 避免出现数据操作不一致

在SpringBoot中提供了 @Transactional 注解, 用于事务的管理, 可以自动开启事务/关闭事务/事务回滚

@Service
public class DeptServiceImpl implements DeptService {

    @Autowired
    private DeptMapper deptMapper;

    @Autowired
    private EmpMapper empMapper;

    //进行事务管理,保证数据操作的同步
    @Transactional 
    public void delete(Integer id) {

      deptMapper.deleteById(id);  //根据id删除部门数据
      int i = 1 / 0;  //模拟异常
      empMapper.deleteByDeptId(id);  //根据部门id删除该部门下的员工数据

    }
}

作用:

  1. 将当前方法交给spring进行事务管理, 方法执行前自动开启事务, 方法结束后自动关闭事务,
  2. 出现异常时自动回滚事务

使用:

  1. 可以在业务层(service)的方法上, 类上或者接口上使用注解
  2. 在方法上使用该注解, 意味着把这个方法交给spring进行事务管理
  3. 在类上使用该注解, 意味着把这个类的所有方法都交给spring进行事务管理
  4. 在接口上使用该注解, 意味着把这个接口的所有实现类的所有方法都交给spring进行事务管理
  5. 一般在业务层的方法中控制事务, 当一个方法需要多次操作数据时, 就要进行事务管理, 保证数据操作的一致性

开启spring事务管理日志

#开启事务管理日志
logging:
  level:
    org.springframework.jdbc.support.JdbcTransactionManager: debug

默认只有RuntimeException(运行时异常)才会回滚事务, 可以通过rollbackFor属性控制回滚的的异常类型

@Service
public class DeptServiceImpl implements DeptService {

    @Autowired
    private DeptMapper deptMapper;

    @Autowired
    private EmpMapper empMapper;

    // 默认只有发生运行时异常才会回滚
    // 指定为所有异常都会回滚
    @Transactional(rollbackFor = Exception.class)
    public void delete(Integer id) {

      deptMapper.deleteById(id);  //根据id删除部门数据
      int i = 1 / 0;  //模拟异常
      empMapper.deleteByDeptId(id);  //根据部门id删除该部门下的员工数据

    }
}

事务传播行为: 当一个事务方法被另一个事务方法调用时, 这个事务方法应该如何进行事务控制

可以通过propagation属性控制事务的传播行为

  1. 事务传播: 可以理解为, 嵌套调用的两个事物方法, 里面的事物方法与外面的事物方法的关系
  2. 加入事务: 可以理解为父子关系, 内层事务方法受外层事务方法的影响, 外层事务回滚会导致内层事务的回滚
  3. 新建事务: 可以理解为兄弟关系, 内存事务是独立于外层事务的, 不受其影响,
  4. 比如下单日志, 无论下单是否成功, 都要保证日志能够记录成功, 就要指定新建事务模式

示例: 解散部门时, 无论成功还是失败, 都要记录操作操作日志

@Service
public class DeptServiceImpl implements DeptService {

    @Autowired
    private DeptMapper deptMapper;

    @Autowired
    private EmpMapper empMapper;

    @Autowired
    private DeptLogService deptLogService;

    //进行事务管理,保证数据同步
    @Transactional(rollbackFor = Exception.class)  
    public void delete(Integer id) {

        try {
            //根据id删除部门数据
            deptMapper.deleteById(id);  
            //模拟异常
            int i = 1 / 0;  
            //根据部门id删除该部门下的员工数据
            empMapper.deleteByDeptId(id);  
        } finally {
            DeptLog deptLog = new DeptLog();
            deptLog.setCreateTime(LocalDateTime.now());
            deptLog.setDescription("解散部门的操作,解散的是" + id + "号部门");
            // 记录解散部门的操作日志
            // 该方法也是一个事务方法
            deptLogService.insert(deptLog);
        }

    }
}
@Service
public class DeptLogServiceImpl implements DeptLogService {

    @Autowired
    private DeptLogMapper deptLogMapper;

    // 指定事务传播模式为 新建事务
    // 保证这个事务方法是独立的, 不会因为其他事务的回滚收到影响
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void insert(DeptLog deptLog) {
        deptLogMapper.insert(deptLog);
    }
}

控制台日志高亮插件: 可以选择日志类型, 高亮显示该类型的控制台日志

AOP

介绍

Aspect Oriented Programming翻译过来就是面向切面编程, 其实就是面向特定方法编程, 在不修改方法的同时, 增强或修改方法的代码逻辑

  1. 如果我们要统计所有业务方法的执行耗时, 比较容易想到的方案, 就是在程序执行前记录时间, 在程序执行后记录时间, 然后计算时间差, 得到程序执行耗时, 虽然可以实现, 但是相当繁琐
  2. 如果采用AOP技术, 我们只需要定义一个模版方法, 然后在模版方法中记录程序开始和结束时间, 就可以在不改变原始方法的同时, 得到程序耗时, 程序就变得非常优雅
  3. 面向切面编程是一种思想, 动态代理是实现面向切面编程的主流技术
  4. SpringAOP是Spring框架的高级技术, Spring实现面向切面编程的技术方案
  5. 旨在管理bean对象的过程中, 主要通过底层动态代理机制, 对特定方法进行增强和修改

AOP面向切面编程的优势

常见的使用AOP技术的场景

  1. SpringBoot中的事务管理就是基于AOP技术实现的
  2. 方法执行前, 自动开启事务
  3. 方法执行后, 自动关闭事务

开发步骤

使用SpringAOP完成面向切面编程, 首先要引入AOP依赖, 然后编写AOP程序, 完成对特定方法的编程

// 引入SpringAOP依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
// 编写AOP程序
@Component
@Slf4j
@Aspect //定义AOP类
public class TimeAspect {
    // 切入点表达式: 决定切面的生效范围
    @Around("execution(* com.itheima.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;
    }

}

执行流程

  1. 切入点表达式指定需要被监听的方法
  2. 条件触发后, 程序进入AOP模版类, 执行AOP方法
  3. 在AOP方法内, 可以实现特定操作

核心概念

AOP中的核心概念

  1. 连接点: JoinPoint, 可以被AOP控制的方法(暗含方法执行时的相关信息)
  2. 通知: Advice, 那些重复的逻辑, 也就是共性功能(最终体现为一个方法)
  3. 切入点: PointCut, 匹配连接点的条件, 通知仅会在切入点方法执行时被应用
  4. 切面: Aspect, 描述通知与切入点的对应关系(通知 + 切入点)
  5. 目标对象: Target, 通知所应用的对象

AOP程序的执行流程

  1. SpringAOP是基于动态代理技术实现
  2. 通过 @Aspect 注解定义切面类, 该类就会被SpringAOP管理
  3. 通过 切入点表达式 指定目标对象, 在程序运行时就会自动生成目标对象的代理对象
  4. 在代理对象中, 就会对原始对象中的方法进行增强, 增强的逻辑就是切面类中定义的通知
  5. 在本案例中, 就是先记录执行前时间, 在执行目标方法,, 再记录执行后时间, 最后统计方法执行耗时, 并且返回目标方法执行的结果
  6. 最终, 在程序中注入目标对象时, 注入的其实是增强后的代理对象, 而不是原始的目标对象

AOP详解

通知类型

通知类型控制通知的执行时机

  1. @Around: 环绕通知,通知方法执行前后都被执行
  • 环绕通知需要自己调用 ProceedingJoinPoint.proceed()方法 让原始方法执行, 其他通知不需要
  • 环绕通知方法的返回值, 必须指定为Object, 来接收原始方法的返回值
  1. @Before: 前置通知,通知方法执行前被执行
  2. @After: 后置通知,通知方法执行后被执行,无论是否异常
  3. @AfterReturning: 通知方法正常执行后被执行, 有异常不执行
  4. @AfterThrowing: 通知方法有异常后执行

通知顺序

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

复用表达式

抽取切入点表达式: 通过 @PoinCut 注解将公共的切入点表达式出来, 需要的时候引用该表达式即可

  1. 如果切入点表达式的修饰符是 private, 则只能在当前切面类中引用
  2. 如果切入点表达式的修饰符是 public, 在其他外部的切面类中也可以引用该表达式

切入点表达式

切入点表达式: 描述切入点方法的一种表达式, 用来决定项目中的哪些方法需要加入通知

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

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

  1. 其中 ?表示可以省略的部分
  2. 访问修饰符: 建议省略(比如public, protected )
  3. 包名.类名: 建议不要省略, 省略后匹配的范围太大, 影响匹配效率
  4. throws 异常: 建议省略不写

通配符

可以使用通配符描述切入点

  1. *匹配单个的任意符号
  2. ..匹配多个连续的任意符号,一般用于描述任意包或任意参数
@Slf4j
@Aspect
@Compoment
public class MyAspect6 {
    // DeptServiceImpl这个类下的delete方法生效, 并且这个方法返回值要是void
    @Pointcut("execution(public void com.itheima.server.impl.DeptServiceImpl.delete(java.lang.Interger))")
    // 匹配com包下的所有方法
    @Pointcut("execution(* com..*.*(..))")
    // 匹配程序中的所有方法(慎用)
    @Pointcut("execution(* *(..))")
    // 匹配符合条件的list方法或者delete方法
    @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("...执行before...");
    }
}

建议

  1. 业务方法名在命名时保持规范, 方便匹配, 查询方法用find开头,更新方法用updata开头
  2. 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强扩展性
  3. 尽量缩小切入点的匹配范围, 匹配范围越大, 性能越差
  4. 根据业务需要, 可以使用 && || ! 来组合比较复杂的切入点表达式
@annotation(...): 根据注解匹配

适用于切入点表达式过于复杂时使用

// 自定义注解
@Retention(RetentionPolicy.RUNTIME)  //指定运行时注解生效
@Target(ElementType.METHOD)  //指定注解生效的范围,此为方法
// 注意是注解
public @interface MyLog {}
@Slf4j
@Server
public class DeptServiceImpl implements DeptService {
    // 加上@MyLog注解
    @MyLog
    public List<Dept> list() {
        ... ...
    }
}
@Slf4j
@Aspect
@Compoment
public class MyAspect6 {
    // 匹配有MyLog注解的方法
    @Pointcut("@annotation(com.itheima.aop.MyLog)")
    private void pt(){}

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

连接点

连接点就是指所有被SpringAOP管理的方法

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

@Around通知类型: 必须使用 ProceedingJoinPoint 获取连接点信息

其他4种通知类型, 使用 JoinPoint 获取连接点信息

  1. 注意使用 org.aspectj.lang 包下的 Joinpoint连接点对象

综合案例

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

引入AOP依赖

 <!-- AOP -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

新建日志操作表(资料中提供)

准备实体类(资料中提供)

@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; //操作耗时
}

准备mapper接口(资料中提供)

@Mapper
public interface DeptLogMapper {

    @Insert("insert into dept_log(create_time,description) values(#{createTime},#{description})")
    void insert(DeptLog log);

}

新增自定义注解

@Retention(RetentionPolicy.RUNTIME)  //指定自定义注解的生效时机
@Target(ElementType.METHOD)  //指定自定义注解生效的范围
public @interface Log { }

创建切面类, 编写通知逻辑

@Slf4j
@Component
@Aspect  //标明是切面类
public class LogAspect {

    @Autowired
    private OperateLogMapper operateLogMapper;

    @Autowired
    // 注入请求对象, 通过请求对象解析JWT
    private HttpServletRequest request;

    @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");  //拿到员工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类型
        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;
    }
}

应用通知: 给所有需要记录操作日志的方法, 添加自定义注解

/**
 * 部门管理Controller
 */
@Slf4j
@RestController
@RequestMapping("/depts")
public class DeptController {

    @Autowired
    private DeptService deptService;

    /**
     * 根据id删除部门信息
     */
    @Log
    @DeleteMapping("/{id}")
    public Result delete(@PathVariable Integer id) {
        log.info("删除部门的id:{}", id);
        deptService.delete(id);
        return Result.success();
    }

    ... ...
}

前后端联调, 操作日志被记录到数据库表中

  • 9
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值