Springboot+AOP + custom annotation implements operation log and exception log

Keywords: Java Spring Spring Boot AOP IDE

         In our daily work, we often encounter the need to implement the functions of operation log, login log, error log, etc. the young partners who have just joined the company do not know what to do, or the first idea is to write a method to add to the log table in each method in the business code, but this is too troublesome.

         When we study Spring, we know that the two core functions of Spring are IOC and AOP. IOC will not be repeated here. When we study AOP, we learned that the role of AOP is aspect oriented programming, that is, weaving the functions not related to the core business into one aspect, and then weaving this aspect into the core code.

        The operation log, error log and login log can be implemented with AOP. Then we customize an annotation, add this annotation to the method that needs to be woven into the aspect, and enter the aspect to execute the method in the aspect every time. The specific application is as follows.

First, we need to create three new tables. The first is the operation log table

CREATE TABLE `operation_log` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT 'Primary key id',
  `oper_module` varchar(64) DEFAULT NULL COMMENT 'functional module ',
  `oper_type` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'Operation type',
  `oper_desc` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'pedagogical operation',
  `oper_requ_param` text CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT 'Request parameters',
  `oper_resp_param` text CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT 'Return parameters',
  `oper_user_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'operator id',
  `oper_user_name` varchar(64) DEFAULT NULL COMMENT 'Operator Name ',
  `oper_method` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'Request method',
  `oper_uri` varchar(255) DEFAULT NULL COMMENT 'request URI',
  `oper_ip` varchar(64) DEFAULT NULL COMMENT 'operation ip',
  `oper_create_time` datetime DEFAULT NULL COMMENT 'Operation time',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3;

Then create an exception log table

CREATE TABLE `error_log` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT 'Primary key id',
  `err_requ_param` text COMMENT 'Request parameters',
  `err_name` varchar(255) DEFAULT NULL COMMENT 'Exception name',
  `err_message` text COMMENT 'Abnormal information',
  `oper_user_id` varchar(64) DEFAULT NULL COMMENT 'operator id',
  `oper_user_name` varchar(64) DEFAULT NULL COMMENT 'Operator name',
  `oper_method` varchar(255) DEFAULT NULL COMMENT 'Operation method',
  `oper_uri` varchar(255) DEFAULT NULL COMMENT 'request URI',
  `oper_ip` varchar(64) DEFAULT NULL COMMENT 'request IP',
  `oper_create_time` datetime DEFAULT NULL COMMENT 'Operation time',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3;

Finally, we build a user table for simple business operations

CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `dept_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb3;

We set all the fields here to be non empty in order to simulate the subsequent error log records.

First, we need to introduce aop dependencies

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

We first use the code generator of MybatisPlus to generate the architecture we need

Then create an annotation package and an annotation annotation class in it

package com.logtest.annotation;

import java.lang.annotation.*;

/**
 * @author az
 * @description TODO Comments for custom logging
 * @date 2021/12/3 0003
 */

/**
 * This annotation represents the scope of annotation scanning. The general values are as follows. Here, we can set the scope to annotation
 *      1.CONSTRUCTOR:Used to describe constructors
 *      2.FIELD:Used to describe a domain
 *      3.LOCAL_VARIABLE:Used to describe local variables
 *      4.METHOD:Used to describe the method
 *      5.PACKAGE:Used to describe the package
 *      6.PARAMETER:Used to describe parameters
 *      7.TYPE:Used to describe a class, interface (including annotation types), or enum declaration
 * @author Administrator
 */
@Target(ElementType.METHOD)
/**
 * This annotation indicates at which stage the annotation is executed, and RUNTIME indicates at RUNTIME
 * 1,RetentionPolicy.SOURCE: Annotations are only kept in the source file. When Java files are compiled into class files, annotations are discarded;
 *      If you only do some checking operations, such as @ Override and @ SuppressWarnings, select RESOURCE annotation
 * 2,RetentionPolicy.CLASS: Annotations are retained in the class file, but are discarded when the jvm loads the class file, which is the default life cycle;
 *      It is generally used to perform some preprocessing operations during compilation, such as generating some auxiliary code (such as ButterKnife)
 * 3,RetentionPolicy.RUNTIME: The annotation is not only saved in the class file, but still exists after the jvm loads the class file;
 *      It is generally used in scenarios where annotation information needs to be dynamically obtained at runtime
 */
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {

    /**
     * Operation module
     */
    String operModule() default "";

    /**
     * Operation type
     */
    String operType() default "";

    /**
     * Operating instructions
     */
    String operDesc() default "";
}

Next, we will create a new aop package, in which we will create new classes to deal with aspects

import com.alibaba.fastjson.JSON;
import com.logtest.annotation.Log;
import com.logtest.entity.ErrorLog;
import com.logtest.entity.OperationLog;
import com.logtest.service.ErrorLogService;
import com.logtest.service.OperationLogService;
import com.logtest.util.IpUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author az
 * @description TODO The faceted processing class is used to process operation logs and exception logs
 * @date 2021/12/3 0003
 */
@Component
/**
 * With this annotation, Spring will process this class as an AOP container when it is scanned
 */
@Aspect
public class OperationLogAspect {

    @Resource
    private OperationLogService operationLogService;

    @Resource
    private ErrorLogService errorLogService;

    /**
     * Set the entry point of the operation log, which is used to record the operation log and enter at the position marked with annotation
     */
    @Pointcut("@annotation(com.logtest.annotation.Log)")
    public void operationLogPointCut() {

    }

    /**
     * Set the operation exception entry point, record the exception log, and scan all operations under the controller package
     */
    @Pointcut("execution(* com.logtest.controller..*.*(..))")
    public void operErrorLogPointCut() {
    }

    /**
     * Set the operation exception entry point, intercept the user's operation log, and execute after the connection point executes normally. If the connection point throws an exception, it will not execute
     * @param joinPoint breakthrough point
     * @param keys Return results
     */
    @AfterReturning(value = "operationLogPointCut()", returning = "keys")
    public void saveOperationLog(JoinPoint joinPoint, Object keys) {
        //Get RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        //Get the information of HttpServletRequest from the obtained RequestAttributes
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);

        OperationLog operationLog = new OperationLog();

        try {
            //Method for obtaining weaving point through reflection mechanism at weaving point of section
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            //Method of obtaining weaving point
            Method method = signature.getMethod();
            //Get operation
            Log log = method.getAnnotation(Log.class);
            if (log != null) {
                String operModule = log.operModule();
                String operDesc = log.operDesc();
                String operType = log.operType();
                operationLog.setOperModule(operModule);
                operationLog.setOperDesc(operDesc);
                operationLog.setOperType(operType);
            }
            //Gets the requested class name
            String className = joinPoint.getTarget().getClass().getName();
            //Method to get the request
            String methodName = method.getName();
            methodName = className + "." + methodName;
            //Request method
            operationLog.setOperMethod(methodName);

            Map<String, String> rtnMap = converMap(request.getParameterMap());
            //Convert the array of parameters to json
            String params = JSON.toJSONString(rtnMap);

            //Request parameters
            operationLog.setOperRequParam(params);
            //Return results
            operationLog.setOperRespParam(JSON.toJSONString(keys));
            //Operator ip address
            operationLog.setOperIp(IpUtils.getIpAddr(request));
            //Request URI
            operationLog.setOperUri(request.getRequestURI());
            //Creation time (operation time)
            operationLog.setOperCreateTime(new Date());
            operationLog.setOperUserId("1");
            operationLog.setOperUserName("test");

            operationLogService.save(operationLog);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * The exception return notification is used to intercept the information in the exception log. It is executed after the connection point throws an exception
     * @param joinPoint breakthrough point
     * @param e Abnormal information
     */
    @AfterThrowing(pointcut = "operErrorLogPointCut()", throwing = "e")
    public void saveErrorLog(JoinPoint joinPoint,Throwable e){
        //Get RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        //Get the information of HttpServletRequest from RequestAttributes
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);

        ErrorLog errorLog = new ErrorLog();
        try {
            //Method for obtaining weaving point through reflection mechanism at weaving point of section
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            //Method of obtaining weaving point
            Method method = signature.getMethod();
            //Get operation
            Log log = method.getAnnotation(Log.class);
            //Gets the requested class name
            String className = joinPoint.getTarget().getClass().getName();
            //Method to get the request
            String methodName = method.getName();
            methodName = className + "." + methodName;
            //Requested parameters
            Map<String, String> rtnMap = converMap(request.getParameterMap());
            String params = JSON.toJSONString(rtnMap);
            //Request parameters
            errorLog.setErrRequParam(params);
            //Request method name
            errorLog.setOperMethod(methodName);
            //Exception name
            errorLog.setErrName(e.getClass().getName());
            //Abnormal information
            errorLog.setErrMessage(stackTraceToString(e.getClass().getName(),e.getMessage(),e.getStackTrace()));
            //Request URI
            errorLog.setOperUri(request.getRequestURI());
            //Operator ip address
            errorLog.setOperIp(IpUtils.getIpAddr(request));
            //Time of exception
            errorLog.setOperCreateTime(new Date());
            errorLog.setOperUserId("1");
            errorLog.setOperUserName("test");

            errorLogService.save(errorLog);
        }catch (Exception exception){
            exception.printStackTrace();
        }
    }

    /**
     * Transform request parameters
     * @param paramMap request Parameter array obtained in
     * @return Converted array
     */
    public Map<String, String> converMap(Map<String,String[]> paramMap){
        Map<String, String> rtnMap = new HashMap<>();
        for (String key : paramMap.keySet()) {
            rtnMap.put(key,paramMap.get(key)[0]);
        }
        return rtnMap;
    }

    public String stackTraceToString(String exceptionName,String exceptionMessage,StackTraceElement[] elements){
        StringBuilder stringBuilder = new StringBuilder();
        for (StackTraceElement element : elements) {
            stringBuilder.append(element).append("\n");
        }
        String message = exceptionName + ":" + exceptionMessage + "\n\t" + stringBuilder.toString();
        return message;
    }

}

After this class is written, our aspect has been written. Then we can create a tool class to query the operator's IP and an enumeration class to save our operation types

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 * @author az
 * @description TODO Tool class for obtaining operator IP
 * @date 2021/12/3 0003
 */
public class IpUtils {
    /**
     * Get current network ip
     * @param request
     * @return
     */
    public static String getIpAddr(HttpServletRequest request){
        String ipAddress = request.getHeader("x-forwarded-for");
        if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("Proxy-Client-IP");
        }
        if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("WL-Proxy-Client-IP");
        }
        if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getRemoteAddr();
            if("127.0.0.1".equals(ipAddress) || "0:0:0:0:0:0:0:1".equals(ipAddress)){
                //Take the configured IP of the machine according to the network card
                InetAddress inet=null;
                try {
                    inet = InetAddress.getLocalHost();
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                }
                ipAddress= inet.getHostAddress();
            }
        }
        //In the case of multiple agents, the first IP is the real IP of the client, and multiple IPS are divided according to ','
        if(ipAddress!=null && ipAddress.length()>15){ //"***.***.***.***".length() = 15
            if(ipAddress.indexOf(",")>0){
                ipAddress = ipAddress.substring(0,ipAddress.indexOf(","));
            }
        }
        return ipAddress;
    }
}
/**
 * @author az
 * @description TODO Operation type enumeration class
 * @date 2021/12/3 0003
 */
public interface OperationLogType {

    String QUERY = "query";

    String ADD = "newly added";

    String MODIFY = "modify";

    String WITHDRAW = "withdraw";

    String DELETE = "delete";
}

Next, let's write some simple business to test whether our logs can be recorded successfully

package com.logtest.controller;


import com.logtest.annotation.Log;
import com.logtest.entity.User;
import com.logtest.service.UserService;
import com.logtest.util.OperationLogType;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Arrays;
import java.util.List;

/**
 * @author az
 * @since 2021-12-03
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;


    @GetMapping("/queryAllUser")
    @Log(operModule = "user management -Query user list",operType = OperationLogType.QUERY,operDesc = "Query all users")
    public Object queryAllUser(){
        return userService.list();
    }

    @PostMapping("/addUser")
    @Log(operModule = "user management -New user",operType = OperationLogType.ADD,operDesc = "New user functions")
    public String addUser(@RequestBody User user){
        boolean save = userService.save(user);
        if (save) {
            return "Successfully added";
        }else {
            return "Failed to add";
        }
    }

    @PostMapping("/modifyUser")
    @Log(operModule = "user management -modify",operType = OperationLogType.MODIFY,operDesc = "according to id Modify user details")
    public String modifyUser(@RequestBody User user){
        boolean update = userService.updateById(user);
        if (update) {
            return "Modified successfully";
        }else {
            return "Modification failed";
        }
    }

    @DeleteMapping("/deleteUser")
    @Log(operModule = "user management -delete",operType = OperationLogType.DELETE,operDesc = "according to id delete user")
    public String deleteUser(@RequestParam Long id){
        boolean delete = userService.removeById(id);
        if (delete) {
            return "Delete succeeded";
        }else {
            return "Deletion failed";
        }
    }


}

  Next, let's run to see if we can do it. Here I use Postman to test

First, let's test the four methods we wrote in turn. After testing, let's look at the operation log table in the database

  The operation log was successfully written, because when we were designing the user table, in order to test the exception log, I set all parameters to be non empty, that is, each field should be passed. Then we deliberately didn't pass one of the parameters to try

Postman reports an error. Next, let's take a look at our exception log  

It is also written successfully, so our SpringBoot integration AOP and custom annotation to realize the operation log is here to a successful end!  

Posted by LonelyPixel on Sun, 05 Dec 2021 22:09:11 -0800