一、背景
在企业级应用,我们经常需要记录后端用户操作各种功能的日志,方便未来发现业务有问题,能通过用户操作日志追溯全流程。此外,我们还可以通过统计用户操作日志,分析某些功能点击的次数,判断哪些功能经常使用,那些功能不经常使用甚至可以下线,通过这种量化的分析,对于产品未来的功能规划也起到指导作用,所以优雅的设计一个后台操作日志功能,对于企业级系统必不可少。
二、应用场景
1、规则类变更场景(比如活动配置规则修改)
2、配置类变更场景(比如公共配置修改)
3、敏感信息类变更场景(比如客户信息修改)
以上场景,如果有变更,通常会对我们系统的业务产生直接的影响,如果人为失误,有可能造成直接的经济损失,所以我们需要记录每一笔操作日志。
那究竟如何优雅的设计一个后端操作日志功能呢?我认为起码要满足以下几个要求:
1)记录后端操作日志必须与业务功能解耦,不能硬编码耦合在一起,增加开发人员的工作量。
2)后端操作日志必须记录管理后台操作的菜单、操作的功能、操作人、操作时间等重要核心的信息。
3)后端操作日志异步入库,不能阻塞主业务流程。
4)后端操作日志重要的信息支持全模糊查询方便管理员快速查询信息。比如:操作内容字段等
三、实现方案
1、底层base公共服务提供日志相关的服务(保存日志、查询日志)
2、通过自定义注解+AOP拦截请求,自动上报日志到base的日志服务
3、通过引入guava的eventbus异步发布事件实现日志的异步入mysql库。
4、考虑到操作内容字段内容比较大,基于canal+kafka,异步将日志表记录同步到es,通过全模糊查询es,可快速查询日志记录。
同时考虑到操作日志表比较大,每3个月归档日志表一次,保存mysql日志表查询性能。
四、架构设计及实现
五、代码案例
查询活动详情接口
/**
* 活动业务处理
*
* @author kyle0432
* @date 2024/03/03 15:01
*/
@Api(tags = "活动相关api")
@RestController
@Slf4j
@RequestMapping(value = "/act/info")
public class ActInfoController {
@ApiOperation(value = "根据Id查询活动信息", notes = "根据Id查询活动信息")
@PostMapping("findById")
@SysOpLogAnnotation(menuName="活动管理", menuBtn = "查看详情", opContent = "查询活动详情",
reqParam = "#req.id")
public RespResult<ActInfoRespDTO> findById(@RequestBody ActInfoReqDTO req) {
return RespResult.success(DCBeanUtil.copyNotNull(actInfoService.findById(req.getId()),
new ActInfoRespDTO()));
}
@ApiOperation(value = "修改活动", notes = "修改活动")
@PostMapping("update")
@SysOpLogAnnotation(menuName="活动管理", menuBtn = "修改活动", opContent = "修改活动",
reqParam = "#actInfoSaveDTO.id")
public RespResult<Boolean> update(@RequestBody ActInfoSaveDTO actInfoSaveDTO) {
log.info("修改保存活动....");
if(StrUtil.isEmpty(actInfoSaveDTO.getId())){
return RespResult.error("id不能为空");
}
return actInfoService.updateActInfo(actInfoSaveDTO);
}
}
根据Id-查询活动信息
@Service
@Slf4j
@Transactional(rollbackFor = Exception.class)
public class ActInfoService extends ServiceImpl<ActInfoDao, ActInfoDO> {
@Resource
private ActInfoDao actInfoDao;
public ActInfoDO findById(String id) {
return actInfoDao.selectById(id);
}
public RespResult<Boolean> updateActInfo(ActInfoSaveDTO actInfoSaveDTO) {
ActInfoDO actInfoDO = actInfoDao.selectById(actInfoSaveDTO.getId());
if(ObjectUtil.isNull(actInfoDO)){
return RespResult.error("活动不存在!actId:{}", actInfoSaveDTO.getId());
}
if(!ActStatusEnum.NOT_PUBLISH.getCode().equals(actInfoDO.getStatus())){
return RespResult.error("只能修改未发布状态的活动");
}
DCBeanUtil.copyNotNull(actInfoDO, actInfoSaveDTO);
this.updateById(actInfoDO);
log.info("修改活动成功!data:{}", JSONUtil.toJsonStr(actInfoDO));
return RespResult.success(true);
}
}
自定义注解
/**
* 系统操作日志注解
*
* @author kyle0432
* @date 2024/03/03 15:15
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
@Documented
public @interface SysOpLogAnnotation {
/**
* 菜单名称
*/
String menuName() default "";
/**
* 按钮名称
*/
String menuBtn() default "";
/**
* 操作内容
*/
String opContent() default "";
/**
* 请求参数
*/
String reqParam() default "";
}
日志AOP拦截
package com.litian.dancechar.framework.log.bizlog.aspect;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.litian.dancechar.framework.common.base.RespResult;
import com.litian.dancechar.framework.common.context.HttpContext;
import com.litian.dancechar.framework.common.eventbus.EventBusFactory;
import com.litian.dancechar.framework.common.util.IPUtil;
import com.litian.dancechar.framework.log.bizlog.annotation.SysOpLogAnnotation;
import com.litian.dancechar.framework.log.bizlog.dto.SysOpLogReqDTO;
import com.litian.dancechar.framework.log.bizlog.eventbus.SysOpLogEvent;
import com.litian.dancechar.framework.log.bizlog.eventbus.SysOpLogEventBusListener;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.Objects;
/**
* 系统操作日志切面
*
* @author kyle0432
* @date 2024/03/03 15:20
*/
@Slf4j
@Aspect
@Configuration
public class SysOpLogAspect {
private static ExpressionParser expressionParser = new SpelExpressionParser();
private static LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
@Pointcut(value = "@annotation(com.litian.dancechar.framework.log.bizlog.annotation.SysOpLogAnnotation)")
public void aroundBefore(){
log.info("log aspect start....");
}
@AfterReturning(value = "aroundBefore()", returning = "respResult")
public void aroundAfter(JoinPoint joinPoint, Object respResult){
try {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature)signature;
Method method = methodSignature.getMethod();
if(method.isAnnotationPresent(SysOpLogAnnotation.class)){
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
SysOpLogAnnotation sysOpLogAnnotation = method.getAnnotation(SysOpLogAnnotation.class);
SysOpLogReqDTO sysOpLogReqDTO = buildSysOpLogReqDTO(sysOpLogAnnotation, request, joinPoint,
respResult, signature, method);
// 发布异步事件
EventBusFactory.build().registerAsyncEvent(SysOpLogEventBusListener.class);
EventBusFactory.build().postAsyncEvent(SysOpLogEvent.builder()
.sysOpLogReqDTO(sysOpLogReqDTO).eventName("后台操作日志发布事件").build());
}
}catch (Throwable e){
log.error(e.getMessage(), e);
}
}
/**
* 构建请求参数
*/
private SysOpLogReqDTO buildSysOpLogReqDTO(SysOpLogAnnotation sysOpLogAnnotation,HttpServletRequest request,
JoinPoint joinPoint, Object respResult, Signature signature,
Method method){
SysOpLogReqDTO sysOpLogReqDTO = new SysOpLogReqDTO();
sysOpLogReqDTO.setMenuName(sysOpLogAnnotation.menuName());
sysOpLogReqDTO.setMenuBtn(sysOpLogAnnotation.menuBtn());
sysOpLogReqDTO.setUrl(request.getRequestURI());
sysOpLogReqDTO.setClassName(signature.getDeclaringTypeName());
sysOpLogReqDTO.setMethodName(method.getName());
sysOpLogReqDTO.setReqType(request.getMethod());
StringBuilder builder = new StringBuilder();
Object[] args = joinPoint.getArgs();
sysOpLogReqDTO.setOpContent(sysOpLogAnnotation.opContent());
if(args != null && args.length > 0){
for (Object obj : args){
builder.append(JSONUtil.toJsonStr(obj));
}
if(StrUtil.isNotEmpty(sysOpLogAnnotation.reqParam())){
sysOpLogReqDTO.setOpContent(sysOpLogReqDTO.getOpContent()+"#" + parse(sysOpLogAnnotation.reqParam(),
method, args));
}
}
sysOpLogReqDTO.setParams(builder.toString());
if(respResult instanceof RespResult){
RespResult result = (RespResult)respResult;
sysOpLogReqDTO.setSuccess(result.isOk());
}
sysOpLogReqDTO.setResult(JSONUtil.toJsonStr(respResult));
sysOpLogReqDTO.setOpAccount(HttpContext.getMobile());
sysOpLogReqDTO.setOpTime(new Date());
sysOpLogReqDTO.setOpIp(IPUtil.getIpAddress(request));
return sysOpLogReqDTO;
}
public static String parse(String key ,Method method, Object[] args){
String[] params = discoverer.getParameterNames(method);
if(params == null || Objects.equals("default", key)){
return key;
}
EvaluationContext context = new StandardEvaluationContext();
for(int i=0; i < params.length;i++){
context.setVariable(params[i], args[i]);
}
String[] keys = key.split(",");
StringBuilder result = new StringBuilder();
for(String k : keys){
result.append(expressionParser.parseExpression(k).getValue(context ,String.class));
result.append(":");
}
return result.deleteCharAt(result.length() -1).toString();
}
}
系统日志实体类
package com.litian.dancechar.framework.log.bizlog.dto;
import com.litian.dancechar.framework.common.base.BasePage;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
/**
* 操作日志请求对象
*
* @author kyle0432
* @date 2024/03/03 16:18
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class SysOpLogReqDTO extends BasePage implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
private String id;
/**
* 菜单名称
*/
private String menuName;
/**
* 菜单按钮
*/
private String menuBtn;
/**
* 请求url
*/
private String url;
/**
* 类名
*/
private String className;
/**
* 方法名
*/
private String methodName;
/**
* 请求的类型(Get、POST)
*/
private String reqType;
/**
* 请求参数
*/
private String params;
/**
* 执行是否成功
*/
private Boolean success;
/**
* 执行结果
*/
private String result;
/**
* 操作时间
*/
private Date opTime;
/**
* 操作内容
*/
private String opAccount;
/**
* 操作内容
*/
private String opContent;
/**
* 操作IP
*/
private String opIp;
}
通过eventbus发布异步事件
package com.litian.dancechar.framework.common.eventbus;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.google.common.collect.Maps;
import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.EventBus;
import com.litian.dancechar.framework.common.thread.CustomThreadPoolFactory;
import lombok.extern.slf4j.Slf4j;
import java.util.EventListener;
import java.util.Map;
/**
* eventbus发布事件工厂类
*
* @author kyle0432
* @date 2024/03/03 17:18
*/
@Slf4j
public class EventBusFactory {
private static volatile EventBusFactory instance;
private Map<String, Class<? extends EventListener>> registerListenerContainers = Maps.newConcurrentMap();
private Map<String, Class<? extends EventListener>> asyncRegisterListenerContainers = Maps.newConcurrentMap();
private final EventBus eventBus = new EventBus();
private final AsyncEventBus asyncEventBus = new AsyncEventBus(new CustomThreadPoolFactory().newFixedThreadPool(
2 * Runtime.getRuntime().availableProcessors() + 1));
private EventBusFactory() {
}
public static EventBusFactory build() {
// 双重校验实现单例
if (instance == null) {
synchronized (EventBusFactory.class) {
if (instance == null) {
instance = new EventBusFactory();
}
}
}
return instance;
}
/**
* 发布异步事件
*
* @param event 事件对象
*/
public void postAsyncEvent(BaseEvent event) {
asyncEventBus.post(event);
}
/**
* 注册异步监听器
*
* @param asyncClass 异步监听器
*/
public void registerAsyncEvent(Class<? extends EventListener> asyncClass) {
String asyncClassName = asyncClass.getSimpleName();
if (asyncRegisterListenerContainers.containsKey(asyncClassName)) {
return;
}
try {
asyncRegisterListenerContainers.put(asyncClassName, asyncClass);
Object obj = SpringUtil.getBean(asyncClass);
if (ObjectUtil.isNull(obj)) {
obj = asyncRegisterListenerContainers.get(asyncClassName).newInstance();
}
asyncEventBus.register(obj);
} catch (Exception e) {
log.error("注册异步监听器系统异常!asyncClassName:{},errMsg:{}", asyncClassName, e.getMessage(), e);
}
}
}
系统操作日志异步事件
package com.litian.dancechar.framework.log.bizlog.eventbus;
import cn.hutool.core.util.IdUtil;
import com.litian.dancechar.framework.common.eventbus.BaseEvent;
import com.litian.dancechar.framework.log.bizlog.dto.SysOpLogReqDTO;
import lombok.Builder;
import lombok.Getter;
/**
* 系统操作日志异步事件
*
* @author kyle0432
* @date 2024/03/03 17:35
*/
@Getter
public class SysOpLogEvent extends BaseEvent {
/**
* 操作日志对象
*/
private SysOpLogReqDTO sysOpLogReqDTO;
@Builder
public SysOpLogEvent(SysOpLogReqDTO sysOpLogReqDTO, String eventName) {
super(IdUtil.objectId(), eventName);
this.sysOpLogReqDTO = sysOpLogReqDTO;
}
}
系统操作日志异步操作监听
package com.litian.dancechar.framework.log.bizlog.eventbus;
import cn.hutool.core.util.ObjectUtil;
import com.google.common.eventbus.AllowConcurrentEvents;
import com.google.common.eventbus.Subscribe;
import com.litian.dancechar.framework.log.bizlog.dto.SysOpLogReqDTO;
import com.litian.dancechar.framework.log.bizlog.feign.BaseServiceClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.EventListener;
/**
* 系统操作日志异步操作监听
*
* @author kyle0432
* @date 2024/03/03 18:15
*/
@Slf4j
@Component
public class SysOpLogEventBusListener implements EventListener {
@Resource
private BaseServiceClient baseServiceClient;
@Subscribe
@AllowConcurrentEvents
public void listener(SysOpLogEvent sysOpLogEvent){
SysOpLogReqDTO sysOpLogReqDTO = sysOpLogEvent.getSysOpLogReqDTO();
if(ObjectUtil.isNotNull(sysOpLogReqDTO)){
baseServiceClient.saveWithInsert(sysOpLogReqDTO);
}
}
}
feign调用另外一个服务,进行数据库新增日志操作
package com.litian.dancechar.framework.log.bizlog.feign;
import com.litian.dancechar.framework.common.base.RespResult;
import com.litian.dancechar.framework.log.bizlog.dto.SysOpLogReqDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* 基础服务feign
*
* @author kyle0432
* @date 2024/03/03 18:04
*/
@FeignClient("dancechar-base-service")
public interface BaseServiceClient {
/**
* 保存操作日志
*/
@PostMapping("/sys/oplog/saveWithInsert")
RespResult<Boolean> saveWithInsert(@RequestBody SysOpLogReqDTO req);
}
调用服务进行数据处理及新增数据库日志
package com.litian.dancechar.base.biz.oplog.controller;
import cn.hutool.core.util.StrUtil;
import com.litian.dancechar.base.biz.oplog.dto.SysOpLogReqDTO;
import com.litian.dancechar.base.biz.oplog.dto.SysOpLogRespDTO;
import com.litian.dancechar.base.biz.oplog.service.SysOpLogService;
import com.litian.dancechar.base.common.constants.CommConstants;
import com.litian.dancechar.framework.common.base.PageWrapperDTO;
import com.litian.dancechar.framework.common.base.RespResult;
import com.litian.dancechar.framework.common.util.DCBeanUtil;
import com.litian.dancechar.framework.es.util.ElasticsearchUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
/**
* 系统操作日志处理
*
* @author kyle0432
* @date 2024/03/03 16:26
*/
@Api(tags = "系统操作日志相关api")
@RestController
@Slf4j
@RequestMapping(value = "/sys/oplog/")
public class SysOpLogController {
@Resource
private SysOpLogService sysOpLogService;
@Resource
private ElasticsearchUtil elasticsearchUtil;
@ApiOperation(value = "通过es分页查询列表", notes = "通过es分页查询列表")
@PostMapping("listPagedByEs")
public RespResult<PageWrapperDTO<SysOpLogRespDTO>> listPagedByEs(@RequestBody SysOpLogReqDTO req) {
// 构造搜索条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchAllQuery());
if(StrUtil.isNotEmpty(req.getMenuName())){
// 模糊查询
sourceBuilder.query(QueryBuilders.multiMatchQuery(req.getMenuName(),"menuName"));
}
if(StrUtil.isNotEmpty(req.getOpAccount())){
// 精准查询
sourceBuilder.query(QueryBuilders.termsQuery(req.getOpContent(),"opContent"));
}
PageWrapperDTO<SysOpLogRespDTO> opLogList = elasticsearchUtil.queryListPageDataByCondition(
CommConstants.EsIndex.INDEX_SYS_OP_LOG, sourceBuilder, req.getPageSize(), req.getPageNo(),
"id,menuName,className", "updateDate", SortOrder.DESC, SysOpLogRespDTO.class);
return RespResult.success(opLogList);
}
@ApiOperation(value = "新增保存", notes = "新增保存")
@PostMapping("saveWithInsert")
public RespResult<Boolean> saveWithInsert(@RequestBody SysOpLogReqDTO req) {
log.info("新增保存数据....");
return sysOpLogService.saveWithInsert(req);
}
}
操作日志服务
package com.litian.dancechar.base.biz.oplog.service;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.PageHelper;
import com.litian.dancechar.base.biz.oplog.dao.entity.SysOpLogDO;
import com.litian.dancechar.base.biz.oplog.dao.inf.SysOpLogDao;
import com.litian.dancechar.base.biz.oplog.dto.SysOpLogReqDTO;
import com.litian.dancechar.base.biz.oplog.dto.SysOpLogRespDTO;
import com.litian.dancechar.framework.common.base.PageWrapperDTO;
import com.litian.dancechar.framework.common.base.RespResult;
import com.litian.dancechar.framework.common.util.DCBeanUtil;
import com.litian.dancechar.framework.common.util.PageResultUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
/**
* 操作日志服务
*
* @author kyle0432
* @date 2024/03/03 18:30
*/
@Service
@Slf4j
@Transactional(rollbackFor = Exception.class)
public class SysOpLogService extends ServiceImpl<SysOpLogDao, SysOpLogDO> {
@Resource
private SysOpLogDao sysOpLogDao;
/**
* 功能: 分页查询操作日志列表
*/
public RespResult<PageWrapperDTO<SysOpLogRespDTO>> listPaged(SysOpLogReqDTO req) {
PageHelper.startPage(req.getPageNo(), req.getPageSize());
PageWrapperDTO<SysOpLogRespDTO> pageCommon = new PageWrapperDTO<>();
PageResultUtil.setPageResult(sysOpLogDao.findList(req), pageCommon);
return RespResult.success(pageCommon);
}
/**
* 功能:查询操作日志信息
*/
public SysOpLogDO findById(String id) {
return sysOpLogDao.selectById(id);
}
/**
* 功能:新增操作日志
*/
public RespResult<Boolean> saveWithInsert(SysOpLogReqDTO sysOpLogReqDTO) {
if(StrUtil.isNotEmpty(sysOpLogReqDTO.getUrl()) && sysOpLogReqDTO.getUrl().length() > 500){
sysOpLogReqDTO.setUrl(sysOpLogReqDTO.getUrl().substring(0,500));
}
if(StrUtil.isNotEmpty(sysOpLogReqDTO.getParams()) && sysOpLogReqDTO.getParams().length() > 1024){
sysOpLogReqDTO.setParams(sysOpLogReqDTO.getParams().substring(0,1024));
}
if(StrUtil.isNotEmpty(sysOpLogReqDTO.getOpContent()) && sysOpLogReqDTO.getOpContent().length() > 1024){
sysOpLogReqDTO.setOpContent(sysOpLogReqDTO.getOpContent().substring(0,1024));
}
SysOpLogDO sysOpLogDO = new SysOpLogDO();
DCBeanUtil.copyNotNull(sysOpLogDO, sysOpLogReqDTO);
save(sysOpLogDO);
return RespResult.success(true);
}
}
安装一个canal服务端,并且配置mysql库,日志写入mysql,然后canal监听mysql并且解析binlog日志,将解析后的信息发送到kafka。
消费操作日志MQ,并且写入到ES
package com.litian.dancechar.canal.biz.oplog;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.otter.canal.protocol.FlatMessage;
import com.litian.dancechar.canal.common.constants.CommConstants;
import com.litian.dancechar.framework.es.util.ElasticsearchUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
/**
* 消费操作日志MQ
*
* @author kyle0432
* @date 2024/03/03 23:13
*/
@Slf4j
@Component
public class ConsumerSysOpLogMQMsg {
@Resource
private ElasticsearchUtil elasticsearchUtil;
@KafkaListener(groupId="dancechar-canal-data-service", topics = {CommConstants.KafkaTopic.TOPIC_SYS_OP_LOG})
public void consumerCustomer(ConsumerRecord<Integer, String> record) {
log.info("消费操作日志消息!topic:{},partition:{},value:{}",record.topic(),record.partition(), record.value());
FlatMessage flatMessage = JSONUtil.toBean(record.value(), FlatMessage.class);
if(ObjectUtil.isNotNull(flatMessage)){
String type = flatMessage.getType();
List<Map<String, String>> result = flatMessage.getData();
if(CollUtil.isNotEmpty(result)){
Map<String, String> dataMap = result.get(0);
SysOpLogDO sysOpLogDO = new SysOpLogDO();
BeanUtil.fillBeanWithMap(dataMap, sysOpLogDO, true);
dealData(type, sysOpLogDO);
}
}
}
private void dealData(String type, SysOpLogDO sysOpLogDO){
if("INSERT".equals(type)){
elasticsearchUtil.addData(sysOpLogDO, CommConstants.EsIndex.INDEX_SYS_OP_LOG,sysOpLogDO.getId());
log.info("新增记录成功!docId:{}", sysOpLogDO.getId());
}else if("UPDATE".equals(type)){
elasticsearchUtil.updateDataById(sysOpLogDO, CommConstants.EsIndex.INDEX_SYS_OP_LOG, sysOpLogDO.getId());
log.info("修改记录成功!docId:{}", sysOpLogDO.getId());
}else if("DELETE".equals(type)){
elasticsearchUtil.deleteDataById(CommConstants.EsIndex.INDEX_SYS_OP_LOG, sysOpLogDO.getId());
log.info("删除记录成功!docId:{}", sysOpLogDO.getId());
}
log.info("操作完成后查询es记录!{}", elasticsearchUtil.searchDataById(CommConstants.EsIndex.INDEX_SYS_OP_LOG, sysOpLogDO.getId()));
}
}
es工具类
package com.litian.dancechar.framework.es.util;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.convert.Convert;
import com.alibaba.fastjson2.JSON;
import com.google.common.collect.Lists;
import com.litian.dancechar.framework.common.base.PageWrapperDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Qualifier;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* es工具类
*
* @author kyle0432
* @date 2024/03/04 14:30
*/
@Slf4j
public class ElasticsearchUtil {
@Resource
@Qualifier("restHighLevelClient")
private RestHighLevelClient restHighLevelClient;
/**
* 获取低水平客户端
*/
public RestClient getLowLevelClient() {
return restHighLevelClient.getLowLevelClient();
}
/**
* 创建索引
* @param indexName 索引
* @return 结果
*/
public boolean createIndex(String indexName){
try{
if(isIndexExist(indexName)){
log.error("Index is exits!");
return false;
}
//1.创建索引请求
CreateIndexRequest request = new CreateIndexRequest(indexName);
//2.执行客户端请求
org.elasticsearch.client.indices.CreateIndexResponse response = restHighLevelClient.indices()
.create(request, RequestOptions.DEFAULT);
return response.isAcknowledged();
} catch (Exception e){
log.error(e.getMessage() ,e);
return false;
}
}
/**
* 判断索引是否存在
* @param indexName 索引
* @return 索引是否存在
*/
public boolean isIndexExist(String indexName) {
try{
GetIndexRequest request = new GetIndexRequest(indexName);
return restHighLevelClient.indices().exists(request, RequestOptions.DEFAULT);
}catch (Exception e){
log.error(e.getMessage(), e);
return false;
}
}
/**
* 删除索引
* @param indexName 索引名称
* @return 索引删除结果
*/
public boolean deleteIndex(String indexName) {
try{
if(!isIndexExist(indexName)) {
log.error("Index is not exits!");
return false;
}
DeleteIndexRequest request = new DeleteIndexRequest(indexName);
AcknowledgedResponse delete = restHighLevelClient.indices().delete(request, RequestOptions.DEFAULT);
return delete.isAcknowledged();
}catch (Exception e){
log.error(e.getMessage(), e);
return false;
}
}
/**
* 数据添加,自定义id
* @param object 要增加的数据
* @param index 索引,类似数据库
* @param id 数据ID,为null时es随机生成
* @return 返回添加数据结果
*/
public String addData(Object object, String index, String id) {
try{
//创建请求
IndexRequest request = new IndexRequest(index);
//规则 put /test_index/_doc/1
request.id(id);
request.timeout(TimeValue.timeValueSeconds(1));
//将数据放入请求 json
IndexRequest source = request.source(JSON.toJSONString(object), XContentType.JSON);
//客户端发送请求
IndexResponse response = restHighLevelClient.index(request, RequestOptions.DEFAULT);
return response.getId();
}catch (Exception e){
log.error(e.getMessage(), e);
return null;
}
}
/**
* 数据添加 随机id
*/
public String addData(Object object, String index) {
try{
return addData(object, index, UUID.randomUUID().toString()
.replaceAll("-", "").toUpperCase());
}catch (Exception e){
log.error(e.getMessage(), e);
return null;
}
}
/**
* 通过ID删除数据
*/
public boolean deleteDataById(String index,String id) {
try{
DeleteRequest request = new DeleteRequest(index, id);
DeleteResponse deleteResponse = restHighLevelClient.delete(request, RequestOptions.DEFAULT);
return deleteResponse.status().getStatus() == 200;
} catch (Exception e){
log.error(e.getMessage(), e);
return false;
}
}
/**
* 通过ID 更新数据
*/
public boolean updateDataById(Object object, String index, String id) {
try{
UpdateRequest update = new UpdateRequest(index, id);
update.timeout("1s");
update.doc(JSON.toJSONString(object), XContentType.JSON);
UpdateResponse updateResponse = restHighLevelClient.update(update, RequestOptions.DEFAULT);
return updateResponse.status().getStatus() == 200;
} catch (Exception e){
log.error(e.getMessage() ,e);
return false;
}
}
/**
* 通过ID获取数据
*/
public Map<String,Object> searchDataById(String index, String id) {
try {
GetRequest request = new GetRequest(index, id);
GetResponse response = restHighLevelClient.get(request, RequestOptions.DEFAULT);
return response.getSource();
}catch (Exception e){
log.error(e.getMessage(), e);
return null;
}
}
/**
* 通过ID获取数据
*/
public Map<String,Object> searchDataById(String index, String id, String fields) {
try {
GetRequest request = new GetRequest(index, id);
if (StringUtils.isNotEmpty(fields)){
//只查询特定字段。如果需要查询所有字段则不设置该项。
request.fetchSourceContext(new FetchSourceContext(true,fields.split(",")
, Strings.EMPTY_ARRAY));
}
GetResponse response = restHighLevelClient.get(request, RequestOptions.DEFAULT);
return response.getSource();
}catch (Exception e){
log.error(e.getMessage(), e);
return null;
}
}
/**
* 通过ID判断文档是否存在
*/
public boolean existsById(String index,String id) {
try{
GetRequest request = new GetRequest(index, id);
//不获取返回的_source的上下文
request.fetchSourceContext(new FetchSourceContext(false));
request.storedFields("_none_");
return restHighLevelClient.exists(request, RequestOptions.DEFAULT);
} catch (Exception e){
log.error(e.getMessage(), e);
return false;
}
}
/**
* 批量插入,true:成功 false: 失败
*/
public boolean bulkPost(String index, List<?> objects, List<String> ids) {
BulkRequest bulkRequest = new BulkRequest();
BulkResponse response=null;
//最大数量不得超过20万
for(int i=0;i<objects.size();i++){
IndexRequest request = new IndexRequest(index);
request.id(ids.get(i));
request.source(JSON.toJSONString(objects.get(i)), XContentType.JSON);
bulkRequest.add(request);
}
try {
response = restHighLevelClient.bulk(bulkRequest,RequestOptions.DEFAULT);
} catch (IOException e) {
log.error(e.getMessage(), e);
}
return !response.hasFailures();
}
/**
* 批量插入,true:成功 false: 失败
*/
public boolean bulkPost(String index, List<?> objects) {
BulkRequest bulkRequest = new BulkRequest();
BulkResponse response=null;
//最大数量不得超过20万
for (Object object: objects) {
IndexRequest request = new IndexRequest(index);
request.source(JSON.toJSONString(object), XContentType.JSON);
bulkRequest.add(request);
}
try {
response = restHighLevelClient.bulk(bulkRequest,RequestOptions.DEFAULT);
} catch (IOException e) {
log.error(e.getMessage(), e);
}
return !response.hasFailures();
}
/**
* 查询es列表(不分页)
* @param index 索引
* @param builder 查询条件
* SearchSourceBuilder sourceBuilder=new SearchSourceBuilder(); // 构造搜索条件
* sourceBuilder.query(QueryBuilders.matchAllQuery());
* sourceBuilder.query(QueryBuilders.multiMatchQuery("java","name")); // 模糊查询name属性中含有Java的
* sourceBuilder.query(QueryBuilders.termsQuery("10001","opContent")) // 精准查询opContent属性中包含10001的
* @param fields 返回的字段
* @param sortField 排序字段
* @param convertResultClass 返回结果对应的实体
* @return 查询的es列表
*/
public <T> List<T> queryListDataByCondition(String index, SearchSourceBuilder builder,
String fields, String sortField,
Class<T> convertResultClass) {
try {
SearchRequest request = new SearchRequest(index);
if (StringUtils.isNotEmpty(fields)){
// 只查询特定字段,如果需要查询所有字段则不设置该项
builder.fetchSource(
new FetchSourceContext(true,fields.split(","), Strings.EMPTY_ARRAY));
}
if (StringUtils.isNotEmpty(sortField)){
// 排序字段,注意如果proposal_no是text类型会默认带有keyword性质,需要拼接.keyword
builder.sort(sortField+".keyword", SortOrder.ASC);
}
request.source(builder);
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
if (response.status().getStatus() == 200) {
SearchHit[] hits = response.getHits().getHits();
if(hits != null && hits.length > 0){
List<T> result = new ArrayList<>();
for(SearchHit hit : hits){
Map<String,Object> source = hit.getSourceAsMap();
result.add(BeanUtil.mapToBean(source, convertResultClass, true));
}
return result;
}
}
return null;
} catch (Exception e){
log.error(e.getMessage(), e);
return null;
}
}
/**
* 查询es列表(分页不带高亮)
* @param index 索引名称
* @param query 查询条件
* @param pageSize 每页多少条
* @param pageNo 第几页
* @param fields 需要显示的字段,逗号分隔(缺省为全部字段)
* @param sortField 排序字段
* @param convertResultClass 返回结果对应的实体
* @return es列表
*/
public <T> PageWrapperDTO<T> queryListPageDataByCondition(String index, SearchSourceBuilder query, Integer pageSize,
Integer pageNo, String fields, String sortField, SortOrder sortOrder, Class<T> convertResultClass) {
try {
SearchRequest request = new SearchRequest(index);
SearchSourceBuilder builder = query;
if (StringUtils.isNotEmpty(fields)){
// 只查询特定字段。如果需要查询所有字段则不设置该项。
builder.fetchSource(new FetchSourceContext(true,fields.split(","),
Strings.EMPTY_ARRAY));
}
int size = (pageSize == null ? 10 : pageSize);
int from = (pageNo == null || pageNo == 0 ? 0 : (pageNo-1) * size);
builder.from(from).size(size);
if (StringUtils.isNotEmpty(sortField)){
// 排序字段,注意如果proposal_no是text类型会默认带有keyword性质,需要拼接.keyword
builder.sort(sortField+".keyword", sortOrder);
}
request.source(builder);
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
if (response.status().getStatus() == 200) {
SearchHits searchHits = response.getHits();
SearchHit[] hits = searchHits.getHits();
if(hits != null && hits.length > 0){
PageWrapperDTO<T> pageWrapperDTO = new PageWrapperDTO<>();
List<T> result = Lists.newArrayList();
for(SearchHit hit : hits){
Map<String,Object> source = hit.getSourceAsMap();
result.add(BeanUtil.mapToBean(source, convertResultClass, true));
}
pageWrapperDTO.setList(result);
pageWrapperDTO.setTotal(Convert.toInt(searchHits.getTotalHits().value));
return pageWrapperDTO;
}
}
return null;
} catch (Exception e){
log.error(e.getMessage(), e);
return null;
}
}
}
六、结果演示
产生日志前数据库数据
调用查询活动详情接口
数据库产生查看活动详情的日志记录
再次操作一下修改活动操作
最后根据es查询日志列表