1. 简介
项目中对日志的收集往往是非常重要的,不仅方便开发人员快速定位问题,而且越来越多的客户需要查询用户行为日志、用户审计日志等。因此,在收集日志时,不仅要考虑功能实现,而且要考虑可靠性、稳定性和不耦合性。
在每个操作和每个方法都加上日志处理肯定时不现实的,因此使用Spring提供的AOP原理就变得非常方便。定义好切面以及切点之后,可以非常方便的打印、收集或保存日志,不影响业务性能。
2. 初始化数据库
创建数据库aop
,并初始化表结构:
DROP TABLE IF EXISTS `sys_log`;
CREATE TABLE `sys_log` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
`log_type` varchar(50) NOT NULL COMMENT '日志类型',
`create_user_code` varchar(64) NOT NULL COMMENT '创建用户编码',
`create_user_name` varchar(100) NOT NULL COMMENT '创建用户名称',
`create_date` datetime NOT NULL COMMENT '创建时间',
`request_uri` varchar(500) DEFAULT NULL COMMENT '请求URI',
`request_method` varchar(10) DEFAULT NULL COMMENT '请求方式',
`request_params` text COMMENT '请求参数',
`request_ip` varchar(20) NOT NULL COMMENT '请求IP',
`server_address` varchar(50) NOT NULL COMMENT '请求服务器地址',
`is_exception` char(1) DEFAULT NULL COMMENT '是否异常',
`exception_info` text COMMENT '异常信息',
`start_time` datetime NOT NULL COMMENT '开始时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
`execute_time` int DEFAULT NULL COMMENT '执行时间',
`user_agent` varchar(500) DEFAULT NULL COMMENT '用户代理',
`device_name` varchar(100) DEFAULT NULL COMMENT '操作系统',
`browser_name` varchar(100) DEFAULT NULL COMMENT '浏览器名称',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_sys_log_lt` (`log_type`) USING BTREE,
KEY `idx_sys_log_cub` (`create_user_code`) USING BTREE,
KEY `idx_sys_log_ie` (`is_exception`) USING BTREE,
KEY `idx_sys_log_cd` (`create_date`) USING BTREE
) COMMENT='系统日志表';
3. 示例代码
4.0.0
com.c3stones
spring-aop-log-demo
0.0.1-SNAPSHOT
spring-aop-log-demo
Spring Aop Log Demo
org.springframework.boot
spring-boot-starter-parent
2.3.4.RELEASE
mysql
mysql-connector-java
com.baomidou
mybatis-plus-boot-starter
3.3.2
org.projectlombok
lombok
true
cn.hutool
hutool-all
5.5.1
org.springframework.boot
spring-boot-starter-aop
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/aop?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull
username: root
password: 123456
# Mybatis-plus配置
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
global-config:
db-config:
id-type: AUTO
# configuration:
# # 打印sql
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
/**
* 公共常量类
*
* @author CL
*/
public interface Global {
/**
* 成功标识
*/
Boolean TRUE = true;
/**
* 失败标识
*/
Boolean FALSE = false;
/**
* 是标识
*/
String YES = "1";
/**
* 否标识
*/
String NO = "0";
/**
* 日志级别-INFO
*/
String LOG_INGO = "INFO";
/**
* 日志级别-DEBUG
*/
String LOG_DEBUG = "DEBUG";
/**
* 日志级别-ERROR
*/
String LOG_ERROR = "ERROR";
}
import java.io.Serializable;
import com.c3stones.constants.Global;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
/**
* 响应工具类
*
* @param
* @author CL
*/
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class R implements Serializable {
private static final long serialVersionUID = 1L;
@Getter
@Setter
private Boolean code;
@Getter
@Setter
private String msg;
@Getter
@Setter
private T data;
public static R ok() {
return restResult(null, Global.TRUE, null);
}
public static R ok(T data) {
return restResult(data, Global.TRUE, null);
}
public static R ok(T data, String msg) {
return restResult(data, Global.TRUE, msg);
}
public static R failed() {
return restResult(null, Global.FALSE, null);
}
public static R failed(String msg) {
return restResult(null, Global.FALSE, msg);
}
public static R failed(T data) {
return restResult(data, Global.FALSE, null);
}
public static R failed(T data, String msg) {
return restResult(data, Global.FALSE, msg);
}
private static R restResult(T data, Boolean code, String msg) {
R apiResult = new R<>();
apiResult.setCode(code);
apiResult.setData(data);
apiResult.setMsg(msg);
return apiResult;
}
}
/**
* 字节转换工具类
*
* @author CL
*/
public class ByteUtils {
private static final int UNIT = 1024;
/**
* 格式化字节大小
*
* @param byteSize 字节大小
* @return
*/
public static String formatByteSize(long byteSize) {
if (byteSize <= -1) {
return String.valueOf(byteSize);
}
double size = 1.0 * byteSize;
String type = "B";
if ((int) Math.floor(size / UNIT) <= 0) { // 不足1KB
type = "B";
return format(size, type);
}
size = size / UNIT;
if ((int) Math.floor(size / UNIT) <= 0) { // 不足1MB
type = "KB";
return format(size, type);
}
size = size / UNIT;
if ((int) Math.floor(size / UNIT) <= 0) { // 不足1GB
type = "MB";
return format(size, type);
}
size = size / UNIT;
if ((int) Math.floor(size / UNIT) <= 0) { // 不足1TB
type = "GB";
return format(size, type);
}
size = size / UNIT;
if ((int) Math.floor(size / UNIT) <= 0) { // 不足1PB
type = "TB";
return format(size, type);
}
size = size / UNIT;
if ((int) Math.floor(size / UNIT) <= 0) {
type = "PB";
return format(size, type);
}
return ">PB";
}
/**
* 格式化字节大小为指定单位
*
* @param size 字节大小
* @param type 单位类型
* @return
*/
private static String format(double size, String type) {
int precision = 0;
if (size * 1000 % 10 > 0) {
precision = 3;
} else if (size * 100 % 10 > 0) {
precision = 2;
} else if (size * 10 % 10 > 0) {
precision = 1;
} else {
precision = 0;
}
String formatStr = "%." + precision + "f";
if ("KB".equals(type)) {
return String.format(formatStr, (size)) + "KB";
} else if ("MB".equals(type)) {
return String.format(formatStr, (size)) + "MB";
} else if ("GB".equals(type)) {
return String.format(formatStr, (size)) + "GB";
} else if ("TB".equals(type)) {
return String.format(formatStr, (size)) + "TB";
} else if ("PB".equals(type)) {
return String.format(formatStr, (size)) + "PB";
}
return String.format(formatStr, (size)) + "B";
}
}
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* 线程池配置类
*
* @author CL
*
*/
@Configuration
public class ThreadPoolTaskExecutorConfig {
@Bean
public Executor customThreadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// Java虚拟机可用的处理器数
int corePoolSize = Runtime.getRuntime().availableProcessors();
// 配置核心线程数
executor.setCorePoolSize(corePoolSize);
// 配置最大线程数
executor.setMaxPoolSize(corePoolSize * 2 + 1);
// 配置队列大小
executor.setQueueCapacity(100);
// 空闲的多余线程最大存活时间
executor.setKeepAliveSeconds(3);
// 配置线程池中的线程的名称前缀
executor.setThreadNamePrefix("thread-execute-");
// 当线程池达到最大大小时,在调用者的线程中执行任务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 执行初始化
executor.initialize();
return executor;
}
}
import java.io.Serializable;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* 系统用户
*
* @author CL
*
*/
@Data
@TableName("sys_log")
public class SysLog implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
@TableId
private Long id;
/**
* 日志类型
*/
private String logType;
/**
* 创建用户编码
*/
private String createUserCode;
/**
* 创建用户名称
*/
private String createUserName;
/**
* 创建时间
*/
private LocalDateTime createDate;
/**
* 请求URI
*/
private String requestUri;
/**
* 请求方式
*/
private String requestMethod;
/**
* 请求参数
*/
private String requestParams;
/**
* 请求IP
*/
private String requestIp;
/**
* 请求服务器地址
*/
private String serverAddress;
/**
* 是否异常
*/
private String isException;
/**
* 异常信息
*/
private String exceptionInfo;
/**
* 开始时间
*/
private LocalDateTime startTime;
/**
* 结束时间
*/
private LocalDateTime endTime;
/**
* 执行时间
*/
private Long executeTime;
/**
* 用户代理
*/
private String userAgent;
/**
* 操作系统
*/
private String deviceName;
/**
* 浏览器名称
*/
private String browserName;
}
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.c3stones.entity.SysLog;
/**
* 系统日志Dao
*
* @author CL
*
*/
@Mapper
public interface SysLogDao extends BaseMapper {
}
import com.baomidou.mybatisplus.extension.service.IService;
import com.c3stones.entity.SysLog;
/**
* 系统日志Service
*
* @author CL
*
*/
public interface SysLogService extends IService {
}
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.c3stones.dao.SysLogDao;
import com.c3stones.entity.SysLog;
import com.c3stones.service.SysLogService;
/**
* 系统日志Service实现
*
* @author CL
*
*/
@Service
public class SysLogServiceImpl extends ServiceImpl implements SysLogService {
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.c3stones.entity.SysLog;
import com.c3stones.service.SysLogService;
import com.c3stones.utils.R;
/**
* 系统日志Controller
*
* @author CL
*
*/
@RestController
@RequestMapping(value = "/sys/log")
public class SysLogController {
@Autowired
private SysLogService sysLogService;
/**
* 日志分页查询
*
* @param start 起始页码
* @param limit 每页数量
* @param sysLog 系统日志
* @return
*/
@RequestMapping(value = "page")
public R> page(int start, int limit, SysLog sysLog) {
QueryWrapper queryWrapper = new QueryWrapper<>(sysLog);
queryWrapper.orderByDesc("create_date");
Page page = sysLogService.page(new Page<>(start, limit), queryWrapper);
return R.ok(page);
}
}
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;
import javax.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
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 com.c3stones.constants.Global;
import com.c3stones.entity.SysLog;
import com.c3stones.service.SysLogService;
import com.c3stones.utils.ByteUtils;
import com.c3stones.utils.R;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import lombok.extern.log4j.Log4j2;
/**
* 系统日志切面
*
* @author CL
*
*/
@Log4j2
@Aspect
@Component
public class SysLogAspect {
private ThreadLocal sysLogThreadLocal = new ThreadLocal<>();
@Autowired
private Executor customThreadPoolTaskExecutor;
@Autowired
private SysLogService sysLogService;
/**
* 日志切点
*/
@Pointcut("execution(public * com.c3stones.controller.*.*(..))")
public void sysLogAspect() {
}
/**
* 前置通知
*
* @param joinPoint
* @throws Throwable
*/
@Before(value = "sysLogAspect()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) Objects
.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
SysLog sysLog = new SysLog();
// 创建人信息请根据实际项目获取方式获取
sysLog.setCreateUserCode("");
sysLog.setCreateUserName("");
sysLog.setStartTime(LocalDateTime.now());
sysLog.setRequestUri(URLUtil.getPath(request.getRequestURI()));
sysLog.setRequestParams(formatParams(request.getParameterMap()));
sysLog.setRequestMethod(request.getMethod());
sysLog.setRequestIp(ServletUtil.getClientIP(request));
sysLog.setServerAddress(request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort());
String userAgentStr = request.getHeader("User-Agent");
sysLog.setUserAgent(userAgentStr);
UserAgent userAgent = UserAgentUtil.parse(userAgentStr);
sysLog.setDeviceName(userAgent.getOs().getName());
sysLog.setBrowserName(userAgent.getBrowser().getName());
sysLogThreadLocal.set(sysLog);
log.info("开始计时: {} URI: {} IP: {}", sysLog.getStartTime(), sysLog.getRequestUri(), sysLog.getRequestIp());
}
/**
* 返回通知
*
* @param ret
*/
@AfterReturning(pointcut = "sysLogAspect()", returning = "ret")
public void doAfterReturning(Object ret) {
SysLog sysLog = sysLogThreadLocal.get();
sysLog.setLogType(Global.LOG_INGO);
sysLog.setEndTime(LocalDateTime.now());
sysLog.setExecuteTime(Long.valueOf(ChronoUnit.MILLIS.between(sysLog.getStartTime(), sysLog.getEndTime())));
R> r = Convert.convert(R.class, ret);
if (r.getCode() == Global.TRUE) {
sysLog.setIsException(Global.NO);
} else {
sysLog.setIsException(Global.YES);
sysLog.setExceptionInfo(r.getMsg());
}
customThreadPoolTaskExecutor.execute(new SaveLogThread(sysLog, sysLogService));
sysLogThreadLocal.remove();
Runtime runtime = Runtime.getRuntime();
log.info("计时结束: {} 用时: {}ms URI: {} 总内存: {} 已用内存: {}", sysLog.getEndTime(), sysLog.getExecuteTime(),
sysLog.getRequestUri(), ByteUtils.formatByteSize(runtime.totalMemory()),
ByteUtils.formatByteSize(runtime.totalMemory() - runtime.freeMemory()));
}
/**
* 异常通知
*
* @param e
*/
@AfterThrowing(pointcut = "sysLogAspect()", throwing = "e")
public void doAfterThrowable(Throwable e) {
SysLog sysLog = sysLogThreadLocal.get();
sysLog.setLogType(Global.LOG_ERROR);
sysLog.setEndTime(LocalDateTime.now());
sysLog.setExecuteTime(Long.valueOf(ChronoUnit.MINUTES.between(sysLog.getStartTime(), sysLog.getEndTime())));
sysLog.setIsException(Global.YES);
sysLog.setExceptionInfo(e.getMessage());
customThreadPoolTaskExecutor.execute(new SaveLogThread(sysLog, sysLogService));
sysLogThreadLocal.remove();
Runtime runtime = Runtime.getRuntime();
log.info("计时结束: {} 用时: {}ms URI: {} 总内存: {} 已用内存: {}", sysLog.getEndTime(), sysLog.getExecuteTime(),
sysLog.getRequestUri(), ByteUtils.formatByteSize(runtime.totalMemory()),
ByteUtils.formatByteSize(runtime.totalMemory() - runtime.freeMemory()));
}
/**
* 格式化参数
*
* @param parameterMap
* @return
*/
private String formatParams(Map parameterMap) {
if (parameterMap == null) {
return null;
}
StringBuilder params = new StringBuilder();
for (Map.Entry param : (parameterMap).entrySet()) {
if (params.length() != 0) {
params.append("&");
}
params.append(param.getKey() + "=");
if (StrUtil.endWithIgnoreCase(param.getKey(), "password")) {
params.append("*");
} else if (param.getValue() != null) {
params.append(ArrayUtil.join(param.getValue(), ","));
}
}
return params.toString();
}
/**
* 保存日志线程
*
* @author CL
*
*/
private static class SaveLogThread extends Thread {
private SysLog sysLog;
private SysLogService sysLogService;
public SaveLogThread(SysLog sysLog, SysLogService sysLogService) {
this.sysLog = sysLog;
this.sysLogService = sysLogService;
}
@Override
public void run() {
sysLog.setCreateDate(LocalDateTime.now());
sysLogService.save(sysLog);
}
}
}
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.c3stones.utils.R;
/**
* 示例Controller
*
* @author CL
*
*/
@RestController
public class DemoController {
/**
* 示例方法1
*
* @return
*/
@RequestMapping(value = "demo1")
public R demo1(String str) {
return R.ok("成功返回 -> " + str);
}
/**
* 示例方法2
*
* @return
*/
@RequestMapping(value = "demo2")
public R demo2(String str, int num) {
return R.failed("失败返回");
}
/**
* 示例方法3
*
* @return
*/
@SuppressWarnings("unused")
@RequestMapping(value = "demo3")
public R demo3() {
int a = 1 / 0;
return R.ok("模拟异常");
}
}
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 启动类
*
* @author CL
*
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
4. 测试
# 接口返回:
{
"code": true,
"msg": null,
"data": "成功返回 -> demo1"
}
# 控制台打印
2021-07-04 14:45:12.077 INFO 16032 --- [nio-8080-exec-4] com.c3stones.aspect.SysLogAspect : 开始计时: 2021-07-04T14:45:12.076 URI: /demo1 IP: 127.0.0.1
2021-07-04 14:45:12.077 INFO 16032 --- [nio-8080-exec-4] com.c3stones.aspect.SysLogAspect : 计时结束: 2021-07-04T14:45:12.077 用时: 1ms URI: /demo1 总内存: 323.5MB 已用内存: 70.332MB
# 接口返回:
{
"code": false,
"msg": "失败返回",
"data": null
}
# 控制台打印
2021-07-04 14:48:57.795 INFO 16032 --- [nio-8080-exec-6] com.c3stones.aspect.SysLogAspect : 开始计时: 2021-07-04T14:48:57.794 URI: /demo2 IP: 127.0.0.1
2021-07-04 14:48:57.796 INFO 16032 --- [nio-8080-exec-6] com.c3stones.aspect.SysLogAspect : 计时结束: 2021-07-04T14:48:57.795 用时: 1ms URI: /demo2 总内存: 323.5MB 已用内存: 72.496MB
# 接口返回:
异常无返回
# 控制台打印
2021-07-04 14:49:30.260 INFO 16032 --- [nio-8080-exec-7] com.c3stones.aspect.SysLogAspect : 开始计时: 2021-07-04T14:49:30.260 URI: /demo3 IP: 127.0.0.1
2021-07-04 14:49:30.261 INFO 16032 --- [nio-8080-exec-7] com.c3stones.aspect.SysLogAspect : 计时结束: 2021-07-04T14:49:30.261 用时: 0ms URI: /demo3 总内存: 323.5MB 已用内存: 72.861MB
2021-07-04 14:49:30.272 ERROR 16032 --- [nio-8080-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause
java.lang.ArithmeticException: / by zero
......
# 接口返回:
{
"code": true,
"msg": null,
"data": {
"records": [
{
"id": 3,
"logType": "ERROR",
"createUserCode": "",
"createUserName": "",
"createDate": "2021-07-04T14:49:30",
"requestUri": "/demo3",
"requestMethod": "GET",
"requestParams": "",
"requestIp": "127.0.0.1",
"serverAddress": "http://127.0.0.1:8080",
"isException": "1",
"exceptionInfo": "/ by zero",
"startTime": "2021-07-04T14:49:30",
"endTime": "2021-07-04T14:49:30",
"executeTime": 0,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
"deviceName": "Windows 10 or Windows Server 2016",
"browserName": "Chrome"
},
{
"id": 2,
"logType": "INFO",
"createUserCode": "",
"createUserName": "",
"createDate": "2021-07-04T14:48:58",
"requestUri": "/demo2",
"requestMethod": "GET",
"requestParams": "str=demo2&num=10",
"requestIp": "127.0.0.1",
"serverAddress": "http://127.0.0.1:8080",
"isException": "1",
"exceptionInfo": "失败返回",
"startTime": "2021-07-04T14:48:58",
"endTime": "2021-07-04T14:48:58",
"executeTime": 1,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
"deviceName": "Windows 10 or Windows Server 2016",
"browserName": "Chrome"
},
{
"id": 1,
"logType": "INFO",
"createUserCode": "",
"createUserName": "",
"createDate": "2021-07-04T14:45:12",
"requestUri": "/demo1",
"requestMethod": "GET",
"requestParams": "str=demo1",
"requestIp": "127.0.0.1",
"serverAddress": "http://127.0.0.1:8080",
"isException": "0",
"exceptionInfo": null,
"startTime": "2021-07-04T14:45:12",
"endTime": "2021-07-04T14:45:12",
"executeTime": 1,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
"deviceName": "Windows 10 or Windows Server 2016",
"browserName": "Chrome"
}
],
"total": 0,
"size": 10,
"current": 1,
"orders": [],
"optimizeCountSql": true,
"hitCount": false,
"searchCount": true,
"pages": 0
}
}
5. 项目地址
spring-aop-log-demo