使用 SpringAop 记录日志
日志打印
- 日志作为项目运行中出错的第一手资料,打印的详细与否直接决定了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的使用可参考这篇文章
- 欢迎指正博客内不足之处