整体Retrofit内容如下:
- 1、Retrofit解析1之前哨站——理解RESTful
- 2、Retrofit解析2之使用简介
- 3、Retrofit解析3之反射
- 4、Retrofit解析4之注解
- 5、Retrofit解析5之代理设计模式
- 6、Retrofit解析6之面向接口编程
- 7、Retrofit解析7之相关类解析
- 8、Retrofit解析8之核心解析——ServiceMethod及注解1
- 9、Retrofit解析8之核心解析——ServiceMethod及注解2
- 10、Retrofit解析9之流程解析
- 11、Retrofit解析10之感谢
由于Retrofit里面大量的用到了注解,为了让大家更好的学习Retrofit,特意准备了一篇Java注解,如果大家已经对Java注解已经很熟悉了,就略过,看下一篇文章
本篇文章主要讲解
- 1、Java 注解技术基本概念
- 2、Java 元注解
- 3、标准注解/内建注解
- 4、自定义注解
- 5、注解处理器
- 6、注解思维导图
- 7、注解原理
一、Java注解技术基本概念
(一) 什么是注解
Annotation是java 5开始引入的新特征。中文名称一般叫注解。它提供了一种安全的类似于注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。
上面的类似官方的解释,那我们再来通俗的解释一下:
我们都知道在Java代码中使用注解是为了提升代码的可读性,也就是说,注释是给人看的(对编译器来说是没有意义上的)。注解可以看做注释的"加强升级版",它可以向编译器、虚拟机等解释说明一些事情(也就是说它对编译器等工具也是"可读"的)。比如我们非常熟悉的@Overrider 注解,它的作用是告诉编译器它所注解的方法是重写父类中的方法,这样编译器就会检查父类是否存在这个方法,以及这个方法的签名与父类是否相同。
也就是说,注解是描述Java代码的代码,它能够被编译器解析,注解处理工具在运行时也能够解释注解。除了向编译器等传递一些信息,我们也可以用注解生成代码。比如我们可以用注解描述我们的意图,然后让注解解析工具来解析注解,以此来生成一些"模板化"的代码。注解是一种"被动"的信息,必须有编译器或虚拟机来"主动"解析它,它才能发挥自己的作用。
(二) 什么是元数据(metadata)
元数据由metadata翻译来的,所谓元数据就是"关于数据的数据",更通俗的说就是描述数据的数据的,对数据及信息资源的描述性信息,比如一个文本文件,有创建时间、创建人、文件大小等数据,都是可以理解为是元数据。在java中,元数据以标签的形式存在java代码中,它的存在并不影响程序代码的编译和执行,通常它被用来生成其他的文件或运行时知道被运行代码的描述信息。java代码中的javadoc和注解都属于元数据。
(三) 注解的前世今生
注解首先在第三版的Java Language Specification中被提出,并在Java 5中被实现。
(四)为什么要使用注解
- 1、在未使用Annotation之前(甚至是使用之后),一般使用XML来应用于元数据的描述。不是何时开始一些开发人员和架构师发现XML的维护原来越复杂和糟糕,他们希望使用一些和代码紧密耦合的东西,而不是像XML一样是松耦合的(在某些情况下甚至是完全分离的)代码描述。如果你在百度或者google中搜索"xml vs annotations",就会看到关于这个话题的辩论。因为XML的配置就是为了分离代码和配置而设置的。但是用Annotation(注解)还是XML各有利弊。(下面有举例说明)
- 2、另外一个很重要的因素是Annotation注解定义一种标准的描述元数据的方式。在这之前,开发者通常使用他们自己的方式定义元数据。例如,使用标记interface,注释,transient关键字等。每个程序员都用自己的方式定义元数据,而不像Annotation这种标准的方式。
下面我简单举例说明。
比如,你想为你的应用设置很多常量或参数,这种情况下,XML是一个很好的选择,因为它不会与特定的代码关联。如果你想把某个方法声明为服务,那么使用Annotation会更好一些,因为这种情况下需要注解和方法高度耦合一起。
因为XML是松耦合的,注解是紧耦合的,所以目前主流的框架将XML和Annotation两种方式结合使用,平衡两者之前的利弊。
在需要高度耦合的地方,Annotation注解比XML更容易维护,阅读更方便
在需要松耦合的地方,使用XML更方便
在某个方法声明为服务时,这种紧耦合的情况下,比较适合Annation注解。
(五)、注解的作用
Annotation 注解 通常被用以作以下目的:
- 1、编译器指令
- 2、构建时指令
- 3、运行时指令
Java 内置了三种编译器指令,Java注解可以应用于构建时,即当你构建你的项目时,构建的过程包括产生源代码、编译源代码、产生xml文件,将编译过的代码或者文件打包进jar文件等。通常情况下,注解不会出现在编译之后的Java代码中,但是想要出现也是可以的。Java支持运行时注解。这些注解可以通过java反射访问,运行时注解主要是提供给程序或者第三方API一些指令。
(六) 注解基础
一个简单的Java注解 类似于下面的这种 @Doctor ,"@" 符号告诉编译器这是一个注解,跟在"@" 符号后面的是注解的名字,上述的例子中注解的名字是Doctor。
(七) 注解元素
Java 注解可以使用元素设置一些值,元素类似于属性或者参数。下面是一个包含元素注解的例子
@Doctor (name = "张三")
上述注解的元素名称是name,值是"张三",没有元素的注解不需要括号。注解可以包含多个元素,下面就是包含多个元素的例子
@Doctor(name = "张三", sex= "男")
当注解只包含一个元素时,你可以省去写元素的名字,直接赋值即可。下面的例子就是直接赋值。
@InsertNew("yes")
(八)注解使用
Annotation 注解可以在以下场合被使用到
- 类
- 接口
- 方法
- 方法参数
- 属性
- 局部变量
二、元注解
(一) 什么是元注解
元注解,元注解就是负责注解其它注解.Java5.0定义了4个标准的meta-annotation类型,它们呗用来提供对其他annotation类型作说明。Java5.0定义的元注解:
- @Target
- @Retention
- @Documented
- @Inherited
这些类型和它们所支持的类在java.lang.annotation包中可以找到。下面我们来看一下每一个元注解的作用和说明
1、@Target
表示该注解可以用在什么地方,由ElementType枚举定义
- CONSTRUCTOR:构造器的声明
- FIELD:域声明(包括enum实例)
- LOCAL_VARIABLE:布局变量声明
- METHOD:方法声明
- PACKAGE:包声明
- PARAMETER:参数声明
- TYPE:类、接口(包括注解类型)或enum声明
- ANNOTATION_TYPE:注解声明(应用于另一个注解上)
- TYPE_PARAMETER:类型参数声明(1.8新加入)
- TYPE_USE:类型使用声明(1.8新加入)
PS: 当注解未制定Target值时,此注解可以使用任何元素之上,就是上面的类型。
举例如下:
@Target(ElementType.METHOD)
public @interface MethodInfo {
}
上面代码中我们使用"@Target"元注解来说明MethodInfo这个注解只能应用于对方法进行注解。
2、@Retention
表示需要在什么级别保存该注解信息,由RetentionPolicy枚举定义
- SOURCE:注解将编译器丢弃(该类型的注解信息只会保留在源码里,源码经过编译后,注解信息会被丢弃,不会保留在编译好的class文件里)
- CLASS:注解在class中可用,但会被VM丢弃(该类型的注解信息会保留在源码里和class文件里,在执行的时候,不会加载到虚拟机中(JVM)中)
- RUNTIME:VM将在运行期也保留注解信息,因此可以通过反射机制读取注解信息(源码、class文件和执行的时候都有注解的信息)
PS:当胡姐未定义Retention值时,默认值是CLASS
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
这表明@Override 注解只在源码阶段存在,javac在编译过程中去掉该注解。
3、@Documented
表示注解会被包含在javaapi文档中
当一个注解被@Documented元注解所修饰时,那么无论在哪里使用这个注解,都会被Javadoc工具文档化。
举例如下
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)public @interface Documented {
}
这个元注解呗@Documented修饰,表示它本身会被文档化。@Retention注解的值RetentionPolicy.RUNTIME表示@Documented这个注解能保留在运行时;@Target元注解的值ElementType.ANNOTATION_TYPE表示@Documented这个注解只能够来修饰注解类型
4、@Inherited
允许子类继承父类的注解。
用于描述某个被标注的类型可被继承的,如果一个使用了@Inherited修饰的annotation类型类型被用于一个class,则这个annotation将被用于该class类的子类。
表明被修饰的注解类型是自动继承的。如果你想让一个类和它的子类都包含某个注解,就可以使用@Inherited来修饰这个注解。也就是说,假设@Parent类是Child类的父类,那么我们若用被@Inherited元注解所修饰的某个注解对Parent类进行了修饰,则相当于Child类也被该注解所修饰了。这个元注解的定义如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)public @interface Inherited {
}
@Inherited
public @interface MyAnnotation {
}
@MyAnnotation
public class MySuperClass {
}
public class MySubClass extends MySuperClass {
}
上述代码的大致意思是使用@Inherited修饰注解MyAnnotation使用MyAnnotation注解MySuperClass实现类MySubclass继承自MySuperClass
当@Inherited annotation类型标注的annotation的Retention是RetentionPolicy.RUNTIME,则反射API增强了这种继承性。如果我们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时,反射代码检查将展开工作:检查class和其父类,直到发现指定的annotation类型被发现,或者到达类继承结构的顶层。
三、标准注解/内建注解
Java本身内建了一些注解,用来为编译器提供指令。如下:
- @Override
- @Deprecated
- @SuppressWarnings
下面让我们详细了解下三个标准注解/内建注解
1、@Override注解
@Override注解用来修饰对父类进行重写的方法。如果一个并非重写父类的方法使用这个注解,编译器将提示错误。
实际上在子类中重写父类或接口的方法,@Overrider并不是必须的。但是还是建议使用这个注解,在某些情况下,假设你修改了父类的方法的名字,那么之前重写子类方法将不再属于重写,如果没有@Override,你将不会觉察到这个子类的方法。有了这个注解修饰,编译器则会提示你这些信息。
例子如下:
public class MySuperClass {
public void doTheThing() {
System.out.println("Do the thing");
}
}
public class MySubClass extends MySuperClass{
@Override
public void doTheThing() {
System.out.println("Do it differently");
}
}
2、@ Deprecated
@Deprecate 标记类、方法、属性,如果上述三种元素不再使用,使用@Deprecated注解,建议用户不再使用
如果代码使用了@Deprecate 注解的类、方法或属性,编译器会进行警告。
举例如下:
@Deprecated
public class MyComponent {
}
当我们使用@Deprecate注解后,建议配合使用对应的@deprecated JavaDoc 符号,并解释说明为什么这个类,方法或属性被弃用,已经替代方案是什么?如下:
@Deprecated
/**
@deprecated This class is full of bugs. Use MyNewComponent instead.
*/
public class MyComponent {
}
3、@SuppressWarnings
@SuppressWarnings 用来抑制编译器生成警告信息。可以修饰的元素为类,方法,方法参数,属性,局部变量。
使用场景:当我们一个方法调用了弃用的方法或者进行不安全的类型转换,编译器会生成警告。我们可以为这个方法增加@SuppressWarnings
注解,来抑制编译器生成警告。
PS:使用@SuppressWarnings注解,采用就近原则,比如一个方法出现警告,我们尽量使用@SuppressWarnings注解这个方法,而不是注解方法所在的类。虽然两个都能抑制编译器生成警告,但是范围越小越好,因为范围到了,不利于我们发现该类下其他方法的警告信息。
举例如下:
@SuppressWarnings
public void methodWithWarning() {
}
四、自定义注解
1、注解格式
了解完系统注解之后,我们就可以自己定义注解了,通过上面的@Override的实例,不难看出定义注解的格式如下:
public @Interface 注解名{定义体}
PS:定义体就是方法的集合,每个方法实则是生命了一个配置参数,方法的名称作为配置参数的名称,方法的返回值类型就是配置参数的类型,和普通的方法不一样,可以通过default关键字来声明配置参数的默认值。
注意:
- 1、注解类型是通过"@interface"关键字定义的
- 2、此处只能使用public或者默认的default两个权限修饰符
- 3、配置参数的类型只能使用基本类型(byte,boolean,char,short,int,long,float,double和String,Enum,Class,annotation)
- 4、对于只含有一个配置参数的注解,参数名建议设置中value,即方法名为value.
- 5、配置参数一旦设置,其参数值必须有确定的值,要不在使用注解的时候指定,要不在定义注解的时候使用default为其设置默认值,对于非基本类型的参数值来说,其不能为null。
2、创建自己的注解
在Java中,我们可以创建自己的注解,注解和类,接口文件一样定义在自己的文件里面。如下:
@interface MyAnnotation {
String name();
int age();
String sex();
}
上述代码定义了一个叫做MyAnnotation的注解,它有4个元素。再次强调一下,@Interface 这个关键字 用来告诉java编译器这是一个注解。
应用举例
@MyAnnotation(
name="张三",
age=18,
sex="男"
)
public class MyClass {
}
注意,我们需要为所有的注解元素设置值,一个都不能少。
3、自定义注解默认值
对于注解总的元素,我们可以为其设置默认值,使用方法如下:
@interface MyAnnotation {
String name();
int age();
String sex() default "男";
}
上述代码,我们设置了sex元素的默认值为"男"。当我们在使用时,可以不设置sex的值,即让value使用空字符串默认值。举例如下;
@MyAnnotation(
name="Jakob",
age=37,
)
public class MyClass {
}
五、注解的原理
1、注解处理器
如果没有用来读取注解的方法和工作,那么注解也就不会比注释更有用户了,使用注解的过程中,很重要的一部分就是创建与使用注解处理器。Java SE 扩展了反射机制的API,以帮助程序员快速的构造自定义注解处理器。
2、注解处理器的分类
我们已经知道了如何自定义注解,当时想要注解发挥实际作用,需要我们为注解编写响应的注解处理器,根据注解的特性,注解处理器可以分为运行时注解处理器和编译时注解处理器。运行时注解处理器需要借助反射机制实现,而编译时处理器则需要借助APT来实现。
无论是运行时注解处理器还是编译时注解处理器,主要工作都是读取注解及处理特定主机,从这个角度来看注解处理器还是非常容易理解的。
3、运行时注解处理器
熟悉Java反射机制的同学一定对java.lang.reflect包非常熟悉,该包中的所有API都支持读取运行时Annotation的能力,即属性为@Retention(RetentionPolicy.RUNTIME)的注解。
在Java.lang.reflect中中的AnnotatedElement接口是所有程序元素的(Class、Method)父接口,我们可以通过反射获取到某个类的AnnotatedElement对象,进而可以通过该对象提供的方法访问Annotation信息,常用的方法如下:
方法 | 含义 |
---|---|
< 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[] getDeclaredAnntationsByType(Class <T> annotationClass) | 返回直接存在该元素上某类型的注解 |
Annotation[] getDeclaredAnnotations() | 返回该元素上的所有注解 |
举例说明
一个User实体类
public class User {
private int id;
private int age;
private String name;
@UserData(id=1,name="张三",age = 10)
public User() {
}
public User(int id, int age, String name) {
this.id = id;
this.age = age;
this.name = name;
}
//...省略setter和getter方法
@Override
public String toString() {
return "User{" +
"id=" + id +
", age=" + age +
", name='" + name + '\'' +
'}';
}
}
我们希望可以通过@UserData(id=1,name="张三",age = 10)这个注解,来为设置User实例的默认值。
自定义注解如下:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.CONSTRUCTOR)
public @interface UserData {
public int id() default 0;
public String name() default "";
public int age() default 0;
}
该注解类作用于构造方法,并在运行时存在,这样我们就可以在运行时通过反射获取注解进而为User实例设值,看看如何处理该注解
运行时注解处理器:
public class AnnotationProcessor {
public static void init(Object object) {
if (!(object instanceof User)) {
throw new IllegalArgumentException("[" + object.getClass().getSimpleName() + "] isn't type of User");
}
Constructor[] constructors = object.getClass().getDeclaredConstructors();
for (Constructor constructor : constructors) {
if (constructor.isAnnotationPresent(UserMeta.class)) {
UserMeta userFill = (UserMeta) constructor.getAnnotation(UserMeta.class);
int age = userFill.age();
int id = userFill.id();
String name = userFill.name();
((User) object).setAge(age);
((User) object).setId(id);
((User) object).setName(name);
}
}
}
}
测试代码
public class Main {
public static void main(String[] args) {
User user = new User();
AnnotationProcessor.init(user);
System.out.println(user.toString());
}
}
运行测试代码,便得到我们想要的结果:
User{id=1, age=10, name=’dong’}
这里通过反射获取User类声明的构造方法,并检测是否使用了@UserData注解。然后从注解中获取参数值并将其复赋值给User对象。
正如上面所说,运行时注解处理器的编写本质上就是通过反射获取注解信息,随后进行其他操作。编译一个运行时注解处理器就是那么简答。运行时注解通常多用于参数配置模块。
4、编译时注解处理器
不同于运行时注解处理器,编写编译时注解处理器(Annotation Processor Tool)。
APT 用于编译时期扫描和处理注解信息,一个特定的注解处理器可以以Java源文件或编译后的class文件作为输入,然后输出另一些文件,而已是.java文件,也可以是.class文件,但通常我们输出的是.java文件。(注意:并不是对源文件进行修改),这些java文件会和其他源文件一起被javac编译。
你可能会很纳闷,注解处理器是到底在什么阶段介入的呢?好吧,其实是在javac开始编译之前,这就是通常我们为什么愿意输出.java文件的原因。
注解最早是在java 5引入的,主要包含APT和com.sum.mirror包中现相关mirror api,此时APT和javac是各自独立的,但是从Java 6开始,注解处理器正式标准化,APT工具也被直接集成在javac当中。
编译时注解处理器编译一个注解时,主要分2步
- 1、 继承AbstractProcessor,实现自己的注解处理器
- 2、注册处理器,并打包成jar
举例说明:
首先来看一下一个标准的注解处理器的格式:
public class MyAnnotationProcessor extends AbstractProcessor {
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
}
来简单的了解下其中5个方法的作用
方法 | 作用 |
---|---|
init(ProcessingEnvironment processingEnv) | 该方法有注解处理器自动调用,其中ProcessingEnvironment类提供了很多有用的工具类:Filter,Types,Elements,Messager等 |
getSupportedAnnotationTypes() | 该方法返回字符串的集合表示该处理器用于处理那些注解 |
getSupportedSourceVersion() | 该方法用来指定支持的Java版本,一般来说我们都是支持到最新版本,因此直接返回SourceVersion.latestSupported()即可 |
process(Set annotations, RoundEnvironment roundEnv) | 该方法是注解处理器处理注解的主要地方,我们需要在这里写扫描和处理注解的代码,以及最终生成的java文件。其中需要深入的是RoundEnvironment类,该用于查找出程序元素上使用的注解 |
编写一个注解处理器首先要对ProcessingEnvironment和RoundEnvironment非常熟悉。接下来我们来了解下这两个类,先看下ProcessingEnvironment类:
public interface ProcessingEnvironment {
Map<String,String> getOptions();
//Messager用来报告错误,警告和其他提示信息
Messager getMessager();
//Filter用来创建新的源文件,class文件以及辅助文件
Filer getFiler();
//Elements中包含用于操作Element的工具方法
Elements getElementUtils();
//Types中包含用于操作TypeMirror的工具方法
Types getTypeUtils();
SourceVersion getSourceVersion();
Locale getLocale();
}
重点来认识一下Element,Types和Filer。Element(元素)是什么呢?
Element
element表示一个静态的,语言级别的构件。而任何一个结构化文档都可以看作是由不同的element组成的结构体,比如XML,JSON等。这里我们用XML来示例:
<root>
<child>
<subchild>.....</subchild>
</child>
</root>
这段xml中包含了三个元素:<root>、<child>、<subchild>到现在你已经明白元素是什么。对java源文件来说,他同样是一种结构化文档:
package com.demo; //PackageElement
public class Main{ //TypeElement
private int x; //VariableElement
private Main(){ //ExecuteableElement
}
private void print(String msg){ //其中的参数部分String msg TypeElement
}
}
对于java源文件来说,Element代表程序元素:包,类,方法都是一种程序元素。另外如果你对网页解析工具jsoup熟悉,你会觉得操作此处的element是非常容易,关于jsoup不在本文讲解之内。
接下来看看各种Element之间的关系图,以便有个大概的了解
element关系图.png
元素 | 含义 |
---|---|
VariableElement | 代表一个 字段, 枚举常量, 方法或者构造方法的参数, 局部变量及 异常参数等元素 |
PackageElement | 代表包元素 |
TypeElement | 代表类或接口元素 |
ExecutableElement | 代码方法,构造函数,类或接口的初始化代码块等元素,也包括注解类型元 |
TypeMirror、TypeElement、DeclaredType 这三个类我也简单的介绍下:
- TypeMirror:代表Java语言中类型.Types包括基本类型,声明类型,数组,类型变量和空类型。也代表通配类型参数,可执行文件的签名和返回类型等.
- TypeElement 代表类或接口元素
- DeclaredType 代表类型或接口类型
简单的来说,Element代表源代码,TypeElement代表的是源码中的类型元素,比如类,虽然我们可以从TypeElement中获取类名,TypeElement中不包含类本身的信息,比如它的父类,要想获取这信息需要借助TypeMirror,可以通过的Element中的asType()获取元素对应的TypeMirror。
Filer
Filter 用于注解处理器中创新文件。
然后看一下RoundEnvironment这个类,这个类比较简单
public interface RoundEnvironment {
boolean processingOver();
//上一轮注解处理器是否产生错误
boolean errorRaised();
//返回上一轮注解处理器生成的根元素
Set<? extends Element> getRootElements();
//返回包含指定注解类型的元素的集合
Set<? extends Element> getElementsAnnotatedWith(TypeElement a);
//返回包含指定注解类型的元素的集合
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a);
}
然后来看一下RoundEnvironment,这个类比较简单,一笔带过:
public interface RoundEnvironment { boolean processingOver(); //上一轮注解处理器是否产生错误 boolean errorRaised(); //返回上一轮注解处理器生成的根元素 Set<? extends Element> getRootElements(); //返回包含指定注解类型的元素的集合 Set<? extends Element> getElementsAnnotatedWith(TypeElement a); //返回包含指定注解类型的元素的集合 Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a);}
Filer
Filer用于注解处理器中创建新文件。具体用法在下面示例会做演示.另外由于Filer用起来实在比较麻烦,后面我们会使用javapoet简化我们的操作.
好了,关于AbstractProcessor中一些重要的知识点我们已经看完了.假设你现在已经编写完一个注解处理器了,下面,要做什么呢? |
打包并注册.
自定义的处理器如何才能生效那?为了让Java编译器找到自定义的注解处理器我们需要对其进行注册和打包:自定义的处理器需要被达成一个jar,并且需要在jar包的META-INF/services路径下中创建一个固定的文件
javax.annotation.processing.processor,在javax.annotation.processing.Processor文件中需要填写自定义处理器的完整路径名,有几个处理器就要填写几个
从Java 6之后,我们只需要将打开的jar防止到项目的buildpath下即可,javac在运行的过程会自动检查javax.annotation.processing.Processor注册的注解处理器,并将其注册上。而Java 5需要单独使用APT工具。
最终我们需要获得一个包含注解处理器的代码的jar包
六、注解基础知识思维导图
最后借用下别人的Java注解的基础知识点导图
Java注解.png
网友评论