用Elasticsearch代替数据库存储日志方式
之前的项目中一直使用的是数据库表记录用户操作日志的,但随着时间的推移,数据库log单表是越来越大「不考虑删除」,再加上近期项目中需要用到Elasticsearch
,所以干脆把这些用户日志迁移到ES上来了。
环境:SpringBoot2.2.6 + Elasticsearch6.8.8
如果你还不了解Elasticsearch的话,可以参考之前的几篇文章:
- ES基本概念:https://www.cnblogs.com/niceyoo/p/10864783.html
- 重温ES基础:https://www.cnblogs.com/niceyoo/p/11329426.html
- ES-Windows集群搭建:https://www.cnblogs.com/niceyoo/p/11343697.html
- ES-Docker集群搭建:https://www.cnblogs.com/niceyoo/p/11342903.html
- MacOS中ES搭建:https://www.cnblogs.com/niceyoo/p/12936325.html
由于之前就是使用的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: true
3、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代替数据库存储日志方式相关推荐
- 【Clickhouse】rsyslog服务器使用clickhouse列数据库存储日志
遇到错误信息: validate rsyslogd: omclickhouse: we are suspending ourselfs due to server failure 35: error ...
- Android基础篇-五大存储方式之一数据库存储
废话不多说,直接看代码 activity_main: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/a ...
- 前端三种本地存储方式+indexedDB浏览器数据库存储
1.cookie存储: 特征:1.不同的浏览器存放的cookie位置不一样,也是不能通用的.2.cookie的存储是以域名形式进行区分的,不同的域下存储的cookie是独立的.3.我们可以设置cook ...
- Mesos各种存储处理方式
2019独角兽企业重金招聘Python工程师标准>>> Mesos各种存储处理方式 需要配合永久存储机制的任务包括MySQL.MongoDB等数据库,以及Nginx缓存.日志记录目录 ...
- 唯品会基于 Clickhouse 存储日志的实践
1.背景 唯品会日志系统dragonfly 1.0是基于EFK构建,于2014年服务至今已长达7年,支持物理机日志采集,容器日志采集,特殊分类日志综合采集等,大大方便了全公司日志的存储和查询. 随着公 ...
- 《SQL Server企业级平台管理实践》读书笔记——关于SQL Server数据库的备份方式...
数据备份一直被认为数据库的生命,也就是一个DBA所要掌握的主要技能之一,本篇就是介绍SQL Server备份原则,SQL Server数据库分为数据文件和日志文件.为了使得数据库能够恢复一致点,备份不 ...
- 数据库存储引擎学习总结
什么是存储引擎以及不同存储引擎特点 http://www.cnblogs.com/wildfox/p/5815414.html 以前一直玩Oracle数据库,整天围着业务需求和执行计划转,刚刚接触My ...
- Mysql数据库存储引擎--转
原文地址:http://pangge.blog.51cto.com/6013757/1303893 简单介绍 存储引擎就是指表的类型.数据库的存储引擎决定了表在计算机中的存储方式.存储引擎的概念是My ...
- java如何做数据归档_oracle数据库的归档方式
数据库可运行在两种不同方式下:NOARCHIVELOG方式(介质恢复无效)或ARCHIVELOG方式(介质恢复有效).数据库的运行方式对数据库的 备份和恢复 策略具有重要的影响.归档日志对数据库备份和 ...
最新文章
- C#如何在Form中嵌入并且操作Excel表格
- 运用神经网络方法找寻集成学习中的最优权重
- 访问web服务器--网络实验
- TortoiseGit清除账号密码
- vscode快捷替换json格式
- java成神之——Fork/Join基本使用
- 宁波计算机软件再好的大学是,浙江这些实力较强的大学,分数会不会虚高?
- 例外被抛出且未被接住--服务端与客户端隐藏
- 微信商户支付平台微信支付怎么开通
- 风险预测模型_5分+整合多中心临床样本构建5分子胰腺癌预后模型
- C语言数字图像处理进阶---1 Photoshop图层算法
- c语言现行的标准,C语言的标准
- ch341a i2c 安卓_CH341A转I2C的Labview应用说明
- 拉普拉斯矩阵(Laplacian matrix)及其变体
- COI 2020 Pastiri(贪心)
- Android TV Menu 3D星体旋转效果
- openwrt+php+not+found,openwrt搜不到wifi
- 2.1.1 Java代码基本格式
- Python量化投资——投资组合的评价和可视化(下):使用Matplotlib生成专业的投资回测数据可视化仪表盘【源码+详解】
- ArcGIS|shp文件提取图斑拐点坐标信息
热门文章
- [css] 对比下px、em、rem有什么不同?
- [css] padding会影响到元素的大小,那不想让它影响到元素的宽度应该怎么办?
- [js] 在DOM上同时绑定两个点击事件(一个用捕获,一个用冒泡),事件总共会执行几次,先执行哪个事件?
- 前端学习(2379):调整初始目录结构
- 前端学习(1900)vue之电商管理系统电商系统之渲染添加用户的表单自定义邮箱的规则
- 前端学习(1675):前端系列实战课程之无缝滚动思路
- 前端学习(142):行级标签和块级标签
- 实例2:python
- CSS之flex需要知道的一切(一)
- 都说dlib是人脸识别的神器,那到底能不能识破妖怪的伪装?