用Elasticsearch代替數據庫存儲日志方式


之前的項目中一直使用的是數據庫表記錄用戶操作日志的,但隨着時間的推移,數據庫log單表是越來越大「不考慮刪除」,再加上近期項目中需要用到Elasticsearch,所以干脆把這些用戶日志遷移到ES上來了。

環境:SpringBoot2.2.6 + Elasticsearch6.8.8

如果你還不了解Elasticsearch的話,可以參考之前的幾篇文章:

  1. ES基本概念:https://www.cnblogs.com/niceyoo/p/10864783.html
  2. 重溫ES基礎:https://www.cnblogs.com/niceyoo/p/11329426.html
  3. ES-Windows集群搭建:https://www.cnblogs.com/niceyoo/p/11343697.html
  4. ES-Docker集群搭建:https://www.cnblogs.com/niceyoo/p/11342903.html
  5. MacOS中ES搭建:https://www.cnblogs.com/niceyoo/p/12936325.html

由於之前就是使用的AOP+注解方式實現日志記錄,而本次依舊采用這種方式,所以改動不大,把保存至數據庫換成ES就可以了,開始吧。

文章最后我會提供源碼的,正文描述部分有省略~

1、引入依賴文件

pom.xml文件中引入需要的esaop所需的依賴:

<?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: 6666
  servlet:
    context-path: /
  tomcat:
    uri-encoding: UTF-8

spring:
  # Elasticsearch
  data:
    elasticsearch:
      client:
        reactive:
          # 要連接的ES客戶端 多個逗號分隔
          endpoints: 127.0.0.1:9300
      # 暫未使用ES 關閉其持久化存儲
      repositories:
        enabled: true

3、Log實體

使用了lombok@Data 注解」簡化 set\getspring-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;
    /** * 主鍵 */
    @Id
    private 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");

    @Autowired
    private 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);
            //日志請求url
            esLog.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());
            //調用線程保存至ES
            ThreadPoolUtil.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;
        }

        @Override
        public 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())){
        // 僅有type
        return 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())){
        // 僅有key
        qb = 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 {

    @Autowired
    private 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 中查詢一下是否保存成功:

image-20200526224423804image-20200526224423804

以getAll()方法為例,再測試一下查詢方法,在瀏覽器輸入 http://127.0.0.1:8888/log/getAll?key=&type=2,返回如下:

image-20200526224614801image-20200526224614801

9、最后補充

本節是我拆分出來的一個demo,經測試增刪改查是沒問題、同時查詢方法加入了分頁查詢,具體代碼細節可以下載本節源碼自行查看。

源碼下載鏈接:https://niceyoo.lanzoux.com/id0yikf

如果你覺得本篇文章對你有所幫助,不如右上角關注一下我~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM