The introduction

Elegant API design is more than a written specification at the code level. It’s almost impossible for an API to be developed and put into use. It’s more about polishing the details. For example, each time the interface is executed, the input parameter is reviewed repeatedly in the API test

thinking

How do you design a solution that allows developers to see at a glance whether the interface’s processing time and input parameters are correct?

Train of thought

The first thing that comes to mind is the AOP aspect of Spring. Now we write API interfaces, which are generally written in the Controller control layer and divided into controller classes under different business packages according to different businesses. The general structure is as follows:According to the writing specification of this control layer, it is only necessary to find the Controller class under each service package by section, monitor the entry parameter and execution time of each method under the class, and print it in the log to visualize the real-time status of each interface in the console.

practice

Guide package

<dependency> <! GroupId >org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <! GroupId > <artifactId> Spring-boot-starter-AOP </artifactId> </dependency>Copy the code

AOP core

Aop is all about pointcuts and advice types. With the solution we need to implement, the cut point we focus on is each class method of the control layer package under each business. The main types of notification are:

  • [Before advice]: is executed before the join point, and the lead notification does not affect the execution of the join point unless an exception is thrown there.
  • [After Returning Advice]: executes after normal execution of the join point, not if the join point throws an exception.
  • [After Throwing Advice]: executes after the join point throws an exception.
  • Return advice [After (finally) advice]: executes after the join point execution completes, either when normal execution completes or when an exception is thrown.
  • [Around Advice]The: wrap notification surrounds the join point, such as before and after a method call. This is the most powerful type of notification and allows you to customize some actions before and after a method call. The surround notification is also responsible for deciding whether to continue processing the JoinPoint (which calls the ProceedingJoinPoint method PROCEED) or to interrupt execution.

Because we need to record the input and interface processing time, we use Before and Around

Define the point of tangency

As a first step, we need to identify the pointcut and create a new RuntimeMethod class with the @aspect @Component modifier. This is a spring-managed Aspect entry class. The @log4j2 annotation makes it easier to print logs later

@aspect @Component @log4j2 public class RuntimeMethod {// Define the aopPoint private method, decorate it with @pointcut and mark the point at which the Aspect is cut // to execute (*) com.staging.business.*.controller.*.*(..) // Com.staging. Business is the package name of the service that the AOP cuts. The business class that needs to be crosscut is called ".. ", "*" after the current package and its subpackages //, "*" after the class name, * that is, all classes //.*(..) Pointcut(" Execution (* com.staging. Business.*.controller.*.*(..)); )") private void aopPoint() { } }Copy the code

Section Step 2, define the front and surround advice and declare the pointcut of the advice as aopPoint()

/** * */ @before ("aopPoint()") public void Before(JoinPoint JoinPoint) throws Throwable {// This is entered Before calling the interface for section management} /** * Function Description: */ @around ("aopPoint()") public Object Around(ProceedingJoinPoint joinPoint) throws Throwable { Object result = joinPoint.proceed(); // The client cannot get the return argument until the result Object is returned. return result; }Copy the code

The previous two steps implement the two notifications needed and briefly explain their role. Next we need to use the ServletRequestAttributes object in the Spring package to get the HttpServletRequest object to get some of the print parameters we want.

Public void before(JoinPoint JoinPoint) throws Throwable {// ServletRequestAttributes requestAttributes is entered before calling the interface for managing facets  = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); Enumeration<String> e = request.getHeaderNames(); JSONObject headers = new JSONObject(); if (null ! = e) { while (e.hasMoreElements()) { String headerName = e.nextElement(); Enumeration<String> headerValues = request.getHeaders(headerName); while (headerValues.hasMoreElements()) { headers.put(headerName, headerValues.nextElement()); }}} / / arguments, in turn, represents the request method, request address, parameters, parameters, call time log. The info (" - in - {} {} -{}{}",request.getMethod(),request.getRequestURI(),joinPoint.getArgs(),headers.toJSONString()} }Copy the code

The interface call time can also be easily printed in the surround notification

public Object around(ProceedingJoinPoint joinPoint) throws Throwable { long begin=System.currentTimeMillis(); Object result = joinPoint.proceed(); // The client cannot get the return argument until the result Object is returned. long end= System.currentTimeMillis(); log.info("-out -time:{}ms", end - begin} return result; }Copy the code

Run, call API interface, we will output the following log

-in- GET /user/info -id=123 header:{"content-length":"0",...... } -out- -time:91ms ......Copy the code

There is no problem with the test, of course this is not the final version, try to put it in the test environment, call more people, it will be very confusing, similar to the following style

-in- GET /user/info -id=123 header:{"content-length":"0",...... } -in- GET /teacher/info -id=123 header:{"content-length":"0",...... } -out- -time:91ms -in- GET /user/info -id=321 header:{"content-length":"0",...... } -out- -time:191ms ......Copy the code

As you can see, the problem is with concurrent operations. When multiple interfaces are called at the same time, the logs get messed up, which is not what I want. Something must be done to solve the problem. Scrolling through the data, I came up with the idea of using ThreadLocal ThreadLocal variables and Tuple objects to solve this problem. Next, change the code. Define a private variable ThreadLocal in the RuntimeMethod class.

    private ThreadLocal<Tuple6<String, String, Object[], String, Long, String>> threadLocal = new ThreadLocal<>();
Copy the code

Remodel the notification part

@before ("aopPoint()") public void Before(JoinPoint JoinPoint) throws Throwable {// Prints the request body ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (null ! = requestAttributes) {// loadingThreadLocal stores parameters with ThreadLocal and Tuple objects. LoadingThreadLocal (requestAttributes, JoinPoint.getargs ()); log.info("-in- {} {} -{}", threadLocal.get().getT1(), threadLocal.get().getT2(), threadLocal.get().getT6()); log.info("Method arguments:{} -{}", threadLocal.get().getT3(), threadLocal.get().getT6()); log.info("Request header:{} -{}", threadLocal.get().getT4(), threadLocal.get().getT6()); }} @around ("aopPoint()") public Object Around(ProceedingJoinPoint joinPoint) throws Throwable {// Call Object result  = joinPoint.proceed(); String requestUrl = threadLocal.get().getT2(); Log.info ("-out- {} ") ("-out- {} ");} ("-out- {});} ("-out- {}); return:{} -time:{}ms -{}", requestUrl, JSONObject.toJSONString(result), System.currentTimeMillis() - threadLocal.get().getT5(), threadLocal.get().getT6()); Return delReturnData(result); } private void loadingThreadLocal(ServletRequestAttributes requestAttributes, Object[] args) { HttpServletRequest request = requestAttributes.getRequest(); Enumeration<String> e = request.getHeaderNames(); JSONObject headers = new JSONObject(); if (null ! = e) { while (e.hasMoreElements()) { String headerName = e.nextElement(); Enumeration<String> headerValues = request.getHeaders(headerName); while (headerValues.hasMoreElements()) { headers.put(headerName, headerValues.nextElement()); }}} // A call chain id is appended here, which can be returned to the client, so that the client can carry this ID in the next request, the method counts a business loop. String businessId = IdUtil.getSnowflake(1, 1).nextIdStr(); Threadlocal.set (tuples.of (request.getMethod(), request.getrequestUri (), args, threadLocal.set(tuples.of (request.getMethod(), request.getrequestUri ()), args, headers.toJSONString(), System.currentTimeMillis(), businessId)); }Copy the code

Take a look at the interface invocation log after using this scenario

The 2021-01-11 20:16:39. 565 [HTTP - nio - 8080 - exec] INFO cn. MC. Apd [86] - in - the GET/activityArea/getUserPrize -1348604735921459200 2021-01-11 20:16:39.565 [HTTP-nio-8080-exec-7] INFO cn.mc.appod[90] -method arguments:[1] -1348604735921459200 2021-01-11 20:16:39.566 [HTTP-niO-8080-exec-7] INFO cn.mc.app.tood[93] -request header:{"content-length":"0","idfa":"00000",x-nondec-sign":"d93207ba","host":"80""} -1348604735921459200 2021-01-11 20:16:39. [HTTP - nio - 8080-593 the exec] INFO cn. MC. App. View interceptor. RuntimeMethod [126] out -- / activityArea getUserPrize return: {" code ": 0," data ": {" userActivePrizeRec" : "0", "message" : "success"} - time: msCopy the code

Afterword.

At this point, a simplified version of the interface input parameter and interface duration statistics scheme work together. Note that this method will cause too many redundant logs. Avoid using this method in production environments. Add @profile ({“dev”, “test”}) in the class header to specify the environment. Screening in a production environment can be done using Ali dad’s Arthas or Zipkin link tracking. Remember, this solution is only convenient to check the test period error entry parameter and interface time is too long