用Elasticsearch代替数据库存储日志方式
之前的項目中一直使用的是數據庫表記錄用戶操作日志的,但隨著時間的推移,數據庫log單表是越來越大「不考慮刪除」,再加上近期項目中需要用到Elasticsearch,所以干脆把這些用戶日志遷移到ES上來了。
環境:SpringBoot2.2.6 + Elasticsearch6.8.8
如果你還不了解Elasticsearch的話,可以參考之前的幾篇文章:
由于之前就是使用的AOP+注解方式實現日志記錄,而本次依舊采用這種方式,所以改動不大,把保存至數據庫換成ES就可以了,開始吧。
文章最后我會提供源碼的,正文描述部分有省略~
1、引入依賴文件
pom.xml文件中引入需要的es、aop所需的依賴:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.6.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.example</groupId><artifactId>demo</artifactId><version>0.0.1-SNAPSHOT</version><name>demo</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId></dependency><!-- Gson --><dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>2.8.6</version></dependency><!-- Hutool工具包 --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.3.2</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build> </project>2、修改yml配置文件
加入elasticsearch的配置信息:
server:port: 6666servlet:context-path: /tomcat:uri-encoding: UTF-8spring:# Elasticsearchdata:elasticsearch:client:reactive:# 要連接的ES客戶端 多個逗號分隔endpoints: 127.0.0.1:9300# 暫未使用ES 關閉其持久化存儲repositories:enabled: true3、Log實體
使用了lombok「 @Data 注解」簡化 set\get,spring-data-elasticsearch提供了@Document、@Id、@Field注解,其中@Document作用在實體類上,指向文檔地址,@Id、@Field作用于成員變量上,分別表示主鍵、字段。
@Data @Document(indexName = "log", type = "log", shards = 1, replicas = 0, refreshInterval = "-1") public class EsLog implements Serializable{private static final long serialVersionUID = 1L;/*** 主鍵*/@Idprivate String id = SnowFlakeUtil.nextId().toString();/*** 創建者*/private String createBy;/*** 創建時間*/@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")@Field(type = FieldType.Date, index = false, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss")private Date createTime = new Date();/*** 時間戳 查詢時間范圍時使用*/private Long timeMillis = System.currentTimeMillis();/*** 方法操作名稱*/private String name;/*** 日志類型*/private Integer logType;/*** 請求鏈接*/private String requestUrl;/*** 請求類型*/private String requestType;/*** 請求參數*/private String requestParam;/*** 請求用戶*/private String username;/*** ip*/private String ip;/*** 花費時間*/private Integer costTime;/*** 轉換請求參數為Json* @param paramMap*/public void setMapToParams(Map<String, String[]> paramMap) {this.requestParam = ObjectUtil.mapToString(paramMap);} }4、Dao層
數據操作層,有兩種方式實現對Elasticsearch數據的修改,一是使用ElasticsearchTemplate,二是通過ElasticsearchRepository接口,本文基于后者接口方式。
用過SpringDataJPA的小伙伴就不陌生了,如下實現接口就跟JPA通過方法名稱生成SQL一樣簡單。
/*** esc dao*/ public interface EsLogDao extends ElasticsearchRepository<EsLog, String> {/*** 通過類型獲取* @param type* @return*/Page<EsLog> findByLogType(Integer type, Pageable pageable); }默認情況下,ElasticsearchRepository提供了findById()、findAll()、findAllById()、search()等方法供我們方便使用。
5、自定義注解
自定義 @SystemLog 注解,用于標記需要記錄日志的方法。
@Target({ElementType.PARAMETER, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SystemLog {/*** 日志名稱* @return*/String description() default "";/*** 日志類型* @return*/LogType type() default LogType.OPERATION; }6、編寫切面、通知
步驟5中自定義了注解,那么接下來就是定位注解,以及對定位后的方法進行業務處理部分了,而對我們來說就是把日志記錄至Elasticsearch中。
/*** 日志管理*/ @Aspect @Component @Slf4j public class SystemLogAspect {private static final ThreadLocal<Date> beginTimeThreadLocal = new NamedThreadLocal<Date>("ThreadLocal beginTime");@Autowiredprivate EsLogService esLogService;@Autowired(required = false)private HttpServletRequest request;/*** Controller層切點,注解方式*/@Pointcut("@annotation(com.example.demo.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 {String username = "";String description = getControllerMethodInfo(joinPoint).get("description").toString();int type = (int)getControllerMethodInfo(joinPoint).get("type");Map<String, String[]> logParams = request.getParameterMap();EsLog esLog = new EsLog();//請求用戶esLog.setUsername("小偉");//日志標題esLog.setName(description);//日志類型esLog.setLogType(type);//日志請求urlesLog.setRequestUrl(request.getRequestURI());//請求方式esLog.setRequestType(request.getMethod());//請求參數esLog.setMapToParams(logParams);//請求開始時間long beginTime = beginTimeThreadLocal.get().getTime();long endTime = System.currentTimeMillis();//請求耗時Long logElapsedTime = endTime - beginTime;esLog.setCostTime(logElapsedTime.intValue());//調用線程保存至ESThreadPoolUtil.getPool().execute(new SaveEsSystemLogThread(esLog, esLogService));} catch (Exception e) {log.error("AOP后置通知異常", e);}}/*** 保存日志至ES*/private static class SaveEsSystemLogThread implements Runnable {private EsLog esLog;private EsLogService esLogService;public SaveEsSystemLogThread(EsLog esLog, EsLogService esLogService) {this.esLog = esLog;this.esLogService = esLogService;}@Overridepublic void run() {esLogService.saveLog(esLog);}}/*** 獲取注解中對方法的描述信息 用于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;}}7、EsLogService接口類
EsLogService中我們編寫幾個常用的接口方法,增刪改查:
/*** 日志操作service*/ public interface EsLogService {/*** 添加日志* @param esLog* @return*/EsLog saveLog(EsLog esLog);/*** 通過id刪除日志* @param id*/void deleteLog(String id);/*** 刪除全部日志*/void deleteAll();/*** 分頁搜索獲取日志* @param type* @param key* @param searchVo* @param pageable* @return*/Page<EsLog> findAll(Integer type, String key, SearchVo searchVo, Pageable pageable); }我們簡單看一下這個 findAll 方法的實現類吧,其他方法就是直接調用ElasticsearchRepository提供的findById()、findAll()、findAllById()、save()等方法。
/*** @param type 類型* @param key 搜索的關鍵字* @param searchVo* @param pageable* @return*/ @Override public Page<EsLog> findAll(Integer type, String key, SearchVo searchVo, Pageable pageable) {if(type==null&&StrUtil.isBlank(key)&&StrUtil.isBlank(searchVo.getStartDate())){// 無過濾條件獲取全部return logDao.findAll(pageable);}else if(type!=null&&StrUtil.isBlank(key)&&StrUtil.isBlank(searchVo.getStartDate())){// 僅有typereturn logDao.findByLogType(type, pageable);}QueryBuilder qb;QueryBuilder qb0 = QueryBuilders.termQuery("logType", type);QueryBuilder qb1 = QueryBuilders.multiMatchQuery(key, "name", "requestUrl", "requestType","requestParam","username","ip");// 在有type條件下if(StrUtil.isNotBlank(key)&&StrUtil.isBlank(searchVo.getStartDate())&&StrUtil.isBlank(searchVo.getEndDate())){// 僅有keyqb = QueryBuilders.boolQuery().must(qb0).must(qb1);}else if(StrUtil.isBlank(key)&&StrUtil.isNotBlank(searchVo.getStartDate())&&StrUtil.isNotBlank(searchVo.getEndDate())){// 僅有時間范圍Long start = DateUtil.parse(searchVo.getStartDate()).getTime();Long end = DateUtil.endOfDay(DateUtil.parse(searchVo.getEndDate())).getTime();QueryBuilder qb2 = QueryBuilders.rangeQuery("timeMillis").gte(start).lte(end);qb = QueryBuilders.boolQuery().must(qb0).must(qb2);}else{// 兩者都有Long start = DateUtil.parse(searchVo.getStartDate()).getTime();Long end = DateUtil.endOfDay(DateUtil.parse(searchVo.getEndDate())).getTime();QueryBuilder qb2 = QueryBuilders.rangeQuery("timeMillis").gte(start).lte(end);qb = QueryBuilders.boolQuery().must(qb0).must(qb1).must(qb2);}//多字段搜索return logDao.search(qb, pageable); }8、controller層測試方法
/*** 日志操作controller*/ @Slf4j @RestController @RequestMapping("/log") public class LogController {@Autowiredprivate EsLogService esLogService;/*** 測試*/@SystemLog(description = "測試", type = LogType.OPERATION)@RequestMapping(value = "/getA", method = RequestMethod.GET)public Result<Object> getA(String va){return ResultUtil.success("測試成功");}/*** 查詢全部* @param type es 中的logType 不能為空* @param key 查詢的關鍵字* @param searchVo* @param pageVo* @return*/@RequestMapping(value = "/getAll", method = RequestMethod.GET)public Result<Object> getAll(@RequestParam(required = false) Integer type,@RequestParam String key,SearchVo searchVo,PageVo pageVo){Page<EsLog> es = esLogService.findAll(type, key, searchVo, PageUtil.initPage(pageVo));return ResultUtil.data(es);}/*** 批量刪除* @param ids* @return*/@RequestMapping(value = "/delByIds", method = RequestMethod.POST)public Result<Object> delByIds(@RequestParam String[] ids){for(String id : ids){esLogService.deleteLog(id);}return ResultUtil.success("刪除成功");}/*** 全部刪除* @return*/@RequestMapping(value = "/delAll", method = RequestMethod.POST)public Result<Object> delAll(){esLogService.deleteAll();return ResultUtil.success("刪除成功");} }以 getA()方法為例,直接通過瀏覽器調用:http://127.0.0.1:6666/log/getA,然后在 ES 中查詢一下是否保存成功:
以getAll()方法為例,再測試一下查詢方法,在瀏覽器輸入 http://127.0.0.1:8888/log/getAll?key=&type=2,返回如下:
9、最后補充
本節是我拆分出來的一個demo,經測試增刪改查是沒問題、同時查詢方法加入了分頁查詢,具體代碼細節可以下載本節源碼自行查看。
源碼下載鏈接:https://niceyoo.lanzous.com/id0yikf
如果你覺得本篇文章對你有所幫助,不如右上角關注一下我~
18年專科畢業后,期間一度迷茫,最近我創建了一個公眾號用來記錄自己的成長。
總結
以上是生活随笔為你收集整理的用Elasticsearch代替数据库存储日志方式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C语言理论作业—2
- 下一篇: python足球数据分析_我用Pytho