preface

Some time ago, there was a requirement to implement an operation log. For almost every interface invoked, a specific log linked to this parameter is logged to the database. For example, for the gag operation, the log needs to record the gag for what, the id of the person banned and various information. Convenient later query.

There are many such interfaces, and most of them have different parameters. An obvious idea would be to implement a logging utility class and then add a line of code to the interface where logging is required. It is up to the logging utility class to determine which parameters should be processed at this point.

But there are big problems with that. If the number of interfaces that need to be logged is very large, I personally find it unacceptable to just add such a line of code to all interfaces, leaving aside the discussion of how much type judgment needs to be done in the utility class. First, it’s too intrusive to code. Secondly, in case of changes in the later period, the maintenance of the people will be very uncomfortable. Imagine searching for the same code globally and modifying it one by one.

So I abandoned this somewhat primitive method. I ended up taking the Aop approach of intercepting requests for logging. But even with this approach, the problem is how to handle a large number of parameters. And how it corresponds to each interface.

Instead of intercepting all controllers, I ended up customizing a log annotation. All methods marked with this annotation will be logged. Also, annotations have types that specify specific log content and parameters for the current interface.

So how do you specify, from the many possible parameters, the corresponding parameters for the current log? My solution is to maintain a parameter class that lists all the parameter names that need to be logged. Then, when intercepting the request, all the parameters and values in the request and response are obtained through reflection. If the parameter exists in the PARam class I maintain, the corresponding value will be assigned to it.

Then, at the end of the request, all reserved parameters in the template are replaced with assigned parameters. In this way, the requirements are met without a lot of intrusion into the business, while also ensuring the maintainability of the code.

I will list the detailed implementation process below.

Before operation

I’ll give you all the source code for the demo project at the end of this article. So do not want to see the process of brother Taiwan can move to the end, directly look at the source. (Heard and source code collocation, read the article more delicious…)

Start operation

New project

You can refer to another article I wrote earlier for a step-by-step guide to building a SpringBoot back-end project framework from scratch. You just need to be able to request a simple interface. The dependencies for this project are as follows.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.1.1. RELEASE</version>
</dependency>

<! -- https://mvnrepository.com/artifact/org.aspectj/aspectjrt -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.2</version>
</dependency>

<! -- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.2</version>
</dependency>


<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.2</version>
</dependency>

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>4.1.14</version>
</dependency>

Copy the code

New Aop classes

Create a LogAspect class. Here’s the code.

package spring.aop.log.demo.api.util;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * LogAspect
 *
 * @author Lunhao Hu
 * @dateThe 2019-01-30 * * / parts
@Aspect
@Component
public class LogAspect {
    /** * defines the pointcut */
    @Pointcut("@annotation(spring.aop.log.demo.api.util.Log)")
    public void operationLog(a) {}/** * Trigger ** when the new result is returned@param point
     * @param returnValue
     */
    @AfterReturning(returning = "returnValue", pointcut = "operationLog() && @annotation(log)")
    public void doAfterReturning(JoinPoint point, Object returnValue, Log log) {
        System.out.println("test"); }}Copy the code

An annotation is passed in the Pointcut indicating that any method annotated with this annotation will trigger the operationLog function, which is decorated by the Pointcut. AfterReturning is triggered after a request is returned.

Custom annotations

The custom annotations mentioned in the previous step will be applied to each method of the Controller. Create a new annotation class. Here’s the code.

package spring.aop.log.demo.api.util;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Log
 *
 * @author Lunhao Hu
 * @dateThe 2019-01-30 is better * * /
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    String type(a) default "";
}
Copy the code

Target and Retention are meta-annotations. There are four types, namely @Retention, @target, @Document, and @Inherited.

The Target Annotation specifies the scope that this Annotation modifies. You can pass in a number of types with the parameter ElementType. For example, TYPE describes a class, interface, or enumeration class. FIELD is used to describe attributes; METHOD describes methods; PARAMETER describes parameters. CONSTRUCTOR is used to describe constructors; LOCAL_VARIABLE describes local variables; ANNOTATION_TYPE describes annotations; PACKAGE describes packages, etc.

The Retention Annotation defines how long the Annotation is retained. The parameter is RetentionPolicy. For example, SOURCE indicates that it exists only in the SOURCE code, not in the compiled class file. CLASS is the default option for this annotation. It exists in both source code and compiled class files, but is not loaded into the virtual machine. RUNTIME exists in source code, class files, and virtual machines, and can be retrieved by reflection at RUNTIME, in plain English.

Add a common note

Annotate the interface for logging.

package spring.aop.log.demo.api.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import spring.aop.log.demo.api.util.Log;

/**
 * HelloController
 *
 * @author Lunhao Hu
 * @dateThe 2019-01-30 15:52 * * /
@RestController
public class HelloController {
    @Log
    @GetMapping("test/{id}")
    public String test(@PathVariable(name = "id") Integer id) {
        return "Hello"+ id; }}Copy the code

This will trigger code in the doAfterReturning method in the interceptor every time the test/{ID} interface is called.

Add type annotations

Now that I’ve covered logging in general, I’ll cover logging in specific. What is the specific log, is that each interface to log different information. To do this, we need to implement an enumerated class of operation types. Here’s the code.

Enumeration of operation type templates

Create a new enumerated class Type. Here’s the code.

package spring.aop.log.demo.api.util;

/**
 * Type
 *
 * @author Lunhao Hu
 * @dateThe 2019-01-30 "* * /
public enum Type {
    /**
     * 操作类型
     */
    WARNING("Warning"."Warn the player after being reported by another player.");

    /** * type */
    private String type;

    /** * Perform the operation */
    private String operation;

    Type(String type, String operation) {
        this.type = type;
        this.operation = operation;
    }

    public String getType(a) { return type; }

    public String getOperation(a) { returnoperation; }}Copy the code

Type annotations

Add type to the annotation in controller above. Here’s the code.

package spring.aop.log.demo.api.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import spring.aop.log.demo.api.util.Log;

/**
 * HelloController
 *
 * @author Lunhao Hu
 * @dateThe 2019-01-30 15:52 * * /
@RestController
public class HelloController {
    @Log(type = "WARNING")
    @GetMapping("test/{id}")
    public String test(@PathVariable(name = "id") Integer id) {
        return "Hello"+ id; }}Copy the code

Modify the aop classes

Make doAfterReturning in the AOP class as follows.

@AfterReturning(returning = "returnValue", pointcut = "operationLog() && @annotation(log)")
public void doAfterReturning(JoinPoint point, Object returnValue, Log log) {
    // Type in annotation
    String enumKey = log.type();
    System.out.println(Type.valueOf(enumKey).getOperation());
}
Copy the code

After this is added, each call to the interface annotated with @log (type = “WARNING”) will print the Log specified by the interface. For example, the above code would print the following code.

Alert the player for being reported by other playersCopy the code

Gets the request parameters that the AOP intercepts

It is not difficult to specify a log for each interface, just a type for each interface. However, you should also note that an interface log, which only records that the player was reported by other players, warning the player of such information does not make any sense.

The person who records the log doesn’t think so, and the person who finally goes to check the log will save my body three times every day, who reported it? For what? Who did I warn?

Such logs are so useless that there is no way to trace back to the source after a problem has occurred. So our next step is to add specific parameters to each interface. If the parameters of each interface are almost different, the utility class will have to pass in a lot of parameters, how to implement it, and even organize parameters, which will greatly invade the business code and add a lot of redundant code.

You might think, implement a logging method, call it on the interface where you want to log, and pass in the parameters. If there are many types, the parameters will increase, and the parameters will be different for each interface. It is cumbersome to process and too intrusive for the business. Log code is embedded almost everywhere. Once modification is involved, it becomes very difficult to maintain.

So I use reflection directly to get all the parameters in the request that AOP intercepts, and if my parameter class (all the parameters to record) contains the parameters in the request, I write the values of the parameters into the parameter class. Finally, replace the parameter reserved field in the logging template with the parameter in the request.

The flow chart is shown below.

Creating a Parameter Class

Create a new class Param that contains all the parameters that might appear in the operation log. Why would you do that? Because each interface may require completely different parameters, it is better to be greedy and pass in all possible parameters instead of maintaining a lot of judgment logic. Of course, if new parameters need to be recorded later, you need to modify the code.

package spring.aop.log.demo.api.util;

import lombok.Data;

/**
 * Param
 *
 * @author Lunhao Hu
 * @dateThe 2019-01-30 immediately * * /
@Data
public class Param {
    /** * all possible arguments */
    private String id;
    private String workOrderNumber;
    private String userId;
}
Copy the code

Modify the template

Change WARNING in the template enumeration class to the following.

WARNING("Warning"."Because be versed in the single number report [workOrderNumber (%)] / ID/ID (%) warned players/userId (%)");
Copy the code

The parameters are the ones to be retrieved and replaced during the AOP interception phase.

Modify the controller

We added the parameters of the above template to the previous controller. Part of the code is as follows.

@Log(type = "WARNING")
@GetMapping("test/{id}")
public String test(
        @PathVariable(name = "id") Integer id,
        @RequestParam(name = "workOrderNumber") String workOrderNumber,
        @RequestParam(name = "userId") String userId,
        @RequestParam(name = "name") String name
) {
    return "Hello" + id;
}
Copy the code

Get the parameters of the request through reflection

There are two kinds of cases, one is simple parameter type, and the other is complex parameter type, that is, the parameter with the request DTO.

Gets the simple parameter type

Add a few private variables to an AOP class.

/** * all parameters in the request */
private Object[] args;

/** * All parameters in the request */
private String[] paramNames;

/** * Parameter class */
private Param params;
Copy the code

Then change the code in doAfterReturning to the following.

try {
    // Get request details
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = attributes.getRequest();
    HttpServletResponse response = attributes.getResponse();
    // Get all request parameters
    Signature signature = point.getSignature();
    MethodSignature methodSignature = (MethodSignature) signature;
    this.paramNames = methodSignature.getParameterNames();
    this.args = point.getArgs();

    // Instantiate the parameter class
    this.params = new Param();
    // Type in annotation
    String enumKey = log.type();
    String logDetail = Type.valueOf(enumKey).getOperation();

    // Get the data from the request-passed parameter
    this.getRequestParam();
} catch (Exception e) {
    System.out.println(e.getMessage());
}
Copy the code

The first thing to do is to intercept requests with custom annotations. We can get the details of the request, all the parameter names in the request, and the parameters. Let’s implement the getRequestParam method in the code above.

getRequestParam

/** * Gets the argument * in the intercepted request@param point
 */
private void getRequestParam(a) {
    // Get the simple parameter type
    this.getSimpleParam();
}
Copy the code

getSimpleParam

/** * gets the value of the simple parameter type */
private void getSimpleParam(a) {
    // Iterate over the parameter names in the request
    for (String reqParam : this.paramNames) {
        // Check whether the parameter exists in the parameter class
        if (this.isExist(reqParam)) {
            this.setRequestParamValueIntoParam(reqParam); }}}Copy the code

The above code, traverse the parameter names of the incoming request and then we realize isExist method, to determine the parameters in our Param class exists, if there are we call setRequestParamValueIntoParam method again, Write the parameter value corresponding to this parameter name to an instance of the Param class.

isExist

The code for isExist is as follows.

/** * Checks whether the parameter exists in the parameter class (if it is a parameter to be logged) *@param targetClass
 * @param name
 * @param <T>
 * @return* /
private <T> Boolean isExist(String name) {
    boolean exist = true;
    try {
        String key = this.setFirstLetterUpperCase(name);
        Method targetClassGetMethod = this.params.getClass().getMethod("get" + key);
    } catch (NoSuchMethodException e) {
        exist = false;
    }
    return exist;
}
Copy the code

As mentioned above, getters and setters are added at compile time, so the first letter of the parameter name is uppercase, so we need to implement our own setFirstLetterUpperCase method to uppercase the first letter of the parameter name we pass in.

setFirstLetterUpperCase

Here’s the code.

/** * capitalizes the first letter of the string **@param str
 * @return* /
private String setFirstLetterUpperCase(String str) {
    if (str == null) {
        return null;
    }
    return str.substring(0.1).toUpperCase() + str.substring(1);
}
Copy the code

setRequestParamValueIntoParam

Here’s the code.

/** * gets * from the argument@param paramName
 * @return* /
private void setRequestParamValueIntoParam(String paramName) {
    int index = ArrayUtil.indexOf(this.paramNames, paramName);
    if(index ! = -1) {
        String value = String.valueOf(this.args[index]);
        this.setParam(this.params, paramName, value); }}Copy the code

ArrayUtil is a utility function in HuTool. Used to determine the index of an element in an array.

setParam

Here’s the code.

/** * writes data to an instance of the argument class *@param targetClass
 * @param key
 * @param value
 * @param <T>
 */
private <T> void setParam(T targetClass, String key, String value) {
    try {
        Method targetClassParamSetMethod = targetClass.getClass().getMethod("set" + this.setFirstLetterUpperCase(key), String.class);
        targetClassParamSetMethod.invoke(targetClass, value);
    } catch(NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); }}Copy the code

This function uses reflection, takes the parameter’s set method, and sets the corresponding parameter in the Param class to the value passed in.

run

Start the project and request methods in controller. And pass in the defined parameters.

http://localhost:8080/test/ 8? workOrderNumber=3231732&userId=748327843&name=testName
Copy the code

The GET request, introduced into four parameters, id, workOrderNumber, userId, name. As you can see, the name field is not defined in the Param class. This is a deliberate addition of an unlogged parameter to verify the robustness of our interface.

After running, you can see the following information printed by the console.

Param(id=8, workOrderNumber=3231732, userId=748327843)
Copy the code

We want all the arguments logged by AOP to be logged into the instance of the Param class, and passing in unexpected arguments without crashing the program. Next, we just need to replace these parameters with the parameter reserved fields defined in the template.

Replace the parameters

After the getRequestParam function in doAfterReturning, add the following code.

if(! logDetail.isEmpty()) {// Replace all the parameters in the template
    logDetail = this.replaceParam(logDetail);
}
System.out.println(logDetail);
Copy the code

Let’s implement the replaceParam method.

replaceParam

Here’s the code.

/** * Replace all the reserved fields in the template with the intercepted argument *@param template
 * @return* /
private String replaceParam(String template) {
    // Convert the parameters in the template to map
    Map<String, String> paramsMap = this.convertToMap(template);
    for (String key : paramsMap.keySet()) {
        template = template.replace("%" + key, paramsMap.get(key)).replace("("."").replace(")"."");
    }
    return template;
}
Copy the code

The convertToMap method extracts all reserved fields from the template as a Map Key.

convertToMap

Here’s the code.

/** * Convert template parameters to map key-value *@param template
 * @return* /
private Map<String, String> convertToMap(String template) {
    Map<String, String> map = new HashMap<>();
    String[] arr = template.split("\\(");
    for (String s : arr) {
        if (s.contains("%")) {
            String key = s.substring(s.indexOf("%"), s.indexOf(")")).replace("%"."").replace(")"."").replace("-"."").replace("]"."");
            String value = this.getParam(this.params, key);
            map.put(key, "null".equals(value) ? "(empty)": value); }}return map;
}
Copy the code

The getParam method, similar to setParam, also uses reflection to get the corresponding value from the Class and Key passed in.

getParam

Here’s the code.

/** * Gets the value of the corresponding key in the passed class * by reflection@param targetClass
 * @param key
 * @param <T>
 */
private <T> String getParam(T targetClass, String key) {
    String value = "";
    try {
        Method targetClassParamGetMethod = targetClass.getClass().getMethod("get" + this.setFirstLetterUpperCase(key));
        value = String.valueOf(targetClassParamGetMethod.invoke(targetClass));
    } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    return value;
}
Copy the code

Run again

Request the above URL again, and you can see the console output below.

Warning players [748327843] due to job order number [3231732] / Report ID [8]Copy the code

As you can see, all the parameters we need to record have been replaced correctly. Parameters that do not need to be recorded also have no impact on the program.

Let’s see what happens if we pass in non-mandatory parameters. Modify the controller as follows and change the workOrderNumber to optional.

@Log(type = "WARNING")
@GetMapping("test/{id}")
public String test(
        @PathVariable(name = "id") Integer id,
        @RequestParam(name = "workOrderNumber", required = false) String workOrderNumber,
        @RequestParam(name = "userId") String userId,
        @RequestParam(name = "name") String name
) {
    return "Hello" + id;
}
Copy the code

Request the following URL.

http://localhost:8080/test/ 8? userId=748327843&name=testName
Copy the code

You can then see the console output as follows.

Warning player for job order [empty] / report ID [8] [748327843]Copy the code

Does not affect the normal operation of the program.

Gets complex parameter types

Next up is how to log complex parameter types. In fact, the general idea is the same. Let’s see if there are any arguments in the class that we pass in that we need to record. If so, replace the simple parameters as above.

Define test complex types

New TestDTO. Here’s the code.

package spring.aop.log.demo.api.util;

import lombok.Data;

/**
 * TestDto
 *
 * @author Lunhao Hu
 * @dateThe 2019-02-01 15:02 * * /
@Data
public class TestDTO {
    private String name;
    
    private Integer age;

    private String email;
}
Copy the code

Modify the Param

Add all of the above parameters to the Param class and define them as strings.

package spring.aop.log.demo.api.util;

import lombok.Data;

/**
 * Param
 *
 * @author Lunhao Hu
 * @dateThe 2019-01-30 immediately * * /
@Data
public class Param {
    /** * all possible arguments */
    private String id;
    private String age;
    private String workOrderNumber;
    private String userId;
    private String name;
    private String email;
}
Copy the code

Modify the template

Modify the WARNING template as follows.

/** * Operation type */
WARNING("Warning"."Because be versed in the single number report [workOrderNumber (%)] / ID/ID (%) warned players [userId (%)], the game name [(% name)], age/age (%)");
Copy the code

Modify the controller

@Log(type = "WARNING")
@PostMapping("test/{id}")
public String test(
        @PathVariable(name = "id") Integer id,
        @RequestParam(name = "workOrderNumber", required = false) String workOrderNumber,
        @RequestParam(name = "userId") String userId,
        @RequestBody TestDTO testDTO
) {
    return "Hello" + id;
}
Copy the code

Modify getRequestParam

/** * Gets the argument * in the intercepted request@param point
 */
private void getRequestParam(a) {
    // Get the simple parameter type
    this.getSimpleParam();

    // Get complex parameter types
    this.getComplexParam();
}
Copy the code

Next, implement the getComplexParam method.

getComplexParam

/** * gets the value of a complex parameter type */
private void getComplexParam(a) {
    for (Object arg : this.args) {
        // Skip simple type values
        if(arg ! =null&&!this.isBasicType(arg)) {
           this.getFieldsParam(arg); }}}Copy the code

getFieldsParam

/** * iterate over a complex type, get the value and assign it to param *@param target
 * @param <T>
 */
private <T> void getFieldsParam(T target) {
    Field[] fields = target.getClass().getDeclaredFields();
    for (Field field : fields) {
        String paramName = field.getName();
        if (this.isExist(paramName)) {
            String value = this.getParam(target, paramName);
            this.setParam(this.params, paramName, value); }}}Copy the code

run

Start the project. Use postman to make a POST request to the above URL. Request body with parameters from TestDTO. When the request returns successfully, you should see the console output below.

Warning player [748327843], game name [Tom], age [12] for job order number [empty] / report ID [8]Copy the code

The above logs can then be logged to the appropriate place as required.

To this may be some brothers feel that the line, everything has, only owe dongfeng. But in fact, there are several problems with this approach.

For example, what if the request fails? A failed request is not required to log an operation at all, but even if the request fails, a return value will be returned, indicating that the log will record success. This brings great trouble to the later viewing log.

For example, what if the argument I need is in the return value? You can run into this problem if you don’t use a uniform service that generates unique ids. For example, if I need to insert new data into the database, I need to get the database increment ID, and our log interception only intercepts the parameters in the request. So that’s what we’re going to do.

Determines whether the request was successful

Implement the SUCCESS function with the following code.

/** * Determine whether the request was successful based on the HTTP status code **@param response
 * @return* /
private Boolean success(HttpServletResponse response) {
    return response.getStatus() == 200;
}
Copy the code

Then wrap all operations after getRequestParam, including getRequestParam itself, in Success. As follows.

if (this.success(response)) {
    // Get the data from the request-passed parameter
    this.getRequestParam();
    if(! logDetail.isEmpty()) {// Replace all the parameters in the template
        logDetail = this.replaceParam(logDetail); }}Copy the code

In this way, it is guaranteed that the logs will only be logged if the request is successful.

Get the returned parameters by reflection

The new Result class

In one project, we use a class to unify the return value.

package spring.aop.log.demo.api.util;

import lombok.Data;

/**
 * Result
 *
 * @author Lunhao Hu
 * @dateThe 2019-02-01 yet * * /
@Data
public class Result {
    private Integer id;

    private String name;

    private Integer age;

    private String email;
}
Copy the code

Modify the controller

@Log(type = "WARNING")
@PostMapping("test")
public Result test(
        @RequestParam(name = "workOrderNumber", required = false) String workOrderNumber,
        @RequestParam(name = "userId") String userId,
        @RequestBody TestDTO testDTO
) {
    Result result = new Result();
    result.setId(1);
    result.setAge(testDTO.getAge());
    result.setName(testDTO.getName());
    result.setEmail(testDTO.getEmail());
    return result;
}
Copy the code

run

If you start the project and make a POST request, you will find that the return value is as follows.

{
    "id": 1,
    "name": "tom"."age": 12."email": "[email protected]"
}
Copy the code

The console output is as follows.

Warning player [748327843], game name [Tom], age [12] for job order number [39424] / Report ID [empty]Copy the code

As you can see, the ID was not obtained. So we also need to add a function to get the id data from the return value.

getResponseParam

After getRequestParam, add the method getResponseParam to call the previously written function directly. Here’s the code.

/** * get data from the return value */
private void getResponseParam(Object value) {
    this.getFieldsParam(value);
}
Copy the code

run

Make the POST request again, and you’ll see the console output below.

Warning player [748327843], game name [Tom], age [12] due to job order number [39424] / Report ID [1]Copy the code

Once we have this information, we can record it anywhere we want.

Project source code address

Want to refer to the source of the big guy please stamp -> here <-

Previous articles:

  • WebAssembly is complete – learn about wASM in its past and present

Related:

  • Personal website: Lunhao Hu
  • Wechat official account: full stack notes of SH (or directly search wechat LunhaoHu in the interface of adding official account)