• 用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 注解」简化 setgetspring-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.lanzous.com/id0yikf

    如果你觉得本篇文章对你有所帮助,不如右上角关注一下我~

  • 相关阅读:
    make 实例 一 3463
    python3 中对arrow库的总结(转发)
    impala 导出CSV 或excel
    设置虚拟机IP
    centos7 tomcat9
    eclipse 创建普通maven项目
    java log4j日志配置
    java运行jar命令提示没有主清单属性
    Java 读取 .properties 配置文件
    python 机器学习多项式回归
  • 原文地址:https://www.cnblogs.com/niceyoo/p/12969341.html
Copyright © 2020-2023  润新知