本篇主要介绍项目开发中两个比较基础但是非常重要的模块,异常管理和事务管理,如何去使用进行了介绍,着重对AOP的使用进行了介绍,实现一个基于AOP的简单案例:定位耗时较长的业务方法,统计部分业务方法的执行耗时。
一、全局异常处理器
需求:软件开发springboot项目过程中,不可避免的需要处理各种异常,spring mvc架构中各层会出现大量的try{...} catch{...} finally{...}代码块,不仅有大量的冗余代码,而且还影响代码的可读性。这样就需要定义个全局统一异常处理器,以便业务层再也不必处理异常。
具体例子如下:
在部门管理系统中,存在了一个就业部,此时再添加一个就业部之后,就会响应500:
查看服务端报错显示duplicate,也就是重复了,因为在数据库建表的时候,给name设置了unique约束,唯一约束。所以出现异常的时候前端不能对返回的结果进行正确的接收以及处理。
所以要进行异常处理:
首先出现了异常其实是一步步向上抛的,mapper向service抛,然后service向controller层抛,然后controller层向上抛,就产生了这种类似500等错误。如果全写到controller层中try...catch...处理,代码非常臃肿,所以最好的方法就是定义一个全局异常处理器,在controller层抛出之后丢给全局异常处理器,然后经过处理之后,返回设定好的类型json格式数据给前端响应过去。
实现方式:
用@RestControllerAdvice+@ExceptionHandler来实现全局异常处理器:
异常处理最好是解耦的,并且都放在一个地方集中管理:
@ExceptionHandler:统一处理某一类异常,从而能够减少代码重复率和复杂度
@RestControllerAdvice:异常集中处理,更好的使业务逻辑与异常处理剥离开,这个注解包含两部分@RestControllerAdvice=@ControllerAdvice+@ResponseBody;可以将java对象数据类型转化为json格式的数据相应给前端。
@ResponseStatus:可以将某种异常映射为HTTP状态码
只使用@ExceptionHandler,只能在当前Controller中处理异常,与@ControllerAdvice结合可以实现全局异常处理,不用每个controller都配置。
下面是一个简单的例子,其中也可以去自定义异常然后在方法中进行判断,然后响应给前端相应的信息:
@RestControllerAdvice
public class GlobeExceptionHandler {
@ExceptionHandler(Exception.class)
public Result ex(Exception ex){
ex.printStackTrace();
return Result.error("对不起,操作失败,请联系管理员");
}
}
二、事务管理
事务具有ACID(原子性,一致性,隔离性,持久性)四个特性,将多个操作合并为一个整体,如果过程中出现了异常,进行rollback回滚,保证全部成功或者全部失败。
同样举个例子来展开说明:
删除部门操作:删除部门后,部门内的员工也要同时删除,但是此时有一些风险,在删除完部门之后发生了异常无法执行删除员工操作,但是得到的结果是删除了部门,员工没有删除,所以要将删除部门和删除员工归为一个事务,要么同时成功要么同时失败;此时就用到了事务,使用@Transactional注解实现:
rollbackFor属性:
如果在代码部分加上1/0这个异常,抛出异常之后,还是会出现删除掉了部门但是没有删除该部门的员工,因为默认情况下只有出现RuntimeException才回滚异常。
想要将所有的异常都回滚,就要使用rollbackFor属性,案例代码如下:
Controller层:
//TODO 删除指定部门:
@MyLog
//@DeleteMapping("/depts/{id}")
@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id){//PathBariable注解将路径中的id绑定到参数id;
log.info("根据参数id删除部门:{}",id);
deptService.delete(id);
return Result.success();
}
Service层:
@Transactional(rollbackFor = Exception.class)//spring事务管理
@Override
public void delete(Integer id) {
try {
deptMapper.delete(id);
//现在进行完善删除部门操作,继续删除该部门所在的员工:
empMapper.deleteByDeptId(id);
}finally {//继续完善操作,记录操作的日志,不管删除成功与否,都将操作记录下来。因为是成功失败都要记录,所以要将这部分代码加到finally中
DeptLog deptLog=new DeptLog();
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("执行了解散部门的操作,此次解散的是"+id+"号部门");
deptLogService.insert(deptLog);
}
}
propagation属性:
事务传播行为:当一个事务中调用了另一个事务的方法,这个方法该如何去进行事务控制。
属性值需要注意REQUIRED和REQUIRES_NEW两个:
案例如下:
finally中:上方service层中调用了setDescription记录操作日志的方法,这个是必须要执行的,无论是否发生异常,但是仔细想如果前面失败这里也要执行的话,这个方法需要用到事务管理,并且需要自己新开一个事务,因为如果跟着这个delete方法事务的话,执行失败了还会进行回滚,所以执行的写入数据库的操作也会回滚,记录也就消失了,所以需要新开一个事务,要用到propagation属性,方法如下:
@Service
public class DeptLogServiceImpl implements DeptLogService {
@Autowired
private DeptLogMapper deptLogMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
}
上面事务管理的底层就是通过AOP来实现的,将代码的前面和后面添加开启事务和回滚事务或者提交事务。
AOP
面向切面编程(AOP),本质上就是面向特定的方法编程。
采用AOP其实是生成了一个代理对象,将目标对象的方法进行了扩充,然后后面使用依赖注入其实注入的是代理对象,最后执行的也是代理对象中的方法。
AOP的优点就是:代码无侵入,减少重复代码,提高开发效率,维护方便。
AOP核心概念:
切入点就是实际被AOP增强的方法。
例如计算程序运行时间,那么通知就是这些重复的逻辑,给方法前面加上时间后面加上时间,最后相减;
连接点:可以被AOP控制的方法
通知所应用的对象就是目标对象,如下代码中DeptserviceImpl就是目标对象
通知类型:
切入点表达式:
@Pointcut注解:有多个连接点方法时候比较方便:
@Slf4j
@Component
//@Aspect
public class MyAspect1 {
//可以采用@Pointcut注解,将公共部分的切入点表达式抽取出来,下面直接引用这个切入点表达式,就不用一个个都写了,如果要改的话,只需要改这一个地方即可;
@Pointcut("execution(* com.springboot_test2.service.impl.DeptServiceImpl.*(..))")
private void pt(){}//此处用private修饰,只能在这个切面类中调用,如果改为public的话,可以在TImeAspect切面类中使用。
@Before("pt()")
public void before_test(){
log.info("before...");
}
@Around("pt()")
public Object around_test(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
log.info("around after...");
return result;
}
@After("pt()")
public void after_test(){
log.info("after...");
}
@AfterReturning("pt()")
public void afterReturning_test(){
log.info("afterReturning...");
}
@AfterThrowing("pt()")
public void afterThrowing_test(){
log.info("afterThrowing...");
}
}
执行顺序:
由案例展开介绍:
部分业务耗时较长,在逐个方法中去实现计时代码非常冗余,所以想把这部分通用的业务代码抽离出来,无需每个方法都去单独写一个计时操作。这就可以使用AOP来实现:
首先使用AOP之前需要导入AOP的依赖,然后根据业务需要编写AOP程序。
然后在业务类上需要添加@Component注解,将该对象注入到容器中;添加@Aspect注解,表示是AOP切面类。中间@Around注解里面的内容代表想要执行哪一个方法。
代码如下:自定义一个MyLog annotation搭配使用
@Slf4j
@Component
@Aspect
public class LogAspect {
//TODO 这个过程中需要调用OperateLogMapper中的方法插入日志表,所以要进行依赖注入
@Autowired
private OperateLogMapper operateLogMapper;
@Autowired
private HttpServletRequest request;
@Around("@annotation(com.springboot_test.anno.MyLog)")//这样指的是匹配加了Mylog注解的方法。
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
//操作人ID以及姓名-当前登录员工的ID: 想要获取ID和姓名可以想到在完成登录后会生成jwt令牌,jwt令牌中携带username以及id,可以解析jwt令牌然后获取这两个数据:
//获取jwt令牌得需要先获取请求对象,HttpServletRequest;
String jwt=request.getHeader("token");
Claims claims = JwtUtils.parseJWT(jwt);
Integer id = (Integer) claims.get("id");
String username=(String)claims.get("username");
//操作时间
LocalDateTime now = LocalDateTime.now();
//操作类名
String className = joinPoint.getClass().getName();
//操作方法名
String methodName = joinPoint.getSignature().getName();
//方法参数
Object[] args = joinPoint.getArgs();
String paramName = 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,id,username,now,className,methodName,paramName,returnValue,costTime);
operateLogMapper.insert(operateLog);
log.info("AOP记录操作日志"+operateLog);
return result;
}
}
标签:事务管理,...,方法,id,AOP,异常,public
From: https://blog.csdn.net/m0_57957187/article/details/143715107