1. 为什么需要自定义注解
在Spring Boot项目中,我们经常会看到各种内置注解,比如@Controller、@Service、@Autowired等。这些注解极大地简化了开发流程,但有时候我们需要一些特定的业务逻辑标记,这时候自定义注解就派上用场了。
自定义注解本质上是一种元数据,它本身不包含任何业务逻辑,但可以通过反射机制在运行时被读取和处理。想象一下,如果你需要在方法执行前后记录日志、校验权限、监控性能,或者实现特定的业务规则,自定义注解可以让这些横切关注点与核心业务逻辑解耦。
我在实际项目中遇到过这样一个场景:需要为不同API接口设置不同的访问频率限制。如果直接在代码中硬编码这些限制规则,不仅难以维护,还会让业务代码变得臃肿。通过自定义@RateLimit注解,我们能够优雅地解决这个问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 注解的基本语法
2.1 定义注解的元注解
在Java中,定义一个新的注解需要使用@interface关键字,但在此之前,我们需要了解几个关键的元注解(用于注解其他注解的注解):
java复制@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCustomAnnotation {
String value() default "";
int priority() default 0;
}
这里有两个重要的元注解:
-
@Target:指定注解可以应用在哪些地方,常见的有:ElementType.METHOD:方法级别ElementType.TYPE:类或接口ElementType.FIELD:字段ElementType.PARAMETER:方法参数
-
@Retention:指定注解的保留策略:RetentionPolicy.SOURCE:仅存在于源码中,编译时丢弃RetentionPolicy.CLASS:保留到class文件,但运行时不可见(默认)RetentionPolicy.RUNTIME:运行时可通过反射获取
2.2 注解元素的定义
注解中可以定义各种元素(看起来像方法,实际上是注解的属性):
java复制public @interface ApiVersion {
String[] supportedVersions();
boolean deprecated() default false;
String changeLog() default "No changes";
}
使用示例:
java复制@ApiVersion(
supportedVersions = {"v1", "v2"},
deprecated = true,
changeLog = "Migrate to v3 API"
)
public class MyController {
// ...
}
注意:注解元素必须是无参的,不能有throws子句,返回类型只能是基本类型、String、Class、枚举、注解或这些类型的数组。
3. 在Spring Boot中处理自定义注解
3.1 通过AOP处理注解
Spring AOP是处理自定义注解最常用的方式。假设我们要实现一个@LogExecutionTime注解来记录方法执行时间:
java复制@Aspect
@Component
public class LogExecutionTimeAspect {
private static final Logger logger = LoggerFactory.getLogger(LogExecutionTimeAspect.class);
@Around("@annotation(LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object proceed = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - startTime;
logger.info("{} executed in {} ms",
joinPoint.getSignature(),
executionTime);
return proceed;
}
}
对应的注解定义:
java复制@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
}
使用示例:
java复制@Service
public class MyService {
@LogExecutionTime
public void performLongRunningTask() {
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
3.2 通过BeanPostProcessor处理注解
对于需要在Bean初始化阶段处理的逻辑,可以实现BeanPostProcessor:
java复制@Component
public class MyAnnotationProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
Class<?> clazz = bean.getClass();
// 处理类级别的注解
if (clazz.isAnnotationPresent(MyClassAnnotation.class)) {
MyClassAnnotation annotation = clazz.getAnnotation(MyClassAnnotation.class);
// 处理注解逻辑
}
// 处理方法级别的注解
for (Method method : clazz.getMethods()) {
if (method.isAnnotationPresent(MyMethodAnnotation.class)) {
MyMethodAnnotation annotation = method.getAnnotation(MyMethodAnnotation.class);
// 处理注解逻辑
}
}
return bean;
}
}
4. 实战案例:实现权限控制注解
让我们实现一个完整的@PreAuthorize注解,类似于Spring Security的功能但更简单:
4.1 定义注解
java复制@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAuthorize {
String[] roles() default {};
String[] permissions() default {};
}
4.2 实现AOP处理
java复制@Aspect
@Component
public class AuthorizationAspect {
@Autowired
private UserService userService;
@Before("@annotation(preAuthorize)")
public void checkAuthorization(JoinPoint joinPoint, PreAuthorize preAuthorize) {
// 获取当前用户
User currentUser = userService.getCurrentUser();
// 检查角色
if (preAuthorize.roles().length > 0) {
boolean hasRole = Arrays.stream(preAuthorize.roles())
.anyMatch(role -> currentUser.getRoles().contains(role));
if (!hasRole) {
throw new AccessDeniedException("Insufficient roles");
}
}
// 检查权限
if (preAuthorize.permissions().length > 0) {
boolean hasPermission = Arrays.stream(preAuthorize.permissions())
.allMatch(perm -> currentUser.getPermissions().contains(perm));
if (!hasPermission) {
throw new AccessDeniedException("Insufficient permissions");
}
}
}
}
4.3 使用示例
java复制@RestController
@RequestMapping("/api/admin")
public class AdminController {
@PreAuthorize(roles = {"ADMIN"})
@GetMapping("/users")
public List<User> getAllUsers() {
return userService.findAllUsers();
}
@PreAuthorize(permissions = {"user:delete"})
@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
}
}
5. 高级应用:组合注解与元注解
Spring框架大量使用了元注解模式,我们也可以创建自己的组合注解:
5.1 创建组合注解
java复制@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@PreAuthorize(roles = {"ADMIN"})
@LogExecutionTime
public @interface AdminOperation {
String description() default "";
}
5.2 使用组合注解
java复制@RestController
@RequestMapping("/api/admin")
public class AdminController {
@AdminOperation(description = "获取所有管理员用户")
@GetMapping("/admins")
public List<User> getAllAdmins() {
return userService.findAdmins();
}
}
这个@AdminOperation注解同时具备了权限检查和执行时间记录的功能,而且语义更加明确。
6. 性能考虑与最佳实践
6.1 反射性能优化
频繁使用反射会影响性能,特别是在高并发场景下。有几种优化策略:
- 缓存反射结果:将
getAnnotation()、getMethods()等反射操作的结果缓存起来 - 使用AnnotationUtils:Spring提供的
AnnotationUtils比JDK原生反射更高效 - 编译时处理:考虑使用注解处理器(APT)在编译时处理
6.2 最佳实践
- 保持注解简单:注解应该只包含配置信息,不包含复杂逻辑
- 明确文档:为自定义注解编写清晰的文档说明
- 合理命名:注解名称应该直观表达其用途
- 避免过度使用:不是所有场景都需要自定义注解
- 考虑可测试性:确保注解逻辑可以被单元测试覆盖
7. 常见问题与解决方案
7.1 注解不生效的可能原因
- 保留策略不正确:确保
@Retention设置为RUNTIME - 目标类型不匹配:检查
@Target是否包含你使用的元素类型 - Spring未扫描到:确保注解处理类在组件扫描路径下
- AOP代理问题:自调用方法上的注解可能不生效
7.2 调试技巧
- 使用
AnnotationUtils.findAnnotation()查找注解 - 检查Spring的代理机制:
AopUtils.isAopProxy() - 使用调试器查看方法/类上的实际注解
8. 扩展应用场景
8.1 数据校验注解
java复制@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface ValidPhoneNumber {
String message() default "Invalid phone number";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String region() default "CN";
}
8.2 缓存注解
java复制@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheResult {
String cacheName();
long ttl() default 3600; // 默认1小时
String key() default ""; // 支持SpEL表达式
}
8.3 分布式锁注解
java复制@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
String lockKey();
long waitTime() default 5; // 获取锁等待时间(秒)
long leaseTime() default 30; // 持有锁时间(秒)
}
在实际项目中,我发现自定义注解特别适合处理横切关注点(cross-cutting concerns)。比如我们曾经用注解实现了:
- 自动重试机制(
@RetryOnFailure) - 接口版本控制(
@ApiVersion) - 敏感数据脱敏(
@SensitiveData) - 操作日志记录(
@OperationLog)
每个注解都让代码更加清晰,业务逻辑与技术实现更好地分离。不过也要注意,过度使用注解会让代码变得"神奇",增加理解成本。我的经验法则是:当某个模式在代码中重复出现三次以上,才考虑用注解来抽象它。
