springboot利用AOP完成日志统计的详细步骤
作者:怪咖软妹@
步骤写的很详细,可以直接复制拿来用的,其中用到了过滤器、自定义注解以及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); } }
调用第一个测试接口:
调用第二个测试接口:
到此这篇关于springboot利用AOP完成日志统计的文章就介绍到这了,更多相关springboot日志统计内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!