问题

自定义注解有哪些场景?如何实现?

答案

一、核心概念

注解(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

六、答题总结

面试回答要点:

  1. 应用场景:AOP 日志、参数校验、权限控制、接口文档、ORM 映射等
  2. 实现步骤
    • 定义注解(使用 @interface + 元注解)
    • 标注目标元素(类/方法/字段)
    • 解析注解(反射 + AOP 或编译时处理器)
  3. 核心原理
    • 运行时通过反射获取注解信息(Method.getAnnotation()
    • 编译时通过 APT(Annotation Processing Tool)处理
  4. 最佳实践
    • 使用 Spring AOP 简化切面逻辑
    • 缓存反射结果提升性能
    • 遵循单一职责原则

典型示例:

  • Spring 的 @Transactional(事务管理)
  • Lombok 的 @Data(编译时代码生成)
  • Hibernate Validator 的 @NotNull(参数校验)