springboot利用AOP完成日志統(tǒng)計的詳細步驟
步驟寫的很詳細,可以直接復(fù)制拿來用的,其中用到了過濾器、自定義注解以及AOP切面,來完成日志記錄統(tǒng)計,感興趣的收藏起來,以后遇到了可以直接用。
可能步驟會比較多,但是整體跟著思路下來,應(yīng)該沒什么大問題的。
項目用到了過濾器,可能有的人會不理解,之所以用過濾器是因為想要在日志記錄post請求的json數(shù)據(jù)。
請求的時候,是通過request的body來傳輸?shù)?。在AOP后置方法中獲取request里面的body,是取不到,直接為空。
原因很簡單:因為是流。想想看,java中的流也是只能讀一次,因為我是在AOP后置方法獲取的,控制器實際上已經(jīng)讀過了一次,后置方法再讀自然為空了。所以用過濾器來進行解決了這個問題。
1、創(chuàng)建日志表
這里我用的是mysql,假如您用的別的數(shù)據(jù)庫,可以自行根據(jù)數(shù)據(jù)庫類型進行修改。
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 '創(chuàng)建人', `create_time` datetime NULL DEFAULT NULL COMMENT '創(chuàng)建時間', `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 '更新次數(shù)', `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 '請求參數(shù)', `request_json` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '請求json數(shù)據(jù)', `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、創(chuàng)建實體類
我的項目運用到了mybatisplus、swagger、lombok,你們可以根據(jù)自己項目框架寫對應(yīng)的實體類。BaseModel 是我們封裝了一個基礎(chǔ)實體類,專門存放關(guān)于操作人的信息,然后實體類直接繼承。
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 = "請求參數(shù)") @TableField(value = "request_param") private String requestParam; @ApiModelProperty(value = "請求json數(shù)據(jù)") @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、創(chuàng)建枚舉類
用來記錄日志操作類型
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、創(chuàng)建自定義注解
import java.lang.annotation.*; @Target({ElementType.PARAMETER, ElementType.METHOD})//作用于參數(shù)或方法上 @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)) { //根據(jù)網(wǎng)卡取本機配置的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); /** * 核心線程數(shù),會一直存活,即使沒有任務(wù),線程池也會維護線程的最少數(shù)量 */ private static final int SIZE_CORE_POOL = 5; /** * 線程池維護線程的最大數(shù)量 */ 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實現(xiàn)類
這個就是重寫的一個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當(dāng)中添加路徑,不需要取request當(dāng)中json數(shù)據(jù)的可以不配置。
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當(dāng)中body數(shù)據(jù)的,就需要進行配置路徑 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() { } }
想要讓過濾器生效需要注入到容器當(dāng)中。
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核心類
對于切面,我們可以通過指定包名,進行日志統(tǒng)計,也可以選擇根據(jù)自定義的注解在方法上添加,然后進行統(tǒng)計,根據(jù)自己的實際情況,在切點進行配置即可。
LogDao我是沒有提供的,每個項目框架不一樣,自行根據(jù)情況進行編寫,就是保存數(shù)據(jù)庫就可以了。
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() { } /** * 前置通知 (在方法執(zhí)行之前返回)用于攔截Controller層記錄用戶的操作的開始時間 * * @param joinPoint 切點 * @throws InterruptedException */ @Before("controllerAspect()") public void doBefore(JoinPoint joinPoint) throws InterruptedException { //線程綁定變量(該數(shù)據(jù)只有當(dāng)前請求的線程可見) Date beginTime = new Date(); beginTimeThreadLocal.set(beginTime); } /** * 后置通知(在方法執(zhí)行之后并返回數(shù)據(jù)) 用于攔截Controller層無異常的操作 * * @param joinPoint 切點 */ @AfterReturning("controllerAspect()") public void after(JoinPoint joinPoint) { try { // 獲取操作人,每個系統(tǒng)不一樣,一般存儲與session,此處就不展示了 String username = HeaderSecurityUtils.getUserName(); // 讀取json數(shù)據(jù) 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()); //請求參數(shù) 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()); //持久化(存儲到數(shù)據(jù)或者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(); } /** * 保存日志至數(shù)據(jù)庫 */ 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(); //獲取相關(guān)參數(shù) 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) { //比較方法中參數(shù)個數(shù)與從切點中獲取的參數(shù)個數(shù)是否相同,原因是方法可以重載哦 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 = "根據(jù)id查詢某某數(shù)據(jù)",type = OperationType.SELECT) public void test1(@RequestParam("id")String id){ System.out.println(id); } @PostMapping("/test2") @SystemLog(description = "根據(jù)id查詢某某數(shù)據(jù),傳json",type = OperationType.SELECT) public void test2(@RequestBody String id){ System.out.println(id); } }
調(diào)用第一個測試接口:
調(diào)用第二個測試接口:
到此這篇關(guān)于springboot利用AOP完成日志統(tǒng)計的文章就介紹到這了,更多相關(guān)springboot日志統(tǒng)計內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot中利用AOP和攔截器實現(xiàn)自定義注解
- SpringBoot使用AOP實現(xiàn)統(tǒng)計全局接口訪問次數(shù)詳解
- SpringBoot通過AOP與注解實現(xiàn)入?yún)⑿r炘斍?/a>
- SpringBoot使用AOP統(tǒng)一日志管理的方法詳解
- Springboot+AOP實現(xiàn)時間參數(shù)格式轉(zhuǎn)換
- springboot使用AOP+反射實現(xiàn)Excel數(shù)據(jù)的讀取
- springboot利用aop實現(xiàn)接口異步(進度條)的全過程
- SpringBoot中通過AOP整合日志文件的實現(xiàn)
- Spring?BOOT?AOP基礎(chǔ)應(yīng)用教程
相關(guān)文章
Spring中Websocket身份驗證和授權(quán)的實現(xiàn)
在Web應(yīng)用開發(fā)中,安全一直是非常重要的一個方面,本文主要介紹了Spring中Websocket身份驗證和授權(quán)的實現(xiàn),具有一定的參考價值,感興趣的可以了解一下2023-08-08JavaWeb頁面中防止點擊Backspace網(wǎng)頁后退情況
當(dāng)鍵盤敲下后退鍵(Backspace)后怎么防止網(wǎng)頁后退情況呢?今天小編通過本文給大家詳細介紹下,感興趣的朋友一起看看吧2016-11-11Netty網(wǎng)絡(luò)編程實戰(zhàn)之搭建Netty服務(wù)器
Netty是JBOSS開源的一款NIO網(wǎng)絡(luò)編程框架,可用于快速開發(fā)網(wǎng)絡(luò)的應(yīng)用。Netty是一個異步的、基于事件驅(qū)動的網(wǎng)絡(luò)應(yīng)用框架,用于快速開發(fā)高性能的服務(wù)端和客戶端。本文將詳細說說如何搭建Netty服務(wù)器,需要的可以參考一下2022-10-10解讀httpclient的validateAfterInactivity連接池狀態(tài)檢測
這篇文章主要為大家介紹了httpclient的validateAfterInactivity連接池狀態(tài)檢測解讀*,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-11-11java實現(xiàn)服務(wù)器文件打包zip并下載的示例(邊打包邊下載)
這篇文章主要介紹了java實現(xiàn)服務(wù)器文件打包zip并下載的示例,使用該方法,可以即時打包文件,一邊打包一邊傳輸,不使用任何的緩存,讓用戶零等待,需要的朋友可以參考下2014-04-04