Springboot AOP custom annotations to achieve logging

1. The interceptor recognizes the request header token, which has been stored in the account information when logging in.
2. When using the token access method, the token can be used to obtain the identity information of the visitor, etc.
3. Mark @Log on the method that needs to record the log. Method record and storage

On the code
maven:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

Two entity classes to be used:

/**
 * 模拟用户信息
 */
@Data
public class UserInfo {

    String username;
    Integer age;
}

//==========================

/**
 * 模拟存储的日志 实体类
 */
@Data
public class SysLog extends BaseEntity{
	
	//操作人id
	private String operatorId;
	//请求类型,0=ajax,1=普通请求
	private int type; 
	//请求动作
	private String action;
	//请求主机
	private String host;
	//请求路径
	private String uri;
	//请求方式
	private String httpMethod;
	//请求类的方法
	private String classMethod;
	//请求参数
	private String params;
}

1. First customize the annotation

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

@Target(ElementType.METHOD)  //表示注解只能标注在方法上
@Retention(RetentionPolicy.RUNTIME) //运行时注解
public @interface Log {

    String value() default "";   //默认值为空串,否则必须传入一个值

}

2. Customize the aspect, all methods with @Log will enter this aspect

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kdmins.annotation.Log;
import com.kdmins.pojo.SysLog;
import com.kdmins.pojo.UserInfo;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.*;


@Aspect
@Component
@Slf4j
public class LogAspect {

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private ObjectMapper objectMapper;


    /**
     * 自定义切点表达式,检测到@Log 注解的进入aop切面
     */
    @Pointcut("@annotation(com.kdmins.annotation.Log)")
    public void pointcut() {
    }


    /**
     * desc: 后置通知,   @Before在Controller层操作前拦截(自己选择)
     *
     * @param joinPoint 切入点
     */
    @After("pointcut()")
    public void before(JoinPoint joinPoint) {
        log.info("=========注解已生效==========");
        try {
            handleLog(joinPoint);
        } catch (Exception e) {
            log.error("[LogAspect] doBefore error = {}", e);
        }
    }

    /**
     * desc: 通过反射获取日志信息,存入数据库
     *
     * @param joinPoint 切点
     * @throws Exception
     */
    private void handleLog(final JoinPoint joinPoint) throws Exception {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        //获取注解
        Log logInfo = method.getAnnotation(Log.class);
        if (logInfo == null) {
            return;
        }

        String method1 = request.getMethod();
        System.out.println("请求类型为" + method1);
        SysLog sysLog = setSysLogInfo(joinPoint, method, logInfo);
        System.out.println(sysLog);

        //todo 日志入库
    }


    /**
     * desc: 初始化日志信息,存入数据库
     *
     * @return SysLog
     */
    private SysLog setSysLogInfo(JoinPoint joinPoint, Method method, Log logInfo ) {
        //拦截器已经存入,直接request获取
        UserInfo  userInfo = (UserInfo) request.getAttribute("userInfo");
        // 操作数据库日志表
        SysLog sysLog = new SysLog();
        //获取当前登陆人,获取操作人id,此处不写获取当前登录人的逻辑,直接写死operatorId=1
        sysLog.setOperatorId(userInfo.getUsername());
        // 请求信息
        // sysLog.setType(AjaxUtils.isAjax(request) ? 0 : 1);
        sysLog.setAction(logInfo.value());
        sysLog.setHost(request.getRemoteHost());
        sysLog.setUri(request.getRequestURI().toString());
        sysLog.setHttpMethod(request.getMethod());
        String classMethod = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
        sysLog.setClassMethod(classMethod);
        // 请求的方法参数值
        Object[] args = joinPoint.getArgs();
        // 请求的方法参数名称
        LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
        String[] paramNames = discoverer.getParameterNames(method);
        if (args != null && paramNames != null) {
            StringBuilder params = new StringBuilder();
            try {
                params = handleParams(params, args, Arrays.asList(paramNames));
            } catch (JsonProcessingException e) {
                log.error("[LogAspect] setSysLogInfo handleParams error = {}", e);
            }
            sysLog.setParams(params.toString());
        }
        return sysLog;
    }


    /**
     * desc: 处理请求参数
     *
     * @param params
     * @param args
     * @param paramNames
     * @return
     * @throws JsonProcessingException
     */
    private StringBuilder handleParams(StringBuilder params, Object[] args, List paramNames) throws JsonProcessingException {
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof Map) {
                Set set = ((Map) args[i]).keySet();
                List list = new ArrayList();
                List paramList = new ArrayList<>();
                for (Object key : set) {
                    list.add(((Map) args[i]).get(key));
                    paramList.add(key);
                }
                return handleParams(params, list.toArray(), paramList);
            } else {
                if (args[i] instanceof Serializable) {
                    Class<?> aClass = args[i].getClass();
                    try {
                        aClass.getDeclaredMethod("toString", new Class[]{null});
                        // 如果不抛出NoSuchMethodException 异常则存在 toString 方法 ,安全的writeValueAsString ,否则 走 Object的 toString方法
                        params.append("  ").append(paramNames.get(i)).append(": ").append(objectMapper.writeValueAsString(args[i]));
                    } catch (NoSuchMethodException e) {
                        params.append("  ").append(paramNames.get(i)).append(": ").append(objectMapper.writeValueAsString(args[i].toString()));
                    }
                } else if (args[i] instanceof MultipartFile) {
                    MultipartFile file = (MultipartFile) args[i];
                    params.append("  ").append(paramNames.get(i)).append(": ").append(file.getName());
                } else {
                    params.append("  ").append(paramNames.get(i)).append(": ").append(args[i]);
                }


            }
        }
        return params;
    }
}

3. Implement the interceptor

import com.kdmins.pojo.UserInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @version 1.0
 * @author: lsy
 * @create: 2021-05-31 15:04
 **/
@Component
@Slf4j
public class TokenInterceptor extends HandlerInterceptorAdapter {

    /**
     * 预处理回调方法,实现处理器的预处理(如检查登陆),第三个参数为响应的处理器,自定义Controller
     * 返回值:true表示继续流程(如调用下一个拦截器或处理器);false表示流程中断(如登录检查失败),不会继续调用其他的拦截器或处理器,此时我们需要通过response来产生响应;
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("进入预拦截方法");
        String token = request.getHeader("token");
        log.info("获取到的 token 值 :{}", token);

        //todo  token中存放的账号信息等 (模拟从token中获取)
        UserInfo userInfo = new UserInfo();
        userInfo.setUsername("admin");
        userInfo.setAge(11);
        // 设置 request attribute 在Controller可以直接 用 @RequestAttribute
        request.setAttribute("userInfo", userInfo);
        //request.setAttribute("auditLog", null);

        //token 校验成功后 可继续校验权限相关,数据库或redis都可以,此类交给spring了,可直接注入
        if ("token".equals(token)) {
            return true;
        }

        //todo 此处可以抛出异常,然后用全局异常包裹后返回响应,也可以直接返回 (建议)
        response.setCharacterEncoding("utf-8");
        response.getWriter().write("token校验失败");
        return false;
    }

    /**
     * 后处理回调方法,实现处理器的后处理(但在渲染视图之前),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。
     *
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    /**
     * 整个请求处理完毕回调方法,即在视图渲染完毕时回调,如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中
     *
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //此方法必须是preHandle方法放行后才会执行
        log.info("方法处理完毕,开始清理request");

        // 移除request attribute
        request.removeAttribute("currentAccount");
        request.removeAttribute("auditLog");
        request.removeAttribute("responseBody");
        //清理资源
        super.afterCompletion(request, response, handler, ex);

    }
}

4. Filter and turn on the interceptor

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 过滤器并开启token拦截器
 *
 * @version 1.0
 * @author: lsy
 * @create: 2021-05-31 15:25
 **/
@Configuration
public class InterceptorConfiguration implements WebMvcConfigurer {

    @Autowired
    private TokenInterceptor tokenInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        InterceptorRegistration registration = registry.addInterceptor(tokenInterceptor);
        registration.addPathPatterns("/**");
        registration.excludePathPatterns(
                "/login",  //登录方法
                "/captcha",//验证码方法
                "/logout"  //登出方法
        );
    }
}

Start the test: I used the Postman
parameter and the request header token

Insert picture description here


Insert picture description here


annotation to take effect, and the log has been stored in the database

Insert picture description here