springboot 项目实现系统日志的最佳实践
效果:
源码:
1.表DDL语句
CREATE TABLE `sys_sys_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
`user_role` varchar(100) DEFAULT NULL COMMENT '用户角色',
`user_name` varchar(100) DEFAULT NULL COMMENT '用户名',
`uri` varchar(100) DEFAULT NULL,
`operation` varchar(100) DEFAULT NULL,
`exception_code` int(10) DEFAULT NULL,
`exception_detail` text,
`run_time` bigint(15) DEFAULT NULL,
`method` varchar(100) DEFAULT NULL,
`params` text,
`ip` varchar(20) DEFAULT NULL,
`status` tinyint(1) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=519 DEFAULT CHARSET=utf8mb4;
2.entity
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import tk.mybatis.mapper.annotation.KeySql;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
/**
* 系统日志
* @author cc
* @date 2020/06/05
*/
@Data
@Table(name = "sys_sys_log")
@ApiModel(value = "系统日志")
public class SysLog {
/**
* ID
*/
@Id
@KeySql(useGeneratedKeys = true)
@ApiModelProperty(value = "ID")
private Long id;
/**
* 用户ID
*/
@ApiModelProperty(value = "用户ID")
private Long userId;
/**
* 用户角色
*/
@ApiModelProperty(value = "用户角色")
private String userRole;
/**
* 用户名
*/
@ApiModelProperty(value = "用户名")
private String userName;
/**
* 请求URI
*/
@ApiModelProperty(value = "请求URI")
private String uri;
/**
* 动作
*/
@ApiModelProperty(value = "动作")
private String operation;
/**
* 异常码
*/
@ApiModelProperty(value = "异常码")
private Integer exceptionCode;
/**
* 异常详情
*/
@ApiModelProperty(value = "异常详情")
private String exceptionDetail;
/**
* 运行时长(ms)
*/
@ApiModelProperty(value = "运行时长(ms)")
private Long runTime;
/**
* 方法
*/
@ApiModelProperty(value = "方法")
private String method;
/**
* 请求参数
*/
@ApiModelProperty(value = "请求参数")
private String params;
/**
* 请求ip
*/
@ApiModelProperty(value = "请求ip")
private String ip;
/**
* 状态
* 0失败 1成功
*/
@ApiModelProperty(value = "状态(0失败1成功)")
private Integer status;
/**
* 创建时间
*/
@ApiModelProperty(value = "创建时间")
private Date createTime;
}
3.注解定义
/**
* MyLog
* 自定义aop环绕系统日志
* @author cc
* @date 2020/06/05
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLog {
/**
* 用户操作哪个模块
*/
String title() default "";
/**
* 记录用户操作的动作
*/
String action() default "";
}
4.切面实现
import com.alibaba.fastjson.JSONObject;
import com.cc672cc.aop.annotation.MyLog;
import com.cc672cc.common.model.Payload;
import com.cc672cc.common.model.UserInfo;
import com.cc672cc.common.utils.*;
import com.cc672cc.entity.sys.SysLog;
import com.cc672cc.exceptions.BusinessException;
import com.cc672cc.mapper.SysLogMapper;
import com.cc672cc.properties.JwtProperties;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.*;
/**
* SysLogAspect
* SysLog切面实现
*
* @author cc
* @date 2020/06/05
*/
@Aspect
@Component
@Slf4j
@SuppressWarnings("all")
public class SysLogAspect {
@Autowired
private JwtProperties jwtProperties;
@Autowired
private SysLogMapper sysLogMapper;
private HttpServletRequest request;
/**
* 配置织入点(以@MyLog注解为标志)
* 只要出现 @MyLog注解都会进入
*/
@Pointcut("@annotation(com.cc672cc.aop.annotation.MyLog)")
public void logPointCut() {
}
/**
* 环绕增强
*
* @param point
* @return
* @throws Throwable
*/
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
Object result = null;
Exception exception = null;
long beginTime = 0L;
long runTime = 0L;
try {
beginTime = System.currentTimeMillis();
result = point.proceed();
runTime = System.currentTimeMillis() - beginTime;
saveSuccessSysLog(point, runTime);
} catch (Exception e) {
saveErrorSysLog(point, e);
exception = e;
log.error("MyLog Exception-->{}", e);
} finally {
// 这里要抛出异常给全局异常处理器处理
if (exception != null) {
throw exception;
}
}
return result;
}
/**
* 失败日志保存
*
* @param joinPoint
* @param e
*/
private void saveErrorSysLog(ProceedingJoinPoint joinPoint, Exception e) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SysLog sysLog = new SysLog();
MyLog myLog = method.getAnnotation(MyLog.class);
if (myLog != null) {
// 注解上的描述
sysLog.setOperation(myLog.title() + "-" + myLog.action());
}
// 请求的方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
sysLog.setMethod(className + "." + methodName + "()");
// 请求的参数
LocalVariableTableParameterNameDiscoverer paramNames = new LocalVariableTableParameterNameDiscoverer();
String[] parameterNames = paramNames.getParameterNames(method);
//获取连接点方法运行时的入参列表
Object[] args = joinPoint.getArgs();
// 将参数名称与入参值一一对应起来
Map<String, Object> params = new HashMap<>();
for (int i = 0; i < parameterNames.length; i++) {
// 如果参数类型是请求和响应的http,则不需要拼接【这两个参数,使用JSON.toJSONString()转换会抛异常】
if (args[i] instanceof HttpServletRequest || args[i] instanceof HttpServletResponse)
{
continue;
}
params.put(parameterNames[i], args[i]);
}
try {
sysLog.setParams(JSONObject.toJSONString(params));
} catch (Exception ex) {
log.error("MyLog methodParams JSON Exception --> {}",ex);
sysLog.setParams("MyLog methodParams JSON Exception");
}
// 获取request
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
sysLog.setUri(request.getRequestURI());
// 设置IP地址
sysLog.setIp(IPUtils.getIpAddr(request));
// 用户名
String token = request.getHeader("CC_TOKEN");
Payload<UserInfo> payload = JwtUtils.getInfoFromToken(token, jwtProperties.getPublicKey(), UserInfo.class);
UserInfo userInfo = payload.getUserInfo();
sysLog.setUserName(userInfo.getUsername());
sysLog.setUserRole(userInfo.getRole());
sysLog.setUserId(userInfo.getId());
sysLog.setRunTime(-1L);
sysLog.setCreateTime(new Date());
sysLog.setStatus(0);
// 这里做异常处理
if (e instanceof BusinessException) {
BusinessException be = (BusinessException) e;
sysLog.setExceptionCode(be.getCode());
sysLog.setExceptionDetail(be.getMessage());
} else {
sysLog.setExceptionCode(5000000);
sysLog.setExceptionDetail(e.getMessage());
}
sysLog.setId(null);
sysLogMapper.insertSelective(sysLog);
LogUtil.logPrint(sysLog);
}
/**
* 成功日志保存
*
* @param joinPoint
* @param time
*/
private void saveSuccessSysLog(ProceedingJoinPoint joinPoint, long time) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SysLog sysLog = new SysLog();
MyLog myLog = method.getAnnotation(MyLog.class);
if (myLog != null) {
// 注解上的描述
sysLog.setOperation(myLog.title() + "-" + myLog.action());
}
// 请求的方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
sysLog.setMethod(className + "." + methodName + "()");
// 请求的参数
LocalVariableTableParameterNameDiscoverer paramNames = new LocalVariableTableParameterNameDiscoverer();
String[] parameterNames = paramNames.getParameterNames(method);
//获取连接点方法运行时的入参列表
Object[] args = joinPoint.getArgs();
// 将参数名称与入参值一一对应起来
Map<String, Object> params = new HashMap<>();
for (int i = 0; i < parameterNames.length; i++) {
// 如果参数类型是请求和响应的http,则不需要拼接【这两个参数,使用JSON.toJSONString()转换会抛异常】
if (args[i] instanceof HttpServletRequest || args[i] instanceof HttpServletResponse)
{
continue;
}
params.put(parameterNames[i], args[i]);
}
try {
sysLog.setParams(JSONObject.toJSONString(params));
} catch (Exception ex) {
log.error("MyLog methodParams JSON Exception --> {}",ex);
sysLog.setParams("MyLog methodParams JSON Exception");
}
// 获取request
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
sysLog.setUri(request.getRequestURI());
// 设置IP地址
sysLog.setIp(IPUtils.getIpAddr(request));
// 用户名
String token = request.getHeader("CC_TOKEN");
Payload<UserInfo> payload = JwtUtils.getInfoFromToken(token, jwtProperties.getPublicKey(), UserInfo.class);
UserInfo userInfo = payload.getUserInfo();
sysLog.setUserName(userInfo.getUsername());
sysLog.setUserRole(userInfo.getRole());
sysLog.setUserId(userInfo.getId());
sysLog.setRunTime(time);
sysLog.setCreateTime(new Date());
sysLog.setStatus(1);
sysLog.setId(null);
sysLogMapper.insertSelective(sysLog);
LogUtil.logPrint(sysLog);
}
}
6.工具类
6.1 IPUtil
import com.alibaba.druid.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
/**
* IP地址
*
*/
@Slf4j
public class IPUtils {
/**
* 获取IP地址
*
* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
*/
public static String getIpAddr(HttpServletRequest request) {
String ip = null;
try {
ip = request.getHeader("x-forwarded-for");
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
} catch (Exception e) {
log.error("IPUtils ERROR ", e);
}
// 使用代理,则获取第一个IP地址
if (StringUtils.isEmpty(ip) && ip.length() > 15) {
if (ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
}
return ip;
}
}
6.2 LogUtil
import com.cc672cc.entity.sys.SysLog;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServletRequest;
/**
* LogUtil
* 日志工具类,用于日志统一打印,日志打印要自行异常捕获,防止中止
* @author cc
* @date 2020/06/07
*/
@Slf4j
public class LogUtil {
/**
* 系统日志打印
*
* @param sysLog
*/
public static void logPrint(SysLog sysLog) {
log.info("------sysLog-print-start------");
try {
log.info("**** sysLog id-> {} ****",sysLog.getId());
log.info("**** sysLog user-> {} ****",sysLog.getUserName()+"("+sysLog.getUserId()+")");
log.info("**** sysLog role-> {} ****",sysLog.getUserRole());
log.info("**** sysLog createTime-> {} ****", DateUtil.format2TimeStr(sysLog.getCreateTime()));
log.info("**** sysLog uri-> {} ****",sysLog.getUri());
log.info("**** sysLog rumTime-> {} ms ****",sysLog.getRunTime());
String status="";
log.info("**** sysLog status-> {} ****",status=sysLog.getStatus()==1?"成功":"异常");
if(sysLog.getStatus()==0){
log.info("**** sysLog exceptionCode-> {} ****",sysLog.getExceptionCode());
log.info("**** sysLog exceptionMsg-> {} ****",sysLog.getExceptionDetail());
}
} catch (Exception e) {
log.error("sysLog print Exception -->{}",e);
}finally {
log.info("------sysLog-print-end------");
}
}
/**
*
* @param request
*/
public static void logPrint(HttpServletRequest request) {
log.info("------request-intercept-start------");
try {
log.info("###### 请求IP: {} ######",IPUtils.getIpAddr(request));
log.info("###### 请求方式: {} ######",request.getMethod());
log.info("###### 请求uri: {} ######",request.getRequestURI());
} catch (Exception e) {
log.error("request intercept print Exception -->{}",e);
}finally {
log.info("------request-intercept-end------");
}
}
}
7.controller 实现
@GetMapping("/user/{id}")
@ApiOperation(value = "根据id获取user")
@RequiresPermissions("user:view")
@MyLog(title = "用户管理", action = "获取用户")
public UserRespVO queryNameById(@ApiParam(value = "用户ID", required = true) @PathVariable("id") int id) {
return BeanHelper.copyProperties(userService.queryById((long) id), UserRespVO.class);
}
8. 日志打印展示
9.说明
记录一些关键性的字段,运行时长,这样我们可以在所有标的字段删去更新人字段,也可以做到一个有效的查看接口响应时长来对接口改造优化处理做了一个参考,在实现切面时正规应该将里面的方法同try-catch 块包起来,同步方法不要影响接口响应,那么在此展示中我们对实现异常或接口抛出异常做了封装处理,注意代码中注释的地方,尤其是传参处理的那些坑还是挺多的,和fastjson工具类本身可能抛出的异常,要对这块进行捕获异常处理。