设计思想
在实际应用开发中, 时常会有需要记录操作日志的需求, 操作日志可以给开发者带来明确的操作数据信息, 进而更加了解项目分析问题, 操作日志设计五花八门, 故因此我设计了一个 通用的 \color{#FF0000}{通用的} 通用的, 灵活的 \color{#FF0000}{灵活的} 灵活的, 简洁的 \color{#FF0000}{简洁的} 简洁的, 插拔式 \color{#FF0000}{插拔式} 插拔式操作日志切面类
使用需求
- 数据库连接
- 需引入spring-boot-starter-jdbc依赖
- 需引入spring-boot-starter-aop依赖
AOP插拔式操作日志类
只需修改
切入点配置
\color{#FF0000}{切入点配置}
切入点配置和
异常匹配包路径
\color{#FF0000}{异常匹配包路径}
异常匹配包路径就可以正常使用
其他配置可根据项目需求灵活变动或修改
/**
* @ClassName LogAspect
* @Description 操作日志组件类
* @Author DLTS
*/
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCallback;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Objects;
@Aspect
@Component
public class LogAspect {
/**
* 数据库操作对象
*/
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 切入点配置
*/
@Pointcut("execution(public * com.dlts.admin.controller.*.*(..))")
public void logPointCut() {
// 切入点根据包结构进行配置
}
/**
* 异常匹配包路径
* 用于筛选异常信息, 过滤无用异常信息
*/
private static final String packagePath = "com.dlts.admin";
/**
* 表前缀
*/
private static final String tablePrefix = "log_";
/**
* 表后缀时间格式(当前按月分表)
*/
private static final DateTimeFormatter tableSuffixFormatter = DateTimeFormatter.ofPattern("yyyyMM");
/**
* 接口排除集
* 支持?匹配符和*通配符
*/
private static final HashSet<String> excludeSet = new HashSet<String>() {{
}};
/**
* 获取操作人
*
* @return 可返回ID, 姓名或其他相关属性
*/
public String getOperator(HttpServletRequest request) {
return null;
}
/**
* 建表语句
* %s 表示当前表名称
*/
private static final String createTableSql = "create table %s\n" +
"(\n" +
" id bigint unsigned primary key auto_increment not null comment '主键id',\n" +
" ip varchar(128) not null comment 'IP地址',\n" +
" operator varchar(256) null comment '操作人',\n" +
" uri varchar(256) not null comment '请求URI',\n" +
" class_info varchar(256) not null comment '类信息',\n" +
" method_info varchar(256) not null comment '方法信息',\n" +
" success tinyint(1) not null comment '是否成功',\n" +
" request_body text null comment '请求体',\n" +
" response_body text null comment '响应体',\n" +
" error_message text null comment '错误信息',\n" +
" create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',\n" +
" update_time timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '修改时间'\n" +
")";
/**
* 判断表是否存在语句
*/
private static final String checkTableSql = "select count(1) from information_schema.tables where table_name = ? limit 1";
/**
* 插入语句
*/
private static final String insertSql = "insert into %s (ip, operator, uri, class_info, method_info, success, request_body, response_body, error_message) values (?, ?, ?, ?, ?, ?, ?, ?, ?)";
public boolean insert(String tableName, Log log) {
return jdbcTemplate.execute(connection -> {
PreparedStatement preparedStatement = connection.prepareStatement(String.format(insertSql, tableName));
preparedStatement.setString(1, log.getIp());
preparedStatement.setString(2, log.getOperator());
preparedStatement.setString(3, log.getUri());
preparedStatement.setString(4, log.getClassInfo());
preparedStatement.setString(5, log.getMethodInfo());
preparedStatement.setBoolean(6, log.getSuccess());
preparedStatement.setString(7, log.getRequestBody());
preparedStatement.setString(8, log.getResponseBody());
preparedStatement.setString(9, log.getErrorMessage());
return preparedStatement;
}, (PreparedStatementCallback<Boolean>) preparedStatement -> preparedStatement.execute());
}
public class Log {
/**
* 主键
*/
private Long id;
/**
* 请求来源IP地址
*/
private String ip;
/**
* 操作人
*/
private String operator;
/**
* 请求地址
*/
private String uri;
/**
* 类信息
*/
private String classInfo;
/**
* 方法信息
*/
private String methodInfo;
/**
* 是否成功
*/
private Boolean success;
/**
* 请求体
*/
private String requestBody;
/**
* 响应体
*/
private String responseBody;
/**
* 错误信息
*/
private String errorMessage;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 修改时间
*/
private LocalDateTime updateTime;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getOperator() {
return operator;
}
public void setOperator(String operator) {
this.operator = operator;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
public String getClassInfo() {
return classInfo;
}
public void setClassInfo(String classInfo) {
this.classInfo = classInfo;
}
public String getMethodInfo() {
return methodInfo;
}
public void setMethodInfo(String methodInfo) {
this.methodInfo = methodInfo;
}
public Boolean getSuccess() {
return success;
}
public void setSuccess(Boolean success) {
this.success = success;
}
public String getRequestBody() {
return requestBody;
}
public void setRequestBody(String requestBody) {
this.requestBody = requestBody;
}
public String getResponseBody() {
return responseBody;
}
public void setResponseBody(String responseBody) {
this.responseBody = responseBody;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public LocalDateTime getCreateTime() {
return createTime;
}
public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
}
public LocalDateTime getUpdateTime() {
return updateTime;
}
public void setUpdateTime(LocalDateTime updateTime) {
this.updateTime = updateTime;
}
}
/******************************** 以上是配置 以下是逻辑 ********************************/
/**
* 日志对象
*/
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
/**
* jackson序列化对象
*/
private static final ObjectMapper objectMapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
/**
* 核心环绕切面方法
*
* @param point 连接点对象
* @return 响应对象
* @throws Throwable
* @throws InterruptedException
*/
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable, InterruptedException {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
log.info("------------------------------------------------------------------------------");
log.info("接口路径: {}", request.getRequestURI());
log.info("请求方式: {}", request.getMethod());
log.info("临时令牌: {}", request.getHeader("token"));
log.info("请求时间: {}", LocalDateTime.now());
log.info("------------------------------------------------------------------------------");
// 校验排除
if (isExclude(excludeSet, request.getRequestURI())) return point.proceed();
// 封装log对象
Log log = new Log();
// 设置请求来源IP
log.setIp(getIp(request));
// 设置操作人(自行补充逻辑)
log.setOperator(getOperator(request));
// 设置请求URI
log.setUri(request.getRequestURI());
// 获取目标类对象
Class<?> targetClass = point.getTarget().getClass();
// 设置类信息
log.setClassInfo(getAnnotationValue(targetClass.getAnnotation((Class<Annotation>) Class.forName("io.swagger.annotations.Api")), "tags", targetClass.getSimpleName()));
// 获取目标方法对象
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = targetClass.getDeclaredMethod(signature.getName(), signature.getParameterTypes());
method.setAccessible(true);
// 设置方法信息
log.setMethodInfo(getAnnotationValue(method.getAnnotation((Class<Annotation>) Class.forName("io.swagger.annotations.ApiOperation")), "value", method.getName()));
// 设置请求参数
log.setRequestBody(getJsonStr(point.getArgs()));
try {
Object result = point.proceed();
// 设置是否成功
log.setSuccess(true);
// 设置响应体
log.setResponseBody(getJsonStr(result));
return result;
} catch (Throwable throwable) {
StackTraceElement[] stackTraceElements = throwable.getStackTrace();
StackTraceElement traceElement = Arrays.stream(stackTraceElements)
.filter(element -> element.getClassName().startsWith(packagePath))
.findFirst().orElse(stackTraceElements[0]);
String errorMessage = String.format("%s\n%s", traceElement.toString(), throwable.toString());
System.err.println(errorMessage);
// 设置是否成功
log.setSuccess(false);
// 设置错误信息
log.setErrorMessage(errorMessage);
throw throwable;
} finally {
// 校验表
String tableName = tablePrefix.concat(tableSuffixFormatter.format(LocalDate.now()));
createTableIfNotExist(tableName);
insert(tableName, log);
}
}
/**
* 校验排除
*
* @param excludeSet
* @param str
* @return
*/
public boolean isExclude(HashSet<String> excludeSet, String str) {
if (Objects.isNull(excludeSet) || excludeSet.isEmpty()) return false;
return excludeSet.stream().anyMatch(p -> isMatch(str, p));
}
/**
* 获取注解的方法值
*
* @param annotation 注解对象
* @param method 注解方法
* @param defaultValue 默认值
* @return 注解方法返回值
*/
public static String getAnnotationValue(Annotation annotation, String method, String defaultValue) {
try {
if (Objects.isNull(annotation)) return defaultValue;
Method api = annotation.getClass().getDeclaredMethod(Objects.isNull(method) || method.trim().isEmpty() ? "value" : method);
if (Objects.isNull(api)) return defaultValue;
Object obj = api.invoke(annotation);
if (obj.getClass().isArray()) return objectMapper.writeValueAsString(obj);
return obj.toString();
} catch (Exception e) {
e.printStackTrace();
}
return defaultValue;
}
/**
* Object对象转Json字符串
*
* @param objs
* @return
*/
public static String getJsonStr(Object... objs) {
try {
if (Objects.isNull(objs) || objs.length == 0) return null;
else if (objs.length == 1) return objectMapper.writeValueAsString(objs[0]);
else return objectMapper.writeValueAsString(objs);
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
/**
* existTableName 作为临时存储, 减少数据库访问次数
*/
private String existTableName;
/**
* 查询数据库表存在, 如果不存在则创建表
*
* @param tableName 动态生成的表名
*/
public void createTableIfNotExist(String tableName) {
// 判断表是否存在
if (Objects.equals(tableName, existTableName)) return;
else if (checkTable(tableName)) existTableName = tableName;
else if (createTable(tableName)) existTableName = tableName;
else throw new RuntimeException("日志表 '" + tableName + "' 创建失败");
}
/**
* 判断数据库表是否存在
*
* @param tableName 表名称
* @return true表示存在, false表示不存在
*/
public boolean checkTable(String tableName) {
return jdbcTemplate.execute(connection -> {
PreparedStatement preparedStatement = connection.prepareStatement(checkTableSql);
preparedStatement.setString(1, tableName);
return preparedStatement;
}, (PreparedStatementCallback<Integer>) preparedStatement -> {
preparedStatement.execute();
ResultSet rs = preparedStatement.getResultSet();
rs.next();
return rs.getInt(1);
}) > 0;
}
/**
* 创建表
*
* @param tableName
* @return
*/
public boolean createTable(String tableName) {
return !jdbcTemplate.execute(connection -> connection.prepareStatement(String.format(createTableSql, tableName))
, (PreparedStatementCallback<Boolean>) preparedStatement -> preparedStatement.execute());
}
/**
* ?单配符和*通配符的校验匹配
*
* @param s 待匹配的字符串
* @param p 匹配规则
* @return 匹配结果
*/
public static Boolean isMatch(String s, String p) {
// s的索引位置
int i = 0;
// p的索引位置
int j = 0;
// 通配符时回溯的位置
int ii = -1;
int jj = -1;
while (i < s.length()) {
if (j < p.length() && p.charAt(j) == '*') {
// 遇到通配符了,记录下位置,规则字符串+1,定位到非通配字符串
ii = i;
jj = j;
j++;
} else if (j < p.length() && (s.charAt(i) == p.charAt(j) || p.charAt(j) == '?')) {
// 匹配到了
i++;
j++;
} else {
// 匹配失败,需要判断s 是否被p的*号匹配着.如果等于-1, 前面没有通配符
if (jj == -1) return false;
// 回到之前记录通配符的位置
j = jj;
// 带匹配字符串也回到记录的位置,并后移一位
i = ii + 1;
}
}
// 当s的每一个字段都匹配成功以后,判断p剩下的串,是*则放行
while (j < p.length() && p.charAt(j) == '*') j++;
// 检测到最后就匹配成功
return j == p.length();
}
/**
* 获取访问真实IP
*
* @return
*/
public static String getIp(HttpServletRequest request) {
if (Objects.isNull(request)) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
request = Objects.requireNonNull(attributes).getRequest();
}
String ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("X-Real-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
}
//对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) { //"***.***.***.***".length() = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
return ipAddress;
}
}
小结
如有不完善的地方或错误, 请评论点出或联系我及时纠正, 谢谢。