day28 SpringBootWeb (four) transaction & AOP

Table of contents

Transaction & AOP

1. Transaction management

1.1 Transaction review

1.2 case

1.3 Spring transaction management

1.4 Advanced transaction

2. AOP basics

2.1 Record method execution time-consuming

2.2 AOP quick start

2.3 Execution process

2.4 AOP Core Concepts

3. Advanced AOP

3.1 Types of notifications

3.2 Notification sequence

3.3 Pointcut expressions

3.4 Connection points

4. AOP case

4.1 Requirements

4.2 Analysis

4.3 Steps:


Transaction & AOP

1. Transaction management

1.1 Transaction review

We have introduced transactions in the database stage: a transaction is a set of operations, which is an indivisible unit of work, and a transaction will submit or revoke an operation request to the system together with all operations as a whole, that is, these operations either succeed at the same time , or both fail.

Transaction operation:
 

  • Start a transaction: begin /start transaction; before a set of operations starts, start a transaction
  • Commit transaction: commit; Commit the transaction after all succeed
  • Rollback transaction: rollback; If there is an exception in any sub-operation in the middle, rollback the transaction
     

1.2 case

Requirement: Disband the department - delete the department and delete the employees under the department at the same time.

We need to improve the code for deleting the department before, and the final code implementation is as follows:
 

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);

Problem: Even if the program runs and throws an exception, the department is still deleted, but the employees under the department are not deleted, resulting in data inconsistency.

Disbanding a department should be a transaction, and a group of operations in this transaction either all succeed or all fail.
 

1.3 Spring transaction management

  • Annotation: @Transactional
  • Location: on the method, class, and interface of the business (service) layer
  • Function: Hand over the current method to spring for transaction management. Before the method is executed, start the transaction; after successful execution, submit the transaction; if an exception occurs, roll back the transaction
  • You can view detailed transaction management logs through the following configuration:

1). Add to the method

@Transactional
@Override
public void delete(Integer id) {
    //1. 删除部门
    deptMapper.delete(id);

    int i = 1/0;  //出现异常

    //2. 根据部门id, 删除部门下的员工信息
    empMapper.deleteByDeptId(id);
}

2). Add to the interface
 

@Transactional
public interface DeptService {

}

3). Add to the class
 

@Transactional
@Service
public class DeptServiceImpl implements DeptService {

}

We will see that when the department is deleted, the program reports an exception, and the database data has also been rolled back.

1.4 Advanced transaction

1.4.1 rollbackFor

The rollbackFor attribute can control what type of exception occurs and roll back the transaction. By default, the exception is rolled back only if a RuntimeException occurs. And if there is a compile-time exception, there is no rollback.

You can roll back the transaction if any exception occurs
 

1). Option 1

However, if this configuration is used, it is more troublesome to use, so in actual development, exception conversion is generally performed
 

2). Option 2

If the business code has a compile-time exception, convert it to a runtime exception and throw

In this way, it is convenient to use and will not cause the transaction to fail. Of course, if all RuntimeExceptions are thrown, it is not conducive to debugging, so you can customize runtime exceptions and throw custom exceptions.

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
 

  • Transaction propagation behavior: refers to how a transaction method should perform transaction control when a transaction method is called by another transaction method.
  • Case:
    In this example, the delete method in DeptServiceImpl and the deleteByDeptId method in EmpServiceImpl are added with @Transactional annotation to control transactions.
    Since the transaction propagation behavior is not configured, the default transaction propagation behavior is REQUIRED, which means that EmpServiceImpl.deleteByDeptId() uses the transaction just opened by DeptServiceImpl.delete(), which means that the two methods share the same transaction, which can be Perform unified commit and rollback operations.

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);
}

  • View the operation log:

  • Transaction propagation behavior can be configured using the propagation property

attribute value

meaning

illustrate

REQUIRE

[Default value] requires a transaction, join if there is one, and create a new transaction if there is no

-

REQUIRES_NEW

A new transaction is required, and new transactions are always created, whether present or not

-

SUPPORTS

Support transactions, join if there is one, run SQL in a separate connection if not

Useful when combined with Hibernate and JPA, with query methods

NOT_SUPPORTED

Does not support transactions, does not join, runs SQL in a separate connection

-

MANDATORY

There must be a transaction, otherwise an exception will be thrown

-

NEVER

There must be no transaction, otherwise an exception will be thrown

-

NESTED

nested transaction

Only valid for DataSourceTransactionManager

We mainly need to master the first two: REQUIRED and REQUIRES_NEW, the others are rarely used and do not need to be mastered
 

Next, let's test REQUIRES_NEW again:

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);
}

  • View the operation log:

At this time, since the transaction propagation behavior of the deleteByDeptId method of EmpServiceImpl opens a new transaction for REQUIRES_NEW, it will not be rolled back because of an exception in the delete method of DeptServiceImpl.
 

  • effect:
    • REQUIRED : In most cases, this propagation behavior is sufficient.
    • REQUIRES_NEW : This propagation behavior can be used when we don't want transactions to affect each other. For example: before placing an order, a log needs to be recorded. Regardless of whether the order is saved successfully or not, it is necessary to ensure that the log record can be recorded successfully.

2. AOP basics

2.1 Record method execution time-consuming

  • Requirement: Record the execution time of the business method and output it to the console.
     

Recording the execution time of a method is actually very simple. We only need to obtain a start timestamp before the method is executed. After the method finishes executing, get the timestamp of the end time. Then the latter minus the former is the execution time of the method.

  • The specific code is as follows:
@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));
}

Although the above functions are realized, we will find that in all methods, the code is fixed, and there are a lot of repeated codes:
 

A. Before the business method is executed, record the start time:

long begin = System.currentTimeMillis();

B. After the business method is executed, record the end time:

long end = System.currentTimeMillis();
log.info("方法执行耗时 : {} ms", (end-begin));

2.2 AOP quick start
 

  • AOP: Aspect Oriented Programming (aspect-oriented programming), its core idea is to strip out the repeated logic and enhance the original function without modifying the original logic.
  • Advantages: no intrusion, reduced duplication of code, improved development efficiency, easy maintenance
  • We can use AOP to complete the optimization of the above code:

1). Pom.xml introduces dependencies
 

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

2). Define classes to extract public codes (perform time-consuming statistical operations)

@Slf4j
public class TimeAspect {
	
    public void recordTime() throws Throwable {
        long begin = System.currentTimeMillis();
        
        //调用原始操作
        
        
        long end = System.currentTimeMillis();
        log.info("执行耗时 : {} ms", (end-begin));
    }
	
}

3). Identify that the current class is an AOP class and is managed by the Spring container

@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: Identifies that the current class is an AOP class

@Component: Declare that this class is a bean object in spring's IOC container
 

4). Configure which target methods the common code acts on

@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: Represents a surround notification, which can execute some common code before and after the execution of the target method

* Indicates a wildcard character, representing any

.. represents a parameter wildcard, representing any parameter

5). Execute the target method
 

@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). Test run

2.3 Execution process
 

One way to realize AOP is through dynamic proxy technology.
 

  • When the function of the target object (here DeptServiceImpl) needs to be enhanced, and we use AOP to define the enhancement logic (in the Aspect class)
  • Spring will automatically generate a proxy object for the target object, and in the corresponding method of the proxy object, combine the AOP enhancement logic we defined to complete the function enhancement

2.4 AOP Core Concepts

  • Connection point: JoinPoint, which can be executed by AOP-controlled methods (including method information)
  • Notification: Advice, repeat logic code
  • Entry point: PointCut, matching the condition of the join point
  • Aspect: Aspect, notification + pointcut

3. Advanced AOP
 

3.1 Types of notifications
 

  • @Around: The notification method annotated by this annotation is executed before and after the target method
  • @Before: The notification method annotated by this annotation is executed before the target method
  • @After : The notification method annotated by this annotation is executed after the target method, regardless of whether there is an exception
  • @AfterReturning : The notification method marked by this annotation is executed after the target method, and will not be executed if there is an exception
  • @AfterThrowing : The notification method annotated by this annotation is executed after an exception occurs

@Around needs to call ProceedingJoinPoint.proceed() to let the target method execute, and other notifications do not need to consider the target method execution

  • Noodles
@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 ....");
    }
}
  • target class
     
@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);
    }
}
  • test 1

When the program is running normally, the notification type @AfterThrowing will not run, but @AfterReturning will.

  • test 2

When the program runs abnormally, the notification type @AfterReturning will run, but @AfterThrowing will not run.

3.2 Notification sequence
 

When the pointcuts of multiple aspects all match the target, multiple advice methods will be executed. When there are multiple notification methods matching the pjp.proceed() introduced earlier, a more accurate description should be as follows:

  • If there is a next notification, call the next notification
  • If there is no next notification, the target is called

So what is the order of their execution?

  • Defaults to sorting alphabetically by bean name
  • Add @Order (number) to the aspect class to control the order
    • Notification method before the goal: small numbers first
    • Notification method after the target: Execute after the number is small

1). Default order

@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 ...." );
    }

}

Define the TimeAspect and AimeAspect aspect classes, and the test execution order is alphabetically sorted by the name of the aspect class by default. In this example, AimeAspect is ranked higher alphabetically than TimeAspect, so AimeAspect is executed first.

So the execution sequence after calling is:
 


 

2). @Order(number) sorting

  • Notification method before the goal: small numbers first
  • Notification method after the target: Execute after the number is small
@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 ...." );
    }

}

The test results are as follows:
 

3.3 Pointcut expressions

Pointcut expressions are used to match [which] target methods require application notifications. Common pointcut expressions are as follows

  • execution (return value type package name. class name. method name (parameter type))
    • * You can wildcard any return value type, package name, class name, method name, or a parameter of any type
    • .. You can wildcard any level of package, or any type, any number of parameters
  • @annotation() matches according to the annotation
  • args() matches against method arguments

3.3.1 execution

execution is mainly matched according to the return value of the method, package name, class name, method name, method parameters and other information. The syntax is:

execution(访问修饰符?  返回值  包名.类名?.方法名(方法参数) throws 异常?)

Among them, the part with ? indicates the part that can be omitted
 

• Access modifier: can be omitted (useless, can only match public, protected, package level, private cannot be enhanced)
 

• Package name. Class name: can be omitted

• throws exception: can be omitted (note that it is the exception declared on the method, not the actual exception thrown)

3.3.2 annotation

Pointcut expressions also support matching whether the target method has annotations. use @annotation

@annotation(com.itheima.anno.Log)

3.3.3 @PointCut

Through the @PointCut annotation, a pointcut expression can be extracted, and then we can refer to the pointcut expression in a form similar to a method call in other places.

   @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 Connection points
 

The simple understanding of the connection point is the target method. JoinPoint is used to abstract the connection point in Spring, and it can be used to obtain relevant information when the method is executed, such as method name, method parameter type, method actual parameter, etc.
 

  • For @Around advice, only ProceedingJoinPoint can be used to obtain join point information
  • For the other four notifications, only JoinPoint can be used to obtain join point information, which is the parent type of ProceedingJoinPoint
     

So how to get this information? Refer to the code below

@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 case
 

4.1 Requirements
 

Save the operation log of adding, deleting, and modifying methods in the business class to the database.

  • Operation logs include:
    • Operator
    • operating time
    • Operation full class name
    • Action method name
    • method parameters
    • return value
    • method execution time
       

4.2 Analysis
 

  • It is necessary to add a unified function to the add, delete and modify methods in all business classes, and it is most convenient to use AOP technology
  • Since there are no rules for adding, deleting, and changing method names, you can customize the @Log annotation to complete the target method selection

4.3 Steps:

1). Create an operation log table

-- 操作日志表
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). Entity class

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). Custom annotation

/**
 * 自定义Log注解
 */
@Target({ElementType.METHOD})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}

4). Add the @Log annotation to the method that needs to record the 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). Define the Mapper interface

@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). Define the aspect class

@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;
    }

}

Guess you like

Origin blog.csdn.net/qq_57277310/article/details/130012771