目录
- 一、前言
- 二、基础环境
- 1. 导入依赖
- 2. 编写yml配置
- 三、数据库设计
- 四、主要功能
- 1. 编写注解
- 2. 业务类型枚举
- 3. 编写切片
- 4. ip工具类
- 5. 事件发布
- 6. 监听者
- 五、测试
- 1. controller
- 2. service
- 3. dao
- 4. 测试
- 5. 数据库
- 六、总结
一、前言
我们在企业级的开发中,必不可少的是对日志的记录,实现有很多种方式,常见的就是基于AOP+注解进行保存,同时考虑到程序的流畅和效率,我们可以使用异步进行保存!
二、基础环境
1. 导入依赖
我这里的springboot版本是:2.7.4
<project xmlns="http://maven.apache.org/POM/.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
xsi:schemaLocation="http://maven.apache.org/POM/.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | |
<modelVersion>.0.0</modelVersion> | |
<parent> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-parent</artifactId> | |
<version>.7.4</version> | |
<relativePath/> <!-- lookup parent from repository --> | |
</parent> | |
<groupId>com.example</groupId> | |
<artifactId>springboot-log</artifactId> | |
<version>.0.1-SNAPSHOT</version> | |
<name>springboot-log</name> | |
<description>springboot-log 日志 Demo</description> | |
<properties> | |
<java.version>.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>.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.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>.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: 8080
spring:
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
三、数据库设计
数据库保存日志表的设计,这里一切从简,一般日志多的后期会进行分库分表,或者搭配ELK进行分析,分库分表一般采用根据方法类型!
DROP TABLE IF EXISTS `sys_log`; | |
CREATE TABLE `sys_log` ( | |
`id` bigint() NOT NULL AUTO_INCREMENT COMMENT '日志主键', | |
`title` varchar() CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '模块标题', | |
`business_type` int() NULL DEFAULT 0 COMMENT '业务类型(0其它 1新增 2修改 3删除)', | |
`method` varchar() CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '方法名称', | |
`request_method` varchar() CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '请求方式', | |
`oper_name` varchar() CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '操作人员', | |
`oper_url` varchar() CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '请求URL', | |
`oper_ip` varchar() CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '主机地址', | |
`oper_time` datetime() NULL DEFAULT NULL COMMENT '操作时间', | |
PRIMARY KEY (`id`) USING BTREE | |
) ENGINE = InnoDB AUTO_INCREMENT = CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '操作日志记录' ROW_FORMAT = Dynamic; | |
SET FOREIGN_KEY_CHECKS =; |
实体类:
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)实体类 | |
* | |
* @author qrxm | |
* @since-03-26 02:09:54 | |
*/ | |
"sys_log") | (|
public class SysLog implements Serializable { | |
private static final long serialVersionUID =L; | |
/** | |
* 日志主键 | |
*/ | |
private Long id; | |
/** | |
* 模块标题 | |
*/ | |
private String title; | |
/** | |
* 业务类型(其它 1新增 2修改 3删除) | |
*/ | |
private Integer businessType; | |
/** | |
* 方法名称 | |
*/ | |
private String method; | |
/** | |
* 请求方式 | |
*/ | |
private String requestMethod; | |
/** | |
* 操作人员 | |
*/ | |
private String operName; | |
/** | |
* 请求URL | |
*/ | |
private String operUrl; | |
/** | |
* 主机地址 | |
*/ | |
private String operIp; | |
/** | |
* 操作时间 | |
*/ | |
= "yyyy-MM-dd HH:mm:ss") | (pattern|
private Date operTime; | |
} |
四、主要功能
大体思路:
- 先手写一个注解
- 切面来进行获取要保存的数据
- 一个发布者来发布要保存的数据
- 一个监听者监听后保存(异步)
完整项目架构图如下:
1. 编写注解
package com.example.springbootlog.annotation; | |
import com.example.springbootlog.constant.BusinessTypeEnum; | |
import java.lang.annotation.*; | |
/** | |
* 自定义操作日志记录注解 | |
* @author qrxm | |
*/ | |
// 注解只能用于方法 | |
// 修饰注解的生命周期 | |
public Log { | |
String value() default ""; | |
/** | |
* 模块 | |
*/ | |
String title() default "测试模块"; | |
/** | |
* 方法名称 | |
*/ | |
String method() default "测试方法"; | |
/** | |
* 功能 | |
*/ | |
BusinessTypeEnum businessType() default BusinessTypeEnum.OTHER; | |
} |
2. 业务类型枚举
package com.example.springbootlog.constant; | |
public enum BusinessTypeEnum { | |
/** | |
* 其它 | |
*/ | |
OTHER(,"其它"), | |
/** | |
* 新增 | |
*/ | |
INSERT(,"新增"), | |
/** | |
* 修改 | |
*/ | |
UPDATE(,"修改"), | |
/** | |
* 删除 | |
*/ | |
DELETE(,"删除"); | |
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. 编写切片
这里是以切片后进行发起的,当然规范流程是要加异常后的切片,这里以最简单的进行测试哈,大家按需进行添加!!
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.slfj.Logger; | |
import org.slfj.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; | |
public class SysLogAspect { | |
private final Logger logger = LoggerFactory.getLogger(SysLogAspect.class); | |
private EventPubListener eventPubListener; | |
/** | |
* 以注解所标注的方法作为切入点 | |
*/ | |
public void sysLog() {} | |
/** | |
* 在切点之后织入 | |
* @throws Throwable | |
*/ | |
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()); | |
// 发布消息 | |
eventPubListener.pushListener(sysLog); | |
logger.info("=======日志发送成功,内容:{}",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() == || "unknown".equalsIgnoreCase(ip)) { | |
ip = request.getHeader("Proxy-Client-IP"); | |
} | |
if (ip == null || ip.length() == || "unknown".equalsIgnoreCase(ip)) { | |
ip = request.getHeader("X-Forwarded-For"); | |
} | |
if (ip == null || ip.length() == || "unknown".equalsIgnoreCase(ip)) { | |
ip = request.getHeader("WL-Proxy-Client-IP"); | |
} | |
if (ip == null || ip.length() == || "unknown".equalsIgnoreCase(ip)) { | |
ip = request.getHeader("X-Real-IP"); | |
} | |
if (ip == null || ip.length() == || "unknown".equalsIgnoreCase(ip)) { | |
ip = request.getRemoteAddr(); | |
} | |
return ":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(",") >) { | |
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); | |
} | |
} |
5. 事件发布
事件发布是由ApplicationContext对象进行发布的,直接注入使用即可!
使用观察者模式的目的:为了业务逻辑之间的解耦,提高可扩展性。
这种模式在spring和springboot底层是经常出现的,大家可以去看看。
发布者只需要关注发布消息,监听者只需要监听自己需要的,不管谁发的,符合自己监听条件即可。
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; | |
public class EventPubListener { | |
private ApplicationContext applicationContext; | |
/** | |
* 事件发布方法 | |
* @param sysLogEvent | |
*/ | |
public void pushListener(SysLog sysLogEvent) { | |
applicationContext.publishEvent(sysLogEvent); | |
} | |
} |
6. 监听者
@Async:单独开启一个新线程去保存,提高效率!
@EventListener:监听
package com.example.springbootlog.listener; | |
import com.example.springbootlog.entity.SysLog; | |
import com.example.springbootlog.service.SysLogService; | |
import lombok.extern.slfj.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; | |
public class MyEventListener { | |
private SysLogService sysLogService; | |
// 开启异步 | |
// 开启监听 | |
public void saveSysLog(SysLog event) { | |
log.info("=====即将异步保存到数据库======"); | |
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.slfj.Slf4j; | |
import org.springframework.web.bind.annotation.*; | |
/** | |
* 操作日志记录(SysLog)表控制层 | |
* | |
*/ | |
public class SysLogController { | |
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)表服务接口 | |
*/ | |
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)表服务实现类 | |
*/ | |
public class SysLogServiceImpl extends ServiceImpl<SysLogDao, SysLog> implements SysLogService { | |
private SysLogDao sysLogDao; | |
public Integer saveLog(SysLog sysLog) { | |
return sysLogDao.insert(sysLog); | |
} | |
} |
3. dao
这里使用mybatis-plus进行保存
package com.example.springbootlog.dao; | |
import com.example.springbootlog.entity.SysLog; | |
import com.baomidou.mybatisplus.core.mapper.BaseMapper; | |
/** | |
* 操作日志记录(SysLog)表数据库访问层 | |
*/ | |
public interface SysLogDao extends BaseMapper<SysLog> { | |
} |
4. 测试
访问地址:http://localhost:8080/sysLog/saveLog
5. 数据库
六、总结
这个实战在企业级必不可少的,每个项目搭建人不同,但是结果都是一样的,保存日志到数据,这样可以进行按钮的点击进行统计,分析那个功能是否经常使用,那些东西需要优化。只要是有数据的东西,分析一下总会有收获的!后面日志多了就行分库分表,ELK搭建。
代码已上传到 Gitee 上面了,地址:https://gitee.com/afoams/springboot-log