As long as you log a lot, locate a BUG shuttle

The complete code covered in this article: GITEE

Or visit: https://gitee.com/topanda/spring-boot-security-quickly-start/tree/jpanda-spring-boot-security-api/src/main/java/cn/jpanda/demo/manager/configuration/log

It is hard to avoid bugs when you write too much code. As a programmer who has been walking all year, it is not a rare thing to talk about an online BUG once in a while

When it comes to online bugs, no matter how big or small, it always makes people sweat. From the time when we found online bugs to the time when we solved them, no one can sit still

Looking at the BUG solution scenario, it's usually one person coding and the whole group watching. It seems that this is not the case, that is, disrespect for BUG

In the meantime, I only heard the clattering of the keyboard. When the team members sat, they stood up and walked back and forth. The shell, log and code in the display were all mixed together

The coder keeps his eyes on the screen and gets information from a line of logs, which corresponds to the code one by one, sometimes smiles and sometimes frowns. If he finds out that the key logs are lost, he will lose his mind

No wonder the programmers make a fuss. The boss doesn't care if the alpine avalanche has something to do with your Himalayan snowflake. The problem can't be solved. The alpine avalanche is caused by you

At this time, a detailed and orderly log record may be able to save you in the water and fire

Determine logging data

The common online BUG is often caused by the incoming data exceeding the threshold value assumed in programming when the user calls

Therefore, in addition to the specific business log, it is necessary to record the parameter values passed in by users

For a call, the key information is: user request address, request mode, hit method, request input parameter, response state

For some interfaces, even the response data and request header of the request can be recorded. In order to analyze the performance of the interface, the processing time and call time of the user's request can also be recorded. In order to locate the cause of the exception, the exception information and exception stack information can also be recorded additionally

Of course, in order to better distinguish interfaces, we can also configure different readable names for different interface requests

In combination with the appeal requirements, we provide a LogInfo object, which is responsible for maintaining the key data involved in an access:

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;

import java.util.Date;
import java.util.Map;

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class LogInfo {
    /**
     * Request path
     */
    private String uri;

    /**
     * Request method
     */
    private HttpMethod httpMethod;

    /**
     * Request header
     */
    private Map<String, String> headers;

    /**
     * Hit Controller
     */
    private Signature signature;

    /**
     * Request parameters
     */
    private Param[] args;
    /**
     * Method description
     */
    private String description;
    /**
     * Request source IP
     */
    private String requestIp;

    /**
     * Request time
     */
    private Long duration;

    /**
     * Response status
     */
    private HttpStatus responseStatus;

    /**
     * Response data
     */
    private Object result;

    /**
     * Error log
     */
    private Object errMessage;

    /**
     * Exception stack information
     */
    private Object errTrace;

    /**
     * Call timestamp
     */
    private Date requestTime;
}

It's worth noting that in order to reduce the amount of data generated during log persistence, we chose to discard the null value data in LogInfo, so we marked @ jsoninclude on the class definition of LogInfo( JsonInclude.Include.NON_ NULL).

The reason for this annotation is that I chose to convert the log object to JSON data through Jackson databind when persisting the log

Define the way to control the logging

The data recorded by the LogInfo object is relatively large and complete. In actual use, we may not need such a detailed log, or for special reasons, we need to mask some data, such as disabling exception stack information and response data

For this scenario, we consider providing a global configuration object, AccessLogProperties, to control the validity of each specific parameter:

import com.live.configuration.log.handler.filters.Filter;
import com.live.configuration.log.handler.filters.NoneFilter;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.io.Serializable;

/**
 * @author HanQi [Jpanda@aliyun.com]
 * @version 1.0
 * @since 2020/6/6 15:34:35
 */
@Data
@ConfigurationProperties("access.log")
public class AccessLogProperties implements Serializable, Cloneable {

    /**
     * Whether to record the request address, core data, it is recommended to enable
     */
    private boolean uri = true;
    /**
     * Whether to record the request header, which is used in special scenarios. It is off by default
     */
    private boolean headers = false;
    /**
     * Request header filter
     */
    private Class<? extends Filter> headersFilter = NoneFilter.class;
    /**
     * Record request method or not
     */
    private boolean httpMethod = true;
    /**
     * Record hit method or not
     */
    private boolean signature = true;
    /**
     * Record request parameters or not
     */
    private boolean params = true;

    /**
     * Whether to record the request source IP
     */
    private boolean ip = true;
    /**
     * Record request time
     */
    private boolean duration = true;
    /**
     * Record response status
     */
    private boolean status = true;
    /**
     * Whether to record the response body
     */
    private boolean result = true;

    /**
     * Critical exception log
     */
    private boolean errMessage = true;
    /**
     * So stack information
     */
    private boolean errTrace = false;


    /**
     * Call time
     */
    private boolean requestTime;

    @Override
    public AccessLogProperties clone() {

        AccessLogProperties clone = new AccessLogProperties();
        clone.setUri(this.uri);
        clone.setHeaders(this.headers);
        clone.setHeadersFilter(this.headersFilter);
        clone.setHttpMethod(this.httpMethod);
        clone.setSignature(this.signature);
        clone.setParams(this.params);
        clone.setIp(this.ip);
        clone.setDuration(this.duration);
        clone.setStatus(this.status);
        clone.setResult(this.result);
        clone.setErrMessage(this.errMessage);
        clone.setErrTrace(this.errTrace);
        clone.setRequestTime(this.requestTime);
        return clone;
    }
}

In AccessLogProperties, a filter property named headersFilter is involved. This property points to an effective instance of filter. Filter is used to filter the request header information to be recorded. From the design point of view, we restrict the instances of filter to provide a non parameter construction method

public interface Filter {
    boolean filter(String str);
}

The default Filter implementation, NoneFilter, will Filter out all request headers:

public class NoneFilter implements Filter {
    @Override
    public boolean filter(String str) {
        return false;
    }
}

In addition, the property definition in AccessLogProperties corresponds to the property in LogInfo one by one, which controls whether the corresponding property takes effect

In addition to global configuration, for some interfaces, we may not need to record so much information. For example, interface definitions such as file download and file upload, we do not need to record input and output parameter data

For this reason, we provide a LogOptions annotation. The property definition and behavior of this annotation are completely consistent with that of AccessLogProperties. The LogOptions annotation is used for local log configuration, and its priority is higher than the global default configuration

import com.live.configuration.log.handler.filters.Filter;
import com.live.configuration.log.handler.filters.NoneFilter;

/**
 * Log option, which can override the default behavior of the log
 *
 * @author HanQi [Jpanda@aliyun.com]
 * @version 1.0
 * @since 2020/6/8 9:57:13
 */
public @interface LogOptions {

    /**
     * Whether to record the request address, core data, it is recommended to enable
     */
    boolean uri() default true;

    /**
     * Whether to record the request header, which is used in special scenarios. It is off by default
     */
    boolean headers() default false;

    /**
     * Request header filter
     */
    Class<? extends Filter> headersFilter() default NoneFilter.class;

    /**
     * Record request method or not
     */
    boolean httpMethod() default true;

    /**
     * Record hit method or not
     */
    boolean signature() default true;

    /**
     * Record request parameters or not
     */
    boolean params() default true;

    /**
     * Whether to record the request source IP
     */
    boolean ip() default true;

    /**
     * Record request time
     */
    boolean duration() default true;

    /**
     * Record response status
     */
    boolean status() default true;

    /**
     * Whether to record the response body
     */
    boolean result() default false;

    /**
     * Log key information
     */
    boolean errMessage() default true;

    /**
     * Log stack information
     */
    boolean errTrace() default false;

    /**
     * Call time
     */
    boolean requestTime() default true;
}

Clear usage of log records

After determining the logging data and configuration content, we need to provide corresponding solutions for it. In the implementation, the basic idea is to intercept each request with the help of OncePerRequestFilter object, so as to record the method call time, request processing time and response state, and complete the log persistence according to the persistence policy

However, because the relevant data of the interface hit method cannot be easily obtained in OncePerRequestFilter, the static method matcherpointcutadvisor interface is additionally provided to intercept the methods that need to record logs and complete the acquisition of the LogInfo data

The data transfer between OncePerRequestFilter and StaticMethodMatcherPointcutAdvisor is completed by LogHolder, which caches the relevant log data and log configuration data in this request with the help of ThreadLocal

import java.util.Optional;

/**
 * Loggers
 *
 * @author HanQi [Jpanda@aliyun.com]
 * @version 1.0
 * @since 2020/6/8 11:31:06
 */
public class LogHolder {
    private static AccessLogProperties DEFAULT_ACCESS_LOG_PROPERTIES = new AccessLogProperties();

    /**
     * Currently logged log data
     */
    private static final ThreadLocal<LogInfo> LOG_THREAD_LOCAL = new ThreadLocal<>();

    private static final ThreadLocal<AccessLogProperties> CONFIG_THREAD_LOCAL = ThreadLocal.withInitial(LogHolder::createDefaultConfig);

    public static boolean hasLog() {
        return Optional.ofNullable(LOG_THREAD_LOCAL.get()).isPresent();
    }

    public static LogInfo log() {
        return LOG_THREAD_LOCAL.get();
    }

    public static void log(LogInfo log) {
        LOG_THREAD_LOCAL.set(log);
    }

    public static void clear() {
        LOG_THREAD_LOCAL.remove();
        CONFIG_THREAD_LOCAL.remove();
    }

    public static AccessLogProperties config() {
        return CONFIG_THREAD_LOCAL.get();
    }

    public static void config(AccessLogProperties accessLogProperties) {
        CONFIG_THREAD_LOCAL.set(accessLogProperties);
    }

    public static AccessLogProperties createDefaultConfig() {
        return DEFAULT_ACCESS_LOG_PROPERTIES.clone();
    }

    public static void initDefaultConfig(AccessLogProperties accessLogProperties) {
        DEFAULT_ACCESS_LOG_PROPERTIES = accessLogProperties;
    }
}

Because in the call chain of a request, the method call can not get the accurate response status code, so it also needs to configure OncePerRequestFilter to use together

The implementation class of OncePerRequestFilter AccessLogOncePerRequestFilter is not complex. It mainly does four things:

  • Record request call time
  • Record request processing time
  • Record request response status
  • Call LogPersistence to complete log persistence
import com.live.configuration.log.entitys.AccessLogProperties;
import com.live.configuration.log.entitys.LogHolder;
import com.live.configuration.log.entitys.LogInfo;
import com.live.configuration.log.persistence.LogPersistence;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;

/**
 * Log
 *
 * @author HanQi [Jpanda@aliyun.com]
 * @version 1.0
 * @since 2020/6/8 11:05:24
 */
@Slf4j
public class AccessLogOncePerRequestFilter extends OncePerRequestFilter {

    private LogPersistence logPersistence;

    public AccessLogOncePerRequestFilter(LogPersistence logPersistence) {
        this.logPersistence = logPersistence;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {

        try {
            doFilter(httpServletRequest, httpServletResponse, filterChain);
        } finally {
            LogHolder.clear();
        }
    }

    private void doFilter(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        // Record request time
        long start = System.currentTimeMillis();
        Date now = new Date();
        try {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        } finally {
            // Record response time and response status 1
            long end = System.currentTimeMillis();
            if (LogHolder.hasLog()) {
                AccessLogProperties config = LogHolder.config();
                LogInfo log = LogHolder.log();
                if(config.isRequestTime()){
                    log.setRequestTime(now);
                }
                if (config.isDuration()) {
                    log.setDuration(end - start);
                }
                if (config.isStatus()) {
                    log.setResponseStatus(HttpStatus.valueOf(httpServletResponse.getStatus()));
                }
                doLog(log);
            }

        }

    }

    protected void doLog(LogInfo logInfo) {
        logPersistence.persistence(logInfo);
    }

}

LogPersistence is responsible for specific log persistence. It defines a void persistence() method to provide corresponding capabilities:

import com.live.configuration.log.entitys.LogInfo;

/**
 * Log persistence
 *
 * @author HanQi [Jpanda@aliyun.com]
 * @version 1.0
 * @since 2020/6/8 13:18:54
 */
public interface LogPersistence {

    void persistence(LogInfo log);
}

Based on the current requirements, with the help of the conventional logging system, I provide a DefaultLogPersistence implementation to complete the simple logging work:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.live.configuration.log.entitys.LogInfo;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

/**
 * Default log persistence policy
 *
 * @author HanQi [Jpanda@aliyun.com]
 * @version 1.0
 * @since 2020/6/8 13:20:00
 */
@Slf4j
public class DefaultLogPersistence implements LogPersistence {
    ObjectMapper objectMapper = new ObjectMapper();

    @Override
    @SneakyThrows
    public void persistence(LogInfo logInfo) {
        log.info("Access log:{}", objectMapper.writeValueAsString(logInfo));
    }

}

The actual log persistence strategy can be customized according to your own needs

The code responsible for making the AccessLogOncePerRequestFilter effective is AccessLogAutoConfiguration. In this class, we transform the AccessLogOncePerRequestFilter object into a spring bean, and introduce the global configuration object AccessLogProperties:

import com.live.configuration.log.entitys.AccessLogProperties;
import com.live.configuration.log.persistence.DefaultLogPersistence;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.web.filter.OncePerRequestFilter;

/**
 * @author HanQi [Jpanda@aliyun.com]
 * @version 1.0
 * @since 2020/6/8 14:50:15
 */
@Import({AccessLogProperties.class})
public class AccessLogAutoConfiguration {
    @Bean
    public OncePerRequestFilter oncePerRequestFilter() {
        return new AccessLogOncePerRequestFilter(new DefaultLogPersistence());
    }
}

After defining the general implementation scheme, which method to record the access log becomes the next problem to be considered. The most common and simplest method in the industry is to provide a separate annotation to complete the log recording work in the annotation section. Therefore, we provide an AccessLog annotation, The annotation has only one value() attribute to maintain a readable alias for the interface:

import java.lang.annotation.*;

/**
 * Log comments
 *
 * @author HanQi [Jpanda@aliyun.com]
 * @version 1.0
 * @since 2020/6/6 15:39:59
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLog {
    /**
     * Log name, optional
     */
    String value() default "";
}

In addition, considering that sometimes we may need to record all access logs, we choose spring's native Mapping annotation as another candidate aspect after a simple survey:

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

/**
 * Meta annotation that indicates a web mapping annotation.
 *
 * @author Juergen Hoeller
 * @since 3.0
 * @see RequestMapping
 */
@Target({ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Mapping {

}

After all, Mapping is the meta annotation of interface Mapping related annotations in spring. In order to be compatible with the aspect support of meta annotation, we provide an AnnotationAspectPointCutAdvisor, which is the implementation class of StaticMethodMatcherPointcutAdvisor. This object has an annotation attribute named interceptionAnnotation, which maintains the method annotation that currently needs to be faceted, At the same time, the implementation of the object's matches() method relies on AnnotationUtils.findAnnotation() method, so it supports getting meta annotation on method annotation:

import com.live.configuration.log.handler.AccessLogProceedingJoinPointHandler;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor;
import org.springframework.core.annotation.AnnotationUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

/**
 * Agree to block log
 *
 * @author HanQi [Jpanda@aliyun.com]
 * @version 1.0
 * @since 2020/6/9 9:37:10
 */
public class AnnotationAspectPointCutAdvisor extends StaticMethodMatcherPointcutAdvisor {
    AccessLogProceedingJoinPointHandler handler;
    private Class<? extends Annotation> interceptionAnnotation;


    public AnnotationAspectPointCutAdvisor(Class<? extends Annotation> interceptionAnnotation, AccessLogProceedingJoinPointHandler handler) {
        this.interceptionAnnotation = interceptionAnnotation;
        this.handler = handler;
        setAdvice((MethodInterceptor) handler::handler);
    }

    @Override
    public boolean matches(Method method, Class<?> aClass) {
        return AnnotationUtils.findAnnotation(method, interceptionAnnotation) != null;
    }
}

In addition, the method also maintains an AccessLogProceedingJoinPointHandler, which is responsible for filling the specific LogInfo

Don't pay too much attention to the name AccessLogProceedingJoinPointHandler, which was originally designed not to handle MethodInvocation but ProceedingJoinPoint objects

AnnotationAspectPointCutAdvisor has two implementation classes, which are used to process AccessLog and Mapping annotations respectively:

MappingAnnotationAspectPointCutAdvisor:

import com.live.configuration.log.handler.AccessLogProceedingJoinPointHandler;
import org.springframework.web.bind.annotation.Mapping;

/**
 * @author HanQi [Jpanda@aliyun.com]
 * @version 1.0
 * @since 2020/6/9 10:04:04
 */
public class MappingAnnotationAspectPointCutAdvisor extends AnnotationAspectPointCutAdvisor {

    public MappingAnnotationAspectPointCutAdvisor(AccessLogProceedingJoinPointHandler handler) {
        super(Mapping.class, handler);
    }
}

AccessLogAnnotationAspectPointCutAdvisor:

import com.live.configuration.log.annotations.AccessLog;
import com.live.configuration.log.handler.AccessLogProceedingJoinPointHandler;

/**
 * @author HanQi [Jpanda@aliyun.com]
 * @version 1.0
 * @since 2020/6/9 10:04:31
 */
public class AccessLogAnnotationAspectPointCutAdvisor extends AnnotationAspectPointCutAdvisor {
    public AccessLogAnnotationAspectPointCutAdvisor(AccessLogProceedingJoinPointHandler handler) {
        super(AccessLog.class, handler);
    }
}

In design, these two classes are mutually exclusive, and only one class can take effect

The EnableAccessLog annotation is responsible for controlling this behavior. This annotation needs to be marked on the SpringBoot startup class to enable the log aspect:

import com.live.configuration.log.AccessLogAutoConfiguration;
import com.live.configuration.log.AccessLogImportSelector;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;

import java.lang.annotation.*;

/**
 * Open log or not
 *
 * @author HanQi [Jpanda@aliyun.com]
 * @version 1.0
 * @since 2020/6/8 10:59:01
 */
@Component
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import({AccessLogImportSelector.class, AccessLogAutoConfiguration.class})
public @interface EnableAccessLog {
    /**
     * Block all requests
     */
    boolean enableGlobal() default false;
}

This annotation defines an enableGlobal() attribute with a default value of false, which is used to control the effective aspect. When this value is true, the access logs of all interfaces marked with Mapping meta annotation directly or indirectly are recorded. Otherwise, the access logs of interfaces marked with AccessLog annotation are recorded

The EnableAccessLog annotation introduces the AccessLogImportSelector and AccessLogAutoConfiguration configuration objects through the Import annotation to complete the Import of the objects required for logging

The AccessLogImportSelector is responsible for determining whether to import MappingAnnotationAspectPointCutAdvisor or AccessLogAnnotationAspectPointCutAdvisor according to the value of enableGlobal():

import com.live.configuration.log.annotations.EnableAccessLog;
import com.live.configuration.log.visitors.AccessLogAnnotationAspectPointCutAdvisor;
import com.live.configuration.log.visitors.MappingAnnotationAspectPointCutAdvisor;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;

/**
 * @author HanQi [Jpanda@aliyun.com]
 * @version 1.0
 * @since 2020/6/8 17:19:50
 */
public class AccessLogImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        // Annotation name to be processed
        Class<?> annotationType = EnableAccessLog.class;
        AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap(
                annotationMetadata.getAnnotationAttributes(annotationType.getName(), false)
        );
        // In a normal scenario, this parameter will never be null
        assert annotationAttributes != null;
        boolean global = annotationAttributes.getBoolean("enableGlobal");
        return new String[]{
                (global ? MappingAnnotationAspectPointCutAdvisor.class : AccessLogAnnotationAspectPointCutAdvisor.class).getCanonicalName()
        };
    }
}

The previously ignored AccessLogProceedingJoinPointHandler interface provides a DefaultAccessLogProceedingJoinPointHandler implementation class to complete the ability to fill in the log data according to the configuration:

import com.live.configuration.log.annotations.AccessLog;
import com.live.configuration.log.annotations.LogOptions;
import com.live.configuration.log.entitys.*;
import com.live.configuration.log.handler.filters.Filter;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;

/**
 * @author HanQi [Jpanda@aliyun.com]
 * @version 1.0
 * @since 2020/6/8 16:35:40
 */
@Component
public class DefaultAccessLogProceedingJoinPointHandler implements AccessLogProceedingJoinPointHandler {
    @Override
    public Object handler(MethodInvocation point) throws Throwable {

        Object result = null;
        Throwable throwable = null;
        try {
            // Process request
            result = point.proceed();
        } catch (Exception e) {
            throwable = e;
        }


        // Processing log parameters
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = Objects.requireNonNull(requestAttributes).getRequest();


        Method method = point.getMethod();

        LogInfo logInfo = new LogInfo();
        logInfo.setDescription(readMethodDescription(method));

        // Get configuration object
        AccessLogProperties config = loadCustomConfig(AnnotationUtils.findAnnotation(method, LogOptions.class));

        if (config.isUri()) {
            logInfo.setUri(request.getRequestURL().toString());
        }
        if (config.isHttpMethod()) {
            logInfo.setHttpMethod(HttpMethod.resolve(request.getMethod()));
        }
        if (config.isHeaders()) {
            // Load get
            Class<? extends Filter> headerFilter = config.getHeadersFilter();
            Filter filter = headerFilter.newInstance();
            Enumeration<String> enumeration = request.getHeaderNames();
            Map<String, String> matchHeaders = new HashMap<>();
            while (enumeration.hasMoreElements()) {
                String headerName = enumeration.nextElement();
                String headerValue = request.getHeader(headerName);
                if (filter.filter(headerName)) {
                    matchHeaders.put(headerName, headerValue);
                }
            }
            logInfo.setHeaders(matchHeaders);
        }

        if (config.isSignature()) {
            Signature signature1 = new Signature();
            signature1.setClassName(point.getThis().getClass().getCanonicalName());
            signature1.setMethodName(point.getMethod().getName());
            logInfo.setSignature(signature1);
        }

        if (config.isParams()) {
            logInfo.setArgs(castParams(method, point.getArguments()));
        }
        if (config.isIp()) {
            logInfo.setRequestIp(loadRealIp(request));
        }

        if (throwable != null) {
            if (config.isErrMessage()) {
                logInfo.setErrMessage(throwable.getMessage());
            }
            if (config.isErrTrace()) {
                logInfo.setErrTrace(throwable);
            }
        } else {
            if (config.isResult()) {
                logInfo.setResult(result);
            }

        }
        if (throwable != null) {
            throw throwable;
        }

        LogHolder.log(logInfo);
        // Return request results
        return result;
    }


    public AccessLogProperties loadCustomConfig(LogOptions logOptions) {
        return Optional.ofNullable(logOptions).map(opt -> {
            AccessLogProperties properties = new AccessLogProperties();
            properties.setUri(opt.uri());
            properties.setHeaders(opt.headers());
            properties.setHeadersFilter(opt.headersFilter());
            properties.setHttpMethod(opt.httpMethod());
            properties.setSignature(opt.signature());
            properties.setParams(opt.params());
            properties.setIp(opt.ip());
            properties.setDuration(opt.duration());
            properties.setStatus(opt.status());
            properties.setResult(opt.result());
            properties.setErrMessage(opt.errMessage());
            properties.setErrTrace(opt.errTrace());

            LogHolder.config(properties);
            return properties;
        }).orElse(LogHolder.config());
    }

    /**
     * Read method description
     *
     * @param method method
     * @return Method description or method name
     */
    protected String readMethodDescription(Method method) {
        if (!method.isAccessible()) {
            method.setAccessible(true);
        }
        // Get method description annotation
        String description = method.getName();
        // Read swagger annotation
        AccessLog accessLog = AnnotationUtils.findAnnotation(method, AccessLog.class);
        if (accessLog != null) {
            // Get parameter description
            description = accessLog.value();
        }
        return description;
    }

    protected Param[] castParams(Method method, Object[] args) {
        if (args == null) {
            return null;
        }
        Class<?>[] types = method.getParameterTypes();
        String[] argNames = Arrays.stream(method.getParameters()).map(Parameter::getName).toArray(String[]::new);

        Param[] params = new Param[args.length];
        for (int i = 0; i < args.length; i++) {
            Object o = args[i];
            // Read swagger annotation
            params[i] = Param.builder()
                    .argName(argNames[i])
                    .name(argNames[i])
                    .type(types[i])
                    .value(args[i])
                    .build();

        }
        return params;
    }

    protected String loadRealIp(HttpServletRequest request) {
        for (String header : Arrays.asList("x-forwarded-for"
                , "Proxy-Client-IP"
                , "WL-Proxy-Client-IP"
                , "HTTP_CLIENT_IP"
                , "HTTP_X_FORWARDED_FOR"
                , "X-Real-IP")) {
            String value = request.getHeader(header);
            if (StringUtils.hasText(value)) {
                if (value.contains(",")) {
                    String[] ips = value.split(",");
                    return ips[ips.length - 1];
                } else {
                    return value;
                }
            }
        }
        return "unknown";
    }
}

Easy to use

@GetMapping("normal")
public Object forceWrapperResult() {
    //  {
    //  "success": true,
    //  "code": "200",
    //  "data": "normal",
    //  "errorMessage": "",
    //  "timestamp": 1591348788622
    //  }
    return "normal";
}
@AccessLog("Test log comments")
@GetMapping("log")
public String log() {
    return "test";
}

@GetMapping("log-param")
public String logParam(@Param("name") String name) {
    return "log-param";
}
[INFO ] 11:26:48.422 [http-nio-8080-exec-7] [] c.l.c.l.p.DefaultLogPersistence - Access log:{"uri":"http://127.0.0.1:8080/live/examples/normal","httpMethod":"GET","signature":{"className":"com.live.controllers.ExamplesController","methodName":"forceWrapperResult"},"args":[],"description":"forceWrapperResult","requestIp":"unknown","duration":14,"responseStatus":"OK","result":"normal"}
[INFO ] 11:26:51.492 [http-nio-8080-exec-9] [] c.l.c.l.p.DefaultLogPersistence - Access log:{"uri":"http://127.0.0.1:8080/live/examples/log","httpMethod":"GET","signature":{"className":"com.live.controllers.ExamplesController","methodName":"log"},"args":[],"description ":" test log Annotation "," requestIp":"unknown","duration":2,"responseStatus":"OK","result":"test "}
[INFO ] 11:26:52.965 [http-nio-8080-exec-10] [] c.l.c.l.p.DefaultLogPersistence - Access log:{"uri":"http://127.0.0.1:8080/live/examples/log-param","httpMethod":"GET","signature":{"className":"com.live.controllers.ExamplesController","methodName":"logParam"},"args":[{"argName":"arg0","name":"arg0","type":"java.lang.String","value":null}],"description":"logParam","requestIp":"unknown","duration":2,"responseStatus":"OK","result":"log-param"}

extend

At present, this article has many supplements, such as integrating session or user unique flag into the log record

If the request triggers an exception before hitting the method, and the method cannot be hit, the request will not be logged

Keywords: Programming Java Spring Lombok Attribute

Added by deejay1111 on Wed, 10 Jun 2020 06:50:17 +0300