我們在企業級的開發中,必不可少的是對紀錄檔的記錄,實現有很多種方式,常見的就是基於AOP+註解
進行儲存,但是考慮到程式的流暢和效率,我們可以使用非同步
進行儲存,小編最近在spring和springboot
原始碼中看到有很多的監聽處理貫穿前後:這就是著名的觀察者模式
!!
專案這裡小編就不帶大家建立了,直接開始!!
小編這裡的springboot版本是:2.7.4
<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>
server:
port: 8088
spring:
datasource:
#使用阿里的Druid
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.239.131:3306/test?serverTimezone=UTC
username: root
password: root
資料庫儲存紀錄檔表的設計,小編一切從簡,一般紀錄檔多的後期會進行分庫分表,或者搭配ELK
進行分析,分庫分表一般採用根據方法型別,這需要開發人員遵循rest風格
,不然肯定都是post
,純屬個人見解哈!!大家可以根據自己的公司的要求進行補充哈!!
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 '模組標題',
`business_type` int(2) NULL DEFAULT 0 COMMENT '業務型別(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 '主機地址',
`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;
實體類:
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 操作紀錄檔記錄表 sys_log
*
*/
@Data
@TableName("sys_log")
public class SysLog {
private static final long serialVersionUID = 1L;
/**
* 紀錄檔主鍵
*/
@TableId
private Long id;
/**
* 操作模組
*/
private String title;
/**
* 業務型別(0其它 1新增 2修改 3刪除)
*/
private Integer businessType;
/**
* 請求方式
*/
private String requestMethod;
/**
* 操作人員
*/
private String operName;
/**
* 請求url
*/
private String operUrl;
/**
* 操作地址
*/
private String operIp;
/**
* 操作時間
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime operTime;
}
大體思路:
先手寫一個註解--->切面來進行獲取要儲存的資料--->一個釋出者來發布要儲存的資料--->一個監聽者監聽後儲存(非同步)
完整專案架構圖如下:
import com.example.demo.constant.BusinessTypeEnum;
import java.lang.annotation.*;
/**
* 自定義操作紀錄檔記錄註解
* @author wangzhenjun
* @date 2022/10/26 15:37
*/
@Target(ElementType.METHOD) // 註解只能用於方法
@Retention(RetentionPolicy.RUNTIME) // 修飾註解的生命週期
@Documented
public @interface Log {
String value() default "";
/**
* 模組
*/
String title() default "測試模組";
/**
* 功能
*/
BusinessTypeEnum businessType() default BusinessTypeEnum.OTHER;
}
/**
* @author wangzhenjun
* @date 2022/10/26 11:22
*/
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;
}
}
這裡小編是以切片後進行發起的,當然規範流程是要加異常後的切片,這裡以最簡單的進行測試哈,大家按需進行新增!!
import com.example.demo.annotation.Log;
import com.example.demo.entity.SysLog;
import com.example.demo.listener.EventPubListener;
import com.example.demo.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.time.LocalDateTime;
/**
* @author wangzhenjun
* @date 2022/10/26 15:39
*/
@Aspect
@Component
public class SysLogAspect {
private final Logger logger = LoggerFactory.getLogger(SysLogAspect.class);
@Autowired
private EventPubListener eventPubListener;
/**
* 以註解所標註的方法作為切入點
*/
@Pointcut("@annotation(com.example.demo.annotation.Log)")
public void sysLog() {}
/**
* 在切點之後織入
* @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.setRequestMethod(method);
sysLog.setOperIp(ip);
sysLog.setOperUrl(url);
// 從登入中token獲取登入人員資訊即可
sysLog.setOperName("我是測試人員");
sysLog.setOperTime(LocalDateTime.now());
// 釋出訊息
eventPubListener.pushListener(sysLog);
logger.info("=======紀錄檔傳送成功,內容:{}",sysLog);
}
}
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import javax.servlet.http.HttpServletRequest;
/**
* @author wangzhenjun
* @date 2022/10/26 16:27
* 獲取IP方法
*
* @author jw
*/
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請求相關
*
* @param checkString 被檢測的字串
* @return 是否未知
*/
public static boolean isUnknown(String checkString) {
return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString);
}
}
事件釋出是由ApplicationContext物件進行釋出的,直接注入使用即可!
使用觀察者模式的目的:為了業務邏輯之間的解耦
,提高可延伸性
。
這種模式在spring和springboot底層是經常出現的,大家可以去看看。
釋出者只需要關注釋出訊息,監聽者只需要監聽自己需要的,不管誰發的,符合自己監聽條件即可。
import com.example.demo.entity.SysLog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author wangzhenjun
* @date 2022/10/26 16:38
*/
@Component
public class EventPubListener {
@Autowired
private ApplicationContext applicationContext;
// 事件釋出方法
public void pushListener(SysLog sysLogEvent) {
applicationContext.publishEvent(sysLogEvent);
}
}
@Async
:單獨開啟一個新執行緒去儲存,提高效率!
@EventListener
:監聽
/**
* @author wangzhenjun
* @date 2022/10/25 15:22
*/
@Slf4j
@Component
public class MyEventListener {
@Autowired
private TestService testService;
// 開啟非同步
@Async
// 開啟監聽
@EventListener(SysLog.class)
public void saveSysLog(SysLog event) {
log.info("=====即將非同步儲存到資料庫======");
testService.saveLog(event);
}
}
/**
* @author wangzhenjun
* @date 2022/10/26 16:51
*/
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
@Log(title = "測試呢",businessType = BusinessTypeEnum.INSERT)
@GetMapping("/saveLog")
public void saveLog(){
log.info("我就是來測試一下是否成功!");
}
}
/**
* @author wangzhenjun
* @date 2022/10/26 16:55
*/
public interface TestService {
int saveLog(SysLog sysLog);
}
/**
* @author wangzhenjun
* @date 2022/10/26 16:56
*/
@Service
public class TestServiceImpl implements TestService {
@Autowired
private TestMapper testMapper;
@Override
public int saveLog(SysLog sysLog) {
return testMapper.insert(sysLog);
}
}
這裡使用mybatis-plus進行儲存
/**
* @author wangzhenjun
* @date 2022/10/26 17:07
*/
public interface TestMapper extends BaseMapper<SysLog> {
}
鐺鐺鐺,終於完成了!這個實戰在企業級必不可少的,每個專案搭建人不同,但是結果都是一樣的,儲存紀錄檔到資料,這樣可以進行按鈕的點選進行統計,分析那個功能是否經常使用,那些東西需要優化。只要是有資料的東西,分析一下總會有收穫的!後面紀錄檔多了就行分庫分表,ELK搭建。知道的越多不知道的就越多,這一次下來,知道下面要學什麼了嘛!!