近期需求中需要做一個(gè)系統(tǒng)日志,需求為記錄到每次操作類型,操作的模塊,IP,操作者昵稱,操作者用戶名,日志描述,操作時(shí)間等信息
首先,設(shè)計(jì)一下數(shù)據(jù)表

image.png
為了獲取用戶信息,寫(xiě)了一個(gè)攔截器,攔截jwt_token獲取用戶信息
向線程變量中封裝了用戶名、昵稱、模塊名(從配置文件注入)、IP地址四個(gè)字段
import cn.hutool.core.util.StrUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.MacSigner;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
/**
* 用戶信息攔截器
* 從Header中取出jwttoken,并獲取其中的用戶名設(shè)置到用戶信息上下文線程變量中
*
* @author wangqichang
* @since 2019/4/24
*/
@Component
public class CustomInfoInterceptor implements HandlerInterceptor {
private ObjectMapper objectMapper = new ObjectMapper();
@Value("${spring.application.chineseName}")
private String moduleName;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String authorization = request.getHeader("Authorization");
authorization = StrUtil.removePrefix(authorization, "Bearer ");
ContextInfo contextInfo = new ContextInfo();
if (StrUtil.isNotBlank(authorization)) {
//驗(yàn)簽
Jwt decode = JwtHelper.decodeAndVerify(authorization, new MacSigner("secretKey"));
String claims = decode.getClaims();
HashMap<String, Object> hashMap = objectMapper.readValue(claims, HashMap.class);
Object userName = hashMap.get("user_name");
Object nickName = hashMap.get("nick_name");
contextInfo.setNickName((String) nickName);
contextInfo.setUserName((String) userName);
} /*else {
throw new ServiceException("當(dāng)前用戶未登錄");
}*/
contextInfo.setModuleName(moduleName);
contextInfo.setIp(getIp());
CustomContext.setContextInfo(contextInfo);
return true;
}
/**
* @return 返回當(dāng)前請(qǐng)求IP
*/
public static String getIp() {
if (null != RequestContextHolder.currentRequestAttributes()) {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
if (null != attr) {
HttpServletRequest request = attr.getRequest();
String header = request.getHeader("x-forwarded-for");
String ip = null;
if (StrUtil.isNotBlank(header) && !"unknown".equalsIgnoreCase(header)) {
ip = header.split(",")[0];
}
if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip.equals("0:0:0:0:0:0:0:1") ? "127.0.0.1" : ip;
}
}
return null;
}
}
mybatis攔截器如下
封裝日志對(duì)象,主要為獲取操作類型(增刪改查)、操作表實(shí)體
package com.zh.linemanager.interceptor;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import com.zh.backend.common.context.CustomContext;
import com.zh.backend.common.dto.ContextInfo;
import com.zh.backend.common.dto.LogDto;
import com.zh.backend.common.interceptor.CustomInfoInterceptor;
import com.zh.backend.common.service.LogService;
import com.zh.backend.common.util.SqlTableUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
/**
* 日志mybatis 攔截器 功能為攔截?cái)?shù)據(jù)庫(kù)操作,獲取操作類型及操作表格,并從CustomContext獲取線程變量中的數(shù)據(jù)封裝日志
* 比較粗糙,下次優(yōu)化
* Signature 對(duì)Executor 的 update 及query 方法進(jìn)行攔截,后面為參數(shù)
*
* @author wangqichang
* @see CustomInfoInterceptor
* @since 2019/5/20
*/
@Slf4j
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class LogInterceptor implements Interceptor {
private static String LOG_TYPE = "操作日志";
/**
* 這個(gè)是日志服務(wù),使用feign 調(diào)用
*/
@Autowired
private LogService logService;
@Override
public Object intercept(Invocation invocation) throws Throwable {
//僅做日志,不能影響正常業(yè)務(wù),故try住
try {
//獲取第一個(gè)參數(shù),就是Signature中args的下標(biāo)
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
LogDto logDto = new LogDto();
//從自定義線程中copy出日志所需的信息,我放了用戶名,ip,模塊名這些
ContextInfo info = CustomContext.current();
BeanUtil.copyProperties(info, logDto);
Date date = new Date();
//獲取第二個(gè)參數(shù)Object,這個(gè)對(duì)象可能是實(shí)體也可能是map,比如自己用@Param注入?yún)?shù),那就是map對(duì)象
Object object = invocation.getArgs()[1];
if (!(object instanceof Map)) {
//直接記錄操作的實(shí)體
logDto.setOperationEntity(object.getClass().getName());
} else {
//如果是注入的map參數(shù),則使用jsqlphaser提取表名
String sql = mappedStatement.getBoundSql(object).getSql();
List<String> tableNames = SqlTableUtil.getTableNames(sql);
String tables = CollUtil.join(tableNames, " ");
logDto.setOperationEntity(tables);
}
String dateTime = DateUtil.formatDateTime(date);
String operationType = null;
switch (mappedStatement.getSqlCommandType()) {
case INSERT:
operationType = "新增";
break;
case UPDATE:
operationType = "更新";
break;
case DELETE:
operationType = "刪除";
break;
case SELECT:
operationType = "查詢";
break;
default:
operationType = "未知";
break;
}
StringBuilder logDesc = new StringBuilder().append("用戶 ").append(info.getNickName()).append(" 于 ").append(dateTime).append(" 對(duì) ").append(logDto.getModuleName()).append(" 模塊進(jìn)行了 ").append(operationType).append(" 操作");
logDto.setOperationType(operationType);
logDto.setCreateTime(date);
logDto.setLogType(LOG_TYPE);
logDto.setLogDesc(logDesc.toString());
logService.recordLog(logDto);
} catch (Exception e) {
log.error("日志記錄出錯(cuò),錯(cuò)誤信息:{}", e.getMessage());
e.printStackTrace();
}
//放行攔截
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
提取sql表名用到的工具類
import net.sf.jsqlparser.parser.CCJSqlParserManager;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.util.TablesNamesFinder;
/**
* @author wangqichang
* @since 2019/5/21
*/
public class SqlTableUtil {
private static CCJSqlParserManager sqlParserManager = new CCJSqlParserManager();
private static TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
/**
* detect table names from given table
* ATTENTION : WE WILL SKIP SCALAR SUBQUERY IN PROJECTION CLAUSE
*/
public static List<String> getTableNames(String sql) throws Exception {
Statement statement = sqlParserManager.parse(new StringReader(sql));
return tablesNamesFinder.getTableList(statement);
}
}
成功記錄如下

image.png
代碼不易,勿忘點(diǎn)贊