Java-Annotation(95%) Java中注解可以说是Java编程的一大特性,本章节主要内容:
如何定义一个注解:包括元注解、重复注解等的定义方式和标记方式
注解标记的位置:注解能够标记在哪个地方
JDK内置注解:包括java.lang、java.lang.annotation、javax.annotation包中的非元注解的使用
注解处理器:注解的定义一般有三种级别,对应三类的注解处理器,将会一一介绍!
注解的结构:该小节会反编译注解的字节码,从底层来查看一个注解的源代码
注解应用框架:会介绍常见的注解驱动框架的基本使用,如core java中介绍到的Jcommander 、大名鼎鼎的Lombok
剩余
注解的定义 首先要知道注解在Java代码中并不起任何实际的作用,他的唯一作用只有标记,标记该对象具备某个性质,在java中所有带@开头的都是注解,如常见的@Override,标记@Override代表该方法是一个重写方法!
定义注解的方式很像定义接口,其格式如下:
1 2 public @interface Common {}
在interface前面加上@定义出来的就是注解,idea提供了快速定义的方式,你可以通过new --> new java class,选中Annotation,就可以创建一个注解:
回到@Override注解,你可以在任何重写方法上标记@Override,但如果你把注解标记在字段上,则IDE会报错!这是因为@Override注解的标记是有范围限定的!他只能标记在方法上,我们查看@Override的源代码:
发现其上有两个注解@Target、@Retention,这两个注解就是用于规定@Override作用范围和存储级别,这种规定注解接口的作用范围和行为属性的注解一般被称之为元注解(即用来定义注解的注解),JDK中的元注解有5个:
@Target:表示该注解能标记在哪个地方,可以传递多个ElementType的枚举值,其枚举量如下:
TYPE:标记在类型上,如类、接口、枚举类
FIELD:标记在字段上,如类或接口的字段或常量字段、枚举量
METHOD:标记在方法上,包括静态或非静态
PARAMETER:标记在方法参数上
CONSTRUCTOR:标记在构造器上
LOCAL_VARIABLE:标记在局部变量上
ANNOTATION_TYPE:标记在注解的定义上,元注解一般都是这个级别
PACKAGE:标记在包上,一般和package-info.java有关
TYPE_PARAMETER(JDK 1.8):标记在泛型类型参数上,如T,这里举个例子:
1 2 3 4 5 6 7 8 @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) public @interface Common {} public class Main <@Common T > { private Class<@Common ?> clazz; }
TYPE_USE(JDK 1.8):标记在所有使用的类型上(只要是类型就行),如抛出的异常、继承时的类名、泛型的?等,这里举个例子:
1 2 3 4 5 6 7 8 @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) public @interface Common {} public class Main extends @Common Number implement @Common Closable { private Class<@Common ?> getClass() throws @Common IOException; }
Retention:表示该注解的存储级别,有三个值,只能填其一
SOURCE:源码级别,该注解在编译成字节码的时候会被去掉,类似于注释的处理!
CLASS:字节码级别,该注解在编译成字节码的时候会被保留,但不会被JVM加载,在处理字节码的时候能获取,但无法被反射API获取!
RUNTIME:运行级别,该注解在编译成字节码的时候会被保留,并且会被JVM加载,可以通过反射来获取API
Documented:指明该注解是否会被Javadoc记录
Inherited:指定注解是否能够被继承,标记了该元注解的注解标记在父类的代码上时,子类也能够获取!
Repeatable(JDK 8):表示注解是否可重复标记!
这5大元注解中,其中Target和Retention是必须的,其他都是可选的,根据实际情况标记即可,因此完整创建一个注解需要如下的模板:
1 2 3 4 @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) public @interface Common {}
到此,注解的声明方式介绍完毕,那么注解内部的定义呢?正如类内部可以定义字段、方法一样,注解内部也可以定义一些字段,用来标记一些信息,注解内部允许定义下面的数据:
基本类型(int、short、long、byte、char、double、float和boolean)
String
Class,如:Class<?>、Class<Integer>
Enum类型
注解类型
上面这些类型的数组,如Class<?>[] String[]
我们仍然能在注解内部定义内部类、内部接口等,注解声明实际上是当作接口看的,在:
src/main/java/cn/argento/askia/annotation/full/BugReport.java
中你可以看到一个注解能够定义的所有定义方式和内容,该Demo参考Core Java 11编写!
值得注意的一点是:
在注解中定义内部类、内部枚举类、内部注解、内部接口等,默认都是public并且不允许使用其他的修饰级别如private、protected,这点也和接口一摸一样!
注解数据的定义也很简单,只需要在定义数据变量加上括号即可,如:
1 2 3 4 5 6 @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) public @interface Common { String version () ; Class<?> testClazz(); }
如果希望数据成员有默认值,则可以使用default,但是注意,注解成员不允许有null值,可以使用Void.class、""、-1、{}等作为代替!
1 2 3 4 5 6 @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) public @interface Common { String version () default "1.5.1" ; Class<?> testClazz() default Void.class; }
那么在标记注解的时候如何提供数据呢?假如这里有一个注解:
1 2 3 4 5 6 7 8 @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) public @interface Common { String value () default "1.5.1" ; String version () default "1.5.1" ; Class<?> testClazz() default Void.class; int [] ip default {192 , 168 , 0 , 1 } }
这个注解包含了四个数据成员,其中一个是数组。提供数据的时候需要写出具体要提供的项等于值的格式,提供多个项使用逗号分隔:
提供数据给注解无需全部提供,只提供需要的即可,其他都会使用默认值,如 :
1 2 3 4 5 6 7 public class @Common (version = "1.0.3" , testClazz = Main.class) Main { }
如果注解中定义了一个特殊的数值成员value,并且当你只需要给这个value提供值的时候,可以不写项名:
1 2 3 4 5 6 7 public class @Common ("1.0.3") Main { }
如果提供的注解数值成员是一个数组,则提供内容的时候需要使用{},但是当提供的数组数据只有一个成员的时候,括号可以省略:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class @Common (ip = {192 , 168 , 0 , 3 }) Main { } public class @Common (ip = 192 ) Main { }
如果只标注注解而不提供任何成员数据时,所有的数据都会使用默认值,这个时候注解无需加括号:
1 2 3 4 5 6 7 8 public class @Common Main { }
注解标记的位置 @Target元注解中共定义了10处位置给注解标记,关键的问题就是如何标记,或者说标记在哪里才算合法?
TYPE类型
Type类型允许你将注解标记在类、接口、枚举类上,需要将注解放置在class、interface、enum前面:
1 2 3 4 5 6 7 8 9 10 11 12 @Common public class Main {} @Common public interface Main { } @Common public enum Main { }
FIELD类型
如果是字段(包括常量)类型,则需要标记在类型前面,枚举值则在枚举值前标记
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public enum C { @Common A, @Common B } public class Main { @Common private A name; @Common private A name; private @Common A name; } public interface Main { @Common String c = "Hello World!" ; }
METHOD类型
标记在方法上即可,如:
1 2 3 4 public interface Main { @Common void hello () ; }
PARAMETER类型
标记在方法的参数上即可,如:
1 2 3 public interface Main { void hello (@Common String name) ; }
CONSTRUCTOR类型
标记在构造器上即可:
1 2 3 4 5 6 7 public class Main { @Common public Main () { } }
ANNOTATION_TYPE类型
标记在注解定义上,如:
1 2 3 4 @Common public @interface Main{ int version () default 2 ; }
LOCAL_VARIABLE类型
标记在局部变量上,注意定义该类型的注解,其源码级别只能是SOURCE级别
1 2 3 4 5 6 public class Main { public Main () { @Common String a = "123" ; } }
PACKAGE
标记在package-info.java里面的package上,如:注意定义该类型的注解,其源码级别只能是SOURCE级别
参考写法:src/main/java/cn/argento/askia/annotation/full/package-info.java
TYPE_PARAMETER
定义在所有类型参数变量的位置上,如:
1 2 3 public class Main <@Common T > { public <@Common S> S getType () ; }
TYPE_USE
定义在所有使用类型上,只要是单独的类型都可,如:List<@Common String>、public void test() throws @Common IOException、public class Main extends @Common Main2,该方式的定义位置非常多样,这里引用core java的结论:
内置标准注解 在java.lang、java.lang.annotation、javax.annotation中,有非常多标准注解!
java.lang包:@Deprecated、@FunctionalInterface、@Override、@SafeVarargs、@SuppressWarnings
java.lang.annotation包(元注解包):@Documented、@Inherited、@Native、@Repeatable、@Retention、@Target
javax.annotation包:@Generated、@PostConstruct、@PreDestroy、@Resource等
其中javax.annotation包是扩展的标准化注解包,其中包括了jsr标准规定的一些注解,该包中的注解并不是重点,这些注解你将在Spring框架中遇到
@Retention和@Target我们已介绍过,则无需再花篇幅!
可继承的注解 标记了@Inherited元注解的注解标记在类上的时候可以在子类中被获取,我们定义一个注解:
1 2 3 4 5 6 7 8 @Target({ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface InheritedAnnotation { String version () default "1.0" ; String name () default "askia" ; Class<?> clazz() default Void.class; }
注意这种带@Inherited的注解只有标记在类上的时候才有继承性,如果标记在方法上或者标记在接口上或其他位置,则无法实现注解继承!
定义一个Father类和FatherInterface接口,在Father类和FatherInterface接口上标记@InheritedAnnotation注解,在Father类上的某个方法上标记@InheritedAnnotation注解,然后让Son类继承Father类,Son2类实现FatherInterface,进行测试!
InheritedAnnotation.java
1 2 3 4 5 6 7 8 @Target({ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface InheritedAnnotation { String version () default "1.0" ; String name () default "askia" ; Class<?> clazz() default Void.class; }
Father.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @InheritedAnnotation(name = "Askia3") public class Father { private String name; public Father () { this .name = "Askia" ; } public Father (String name) { this .name = name; } @InheritedAnnotation(name = "Askia2") public String getName () { return name; } @InheritedAnnotation(name = "Askia2") public Father setName (String name) { this .name = name; return this ; } }
FatherInterface.java
1 2 3 4 @InheritedAnnotation public interface FatherInterface {}
Son.java
1 2 3 4 5 6 7 8 9 10 11 12 public class Son extends Father implements Serializable , Comparable <Son > { @Override public String getName () { return super .getName() + " = " + LocalDateTime.now(); } @Override public int compareTo (Son o) { return 0 ; } }
Son2.java
1 2 public class Son2 implements FatherInterface {}
在反射API中,获取Runtime级别的注解可以通过getAnnotations()和getDeclaredAnnotations()获取,他们的区别是getDeclaredAnnotations()不会获取继承性的注解!
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class InheritedTest { public static void main (String[] args) throws NoSuchMethodException { final Method getName = Son.class.getMethod("getName" ); final Annotation[] annotations = getName.getAnnotations(); System.out.println(Arrays.toString(annotations)); final Annotation[] annotations1 = Son.class.getAnnotations(); System.out.println(Arrays.toString(annotations1)); final Annotation[] annotations12 = Son.class.getDeclaredAnnotations(); System.out.println(Arrays.toString(annotations12)); final Annotation[] annotations2 = Son2.class.getAnnotations(); System.out.println(Arrays.toString(annotations2)); } }
结果只有标记在Father类上的注解能够被反射API获取,其他的都无法获取!具体可以参考Demo:
src/main/java/cn/argento/askia/annotation/inherited/InheritedTest.java
另外继承性可以多级继承,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Inherited @interface Anno{} @Anno class A {} class B extends A {} class C extends B { public static void main (String[] args) { final Anno annotation = C.class.getAnnotation(Anno.class); System.out.println(annotation); } }
实际上方法上、字段上也能获取类似于继承性质的注解,如果子类继承了父类,但是却没有重写父类的某个方法,则通过子类的Class对象获取该方法时(注意,getMethod()的核心有两:1.只能获取public,2.获取Class对象对应的类及其所有父类)可以获取,如上面的Father类中的setName(),该方法由于是public的,且Son类没有重写,所以通过Son.class.getMethod(“setName”, String.class).getAnnotations()可以获取到@InheritedAnnotation。同样的,字段、接口中的常量和方法、接口中的默认方法也适用这个规则!
可重复标记的注解 @Repeatable源代码如下:
1 2 3 4 5 6 @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Repeatable { Class<? extends Annotation> value(); }
标记了@Repeatable的元注解的注解支持重复标记在类、方法等上面。我们先定义一个注解,并将其变成可重复标记的注解:
1 2 3 4 5 6 7 @Target({ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RepeatableAnnotation { String description () default "" ; }
@Repeatable需要一个装载重复注解的容器,这个容器一般也是一个注解。为了区分,其命名一般是可重复注解的名称加上s。
另外这个容器注解的定义有下面的要求:
@Target必须是可重复标记注解的全集或者子集
@Retention必须和可重复标记注解相同
必须要定义value()数据成员来装载可重复注解,并且value()数据成员的类型是可重复标记注解的数组类型!
可重复的注解标记了@Documented,则注解容器也要标记@Documented
我们现在根据上面的4个要求来定义这个容器注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Target({ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @interface RepeatableAnnotations{ RepeatableAnnotation[] value() default {}; }
然后在RepeatableAnnotation标记上@Repeatable注解!指定其对应的容器注解!
1 2 3 4 5 6 7 8 9 @Target({ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Repeatable(RepeatableAnnotations.class) public @interface RepeatableAnnotation { String description () default "" ; }
现在分别在ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE上进行标记,可以看到只有ElementType.TYPE无法重复!
可以通过反射API中的getAnnotationsByType(RepeatableAnnotation.class)来获取重复的注解!
src/main/java/cn/argento/askia/annotation/repeat/RepeatableTest.java
文档注解 该注解主要和javadoc文档有关,如果希望注解被记录在文档,则标上即可,这里引用core java的说明:
本地变量 首先查看一下@Native注解的源码信息,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Documented @Target(ElementType.FIELD) @Retention(RetentionPolicy.SOURCE) public @interface Native {}
可以看到@Native注解自jdk1.8引入,用于解释该字段是一个常量,其值引用native code,该注解更多和@Override一样,是起到纯标记作用!可以发现它的保留时间为SOURCE阶段,这个用的不是很多,在JNI中可能用到,常常被代码生成工具使用。
重写方法 @Override注解标记的方法是重写方法:
1 2 3 4 @Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override {}
作为一种习惯,建议在所有的重写方法上都标记该注解
1 2 3 4 5 6 7 8 9 10 11 class Father { public void eat () { ... } } class Son extends Father { @Override public void eat () { } }
废弃方法 标记@Deprecated的方法作为是废弃方法
1 2 3 4 5 @Documented @Retention(RetentionPolicy.RUNTIME) @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE}) public @interface Deprecated {}
@Deprecated标记的内容,在被使用时会被加上删除线
函数式接口 只有一个公开方法(public,default方法不算)的接口就被称之为函数式接口,如:
1 2 3 4 5 6 7 8 9 10 11 12 public interface FatherInterface { int size () ; default boolean isEmpty () { return size() == 0 ; } default boolean isNotEmpty () { return !isEmpty(); } }
我们可以在这些函数式接口上标记@FunctionalInterface
1 2 3 4 5 6 7 8 9 10 11 12 13 @FunctionalInterface public interface FatherInterface { int size () ; default boolean isEmpty () { return size() == 0 ; } default boolean isNotEmpty () { return !isEmpty(); } }
安全可变参数 @SafeVarargs用于标记可变参数是安全的,在声明具有模糊类型(比如:泛型)的可变参数的构造函数或方法时 ,Java编译器会报unchecked警告。鉴于这些情况,如果程序员断定声明的构造函数和方法的主体不会对其可变参数执行潜在的不安全的操作,可使用@SafeVarargs进行标记,这样的话,Java编译器就不会报unchecked警告。
1 2 3 4 @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.CONSTRUCTOR, ElementType.METHOD}) public @interface SafeVarargs {}
@SafeVarargs注解,对于非static或非final声明的方法,不适用,会编译不通过,对于非static或非final声明的方法,请使用@SuppressWarnings(“unchecked”)
警告抑制 @SuppressWarnings用于忽略某些警告,如上面的unchecked,参数Value可以填入警告类型,支持同时抑制多种警告
2023.12.17:@SuppressWarnings的可选项取决于你使用哪个发行商的Java编译器的哪个版本以及所使用的IDE环境!
如eclipse支持的选项参考:https://help.eclipse.org/latest/index.jsp?topic=/org.eclipse.jdt.doc.user/tasks/task-suppress_warnings.htm,
idea的参考选项:https://gist.github.com/vegaasen/157fbc6dce8545b7f12c
其中Java语言规范中规定,任何的编译器和IDE都需要实现这两个选项:unchecked和deprecation,可以使用javac -X指令来查看当前版本编译器支持的选项!
idea支持自动生成@Suppresswarnings注解的值:https://www.jetbrains.com/help/idea/disabling-and-enabling-inspections.html#suppress-inspections
ref:https://stackoverflow.com/questions/1205995/what-is-the-list-of-valid-suppresswarnings-warning-names-in-java
可惜我并没有找到关于这个注解的value值有哪些,下表中的值来源于各大网站:
部分无法翻译的或者没有把握是什么意思的则保留原文,写未知的则代表该警告值不知道用在什么用途但确实存在该值!部分地方提供了idea的警告显示!
加粗的为常用值:
value
中文描述
IDEA中警告显示
all
压制所有警告
boxing
抑制装箱、拆箱的警告
cast
抑制强制转换产生的警告
dep-ann
抑制使用了@deprecated的注解
deprecation
抑制使用了@deprecated的方法或者字段
fallthrough
抑制在switch中缺失break的警告
FragmentNotInstantiable
未知
finally
抑制finally模块不返回的警告
hiding
to suppress warnings relative to locals that hide variable
incomplete-switch
忽略没有case的switch语句警告
nls
to suppress warnings relative to non-nls string literals
null
忽略对null的操作(to suppress warnings relative to null analysis)
path
在类路径、源文件路径等中有不存在的路径时的警告。
PointlessBitwiseExpression
未知
rawtypes
使用泛型时忽略没有指定相应类型的警告
restriction
to suppress warnings relative to usage of discouraged or forbidden references
ReferenceEquality
未知
ResultOfMethodCallIgnored
压制返回值被忽略的警告
SameParameterValue
压制参数总是等于某个值的警告
serial
忽略在serializable类中没有声明serialVersionUID变量
static-access
抑制不正确的静态访问方式警告(to suppress warnings relative to incorrect static access)
sunapi
sunapi suppresses javac's warning about using Unsafe; 'all' suppresses eclipse's warning about the unspecified 'sunapi' key. Leave them both. Yes, javac's definition of the word 'all' is quite contrary to what the dictionary says it means. 'all' does NOT include 'sunapi' according to javac.
来自Lombok
synthetic-access
抑制子类没有按最优方法访问内部类的警告(to suppress warnings relative to unoptimized access from inner classes)
try
抑制没有catch时的警告
unchecked
抑制没有进行类型检查操作的警告
unqualified-field-access
抑制没有权限访问的字段的警告(to suppress warnings relative to field access unqualified)
NullableProblems
未知
unused
抑制没被使用过的代码(方法、字段、局部变量等)的警告,有几个子抑制,如:UnusedReturnValue、UnusedParameters
UnusedReturnValue
压制方法返回值从未被使用
UnusedParameters
压制未使用的方法参数
WeakerAccess
未知
注解处理器 根据注解的三种存储策略,因此处理不同策略的注解要使用对应的方式。
需要注意,注解处理器的处理能力有限,部分注解的处理难度相当大,如@Target(ElementType.TYPE_USE),还有一些,如@Target(ElementType.LOCAL_VARIABLE)级别的注解,目前没有更多有效的获取方式!
另外注解处理方法也存在通用性,一般:
SOURCE级别的注解处理手段同样适用于CLASS级别和RUNTIME级别
因为RUNTIME级别注解也会存在字节码中,所以使用CLASS级别的注解处理方法也能处理RUNTIME级别的注解
RUNTIME级别注解处理 在反射API中,有一套专门处理注解的API,这些API位于java.lang.reflect包中,包括所有以AnnotatedXXX的接口:
AnnotatedElement:代表被注解标记的元素,如被一个或者多个注解标记的类、字段、方法等。(JDK 1.5),AnnotatedElement接口是整个RUNTIME级别注解处理中最核心的接口!
AnnotatedType:代表被注解的元素的类型,在Type系统引入之后同时引入的接口,该接口继承自AnnotatedElement,不仅拥有获取、处理注解的能力,还返回一个Type对象,代表一个对象、方法参数、返回值、异常等所属的具体类型(JDK 1.8)
AnnotatedArrayType:代表被注解的类型是一个数组类型,如String[],该接口是AnnotatedType的子接口(JDK 1.8)
AnnotatedParameterizedType:代表被注解的类型是一个参数化类型(泛型),如List<String>、Set<List<String>>等,该接口是AnnotatedType的子接口(JDK 1.8)
AnnotatedTypeVariable:代表被注解的类型是一个泛型变量,如T、U、S,该接口是AnnotatedType的子接口(JDK 1.8)
AnnotatedWildcardType:代表被注解的类型是一个继承性的泛型,如:List<? extends MyClass>、List<T extends MyClass>(JDK 1.8)
除了上面的6大接口外,还有一个类AnnotatedTypeBaseImpl,它代表所有除上面6大接口对应的类型外的其他类型,如String、Integer、int等都会返回AnnotatedTypeBaseImpl实现,该类在sun.reflect.annotation包下!
AnnotatedElement可以说是整个处理中最核心,最重要的接口,其主要用于获取标记在元素(如类、方法、字段等)上的注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public interface AnnotatedElement { default boolean isAnnotationPresent (Class<? extends Annotation> annotationClass) ; <T extends Annotation> T getAnnotation (Class<T> annotationClass) ; Annotation[] getAnnotations(); default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass); default <T extends Annotation> T getDeclaredAnnotation (Class<T> annotationClass) ; default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass); Annotation[] getDeclaredAnnotations(); }
相关的Demo可以参考:
src/main/java/cn/argento/askia/annotation/runtime/AnnotationAPIs.java
AnnotatedElement接口的所有子接口和类基本可以分成两大类:一类是以AnnotatedType及其子接口为主代表类型的接口,另一类则是形如Class<T>、Constructor<T>等对象的组成元素!采用这种设计能帮让你在获取注解的同时获取相关元素的类型,如获取标记在方法返回值上的注解的同时获取该方法返回值的类型!
AnnotatedType及其所有的子接口除了用于获取注解外,额外提供了用于获取具体类型的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public interface AnnotatedType extends AnnotatedElement { public Type getType () ; } public interface AnnotatedArrayType extends AnnotatedType { AnnotatedType getAnnotatedGenericComponentType () ; } public interface AnnotatedParameterizedType extends AnnotatedType { AnnotatedType[] getAnnotatedActualTypeArguments(); } public interface AnnotatedTypeVariable extends AnnotatedType { AnnotatedType[] getAnnotatedBounds(); } public interface AnnotatedWildcardType extends AnnotatedType { AnnotatedType[] getAnnotatedLowerBounds(); AnnotatedType[] getAnnotatedUpperBounds(); }
而另外一类继承了AnnotatedElement接口的对象组成元素,这些对象组成元素可能有多个可被注解标注的地方,比如:在方法上,注解可以标记在方法返回值、方法参数,如果方法抛出异常,则还有可能会标记在异常上!因此通常对象组成元素提供了各种getAnnotatedXXX()方法来获取在这些特定位置上的AnnotatedType及其子接口实例!
继承了AnnotatedElement接口的对象组成元素主要有:Class<T>、Constructor<T> 、Field、Method、Package、Parameter,其中他们的获取方法如下:
Constructor、Method的getAnnotatedReturnType()、getAnnotatedReceiverType()、getAnnotatedParameterTypes()、getAnnotatedExceptionTypes()
Field、Parameter的getAnnotatedType()
Class类上的getAnnotatedSuperclass()、getAnnotatedInterfaces()
1 2 3 4 5 6 7 8 9 10 11 12 13 public AnnotatedType[] getAnnotatedExceptionTypes();public AnnotatedType[] getAnnotatedParameterTypes();public AnnotatedType getAnnotatedReceiverType () ;public abstract AnnotatedType getAnnotatedReturnType () ;public AnnotatedType getAnnotatedType () ;public AnnotatedType[] getAnnotatedInterfaces();public AnnotatedType getAnnotatedSuperclass () ;
Demo参考:src/main/java/cn/argento/askia/annotation/runtime/AnnotatedAPIs.java
receiverType指的是这种类型:
1 2 3 4 5 6 7 public class MyClass <@MyAnnotation T > { public void myMethod (@MyAnnotation MyClass<T> this ) {} }
特别注意,method.getAnnotatedReceiverType().getAnnotations()和method.getAnnotations()的区别:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class MyClass { public void myMethod (@MyAnnotation MyClass this ) { } public static void main (String[] args) { MyClass obj = new MyClass<>(); Class<?> clazz = obj.getClass(); Method md = clazz.getMethod("myMethod" ); Annotation[] myMethods = md.getAnnotations(); AnnotatedType annotatedType = md.getAnnotatedReceiverType(); Annotation[] annotations = annotatedType.getAnnotations(); } }
method.getAnnotatedReceiverType().getAnnotations() 是获取方法receiverType上的注解
method.getAnnotations()是获取标记在方法上的注解!
同理应用于其他getAnnotatedXXXX()方法
参考:src/main/java/cn/argento/askia/annotation/runtime/MyClass.java
2024.2.19
对同一个Class对象,调用多次getAnnotation()(对getDeclaredAnnotation()同理)将获取相同引用对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Utility(name = "AnnotationProcessingHelper", version = "1.0") public class A { } @Utility(name = "AnnotationProcessingHelper", version = "1.0") public class B{ } public static void main(String[] args){ final Utility annotation = A.class.getAnnotation(Utility.class); final Utility annotation1 = A.class.getAnnotation(Utility.class); final Utility annotation2 = B.class.getAnnotation(Utility.class); final Utility annotation3 = B.class.getAnnotation(Utility.class); System.out.println(annotation == annotation1); // true System.out.println(annotation2 == annotation1); // false System.out.println(annotation3 == annotation2); // true System.out.println(annotation2.equals(annotation1)); // true }
这是因为Class类中使用Map来记录所有获取过的注解,当注解第一次被获取并创建对象之后就会被存放在这个Map中(缓存起来),下一次在获取不会重新创建而是直接在这个Map中拿!因此它们实际上是同一个对象
注解中的equals()的比较原理是比较各个属性是否相同,因此只需要属性值相同即可而无需考虑是否来自相同的Class对象
SOURCE级别的注解处理 source级别的注解在编译的时候会被去除,所以唯一能对该类注解进行处理的就是在编译java代码之前,也就是在将java代码编译成字节码这个过程之前进行处理,基于这个特性决定了处理这些SOURCE级别的注解最好的工具就是javac.exe,也就是java编译器(这样可以方便在处理完注解之后立即进行源代码的编译)
JDK 5时期提供了一个可以处理注解的命令行工具:Annotation Processing Tool(APT ),可惜工具没有集成到javac.exe,需要额外运行,并且api在com.sun.mirror包下而非标准包,因此APT在JDK 7以后就被废弃了,取而代之的是JDK 6加入的PAP(Pluggable Annotation Processinig API,插件式注解处理API),该API解决了APT工具遗留的问题。该API位于javax.annotation.processing包中,其中的核心是:
定义一个注解处理器只需要继承AbstractProcess抽象类实现process()方法即可,并且使用@SupportedAnnotationTypes和@SupportedAnnotationTypes指定需要处理的注解:
1 2 3 4 5 6 7 8 @SupportedAnnotationTypes("cn.argento.askia.processors.source.ToString") @SupportedSourceVersion(SourceVersion.RELEASE_8) public class ToStringAnnotationProcessor extends AbstractProcessor { @Override public boolean process (Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { return false ; } }
1 2 3 4 5 6 7 8 9 10 import static java.lang.annotation.RetentionPolicy.*;import static java.lang.annotation.ElementType.*;@Target({TYPE}) @Retention(SOURCE) public @interface ToString { String delimiter () default ", " ; boolean ignoreFieldName () default false ; boolean appendHashCode () default false ; }
可以使用@SupportedOptions()指定编译参数,如:
1 2 3 4 5 6 7 8 9 @SupportedAnnotationTypes("cn.argento.askia.processors.source.ToString") @SupportedSourceVersion(SourceVersion.RELEASE_8) @SupportedOptions("-parameters") public class ToStringAnnotationProcessor extends AbstractProcessor { @Override public boolean process (Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { return false ; } }
源码级注解处理器理论上只能生成新的源代码文件,而无法修改现有的源代码,想要实现修改现有源代码,可以通过修改字节码实现,但源码级注解处理器却可以干预javac对源代码的分析,从而干预字节码的最终生成。javac对源代码进行分析时,会生成一棵抽象语法树(AST),最终编译出来的字节码也是以这颗AST为原型进行编译的,类似于DOM的XML。可以通过直接修改AST的节点的形式来改变最终的字节码的生成,举例就是Lombok中的@Data注解为实体类添加Getter、Setter方法!特别注意这种修改AST的方式并没有修改源文件,只是对源文件做了增强,类似于AOP!
但想要修改AST并非易事,首先修改AST的相关API并没有标准化(在tools.jar中),这意味着需要自行承担使用tools.jar中代码产生的所有风险,其次这些API可能缺少文档和Demo,需要自行研究(这才是使用这些API最大的阻碍!)。
我们先介绍如何使用源码注解处理器来生成新的代码源文件,在注解处理器中,核心就是process()方法的参数,以及AbstractProcessor类中的processingEnv变量:
1 2 3 4 5 6 Set<? extends TypeElement> annotations: 代表要处理的注解,其值是@SupportedAnnotationTypes() 的value值 RoundEnvironment currentRoundEnv: 代表轮询环境。process()会被轮询调用,因为源代码上可能拥有多个注解,需要多个注解处理器进行处理,所以会多次调用process()运行每一个注解处理器。即一个源码级别的注解处理器会触发一次process() 在每一轮中,process()都会被调用一次,调用时会传递改轮中所有文件中发现的所有注解构成的Set集合,以及当前的处理轮询的信息的RoundEnvironment引用。
关于RoundEnvironment,它主要有三个方法:
1 2 3 4 5 6 7 8 Set<? extends Element> getRootElements(); Set<? extends Element> getElementsAnnotatedWith(TypeElement a); Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a); boolean errorRaised () ;boolean processingOver () ;
前面有提到,虽然我们无法直接使用现有的标准化的API来修改AST树,但JDK还是提供了相应的API供我们访问AST树中的各种节点,它们叫语言模型API,位于javax.lang.model包中,注解处理器使用到了其下的element子包,在该子包下定义了各类AST树的节点接口:
TypeElement:代表一个类型节点,可以代表一个注解类型、类、接口、枚举等等
TypeParameterElement:代表泛型参数节点,如T,V extends Number
VariableElement:代表字段,变量,方法参数等
PackageElement:代表包
ExecutableElement:代表可执行的元素,如方法、构造器
这里简单介绍下各个节点接口的API,更多的细节参考LanguageModel模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 TypeMirror asType () ;ElementKind getKind () ;Set<Modifier> getModifiers () ;Name getSimpleName () ;Element getEnclosingElement () ;List<? extends Element> getEnclosedElements(); List<? extends AnnotationMirror> getAnnotationMirrors(); <A extends Annotation> A getAnnotation (Class<A> annotationType) ; <A extends Annotation> A[] getAnnotationsByType(Class<A> annotationType); AnnotationValue getDefaultValue () ;List<? extends VariableElement> getParameters(); TypeMirror getReceiverType () ;TypeMirror getReturnType () ;List<? extends TypeMirror> getThrownTypes(); List<? extends TypeParameterElement> getTypeParameters(); boolean isDefault () ;boolean isVarArgs () ;Name getQualifiedName () ;boolean isUnnamed () ;List<? extends TypeMirror> getInterfaces(); NestingKind getNestingKind () ;TypeMirror getSuperclass () ;Name getQualifiedName () ;List<? extends TypeParameterElement> getTypeParameters(); Element getGenericElement () ;List<? extends TypeMirror> getBounds(); Object getConstantValue () ;
ProcessingEnvironment参数主要用于处理语言模型API,关于语言模型,请参考LanguageModel模块。其中的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Map<String,String> getOptions () ;Messager getMessager () ;Filer getFiler () ;Elements getElementUtils () ;Types getTypeUtils () ;SourceVersion getSourceVersion () ;Locale getLocale () ;
如果希望创建一个源文件,则可以使用getFiler()方法,获取Filer对象之后调用createSourceFile()获取一个JavaFileObject对象,该对象代表一个Java源文件,在编译器API一节中经常用到。
那么如何编写一个注解处理器?可以按照下面的步骤:
调用RoundEnvironment的Set<? extends Element> getElementsAnnotatedWith(TypeElement a);,传递Set<? extends TypeElement> annotations:参数来获取所有标记了该注解的Element对象
对标记了注解的元素进行处理,处理过程需要了解语言模型API
参考:Java-Annotation/src/main/java/cn/argento/askia/processors/source/ToStringAnnotationProcessor.java
代码编写完之后就是编译的问题,那么如何编译注解处理器并使用注解处理器编译其他代码?
要想使用Processor处理源代码,你可以使用下面的方式:
直接使用编译参数指定,格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 javac.exe -processor [注解处理器全限定类名1 , 注解处理器全限定类名2 , ...] [源代码1. java, 源代码2. java, ...] javac.exe -processor [注解处理器全限定类名1 , 注解处理器全限定类名2 , ...] [源代码1. java, 源代码2. java, ...] -XprintRounds
通过服务注册(也就是所谓的SPI机制)指定,在项目根路径下创建:META-INF/services/javax.annotation.processing.Processor文件,添加上自己的注解处理器的全限定类名,多个注解处理器换行分割!
1 2 cn.argento.askia.processors.source.EnumInnerConstantProcessor cn.argento.askia.processors.source.ToStringAnnotationProcessor
通过Maven的编译插件的配置指定如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <plugin > <artifactId > maven-compiler-plugin</artifactId > <version > 3.5.1</version > <configuration > <source > 1.8</source > <target > 1.8</target > <encoding > UTF-8</encoding > <annotationProcessors > <annotationProcessor > cn.argento.askia.processors.source.EnumInnerConstantProcessor </annotationProcessor > </annotationProcessors > </configuration > </plugin >
使用这些方式的大前提是,注解处理器已经编译完成 。因此您需要先对注解处理器进行编译再去编译带注解的源代码,或者是把注解处理器放到一个独立的Jar包引入,然后运行!
为了演示,我们先把所有源代码准备好,所有的源代码都位于cn.argento.askia.processors.source包下,如果你需要更换包,则可能需要修改注解处理器中的代码,因为该注解处理器生成的Java文件都是基于cn.argento.askia.processors.source包的
基于命令行编译运行 我们演示第一种编译方式。首先,我们需要先将注解处理器进行编译(特别注意,您的注解处理器应该也会依赖待处理的注解,如果有则要先把待处理的注解编译然后再编译注解处理器 ,基于命令参数的编译方式非常依赖类的编译顺序 !):
1 2 3 4 5 6 7 8 9 # 编译注解 # ToString.java所在位置:D:\MinecraftModProject\AnnotationProcessTest\src\main\java\cn\argento\askia\processors\source\ToString.java # 这要求您当前CMD 的位置是:D:\MinecraftModProject\AnnotationProcessTest\src\main\java\cn\argento\askia\processors\source\: javac -encoding utf-8 ToString.java # ----------------------------- # 编译注解处理器 javac -encoding utf-8 -classpath D:\MinecraftModProject\AnnotationProcessTest\src\main\java ToStringAnnotationProcessor.java
指定encoding参数是因为Java源代码文件内有中文,不加上这个参数会出现:
由于注解处理器的编译依赖注解@ToSting,当我们编译完@ToString之后,需要告诉java编译器@ToString.class所在的位置,即指定编译参数-classpath,该位置也比较特殊,需要去掉@ToString所在包,如@ToString.class所在位置是:D:\MinecraftModProject\AnnotationProcessTest\src\main\java\cn\argento\askia\processors\source\ToString.java
所在包:
则需要指定编译参数:javac -classpath D:\MinecraftModProject\AnnotationProcessTest\src\main\java ToStringAnnotationProcessor.java
然后,我们编译待处理用户类:
1 2 3 4 5 6 7 8 9 10 11 12 javac -cp D:\OpenSourceProject\JavaProject\Java-Annotation\src\main\java -processor cn.argento.askia.processors.source.ToStringAnnotationProcessor User.java -XprintRounds # 生成的ToStrings.java会放在javac运行的回显位置,如你的cmd 是这样的: C:\Users \asus >javac -cp D :\OpenSourceProject \JavaProject \Java -Annotation \src \main \java -processor cn.argento.askia.processors.source.ToStringAnnotationProcessor D :\OpenSourceProject \JavaProject \Java -Annotation \src \main \java \cn \argento \askia \processors \source \User.java -XprintRounds # 则生成的ToStrings.java 会放在C :\Users \asus 里面! # 当然你可以使用-s 参数指定生成的源文件的位置 # 另外说一下 -d 是指定生成的class 文件的位置 # -h 指定生成的头文件的位置 javac -s D :\ -cp D :\OpenSourceProject \JavaProject \Java -Annotation \src \main \java -processor cn.argento.askia.processors.source.ToStringAnnotationProcessor D :\OpenSourceProject \JavaProject \Java -Annotation \src \main \java \cn \argento \askia \processors \source \User.java -XprintRounds
编译过程会显示下面的注解处理循环信息:
等待编译完成之后,则在源代码中会出现生成的代码文件和字节码文件:
基于服务注册编译运行
我们先将注解处理器代码和对应的注解代码放在一个新的项目中:
在项目根目录中添加SPI文件,如果是普通的Idea Java项目,则在src添加,如果是maven项目,则在resources
SPI文件内填写注解处理器的全限定类名
使用IDEA或者Maven编译成jar包, 请确保编译出来的jar包有如下文件:
接下来使用这个Jar包来编译Java文件(即使用Javac的-cp或者-classpath参数指定依赖的Jar)就可以触发注解处理器了,命令如下:
1 javac -classpath AnnoJAr-1 .0 -SNAPSHOT.jar User.java
如果是maven项目,则可以采用添加本地Jar依赖的方式(只要能够将Jar放入classpath就ok)
新建另外一个项目,放入待编译的代码
引入Jar依赖(本地依赖,把Jar包放在项目根本目录,然后写Dependency)
现在点击 就可以看到注解处理编译了
同时在target目录下会有我们生成的代码
基于Maven编译插件中注解处理器参数运行 这种方式和基于服务注册编译运行一致,区别在于如果编译出来的Jar没有带SPI文件,如:
则您可以在pom.xml中指定编译插件的注解处理器参数来编译项目,参数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <build > <plugins > <plugin > <artifactId > maven-compiler-plugin</artifactId > <version > 3.6.1</version > <configuration > <source > 1.8</source > <target > 1.8</target > <encoding > UTF-8</encoding > <annotationProcessors > <annotationProcessor > cn.argento.askia.processors.source.ToStringAnnotationProcessor </annotationProcessor > </annotationProcessors > </configuration > </plugin > </plugins > </build >
这样也可以运行注解处理器而无需在META-INF/services下编写SPI文件!
CLASS级别注解处理 // 待补充,暂时不会字节码插桩!笨比博主在学了在学了!😭😭
注解源码结构 所有的注解实际上都隐式地继承于java.lang.annotation.Annotation接口 ,该接口是个常规接口 ,定义如下:
1 2 3 4 5 6 7 public interface Annotation { boolean equals (Object obj) ; int hashCode () ; String toString () ; Class<? extends Annotation> annotationType(); }
注解类型直接通过getClass()获得的Class对象不是注解类型本身,而是代理对象(注解的结构和动态代理有关),也就是注解接口的具体实现对象,想要获取真正的注解类型,必须使用Annotation接口提供给的annotationType()
1 2 3 4 5 6 7 8 Test test = this .getClass().getAnnotation(Test.class); System.out.println(test.annotationType()); System.out.println(test.getClass());
Annotation接口的String toString();将会输入一个包含注解接口及其元素名称和默认值的字符串表示,如:
1 2 System.out.println(test.toString());
同样回到BugReport注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface BugReport { long value () default 2000 ; String versions () default "1.1.0" ; int version () default 1 ; double subVersion () default 1.0 ; boolean showStopper () default false ; String assignedTo () default "[none]" ; Class<?> testCase() default Void.class; Status status () default Status.CONFIRMED ; Reference ref () default @Reference ; String[] reportedBy() default "" ; Class<?>[] clazz() default {}; enum Status {UNCONFIRMED, CONFIRMED, FIXED, NOTABUG} @interface Reference{ String ref () default "" ; String url () default "" ; } interface B { } class A { public void test () { } } }
对注解进行反编译(使用javap.exe),获取到底层处理注解的方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public interface cn .argento .askia .annotation .full .BugReport extends java .lang .annotation .Annotation { public abstract long value () ; public abstract java.lang.String versions () ; public abstract int version () ; public abstract double subVersion () ; public abstract boolean showStopper () ; public abstract java.lang.String assignedTo () ; public abstract java.lang.Class<?> testCase(); public abstract cn.argento.askia.annotation.full.BugReport$Status status () ; public abstract cn.argento.askia.annotation.full.BugReport$Reference ref () ; public abstract java.lang.String[] reportedBy(); public abstract java.lang.Class<?>[] clazz(); }
到此位置,我们知道定义注解的@interface和相关数据成员会被进行怎样的处理,但还有一个关键问题,注解中的数据成员的默认值是如何绑定到方法的呢?
既然注解技术依赖于动态代理,我们可以尝试将代理对象的源代码拿出来进行分析,在代码中,添加这个系统属性可以将代理类导出!
1 System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles" , "true" );
拿到这个代理类的源代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 package com.sun.proxy;import cn.argento.askia.annotation.full.BugReport;import cn.argento.askia.annotation.full.BugReport.Reference;import cn.argento.askia.annotation.full.BugReport.Status;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.lang.reflect.UndeclaredThrowableException;public final class $Proxy2 extends Proxy implements BugReport { private static Method m1; private static Method m6; private static Method m12; private static Method m2; private static Method m8; private static Method m14; private static Method m5; private static Method m11; private static Method m0; private static Method m10; private static Method m7; private static Method m9; private static Method m13; private static Method m3; private static Method m4; public $Proxy2(InvocationHandler对象中 var1) throws { super (var1); } public final boolean equals (Object var1) throws { try { return (Boolean)super .h.invoke(this , m1, new Object[]{var1}); } catch (RuntimeException | Error var3) { throw var3; } catch (Throwable var4) { throw new UndeclaredThrowableException(var4); } } public final Reference ref () throws { try { return (Reference)super .h.invoke(this , m6, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final Class testCase () throws { try { return (Class)super .h.invoke(this , m12, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final String toString () throws { try { return (String)super .h.invoke(this , m2, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final boolean showStopper () throws { try { return (Boolean)super .h.invoke(this , m8, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final Class annotationType () throws { try { return (Class)super .h.invoke(this , m14, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final Status status () throws { try { return (Status)super .h.invoke(this , m5, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final String assignedTo () throws { try { return (String)super .h.invoke(this , m11, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final int hashCode () throws { try { return (Integer)super .h.invoke(this , m0, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final String versions () throws { try { return (String)super .h.invoke(this , m10, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final int version () throws { try { return (Integer)super .h.invoke(this , m7, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final double subVersion () throws { try { return (Double)super .h.invoke(this , m9, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final String[] reportedBy() throws { try { return (String[])super .h.invoke(this , m13, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final Class[] clazz() throws { try { return (Class[])super .h.invoke(this , m3, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final long value () throws { try { return (Long)super .h.invoke(this , m4, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } static { try { m1 = Class.forName("java.lang.Object" ).getMethod("equals" , Class.forName("java.lang.Object" )); m6 = Class.forName("cn.argento.askia.annotation.full.BugReport" ).getMethod("ref" ); m12 = Class.forName("cn.argento.askia.annotation.full.BugReport" ).getMethod("testCase" ); m2 = Class.forName("java.lang.Object" ).getMethod("toString" ); m8 = Class.forName("cn.argento.askia.annotation.full.BugReport" ).getMethod("showStopper" ); m14 = Class.forName("cn.argento.askia.annotation.full.BugReport" ).getMethod("annotationType" ); m5 = Class.forName("cn.argento.askia.annotation.full.BugReport" ).getMethod("status" ); m11 = Class.forName("cn.argento.askia.annotation.full.BugReport" ).getMethod("assignedTo" ); m0 = Class.forName("java.lang.Object" ).getMethod("hashCode" ); m10 = Class.forName("cn.argento.askia.annotation.full.BugReport" ).getMethod("versions" ); m7 = Class.forName("cn.argento.askia.annotation.full.BugReport" ).getMethod("version" ); m9 = Class.forName("cn.argento.askia.annotation.full.BugReport" ).getMethod("subVersion" ); m13 = Class.forName("cn.argento.askia.annotation.full.BugReport" ).getMethod("reportedBy" ); m3 = Class.forName("cn.argento.askia.annotation.full.BugReport" ).getMethod("clazz" ); m4 = Class.forName("cn.argento.askia.annotation.full.BugReport" ).getMethod("value" ); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError(var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError(var3.getMessage()); } } }
代理对象不出意外地实现了注解BugReport接口,动态代理的关键就在InvocationHandler对象中,代理对象的所有方法都是委托InvocationHandler对象来完成的!那传递给这个代理对象的InvocationHandler对象又是什么呢?
幸好,动态代理的API提供了获取InvocationHandler对象的方法:
1 2 public static InvocationHandler getInvocationHandler (Object proxy) throws IllegalArgumentException
使用下面的代码获取具体的InvocationHandler对象:
1 2 3 final BugReport annotation = Myclass2.class.getAnnotation(BugReport.class);final InvocationHandler invocationHandler = Proxy.getInvocationHandler(annotation);System.out.println(invocationHandler.getClass());
得到结果:
1 class sun .reflect .annotation .AnnotationInvocationHandler
AnnotationInvocationHandler类中的主要核心如下:
其中memberValues用于存放数据成员的值,invoke()方法对注解中的方法的调用进行拦截,然后返回值!
据此可以得到初步结论:
注解采用接口中的方法来表示变量
Java为注解创建一个代理类。这个代理类包括一个AnnotationInvocationHandler成员变量
AnnotationInvocationHandler有一个Map成员变量,用于存储所有的注解的属性赋值!
在程序中,调用注解接口的方法,将会被代理类接管,然后根据方法名字,到Map里面拿相应的Value并返回。
传递给AnnotationInvocationHandler的用于初始化Map成员变量的各种注解方法的默认值被AnnotationParser类的parseXXX()解析获得
2024.1.22
虽然没有什么意义,但你仍然可以采用匿名类的形式直接new注解并且直接调用他的方法,这时注解属性的default将不起作用,相当于new接口!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface Report{ String value () default "" ; } Report report = new Report(){ @Override public String value () { return null ; } @Override public Class<? extends Annotation> annotationType() { return null ; } }; report.value();
可重复标记注解的实现 如下面的类,他标记了可重复注解@RepeatableAnnotation
1 2 3 4 5 6 7 8 9 10 11 @BugReport(2) @InheritedAnnotation class Bug { @BugReport(2) @RepeatableAnnotation(description = "setBug method1") @RepeatableAnnotation(description = "setBug method2") public Bug setBug (String bug) { this .bug = bug; return this ; } }
经过编译之后,它实际上会变成这样(InheritedAnnotationContainer是InheritedAnnotation的注解容器):
1 2 3 4 5 6 7 8 9 10 11 12 13 @BugReport(2) @InheritedAnnotation class Bug{ @BugReport(2) @InheritedAnnotationContainer({ @RepeatableAnnotation(description = "setBug method1"), @RepeatableAnnotation(description = "setBug method2") }) public Bug setBug(String bug) { this.bug = bug; return this; } }
没错,当可重复注解标记了一个的时候,则会只显示一个可重复注解,但是如果标记了多个的话则会使用注解容器!
使用反射获取注解时,特别注意getAnnotationsByType()方法是JDK 1.8才引入的,在1.8以前,只能通过获取可重复注解的注解容器的形式来获取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Target({ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Repeatable(RepeatableAnnotation.RepeatableAnnotations.class) public @interface RepeatableAnnotation { String description () default "" ; @Documented @Target({ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @interface RepeatableAnnotations{ RepeatableAnnotation[] value() default {}; } }
1 2 3 4 5 6 7 8 9 final RepeatableAnnotation.RepeatableAnnotations annotation1 = setBug.getAnnotation(RepeatableAnnotation.RepeatableAnnotations.class);final RepeatableAnnotation[] value = annotation1.value();System.out.println(Arrays.toString(value)); final RepeatableAnnotation[] annotationsByType = setBug.getAnnotationsByType(RepeatableAnnotation.class);System.out.println(Arrays.toString(annotationsByType));
在标记可重复注解的时候,实际上最终被编译成字节码的时候标记在元素上的不是可重复注解而是注解容器,所以使用isAnnotationPresent()判断是否标记上可重复注解时,永远都返回false,除非你只标记了一个可重复注解!
1 2 3 4 5 System.out.println("setBug方法上是否标记了RepeatableAnnotation注解=" + setBug.isAnnotationPresent(RepeatableAnnotation.class)); System.out.println("setBug方法上是否标记了RepeatableAnnotations注解容器=" + setBug.isAnnotationPresent(RepeatableAnnotation.RepeatableAnnotations.class));
注解应用框架 Lombok Lombok是一个非常优秀的java类库,它利用注解方式自动生成java bean中getter、setter、equals等方法,还能自动生成 logger、toString、hashCode、builder等 日志相关变量。
在Lombok的使用中,核心的注解主要位于:
lombok.*(主包),该包下的注解都是功能稳定的注解,如常用的@Setter、@Getter、@Data等
lombok.experimental.*(实验包)该包下的注解都是带实验性质的(不稳定),一些测试注解或者从主包被废弃的注解会被移动到这里
lombok.extern.*(扩展包)该包下的注解针对其他第三方库(主要是日志),如slf4j、log4j、commonLog等
主包注解
val&&var 首先lombok中支持你在局部变量里面使用val和var来代替具体的类型,val会生成final的局部变量
1 2 3 4 5 6 public void test () { var a = new ArrayList<String>(); val b = new User(); }
@Data及相关的注解 对于实体类,使用@Data注解将会为:
默认构造器(空构造器,如果有final字段或者字段被标记了@NonNull,则为这些字段生成一个构造器)
为所有非final字段生成Setter
为所有字段生成Getter
生成equals()和hashCode()
生成一个以逗号分割成员,括号作为左右链接符的toString()
@Data中有一个数据成员:
1 String staticConstructor () default ""
如果指定了数据成员,则使用工厂的方式来初始化实体类,会将构造器定义为private,然后生成一个public static 的工厂方法,工厂方法名就是staticConstructor()的值!
1 2 3 4 5 6 7 8 9 10 11 12 @Data(staticConstructor = "newInstance") public class StaticUser { } public class StaticUser { private StaticUser () {} public static StaticUser newInstance () { return new StaticUser(); } }
由于默认情况下@Data注解只会生成默认构造器或者为final成员生成构造器。在lombok中,有三个和构造器生成有关的注解:
@AllArgsConstructor:生成全参构造器
@NoArgsConstructor:生成无参构造器
@RequiredArgsConstructor:和@Data注解的构造器生成一样,专门生成final字段和@NonNull字段的构造器
注意当标记了@Data和@AllArgsConstructor的时候,@AllArgsConstructor会覆盖掉@Data的默认构造器,使类只有一个AllArgs的构造器,因此如果希望实体类既有无参构造器,又有全参构造器,则需要将@AllArgsConstructor和@NoArgsConstructor都标记上!
1 2 3 4 5 6 @Data @AllArgsConstructor @NoArgsConstructor public class StaticUser { }
如果希望生成无参构造、全参构造和部分参数构造,则可以这三个注解配合来使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data @NoArgsConstructor @AllArgsConstructor @RequiredArgsConstructor public class User { @NonNull private String name; @NonNull private int id; private int age; private List<String> cards; } public User () {}public User (String name, int id) {}public User (String name, int id, inta age, List<String> cards) {}
这三个构造器注解中,有两个比较重要的数据属性:
staticName():和@Data的staticConstructor()作用相同
access():代表访问级别,如public、private等
除了使用@Data之外,还可以使用@Getter和@Setter为其添加响应字段的Setter和Getter方,@Getter和@Setter的value属性可以指定方法的访问级别。
@Getter和@Setter也可以标记在单个字段上,这样则只会为该字段生成Getter和Setter
1 2 3 4 5 6 7 8 9 10 11 12 13 @Getter @Setter public class User { @NonNull private String name; @NonNull private int id; @Getter @Setter("lombok.AccessLevel.protected") private int age; private List<String> cards; }
也可以单独使用@ToString来为实体类生成toString(),@ToString中,你可以使用下面属性:
boolean includeFieldNames():输出的toString()中是否包含字段名称,默认是true
boolean callSuper():子类的toString是否调用super.toString()来打印父类的字段,默认是false
boolean doNotUseGetters() default false;:是否不使用getter方法来获取值,一般情况下@ToString生成的toString()是调用getter来获取值的,设置为true则直接使用字段而非Getter
另外还有两个数据成员:exclude()和of(),他们分别代表打印toString时需要排除掉的字段和需要打印的字段,这两个数据成员将会被@ToString.Exclude注解和@ToString.Include注解替代。
当前Lombok中,@ToString默认情况下会自动输出全部字段的toString(),只有设置了boolean onlyExplicitlyIncluded()为true,这样他就只会输出标记了@ToString.Include的字段。
@EqualsAndHashCode注解数据成员和@ToString相同!
@Builder和@Singular @Builder注解用于为一个实体类生成一个Builder类,如果实体类中有List、Set等集合类型,则额外添加清空所有成员的clearXX()方法,并且可以配合
@Singular使用为这些集合类型生成一个添加单个成员的add(),该add()方法名由@Singular的value数据成员控制
@Builder注解生成如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 @Builder class Example <T > { private T foo; private final String bar; @Singular("addCard") private List<String> cards; } class Example <T > { private T foo; private final String bar; private List<String> cards; private Example (T foo, String bar, List<String> cards) { this .foo = foo; this .bar = bar; this .cards = cards; } public static <T> ExampleBuilder<T> builder () { return new ExampleBuilder<T>(); } public static class ExampleBuilder <T > { private T foo; private String bar; private List<String> cards; public User2.User2Builder addCard (String addCard) { if (this .cards == null ) { this .cards = new ArrayList(); } this .cards.add(addCard); return this ; } public User2.User2Builder cards (Collection<? extends String> cards) { if (cards == null ) { throw new NullPointerException("cards cannot be null" ); } else { if (this .cards == null ) { this .cards = new ArrayList(); } this .cards.addAll(cards); return this ; } } public User2.User2Builder clearCards () { if (this .cards != null ) { this .cards.clear(); } return this ; } private ExampleBuilder () {} public ExampleBuilder foo (T foo) { this .foo = foo; return this ; } public ExampleBuilder bar (String bar) { this .bar = bar; return this ; } @java .lang.Override public String toString () { return "ExampleBuilder(foo = " + foo + ", bar = " + bar + ", cards = " + this .cards + ")" ; } public Example build () { return new Example(foo, bar, cards); } } }
@Cleanup @Cleanup将用于自动关闭IO流,注意他将会包裹整个方法的所有代码!该注解接收一个数据成员,用于指定关闭外部资源的方法的名称!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public void copyFile (String in, String out) throws IOException { @Cleanup FileInputStream inStream = new FileInputStream(in); @Cleanup FileOutputStream outStream = new FileOutputStream(out); byte [] b = new byte [65536 ]; while (true ) { int r = inStream.read(b); if (r == -1 ) break ; outStream.write(b, 0 , r); } } public void copyFile (String in, String out) throws IOException { @Cleanup FileInputStream inStream = new FileInputStream(in); try { @Cleanup FileOutputStream outStream = new FileOutputStream(out); try { byte [] b = new byte [65536 ]; while (true ) { int r = inStream.read(b); if (r == -1 ) break ; outStream.write(b, 0 , r); } } finally { if (outStream != null ) outStream.close(); } } finally { if (inStream != null ) inStream.close(); } }
@NonNull @NonNull可以标记在字段、参数上,标记在字段上,则会在判断是否字段为null的基础上,在构造器添加该字段的初始化!注意如果构造方法中有this()和super()则会在这行代码之后添加null检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package cn.argento.askia.apps.lombok;import lombok.NonNull;import lombok.RequiredArgsConstructor;@RequiredArgsConstructor public class NonNullExample extends Object { private String name; @NonNull private Integer id; public NonNullExample (@NonNull String person) { super (); this .name = person; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package cn.argento.askia.apps.lombok;import lombok.NonNull;public class NonNullExample { private String name; @NonNull private Integer id; public NonNullExample (@NonNull String person) { if (person == null ) { throw new NullPointerException("person is marked non-null but is null" ); } else { this .name = person; } } public NonNullExample (@NonNull Integer id) { if (id == null ) { throw new NullPointerException("id is marked non-null but is null" ); } else { this .id = id; } } }
@SneakyThrows @SneakyThrows用于再抛出异常,数据成员value指定再抛出的类型,生成的代码会调用Lombok类的下面两个方法抛出异常:
1 2 3 4 5 6 7 8 9 public static RuntimeException sneakyThrow (Throwable t) { if (t == null ) throw new NullPointerException("t" ); return Lombok.<RuntimeException>sneakyThrow0(t); } @SuppressWarnings("unchecked") private static <T extends Throwable> T sneakyThrow0 (Throwable t) throws T { throw (T)t; }
1 2 3 4 5 6 7 8 9 10 11 public class SneakyThrowsExample implements Runnable { @SneakyThrows(UnsupportedEncodingException.class) public String utf8ToString (byte [] bytes) { return new String(bytes, "UTF-8" ); } @SneakyThrows public void run () { throw new Throwable(); } }
生成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import lombok.Lombok;public class SneakyThrowsExample implements Runnable { public String utf8ToString (byte [] bytes) { try { return new String(bytes, "UTF-8" ); } catch (UnsupportedEncodingException e) { throw Lombok.sneakyThrow(e); } } public void run () { try { throw new Throwable(); } catch (Throwable t) { throw Lombok.sneakyThrow(t); } } }
@Synchronized @Synchronized作用于方法,代表方法是同步的!使用value数据成员指定锁!(指定锁的时候,锁一定要已经存在!)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class SynchronizedExample { private final Object readLock = new Object(); @Synchronized public static void hello () { System.out.println("world" ); } @Synchronized public int answerToLife () { return 42 ; } @Synchronized("readLock") public void foo () { System.out.println("bar" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class SynchronizedExample { private static final Object $LOCK = new Object[0 ]; private final Object $lock = new Object[0 ]; private final Object readLock = new Object(); public static void hello () { synchronized ($LOCK) { System.out.println("world" ); } } public int answerToLife () { synchronized ($lock) { return 42 ; } } public void foo () { synchronized (readLock) { System.out.println("bar" ); } } }
@Value @Value和@Data很像,但@Value不会生成Setter方法,并且类和字段都会被设置成final形式,数据成员staticConstructor()和@AllArgsConstructor等的相同,可以指定@NonFinal注解来改变是否需要加final关键字:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import lombok.AccessLevel;import lombok.experimental.NonFinal;import lombok.experimental.Value;import lombok.experimental.With;import lombok.ToString;@Value public class ValueExample { String name; @With(AccessLevel.PACKAGE) @NonFinal int age; double score; protected String[] tags; @ToString(includeFieldNames=true) @Value(staticConstructor="of") public static class Exercise <T > { String name; T value; } }
生成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 import java.util.Arrays;public final class ValueExample { private final String name; private int age; private final double score; protected final String[] tags; @java .beans.ConstructorProperties({"name" , "age" , "score" , "tags" }) public ValueExample (String name, int age, double score, String[] tags) { this .name = name; this .age = age; this .score = score; this .tags = tags; } public String getName () { return this .name; } public int getAge () { return this .age; } public double getScore () { return this .score; } public String[] getTags() { return this .tags; } @java .lang.Override public boolean equals (Object o) { if (o == this ) return true ; if (!(o instanceof ValueExample)) return false ; final ValueExample other = (ValueExample)o; final Object this $name = this .getName(); final Object other$name = other.getName(); if (this $name == null ? other$name != null : !this $name.equals(other$name)) return false ; if (this .getAge() != other.getAge()) return false ; if (Double.compare(this .getScore(), other.getScore()) != 0 ) return false ; if (!Arrays.deepEquals(this .getTags(), other.getTags())) return false ; return true ; } @java .lang.Override public int hashCode () { final int PRIME = 59 ; int result = 1 ; final Object $name = this .getName(); result = result * PRIME + ($name == null ? 43 : $name.hashCode()); result = result * PRIME + this .getAge(); final long $score = Double.doubleToLongBits(this .getScore()); result = result * PRIME + (int )($score >>> 32 ^ $score); result = result * PRIME + Arrays.deepHashCode(this .getTags()); return result; } @java .lang.Override public String toString () { return "ValueExample(name=" + getName() + ", age=" + getAge() + ", score=" + getScore() + ", tags=" + Arrays.deepToString(getTags()) + ")" ; } ValueExample withAge (int age) { return this .age == age ? this : new ValueExample(name, age, score, tags); } public static final class Exercise <T > { private final String name; private final T value; private Exercise (String name, T value) { this .name = name; this .value = value; } public static <T> Exercise<T> of (String name, T value) { return new Exercise<T>(name, value); } public String getName () { return this .name; } public T getValue () { return this .value; } @java .lang.Override public boolean equals (Object o) { if (o == this ) return true ; if (!(o instanceof ValueExample.Exercise)) return false ; final Exercise<?> other = (Exercise<?>)o; final Object this $name = this .getName(); final Object other$name = other.getName(); if (this $name == null ? other$name != null : !this $name.equals(other$name)) return false ; final Object this $value = this .getValue(); final Object other$value = other.getValue(); if (this $value == null ? other$value != null : !this $value.equals(other$value)) return false ; return true ; } @java .lang.Override public int hashCode () { final int PRIME = 59 ; int result = 1 ; final Object $name = this .getName(); result = result * PRIME + ($name == null ? 43 : $name.hashCode()); final Object $value = this .getValue(); result = result * PRIME + ($value == null ? 43 : $value.hashCode()); return result; } @java .lang.Override public String toString () { return "ValueExample.Exercise(name=" + getName() + ", value=" + getValue() + ")" ; } } }
@With @With注解用来实现给类字段生成一个withXXX(XXX为字段名)的方法,其内部方法代码如下:
1 2 3 public WithExample withAge (int age) { return this .age == age ? this : new WithExample(name, age); }
@With可以标记在类和字段上,标记在类上,则为类的所有字段生成withXXX(),标记在字段上则单独生成withXXX()
1 2 3 4 5 6 7 8 @Target({ElementType.FIELD, ElementType.TYPE}) @Retention(RetentionPolicy.SOURCE) public @interface With { AccessLevel value () default AccessLevel.PUBLIC ; AnyAnnotation[] onMethod() default {}; AnyAnnotation[] onParam() default {}; }
1 2 3 4 5 6 7 8 9 10 11 12 13 import lombok.AccessLevel;import lombok.NonNull;import lombok.With;public class WithExample { @With(AccessLevel.PROTECTED) @NonNull private final String name; @With private final int age; public WithExample (@NonNull String name, int age) { this .name = name; this .age = age; } }
生成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import lombok.NonNull;public class WithExample { private @NonNull final String name; private final int age; public WithExample (String name, int age) { if (name == null ) throw new NullPointerException(); this .name = name; this .age = age; } protected WithExample withName (@NonNull String name) { if (name == null ) throw new java.lang.NullPointerException("name" ); return this .name == name ? this : new WithExample(name, age); } public WithExample withAge (int age) { return this .age == age ? this : new WithExample(name, age); } }
扩展包注解(日志) 扩展包中的注解主要用来生成日志Logger对象,如:
1 private static final Logger log = LoggerFactory.getLogger(LogExample.class);
相关的注解主要有及其生成的Logger对象:
使用方法如下:
1 2 3 4 5 6 7 8 @Log public class LogExample { public static void main (String... args) { log.severe("Something's wrong here" ); } }
将会生成:
1 2 3 4 5 6 7 public class LogExample { private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(LogExample.class.getName()); public static void main (String... args) { log.severe("Something's wrong here" ); } }
实验包注解
Jcommander http://jcommander.org/#_overview
// 带补充…
引用文章参考
《core java 11》
document JDK 8
华东师范大学慕课Java核心技术
CSDN文章
Lombok:
JDK 11 @Deprecated JDK 11时,该注解添加了两个数据成员:
1 2 3 4 5 6 7 @Documented @Retention(RetentionPolicy.RUNTIME) @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE}) public @interface Deprecated { String since () default "" ; boolean forRemoval () default false ; }