注解是JDK1.5版本开始引入的一个特性,用于对程序代码的说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。
它主要的作用有以下四方面:
注解的常见分类有三种:
接下来我们通过这三种分类来逐一理解注解。
我们先从Java内置注解开始说起,先看下下面的代码:
class Parent { public void rewriteMethod() { } } class Child extends Parent { /** * 重载父类的 rewriteMethod() 方法 */ @Override public void rewriteMethod() { } /** * 被弃用的过时方法 */ @Deprecated public void oldMethod() { } /** * 忽略告警 * * @return */ @SuppressWarnings("keep run") public List infoList() { List list = new ArrayList(); return list; } }
Java 1.5开始自带的标准注解,包括@Override、@Deprecated和@SuppressWarnings:
@Override
:表示当前类中的方法定义将覆盖父类中的方法@Deprecated
:表示该代码段被弃用,但是可以使用,只是编译器会发出警告而已@SuppressWarnings
:表示关闭编译器的警告信息我们先来看一下这个注解类型的定义:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
从它的定义我们可以看到,这个注解可以被用来修饰方法,并且它只在编译时有效,在编译后的class文件中便不再存在。这个注解的作用我们大家都不陌生,那就是告诉编译器被修饰的方法是重写的父类的中的相同签名的方法,编译器会对此做出检查,
若发现父类中不存在这个方法或是存在的方法签名不同,则会报错。
这个注解的定义如下:
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE}) public @interface Deprecated { }
从它的定义我们可以知道,它会被文档化,能够保留到运行时,能够修饰构造方法、属性、局部变量、方法、包、参数、类型。这个注解的作用是告诉编译器被修饰的程序元素已被“废弃”,不再建议用户使用。
这个注解我们也比较常用到,先来看下它的定义:
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) @Retention(RetentionPolicy.SOURCE) public @interface SuppressWarnings { String[] value(); }
它能够修饰的程序元素包括类型、属性、方法、参数、构造器、局部变量,只能存活在源码时,取值为String[]。它的作用是告诉编译器忽略指定的警告信息,它可以取的值如下所示:
参数 | 作用 | 原描述 |
---|---|---|
all | 抑制所有警告 | to suppress all warnings |
boxing | 抑制装箱、拆箱操作时候的警告 | to suppress warnings relative to boxing/unboxing operations |
cast | 抑制映射相关的警告 | to suppress warnings relative to cast operations |
dep-ann | 抑制启用注释的警告 | to suppress warnings relative to deprecated annotation |
deprecation | 抑制过期方法警告 | to suppress warnings relative to deprecation |
fallthrough | 抑制确在switch中缺失breaks的警告 | to suppress warnings relative to missing breaks in switch statements |
finally | 抑制finally模块没有返回的警告 | to suppress warnings relative to finally block that don’t return |
hiding | 抑制与隐藏变数的区域变数相关的警告 | to suppress warnings relative to locals that hide variable() |
incomplete-switch | 忽略没有完整的switch语句 | to suppress warnings relative to missing entries in a switch statement (enum case) |
nls | 忽略非nls格式的字符 | to suppress warnings relative to non-nls string literals |
null | 忽略对null的操作 | to suppress warnings relative to null analysis |
rawtype | 使用generics时忽略没有指定相应的类型 | to suppress warnings relative to un-specific types when using |
restriction | 抑制与使用不建议或禁止参照相关的警告 | to suppress warnings relative to usage of discouraged or |
serial | 忽略在serializable类中没有声明serialVersionUID变量 | to suppress warnings relative to missing serialVersionUID field for a serializable class |
static-access | 抑制不正确的静态访问方式警告 | to suppress warnings relative to incorrect static access |
synthetic-access | 抑制子类没有按最优方法访问内部类的警告 | to suppress warnings relative to unoptimized access from inner classes |
unchecked | 抑制没有进行类型检查操作的警告 | to suppress warnings relative to unchecked operations |
unqualified-field-access | 抑制没有权限访问的域的警告 | to suppress warnings relative to field access unqualified |
unused | 抑制没被使用过的代码的警告 | to suppress warnings relative to unused code |
上述内置注解的定义中使用了一些元注解(注解类型进行注解的注解类),在JDK 1.5中提供了4个标准的元注解:@Target,@Retention,@Documented,@Inherited, 在JDK 1.8中提供了两个新的元注解 @Repeatable和@Native。
Target注解的作用是:描述注解的使用范围(即:被修饰的注解可以用在什么地方) 。
Target注解用来说明那些被它所注解的注解类可修饰的对象范围:
public enum ElementType { TYPE, // 类、接口、枚举类 FIELD, // 成员变量(包括:枚举常量) METHOD, // 成员方法 PARAMETER, // 方法参数 CONSTRUCTOR, // 构造方法 LOCAL_VARIABLE, // 局部变量 ANNOTATION_TYPE, // 注解类 PACKAGE, // 可用于修饰:包 TYPE_PARAMETER, // 类型参数,JDK 1.8 新增 TYPE_USE // 使用类型的任何地方,JDK 1.8 新增 }
Reteniton注解的作用是:描述注解保留的时间范围(即:被描述的注解在它所修饰的类中可以被保留到何时) 。
Reteniton注解用来限定那些被它所注解的注解类在注解到其他类上以后,可被保留到何时,一共有三种策略,定义在RetentionPolicy枚举中。枚举如下:
public enum RetentionPolicy { SOURCE, // 源文件保留 CLASS, // 编译期保留,默认为该值,CLASS RUNTIME // 运行期保留,可通过反射去获取注解信息 }
我们测试下这三种策略,在定义注解类的时候什么区别:
@Retention(RetentionPolicy.SOURCE) public @interface SourcePolicy { // 源文件保留策略 } @Retention(RetentionPolicy.CLASS) public @interface ClassPolicy { // 编译器保留策略 } @Retention(RetentionPolicy.RUNTIME) public @interface RuntimePolicy { // 运行期保留策略 }
上面已经定义好了三个注解类,我们再用这三个注解类再去注解方法,如下:
public class RetentionTest { @SourcePolicy public void sourcePolicy() { } @ClassPolicy public void classPolicy() { } @RuntimePolicy public void runtimePolicy() { } }
通过执行 javap -verbose RetentionTest命令获取到的RetentionTest 的 class 字节码内容如下。
{ public retention.RetentionTest(); flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 public void sourcePolicy(); flags: ACC_PUBLIC Code: stack=0, locals=1, args_size=1 0: return LineNumberTable: line 7: 0 public void classPolicy(); flags: ACC_PUBLIC Code: stack=0, locals=1, args_size=1 0: return LineNumberTable: line 11: 0 RuntimeInvisibleAnnotations: 0: #11() public void runtimePolicy(); flags: ACC_PUBLIC Code: stack=0, locals=1, args_size=1 0: return LineNumberTable: line 15: 0 RuntimeVisibleAnnotations: 0: #14() }
从 RetentionTest 的字节码内容我们可以得出以下两点结论:
Documented注解的作用如下:使用 javadoc 工具为类生成帮助文档,并确认是否保留注解信息。
以下代码在使用Javadoc工具可以生成 @DocAnnotation注解信息。
import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Target; @Documented @Target({ElementType.TYPE,ElementType.METHOD}) public @interface DocAnnotation { public String value() default "default"; }
@DocAnnotation("some method doc") public void testMethod() { // 测试方法的文档注解 }
Inherited注解的作用:被它修饰的Annotation将具有继承特性。父类使用了被@Inherited修饰的Annotation,则子类将自动具备该注解。
我们来测试下这个注解:
@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE,ElementType.METHOD}) public @interface InheritedAnnotation { String [] values(); int number(); }
@InheritedAnnotation(values = {"brand"}, number = 100) public class UserInfo { } class Customer extends UserInfo { @Test public void testMethod(){ Class clazz = Student.class; Annotation[] annotations = clazz.getAnnotations(); for (Annotation annotation : annotations) { System.out.println(annotation.toString()); } } }
xxx.InheritedAnnotation(values=[brand], number=100)
虽然Customer类没有显示地被注解@InheritedAnnotation,但是它的父类UserInfo被注解,而且@InheritedAnnotation被@Inherited注解,因此Customer类自动继承注解
Repeatable是可重复使用的意思,允许在同一声明的类型(类,属性,或方法)中,可以多次使用同一个注解
JDK8之前要想实现注解重复使用,需要组合模式,编写和可读性都不是很好
public @interface Pet { String myPet(); } public @interface Pets { Pet[] value(); } public class RepeatAnnotationOV { @Pets({@Pet(myPet="dog"),@Pet(myPet="cat")}) public void workMethod(){ } }
由另一个注解来存储重复注解,在使用时候,用存储注解Authorities来扩展重复注解。
Java 8中的做法:
@Repeatable(Pets.class) public @interface Pet { String myPet(); } public @interface Pets { Pet[] value(); } public class RepeatAnnotationNV { @Pet(role="dog") @Pet(role="cat") public void workMethod(){ } }
不同的地方是,创建重复注解Authority时,加上@Repeatable,指向存储注解Authorities,在使用时候,直接可以重复使用Authority注解。从上面例子看出,java 8里面做法更适合常规的思维,可读性强一点
使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。对于 @Native 注解不常使用,了解即可
定义注解后,如何获取注解中的内容呢?反射包java.lang.reflect下的AnnotatedElement接口提供这些方法。这里注意:只有注解被定义为RUNTIME后,该注解才能是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。
AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的方法来访问Annotation信息。我们看下具体的先关接口
当我们理解了内置注解, 元注解和获取注解的反射接口后,我们便可以开始自定义注解了。这个例子我把上述的知识点全部融入进来, 代码很简单:
package com.helenlyn.common.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * <p>Description: 水果供应者注解 </p> * <p>Copyright: Copyright (c) 2021 </p> * <p>Company: helenlyn Co., Ltd. </p> * * @author brand * @date 2021/5/16 16:35 * <p>Update Time: </p> * <p>Updater: </p> * <p>Update Comments: </p> */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface FruitProvider { /** * 供应商编号 * @return */ public int id() default -1; /** * 供应商名称 * @return */ public String name() default ""; /** * 供应商地址 * @return */ public String address() default ""; }
package com.helenlyn.common.dto; import com.helenlyn.common.annotation.FruitColor; import com.helenlyn.common.annotation.FruitName; import com.helenlyn.common.annotation.FruitProvider; /** * <p>Description: </p> * <p>Copyright: Copyright (c) 2021 </p> * <p>Company: helenlyn Co., Ltd. </p> * * @author brand * @date 2021/5/16 16:28 * <p>Update Time: </p> * <p>Updater: </p> * <p>Update Comments: </p> */ public class AppleDto { @FruitName("Apple") private String appleName; @FruitColor(fruitColor=FruitColor.Color.RED) private String appleColor; @FruitProvider(id=1,name="helenlyn 贸易公司",address="福州xx路xxx大楼") private String appleProvider; }
/** * <p>Description: FruitInfoUtil注解实现 </p> * <p>Copyright: Copyright (c) 2021 </p> * <p>Company: helenlyn Co., Ltd. </p> * * @author brand * @date 2021/5/16 16:37 * <p>Update Time: </p> * <p>Updater: </p> * <p>Update Comments: </p> */ public class FruitInfoUtil { public static String getFruitInfo(Class<?> clazz) { String strFruitName = " 水果名称:"; String strFruitColor = " 水果颜色:"; String strFruitProvicer = "供应商信息:"; Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(FruitName.class)) { FruitName fruitName = (FruitName) field.getAnnotation(FruitName.class); strFruitName += fruitName.value(); System.out.println(strFruitName); } else if (field.isAnnotationPresent(FruitColor.class)) { FruitColor fruitColor = (FruitColor) field.getAnnotation(FruitColor.class); strFruitColor += fruitColor.fruitColor().toString(); System.out.println(strFruitColor); } else if (field.isAnnotationPresent(FruitProvider.class)) { FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class); strFruitProvicer = " 供应商编号:" + fruitProvider.id() + " 供应商名称:" + fruitProvider.name() + " 供应商地址:" + fruitProvider.address(); System.out.println(strFruitProvicer); } } return String.format("%s;%s;%s;", strFruitName, strFruitColor, strFruitProvicer); } }
2022-07-09 11:33:41.688 INFO 5895 --- [TaskExecutor-35] o.s.a.r.c.CachingConnectionFactory : Attempting to connect to: cl-debug-rabbitmq-erp-service-7w0cpa.docker.sdp:9146 Hibernate: update UserBasicInfo set personName=? where personCode=? 水果名称:Apple 水果颜色:RED 供应商编号:1 供应商名称:helenlyn 贸易公司 供应商地址:福州xx路xxx大楼
ElementType.TYPE_USE(此类型包括类型声明和类型参数声明,是为了方便设计者进行类型检查)包含了ElementType.TYPE(类、接口(包括注解类型)和枚举的声明)和ElementType.TYPE_PARAMETER(类型参数声明), 可以看下面这个例子:
// 自定义ElementType.TYPE_PARAMETER注解 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE_PARAMETER) public @interface MyNotEmpty { } // 自定义ElementType.TYPE_USE注解 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE_USE) public @interface MyNotNull { } // 测试类 public class TypeParameterAndTypeUseAnnotation<@MyNotEmpty T>{ //使用TYPE_PARAMETER类型,会编译不通过 // public @MyNotEmpty T test(@MyNotEmpty T a){ // new ArrayList<@MyNotEmpty String>(); // return a; // } //使用TYPE_USE类型,编译通过 public @MyNotNull T test2(@MyNotNull T a){ new ArrayList<@MyNotNull String>(); return a; } }
注解是不支持继承的
不能使用关键字extends来继承某个@interface,但注解在编译后,编译器会自动继承java.lang.annotation.Annotation接口。 虽然反编译后发现注解继承了Annotation接口,请记住,即使Java的接口可以实现多继承,但定义注解时依然无法使用extends关键字继承@interface。 区别于注解的继承,被注解的子类继承父类注解可以用@Inherited: 如果某个类使用了被@Inherited修饰的Annotation,则其子类将自动具有该注解。
笔者曾经在 《基于AOP的动态数据源切换》 这篇文章中有个典型的例子,就是使用AOP切面来对多数据源进行使用场景的切换,下面展示下如何通过注解实现解耦的。
/** * @author brand * @Description: 数据源切换注解 * @Copyright: Copyright (c) 2021 * @Company: Helenlyn, Inc. All Rights Reserved. * @date 2021/12/15 7:36 下午 */ @Target({ ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataSource { String name() default ""; }
/** * @author brand * @Description: * @Copyright: Copyright (c) 2021 * @Company: Helenlyn, Inc. All Rights Reserved. * @date 2021/12/15 7:49 下午 */ @Aspect @Component public class DataSourceAspect implements Ordered { /** * 定义一个切入点,匹配到上面的注解DataSource */ @Pointcut("@annotation(com.helenlyn.dataassist.annotation.DataSource)") public void dataSourcePointCut() { } /** * Around 环绕方式做切面注入 * @param point * @return * @throws Throwable */ @Around("dataSourcePointCut()") public Object around(ProceedingJoinPoint point) throws Throwable { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); DataSource ds = method.getAnnotation(DataSource.class); String routeKey = ds.name(); // 从头部中取出注解的name(basic 或 cloudoffice 或 attend),用这个name进行数据源查找。 String dataSourceRouteKey = DynamicDataSourceRouteHolder.getDataSourceRouteKey(); if (StringUtils.isNotEmpty(dataSourceRouteKey)) { // StringBuilder currentRouteKey = new StringBuilder(dataSourceRouteKey); routeKey = ds.name(); } DynamicDataSourceRouteHolder.setDataSourceRouteKey(routeKey); try { return point.proceed(); } finally { // 最后做清理,这个步骤很重要,因为我们的配置中有一个默认的数据源,执行完要回到默认的数据源。 DynamicDataSource.clearDataSource(); DynamicDataSourceRouteHolder.clearDataSourceRouteKey(); } } @Override public int getOrder() { return 1; } }
/** * 无注解默认情况:数据源指向basic * @return */ @RequestMapping(value = "/default/{user_code}", method = RequestMethod.GET) public UserInfoDto getUserInfo(@PathVariable("user_code") String userCode) { return userInfoService.getUserInfo(userCode); } /** * 数据源指向attend * @return */ @DataSource(name= Constant.DATA_SOURCE_ATTEND_NAME) @RequestMapping(value = "/attend/{user_code}", method = RequestMethod.GET) public UserInfoDto getUserInfoAttend(@PathVariable("user_code") String userCode) { return userInfoService.getUserInfo(userCode); } /** * 数据源指向cloud * @return */ @DataSource(name= Constant.DATA_SOURCE_CLOUD_NAME) @RequestMapping(value = "/cloud/{user_code}", method = RequestMethod.GET) public UserInfoDto getUserInfoCloud(@PathVariable("user_code") String userCode) { return userInfoService.getUserInfo(userCode); }
除此之外,我们可以看到很多日志管理、权限管理,也都是也是通过类似的注解机制来实现的,通过注解+AOP来最终实现模块之间的解耦,以及业务与系统层面的解耦 。