springboot uses AOP to implement custom log saving

Preface

Previously, there was a test blog about springboot Face-to-Face AOP, which gives you a brief overview of the order in which each method works. I always wanted to get a chance to do something about my blog, but I finally got some time out today.

Tip: The following is the main body of this article. The following cases can be used as reference.

1. Project introduction

There is no doubt that this project is also a test project, but it will never be too watery and there are many small details to cover. First of all, there are two tables in the database: user table and log table. The function user table corresponding to java has the function of adding, deleting, and checking. Log table only has the function of adding, no front-end page, queryString mode for all operations (simple demonstration project, no need to do too much);

2. Coding process

1. Database creation user and log tables


2. Create a springboot project

There are no screenshots of the process of creating a project. Here's a look at dependencies and project configuration

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.0</version>
        <relativePath/>
    </parent>
    <groupId>org.kangjia</groupId>
    <artifactId>springboot_log_demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot_log_demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <spring.boot.web.version>2.6.0</spring.boot.web.version>
        <mysql.version>5.1.38</mysql.version>
        <druid.version>1.2.5</druid.version>
        <mybatis.spring-boot.version>2.1.4</mybatis.spring-boot.version>
        <aop.version>1.7.4</aop.version>
    </properties>
    <dependencies>
        <!-- springboot web rely on -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring.boot.web.version}</version>
        </dependency>
        <!--Database Driver-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>${mysql.version}</scope>
        </dependency>
        <!-- Connection Pool -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <!-- mybatis integration springboot rely on -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis.spring-boot.version}</version>
        </dependency>
        <!-- aop rely on -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>${aop.version}</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

There are other blogs on the web that introduce the dependency of spring-boot-starter-aop, which I don't need here because AOP dependency is already included in spring-boot-starter-web dependency

The project is configured with

#Service Port
server.port=8090
#Project Access Path
server.servlet.context-path=/
#Item Code
server.tomcat.uri-encoding=utf-8

# log4j Configure Global Scan Level
log4j.rootLogger=info
# log4j Configure Local Scan Level
logging.level.org.kangjia=debug

#Database Configuration
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root

#Scan mapper.xml file
mybatis.mapper-locations=classpath:mapper/*.xml

3. Write AOP Core

  • annotation
package org.kangjia.config.aop;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface MyProjectLog {

    /** Operator account */
    String operationAccount() default "";

    /** Operator */
    String operationUser() default "";

    /** Operation module */
    Module operationModule() default Module.test;

    /** Operation type */
    Type operationType() default Type.test;

    /** Operation Brief */
    String value() default "";

    /** Operation Status */
    State operationState() default State.success;

    /**
     * Operation module enumeration, each module name in the actual project is written here
     */
    public enum Module{
        test((byte)1),user((byte)2);
        private final byte value;

        Module(byte value){this.value = value;}

        public byte value(){return this.value;}
    }

    /**
     * Enumeration of operation types, operations for each module in the actual project are written here
     */
    public enum Type{
        test((byte)0),add((byte)1),edit((byte)2),del((byte)3),select((byte)4),export((byte)5),impor((byte)6);
        private final byte value;

        Type(byte value){this.value = value;}

        public byte value(){return this.value;}
    }

    /**
     * Operation state enumeration
     */
    public enum State{
        success((byte)1),error((byte)0);
        private final byte value;

        State(byte value){this.value = value;}

        public byte value(){return this.value;}
    }
}

It is recommended that you use enumeration like this, the most obvious advantage of enumeration over constants when passing parameters.

  • AOP Face
package org.kangjia.config.aop;

import com.alibaba.druid.util.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.kangjia.entity.BusinessLog;
import org.kangjia.service.BusinessLogService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;

/**
 * Write logs to database using AOP
 * @author ren
 * @date 2021 October 08, 2001 14:58:56
 */
@Aspect
@Component
public class MyLogListener {
    private static final Logger log = LoggerFactory.getLogger(MyLogListener.class);

    private BusinessLog businessLog = new BusinessLog();

    @Autowired
    private BusinessLogService businessLogService;

    //Trigger Conditions for Implanting Advice
    @Pointcut("@annotation(org.kangjia.config.aop.MyProjectLog)")
    private void addTaskAction(){}

    //Surround Enhancement
    @Around("addTaskAction()")
    public Object aroundInterviewTask(ProceedingJoinPoint joinPoint) throws Throwable {
        Object proceed = joinPoint.proceed(joinPoint.getArgs());
        return proceed;
    }

    //Pre-Enhancement
    @Before("addTaskAction()")
    public void beforeInterviewTask(JoinPoint joinPoint) {
    }

    //Execute when method exits normally
    @AfterReturning(returning="result",value="addTaskAction()")
    public void afterReturningInterviewTask(JoinPoint joinPoint, Object result){
        businessLog.setOperationState(MyProjectLog.State.success.value());
    }

    //Exception throw enhancement
    @AfterThrowing(throwing="ex",value="addTaskAction()")
    public void afterThrowingInterviewTask(JoinPoint joinPoint, Throwable ex){
        businessLog.setOperationState(MyProjectLog.State.error.value());
    }

    //final enhancements, whether throwing exceptions or exiting normally
    @After("addTaskAction()")
    public void afterInterviewTask(JoinPoint joinPoint) throws Exception {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        MyProjectLog myProjectLog = method.getAnnotation(MyProjectLog.class);
        HttpServletRequest request = null;
        Object[] args = joinPoint.getArgs();
        String[] parameterNames = methodSignature.getParameterNames();
        for(int i = 0;i<parameterNames.length;i++){
            if("request".equals(parameterNames[i])){
                request = (HttpServletRequest)args[i];
                break;
            }
        }
        String operationAccount = myProjectLog.operationAccount();
        if(StringUtils.isEmpty(operationAccount)){
            //Take the token from the request (token is always included in the request in general projects), where the test writes a dead value
            operationAccount = "admin";
        }
        businessLog.setOperationAccount(operationAccount);
        String operationUser = myProjectLog.operationUser();
        if(StringUtils.isEmpty(operationUser)){
            //Take the token from the request (token is always included in the request in general projects), where the test writes a dead value
            operationUser = "admin";
        }
        businessLog.setOperationUser(operationUser);
        businessLog.setOperationModule(myProjectLog.operationModule().value());
        businessLog.setOperationType(myProjectLog.operationType().value());
        businessLog.setOperationDate(new Date());
        String operationSketch = myProjectLog.value();
        if(StringUtils.isEmpty(operationSketch)){
            //Take it from the request (I've put it in the method)
            operationSketch = request.getAttribute("logCache").toString();
        }
        businessLog.setOperationSketch(operationSketch);
        businessLog.setSessionId(request.getSession().getId());
        //Because the project is deployed to run on the server, client IP is remote ip, service IP is local ip, and service port is local IP for code
        businessLog.setClientIp(getRemortIP(request));
        businessLog.setServerIp(getLocalAddr());
        businessLog.setServerPort(request.getLocalPort());
        businessLog.setRequestUrl(request.getRequestURL().toString());
        businessLogService.insert(businessLog);
    }

    /**
     * Get Client ip
     * @param request
     * @return
     */
    private String getRemortIP(HttpServletRequest request) {
        String ip = null;
        if (request != null && request.getHeader("x-forwarded-for") == null) {
            ip = request.getRemoteAddr();
        }else{
            ip = request.getHeader("x-forwarded-for");
        }
        if("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip) || "localhost".equals(ip)){
            ip = getLocalAddr();
        }
        return ip;
    }

    /**
     * Get the server IP (native ip)
     * @return
     */
    private String getLocalAddr() {
        try {
            return InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
        return null;
    }
}

The code here means taking the data to be saved from the method's comments and parameters and calling the method to save the log. The most important detail here is Object[] args = joinPoint.getArgs(); All I get is a list of parameters, each object value in the array is a parameter value, but the type is not the type of the original parameter, so I can only get the parameter name here and find the specified parameter to force.

4.controller Layer Display

package org.kangjia.controller;

import org.kangjia.config.aop.MyProjectLog;
import org.kangjia.entity.User;
import org.kangjia.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

/**
 * (User)Table Control Layer
 *
 * @author ren
 * @since 2021-11-27 16:08:39
 */
@RestController
@RequestMapping("user")
public class UserController {
    @Autowired
    private UserService userService;

    private final static Logger log = LoggerFactory.getLogger(UserController.class);

    /**
     * Define test interfaces to simulate user's front-end requests
     * @return
     */
    @GetMapping("test")
    @MyProjectLog(value = "Operational test methods",operationModule = MyProjectLog.Module.test)
    public String test(HttpServletRequest request){
        log.debug("Operational test methods");
        return "test ok!!!";
    }

    /**
     * Define test interfaces to simulate user's front-end requests
     * @return
     */
    @GetMapping("demo")
    @MyProjectLog(value = "Operation Case Method",operationModule = MyProjectLog.Module.test)
    public String demo(HttpServletRequest request){
        log.debug("Operation Case Method");
        return "demo ok!!!";
    }

    /**
     * Query single data by primary key
     * @param id Primary key
     * @return Single Data
     */
    @GetMapping("selectOne")
    @MyProjectLog(operationModule = MyProjectLog.Module.user,operationType = MyProjectLog.Type.select)
    public User selectOne(HttpServletRequest request,Integer id) {
        request.setAttribute("logCache","query id by"+id+"User information");
        log.debug("query id by{}User information",id);
        return this.userService.queryById(id);
    }

    /**
     * Add Single Data
     * @param userName
     * @param password
     * @return
     */
    @GetMapping("add")
    @MyProjectLog(operationModule = MyProjectLog.Module.user,operationType = MyProjectLog.Type.add)
    public User addUser(HttpServletRequest request,String userName,String password) {
        request.setAttribute("logCache","Add a new user information,The user name is:"+userName+",Password is:"+password);
        log.debug("Add a new user information,The user name is:{},Password is:{}",userName,password);
        User u = new User();
        u.setUserName(userName);
        u.setPassword(password);
        User user = this.userService.insert(u);
        return user;
    }

    /**
     * Modify individual data by primary key
     * @param id
     * @param userName
     * @param password
     * @return
     */
    @GetMapping("edit")
    @MyProjectLog(operationModule = MyProjectLog.Module.user,operationType = MyProjectLog.Type.edit)
    public User editUser(HttpServletRequest request,Integer id,String userName, String password){
        request.setAttribute("logCache","modify id by"+id+"User information,Change user name to:"+userName+",Change password to:"+password);
        log.debug("modify id by{}User information,Change user name to:{},Change password to:{}",id,userName,password);
        User u = new User();
        u.setId(id);
        u.setUserName(userName);
        u.setPassword(password);
        User user = this.userService.update(u);
        return user;
    }

    /**
     * Delete single data by primary key
     * @param id Primary key
     * @return Single Data
     */
    @GetMapping("del")
    @MyProjectLog(operationModule = MyProjectLog.Module.user,operationType = MyProjectLog.Type.del)
    public String delUser(HttpServletRequest request,Integer id) {
        request.setAttribute("logCache","delete id by"+id+"User information");
        log.debug("delete id by{}User information",id);
        this.userService.deleteById(id);
        return "del ok!!!";
    }
}

Here's a small tips, where static log descriptions are directly assigned to the annotated value attribute, and dynamic log descriptions are placed in requests, which is why I'm coding in AOP after the target method has been executed. Another detail is that if exceptions are caught in the control layer's method, AOP facet processing can't simply set the failure state in the afterThrowing InterviewTask method, or you can put the failure state value in the request and then take the value from the request in the normal exit method.

3. Test Results

summary

That's all about springboot using AOP technology for logging. The advantage of this is that the database tables are free, and log tables can be created as desired without the constraints of the Logback log format. The disadvantage is also obvious. In addition to printing logs using logs, there are times when dynamic log descriptions need to be included in request s, making it more cumbersome to work with methods that catch exceptions. If you can ultimately get a certain level of log printing directly, you will be perfect. However, you have not yet thought of a solution, and you may design a better one if you think about it later.

Tags: Java Spring AOP

Posted on Wed, 01 Dec 2021 08:19:58 -0500 by mgzee