1. 注解的概念Java 注解Annotation是 JDK 5 引入的一种元数据形式它提供了一种安全的方式将信息与程序元素类、方法、变量、参数、包等关联起来。注解本身不做任何事情它只是提供信息元数据。真正让注解发挥作用的是处理这些注解的工具或框架编译器、开发工具或运行时反射机制来读取和处理这些信息的框架。1.1. 注解的本质标签/标记可以把注解想象成给代码元素贴的便利贴或标签Override// ← 这就是一个标签告诉编译器我打算重写父类方法publicStringtoString(){returnMyClass;}这个 Override 标签本身不会改变 toString() 方法的行为。但编译器会检查这个标签是否正确使用。1.2. 谁在读取注解注解的功能取决于谁在处理它。【编译器】处理的例子处理者Java 编译器功能编译时验证Override// ← 编译器会处理这个注解publicvoidnonExistentMethod(){}// 编译错误因为父类没有这个方法【开发工具】处理的例子处理者IDE如 IntelliJ IDEA、Eclipse功能在代码中显示删除线提醒开发者不要使用Deprecated// ← 开发工具IDE会处理这个注解publicvoidoldMethod(){}【运行时框架】处理的例子处理者Spring 框架功能在应用启动时Spring 扫描这些注解自动注册控制器和路由RestController// ← Spring 框架会处理这个注解publicclassMyController{GetMapping(/hello)// ← Spring 框架会处理这个注解publicStringhello(){returnHello World;}}1.3. 生活中的类比想象一个图书馆书籍 你的代码书上的标签 注解比如新书、“参考书”、“限阅”图书管理员 处理注解的工具/框架标签本身不会改变书的内容但图书管理员看到新书标签就会把它放到新书架看到限阅标签就知道只有特定读者能借阅。2. 注解的功能2.1 注解的三种主要功能场景2.2.1. 编译时检查Compiler-time帮助【编译器】发现错误或警告。例如Override 确保方法正确重写了父类方法。// 功能防止编程错误Overridepublicbooleanequals(Objectobj){// 如果方法签名错了编译器会报错returnsuper.equals(obj);}2.1.2. 编译时代码生成Code Generation根据注解自动生成代码减少样板代码的编写。// Lombok 注解 - 编译时自动生成 getter/setterDatapublicclassUser{privateStringname;privateintage;}// 编译后实际生成了 getName(), setName(), getAge(), setAge() 等方法2.1.3. 运行时反射处理Runtime Reflection通过反射在运行时读取注解信息实现框架的核心功能如 Spring、Hibernate或自定义逻辑。// JUnit 测试 - 运行时测试框架通过反射找到带 Test 的方法并执行TestpublicvoidtestAddition(){assertEquals(4,22);}// Spring 依赖注入 - 运行时Spring框架通过反射找到 Autowired 字段并注入实例AutowiredprivateUserServiceuserService;2.2 使用注解的优势使用注解的优势提高代码可读性 - 清晰表达代码意图减少配置文件 - 将配置信息直接嵌入代码中增强类型安全 - 编译时就能发现问题简化开发 - 自动化处理重复性工作框架集成 - 现代 Java 框架大量依赖注解这里从两个视角对注解 vs 传统方式直观感受注解优势XML 配置 vs 注解配置注解更直观与代码紧密耦合接口/抽象类 vs 注解注解更轻量无需继承关系2.2.1 传统XML配置 vs 注解以生成和注入bean为例传统 XML 配置!-- applicationContext.xml --beaniduserServiceclasscom.example.UserService/beaniduserControllerclasscom.example.UserControllerpropertynameuserServicerefuserService//bean注解配置注解把配置信息直接嵌入到代码中更加直观和类型安全。ServicepublicclassUserService{}ControllerpublicclassUserController{AutowiredprivateUserServiceuserService;}2.2.2 接口/抽象类 vs 注解以缓存为例需求标记某些方法的结果可以被缓存。传统方式使用接口/抽象类step1. 定义接口publicinterfaceCacheable{// 空接口仅作标记}step2. 业务类实现接口publicclassUserServiceimplementsCacheable{publicUsergetUserById(Longid){returnuserRepository.findById(id);}publicListUsergetAllUsers(){returnuserRepository.findAll();}}step3.新建缓存处理器实现缓存逻辑publicclassCacheHandler{publicObjectexecuteWithCache(Objecttarget,StringmethodName,Object...args)throwsException{// 检查目标对象是否实现了 Cacheable 接口if(targetinstanceofCacheable){// 生成缓存key这里简化处理Stringkeytarget.getClass().getSimpleName()_methodName;// 检查缓存ObjectcachedgetFromCache(key);if(cached!null){returncached;}// 执行方法Methodmethodtarget.getClass().getMethod(methodName);Objectresultmethod.invoke(target,args);// 存入缓存putInCache(key,result);returnresult;}else{// 不是 Cacheable直接执行Methodmethodtarget.getClass().getMethod(methodName);returnmethod.invoke(target,args);}}privateObjectgetFromCache(Stringkey){/* ... */}privatevoidputInCache(Stringkey,Objectvalue){/* ... */}}step4. 使用publicclassApplication{publicstaticvoidmain(String[]args)throwsException{UserServiceuserServicenewUserService();CacheHandlercacheHandlernewCacheHandler();// 通过缓存处理器调用方法Useruser(User)cacheHandler.executeWithCache(userService,getUserById,1L);}}传统实现方式有如下问题粒度太粗只能标记整个类无法精确到方法。业务类实现缓存标记接口会导致业务类所有方法都默认被缓存不能标记单个方法调用方式不自然不能直接调用 userService.getUserById(1L) 必须通过特殊的调用方式或复杂的代理机制间接调用破坏了正常的面向对象调用习惯缺乏配置能力无法传递缓存时间、缓存key等配置信息侵入性强业务类被迫实现与业务无关的接口注解方式step1. 定义注解Retention(RetentionPolicy.RUNTIME)Target(ElementType.METHOD)// 关键可以标记在方法上publicinterfaceCacheable{Stringvalue()default;longttl()default3600;// 缓存时间秒}step2. 直接在需要的方法上使用publicclassUserService{Cacheable(ttl1800)// 只缓存这个方法30分钟过期publicUsergetUserById(Longid){returnuserRepository.findById(id);}// 这个方法不缓存publicListUsergetAllUsers(){returnuserRepository.findAll();}Cacheable(ttl7200)// 这个方法也缓存但2小时过期publicUsergetUserByEmail(Stringemail){returnuserRepository.findByEmail(email);}}step3. 处理注解框架层面// AOP 切面处理缓存逻辑AspectComponentpublicclassCacheAspect{Around(annotation(cacheable))publicObjectcacheAdvice(ProceedingJoinPointjoinPoint,Cacheablecacheable)throwsThrowable{// 1. 生成缓存keyStringkeygenerateKey(joinPoint);// 2. 检查缓存是否存在ObjectcachedredisTemplate.opsForValue().get(key);if(cached!null){returncached;}// 3. 执行原方法ObjectresultjoinPoint.proceed();// 4. 存入缓存redisTemplate.opsForValue().set(key,result,cacheable.ttl(),TimeUnit.SECONDS);returnresult;}}step4. 使用方式publicclassApplication{publicstaticvoidmain(String[]args)throwsException{Autowired// 直接注入使用无需特殊处理privateUserServiceuserService;UseruseruserService.getUserById(1L);// 自动缓存调用方式完全自然}}注解的真正威力在于它提供了一种与代码逻辑完全解耦的元数据表达方式。你的业务代码还是纯粹的业务代码缓存、事务、安全等横切关注点通过注解声明框架在运行时读取这些注解并织入相应的逻辑3. 注解的分类3.1. 内置注解Built-in Annotations通用注解Override - 标记方法重写Deprecated - 标记过时的 API类、方法等SuppressWarnings - 抑制编译器警告SafeVarargs - JDK 7新增抑制堆污染警告FunctionalInterface - Java8新增函数式接口如果接口中只有一个抽象方法(可以包含多个 default 方法或多个 static 方法)该接口称为函数式接口元注解Meta-Annotations用于定义其他注解的注解Retention - 指定注解的保留策略RetentionPolicy.SOURCE - 仅在源码中保留编译后丢弃使用场景Override, SuppressWarningsRetentionPolicy.CLASS - 编译时写入 class 文件运行时不可见使用场景字节码分析工具RetentionPolicy.RUNTIME - 运行时保留可通过反射获取使用场景框架配置、AOP、序列化等Target - 指定注解的使用目标ElementType.TYPE - 类、接口、枚举ElementType.METHOD - 方法ElementType.FIELD - 字段ElementType.PARAMETER - 参数等Documented - 包含在 Javadoc 中Inherited - 允许子类继承父类的注解Repeatable - 允许同一位置重复使用Java 83.2 框架注解Spring Framework框架Component, Service, Repository, ControllerAutowired, QualifierRequestMapping, GetMapping, PostMappingConfiguration, BeanJPA/Hibernate框架Entity, TableId, GeneratedValueColumn, OneToMany, ManyToOneJUnit框架Test, Before, AfterBeforeEach, AfterEach (JUnit 5)3.3. 自定义注解自定义注解时理解注解的功能 注解定义 注解处理器就掌握了注解的核心思想。// 1. 定义注解Retention(RetentionPolicy.RUNTIME)Target(ElementType.METHOD)interfaceLoggable{}// 2. 使用注解classCalculator{Loggablepublicintadd(inta,intb){returnab;}publicintmultiply(inta,intb){returna*b;}}// 3. 处理注解这就是让注解发挥作用的地方publicclassAnnotationDemo{publicstaticvoidmain(String[]args)throwsException{CalculatorcalcnewCalculator();Method[]methodsCalculator.class.getDeclaredMethods();for(Methodmethod:methods){if(method.isAnnotationPresent(Loggable.class)){System.out.println(发现可记录日志的方法: method.getName());// 这里可以添加日志逻辑、性能监控等}}}}4. 自定义注解完整DEMO日常开发中使用自定义注解可分为三步step1: 定义注解step2: 处理注解step3: 使用注解4.1 Demo1: 字段值校验需求描述检测用户输入的 person 对象的 年龄值是否符合要求(0~200岁)、姓名的字符串长度是否符合要求(1 ~ 20个字符)解决方案通过注解的方式校验Person的年龄、name 是否符合要求。4.1 定义注解分析定义一个名称为Range的注解标记在需要校验的地方希望对 person对象的 年龄、姓名字段做校验所以注解使用目标Target 取字段类型ElementType.FIELD 要在程序运行时做校验Retention策略取 RetentionPolicy.RUNTIME校验字段是否属于某个范围(年龄的取值、姓名的字符长度)应有 最大值、最小值信息在 Range里定义两个参数方法minValue 和maxValueRetention(RetentionPolicy.RUNTIME)Target(ElementType.FIELD)publicinterfaceRange{intminValue()default0;intmaxValue()default250;}注解也是一种特殊的类使用interface关键字定义将自动继承java.lang.annotation.Annotation 接口编译程序将自动完成其他细节。一些需要注意的点在定义注解时不能继承其他的注解或接口。注解中的每一个方法实际上是声明了一个配置参数方法的名称就是参数的名称返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、Enum)。可以通过 default 来声明参数的默认值。4.2 处理注解根据需求可知自定义Range注解会用到两种类型的字段上数值型(年龄) 和 字符串型(姓名)在处理注解时需要分别针对这两种情况处理。可以写个专门处理 Range 注解的工具类方便在各处使用publicclassRangeCheckUtils{/** * 检查 Integer/String 类型的字段长度是否符合要求 * param object * throws IllegalArgumentException * throws ReflectiveOperationException */publicstaticvoidcheckFiledRange(Objectobject)throwsIllegalArgumentException,ReflectiveOperationException{//遍历传入对象所有的Filedfor(Fieldfield:object.getClass().getDeclaredFields()){//通过字段获取该字段上的注解 RangeRangerangefield.getAnnotation(Range.class);if(null!range){//获取注解 Range 定义的范围值intminValuerange.minValue();intmaxValuerange.maxValue();//获取 filed 的值field.setAccessible(true);ObjectfieldValuefield.get(object);// 按类型检查传入对象的 String 或 Integer 类型字段的范围是否符合要求if(fieldValueinstanceofString){StringstringValue(String)fieldValue;//字符型类型检查字符串长度if(stringValue.length()minValue||stringValue.length()maxValue){thrownewIllegalArgumentException(The length of a string filed is invalid, invalid filed: field.getName());}}if(fieldValueinstanceofInteger){intintegerValue(Integer)fieldValue;//数字型类型检查数字大小if(integerValueminValue||integerValuemaxValue){thrownewIllegalArgumentException(The value of a integer filed is invalid, invalid filed: field.getName());}}}}}}4.3 使用注解在Person类上使用注解publicclassPerson{/** * 姓名的 字符串长度范围为 120 个字符 * 这里使用注解设置name的长度范围 */Range(minValue1,maxValue20)privateStringname;/** * 年龄的范围是 0 200 * 这里使用注解设置age的大小范围 * 注解Range 的 minValue 0 故可以省去 */Range(maxValue200)privateIntegerage;/** * 性别不需要做检验 */privateStringgender;//get/set方法}测试注解功能功能是否符合预期测试代码如下publicclassTestAnnotation{publicstaticvoidmain(String[]args){//case1: name 和 age 都符合要求的person对象try{Personperson1newPerson();person1.setAge(20);person1.setName(BigBear);person1.setGender(male);//检查person的字段是否符合要求RangeCheckUtils.checkFiledRange(person1);System.out.println(case1 check pass ! person1 info: person1.toString());}catch(Exceptione){System.out.println(case1 check failed! exception info: e.getMessage());}//case2: name不符合要求的person对象try{Personperson2newPerson();person2.setAge(20);person2.setName(BigBearBigBearBigBearBigBearBigBear);person2.setGender(male);//检查person的字段是否符合要求RangeCheckUtils.checkFiledRange(person2);System.out.println(case2 check pass ! person1 info: person2.toString());}catch(Exceptione){System.out.println(case2 check failed! exception info: e.getMessage());}//case3: age不符合要求的person对象try{Personperson3newPerson();person3.setAge(270);person3.setName(BigBear);person3.setGender(male);//检查person的字段是否符合要求RangeCheckUtils.checkFiledRange(person3);System.out.println(case3 check pass ! person1 info: person3.toString());}catch(Exceptione){System.out.println(case3 check failed! exception info: e.getMessage());}//case4: age 和 name 都不符合要求的person对象try{Personperson4newPerson();person4.setAge(-1);person4.setName(BigBearBigBearBigBearBigBearBigBear);person4.setGender(male);//检查字段是否符合要求RangeCheckUtils.checkFiledRange(person4);System.out.println(case4 check pass ! person1 info: person4.toString());}catch(Exceptione){System.out.println(case4 check failed! exception info: e.getMessage());}}}对应的输出结果case1 check pass!person1 info:Person{nameBigBear,age20,gendermale}case2 check failed!exception info:Thelength of a string filed is invalid,invalid filed:name case3 check failed!exception info:Thevalue of a integer filed is invalid,invalid filed:age case4 check failed!exception info:Thelength of a string filed is invalid,invalid filed:name可见 RangeCheckUtils.checkFiledRange(person) 工具方法成功的通过注解找到了需要检查的 person 对象字段并检查出是否符合预期。4.2 Demo2: 自定义注解AOPAOP自定义注解的用法参考https://blog.csdn.net/taotiezhengfeng/article/details/149515395