目录
事务&AOP
1. 事务管理
1.1 事务回顾
在数据库阶段我们就介绍过事务:事务 是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
事务操作:
- 开启事务:begin /start transaction ; 一组操作开始前,开启事务
- 提交事务: commit; 全部成功后提交事务
- 回滚事务: rollback; 中间有任何一个子操作出现异常,回滚事务
1.2 案例
需求:解散部门-删除部门、同时删除部门下的员工。
我们需要完善之前删除部门的代码,最终代码实现如下:
1). DeptServiceImpl
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
@Override
public void delete(Integer id) {
//1. 删除部门
deptMapper.delete(id);
int i = 1/0;//出现异常
//2. 根据部门id, 删除部门下的员工信息
empMapper.deleteByDeptId(id);
}
}
2). EmpMapper
//根据部门ID, 删除该部门下的员工数据
@Delete("delete from emp where dept_id = #{deptId}")
void deleteByDeptId(Integer deptId);
问题:即使程序运行抛出了异常,部门依然删除了,但是部门下的员工却没有删除,造成了数据的不一致。
解散部门,应该是一个事务,这一个事务中的一组操作,要么全部成功,要么全部失败。
1.3 Spring事务管理
- 注解:@Transactional
- 位置:业务(service)层的方法上、类上、接口上
- 作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务
- 可以通过如下配置,查看详细的事务管理日志:
1). 加在方法上
@Transactional
@Override
public void delete(Integer id) {
//1. 删除部门
deptMapper.delete(id);
int i = 1/0; //出现异常
//2. 根据部门id, 删除部门下的员工信息
empMapper.deleteByDeptId(id);
}
2). 加在接口上
@Transactional
public interface DeptService {
}
3). 加在类上
@Transactional
@Service
public class DeptServiceImpl implements DeptService {
}
我们会看到,当进行部门删除时,程序报出异常,而数据库数据呢,也已经回滚了。
1.4 事务进阶
1.4.1 rollbackFor
rollbackFor属性可以控制出现何种异常类型,回滚事务。默认情况下,只有出现 RuntimeException 才回滚异常。而如果出现编译时异常,则不回滚。
可以其出现任意异常都回滚事务
1). 方案一
而这样配置的话,使用比较麻烦,故而在实际开发中一般会进行异常转换
2). 方案二
如果业务代码有编译时异常,则将其转换为运行时异常,再抛出
这样即使用方便,又不至于事务失效。当然,如果全部抛出RuntimeException 不利于调错,故而可以自定义运行时异常,并抛出自定义异常。
public class CustomerException extends RuntimeException{
public CustomerException() {
}
public CustomerException(String message) {
super(message);
}
}
@Transactional
@Override
public void delete(Integer id) throws Exception {
//1. 删除部门
deptMapper.delete(id);
try {
InputStream in = new FileInputStream("E:/1.txt");
} catch (Exception e) {
throw new Exception("出错了");
}
//2. 根据部门id, 删除部门下的员工信息
empMapper.deleteByDeptId(id);
}
1.4.2 propagation
- 事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
- 案例:
在本例中 DeptServiceImpl中的delete方法,与 EmpServiceImpl中的deleteByDeptId方法, 两个方法都加上@Transactional 注解控制事务。
由于没有配置事务传播行为,则默认事务传播行为 为 REQUIRED, 则表示 EmpServiceImpl.deleteByDeptId(),使用 DeptServiceImpl.delete() 刚才开启的事务,也就意味着这两个方法公用同一个事务,可以对其进行 统一提交和回滚操作。
1). DeptServiceImpl
@Transactional
@Override
public void delete(Integer id) throws Exception {
//1. 删除部门
deptMapper.delete(id);
//2. 根据部门id, 删除部门下的员工信息
empService.deleteByDeptId(id);
//int i = 1/0;
}
2). EmpServiceImpl
@Transactional
@Override
public void deleteByDeptId(Integer deptId) {
empMapper.deleteByDeptId(deptId);
}
- 查看运行日志:
- 使用 propagation 属性可以配置事务传播行为
属性值 |
含义 |
说明 |
REQUIRE |
【默认值】需要事务,有则加入,无则创建新事务 |
- |
REQUIRES_NEW |
需要新事务,无论有无,总是创建新事务 |
- |
SUPPORTS |
支持事务,有则加入,无则在独立的连接中运行 SQL |
结合 Hibernate、JPA 时有用,配在查询方法上 |
NOT_SUPPORTED |
不支持事务,不加入,在独立的连接中运行 SQL |
- |
MANDATORY |
必须有事务,否则抛异常 |
- |
NEVER |
必须没事务,否则抛异常 |
- |
NESTED |
嵌套事务 |
仅对 DataSourceTransactionManager 有效 |
我们主需要掌握前两个 : REQUIRED 以及 REQUIRES_NEW ,其他很少用到,无需掌握
接下来,我们再来测试一下 REQUIRES_NEW:
1). DeptServiceImpl
@Transactional
@Override
public void delete(Integer id) throws Exception {
//1. 删除部门
deptMapper.delete(id);
//2. 根据部门id, 删除部门下的员工信息
empService.deleteByDeptId(id);
int i = 1/0; //抛出异常
}
2). EmpServiceImpl
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void deleteByDeptId(Integer deptId) {
empMapper.deleteByDeptId(deptId);
}
- 查看运行日志:
此时由于 EmpServiceImpl的deleteByDeptId方法 的事务传播行为为 REQUIRES_NEW 开启了一个新事务,则不会因为 DeptServiceImpl的delete方法 出现异常而回滚。
- 作用:
-
- REQUIRED :大部分情况下都是用该传播行为即可。
- REQUIRES_NEW :当我们不希望事务之间相互影响时可以使用该传播行为。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。
2. AOP基础
2.1 记录方法执行耗时
- 需求:记录业务方法的执行耗时,并输出到控制台。
记录方法执行耗时,其实也非常简单,我们只需要在方法执行之前,获取一个开始时间戳。在方法执行完毕后,获取结束时间的时间戳。然后后者减去前者,就是方法的执行耗时。
- 具体代码如下所示:
@Override
public List<Dept> list() {
long begin = System.currentTimeMillis();
List<Dept> deptList = deptMapper.list();
long end = System.currentTimeMillis();
log.debug("方法执行耗时 : {} ms", (end-begin));
return deptList;
}
@Transactional
@Override
public void delete(Integer id) throws Exception {
long begin = System.currentTimeMillis();
//1. 删除部门
deptMapper.delete(id);
//2. 根据部门id, 删除部门下的员工信息
empService.deleteByDeptId(id);
//int i = 1/0;
long end = System.currentTimeMillis();
log.info("方法执行耗时 : {} ms", (end-begin));
}
@Override
public void save(Dept dept) {
long begin = System.currentTimeMillis();
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
deptMapper.save(dept);
long end = System.currentTimeMillis();
log.info("方法执行耗时 : {} ms", (end-begin));
}
上述功能虽然实现了,但是我们会发现,所有的方法中,代码都是固定的,存在大量的重复代码:
A. 业务方法执行之前,记录开始时间:
long begin = System.currentTimeMillis();
B. 业务方法执行之后,记录结束时间:
long end = System.currentTimeMillis();
log.info("方法执行耗时 : {} ms", (end-begin));
2.2 AOP快速入门
- AOP:Aspect Oriented Programming(面向切面编程),它的核心思想是将重复的逻辑剥离出来,在不修改原始逻辑的基础上对原始功能进行增强。
- 优势:无侵入、减少重复代码、提高开发效率、维护方便
- 我们可以通过AOP来完成上述代码的优化:
1). pom.xml 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2). 定义类抽取公共代码(执行耗时统计操作)
@Slf4j
public class TimeAspect {
public void recordTime() throws Throwable {
long begin = System.currentTimeMillis();
//调用原始操作
long end = System.currentTimeMillis();
log.info("执行耗时 : {} ms", (end-begin));
}
}
3). 标识当前类是一个AOP类,并被Spring容器管理
@Component
@Aspect
@Slf4j
public class TimeAspect {
public void recordTime() throws Throwable {
long begin = System.currentTimeMillis();
//调用原始操作
long end = System.currentTimeMillis();
log.info("执行耗时 : {} ms", (end-begin));
}
}
@Aspect:标识当前类是一个AOP类
@Component:声明该类是spring的IOC容器中的bean对象
4). 配置公共代码作用于哪些目标方法
@Component
@Aspect
@Slf4j
public class TimeAspect {
@Around("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void recordTime() throws Throwable {
long begin = System.currentTimeMillis();
//调用原始操作
long end = System.currentTimeMillis();
log.info("执行耗时 : {} ms", (end-begin));
}
}
@Around: 表示环绕通知,可以在目标方法执行前后执行一些公共代码
* 表示通配符,代表任意
.. 表示参数通配符,代表任意参数
5). 执行目标方法
@Component
@Aspect
@Slf4j
public class TimeAspect {
@Around("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
long begin = System.currentTimeMillis();
//调用原始操作
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
log.info("执行耗时 : {} ms", (end-begin));
return result;
}
}
6). 测试运行
2.3 执行流程
AOP 的一种实现方式,是通过动态代理技术实现。
- 当目标对象(此处为DeptServiceImpl)功能需要被增强时,并且我们使用AOP方式定义了增强逻辑(在Aspect类中)
- Spring会为目标对象自动生成一个代理对象,并在代理对象对应方法中,结合我们定义的AOP增强逻辑完成功能增强
2.4 AOP核心概念
- 连接点:JoinPoint,可以被AOP控制的方法执行(包含方法信息)
- 通知:Advice ,重复逻辑代码
- 切入点:PointCut ,匹配连接点的条件
- 切面:Aspect,通知+切点
3. AOP进阶
3.1 通知类型
- @Around:此注解标注的通知方法在目标方法前、后都被执行
- @Before:此注解标注的通知方法在目标方法前被执行
- @After :此注解标注的通知方法在目标方法后被执行,无论是否有异常
- @AfterReturning : 此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing : 此注解标注的通知方法发生异常后执行
@Around 需要自己调用 ProceedingJoinPoint.proceed() 来让目标方法执行,其他通知不需要考虑目标方法执行
- 切面类
@Component
@Aspect
@Slf4j
public class TimeAspect {
@Around("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
long begin = System.currentTimeMillis();
//调用原始操作
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
log.info("执行耗时 : {} ms", (end-begin));
return result;
}
@Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void before(){
log.info(" T before ....");
}
@After("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void after(){
log.info(" T after ....");
}
@AfterReturning("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void afterReturning(){
log.info("afterReturning ....");
}
@AfterThrowing("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void afterThrowing(){
log.info("afterThrowing ....");
}
}
- 目标类
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Override
public List<Dept> list() {
List<Dept> deptList = deptMapper.list();
return deptList;
}
@Override
public void delete(Integer id) {
deptMapper.delete(id);
}
@Override
public void save(Dept dept) {
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
deptMapper.save(dept);
}
}
- 测试1
程序正常运行的情况下,通知类型 @AfterThrowing 是不会运行的,但是@AfterReturning 是会运行的。
- 测试2
程序运行出现异常的情况下,通知类型 @AfterReturning 是会运行的,但是@AfterThrowing 是不会运行的。
3.2 通知顺序
当有多个切面的切点都匹配目标时,多个通知方法都会被执行。之前介绍的 pjp.proceed() 在有多个通知方法匹配时,更准确的描述应该是这样的:
- 如果还有下一个通知,则调用下一个通知
- 如果没有下一个通知,则调用目标
那么它们的执行顺序是怎样的呢?
- 默认按照 bean 的名称字母排序
- 用 @Order(数字) 加在切面类上来控制顺序
-
- 目标前的通知方法:数字小先执行
- 目标后的通知方法:数字小后执行
1). 默认顺序
@Component
@Aspect
@Slf4j
public class TimeAspect {
@Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void before(){
log.info(" T before ....");
}
}
@Component
@Aspect
@Slf4j
public class AimeAspect {
@Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void before(){
log.info("before ...." );
}
}
定义TimeAspect 和 AimeAspect 切面类,测试执行顺序,默认按照切面类的名称字母排序。此例中AimeAspect 比 TimeAspect 字母顺序排名靠前,故此,AimeAspect 先执行。
所以调用之后执行顺序为:
2). @Order(数字) 排序
- 目标前的通知方法:数字小先执行
- 目标后的通知方法:数字小后执行
@Component
@Aspect
@Slf4j
@Order(1)
public class TimeAspect {
@Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void before(){
log.info(" T before ....");
}
}
@Component
@Aspect
@Slf4j
@Order(2)
public class AimeAspect {
@Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void before(){
log.info("before ...." );
}
}
测试结果如下:
3.3 切点表达式
切点表达式用来匹配【哪些】目标方法需要应用通知,常见的切点表达式如下
- execution(返回值类型 包名.类名.方法名(参数类型))
-
- * 可以通配任意返回值类型、包名、类名、方法名、或任意类型的一个参数
- .. 可以通配任意层级的包、或任意类型、任意个数的参数
- @annotation() 根据注解匹配
- args() 根据方法参数匹配
3.3.1 execution
execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名?.方法名(方法参数) throws 异常?)
其中带 ? 的表示可以省略的部分
• 访问修饰符:可省略(没啥用,仅能匹配 public、protected、包级,private 不能增强)
• 包名.类名: 可省略
• throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
3.3.2 annotation
切点表达式也支持匹配目标方法是否有注解。使用 @annotation
@annotation(com.itheima.anno.Log)
3.3.3 @PointCut
通过@PointCut注解,可以抽取一个切入点表达式,然后再其他的地方我们就可以通过类似于 方法调用 的形式来引用该切入点表达式。
@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void pt(){}
@Around("pt()")
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
long begin = System.currentTimeMillis();
//调用原始操作
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
log.info("执行耗时 : {} ms", (end-begin));
return result;
}
3.4 连接点
连接点简单理解就是 目标方法,在Spring 中用 JoinPoint 抽象了连接点,用它可以获得方法执行时的相关信息,如方法名、方法参数类型、方法实际参数等等
- 对于 @Around 通知,获取连接点信息只能使用 ProceedingJoinPoint
- 对于其他四种通知,获取连接点信息只能使用 JoinPoint,它是 ProceedingJoinPoint 的父类型
那么如何获取这些信息呢?参考下面的代码
@Slf4j
@Aspect
@Component
public class MyAspect1 {
@Pointcut("execution(* com.itheima.service.impl.*.*(..)) && @annotation(com.itheima.anno.Log)")
public void pt(){}
@Before("pt()")
public void before(JoinPoint joinPoint){
log.info("方法名: "+joinPoint.getSignature().getName());
log.info("类名: "+joinPoint.getTarget().getClass().getName());
log.info("参数: "+Arrays.asList(joinPoint.getArgs()).toString());
log.info("before...1");
}
}
4. AOP案例
4.1 需求
将对业务类中的增、删、改 方法操作日志保存到数据库。
- 操作日志包括:
-
- 操作人
- 操作时间
- 操作全类名
- 操作方法名
- 方法参数
- 返回值
- 方法执行耗时
4.2 分析
- 需要对所有业务类中的增、删、改 方法添加统一功能,使用AOP技术最为方便
- 由于增、删、改 方法名没有规律,可以自定义@Log注解完成目标方法选取
4.3 步骤:
1). 创建操作日志表
-- 操作日志表
create table operate_log(
id int unsigned primary key auto_increment comment 'ID',
operate_user int unsigned comment '操作人',
operate_time datetime comment '操作时间',
class_name varchar(100) comment '操作的类名',
method_name varchar(100) comment '操作的方法名',
method_params varchar(1000) comment '方法参数',
return_value varchar(2000) comment '返回值',
cost_time bigint comment '方法执行耗时, 单位:ms'
) comment '操作日志表';
2). 实体类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
private Integer id; //ID
private Integer operateUser; //操作人
private LocalDateTime operateTime; //操作时间
private String className; //操作类名
private String methodName; //操作方法名
private String methodParams; //操作方法参数
private String returnValue; //操作方法返回值
private Long costTime; //操作耗时
}
3). 自定义注解
/**
* 自定义Log注解
*/
@Target({ElementType.METHOD})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}
4). 在需要记录日志的方法上加 @Log注解
@Log
@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id) throws Exception {
deptService.delete(id);
return Result.success();
}
@Log
@PostMapping
public Result save(@RequestBody Dept dept){
deptService.save(dept);
return Result.success();
}
@Log
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id){
Dept dept = deptService.getById(id);
return Result.success(dept);
}
5). 定义Mapper接口
@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);
}
6). 定义切面类
@Component
@Aspect
public class LogAspect {
@Autowired
private OperateLogMapper operateLogMapper;
@Autowired
private HttpServletRequest request;
@Around("@annotation(com.itheima.anno.Log)")
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
long begin = System.currentTimeMillis(); //开始时间
String token = request.getHeader("token");
Integer operateUser = (Integer) JwtUtils.parseJWT(token).get("id");
String className = joinPoint.getTarget().getClass().getName(); //操作类名
String methodName = joinPoint.getSignature().getName(); //操作方法名
Object[] args = joinPoint.getArgs();
String methodParams = Arrays.toString(args); //操作方法参数
//放行原始方式
Object result = joinPoint.proceed();
String returnValue = JSONObject.toJSONString(result);
long end = System.currentTimeMillis(); //结束时间
long costTime = end - begin;
OperateLog log = new OperateLog(null,operateUser, LocalDateTime.now(),className,methodName,methodParams,returnValue,costTime);
operateLogMapper.insert(log);
return result;
}
}