用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: 6666servlet:context-path: /tomcat:uri-encoding: UTF-8spring:# Elasticsearchdata: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;/*** 主键*/@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年专科毕业后,期间一度迷茫,最近我创建了一个公众号用来记录自己的成长。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/414005.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[js] 写一个方法实现promise失败后自动重试

[js] 写一个方法实现promise失败后自动重试 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><meta http-equiv&…

jacky解读麻省理工《计算机科学与Python编程导论》第1集

文&#xff1a;数据分析&#xff0d;jacky(朱元禄) &#xff08;一&#xff09;导言 本课程讲的中心思想就是五个字&#xff1a;计算机思维Python只是辅助工具&#xff0c;是辅助大家理解计算机思维&#xff0c;仅此而已 急功近利是人性&#xff0c;适得其反是结果&#xff1a;…

如何理解Java中的自动拆箱和自动装箱?

小伟刚毕业时面的第一家公司就被面试官给问住了&#xff0c;记忆尤深啊… 如何理解Java中的自动拆箱和自动装箱&#xff1f; 自动拆箱&#xff1f;自动装箱&#xff1f;什么鬼&#xff0c;听都没听过啊&#xff0c;这…这…知识盲区… 回到家后小伟赶紧查资料&#xff0c;我…

[js] 实现多张图片合成一张的效果

[js] 实现多张图片合成一张的效果 原理是使用canvas画布。在页面加载前 mounted方法里&#xff1a;setTimeout(() > {this.changeimg();}, 1000);changeimg( )方法&#xff1a;changeimg(){var self this;var imgsrcArray [require(../../assets/image/income/background…

chrome浏览器中解决embed标签 loop=true 背景音乐无法循环的问题。

今天试了下在html网页中加入背景音乐并设置为循环播放。一开始用<embed>标签&#xff0c;设置loop"true", 但是结果发现在IE浏览器可以&#xff0c;但是在chrome浏览器却无法实现循环&#xff0c; 播放完一次自动停止了。代码如下&#xff1a; <embed src&q…

基于Docker的Redis集群简单搭建

环境&#xff1a;Docker ( Redis:5.0.5 * 3 ) 1、拉取镜像 docker pull redis:5.0.52、创建Redis容器 创建三个 redis 容器&#xff1a; redis-node1&#xff1a;6379redis-node2&#xff1a;6380redis-node3&#xff1a;6381 docker create --name redis-node1 -v /data…

[js] ajax如何接收后台传来的图片?

[js] ajax如何接收后台传来的图片&#xff1f; 1.设置responseType为 Blob&#xff0c;2.将Blob保存为文件个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但坚持一定很酷。欢迎大家一起讨论 主目录 与歌谣一起通关前端面试题

Python 全栈开发十 socket网络编程

一、客户端&#xff08;client&#xff09;服务端&#xff08;sever&#xff09;架构 在计算机中有很多常见的C/S架构&#xff0c;例如我们的浏览器是客户端、而百度网站和其他的网站就是服务端&#xff1b;视频软件是客户端&#xff0c;提供视频的腾讯、优酷、爱奇艺就是服务端…

[js] js源代码压缩都有哪些方法?它们的压缩原理分别是什么

[js] js源代码压缩都有哪些方法&#xff1f;它们的压缩原理分别是什么 方法 1.在线工具 2.webpack原理 1.删除注释 2.变量名方法名字符精减个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但坚持一定很酷。欢迎大家一起讨论 主目录 与…

基于Docker方式实现Elasticsearch集群

文本环境&#xff1a;Docker (Elasticsearch6.8.5 * 3) 1、拉取Elasticsearch docker pull elasticsearch6.8.52、创建es挂载目录 创建3个文件夹用于存放es挂载地址&#xff1a;es01、es02、es03 [rootCentOS7 ~]# mkdir /es-cluster [rootCentOS7 ~]# cd /es-cluster/ [ro…

[js] 不用 + eval Function 实现加法

[js] 不用 eval Function 实现加法 // 使用位运算符实现 function add (a, b) {if (a 0 || b 0) {return a || b;}while (b ! 0) {let i b;b (a & b) << 1;a a ^ i;}return a; };个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&…

基于Docker搭建Gitlab代码存储

关于Docker搭建Gitlab&#xff0c;在19年时就已经在博客发过文章了&#xff0c;今天重新回顾一下。 1、拉取镜像 docker pull gitlab/gitlab-ce默认拉取最新版本&#xff1a; 2、创建Gitlab配置 创建GitLab 的配置 (etc) 、 日志 (log) 、数据 (data) 放到容器之外&#xff…

Java Web开发技术教程入门-JavaBean组件与Servlet

补更&#xff1a;阅战阅勇第7/8/9Days笔记昨天我们了解了JDBC技术的一些日常操作&#xff0c;对于数据库而言&#xff0c;不仅仅的只有“增&#xff0c;删&#xff0c;改&#xff0c;查”。博主觉得最重要的是SQL语句的优化&#xff0c;一个“完美”的SQL语句可以大大减少程序的…

[js] 写一个 document.querySelector 的逆方法

[js] 写一个 document.querySelector 的逆方法 document.queryNode function(node){if(node.id){return # node.id;}if(node.className){return . node.id;}return node.nodeName; };个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但…

读书笔记--Android Gradle权威指南(上)

本篇文章已授权微信公众号 dasu_Android&#xff08;大苏&#xff09;独家发布 最近看了一本书《Android Gradle 权威指南》&#xff0c;对于 Gradle 理解又更深了&#xff0c;但不想过段时间就又忘光了&#xff0c;所以打算写一篇读书笔记&#xff0c;将书中一些我个人觉得蛮有…

[js] 如何判断对象是否属于某个类?

[js] 如何判断对象是否属于某个类&#xff1f; obj.proto class.prototype 可以递归去找obj instanceof class个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但坚持一定很酷。欢迎大家一起讨论 主目录 与歌谣一起通关前端面试题

基于Docker搭建私有镜像仓库

通常我们在docker中拉取的镜像都是在docker hub在线存储库中获取的&#xff0c;这个在线存储库里的docker镜像可以由任何用户发布和使用&#xff0c;显然这在某些场景下是不适用的&#xff0c;比如某些互金的隐私项目&#xff0c;或者是公司完全处于内网状态不能访问外网&#…

[js] 说说你对js沙箱的理解,它有什么应用场景?

[js] 说说你对js沙箱的理解&#xff0c;它有什么应用场景&#xff1f; 在微前端有用到js沙箱&#xff0c;例如qiankun框架&#xff0c;主应用的js运行和子任务的js运行不会相互影响&#xff0c;是使用es6的proxy来实现的个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后…

volatile理解了吗?

到这里大家感觉自己对volatile理解了吗&#xff1f; 如果理解了&#xff0c;大家考虑这么一个问题&#xff1a;ReentrantLock&#xff08;或者其它基于AQS实现的锁&#xff09;是如何保证代码段中变量&#xff08;变量主要是指共享变量&#xff0c;存在竞争问题的变量&…

Linux|CentOS下配置Maven环境

1、下载maven包 wget http://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.tar.gz如果提示 wget: 未找到命令&#xff0c;请尝试如下指令安装 wget yum -y install wget2、解压下载的maven压缩吧 tar -xzvf apache-maven-3.3.…