异常处理
在程序开发过程中, 不可避免的会遇到异常现象, 如果不处理异常, 那么程序的异常会层层传递, 直到spring抛出标准错误, 标准错误不符合我们的结果规范
手动处理: 在所有Controller的方法中添加 try/catch 处理错误, 代码臃肿, 所以并不推荐
全局异常处理器: 统一捕获程序中的所有异常, 简单优雅
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class) //指定捕获所有异常
public Result ex(Exception ex) {
// 输出堆栈信息
ex.printStackTrace();
return Result.error("对不起,出现错误,请联系管理员");
}
}
- 新建exception包, 新建GlobalExceptionHandler类
- 使用 @RestControllerAdvice 注解 注册全局异常处理器
- @RestControllerAdvice = @ControllerAdvice + @ResponseBody
- 使用 @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删除该部门下的员工数据
}
}
作用:
- 将当前方法交给spring进行事务管理, 方法执行前自动开启事务, 方法结束后自动关闭事务,
- 出现异常时自动回滚事务
使用:
- 可以在业务层(service)的方法上, 类上或者接口上使用注解
- 在方法上使用该注解, 意味着把这个方法交给spring进行事务管理
- 在类上使用该注解, 意味着把这个类的所有方法都交给spring进行事务管理
- 在接口上使用该注解, 意味着把这个接口的所有实现类的所有方法都交给spring进行事务管理
- 一般在业务层的方法中控制事务, 当一个方法需要多次操作数据时, 就要进行事务管理, 保证数据操作的一致性
开启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属性控制事务的传播行为
- 事务传播: 可以理解为, 嵌套调用的两个事物方法, 里面的事物方法与外面的事物方法的关系
- 加入事务: 可以理解为父子关系, 内层事务方法受外层事务方法的影响, 外层事务回滚会导致内层事务的回滚
- 新建事务: 可以理解为兄弟关系, 内存事务是独立于外层事务的, 不受其影响,
- 比如下单日志, 无论下单是否成功, 都要保证日志能够记录成功, 就要指定新建事务模式
示例: 解散部门时, 无论成功还是失败, 都要记录操作操作日志
@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翻译过来就是面向切面编程, 其实就是面向特定方法编程, 在不修改方法的同时, 增强或修改方法的代码逻辑
- 如果我们要统计所有业务方法的执行耗时, 比较容易想到的方案, 就是在程序执行前记录时间, 在程序执行后记录时间, 然后计算时间差, 得到程序执行耗时, 虽然可以实现, 但是相当繁琐
- 如果采用AOP技术, 我们只需要定义一个模版方法, 然后在模版方法中记录程序开始和结束时间, 就可以在不改变原始方法的同时, 得到程序耗时, 程序就变得非常优雅
- 面向切面编程是一种思想, 动态代理是实现面向切面编程的主流技术
- SpringAOP是Spring框架的高级技术, Spring实现面向切面编程的技术方案
- 旨在管理bean对象的过程中, 主要通过底层动态代理机制, 对特定方法进行增强和修改
AOP面向切面编程的优势
常见的使用AOP技术的场景
- SpringBoot中的事务管理就是基于AOP技术实现的
- 方法执行前, 自动开启事务
- 方法执行后, 自动关闭事务
开发步骤
使用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;
}
}
执行流程
- 切入点表达式指定需要被监听的方法
- 条件触发后, 程序进入AOP模版类, 执行AOP方法
- 在AOP方法内, 可以实现特定操作
核心概念
AOP中的核心概念
- 连接点: JoinPoint, 可以被AOP控制的方法(暗含方法执行时的相关信息)
- 通知: Advice, 那些重复的逻辑, 也就是共性功能(最终体现为一个方法)
- 切入点: PointCut, 匹配连接点的条件, 通知仅会在切入点方法执行时被应用
- 切面: Aspect, 描述通知与切入点的对应关系(通知 + 切入点)
- 目标对象: Target, 通知所应用的对象
AOP程序的执行流程
- SpringAOP是基于动态代理技术实现
- 通过 @Aspect 注解定义切面类, 该类就会被SpringAOP管理
- 通过 切入点表达式 指定目标对象, 在程序运行时就会自动生成目标对象的代理对象
- 在代理对象中, 就会对原始对象中的方法进行增强, 增强的逻辑就是切面类中定义的通知
- 在本案例中, 就是先记录执行前时间, 在执行目标方法,, 再记录执行后时间, 最后统计方法执行耗时, 并且返回目标方法执行的结果
- 最终, 在程序中注入目标对象时, 注入的其实是增强后的代理对象, 而不是原始的目标对象
AOP详解
通知类型
通知类型控制通知的执行时机
- @Around: 环绕通知,通知方法执行前后都被执行
- 环绕通知需要自己调用 ProceedingJoinPoint.proceed()方法 让原始方法执行, 其他通知不需要
- 环绕通知方法的返回值, 必须指定为Object, 来接收原始方法的返回值
- @Before: 前置通知,通知方法执行前被执行
- @After: 后置通知,通知方法执行后被执行,无论是否异常
- @AfterReturning: 通知方法正常执行后被执行, 有异常不执行
- @AfterThrowing: 通知方法有异常后执行
通知顺序
通知顺序: 当有多个切面的切入点都匹配到了方法, 目标方法执行时, 多个通知方法都会被执行
复用表达式
抽取切入点表达式: 通过 @PoinCut 注解将公共的切入点表达式出来, 需要的时候引用该表达式即可
- 如果切入点表达式的修饰符是 private, 则只能在当前切面类中引用
- 如果切入点表达式的修饰符是 public, 在其他外部的切面类中也可以引用该表达式
切入点表达式
切入点表达式: 描述切入点方法的一种表达式, 用来决定项目中的哪些方法需要加入通知
excution(): 根据方法的签名来匹配
主要根据方法的返回值, 包名, 类名, 方法名, 方法参数等信息来匹配
- 其中 ?表示可以省略的部分
- 访问修饰符: 建议省略(比如public, protected )
- 包名.类名: 建议不要省略, 省略后匹配的范围太大, 影响匹配效率
- throws 异常: 建议省略不写
通配符
可以使用通配符描述切入点
- *匹配单个的任意符号
- ..匹配多个连续的任意符号,一般用于描述任意包或任意参数
@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...");
}
}
建议
- 业务方法名在命名时保持规范, 方便匹配, 查询方法用find开头,更新方法用updata开头
- 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强扩展性
- 尽量缩小切入点的匹配范围, 匹配范围越大, 性能越差
- 根据业务需要, 可以使用 && || ! 来组合比较复杂的切入点表达式
@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 获取连接点信息
- 注意使用 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();
}
... ...
}
前后端联调, 操作日志被记录到数据库表中