springboot日志统计
怪咖软妹@ 人气:0步骤写的很详细,可以直接复制拿来用的,其中用到了过滤器、自定义注解以及AOP切面,来完成日志记录统计,感兴趣的收藏起来,以后遇到了可以直接用。
可能步骤会比较多,但是整体跟着思路下来,应该没什么大问题的。
项目用到了过滤器,可能有的人会不理解,之所以用过滤器是因为想要在日志记录post请求的json数据。
请求的时候,是通过request的body来传输的。在AOP后置方法中获取request里面的body,是取不到,直接为空。
原因很简单:因为是流。想想看,java中的流也是只能读一次,因为我是在AOP后置方法获取的,控制器实际上已经读过了一次,后置方法再读自然为空了。所以用过滤器来进行解决了这个问题。
1、创建日志表
这里我用的是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;
2、创建实体类
我的项目运用到了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; }
3、创建枚举类
用来记录日志操作类型
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; } }
4、创建自定义注解
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; }
5、获取ip的util
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; } }
6、线程池util
利用线程异步记录日志。所以直接用了一个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()); } }
7、HttpServletRequest实现类
这个就是重写的一个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; } }
8、添加过滤器
这个过滤器我添加了一个路径,就是代表需要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; } }
9、添加AOP核心类
对于切面,我们可以通过指定包名,进行日志统计,也可以选择根据自定义的注解在方法上添加,然后进行统计,根据自己的实际情况,在切点进行配置即可。
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; } }
10、接口测试
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); } }
调用第一个测试接口:
调用第二个测试接口:
加载全部内容