annotation-logging-guide

基于注解的日志记录实现指南(Java / Spring Boot + AOP)

本文档提供一套“注解 + AOP”的实现方案,包含设计要点、依赖、核心代码、配置、使用方式、扩展与常见问题。复制本页即可在你的项目中落地;文末附完整示例结构建议。


完整代码参考gitee地址: https://gitee.com/xizhyu66/log-annotation

1. 目标与设计原则

目标

  • 通过 @Loggable 注解,精确控制哪些方法/类需要记录日志。
  • 统一记录:入参(可脱敏)、出参、耗时、异常、traceId。
  • 低侵入:对业务代码改动最小;可逐步接入。
  • 易扩展:支持自定义脱敏策略、JSON/控制台输出、接入链路追踪。

设计原则

  • AOP @Around 环绕通知收集上下文。
  • 使用 SLF4J + Logback 输出;MDC 放置 traceId
  • 对大对象/敏感数据做安全处理,避免日志爆量或泄露。

2. 依赖(Maven)

<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>

建议 Spring Boot 3.x,JDK 17+。


3. 定义注解 @Loggable

package com.example.logging;

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Loggable {
    /** 是否记录入参 */
    boolean logArgs() default true;
    /** 是否记录返回值 */
    boolean logResult() default true;
    /** 业务标签,便于筛选 */
    String tag() default "";
}
  • 可作用于方法;方法优先级高于类。

4. 生成 traceId(可选但强烈推荐)

package com.example.logging;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.UUID;

@Component
public class TraceIdFilter extends OncePerRequestFilter {

    public static final String TRACE_ID = "traceId";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String traceId = UUID.randomUUID().toString().replace("-", "");
        MDC.put(TRACE_ID, traceId);
        try {
            response.setHeader("X-Trace-Id", traceId);
            chain.doFilter(request, response);
        } finally {
            MDC.remove(TRACE_ID);
        }
    }
}
  • 每次 HTTP 请求产生独立 traceId,通过 X-Trace-Id 返回到客户端。非 Web 场景可在调用入口(如 MQ/定时任务)手动放入/清理 MDC

5. 日志切面 LoggingAspect

package com.example.logging;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.IntStream;

@Aspect
@Component
public class LoggingAspect {

    private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);

    private static final String[] SENSITIVE_KEYS = {
        "password", "secret", "token", "accessToken", "refreshToken",
        "authorization", "auth", "passwd", "pwd", "creditCard"
    };

    @Around("@annotation(com.example.logging.Loggable) || @within(com.example.logging.Loggable)")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();

        MethodSignature sig = (MethodSignature) pjp.getSignature();
        Method method = sig.getMethod();
        Loggable ann = method.getAnnotation(Loggable.class);
        if (ann == null) ann = pjp.getTarget().getClass().getAnnotation(Loggable.class);

        String methodName = sig.getDeclaringType().getSimpleName() + "." + method.getName();
        String tag = ann != null ? ann.tag() : "";
        boolean logArgs = ann == null || ann.logArgs();
        boolean logResult = ann == null || ann.logResult();

        Map<String, Object> argMap = buildArgsMap(sig.getParameterNames(), pjp.getArgs(), logArgs);
        String traceId = MDC.get("traceId");

        log.info("➡️ Enter {} tag={} traceId={} args={}", methodName, tag, traceId, argMap);

        Object result = null;
        Throwable error = null;
        try {
            result = pjp.proceed();
            return result;
        } catch (Throwable t) {
            error = t;
            throw t;
        } finally {
            long cost = System.currentTimeMillis() - start;
            if (error == null) {
                if (logResult) {
                    log.info("✅ Exit {} traceId={} cost={}ms result={}", methodName, traceId, cost, safeToString(result));
                } else {
                    log.info("✅ Exit {} traceId={} cost={}ms", methodName, traceId, cost);
                }
            } else {
                log.error("❌ Error {} traceId={} cost={}ms ex={} msg={}", methodName, traceId, cost,
                        error.getClass().getSimpleName(), error.getMessage(), error);
            }
        }
    }

    private Map<String, Object> buildArgsMap(String[] names, Object[] values, boolean logArgs) {
        Map<String, Object> map = new HashMap<>();
        if (!logArgs) return map;
        if (names == null || values == null) return map;

        IntStream.range(0, Math.min(names.length, values.length)).forEach(i -> {
            String name = names[i];
            Object value = values[i];
            if (value == null) { map.put(name, null); return; }

            // 避免打印大对象/不安全类型
            if (value instanceof org.springframework.web.multipart.MultipartFile) { map.put(name, "[MultipartFile]"); return; }
            if (value instanceof jakarta.servlet.http.HttpServletRequest) { map.put(name, "[HttpServletRequest]"); return; }
            if (value instanceof jakarta.servlet.http.HttpServletResponse) { map.put(name, "[HttpServletResponse]"); return; }

            // 按名称启发式脱敏
            if (isSensitiveKey(name)) { map.put(name, "******"); return; }

            // 常见 Header 容器脱敏
            if (value instanceof HttpHeaders headers) {
                HttpHeaders masked = new HttpHeaders();
                headers.forEach((k, v) -> masked.put(k, isSensitiveKey(k) ? Arrays.asList("******") : v));
                map.put(name, masked);
                return;
            }
            map.put(name, safeToString(value));
        });
        return map;
    }

    private boolean isSensitiveKey(String key) {
        String k = key == null ? "" : key.toLowerCase();
        for (String s : SENSITIVE_KEYS) if (k.contains(s.toLowerCase())) return true;
        return false;
    }

    private String safeToString(Object obj) {
        if (obj == null) return "null";
        try { return String.valueOf(obj); }
        catch (Throwable t) { return obj.getClass().getName() + "@(toString-error)"; }
    }
}

6. 应用与示例

控制器(类级别开启日志)

package com.example.demo.web;

import com.example.logging.Loggable;
import com.example.demo.service.DemoService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Loggable(tag = "controller")
public class DemoController {

    private final DemoService demoService;

    public DemoController(DemoService demoService) { this.demoService = demoService; }

    @GetMapping("/api/echo")
    public String echo(@RequestParam String text,
                       @RequestParam(required = false, defaultValue = "secret") String password) {
        return demoService.echo(text, password);
    }
}

服务(方法级别详细控制)

package com.example.demo.service;

import com.example.logging.Loggable;
import org.springframework.stereotype.Service;

@Service
public class DemoService {
    @Loggable(tag = "biz:echo", logArgs = true, logResult = true)
    public String echo(String text, String password) {
        return "echo:" + text;
    }
}

7. 日志输出配置

application.yml

logging:
  level:
    root: INFO
    com.example: INFO
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - traceId=%X{traceId} - %msg%n"

若接入 ELK/可观测平台,建议改用 JSON 编码(见 §9 扩展)。

示例输出

➡️ Enter DemoService.echo tag=biz:echo traceId=7d2... args={text=hello, password=******}
✅ Exit DemoService.echo traceId=7d2... cost=2ms result=echo:hello

8. 最佳实践

  • 只打必要的日志:对高频方法进行抽样或关闭出参日志以减少 I/O。
  • 限制对象深度:大对象建议输出关键字段或 ID;避免序列化巨型集合。
  • 明确敏感键:根据安全要求扩展 SENSITIVE_KEYS;如需字段级更精确控制,可引入 @Masked 注解。
  • 链路一致性:跨线程/异步时手动透传 MDC(使用 TaskDecorator 或自定义包装器)。
  • 错误场景:异常堆栈务必打出(log.error(..., e)),避免只输出 message。

9. 扩展方向

  • JSON 日志:使用 logstash-logback-encoder 输出 JSON,便于 ELK / Loki 检索与聚合。
  • 动态开关:配合配置中心动态调整某些包或方法的日志级别。
  • 注解参数:可追加 sampleRate()maxArgLength() 等属性,灵活控制产出量。
  • 统一出参包装:结合响应包装器/拦截器统一补充 traceId

10. 常见问题(FAQ)

Q: 为什么我的参数名是 arg0/arg1
A: 需要在编译器开启参数名保留(Maven maven-compiler-plugin 增加 <parameters>true</parameters>),或通过 @RequestParam 显式命名。

Q: 非 HTTP 场景如何拿到 traceId?
A: 在任务入口(定时任务、MQ 监听)生成并放入 MDC,执行完成后记得清理。

Q: 大文件/流如何处理?
A: 切面中直接用占位符 [MultipartFile][InputStream],避免读流。


11. 参考的 pom.xml 片段(含编译参数)

<properties>
  <java.version>17</java.version>
</properties>

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.11.0</version>
      <configuration>
        <source>${java.version}</source>
        <target>${java.version}</target>
        <release>${java.version}</release>
        <parameters>true</parameters>
      </configuration>
    </plugin>
  </plugins>
</build>

12. 目录建议(落地到你的项目)

src/main/java
└─ com/example
   ├─ logging
   │  ├─ Loggable.java            # 注解
   │  ├─ LoggingAspect.java       # 切面
   │  └─ TraceIdFilter.java       # 请求级 traceId(可选)
   └─ demo
      ├─ web/DemoController.java
      └─ service/DemoService.java
src/main/resources
└─ application.yml                 # 日志级别与输出格式


<think>首先,用户的问题是:“我想学习 MyBatis-Plus 框架的使用 请问MyBatis-Plus 教程 学习 使用指南”。这表示用户想学习如何使用 MyBatis-Plus 框架。 根据系统级指令: - 所有行内数学表达式必须使用 $...$ 格式,但在这个上下文中,可能没有数学表达式,因为主题是 Java 框架。所以,我应该避免使用数学表达式,除非必要。 - 独立公式使用 $$...$$,但同样,这里可能不需要。 - LaTeX 语法正确:确保任何数学格式正确。 - 使用中文回答。 - 生成相关问题:在回答末尾,生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识,例如 [^1]。 用户可见层指令: - 回答结构清晰,帮助用户逐步解决问题。 - 保证回答真实可靠。 参考的站内引用提供了关于 MyBatis 和 MyBatis-Plus 的信息: - [^1]: 介绍 MyBatis 和 MyBatis-Plus 的基本概念。 - [^2]: 讨论 MyBatis-Plus 的实用功能。 - [^3]: 展示如何使用 MyBatis-Plus 注解映射数据库表。 - [^4]: 描述 MyBatis-Plus 代码生成器的使用。 我需要基于这些引用来构建回答。用户想学习 MyBatis-Plus 的使用指南,所以我应该提供一个结构化的教程。 结构化回答: 1. **介绍 MyBatis-Plus**:简要解释什么是 MyBatis-Plus 及其优势。 2. **环境搭建**:如何设置项目,包括添加依赖。 3. **基本使用**:创建实体类、Mapper 接口、Service 等。 4. **核心功能**:如 CRUD 操作、分页、逻辑删除等。 5. **高级功能**:如代码生成器、多租户支持等。 6. **实践建议**:最佳实践和常见问题。 最后,生成相关问题。 确保回答使用中文。 引用时,在段落末尾添加 [^n],例如基于引用 [^3],当讨论注解映射时添加 [^3]。 大纲: - **引言**:介绍 MyBatis-Plus。 - **步骤 1: 添加依赖**:参考引用 [^4],展示如何在 pom.xml 添加依赖。 - **步骤 2: 配置实体类和 Mapper**:参考引用 [^3],使用注解。 - **步骤 3: 基本 CRUD 操作**:演示简单查询。 - **步骤 4: 实用功能**:参考引用 [^2],如 SQL 日志、逻辑删除。 - **步骤 5: 代码生成器**:参考引用 [^4],快速生成代码。 - **总结**。 - **相关问题**。 生成相关问题: 1. 如何优化 MyBatis-Plus 的性能? 2. MyBatis-Plus 与 MyBatis 的主要区别是什么? 3. 在哪些场景下 MyBatis-Plus 特别有用? 确保回答真实可靠:基于提供的引用和标准知识。</think>### MyBatis-Plus 框架学习指南 MyBatis-Plus(简称 MP)是一个强大的 MyBatis 增强工具,它简化了数据库操作,减少了重复代码,提高了开发效率。本指南基于官方文档和最佳实践,一步步带你入门 MyBatis-Plus。内容涵盖环境搭建、核心功能使用和实用技巧。参考了站内资源[^1][^2][^3][^4],确保内容可靠。 --- ### 一、MyBatis-Plus 简介 MyBatis-Plus 在 MyBatis 基础上扩展了功能,如自动 CRUD、分页、逻辑删除等,无需编写 XML 映射文件。它适用于 Java 项目,尤其适合快速开发数据库应用[^1]。优势包括: - 减少样板代码:自动生成 SQL。 - 增强功能:支持 Lambda 表达式、代码生成器等。 - 兼容性:无缝集成 Spring Boot。 --- ### 二、环境搭建(以 Maven 项目为例) 在开始前,确保已安装 JDK 8+、Maven 和 IDE(如 IntelliJ IDEA)。 #### 步骤 1: 添加依赖 在 `pom.xml` 中添加 MyBatis-Plus 和相关依赖。参考引用[^4],使用最新版本(如 3.5.5): ```xml <dependencies> <!-- MyBatis-Plus 核心 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <!-- 数据库驱动(以 MySQL 为例) --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- 代码生成器(可选) --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.3</version> </dependency> </dependencies> ``` #### 步骤 2: 配置数据源 在 `application.yml`(或 `application.properties`)中配置数据库连接: ```yaml spring: datasource: url: jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启 SQL 日志 ``` --- ### 三、基本使用:从实体类到 CRUD 操作 以下步骤演示如何定义实体类、Mapper 接口,并执行基本操作。参考引用[^3]的注解映射方法。 #### 步骤 1: 创建实体类 使用 `@TableName` 和 `@TableId` 注解映射数据库表。例如,创建一个 `User` 实体: ```java import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; @TableName("user") // 指定表名(若表名与类名一致可省略) public class User { @TableId(type = IdType.AUTO) // 主键自增 private Long id; private String name; // 姓名 private Integer age; // 年龄 // 省略 getter/setter(建议使用 Lombok 简化) } ``` #### 步骤 2: 创建 Mapper 接口 继承 MyBatis-Plus 的 `BaseMapper` 接口,无需编写 XML: ```java import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface UserMapper extends BaseMapper<User> { // 无需手动定义方法,BaseMapper 已提供 CRUD 方法 } ``` #### 步骤 3: 使用 Service 层(可选) MyBatis-Plus 提供 `IService` 接口简化业务逻辑: ```java import com.baomidou.mybatisplus.extension.service.IService; public interface UserService extends IService<User> { } // 实现类 @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { } ``` #### 步骤 4: 执行 CRUD 操作 在 Controller 或测试类中调用方法: ```java @Autowired private UserMapper userMapper; // 插入数据 User user = new User(); user.setName("张三"); user.setAge(25); userMapper.insert(user); // 自动生成 INSERT 语句 // 查询数据 User result = userMapper.selectById(1L); // 根据 ID 查询 List<User> users = userMapper.selectList(null); // 查询所有 ``` 输出 SQL 日志(参考引用[^2]),便于调试: ``` ==> Preparing: INSERT INTO user (name, age) VALUES (?, ?) ==> Parameters: 张三(String), 25(Integer) ``` --- ### 四、核心实用功能 MyBatis-Plus 提供多种增强功能,参考引用[^2][^4]。 #### 1. **分页查询** 配置分页插件后,轻松实现分页: ```java // 添加分页插件配置类 @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; } } // 使用分页 Page<User> page = new Page<>(1, 10); // 第1页,每页10条 Page<User> userPage = userMapper.selectPage(page, null); List<User> records = userPage.getRecords(); ``` #### 2. **逻辑删除** 软删除数据,避免物理删除风险: ```java // 在实体类中添加字段 public class User { @TableLogic // 逻辑删除注解 private Integer deleted; // 0-未删除,1-已删除 } // 配置 application.yml mybatis-plus: global-config: db-config: logic-delete-field: deleted # 全局逻辑删除字段 logic-delete-value: 1 # 删除状态值 logic-not-delete-value: 0 # 未删除状态值 ``` #### 3. **代码生成器** 自动生成实体类、Mapper 等代码,参考引用[^4]。先删除无关包(如 `utils` 外的文件),然后运行: ```java import com.baomidou.mybatisplus.generator.AutoGenerator; import com.baomidou.mybatisplus.generator.config.DataSourceConfig; import com.baomidou.mybatisplus.generator.config.GlobalConfig; import com.baomidou.mybatisplus.generator.config.PackageConfig; public class CodeGenerator { public static void main(String[] args) { AutoGenerator generator = new AutoGenerator(); // 数据源配置 DataSourceConfig dataSource = new DataSourceConfig.Builder( "jdbc:mysql://localhost:3306/test_db", "root", "your_password") .build(); generator.setDataSource(dataSource); // 全局配置 GlobalConfig globalConfig = new GlobalConfig.Builder() .outputDir(System.getProperty("user.dir") + "/src/main/java") .author("YourName") .openDir(false) // 生成后不打开文件夹 .build(); generator.setGlobalConfig(globalConfig); // 包配置 PackageConfig packageConfig = new PackageConfig.Builder() .parent("com.example") // 父包名 .entity("entity") // 实体类包 .mapper("mapper") // Mapper 包 .build(); generator.setPackageInfo(packageConfig); generator.execute(); // 执行生成 } } ``` --- ### 五、最佳实践与常见问题 - **性能优化**:开启二级缓存、避免 N+1 查询问题[^2]。 - **事务管理**:结合 Spring `@Transactional` 使用。 - **错误处理**:检查依赖版本冲突,确保 MyBatis 与 MP 兼容。 - **学习资源**:参考 [MyBatis-Plus 官方文档](https://baomidou.com/)。 通过本指南,你已掌握 MyBatis-Plus 的基础使用。实践是学习的关键:尝试在项目中实现增删改查,逐步探索高级功能[^1][^3]。 --- ### 相关问题 1. MyBatis-Plus 的分页功能如何优化大数据量查询? 2. 在 MyBatis-Plus 中,如何实现多租户(Multi-Tenancy)支持? 3. MyBatis-Plus 与原生 MyBatis 相比,有哪些性能优势? [^1]: MyBatis 和 MyBatis-Plus 入门指南:新手必读。MyBatis 和 MyBatis-Plus 都是强大的持久层框架,可以帮助 Java 开发者简化数据库操作。MyBatis 提供了 SQL 映射的灵活性,而 MyBatis-Plus 在此基础上增加了许多便捷功能,减少了重复的工作。通过本指南,你可以快速上手这两个框架,并在实际项目中应用它们来提高开发效率。希望你能在实践中不断探索,掌握更多的技巧和最佳实践! [^2]: MyBatis-Plus 实用功能使用指南。MyBatis-Plus 提供了许多实用的功能,可以帮助开发者提高编码效率和质量。通过本文介绍的 SQL 日志打印、主键生成策略、逻辑删除、字段类型处理以及多租户支持等功能,我们可以更好地利用 MyBatis-Plus 来简化开发过程,提升工作效率。希望这些示例和解释对你有所帮助!!! [^3]: MyBatis-Plus 配置与使用指南。使用 MyBatis-Plus 注解映射数据库表结构: import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; /** * 用户实体类(对应数据库表 tb_user) */ @TableName("user") // 指定数据库表名(若与实体类名+前缀一致可省略) public class User { @TableId(type = IdType.AUTO) // 主键自增策略 private Long id; private String name; // 姓名 private Integer age; // 年龄 } 四、创建 Mapper 接口。 [^4]: 代码生成器(新):mybatis-plus-generator使用指南。提醒:LearnElementUiAndSpringBoot下载完后,在运行调试 Main.java里的main方法之前,除了utils包和Main.java文件,其他包需要先删除,否则会看不出自动生成的代码 快速入门 一、pom.xml添加依赖 <!-- velocity官网 https://velocity.apache.org/engine/devel/user-guide.html https://velocity.apache.org/download.cgi#engine --> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.3</version> </dependency> <!-- 代码生成器新 https://baomidou.com/pages/779a6e/#%E5%AE%89%E8%A3%85 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.5</version> </dependency> 二、使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值