SpringBoot通過自定義注解與異步來管理日志流程
一、前言
我們在企業(yè)級的開發(fā)中,必不可少的是對日志的記錄,實(shí)現(xiàn)有很多種方式,常見的就是基于AOP+注解進(jìn)行保存,同時考慮到程序的流暢和效率,我們可以使用異步進(jìn)行保存!
二、基礎(chǔ)環(huán)境
1. 導(dǎo)入依賴
我這里的springboot版本是:2.7.4
<?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.7.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springboot-log</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-log</name>
<description>springboot-log 日志 Demo</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.16</version>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. 編寫yml配置
server:
port: 8080spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/admin?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
druid:
initial-size: 5
max-active: 100
min-idle: 5
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 30000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: true
test-on-return: false
三、數(shù)據(jù)庫設(shè)計(jì)
數(shù)據(jù)庫保存日志表的設(shè)計(jì),這里一切從簡,一般日志多的后期會進(jìn)行分庫分表,或者搭配ELK進(jìn)行分析,分庫分表一般采用根據(jù)方法類型!
DROP TABLE IF EXISTS `sys_log`; CREATE TABLE `sys_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志主鍵', `title` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '模塊標(biāo)題', `business_type` int(2) NULL DEFAULT 0 COMMENT '業(yè)務(wù)類型(0其它 1新增 2修改 3刪除)', `method` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '方法名稱', `request_method` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '請求方式', `oper_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '操作人員', `oper_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '請求URL', `oper_ip` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '主機(jī)地址', `oper_time` datetime(0) NULL DEFAULT NULL COMMENT '操作時間', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1585197503834284034 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '操作日志記錄' ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;
實(shí)體類:
package com.example.springbootlog.entity;
import java.util.Date;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.ToString;
/**
* 操作日志記錄(SysLog)實(shí)體類
*
* @author qrxm
* @since 2023-03-26 02:09:54
*/
@Data
@ToString
@TableName("sys_log")
public class SysLog implements Serializable {
private static final long serialVersionUID = 811241510868515068L;
/**
* 日志主鍵
*/
@TableId
private Long id;
/**
* 模塊標(biāo)題
*/
private String title;
/**
* 業(yè)務(wù)類型(0其它 1新增 2修改 3刪除)
*/
private Integer businessType;
/**
* 方法名稱
*/
private String method;
/**
* 請求方式
*/
private String requestMethod;
/**
* 操作人員
*/
private String operName;
/**
* 請求URL
*/
private String operUrl;
/**
* 主機(jī)地址
*/
private String operIp;
/**
* 操作時間
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date operTime;
}
四、主要功能
大體思路:
- 先手寫一個注解
- 切面來進(jìn)行獲取要保存的數(shù)據(jù)
- 一個發(fā)布者來發(fā)布要保存的數(shù)據(jù)
- 一個監(jiān)聽者監(jiān)聽后保存(異步)
完整項(xiàng)目架構(gòu)圖如下:

1. 編寫注解
package com.example.springbootlog.annotation;
import com.example.springbootlog.constant.BusinessTypeEnum;
import java.lang.annotation.*;
/**
* 自定義操作日志記錄注解
* @author qrxm
*/
@Target(ElementType.METHOD) // 注解只能用于方法
@Retention(RetentionPolicy.RUNTIME) // 修飾注解的生命周期
@Documented
public @interface Log {
String value() default "";
/**
* 模塊
*/
String title() default "測試模塊";
/**
* 方法名稱
*/
String method() default "測試方法";
/**
* 功能
*/
BusinessTypeEnum businessType() default BusinessTypeEnum.OTHER;
}
2. 業(yè)務(wù)類型枚舉
package com.example.springbootlog.constant;
public enum BusinessTypeEnum {
/**
* 其它
*/
OTHER(0,"其它"),
/**
* 新增
*/
INSERT(1,"新增"),
/**
* 修改
*/
UPDATE(2,"修改"),
/**
* 刪除
*/
DELETE(3,"刪除");
private Integer code;
private String message;
BusinessTypeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
3. 編寫切片
這里是以切片后進(jìn)行發(fā)起的,當(dāng)然規(guī)范流程是要加異常后的切片,這里以最簡單的進(jìn)行測試哈,大家按需進(jìn)行添加?。?/p>
package com.example.springbootlog.aspect;
import com.example.springbootlog.annotation.Log;
import com.example.springbootlog.entity.SysLog;
import com.example.springbootlog.listener.EventPubListener;
import com.example.springbootlog.utils.IpUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
@Aspect
@Component
public class SysLogAspect {
private final Logger logger = LoggerFactory.getLogger(SysLogAspect.class);
@Autowired
private EventPubListener eventPubListener;
/**
* 以注解所標(biāo)注的方法作為切入點(diǎn)
*/
@Pointcut("@annotation(com.example.springbootlog.annotation.Log)")
public void sysLog() {}
/**
* 在切點(diǎn)之后織入
* @throws Throwable
*/
@After("sysLog()")
public void doAfter(JoinPoint joinPoint) {
Log log = ((MethodSignature) joinPoint.getSignature()).getMethod()
.getAnnotation(Log.class);
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String method = request.getMethod();
String url = request.getRequestURL().toString();
String ip = IpUtils.getIpAddr(request);
SysLog sysLog = new SysLog();
sysLog.setBusinessType(log.businessType().getCode());
sysLog.setTitle(log.title());
sysLog.setMethod(log.method());
sysLog.setRequestMethod(method);
sysLog.setOperIp(ip);
sysLog.setOperUrl(url);
// 從登錄中token獲取登錄人員信息即可
sysLog.setOperName("我是測試人員");
sysLog.setOperTime(new Date());
// 發(fā)布消息
eventPubListener.pushListener(sysLog);
logger.info("=======日志發(fā)送成功,內(nèi)容:{}",sysLog);
}
}
4. ip工具類
package com.example.springbootlog.utils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import javax.servlet.http.HttpServletRequest;
public class IpUtils {
/**
* 獲取客戶端IP
*
* @param request 請求對象
* @return IP地址
*/
public static String getIpAddr(HttpServletRequest request) {
if (request == null) {
return "unknown";
}
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Forwarded-For");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip);
}
/**
* 從多級反向代理中獲得第一個非unknown IP地址
*
* @param ip 獲得的IP地址
* @return 第一個非unknown IP地址
*/
public static String getMultistageReverseProxyIp(String ip) {
// 多級反向代理檢測
if (ip != null && ip.indexOf(",") > 0) {
final String[] ips = ip.trim().split(",");
for (String subIp : ips) {
if (false == isUnknown(subIp)) {
ip = subIp;
break;
}
}
}
return ip;
}
/**
* 檢測給定字符串是否為未知,多用于檢測HTTP請求相關(guān)
*
* @param checkString 被檢測的字符串
* @return 是否未知
*/
public static boolean isUnknown(String checkString) {
return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString);
}
}
5. 事件發(fā)布
事件發(fā)布是由ApplicationContext對象進(jìn)行發(fā)布的,直接注入使用即可!
使用觀察者模式的目的:為了業(yè)務(wù)邏輯之間的解耦,提高可擴(kuò)展性。
這種模式在spring和springboot底層是經(jīng)常出現(xiàn)的,大家可以去看看。
發(fā)布者只需要關(guān)注發(fā)布消息,監(jiān)聽者只需要監(jiān)聽自己需要的,不管誰發(fā)的,符合自己監(jiān)聽條件即可。
package com.example.springbootlog.listener;
import com.example.springbootlog.entity.SysLog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
@Component
public class EventPubListener {
@Autowired
private ApplicationContext applicationContext;
/**
* 事件發(fā)布方法
* @param sysLogEvent
*/
public void pushListener(SysLog sysLogEvent) {
applicationContext.publishEvent(sysLogEvent);
}
}6. 監(jiān)聽者
@Async:單獨(dú)開啟一個新線程去保存,提高效率!
@EventListener:監(jiān)聽
package com.example.springbootlog.listener;
import com.example.springbootlog.entity.SysLog;
import com.example.springbootlog.service.SysLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class MyEventListener {
@Autowired
private SysLogService sysLogService;
// 開啟異步
@Async
// 開啟監(jiān)聽
@EventListener(SysLog.class)
public void saveSysLog(SysLog event) {
log.info("=====即將異步保存到數(shù)據(jù)庫======");
sysLogService.saveLog(event);
}
}
五、測試
1. controller
package com.example.springbootlog.controller;
import com.example.springbootlog.annotation.Log;
import com.example.springbootlog.constant.BusinessTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* 操作日志記錄(SysLog)表控制層
*
*/
@Slf4j
@RestController
@RequestMapping("sysLog")
public class SysLogController {
@Log(method = "測試添加方法", title = "測試呢", businessType = BusinessTypeEnum.INSERT)
@GetMapping("/saveLog")
public void saveLog() {
log.info("我就是來測試一下是否成功!");
}
}
2. service
package com.example.springbootlog.service;
import com.example.springbootlog.entity.SysLog;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* 操作日志記錄(SysLog)表服務(wù)接口
*/
public interface SysLogService extends IService<SysLog> {
Integer saveLog(SysLog sysLog);
}
package com.example.springbootlog.service.impl;
import com.example.springbootlog.entity.SysLog;
import com.example.springbootlog.service.SysLogService;
import com.example.springbootlog.dao.SysLogDao;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 操作日志記錄(SysLog)表服務(wù)實(shí)現(xiàn)類
*/
@Service("sysLogService")
public class SysLogServiceImpl extends ServiceImpl<SysLogDao, SysLog> implements SysLogService {
@Autowired
private SysLogDao sysLogDao;
@Override
public Integer saveLog(SysLog sysLog) {
return sysLogDao.insert(sysLog);
}
}
3. dao
這里使用mybatis-plus進(jìn)行保存
package com.example.springbootlog.dao;
import com.example.springbootlog.entity.SysLog;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* 操作日志記錄(SysLog)表數(shù)據(jù)庫訪問層
*/
public interface SysLogDao extends BaseMapper<SysLog> {
}
4. 測試
訪問地址:http://localhost:8080/sysLog/saveLog

5. 數(shù)據(jù)庫

六、總結(jié)
這個實(shí)戰(zhàn)在企業(yè)級必不可少的,每個項(xiàng)目搭建人不同,但是結(jié)果都是一樣的,保存日志到數(shù)據(jù),這樣可以進(jìn)行按鈕的點(diǎn)擊進(jìn)行統(tǒng)計(jì),分析那個功能是否經(jīng)常使用,那些東西需要優(yōu)化。只要是有數(shù)據(jù)的東西,分析一下總會有收獲的!后面日志多了就行分庫分表,ELK搭建。
代碼已上傳到 Gitee 上面了,地址:https://gitee.com/afoams/springboot-log
到此這篇關(guān)于SpringBoot通過自定義注解與異步來管理日志流程的文章就介紹到這了,更多相關(guān)SpringBoot自定義注解內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java求s=a+aa+aaa+aaaa+aa...a 5個數(shù)相加的值
求s=a+aa+aaa+aaaa+aa...a的值,其中a是一個數(shù)字。例如2+22+222+2222+22222(此時共有5個數(shù)相加),幾個數(shù)相加有鍵盤控制2017-02-02
海量數(shù)據(jù)去重排序bitmap(位圖法)在java中實(shí)現(xiàn)的兩種方法
今天小編就為大家分享一篇關(guān)于海量數(shù)據(jù)去重排序bitmap(位圖法)在java中實(shí)現(xiàn)的兩種方法,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-02-02
Android中幾種圖片特效的處理的實(shí)現(xiàn)方法
這篇文章主要介紹了 Android中幾種圖片特效的處理的實(shí)現(xiàn)方法的相關(guān)資料,這里有放大縮小圖片,獲得圓角圖片,獲得帶倒影圖片的幾種方法,需要的朋友可以參考下2017-08-08
JAVA8 List<List<Integer>> list中再裝一個list轉(zhuǎn)成一個list操
這篇文章主要介紹了JAVA8 List<List<Integer>> list中再裝一個list轉(zhuǎn)成一個list操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08
一文詳解SpringMVC中的@RequestMapping注解
@RequestMapping是一個用于映射HTTP請求到處理方法的注解,在Spring框架中使用,它可以用于控制器類和處理方法上,用來指定處理不同URL路徑的請求,并定義請求的方法等,本文小編將給大家詳細(xì)的介紹一下SpringMVC中的@RequestMapping注解,需要的朋友可以參考下2023-08-08

