问题
自定义注解有哪些场景?如何实现?
答案
一、核心概念
注解(Annotation) 是 Java 5 引入的元数据机制,用于为代码添加声明式配置,本质是一个继承了 java.lang.annotation.Annotation 的特殊接口。
关键特性:
- 不直接影响代码逻辑,需要配合反射或编译时处理器使用
- 可以标注在类、方法、字段、参数等元素上
- 通过元注解(Meta-Annotation)控制注解的行为
二、常见应用场景
| 场景 | 典型注解 | 说明 |
|---|---|---|
| AOP 切面编程 | @Log、@Transactional | 日志记录、事务管理、性能监控 |
| 参数校验 | @NotNull、@Valid | Bean Validation 框架 |
| 权限控制 | @RequiresPermission、@PreAuthorize | 接口鉴权、角色校验 |
| 接口文档 | @ApiOperation、@ApiParam | Swagger/OpenAPI 文档生成 |
| ORM 映射 | @Table、@Column | JPA/MyBatis 实体映射 |
| 依赖注入 | @Autowired、@Resource | Spring IoC 容器 |
| 测试框架 | @Test、@BeforeEach | JUnit/TestNG 测试标记 |
三、实现步骤与示例
1. 定义注解
import java.lang.annotation.*;
/**
* 自定义日志注解
* 用于标记需要记录操作日志的方法
*/
@Target(ElementType.METHOD) // 作用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时保留
@Documented // 包含在 JavaDoc 中
public @interface OperationLog {
/**
* 操作模块
*/
String module() default "";
/**
* 操作类型(如:新增、修改、删除)
*/
String type() default "查询";
/**
* 是否记录请求参数
*/
boolean recordParams() default true;
}
元注解说明:
| 元注解 | 作用 |
|---|---|
@Target | 指定注解可以标注的位置(TYPE/METHOD/FIELD/PARAMETER 等) |
@Retention | 指定注解保留策略(SOURCE/CLASS/RUNTIME) |
@Documented | 注解信息包含在 JavaDoc 中 |
@Inherited | 子类可以继承父类的注解 |
2. 使用注解
@RestController
@RequestMapping("/user")
public class UserController {
@OperationLog(module = "用户管理", type = "新增")
@PostMapping("/add")
public Result addUser(@RequestBody User user) {
// 业务逻辑
return Result.success();
}
@OperationLog(module = "用户管理", type = "删除", recordParams = false)
@DeleteMapping("/{id}")
public Result deleteUser(@PathVariable Long id) {
// 业务逻辑
return Result.success();
}
}
3. 解析注解(两种方式)
方式一:反射解析(运行时)
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
/**
* 日志切面处理器
*/
@Aspect
@Component
public class OperationLogAspect {
@Around("@annotation(com.example.annotation.OperationLog)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 获取方法签名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 2. 获取注解信息
OperationLog annotation = method.getAnnotation(OperationLog.class);
String module = annotation.module();
String type = annotation.type();
boolean recordParams = annotation.recordParams();
// 3. 记录日志
long startTime = System.currentTimeMillis();
System.out.println("=== 操作日志 ===");
System.out.println("模块: " + module);
System.out.println("类型: " + type);
if (recordParams) {
Object[] args = joinPoint.getArgs();
System.out.println("参数: " + Arrays.toString(args));
}
// 4. 执行目标方法
Object result = joinPoint.proceed();
// 5. 记录耗时
long endTime = System.currentTimeMillis();
System.out.println("耗时: " + (endTime - startTime) + "ms");
return result;
}
}
方式二:编译时处理(APT)
import javax.annotation.processing.*;
import javax.lang.model.element.*;
import java.util.Set;
/**
* 编译时注解处理器
* 用于在编译期生成代码或校验
*/
@SupportedAnnotationTypes("com.example.annotation.OperationLog")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class OperationLogProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(OperationLog.class)) {
if (element.getKind() == ElementKind.METHOD) {
OperationLog annotation = element.getAnnotation(OperationLog.class);
// 编译期校验或代码生成
System.out.println("检测到日志注解: " + annotation.module());
}
}
return true;
}
}
四、进阶场景:组合注解
/**
* 组合注解:同时标记事务和日志
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Transactional(rollbackFor = Exception.class)
@OperationLog(type = "修改")
public @interface TransactionalLog {
String module();
}
// 使用
@TransactionalLog(module = "订单管理")
public void updateOrder(Order order) {
// 自动具备事务和日志功能
}
五、性能与最佳实践
1. 性能考量
- 反射解析有一定开销,避免在高频方法中过度使用
- 使用缓存优化:将解析结果缓存到
ConcurrentHashMap
private static final Map<Method, OperationLog> CACHE = new ConcurrentHashMap<>();
public OperationLog getAnnotation(Method method) {
return CACHE.computeIfAbsent(method,
m -> m.getAnnotation(OperationLog.class));
}
2. 线程安全
- 注解本身是不可变的,天然线程安全
- 注解处理器需要注意共享状态的并发问题
3. 设计原则
- 单一职责:一个注解只做一件事
- 合理默认值:减少使用者的配置负担
- 清晰命名:注解名应表达明确意图(如
@Cacheable而非@Cache)
六、答题总结
面试回答要点:
- 应用场景:AOP 日志、参数校验、权限控制、接口文档、ORM 映射等
- 实现步骤:
- 定义注解(使用
@interface+ 元注解) - 标注目标元素(类/方法/字段)
- 解析注解(反射 + AOP 或编译时处理器)
- 定义注解(使用
- 核心原理:
- 运行时通过反射获取注解信息(
Method.getAnnotation()) - 编译时通过 APT(Annotation Processing Tool)处理
- 运行时通过反射获取注解信息(
- 最佳实践:
- 使用 Spring AOP 简化切面逻辑
- 缓存反射结果提升性能
- 遵循单一职责原则
典型示例:
- Spring 的
@Transactional(事务管理) - Lombok 的
@Data(编译时代码生成) - Hibernate Validator 的
@NotNull(参数校验)