使用 SpringAop 记录日志

526

日志打印

  • 日志作为项目运行中出错的第一手资料,打印的详细与否直接决定了bug解决的快慢。
  • 新建一个注解值记录的bean
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 功能描述:
 * 【注解值bean】
 *
 * @author chihiro
 * @version V1.0
 * @date 2019/08/18 14:23
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WebLogValue {

    public String value;
    public String name;

}
  • 输出日志
/**
 * 功能描述:
 * 【接口入出参日志打印】
 *
 * @author chihiro
 * @version V1.0
 * @date 2019/08/18 01:06
 */
@Aspect
@Order(99)
@Component
@Slf4j
public class WebLogAspect {

    /**
     * Controller层切点 使用到了spring原生的RequestMapping 作为切点表达式。
     */
    @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public void webLog() {
    }

    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) {
        try {
            StringBuilder params = new StringBuilder();

            Object[] args = joinPoint.getArgs();
            for (int i = 0; i < args.length; i++) {
                Object object = args[i];
				// 此处过滤掉一些无需打印的参数
                if (object instanceof MultipartFile || object instanceof HttpServletRequest || object instanceof HttpServletResponse) {
                    continue;
                }
                params.append(JSON.toJSONString(object));
                if (i < args.length - 1) {
                    params.append(",");
                }
            }
            log.info("类信息[{}],请求参数[{}]", getRequestMappingAnnotationValue(joinPoint), params.toString());
        } catch (Exception e) {
            //记录本地异常日志
            log.error("===前置Controller通知异常===");
            log.error("异常信息:{}", e.getMessage());
        }

    }

    @AfterReturning(returning = "response", pointcut = "webLog()")
    public void doAfterReturning(JoinPoint joinPoint, Object response) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        if (response != null) {
            try {
                log.info("类信息[{}],请求地址[{}],返回参数[{}]", getRequestMappingAnnotationValue(joinPoint), request.getRemoteAddr(), JSON.toJSONString(response));
            } catch (Exception e) {
                //记录本地异常日志
                log.error("===后置Controller通知异常===");
                log.error("异常信息:{}", e.getMessage());
            }
        }
    }

    /**
     * 获取RequestMapping中注解值
     */
    private static WebLogValue getRequestMappingAnnotationValue(JoinPoint joinPoint) throws Exception {
        String targetName = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        Object[] arguments = joinPoint.getArgs();
        Class targetClass = Class.forName(targetName);
        Method[] methods = targetClass.getMethods();
        WebLogValue webLogValue = new WebLogValue();
        for (Method method : methods) {
            if (method.getName().equals(methodName)) {
                Class[] parameterTypes = method.getParameterTypes();
                if (parameterTypes.length == arguments.length) {
                    String[] value = method.getAnnotation(RequestMapping.class).value();
                    String name = method.getAnnotation(RequestMapping.class).name();
                    webLogValue.setValue(Arrays.toString(value));
                    webLogValue.setName(name);
                    break;
                }
            }
        }
        return webLogValue;
    }

日志入库

  • 日志表的DDL信息
CREATE TABLE `pm_log` (
  `log_id` varchar(32) NOT NULL COMMENT '主键id',
  `user_name` varchar(50) DEFAULT NULL COMMENT '用户名',
  `ip` varchar(20) DEFAULT NULL COMMENT 'ip信息',
  `params` varchar(255) DEFAULT NULL COMMENT '请求参数',
  `result` longtext COMMENT '返回结果',
  `method` varchar(150) DEFAULT NULL COMMENT '方法名',
  `operation` varchar(50) DEFAULT NULL COMMENT '操作',
  `unique_code` varchar(20) DEFAULT NULL COMMENT '唯一标识',
  `error` char(2) DEFAULT NULL COMMENT '是否异常00:异常,01:正常',
  `stack` longtext COMMENT '异常堆栈',
  `take_time` bigint(20) DEFAULT NULL COMMENT '请求耗时',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`log_id`) USING BTREE,
  KEY `unique_code` (`unique_code`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

此处数据库设计未经详细考虑,有更好想法的朋友欢迎留言讨论。

  • 核心代码
/**
 * 功能描述:
 * 【操作日志记录切面】
 *
 * @author chihiro
 * @version V1.0
 * @date 2019/08/18 01:06
 */
@Aspect
@Order(100)
@Component
@Slf4j
public class SysLoggerAspect {

    @Autowired
    private LogRecordHandler logRecordHandler;

    @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public void sysLogger() {
    }

    @Before("sysLogger()")
    public void doBeforeController(JoinPoint joinPoint) {
        // 开始时间
        long startTime = System.currentTimeMillis();
        ThreadLocalUtil.setValue(KeyConstants.START_TIME.getKey(), String.valueOf(startTime));

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        SysLog sysLog = new SysLog();
        RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
        if (requestMapping != null) {
            //注解上的描述
            sysLog.setOperation(requestMapping.name());
        }
        //请求的方法名
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = signature.getName();
        sysLog.setMethod(className + "." + methodName + "()");
        //请求的参数
        Object[] args = joinPoint.getArgs();
        StringBuilder sbParams = new StringBuilder();
        for (int i = 0; i < args.length; i++) {
            Object object = args[i];
            if (object instanceof MultipartFile || object instanceof HttpServletRequest || object instanceof HttpServletResponse) {
                continue;
            }
            sbParams.append(JSON.toJSONString(object));
            if (i < args.length - 1) {
                sbParams.append(",");
            }
        }
        String params = sbParams.toString();
        if (StrUtil.isNotBlank(params)) {
            sysLog.setParams(params);
        }
        //请求的用户
        String userName = ThreadLocalUtil.getValue(KeyConstants.USER_NAME.getKey());
        if (StrUtil.isNotBlank(userName)) {
            sysLog.setUserName(userName);
        }
        //用户的IP
        sysLog.setIp(WebUtil.getIpAddress());
        // 设置唯一标识
        sysLog.setUniqueCode(ThreadLocalUtil.getValue(KeyConstants.UNIQUE_CODE.getKey()));
        ThreadLocalUtil.setValue(KeyConstants.SYS_LOG.getKey(), JSON.toJSONString(sysLog));
//        logRecordHandler.recordLog(sysLog);
    }

    @AfterReturning(value = "sysLogger()", returning = "res")
    public void doAfterReturning(JoinPoint joinPoint, Object res) {
        long takeTime = System.currentTimeMillis() - Long.valueOf(ThreadLocalUtil.getValue(KeyConstants.START_TIME.getKey()));
        SysLog sysLog = new SysLog();
        SysLog sys = JSON.parseObject(ThreadLocalUtil.getValue(KeyConstants.SYS_LOG.getKey()), SysLog.class);
        BeanUtil.copyProperties(sys, sysLog);
        // 设置返回结果
        sysLog.setResult(JSON.toJSONString(res));
        // 设置请求耗时
        sysLog.setTakeTime(takeTime);
        sysLog.setError("01");
        ThreadLocalUtil.removeValue(KeyConstants.START_TIME.getKey());
        ThreadLocalUtil.removeValue(KeyConstants.SYS_LOG.getKey());
        // 发送MQ消息
        logRecordHandler.recordLog(sysLog);
    }

    @AfterThrowing(value = "sysLogger()", throwing = "throwable")
    public void doAfterThrowing(JoinPoint joinPoint, Throwable throwable) {
        long takeTime = System.currentTimeMillis() - Long.valueOf(ThreadLocalUtil.getValue(KeyConstants.START_TIME.getKey()));
        SysLog sysLog = new SysLog();
        SysLog sys = JSON.parseObject(ThreadLocalUtil.getValue(KeyConstants.SYS_LOG.getKey()), SysLog.class);
        BeanUtil.copyProperties(sys, sysLog);
        // 设置请求耗时
        sysLog.setTakeTime(takeTime);
        // 设置堆栈信息
        sysLog.setStack(Arrays.toString(throwable.getStackTrace()));
        sysLog.setError("00");
        ThreadLocalUtil.removeValue(KeyConstants.START_TIME.getKey());
        ThreadLocalUtil.removeValue(KeyConstants.SYS_LOG.getKey());
        // 发送MQ消息
        logRecordHandler.recordLog(sysLog);
    }

}
  • 日志实体
/**
 * 功能描述:
 * 【日志实体】
 *
 * @author chihiro
 * @version V1.0
 * @date 2019/08/18 01:59
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SysLog implements Serializable {
    private static final long serialVersionUID = 5L;

    /**
     * 主键id
     */
    private String logId;
    /**
     * 用户名
     */
    private String userName;
    /**
     * 用户操作
     */
    private String operation;
    /**
     * 请求方法
     */
    private String method;
    /**
     * 请求参数
     */
    private String params;
    /**
     * 请求结果
     */
    private String result;
    /**
     * IP地址
     */
    private String ip;
    /**
     * 唯一标识
     */
    private String uniqueCode;
    /**
     * 是否异常
     * 00:异常
     * 01:正常
     */
    private String error;
    /**
     * 异常堆栈
     */
    private String stack;
    /**
     * 请求耗时
     */
    private Long takeTime;

    /**
     * 创建时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;

}
  • key枚举
/**
 * 功能描述:
 * 【项目key枚举】
 *
 * @author chihiro
 * @version V1.0
 * @date 2019/09/04 11:33
 */
public enum KeyConstants {

    // 用户编号记录
    USER_ID("userId"),
    // 用户名记录
    USER_NAME("username"),
    // 用户所属唯一标识
    UNIQUE_CODE("uniqueCode"),
    // 请求开始时间
    START_TIME("startTime"),
    // 日志对象
    SYS_LOG("sysLog");

    private String key;

    public String getKey() {
        return key;
    }

    KeyConstants(String key) {
        this.key = key;
    }
}
  • MQ处理器
/**
 * 功能描述:
 * 【日志记录处理器】
 *
 * @author chihiro
 * @version V1.0
 * @date 2019/09/04 11:07
 */
@Component
@Slf4j
public class LogRecordHandler {

    @Autowired
    private AmqpTemplate rabbitTemplate;

    /**
     * 将操作日记发往MQ处理
     *
     * @param sysLog 日志实体
     */
    public void recordLog(SysLog sysLog) {
        rabbitTemplate.convertAndSend(MqConstants.QUEUE_LOG_RECODE.getTopic(), JSON.toJSONString(sysLog));
    }

}
  • 本地线程工具类
/**
 * 本地线程工具类
 *
 * @author chihiro
 * @version V1.0
 * @date 2019/09/04 11:10
 */
public final class ThreadLocalUtil {

    private static final ThreadLocal<Map<String, String>> THREAD_CONTEXT = ThreadLocal.withInitial(HashMap::new);

    /**
     * 根据key获取值
     *
     * @param key 键值
     * @return value
     */
    public static String getValue(String key) {
        if (THREAD_CONTEXT.get() == null) {
            return null;
        }
        return THREAD_CONTEXT.get().get(key);
    }

    /**
     * 存储
     *
     * @param key   键值
     * @param value 值
     */
    public static String setValue(String key, String value) {
        Map<String, String> cacheMap = THREAD_CONTEXT.get();
        if (cacheMap == null) {
            cacheMap = new HashMap<>();
            THREAD_CONTEXT.set(cacheMap);
        }
        return cacheMap.put(key, value);
    }

    /**
     * 根据key移除值
     *
     * @param key 键值
     */
    public static void removeValue(String key) {
        Map<String, String> cacheMap = THREAD_CONTEXT.get();
        if (cacheMap != null) {
            cacheMap.remove(key);
        }
    }

    /**
     * 重置
     */
    public static void reset() {
        if (THREAD_CONTEXT.get() != null) {
            THREAD_CONTEXT.get().clear();
        }
    }

}
  • http工具类
/**
 * 功能描述:
 * 【Http工具类】
 *
 * @author chihiro
 * @version V1.0
 * @date 2019/09/04 11:58
 */
public class WebUtil {

    public static Map<String, String> queryStringToMap(String queryString, String charset) {
        try {
            Map<String, String> map = new HashMap<>();

            String[] decode = URLDecoder.decode(queryString, charset).split("&");
            for (String keyValue : decode) {
                String[] kv = keyValue.split("[=]", 2);
                map.put(kv[0], kv.length > 1 ? kv[1] : "");
            }
            return map;
        } catch (UnsupportedEncodingException e) {
            throw new UnsupportedOperationException(e);
        }
    }

    /**
     * 尝试获取当前请求的HttpServletRequest实例
     *
     * @return HttpServletRequest
     */
    public static HttpServletRequest getHttpServletRequest() {
        try {
            return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        } catch (Exception e) {
            return null;
        }
    }

    public static Map<String, String> getParameters(HttpServletRequest request) {
        Map<String, String> parameters = new HashMap<>();
        Enumeration enumeration = request.getParameterNames();
        while (enumeration.hasMoreElements()) {
            String name = String.valueOf(enumeration.nextElement());
            parameters.put(name, request.getParameter(name));
        }
        return parameters;
    }

    public static Map<String, String> getHeaders(HttpServletRequest request) {
        Map<String, String> map = new LinkedHashMap<>();
        Enumeration<String> enumeration = request.getHeaderNames();
        while (enumeration.hasMoreElements()) {
            String key = enumeration.nextElement();
            String value = request.getHeader(key);
            map.put(key, value);
        }
        return map;
    }

    private static final String[] IP_HEADERS = {
            "X-Forwarded-For",
            "X-Real-IP",
            "Proxy-Client-IP",
            "WL-Proxy-Client-IP"
    };

    /**
     * 获取请求客户端的真实ip地址
     *
     * @param request 请求对象
     * @return ip地址
     */
    public static String getIpAddress(HttpServletRequest request) {

        // 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址
        String ip = request.getHeader(IP_HEADERS[0]);

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_CLIENT_IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_X_FORWARDED_FOR");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
            }
        } else if (ip.length() > 15) {
            String[] ips = ip.split(",");
            for (int index = 0; index < ips.length; index++) {
                String strIp = (String) ips[index];
                if (!("unknown".equalsIgnoreCase(strIp))) {
                    ip = strIp;
                    break;
                }
            }
        }
        return ip;
    }

    /**
     * 获取请求客户端的真实ip地址
     *
     * @return ip地址
     */
    public static String getIpAddress() {
        // 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址
        return getIpAddress(getHttpServletRequest());
    }

    /**
     * web应用绝对路径
     *
     * @param request 请求对象
     * @return 绝对路径
     */
    public static String getBasePath(HttpServletRequest request) {
        String path = request.getContextPath();
        return request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
    }

}
  • 以上,代码展示完毕,日志的入库使用MQ进行异步。经测试,功能无误,尚未上线测试,未知性能问题。

后记

  • MQ的使用可参考这篇文章
  • 欢迎指正博客内不足之处