前言
在上一章中,我们搭建了项目的基础架构。本章将深入探讨注解的设计艺术,学习如何创建既强大又易用的API接口。好的注解设计是框架成功的关键,它直接影响到开发者的使用体验。
Java注解的元数据编程思想
什么是元数据编程?
元数据编程是一种用数据描述数据的编程范式。在Java中,注解就是一种特殊的元数据,它可以:
// 传统方式:代码与配置混合
public class UserService {
public void saveUser(User user) {
Logger logger = LoggerFactory.getLogger(this.getClass());
logger.info("开始保存用户"); // 硬编码日志逻辑
// 业务逻辑
logger.info("保存用户完成");
}
}
// 注解方式:元数据描述行为
public class UserService {
@LogMethod(
level = LogLevel.INFO,
startMessage = "开始保存用户:{}",
successMessage = "保存用户完成:{}"
)
public void saveUser(User user) {
// 纯粹的业务逻辑
}
}
元数据编程的优势:
- 📝 声明式:通过声明而非编程描述行为
- 🔄 可重用:同一套元数据可以被多种处理器使用
- 🎯 关注点分离:业务逻辑与横切关注点解耦
- 🛠️ 工具友好:IDE和框架可以更好地理解代码意图
核心注解设计
1. @LogMethod - 方法级精确控制
这是我们框架的核心注解,用于精确控制单个方法的日志行为:
package com.simpleflow.log.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 方法级日志注解
*
* 使用示例:
* <pre>
* {@code
* @LogMethod(
* level = LogLevel.INFO,
* logArgs = true,
* logResult = true,
* startMessage = "开始执行用户查询,参数:{}",
* successMessage = "用户查询成功,耗时:{}ms",
* sensitiveFields = {"password", "idCard"}
* )
* public User findUser(String username, String password) {
* // 业务逻辑
* }
* }
* </pre>
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogMethod {
/**
* 日志级别
* @return 日志级别,默认INFO
*/
LogLevel level() default LogLevel.INFO;
/**
* 是否记录方法参数
* @return true表示记录参数,默认true
*/
boolean logArgs() default true;
/**
* 是否记录方法返回值
* @return true表示记录返回值,默认true
*/
boolean logResult() default true;
/**
* 是否记录方法执行时间
* @return true表示记录执行时间,默认true
*/
boolean logExecutionTime() default true;
/**
* 是否记录异常信息
* @return true表示记录异常,默认true
*/
boolean logException() default true;
/**
* 日志前缀,用于标识特定的业务模块
* @return 日志前缀字符串
*/
String prefix() default "";
/**
* 方法开始执行时的日志模板
* 支持占位符:{},可使用SpEL表达式
* @return 开始消息模板
*/
String startMessage() default "";
/**
* 方法成功执行完成时的日志模板
* @return 成功消息模板
*/
String successMessage() default "";
/**
* 方法执行异常时的日志模板
* @return 异常消息模板
*/
String errorMessage() default "";
/**
* 是否启用SpEL表达式解析
* @return true表示启用SpEL,默认true
*/
boolean enableSpel() default true;
/**
* 是否在日志中包含请求ID
* @return true表示包含RequestId,默认true
*/
boolean includeRequestId() default true;
/**
* 排除的参数索引(从0开始)
* 指定的参数将不会被记录到日志中
* @return 排除的参数索引数组
*/
int[] excludeArgs() default {};
/**
* 敏感字段名称数组
* 这些字段在日志中将被脱敏处理
* @return 敏感字段数组
*/
String[] sensitiveFields() default {};
}
2. LogLevel - 日志级别枚举
package com.simpleflow.log.annotation;
/**
* 日志级别枚举
*
* 对应SLF4J的日志级别,便于统一管理
*/
public enum LogLevel {
/**
* 跟踪级别 - 最详细的日志信息
*/
TRACE("TRACE", 0),
/**
* 调试级别 - 调试信息
*/
DEBUG("DEBUG", 1),
/**
* 信息级别 - 一般信息
*/
INFO("INFO", 2),
/**
* 警告级别 - 警告信息
*/
WARN("WARN", 3),
/**
* 错误级别 - 错误信息
*/
ERROR("ERROR", 4);
private final String name;
private final int level;
LogLevel(String name, int level) {
this.name = name;
this.level = level;
}
public String getName() {
return name;
}
public int getLevel() {
return level;
}
/**
* 判断当前级别是否启用
*/
public boolean isEnabled(LogLevel targetLevel) {
return this.level >= targetLevel.level;
}
/**
* 从字符串转换为枚举
*/
public static LogLevel fromString(String level) {
if (level == null) {
return INFO;
}
try {
return LogLevel.valueOf(level.toUpperCase());
} catch (IllegalArgumentException e) {
return INFO;
}
}
}
3. @LogClass - 类级统一配置
package com.simpleflow.log.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 类级日志注解
*
* 为类中的所有公共方法提供统一的日志配置。
* 方法级的@LogMethod注解会覆盖类级配置。
*
* 使用示例:
* <pre>
* {@code
* @LogClass(
* level = LogLevel.INFO,
* prefix = "用户服务",
* includeMethods = {"find.*", "save.*"},
* excludeMethods = {"get.*", "set.*"},
* logArgs = true,
* logResult = false
* )
* public class UserService {
* // 所有方法都会应用类级配置
* public User findById(Long id) { }
*
* // 可以通过方法级注解覆盖类级配置
* @LogMethod(logResult = true)
* public void saveUser(User user) { }
* }
* }
* </pre>
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogClass {
/**
* 默认日志级别
*/
LogLevel level() default LogLevel.INFO;
/**
* 是否记录方法参数
*/
boolean logArgs() default true;
/**
* 是否记录方法返回值
*/
boolean logResult() default true;
/**
* 是否记录方法执行时间
*/
boolean logExecutionTime() default true;
/**
* 是否记录异常信息
*/
boolean logException() default true;
/**
* 类级日志前缀
*/
String prefix() default "";
/**
* 默认的开始消息模板
*/
String startMessage() default "";
/**
* 默认的成功消息模板
*/
String successMessage() default "";
/**
* 默认的异常消息模板
*/
String errorMessage() default "";
/**
* 是否启用SpEL表达式
*/
boolean enableSpel() default true;
/**
* 是否包含请求ID
*/
boolean includeRequestId() default true;
/**
* 全局敏感字段配置
*/
String[] sensitiveFields() default {};
/**
* 包含的方法名模式(支持正则表达式)
* 空数组表示包含所有方法
* @return 方法名模式数组
*/
String[] includeMethods() default {};
/**
* 排除的方法名模式(支持正则表达式)
* @return 排除的方法名模式数组
*/
String[] excludeMethods() default {};
/**
* 是否包含私有方法
* @return true表示包含私有方法,默认false
*/
boolean includePrivateMethods() default false;
/**
* 是否包含getter/setter方法
* @return true表示包含getter/setter,默认false
*/
boolean includeGetterSetter() default false;
}
4. @LogIgnore - 排除特定方法
package com.simpleflow.log.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 日志忽略注解
*
* 用于排除特定方法的日志记录。
* 当类上有@LogClass注解时,可以使用此注解排除特定方法。
*
* 使用示例:
* <pre>
* {@code
* @LogClass
* public class UserService {
*
* public User findById(Long id) {
* // 这个方法会记录日志
* }
*
* @LogIgnore(reason = "内部工具方法,无需记录日志")
* private String formatUserName(String name) {
* // 这个方法不会记录日志
* }
* }
* }
* </pre>
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogIgnore {
/**
* 忽略原因说明
* 用于文档化为什么要忽略此方法的日志记录
* @return 忽略原因
*/
String reason() default "";
}
注解设计的最佳实践
1. 合理的默认值策略
// ✅ 好的设计:提供合理的默认值
@LogMethod // 使用默认配置即可工作
public User findUser(Long id) {
return userRepository.findById(id);
}
// ✅ 需要时才自定义
@LogMethod(
level = LogLevel.WARN, // 只覆盖需要的属性
sensitiveFields = {"password"} // 其他使用默认值
)
public boolean authenticate(String username, String password) {
// 认证逻辑
}
2. 注解的继承和覆盖机制
// 类级配置作为默认值
@LogClass(
level = LogLevel.INFO,
logArgs = true,
logResult = false,
prefix = "用户服务"
)
public class UserService {
// 继承类级配置
public List<User> findAll() {
// level=INFO, logArgs=true, logResult=false, prefix="用户服务"
}
// 方法级配置覆盖类级配置
@LogMethod(logResult = true) // 只覆盖logResult
public User findById(Long id) {
// level=INFO, logArgs=true, logResult=true, prefix="用户服务"
}
// 完全忽略日志
@LogIgnore(reason = "内部工具方法")
private String formatId(Long id) {
return "ID:" + id;
}
}
3. 配置优先级规则
我们设计的优先级规则如下:
优先级说明:
- @LogIgnore - 绝对优先,直接排除
- @LogMethod - 方法级配置,覆盖其他配置
- @LogClass - 类级配置,作为方法的默认值
- 框架默认 - 当没有任何配置时使用
实战演示
1. 创建测试用例
package com.simpleflow.log.annotation;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.lang.reflect.Method;
/**
* 注解功能测试
*/
class AnnotationTest {
@Test
void testLogMethodAnnotation() throws Exception {
Method method = TestService.class.getMethod("findUser", Long.class);
assertTrue(method.isAnnotationPresent(LogMethod.class));
LogMethod logMethod = method.getAnnotation(LogMethod.class);
assertEquals(LogLevel.INFO, logMethod.level());
assertTrue(logMethod.logArgs());
assertTrue(logMethod.logResult());
assertEquals("查询用户", logMethod.prefix());
}
@Test
void testLogClassAnnotation() {
Class<?> clazz = TestService.class;
assertTrue(clazz.isAnnotationPresent(LogClass.class));
LogClass logClass = clazz.getAnnotation(LogClass.class);
assertEquals(LogLevel.WARN, logClass.level());
assertArrayEquals(new String[]{"find.*"}, logClass.includeMethods());
}
@Test
void testLogIgnoreAnnotation() throws Exception {
Method method = TestService.class.getMethod("internalMethod");
assertTrue(method.isAnnotationPresent(LogIgnore.class));
LogIgnore logIgnore = method.getAnnotation(LogIgnore.class);
assertEquals("内部方法", logIgnore.reason());
}
// 测试用的服务类
@LogClass(
level = LogLevel.WARN,
includeMethods = {"find.*"},
prefix = "测试服务"
)
static class TestService {
@LogMethod(
level = LogLevel.INFO,
logArgs = true,
logResult = true,
prefix = "查询用户"
)
public User findUser(Long id) {
return new User(id, "测试用户");
}
@LogIgnore(reason = "内部方法")
public void internalMethod() {
// 内部逻辑
}
}
// 简单的用户类
static class User {
private Long id;
private String name;
public User(Long id, String name) {
this.id = id;
this.name = name;
}
// getters and setters...
}
}
2. 运行测试验证
mvn test -Dtest=AnnotationTest
高级特性设计
1. SpEL表达式支持
我们的注解支持Spring Expression Language,让日志模板更加灵活:
@LogMethod(
startMessage = "用户 #{#username} 开始登录",
successMessage = "用户 #{#result.username} 登录成功,权限:#{#result.roles}",
enableSpel = true
)
public LoginResult login(String username, String password) {
// 认证逻辑
}
2. 敏感信息脱敏
通过配置敏感字段,自动对日志进行脱敏处理:
@LogMethod(
sensitiveFields = {"password", "idCard", "phone"},
logArgs = true
)
public User registerUser(UserRegistrationRequest request) {
// 注册逻辑
// 日志中 password 会显示为 "******"
}
3. 条件化日志记录
// 根据方法名模式包含/排除
@LogClass(
includeMethods = {"save.*", "update.*", "delete.*"}, // 只记录写操作
excludeMethods = {"get.*", "is.*"}, // 排除简单查询
includeGetterSetter = false // 排除getter/setter
)
public class UserService {
// 配置会自动应用到匹配的方法
}
本章小结
✅ 完成的任务
- 深入理解:学习了元数据编程思想
- 注解设计:完成了四个核心注解的设计
- 最佳实践:掌握了注解设计的原则和技巧
- 测试验证:编写测试用例验证注解功能
- 高级特性:设计了SpEL支持和脱敏功能
🎯 学习要点
- 元数据编程的核心思想和优势
- 注解属性设计的合理性和易用性
- 默认值策略的重要性
- 配置继承和覆盖的优先级规则
- 测试驱动的开发方式
💡 思考题
- 为什么要设计类级和方法级两种注解?
- 如何平衡注解的功能性和易用性?
- 敏感信息脱敏还可以有哪些实现方式?
🚀 下章预告
下一章我们将学习配置管理的艺术,探讨如何设计一个灵活的配置体系,包括配置解析、合并策略、默认值处理等核心机制。我们将实现LogConfig类和AnnotationConfigResolver解析器。
💡 设计原则: 好的注解应该是易懂、易用、易扩展的。记住:框架的API设计直接决定了开发者的使用体验!