事务是一组操作的集合,它是一个不可分割的工作单位,这些操作要么同时成功,要么同时失败。
开启事务(一组操作开始前,开启事务):start transaction / begin ;
提交事务(这组操作全部成功后,提交事务):commit ;
回滚事务(中间任何一个操作出现异常,回滚事务):rollback ;
Spring事务管理
例如有一个部门表dept,一个员工表emp,emp中有一个字段dept_id关联dept表,在某一个方法中,要解散部门:删除部门,同时删除该部门下的员工。
存在问题: 当deptMapper.deleteById(id);删除成功,empService.deleteByDeptId(id);想要执行时,发生了异常,这时候即使程序运行抛出了异常,部门也已经被删除了,但是部门下的员工却没有被删除,就会造成数据的不一致。
解决方案:在该方法上开启事务,根据事务的四大特性保证数据的一致性。
注解:@Transactional
位置:业务(service)层的方法上、类上、接口上
作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务
@Transactional(rollbackFor = Exception.class) //开启事务 未加rollbackFor:运行时异常回滚事务 @Override public void deleteById(Integer id) throws Exception { deptMapper.deleteById(id); // int i = 1 / 0; //人为异常 运行时异常 // if (true) { // throw new Exception("编译时异常"); // } empService.deleteByDeptId(id); }
#开启事务管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
事务进阶
事务属性-回滚 rollbackFor
默认情况下,只有出现 RuntimeException 才回滚异常。rollbackFor属性用于控制出现何种异常类型,回滚事务。
事务属性-传播行为 propagation
事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
属性值 | 含义 |
REQUIRED | 【默认值】需要事务,有则加入,无则创建新事务 |
REQUIRES_NEW | 需要新事务,无论有无,总是创建新事务 |
SUPPORTS | 支持事务,有则加入,无则在无事务状态中运行 |
NOT_SUPPORTED | 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务 |
MANDATORY | 必须有事务,否则抛异常 |
NEVER | 必须没事务,否则抛异常 |
… |
REQUIRED :大部分情况下都是用该传播行为即可。
REQUIRES_NEW :当我们不希望事务之间相互影响时,可以使用该传播行为。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。
例如:在上述问题中,需要实现以下功能:解散部门时,无论成功失败,记录操作日志
@Transactional(rollbackFor = Exception.class) //开启事务 未加rollbackFor:运行时异常回滚事务 @Override public void deleteById(Integer id) throws Exception { // try { deptMapper.deleteById(id); // int i = 1 / 0; // if (true) { // throw new Exception("编译时异常"); // } empService.deleteByDeptId(id); } finally { DeptLog log = new DeptLog(); log.setCreateTime(LocalDateTime.now()); log.setDescription("删除部门id:" + id + ",并删除部门下的员工"); deptLogService.insert(log); }}
@Transactional(propagation = Propagation.REQUIRES_NEW) @Override public void insert(DeptLog deptLog) { deptLogMapper.insert(deptLog); }
AOP
面向切面编程,其实就是面向特定方法编程
场景
案例部分功能运行较慢,定位执行耗时较长的业务方法,此时需要统计每一个业务方法的执行耗时。
实现:
动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
场景使用
记录操作日志、权限控制、事务管理
优势:
代码无侵入、减少重复代码、提高开发效率、维护方便
@Autowired private OperateLogMapper operateLogMapper; @Autowired private HttpServletRequest request; /* 0. 创建数据表,创建对应的实体类以及 mapper【我给你提供】 1. 先创建一个切面类【@Component @Aspect】 2. 定义一个方法,方法上面通知类型为@Around(注解的方式去匹配对应的方法) // 2.1 先创建一个注解 // 2.2 在对应的方法【实现类的方法】上面添加注解 */ @Around("pt()") public Object around(ProceedingJoinPoint pj) throws Throwable { log.info("从此处开始AOP方法"); // 3. 方法中 // 3.1 我们需要获取当前操作人【从request中获取token,然后解析token ,把解析结果强转为map】 // String token = (String) args[args.length -1]; String token = request.getHeader("token"); Map<String, Object> map = (Map<String, Object>) JWTUtils.parseJWTStr(token); Integer operateUser = (Integer) map.get("id"); // Integer operateUser = (Integer) args[args.length -1]; Object[] args = pj.getArgs(); // 3.2 操作时间: 当前的时间 LocalDateTime operateTime = LocalDateTime.now(); // 3.3 获取方法的相关信息 String className = pj.getTarget().getClass().getName(); String methodName = pj.getSignature().getName(); // 3.4 定义一个开始的时间 long methodBeginTime = System.currentTimeMillis(); // 3.5 调用连接点的方法 Object result = pj.proceed(); // 3.6 定义一个结束时间,统计时长 long methodEndTime = System.currentTimeMillis(); // 3.6 把这些信息保存到对象中,然后再插入到数据库 OperateLog operateLog = new OperateLog(null,operateUser,operateTime, className, methodName, Arrays.toString(args), (String)result, (methodEndTime - methodBeginTime)); operateLogMapper.insert(operateLog); log.info("AOP方法到此结束"); // 3.7 返回连接点执行完方法之后的结果 return result; }
AOP核心概念
AOP进阶
执行顺序
@Around -> @Before->@AfterReturning/@AfterThrowing->@After->@Around
也可以使用@Order(数字值)来自定义执行顺序
切入点表达式-execution
execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带 ? 的表示可以省略的部分
访问修饰符:可省略(比如: public、protected)
包名.类名: 可省略
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
可以使用通配符描述切入点
* :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
execution(* com.*.service.*.update*(*))
.. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
execution(* com.it..DeptService.*(..))
注意:
根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。
书写建议
所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是 update开头。
描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。
在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 ..,使用 * 匹配单个包。