How to elegantly log complex Web interfaces with parameters using Aop
January 21, 2024
by 晏宇軒
No Comments
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@ComponentpublicclassLogAspect{
/** * defines the pointcut */@Pointcut("@annotation(spring.aop.log.demo.api.util.Log)")
publicvoidoperationLog(a){}/** * Trigger ** when the new result is returned@param point
* @param returnValue
*/@AfterReturning(returning = "returnValue", pointcut = "operationLog() && @annotation(log)")
publicvoiddoAfterReturning(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.
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.
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 "* * /publicenum 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.
Make doAfterReturning in the AOP class as follows.
@AfterReturning(returning = "returnValue", pointcut = "operationLog() && @annotation(log)")
publicvoiddoAfterReturning(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 * * /@DatapublicclassParam{
/** * 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.
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 classthis.params = new Param();
// Type in annotation
String enumKey = log.type();
String logDetail = Type.valueOf(enumKey).getOperation();
// Get the data from the request-passed parameterthis.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
*/privatevoidgetRequestParam(a){
// Get the simple parameter typethis.getSimpleParam();
}
Copy the code
getSimpleParam
/** * gets the value of the simple parameter type */privatevoidgetSimpleParam(a){
// Iterate over the parameter names in the requestfor (String reqParam : this.paramNames) {
// Check whether the parameter exists in the parameter classif (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) {
returnnull;
}
return str.substring(0.1).toUpperCase() + str.substring(1);
}
Copy the code
setRequestParamValueIntoParam
Here’s the code.
/** * gets * from the argument@param paramName
* @return* /privatevoidsetRequestParamValueIntoParam(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> voidsetParam(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.
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.
/** * 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
/** * Gets the argument * in the intercepted request@param point
*/privatevoidgetRequestParam(a){
// Get the simple parameter typethis.getSimpleParam();
// Get complex parameter typesthis.getComplexParam();
}
Copy the code
Next, implement the getComplexParam method.
getComplexParam
/** * gets the value of a complex parameter type */privatevoidgetComplexParam(a){
for (Object arg : this.args) {
// Skip simple type valuesif(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> voidgetFieldsParam(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 parameterthis.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.