1. 项目缘起:为什么我要整理这份Java注解大全
干了十多年Java开发,从早期的SSH框架到现在的Spring Boot微服务,注解(Annotation)这东西真是无处不在。我记得刚入行那会儿,看到代码里一堆@Override、@Deprecated还觉得挺新鲜,后来项目里到处都是@Controller、@Autowired、@Transactional,再到现在各种自定义注解满天飞。说实话,注解用好了是真方便,能让代码简洁得像首诗;但要是没吃透,踩起坑来也真是要命,比如那个经典的@Transactional不生效问题,我猜不少人都遇到过。
这个“Java注解大全”项目,其实是我给自己搞的一个复习笔记。市面上资料很多,但要么太散,要么太旧,要么就是光讲语法不讲“为什么”。我寻思着,不如自己动手整理一份,把工作中常用的、面试常问的、以及那些容易让人迷糊的注解都捋清楚。这不是一份死板的API文档,而是融合了我个人实战经验和深度思考的“内功心法”。我会持续往里面加东西,所以叫“不断增加”。希望这份笔记不仅能帮我巩固知识,也能给正在学习或复习Java的你,提供一个有温度、有深度的参考。
2. 注解的本质:超越标记的元编程利器
很多人把注解简单地理解为“标签”或者“标记”,这没错,但只看到了第一层。在我看来,注解是Java元编程(Metaprogramming)能力的一种体现。所谓元编程,就是“编写能操作程序的程序”。注解本身不直接参与业务逻辑,但它能向编译器、JVM或者各种框架工具(如Spring、Lombok)传递信息,从而影响它们的行为,间接地改变了程序的编译过程、运行时状态甚至生成新的代码。
2.1 注解的底层机制与生命周期
理解注解,必须从它的生命周期(RetentionPolicy)说起。这是注解定义时@Retention元注解指定的,它决定了注解信息能“活”多久:
SOURCE:仅存在于源代码阶段。编译成
.class文件后就被丢弃了。最典型的例子是@Override和@Deprecated。它们的作用就是给编译器看的。@Override告诉编译器:“喂,检查一下我这个方法是不是真的重写了父类方法,别让我写错了。”@Deprecated则警告开发者:“这个方法过时了,别再用啦,我有更好的替代品。” 一旦编译完成,它们的任务就结束了。CLASS:注解信息会被保留在
.class文件中,但不会被加载到JVM里。这是默认的保留策略。这个策略比较尴尬,用得不多。一些在编译后还需要被其他工具(如字节码分析工具)处理,但运行时不需要的注解可能会用这个。RUNTIME:这是最常用、最强大的策略。注解信息不仅存在于
.class文件中,还会在JVM加载类时被读取到内存中。因此,我们可以在程序运行时,通过反射(Reflection)API动态地获取注解信息,并据此做出相应的处理。Spring的@Controller、@Autowired,JUnit的@Test,都是RUNTIME级别的。
实操心得:当你自定义一个注解时,第一个要问自己的问题就是:“我这个注解是给谁用的?编译器?某个编译期工具?还是运行时的我自己/框架?” 答案决定了你的
@Retention应该选哪个。99%的业务场景下,如果你希望注解在运行时起作用,那就选RUNTIME。
2.2 元注解:注解的注解
就像类是对象的模板,元注解(Meta-Annotation)就是注解的模板。JDK内置了5个元注解,用来修饰其他注解的定义:
@Retention:上面刚讲过,定义注解的生命周期。@Target:指定注解可以应用在哪些Java元素上。这是个ElementType数组,常用值有:TYPE:类、接口、枚举FIELD:字段(包括枚举常量)METHOD:方法PARAMETER:方法参数CONSTRUCTOR:构造器LOCAL_VARIABLE:局部变量ANNOTATION_TYPE:注解类型(用于元注解)PACKAGE:包 例如,@Autowired的@Target是{ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD},意味着它可以用在这四个地方。
@Documented:被它修饰的注解,会被javadoc工具提取到API文档中。如果你希望自定义的注解出现在文档里,就加上它。@Inherited:这是一个容易被忽略但有时很有用的元注解。它表示注解是否具有继承性。如果一个类用了被@Inherited修饰的注解,那么它的子类会自动继承这个注解(前提是子类没有被其他注解覆盖)。注意,这只对@Target(ElementType.TYPE)的类注解有效,对方法、字段注解无效。@Repeatable(JDK 1.8引入):允许同一个注解在同一个地方重复使用。在JDK 1.8之前,同一个注解在一个地方只能用一次。有了@Repeatable,你需要为注解定义一个“容器注解”。例如,你可以定义一个@Schedule注解,并用@Repeatable(Schedules.class)修饰,这样你就可以在同一个方法上写多个@Schedule了。
3. JDK内置核心注解深度解析
别看JDK自带的注解不多,但每一个都是精华,理解透了能避免很多低级错误。
3.1@Override:不仅仅是“重写”检查
public class Parent { public void doSomething(String input) { // ... } } public class Child extends Parent { @Override // 正确:方法签名匹配 public void doSomething(String input) { // ... } @Override // 编译错误!方法名拼写错误,实际上不是重写 public void doSomethng(String input) { // ... } @Override // 编译错误!参数类型不匹配,是重载(Overload)不是重写 public void doSomething(Integer input) { // ... } }为什么一定要用@Override?
- 安全网:如上例,它能帮你第一时间发现拼写错误或签名不匹配,避免你以为重写了父类方法,实际上却定义了一个新方法,导致多态行为不符合预期。这种Bug非常隐蔽。
- 提高可读性:明确告诉阅读代码的人,这是一个重写方法,不是类自己新加的方法。
- 应对父类变更:如果父类将来删除了这个方法,所有加了
@Override的子类方法会立刻编译报错,迫使你同步修改,保证了代码的同步性。
注意事项:从JDK 1.6开始,
@Override不仅可以用于重写父类方法,还可以用于实现接口方法。但在1.5及以前,用于实现接口方法会编译报错。如果你的项目还在用很老的JDK(虽然不推荐),需要注意这一点。
3.2@Deprecated:优雅的API退役声明
标记一个类、方法或字段已过时,不建议继续使用。
/** * 老旧的连接方式,性能较差。 * 请使用新的 {@link #connectWithPool()} 方法。 * @deprecated 自 v2.0 起废弃,将在 v3.0 中移除。 */ @Deprecated(since = "2.0", forRemoval = true) // JDK 9+ 增强 public void connect() { // ... 旧逻辑 }since:从哪个版本开始废弃。forRemoval:为true表示计划在将来移除,警告级别更高。- 最佳实践:务必在javadoc中使用
@deprecated标签(注意小写d)说明废弃原因和替代方案。光加一个注解是极其不负责的。
3.3@SuppressWarnings:压制编译器警告
用来有选择地关闭编译器警告。警告通常是有用的,但在某些确知安全的场景下,警告信息会干扰视线。
@SuppressWarnings("unchecked") // 我知道这个强制转换是安全的 public List<String> getRawList() { List rawList = someLegacyCode(); return (List<String>) rawList; // 未经检查的转换,会产生警告 } // 可以同时压制多种警告 @SuppressWarnings({"unchecked", "rawtypes", "deprecation"}) public void someMethod() { // ... }常用参数值:
"unchecked":抑制未经检查的转换警告(如使用泛型集合)。"rawtypes":抑制使用原始类型的警告。"deprecation":抑制使用已废弃API的警告。"all":抑制所有警告(慎用!)。
重要原则:
@SuppressWarnings的作用范围应尽可能小。不要把它加在整个类上,最好只加在方法上,甚至只加在局部变量声明语句上。并且,一定要写注释说明为什么这里可以安全地忽略警告。
3.4@FunctionalInterface:函数式接口的契约
JDK 1.8引入,标记一个接口是函数式接口(Functional Interface)。函数式接口指有且仅有一个抽象方法的接口(不包括default方法和从Object类继承的方法)。
@FunctionalInterface // 加上它,编译器会帮你检查是否符合函数式接口定义 public interface MyCalculator { int calculate(int a, int b); // 唯一的抽象方法 // default方法不影响 default void printResult(int result) { System.out.println("Result: " + result); } // 从Object继承的方法不影响 @Override boolean equals(Object obj); } // 如果没有@FunctionalInterface,但符合定义,它依然是函数式接口,可以用Lambda。 // 但加上注解是更好的实践,相当于一份契约和文档,防止他人意外添加第二个抽象方法。为什么需要它?它是一种编译期的“契约检查”和“文档说明”。告诉编译器和代码阅读者:“我设计这个接口就是为了给Lambda表达式或方法引用用的,你别乱加抽象方法破坏这个约定。”
3.5@SafeVarargs:泛型可变参数的安全承诺
这个注解有点冷门,但涉及泛型和可变参数时很重要。在Java中,泛型信息在运行时会被擦除。当可变参数遇到泛型时,容易产生“堆污染”(Heap Pollution)警告。
public class SafeVarargsDemo { // 没有 @SafeVarargs,编译器会警告:Possible heap pollution from parameterized vararg type T @SafeVarargs // 加上它,告诉编译器:“我保证这个方法体内部不会对泛型数组进行不安全的操作。” public static <T> void printAll(T... elements) { for (T element : elements) { System.out.println(element); } } // 不安全的例子:将外部传入的泛型数组暴露出去或进行错误赋值 @SafeVarargs // 错误使用!这里实际上不安全。 public static <T> void unsafeMerge(List<T>... lists) { Object[] array = lists; // 泛型数组向上转型为Object[],这是允许的 array[0] = Arrays.asList(42); // 污染!将Integer的List放入原本是List<String>的数组中 // 如果外部调用 unsafeMerge(listOfStrings, listOfIntegers); 就会在后续操作中抛出ClassCastException } }使用条件:@SafeVarargs只能用在static、final或private的构造器或方法上。因为非static、非final、非private的方法可能被重写,无法保证其安全性。
4. Lombok注解:告别样板代码的“魔法”
Lombok通过注解在编译期自动生成字节码,极大减少了getter、setter、构造器等样板代码。但“魔法”用不好,也会带来困惑。
4.1 基础注解:@Data,@Getter/@Setter,@ToString等
@Data:一个复合注解,相当于@Getter+@Setter+@ToString+@EqualsAndHashCode+@RequiredArgsConstructor。非常方便,但要注意它默认使用所有非static字段生成equals和hashCode,如果实体有关联对象(如List<Order>),直接使用可能导致栈溢出。这时需要用@EqualsAndHashCode.Exclude排除某些字段,或者不用@Data,自己组合需要的注解。@AllArgsConstructor/@NoArgsConstructor/@RequiredArgsConstructor:@RequiredArgsConstructor:只为final字段和标记了@NonNull的字段生成构造器。在Spring的构造函数注入场景下非常有用。
@Service @RequiredArgsConstructor // 为 final 修饰的依赖生成构造器 public class MyService { private final UserRepository userRepository; // 通过构造器注入 private final OrderService orderService; // 不需要写构造器,Lombok会生成:MyService(UserRepository ur, OrderService os){...} }
4.2 易错点与排查技巧
问题1:@RequiredArgsConstructor注解后@Lazy注解没用了?这是一个经典的Spring和Lombok结合使用的坑。场景是:你有循环依赖,想用@Lazy来延迟注入打破循环。
@Component @RequiredArgsConstructor public class ServiceA { private final @Lazy ServiceB serviceB; // 期望延迟注入 // Lombok生成的构造器:ServiceA(ServiceB serviceB) { this.serviceB = serviceB; } // 问题:Spring在调用这个构造器创建Bean时,会直接传入一个真实的ServiceB代理吗?不会! }原因:@Lazy是Spring的注解,它的生效依赖于Spring的注入方式(如字段注入@Autowired、setter注入)。而@RequiredArgsConstructor生成的是普通的构造器,Spring在通过构造器注入时,会先去容器里找ServiceB的Bean。如果ServiceB还没初始化(可能正等着ServiceA),就会触发循环依赖错误。@Lazy注解在字段上,但注入是通过构造器参数进行的,Spring对构造器参数的@Lazy支持需要特殊处理(在构造器参数上直接加@Lazy),而Lombok不会在生成的构造器参数上自动添加@Lazy。
解决方案:
- 放弃
@RequiredArgsConstructor,手动写构造器,并在参数上加@Lazy:@Component public class ServiceA { private final ServiceB serviceB; public ServiceA(@Lazy ServiceB serviceB) { // 手动写,参数加@Lazy this.serviceB = serviceB; } } - 使用字段注入或setter注入(不推荐,破坏了不可变性):
@Component public class ServiceA { @Lazy @Autowired private ServiceB serviceB; } - 重新设计,消除循环依赖(最推荐):循环依赖通常是设计有问题的信号。
问题2:java: You aren‘t using a compiler supported by lombok, so lombok will not work这个错误通常出现在IDE中。Lombok需要在编译期工作,所以IDE(如IntelliJ IDEA、Eclipse)需要安装对应的Lombok插件并启用注解处理(Annotation Processing)。
- IntelliJ IDEA:确保安装了“Lombok”插件(File -> Settings -> Plugins)。然后,在 Settings -> Build, Execution, Deployment -> Compiler -> Annotation Processors 中,勾选 “Enable annotation processing”。
- Eclipse:需要将
lombok.jar作为“Java Agent”运行一次,它会自动安装到Eclipse中。 - Maven/Gradle:确保依赖配置正确,并且IDE的构建工具配置指向了正确的JDK。
问题3:java: jps incremental annotation process is disabled. Compilation results on partial recompilations may be inaccurate.这是一个警告,不是错误。意思是JPS(Java Compiler Server)的增量注解处理被禁用了。在部分重新编译(比如只改了一个文件)时,编译结果可能不准确。这通常发生在IntelliJ IDEA中,当Lombok等注解处理器与IDEA的增量编译模式有冲突时。
- 处理:一般不影响运行。如果担心,可以在IDEA的 Settings -> Build, Execution, Deployment -> Compiler 中,将 “Build process heap size” 调大一些(比如1024或2048),或者尝试重启IDEA。也可以尝试禁用 “Use compiler process for remote external annotations” 等选项试试。最根本的解决方式是等待IDEA或Lombok插件的更新。
5. Spring/Spring Boot注解生态全景
Spring的注解体系极其庞大,是Java后端开发的核心。我们可以将其分层理解。
5.1 核心容器与Bean管理
这一层注解负责定义和组装Bean。
| 注解 | 作用 | 核心区别与选择 |
|---|---|---|
@Component | 通用泛型组件,标记一个类为Spring容器管理的Bean。 | 最基础的注解。其他如@Service、@Repository本质都是@Component,加了特定的语义和可能额外的功能(如@Repository的异常转换)。 |
@Service | 标记业务逻辑层(Service层)的组件。 | 语义化注解。代码可读性更好,Spring目前没有为其提供特殊处理,但未来可能会。 |
@Repository | 标记数据访问层(DAO层)的组件。 | 关键点:它启用了Spring的数据访问异常转换,将特定持久化技术(如JPA、Hibernate)抛出的异常统一转换为Spring的DataAccessException层次结构,使异常处理与具体技术解耦。 |
@Controller | 标记Web控制层(MVC)组件。 | 用于Spring MVC,配合@RequestMapping等使用。 |
@RestController | @Controller+@ResponseBody。 | 用于RESTful Web服务,直接返回JSON/XML数据,不找视图解析器。 |
@Configuration | 标记一个类为配置类,替代XML配置文件。 | 其内部使用@Bean注解的方法,用于显式声明Bean。 |
@Bean | 在@Configuration或@Component类的方法上,声明一个Bean。 | 用于导入第三方库的类,或需要复杂初始化逻辑的Bean。方法名默认即为Bean名称。 |
依赖注入注解:
@Autowired:Spring提供的,按类型自动装配。可以用在字段、构造器、setter方法、普通方法上。如果找到多个同类型Bean,需配合@Qualifier指定名称。@Resource(JSR-250):Java标准注解,默认按名称装配,名称找不到再按类型。name属性指定Bean名。@Inject(JSR-330):另一个Java标准,功能类似@Autowired,但无required属性。@Value:注入外部配置(如application.properties)的值或SpEL表达式。
面试高频题:
@Resource与构造函数注入哪个效率高?这个问题有点“关公战秦琼”。@Resource是字段/方法注入的一种形式。而构造函数注入是另一种注入方式。
- 从运行时效率看:微乎其微的差异,可以忽略不计。依赖注入在容器启动时完成,对运行时性能无影响。
- 从设计模式与代码质量看:构造函数注入被广泛推荐。
- 不可变性:依赖字段可以用
final修饰,确保Bean在构造后不可变,线程安全。- 明确依赖:一眼就能看出这个类需要哪些依赖,没有隐藏的依赖。
- 易于测试:不需要Spring容器,可以直接在单元测试中用
new创建对象并传入mock依赖。- 避免循环依赖:Spring官方文档指出,构造函数注入能暴露循环依赖问题(会直接报错),而字段注入可能掩盖问题,导致运行时出现不可预知的行为。
@Resourcevs@Autowired:两者效率无差别。@Resource是JSR标准,@Autowired是Spring特有但功能更丰富(如required=false)。在纯Spring环境下用@Autowired更常见。结论:优先使用构造函数注入(结合Lombok的@RequiredArgsConstructor)。对于可选依赖或循环依赖(应尽量避免),可考虑setter注入。字段注入(@Autowired/@Resourceon field)因其缺点,在现代Spring开发中已不推荐作为首选。
5.2 Web MVC与RESTful注解
| 注解 | 作用 |
|---|---|
@RequestMapping | 通用请求映射。可指定URL、HTTP方法(GET/POST等)、请求头等。 |
@GetMapping/ **@PostMapping**等 | @RequestMapping的快捷方式,分别对应特定HTTP方法。代码更简洁。 |
@RequestParam | 绑定请求参数到方法参数。可指定参数名、是否必需、默认值。 |
@PathVariable | 绑定URL模板变量到方法参数。如/users/{id}。 |
@RequestBody | 将HTTP请求体(如JSON)绑定到方法参数对象上。 |
@ResponseBody | 将方法返回值直接写入HTTP响应体,不经过视图解析器。 |
@RestControllerAdvice(@ControllerAdvice) | 全局异常处理、数据绑定、数据预处理。@RestControllerAdvice是@ControllerAdvice+@ResponseBody。 |
@ExceptionHandler | 在@ControllerAdvice或Controller内部,处理特定异常。 |
@CrossOrigin | 处理跨域请求(CORS)。 |
5.3 事务管理:@Transactional的深水区
这是面试和工作中的绝对重点和难点。
@Service public class OrderService { @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void placeOrder(Order order) { // 1. 保存订单 orderRepository.save(order); // 2. 扣减库存 inventoryService.deduct(order.getProductId(), order.getQuantity()); // 如果deduct方法抛出异常,订单保存也会回滚 } }核心属性解析:
rollbackFor/noRollbackFor:指定哪些异常触发/不触发回滚。默认只在运行时异常(RuntimeException)和错误(Error)下回滚,受检异常(Exception)不回滚!这是一个巨坑。所以,如果你希望任何异常都回滚,通常要设置rollbackFor = Exception.class。propagation:事务传播行为。最常用的是REQUIRED(默认)和REQUIRES_NEW。REQUIRED:如果当前没有事务,就新建一个;如果已存在,就加入。这是最常用的。REQUIRES_NEW:无论如何都新建一个事务,如果当前有事务,则将其挂起。适用于需要独立提交/回滚的子操作,比如记录日志,即使主业务失败,日志也要保存。- 其他如
SUPPORTS、MANDATORY、NOT_SUPPORTED、NEVER、NESTED,根据具体业务场景选用。
isolation:事务隔离级别。解决脏读、不可重复读、幻读问题。默认是数据库的默认级别(通常是READ_COMMITTED)。高隔离级别(如SERIALIZABLE)性能差,慎用。readOnly:是否只读事务。设置为true可以给数据库一个优化提示(如MySQL会将事务设置为只读)。对于纯查询方法,建议加上。
@Transactional失效的常见场景(避坑指南):
- 方法非public:
@Transactional基于AOP(动态代理或CGLIB),非public方法代理无法切入。 - 自调用问题:在同一个类中,一个非事务方法A调用同一个类的事务方法B,事务不会生效。因为代理对象调用A,A内部调用B走的是
this.B(),而不是代理对象的B(),绕过了代理。
解决方案:将@Service public class MyService { public void outer() { this.inner(); // 事务不生效! } @Transactional public void inner() { // ... } }inner()方法移到另一个Service中,或者通过AopContext.currentProxy()获取当前代理对象再调用(需要开启exposeProxy = true),或者(不推荐)使用AspectJ的编译时织入。 - 异常被捕获:如果在方法内用
try-catch吞掉了异常,事务管理器感知不到异常,就不会回滚。
解决方案:在catch块中手动抛出运行时异常,或者将异常原样抛出。@Transactional public void method() { try { // 可能出错的业务 int i = 1 / 0; } catch (Exception e) { e.printStackTrace(); // 异常被捕获并处理,事务不会回滚! } } - 数据库引擎不支持:比如MySQL的MyISAM引擎不支持事务,只有InnoDB支持。
- 未启用事务管理:在Spring Boot中,通常会自动配置。但在纯Spring项目中,需要在配置类上添加
@EnableTransactionManagement。
5.4 条件化装配与配置注解
@Conditional及其衍生注解:Spring Boot自动配置的灵魂。根据特定条件(如类路径下是否存在某个类、某个Bean是否存在、配置文件属性等)决定是否装配一个Bean或配置类。@ConditionalOnClass:类路径下存在指定类时生效。@ConditionalOnMissingBean:容器中不存在指定Bean时生效。@ConditionalOnProperty:配置文件中存在指定属性且为特定值时生效。
@Profile:根据激活的Spring Profile来条件化地装配Bean。用于区分开发、测试、生产环境。@ConfigurationProperties:将配置文件(如application.yml)中的属性批量绑定到一个Java对象的字段上,类型安全,支持嵌套、验证、宽松绑定(kebab-case转camelCase)。@PostConstruct和@PreDestroy(JSR-250):生命周期回调注解。分别标记在Bean初始化后和销毁前执行的方法。常用于资源初始化(如缓存预热)和清理(如关闭连接池)。
6. 测试相关注解
@RunWith(JUnit 4) /@ExtendWith(JUnit 5):指定测试运行器。Spring Boot测试通常用@RunWith(SpringRunner.class)或@ExtendWith(SpringExtension.class)来启动Spring测试上下文。@SpringBootTest:Spring Boot提供的集成测试注解,会加载完整的应用程序上下文。可以指定web环境(如WebEnvironment.MOCK或RANDOM_PORT)。@DataJpaTest,@WebMvcTest,@JsonTest等:Spring Boot的“切片测试”(Slice Test)注解,只加载应用程序的一部分,测试速度更快。@MockBean/@SpyBean:在Spring的测试上下文中注入Mockito的mock或spy对象,用于模拟依赖。@Test:标记测试方法。JUnit 5的@Test在org.junit.jupiter.api包下,与JUnit 4不同。
7. 自定义注解实战:打造你的元数据工具
当内置注解无法满足需求时,就需要自定义注解。步骤通常分为三步:定义注解、使用注解、处理注解。
7.1 案例:基于注解的API接口耗时监控
1. 定义注解
import java.lang.annotation.*; /** * 用于监控方法执行耗时 */ @Target(ElementType.METHOD) // 只能用在方法上 @Retention(RetentionPolicy.RUNTIME) // 运行时保留,因为我们要在AOP中处理 @Documented public @interface MonitorExecutionTime { /** * 监控的名称,默认用方法名 */ String value() default ""; /** * 时间单位,默认毫秒 */ TimeUnit unit() default TimeUnit.MILLISECONDS; }2. 使用注解
@RestController @RequestMapping("/api/users") public class UserController { @GetMapping("/{id}") @MonitorExecutionTime("根据ID查询用户") // 使用自定义注解 public User getUser(@PathVariable Long id) { // ... 业务逻辑 return userService.findById(id); } }3. 处理注解(通过Spring AOP)
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.springframework.stereotype.Component; import java.lang.reflect.Method; import java.util.concurrent.TimeUnit; @Aspect @Component public class MonitorExecutionTimeAspect { // 定义切点:所有被@MonitorExecutionTime注解的方法 @Around("@annotation(com.yourpackage.annotation.MonitorExecutionTime)") public Object monitorTime(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); MonitorExecutionTime annotation = method.getAnnotation(MonitorExecutionTime.class); String methodName = annotation.value().isEmpty() ? method.getName() : annotation.value(); TimeUnit unit = annotation.unit(); long startTime = System.nanoTime(); Object result; try { result = joinPoint.proceed(); // 执行原方法 } finally { long endTime = System.nanoTime(); long duration = endTime - startTime; long convertedDuration = unit.convert(duration, TimeUnit.NANOSECONDS); // 这里可以输出到日志、发送到监控系统等 System.out.printf("[监控] 方法 %s 执行耗时: %d %s%n", methodName, convertedDuration, unit.name().toLowerCase()); } return result; } }通过这个简单的例子,你可以看到自定义注解结合AOP的强大之处:无侵入式地给系统添加横切关注点功能,如日志、监控、鉴权、缓存等。
7.2 注解处理器(Annotation Processor)简介
除了运行时通过反射处理注解,还可以在编译期处理注解,生成额外的源代码或资源文件。Lombok、MapStruct都是这么做的。这需要实现javax.annotation.processing.AbstractProcessor接口。编译期处理性能更好,但更复杂。对于大多数业务场景,运行时处理(AOP)已经足够。
8. 其他实用注解与疑难杂症
@SneakyThrows(Lombok):这个注解很“狡猾”。它用在方法上,可以让你在方法中抛出受检异常,而无需在方法签名上声明throws。Lombok会在编译时生成一个try-catch块,将受检异常包装成RuntimeException再抛出。慎用,因为它破坏了Java的受检异常机制,可能掩盖真正的错误。仅在你确信异常应该作为运行时异常处理,或者与某些框架(如Lambda表达式,它不允许抛出受检异常)交互时使用。@NonNull(Lombok):生成空值检查。用在字段、参数或方法上,Lombok会生成相应的if (param == null) throw new NullPointerException(...);代码。有助于提前暴露NPE问题。- MyBatis-Plus多租户注解:通过
@Interceptor和自定义注解(如@TenantId)结合,在SQL解析层面自动添加租户ID过滤条件,实现数据隔离。这需要深入理解MyBatis-Plus的插件机制。 - Java虚拟线程(Virtual Threads)与注解:JDK 19+引入了虚拟线程(轻量级线程)。目前还没有直接相关的特定注解,但像
@Async这样的异步注解,其背后的线程池执行器(TaskExecutor)可以配置为使用虚拟线程,从而获得极高的并发性能。这是未来高并发编程的一个重要方向。
这份“Java注解大全”就像我的技术地图,随着Java生态的演进和我自身经验的积累,它会不断被修订和扩充。注解的世界远不止于此,还有JPA的@Entity、@Id,Jackson的@JsonIgnore,Validation的@NotNull等等。关键在于理解其核心思想:注解是元数据,它本身不做任何事,但配合特定的处理器(编译器、框架、AOP、注解处理器),就能产生强大的声明式编程效果。希望这份笔记能帮助你构建起自己的注解知识体系,在编码时更加得心应手。