步骤写的很详细,可以直接复制拿来用的,其中用到了过滤器、自定义注解以及AOP切面,来完成日志记录统计,感兴趣的收藏起来,以后遇到了可以直接用。
可能步骤会比较多,但是整体跟着思路下来,应该没什么大问题的。
项目用到了过滤器,可能有的人会不理解,之所以用过滤器是因为想要在日志记录post请求的json数据。
请求的时候,是通过request的body来传输的。在AOP后置方法中获取request里面的body,是取不到,直接为空。
原因很简单:因为是流。想想看,java中的流也是只能读一次,因为我是在AOP后置方法获取的,控制器实际上已经读过了一次,后置方法再读自然为空了。所以用过滤器来进行解决了这个问题。
这里我用的是mysql,假如您用的别的数据库,可以自行根据数据库类型进行修改。
CREATE TABLE `log` (
`id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键',
`create_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '最近更新时间',
`update_time` datetime NULL DEFAULT NULL COMMENT '最近更新人',
`update_count` int(11) NULL DEFAULT NULL COMMENT '更新次数',
`delete_flag` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '删除标志',
`delete_time` datetime NULL DEFAULT NULL COMMENT '删除日期',
`delete_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '删除人',
`cost_time` int(11) NULL DEFAULT NULL COMMENT '花费时间',
`ip` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'ip',
`description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '日志描述',
`request_param` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '请求参数',
`request_json` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '请求json数据',
`request_type` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求类型',
`request_url` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求路径',
`username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求用户',
`operation_type` int(3) NULL DEFAULT NULL COMMENT '操作类型',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
我的项目运用到了mybatisplus、swagger、lombok,你们可以根据自己项目框架写对应的实体类。BaseModel 是我们封装了一个基础实体类,专门存放关于操作人的信息,然后实体类直接继承。
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModelProperty;
import cn.org.xaas.mybatis.model.BaseModel;
import lombok.Data;
import lombok.ToString;
@TableName(value = "log")
@Data
@ToString(callSuper = true)
public class Log extends BaseModel {
@ApiModelProperty(value = "花费时间")
@TableField(value = "cost_time")
private Integer costTime;
@ApiModelProperty(value = "ip")
@TableField(value = "ip")
private String ip;
@ApiModelProperty(value = "日志描述")
@TableField(value = "description")
private String description;
@ApiModelProperty(value = "请求参数")
@TableField(value = "request_param")
private String requestParam;
@ApiModelProperty(value = "请求json数据")
@TableField(value = "request_json")
private String requestJson;
@ApiModelProperty(value = "请求类型")
@TableField(value = "request_type")
private String requestType;
@ApiModelProperty(value = "请求路径")
@TableField(value = "request_url")
private String requestUrl;
@ApiModelProperty(value = "请求用户")
@TableField(value = "username")
private String username;
@ApiModelProperty(value = "操作类型")
@TableField(value = "operation_type")
private Integer operationType;
}
用来记录日志操作类型
public enum OperationType {
/**
* 操作类型
*/
UNKNOWN("unknown"),
DELETE("delete"),
SELECT("select"),
UPDATE("update"),
INSERT("insert");
OperationType(String s) {
this.value = s;
}
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
import java.lang.annotation.*;
@Target({ElementType.PARAMETER, ElementType.METHOD})//作用于参数或方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemLog {
/**
* 日志名称
*
* @return
*/
String description() default "";
/**
* 操作类型
*
* @return
*/
OperationType type() default OperationType.UNKNOWN;
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
@Slf4j
@Component
public class IpInfoUtil {
/**
* 获取客户端IP地址
*
* @param request 请求
* @return
*/
public String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
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.getRemoteAddr();
if ("127.0.0.1".equals(ip)) {
//根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ip = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ip != null && ip.length() > 15) {
if (ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
}
if ("0:0:0:0:0:0:0:1".equals(ip)) {
ip = "127.0.0.1";
}
return ip;
}
}
利用线程异步记录日志。所以直接用了一个util维护线程池。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolUtil {
/**
* 线程缓冲队列
*/
private static BlockingQueue<Runnable> bqueue = new ArrayBlockingQueue<Runnable>(100);
/**
* 核心线程数,会一直存活,即使没有任务,线程池也会维护线程的最少数量
*/
private static final int SIZE_CORE_POOL = 5;
/**
* 线程池维护线程的最大数量
*/
private static final int SIZE_MAX_POOL = 10;
/**
* 线程池维护线程所允许的空闲时间
*/
private static final long ALIVE_TIME = 2000;
private static ThreadPoolExecutor pool = new ThreadPoolExecutor(SIZE_CORE_POOL, SIZE_MAX_POOL, ALIVE_TIME, TimeUnit.MILLISECONDS, bqueue, new ThreadPoolExecutor.CallerRunsPolicy());
static {
pool.prestartAllCoreThreads();
}
public static ThreadPoolExecutor getPool() {
return pool;
}
public static void main(String[] args) {
System.out.println(pool.getPoolSize());
}
}
这个就是重写的一个HttpServletRequest类。
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
public class BodyReaderRequestWrapper extends HttpServletRequestWrapper {
private final String body;
/**
* @param request
*/
public BodyReaderRequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder sb = new StringBuilder();
InputStream ins = null;
BufferedReader isr = null;
try {
ins = request.getInputStream();
if (ins != null) {
isr = new BufferedReader(new InputStreamReader(ins));
char[] charBuffer = new char[128];
int readCount = 0;
while ((readCount = isr.read(charBuffer)) != -1) {
sb.append(charBuffer, 0, readCount);
}
} else {
sb.append("");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (isr != null) {
isr.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (ins != null) {
ins.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
body = sb.toString();
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayIns = new ByteArrayInputStream(body.getBytes());
ServletInputStream servletIns = new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return byteArrayIns.read();
}
};
return servletIns;
}
}
这个过滤器我添加了一个路径,就是代表需要json日志的接口,可以在list当中添加路径,不需要取request当中json数据的可以不配置。
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class BodyReaderRequestFilter implements Filter {
private static final Pattern SHOULD_NOT_FILTER_URL_PATTERN;
static {
List<String> urlList = new ArrayList<>();
// 想要通过aop记录request当中body数据的,就需要进行配置路径
urlList.add("(socket/.*)");
urlList.add("(test/test1)");
urlList.add("(test/test2)");
StringBuilder sb = new StringBuilder();
for (String url : urlList) {
sb.append(url);
sb.append("|");
}
sb.setLength(sb.length() - 1);
SHOULD_NOT_FILTER_URL_PATTERN = Pattern.compile(sb.toString());
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 获取访问的url
String servletPath = request.getServletPath();
if (SHOULD_NOT_FILTER_URL_PATTERN.matcher(servletPath).find()) {
BodyReaderRequestWrapper requestWrapper = new BodyReaderRequestWrapper(request);
if (requestWrapper == null) {
filterChain.doFilter(request, response);
} else {
filterChain.doFilter(requestWrapper, response);
}
}else {
filterChain.doFilter(request, response);
}
}
@Override
public void destroy() {
}
}
想要让过滤器生效需要注入到容器当中。
import cn.org.bjca.szyx.xaas.equipment.filter.BodyReaderRequestFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyServerConfig {
@Bean
public FilterRegistrationBean myFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new BodyReaderRequestFilter());
return registrationBean;
}
}
对于切面,我们可以通过指定包名,进行日志统计,也可以选择根据自定义的注解在方法上添加,然后进行统计,根据自己的实际情况,在切点进行配置即可。
LogDao我是没有提供的,每个项目框架不一样,自行根据情况进行编写,就是保存数据库就可以了。
import cn.hutool.core.util.IdUtil;
import cn.hutool.json.JSONUtil;
import cn.org.xaas.core.util.HeaderSecurityUtils;
import cn.org.xaas.equipment.annotation.SystemLog;
import cn.org.xaas.equipment.dao.LogDao;
import cn.org.xaas.equipment.model.base.Log;
import cn.org.xaas.equipment.utils.IpInfoUtil;
import cn.org.xaas.equipment.utils.ThreadPoolUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.NamedThreadLocal;
import org.springframework.stereotype.Component;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Aspect
@Component
@Slf4j
public class SystemLogAspect {
private static final ThreadLocal<Date> beginTimeThreadLocal = new NamedThreadLocal<Date>("ThreadLocal beginTime");
@Autowired
private LogDao logDao;
@Autowired
private IpInfoUtil ipInfoUtil;
@Autowired(required = false)
private HttpServletRequest request;
/**
* Controller层切点,注解方式
*/
//@Pointcut("execution(* *..controller..*Controller*.*(..))")
@Pointcut("@annotation(cn.org.xaas.equipment.annotation.SystemLog)")
public void controllerAspect() {
}
/**
* 前置通知 (在方法执行之前返回)用于拦截Controller层记录用户的操作的开始时间
*
* @param joinPoint 切点
* @throws InterruptedException
*/
@Before("controllerAspect()")
public void doBefore(JoinPoint joinPoint) throws InterruptedException {
//线程绑定变量(该数据只有当前请求的线程可见)
Date beginTime = new Date();
beginTimeThreadLocal.set(beginTime);
}
/**
* 后置通知(在方法执行之后并返回数据) 用于拦截Controller层无异常的操作
*
* @param joinPoint 切点
*/
@AfterReturning("controllerAspect()")
public void after(JoinPoint joinPoint) {
try {
// 获取操作人,每个系统不一样,一般存储与session,此处就不展示了
String username = HeaderSecurityUtils.getUserName();
// 读取json数据
String openApiRequestData = getJSON(request);
Map<String, String[]> requestParams = request.getParameterMap();
Log log = new Log();
if (openApiRequestData != null) {
log.setRequestJson(JSONUtil.toJsonStr(openApiRequestData));
}
log.setId(IdUtil.simpleUUID());
log.setUsername(username);
//日志标题
String description = getControllerMethodInfo(joinPoint).get("description").toString();
log.setDescription(description);
//日志类型
log.setOperationType((int) getControllerMethodInfo(joinPoint).get("type"));
//日志请求url
log.setRequestUrl(request.getRequestURI());
//请求方式
log.setRequestType(request.getMethod());
//请求参数
log.setRequestParam(JSONUtil.toJsonStr(requestParams));
//其他属性
log.setIp(ipInfoUtil.getIpAddr(request));
log.setCreateBy(username);
log.setUpdateBy(username);
log.setCreateTime(new Date());
log.setUpdateTime(new Date());
log.setDeleteFlag("0");
//请求开始时间
long beginTime = beginTimeThreadLocal.get().getTime();
long endTime = System.currentTimeMillis();
//请求耗时
Long logElapsedTime = endTime - beginTime;
log.setCostTime(logElapsedTime.intValue());
//持久化(存储到数据或者ES,可以考虑用线程池)
ThreadPoolUtil.getPool().execute(new SaveSystemLogThread(log, logDao));
} catch (Exception e) {
log.error("AOP后置通知异常", e);
}
}
/**
* 获取request的body
*
* @param request
* @return
*/
public String getJSON(HttpServletRequest request) {
ServletInputStream inputStream = null;
InputStreamReader inputStreamReader = null;
BufferedReader streamReader = null;
StringBuilder responseStrBuilder = new StringBuilder();
try {
inputStream = request.getInputStream();
inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
streamReader = new BufferedReader(inputStreamReader);
String inputStr;
while ((inputStr = streamReader.readLine()) != null) {
responseStrBuilder.append(inputStr);
}
} catch (IOException ioException) {
ioException.printStackTrace();
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (inputStreamReader != null) {
inputStreamReader.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (streamReader != null) {
streamReader.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return responseStrBuilder.toString();
}
/**
* 保存日志至数据库
*/
private static class SaveSystemLogThread implements Runnable {
private Log log;
private LogDao logDao;
public SaveSystemLogThread(Log esLog, LogDao logDao) {
this.log = esLog;
this.logDao = logDao;
}
@Override
public void run() {
logDao.insert(log);
}
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param joinPoint 切点
* @return 方法描述
* @throws Exception
*/
public static Map<String, Object> getControllerMethodInfo(JoinPoint joinPoint) throws Exception {
Map<String, Object> map = new HashMap<String, Object>(16);
//获取目标类名
String targetName = joinPoint.getTarget().getClass().getName();
//获取方法名
String methodName = joinPoint.getSignature().getName();
//获取相关参数
Object[] arguments = joinPoint.getArgs();
//生成类对象
Class targetClass = Class.forName(targetName);
//获取该类中的方法
Method[] methods = targetClass.getMethods();
String description = "";
Integer type = null;
for (Method method : methods) {
if (!method.getName().equals(methodName)) {
continue;
}
Class[] clazzs = method.getParameterTypes();
if (clazzs.length != arguments.length) {
//比较方法中参数个数与从切点中获取的参数个数是否相同,原因是方法可以重载哦
continue;
}
description = method.getAnnotation(SystemLog.class).description();
type = method.getAnnotation(SystemLog.class).type().ordinal();
map.put("description", description);
map.put("type", type);
}
return map;
}
}
在springboot配置文件当中配置即可,false的时候,日志就不记录了。
spring:
aop:
auto: true
import cn.org.xaas.equipment.annotation.SystemLog;
import cn.org.xaas.equipment.constant.OperationType;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/test")
public class TestController {
@PostMapping("/test1")
@SystemLog(description = "根据id查询某某数据",type = OperationType.SELECT)
public void test1(@RequestParam("id")String id){
System.out.println(id);
}
@PostMapping("/test2")
@SystemLog(description = "根据id查询某某数据,传json",type = OperationType.SELECT)
public void test2(@RequestBody String id){
System.out.println(id);
}
}