Java基于注解实现日志记录模块,超详细注释!

背景

在项目开发过程中,日志记录是一个至关重要的环节,它能够帮助开发人员追踪用户的重要操作,如新增、删除、修改等,从而有效监控系统的运行状态。通过日志记录,我们可以深入了解系统的运行情况,及时发现并解决问题,优化性能,提高用户体验。

为了满足不同项目的需求,我们期望设计一个可移植性强、易于使用的日志记录系统。这样的系统应该能够轻松集成到各个项目中,而不需要对原有业务代码进行大量修改,在这种场景下,Spring AOP(面向切面编程)为我们提供了一个优雅的解决方案。

Spring AOP 允许我们在不改变原有业务逻辑的情况下,通过添加注解的方式,在关键方法或类上插入日志记录代码。这样,我们就能在不影响业务逻辑的前提下,实现对用户操作的监控和记录。

基于Spring AOP的日志记录系统不仅具有高度的可移植性,而且易于扩展和维护,通过简单的配置和少量的代码编写,我们就能轻松地将日志记录功能集成到其他Spring Boot项目中,这样的设计使得我们能够在多个项目之间共享和复用日志记录逻辑,提高了开发效率和质量。

准备工作

依赖引入

<!-- 版本控制 -->
<hutool.versin>5.8.25</hutool.versin>
<lombok.versin>1.18.22</lombok.versin>
<mybatis-plus.version>3.5.4</mybatis-plus.version>

<!-- hutool -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>${hutool.versin}</version>
</dependency>

<!-- lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>${lombok.versin}</version>
</dependency>

<!-- mybatis-plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>${mybatis-plus.version}</version>
</dependency>

数据表建立

-- MySQL dump 10.13  Distrib 8.0.26, for Win64 (x86_64)
--
-- Host: 127.0.0.1    Database: logger
-- ------------------------------------------------------
-- Server version	8.0.26

DROP TABLE IF EXISTS `logger`;

CREATE TABLE `logger` (
  `id` bigint NOT NULL COMMENT '日志id',
  `operation_user` varchar(32) DEFAULT NULL COMMENT '用户名',
  `operation_ip` varchar(64) DEFAULT NULL COMMENT 'ip地址',
  `request_method` varchar(255) DEFAULT NULL COMMENT '请求方法',
  `operation` varchar(255) DEFAULT NULL COMMENT '用户操作',
  `params` text COMMENT '请求参数',
  `run_time` bigint DEFAULT NULL COMMENT '运行时间',
  `create_date` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='日志表';

LOCK TABLES `logger` WRITE;

UNLOCK TABLES;

其他

为了方便代码管理,我们先建好相关包文件夹

  • annotation目录:包含自定义注解
  • aspect目录:包含自定义切面类
  • controller目录:后台日志管理Controller层
  • entity目录:日志记录实体类
  • mapper目录:日志管理Mapper层
  • service目录:日志管理Service层
  • utils目录:包含一些工具类

代码实现

自定义注解-SystemLogger

import java.lang.annotation.*;

/**
 * 该注解用于标记在系统日志中需要记录的方法
 * 注解被定义为 @interface,表示它是一个自定义注解
 *
 * @author b16mt
 */
@Target(ElementType.METHOD) // 该注解仅可用于方法声明
@Retention(RetentionPolicy.RUNTIME) // 该注解在运行时可见
@Documented // 该注解将包含在 Javadoc 中
public @interface SystemLogger {

    /**
     * 用于指定记录的系统日志的描述信息
     * 默认值为空字符串,可以在使用注解时提供自定义的描述信息
     * 例如:@SystemLogger(value = "员工管理-新增员工数据")
     */
    String value() default "";

    /**
     * 开启控制台输出日志,默认关闭
     * 例如:@SystemLogger(value = "员工管理-新增员工数据", consoleLog = true)
     */
    boolean consoleLog() default false;
}

自定义切面类-LoggerAspect

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.date.SystemClock;
import com.xjl.sys.log.entity.Logger;
import com.xjl.sys.log.annotation.SystemLogger;
import com.xjl.sys.log.service.SystemLoggerService;
import com.xjl.sys.log.utils.IpAddressUtil;
import com.xjl.sys.log.utils.JsonUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
 * 切面类,用于记录系统日志信息
 *
 * @author b16mt
 */
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class LoggerAspect {

	private final SystemLoggerService systemLoggerService;

	/**
	 * 环绕通知,用于在目标方法执行前后进行日志记录
	 */
	@Around("@annotation(systemLogger)")
	public Object around(ProceedingJoinPoint joinPoint, SystemLogger systemLogger) throws Throwable {
		// 记录方法开始执行的时间
		long beginTime = SystemClock.now();

		// 执行目标方法
		Object result = joinPoint.proceed();

		// 记录方法执行时长(毫秒)
		long time = SystemClock.now() - beginTime;

		// 创建系统日志实体
		Logger logger = new Logger();

		// 从注解中获取操作描述
		if (systemLogger != null) {
			logger.setOperation(systemLogger.value());
		}

		// 获取请求的方法名
		String className = joinPoint.getTarget().getClass().getName();
		String methodName = joinPoint.getSignature().getName();
		logger.setRequestMethod(className + "." + methodName + "()");

		// 获取请求的参数
		Object[] args = joinPoint.getArgs();
		String params = JsonUtil.toJsonString(args[0]);
		logger.setParams(params);

		// 设置IP地址
		logger.setOperationIp(IpAddressUtil.getIpAddr());

		// 设置用户名
		String username = getUsername();
		logger.setOperationUser(username);

		// 设置日志记录时间
		logger.setCreateDate(LocalDateTime.now());
		logger.setRunTime(time);

		// 保存系统日志
		systemLoggerService.save(logger);

		// 控制台输出日志,这块默认是不开启滴,一般也就在项目测试的时候开启一下
		if (systemLogger != null && systemLogger.consoleLog()) {
			log.info("用户【{}】进行了【{}】操作,请求参数为:{}", username, systemLogger.value(), params);
		}

		return result;
	}

	// TODO 获取当前登录用户需根据自己项目的登录方法修改,当前采用的方案为Sa-Token
	/**
	 * 获取用户名
	 *
	 * @return 用户名
	 */
	private String getUsername() {
		try {
			// 尝试获取登录用户名
			return (String) StpUtil.getLoginId();
		} catch (Exception e) {
			// 获取登录用户名失败,获取用户ip地址
			return IpAddressUtil.getIpAddr();
		}
	}
}

工具类-HttpContextUtil

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * @author b16mt
 */
public class HttpContextUtil {

	/**
	 * 获取当前请求的 HttpServletRequest
	 *
	 * @return HttpServletRequest 对象
	 */
	public static HttpServletRequest getHttpServletRequest() {
		return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
	}


	/**
	 * 获取当前请求的域名
	 *
	 * @return 当前请求的域名
	 */
	public static String getDomain() {
		HttpServletRequest request = getHttpServletRequest();
		StringBuilder url = new StringBuilder(request.getRequestURL());

		// 移除 URI 部分,保留域名
		int lastSlashIndex = url.lastIndexOf(request.getRequestURI());
		if (lastSlashIndex > 0) {
			url.delete(lastSlashIndex, url.length());
		}

		return url.toString();
	}


	/**
	 * 获取当前请求的 Origin 头信息
	 *
	 * @return Origin 头信息
	 */
	public static String getOrigin() {
		HttpServletRequest request = getHttpServletRequest();
		return request.getHeader("Origin");
	}

}

工具类-IpAddressUtil

import javax.servlet.http.HttpServletRequest;

/**
 * IP工具
 *
 * @author b16mt
 */
public class IpAddressUtil {
    private static final String UNKNOWN = "unknown";

    /**
     * 获取用户的真实地址,如果有多个地址则返回第一个
     *
     * @return 用户真实地址,如果无法获取则返回 null
     */
    public static String getIpAddr() {
        // 获取 HttpServletRequest 对象
        HttpServletRequest request = HttpContextUtil.getHttpServletRequest();

        // 如果 HttpServletRequest 对象为空,则无法获取地址,返回 null
        if (request == null) {
            return null;
        }

        // 尝试从请求头中获取用户 IP 地址
        String[] headersToCheck = {"x-forwarded-for", "Proxy-Client-IP", "WL-Proxy-Client-IP"};
        String ip = null;
        for (String header : headersToCheck) {
            ip = request.getHeader(header);
            if (!isEmptyIp(ip)) {
                break;
            }
        }

        // 如果仍然无法获取 IP 地址,则使用默认的 RemoteAddr
        if (isEmptyIp(ip)) {
            ip = request.getRemoteAddr();
        }

        // 处理可能存在的多个 IP 地址,只返回第一个
        String[] ips = ip.split(",");
        return ips[0].trim();
    }

    /**
     * 检查IP地址是否为空或为unknown
     *
     * @param ip 待检查的IP地址
     * @return 如果IP地址为空或为unknown,则返回true;否则返回false
     */
    private static boolean isEmptyIp(String ip) {
        return ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip);
    }

}

工具类-JsonUtil

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.json.JsonWriteFeature;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import lombok.extern.slf4j.Slf4j;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * JSON工具类,提供对象与JSON字符串之间的转换
 *
 * @author b16mt
 */
@Slf4j
public class JsonUtil {

	/** 使用 Jackson ObjectMapper 处理 JSON 的序列化和反序列化 */
	private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder()
			// 在序列化时,排除空属性
			.serializationInclusion(JsonInclude.Include.NON_EMPTY)
			// 在序列化时,禁用空bean的异常
			.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
			// 在序列化时,禁用将日期写为时间戳的功能
			.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
			// 在反序列化时,禁用未知属性的异常
			.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
			// 配置禁用非ASCII字符的转义
			.configure(JsonWriteFeature.ESCAPE_NON_ASCII.mappedFeature(), false)
			.build();

	/**
	 * 将对象转换为JSON字符串
	 *
	 * @param object 要转换的对象
	 * @return 对象的JSON字符串表示形式
	 */
	public static String toJsonString(Object object) {
		try {
			return OBJECT_MAPPER.writeValueAsString(object);
		} catch (JsonProcessingException e) {
			log.error("对象转json错误:", e);
			return null;
		}
	}

	/**
	 * 将JSON字符串转换为指定类型的对象
	 *
	 * @param json  要解析的JSON字符串
	 * @param clazz 目标对象的类型
	 * @return 解析后的对象
	 */
	public static <T> T parseObject(String json, Class<T> clazz) {
		try {
			return OBJECT_MAPPER.readValue(json, clazz);
		} catch (Exception e) {
			log.error("Json转换错误:", e);
			return null;
		}
	}

	/**
	 * 将JSON字符串转换为指定类型的对象列表
	 *
	 * @param json  要解析的JSON字符串
	 * @param clazz 目标对象的数组类型
	 * @return 解析后的对象列表
	 */
	public static <T> List<T> parseArray(String json, Class<T[]> clazz) {
		try {
			return Arrays.asList(OBJECT_MAPPER.readValue(json, clazz));
		} catch (Exception e) {
			log.error("Json转换错误:", e);
			return Collections.emptyList();
		}
	}

	/**
	 * 将JSON字符串转换为JsonNode对象
	 *
	 * @param jsonStr 要解析的JSON字符串
	 * @return JsonNode对象
	 */
	public static JsonNode parseJson(String jsonStr) {
		try {
			return OBJECT_MAPPER.readTree(jsonStr);
		} catch (Exception e) {
			log.error("Json转换错误:", e);
			return null;
		}
	}

	/**
	 * 将JSON字符串转换为指定键的JsonNode对象
	 *
	 * @param jsonStr 要解析的JSON字符串
	 * @param key     要获取的键
	 * @return 指定键对应的JsonNode对象
	 */
	public static JsonNode parseJson(String jsonStr, String key) {
		JsonNode jsonNode = parseJson(jsonStr);
		if (jsonNode != null) {
			return jsonNode.get(key);
		} else {
			return null;
		}
	}

	/**
	 * 获取全局的ObjectMapper实例
	 *
	 * @return ObjectMapper实例
	 */
	public static ObjectMapper getObjectMapper() {
		return OBJECT_MAPPER;
	}
}

实体类-Logger

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.time.LocalDateTime;


/**
 * 日志表logger-实体类
 *
 * @author b16mt
 */
@Data
@TableName("logger")
public class Logger implements Serializable {

	private static final long serialVersionUID = 1L;

	/**
	 * 日志id
	 */
	private Long id;

	/**
	 * 用户名
	 */
	private String operationUser;

	/**
	 * IP地址
	 */
	private String operationIp;

	/**
	 * 请求方法
	 */
	private String requestMethod;

	/**
	 * 用户操作
	 */
	private String operation;

	/**
	 * 请求参数
	 */
	private String params;

	/**
	 * 执行时长(毫秒)
	 */
	private Long runTime;

	/**
	 * 创建时间
	 */
	private LocalDateTime createDate;

}

控制层-LoggerController

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

import com.xjl.common.result.Result;
import com.xjl.sys.log.service.SystemLoggerService;
import com.xjl.sys.log.entity.Logger;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;

import java.util.Arrays;

/**
 * @author b16mt
 */
@Slf4j
@RestController
@RequestMapping("/sysLogger")
public class LoggerController {

    private final SystemLoggerService systemLoggerService;

    public LoggerController(SystemLoggerService systemLoggerService) {
        this.systemLoggerService = systemLoggerService;
    }

    /**
     * 分页查询-日志信息
     *
     * @param page 查询页面
     * @param pageSize 每页展示最大条数
     * @param username 用户名
     * @param ip IP地址
     * @param operation 用户操作
     *
     * @return Page<SystemLogger>对象
     */
    @GetMapping("/getLogInformation")
    public Result<Page<Logger>> loggerList(@RequestParam Integer page,
                                           @RequestParam Integer pageSize,
                                           @RequestParam String username,
                                           @RequestParam String ip,
                                           @RequestParam String operation){
        // 参数校验
        if (page == null || page < 1) {
            // 如果页码为null或小于1,将页码设为默认值1
            page = 1;
        }
        if (pageSize == null || pageSize < 1) {
            // 如果每页展示的最大条数为null或小于1,将其设为默认值20
            pageSize = 20;
        }

        // 构造分页构造器,即Page对象
        Page<Logger> pageInfo = new Page<>(page, pageSize);

        // 构造条件构造器 queryWrapper
        LambdaQueryWrapper<Logger> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(StringUtils.isNotEmpty(operation), Logger::getOperation, operation)
                    .like(StringUtils.isNotEmpty(username), Logger::getOperationUser, username)
                    .like(StringUtils.isNotEmpty(ip), Logger::getOperationIp, ip);

        // 调用部门服务的分页查询方法,将查询结果封装到pageInfo中
        systemLoggerService.page(pageInfo, queryWrapper);

        // 将查询结果包装成成功的Result对象并返回
        return Result.success(pageInfo);
    }


    /**
     * 删除-日志信息
     *
     * @param ids 日志id,支持批量删除
     * @return 响应结果-success/error
     */
    @DeleteMapping("/delLogInformation")
    public Result<String> loggerDelete(@RequestParam Long[] ids){
        if (ids == null || ids.length == 0) {
            return Result.error("请提供有效的日志ID~");
        }

        systemLoggerService.removeByIds(Arrays.asList(ids));

        return Result.success("删除日志信息成功~");
    }
}

Service层

interface-SystemLoggerService

import com.baomidou.mybatisplus.extension.service.IService;
import com.xjl.sys.log.entity.Logger;


/**
 * 系统日志
 *
 * @author b16mt
 */
public interface SystemLoggerService extends IService<Logger> {
}

impl-SystemLoggerServiceImpl

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xjl.sys.log.entity.Logger;
import com.xjl.sys.log.mapper.SystemLoggerMapper;
import com.xjl.sys.log.service.SystemLoggerService;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;

/**
 * @author b16mt
 */
@Service("LoggerService")
public class SystemLoggerServiceImpl extends ServiceImpl<SystemLoggerMapper, Logger> implements SystemLoggerService {
}

Mapper层-SystemLoggerMapper

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xjl.sys.log.entity.Logger;

/**
 * 系统日志
 *
 * @author b16mt
 */
public interface SystemLoggerMapper extends BaseMapper<Logger> {
}

注意

上述控制层使用的分页插件是mp的分页插件,建议自己根据项目实际需求更换,上述代码是为了进行简单演示而使用。如果坚持使用mp的分页插件,那么请你添加如下mp配置类MybatisPlusConfig

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * MyBatis Plus 配置类,配置分页插件
 */
@Configuration
public class MybatisPlusConfig {

    /**
     * 配置 MyBatis Plus 的分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }
}

测试

先发送几个测试请求

在POST接口中,开启了控制台日志输出,即consoleLog = true,所以在控制台中我们可以看到输出结果:

在数据库中,我们也可以看到记录了相关操作日志:

 查询日志记录:

写在最后

该demo基于Spring AOP完成了一个日志记录模块,大家可以基于自己项目实际进行简单的修改(比如拓展控制层的其他接口功能)使用,可以使用在一些竞赛项目系统、毕业设计XX管理系统等等,代码中我已经添加了详细的注释,如有任何疑问或建议,欢迎随时与我交流,期待与你共同学习进步!

  • 16
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

这小鱼在乎

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值