java注解

内容:

  1. 注解的定义
  2. 注解的语法
  3. 源码级别的注解的使用
  4. 运行时注解的使用
  5. 编译时注解的使用
  6. Android 预置的注解

一 注解的定义

  • 注解(Annotation),也叫元数据。
  • 一种代码级别的说明。它是 JDK 1.5 以后版本引入的一个特性,与类、接口、枚举是在同一个层次。
  • 它可以声明在包、类、字段、方法、局部变量、方法参数等元素上。
  • 它提供数据用来解释程序代码,但是注解并非是所解释的代码本身的一部分。
  • 注解对于代码的运行效果没有直接影响。

注解有许多用处,主要如下:

  • 提供信息给编译器: 编译器可以利用注解来探测错误和警告信息

  • 编译阶段时的处理: 软件工具可以用来利用注解信息来生成代码、Html 文档或者做其它相应处理。

  • 运行时的处理: 某些注解可以在程序运行的时候接受代码的提取

  • 如我们所熟知的依赖注入框架 ButterKnife 就是在编译阶段来生成 findViewById 的代码(文件)的

  • 而我们所见过的 @Deprecated 就是提供信息给编辑器的RetentionPolicy.SOURCE类型注解,

  • 在自定义了一个编译或者运行阶段的注解后,需要一个开发者编写相应的代码来解释这些注解,从而来发挥注解的作用。

  • 这些用来解释注解的代码被统称为是 APT(Annotation Processing Tool)。换句话说注解其实是给 APT 或者编辑器来使用的,而对于非框架开发人员的我们我们只需要关注注解的使用,并遵守规则即可,从而我们节省了很多代码提高了效率

但是凡事如果只满足于用上,就不算是一个合 (tong) 格 (guo) 程 (mian)序 (shi) 员 (de)! 但是不要慌,当你打开这篇文章的时候你已经离 offer 又进了一步。

二、 注解的语法

2.1 注解的声明

注解的声明和声明一个接口十分类似,没错只是名字很类似~ 我们使用@interface 来声明一个注解,如我们最常见的Override 注解的声明

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

注解声明的修饰符,可以是 private,public, protected 或者默认 default 这一点跟定义一个类或者接口相同。

在声明一个注解的时候我们常常需要一些其他注解来修饰和限制该定义注解的使用和运行方式。上述的 @Target 和 @Retention 就是如此,我们称之为元注解,详细的元注解在下边说明。

2.2 注解成员

  • 注解跟一个类相似,它们并不是都是像上面的 @Override一样只有声明。
  • 一个类大概可以包含构造函数,成员变量,成员函数等,而一个注解只能包含注解成员,注解成员的声明格式为:

类型 参数名() default 默认值;

注解成员可以是:

  1. 基本类型 byte,short,int,long,float,double,boolean 八种基本类型及这些类型的数组, 注意这里没对应基本数据类型的包装类。
  2. String,Enum,Class,annotations 及这些类型的数组
  3. 注解的成员修饰符只能是 public 或默认(default)
  4. 注解元素必须有确定的值,可以在注解中定义默认值,也可以使用注解时指定。即我们在定义注解的时候声明的成员,可以不赋值,但是就跟抽象函数一样,在使用的时候就必须指定。
public @interface TestAnnotation {

   String value() default "";

   String[] values();

   int id() default -1;

   int[] ids();

   // 错误的不能使用包装类 以及自定义类型
   // Integer idInt();
   // Apple apple();

   enum Color {BULE, RED, GREEN}
   Color testEnum() default Color.BULE;
   Color[] testEnums();
   
   //注解类型成员 注解元素必须有确定的值,可以在注解中定义默认值,也可以使用注解时指定
   FruitName fruitName() default @FruitName("apple");
}
    
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
protected @interface FruitName {
   String value();
   String alias() default "no alias";
}

三、元注解

  • 我们在 Override 注解的声明中可以看到还有注解修饰着如@Target(ElementType.METHOD),我们讲元注解理解为修饰注解定义的注解。
  • 换句话说元注解为 JDK 提供给我们的一些基本注解,我们使用元注解来定义一个注解是如何工作的。

JDK 1.8 中存在的元注解有以下 5 种:

@Target, @Retention、@Documented、@Inherited、@Repeatable

下面我们依次来说明这几种类型的注解是如何使用的。

3.1 @Target 元注解

@Target 指定了被修饰的注解运用的地方,这些 "地方" 定义在 ElementType 类中,包括:

  1. ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
  2. ElementType.CONSTRUCTOR 可以给构造方法进行注解
  3. ElementType.FIELD 可以给属性进行注解
  4. ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
  5. ElementType.METHOD 可以给方法进行注解
  6. ElementType.PACKAGE 可以给一个包进行注解
  7. ElementType.PARAMETER 可以给一个方法内的参数进行注解
  8. ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举

其中 METHOD、PARAMETER、FIELD 最为常见,如 Override 注解被 @Target(ElementType.METHOD) 修饰,如果我们想要标记一个参数不能为空则可以使用 @NonNull 去修饰一个 param, FIELD 用来指定注解只能用来修饰成员变量如我们经常使用的 @BindView。

值得注意的是 @Target 元注解定义如下,

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

它内部的成员为ElementType[] 数组也就是说,我们可以同时指定一个注解可以用于很多地方。如 @ColorRes 的注解的元注解为@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})。

3.2 Retention 元注解

Retention 翻译过来是保留期的意思。当@Retention 用于修饰一个注解上的时候,它规定了了被修饰的注解应用的时期,或者存活的时期
它可以有如下 3 种取值:

  • RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃。
  • RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时通过反射获取到它们,并解释他们。
  • RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。

3.2.1 源码级别注解 RetentionPolicy.SOURCE

  • 对于第一种 RetentionPolicy.SOURCE 注解只在源码阶段保留,更多的效果时做一些编译检查
  • 在 Android 中有个为 @IntDef 的注解,他可以和常量组合一起 代替枚举 enum 做参数限制作用,来优化内存使用。

这里说只是替代了参数限制作用,而 JDK 1.5 为我们带来的 enum 的作用不只是简单的参数限制作用作用,对于 Enum 更多优雅使用可以参考 《Effective Java》。

如 @IntDef 的注解定义如下:

@Retention(SOURCE)
@Target({ANNOTATION_TYPE})
public @interface IntDef {
    long[] value() default {};
    boolean flag() default false;
}

如我们常用的设置一个 View 的可见属性就使用了 @IntDef 注解来保证使用者传入的参数是对的,如下:

@IntDef({VISIBLE, INVISIBLE, GONE})
@Retention(RetentionPolicy.SOURCE)
public @interface Visibility {}

@RemotableViewMethod
public void setVisibility(@Visibility int visibility) {
        setFlags(visibility, VISIBILITY_MASK);
}

//设置一个 View 的属性:
...
toolbar.setVisibility(View.VISIBLE);// it is Ok

//toolbar.setVisibility(1000);// 如果我们随便写一个数值 那么编辑器将会报错 

3.2.2 运行期时的注解 RetentionPolicy.RUNTIME

  • 源码级别的注解对我们的编码约束
  • 运行期注解与之不同的是,如果要是让该注解生效,我们必须要编写一定的代码去将定义好的注解,在运行中"注入"应用中,看到运行时注入就可以应该能想得起Java反射,是的注入这个操作就是需要开发人员自己编写的。
  • 另外,我们也都了解,在运行反射的时候效率是无法保证的。因为反射将遍历对应类的 Class 文件来获取相应的信息。所以运行时注解,并不是那么广泛被运用
  • 而编译期注解则不会对程序的运行造成效率的影响,因此应用更广泛一些。

我们来试着写一个 Dota 英雄名称的运行期注解来了解下他的运作方式:

/**
* 定义一个注解表示英雄的名字
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
private @interface HeroName {
   String value();

   String alias();
}

/**
* 定义一个类包含英雄名称的属性
*/
public class Hero {
    // 定义注解的时候没有 deflaut 属性名称所以在使用的时候必须赋值
   @HeroName(value = "Spirit Walker", alias = "NB")
   private String heroName;

   public void setHeroName(String heroName) {
       this.heroName = heroName;
   }

   public String getHeroName() {
       return heroName;
   }
}

ok 声明就是这么简单,那么如何让一个属性生效呢,这时候我们就需要一个注解处理方法。为了方便观察运行注解的结果,所以我们这个处理方法选择传递一个 Hero 对象,不过你为了更通用也可以不用这么做。

/** 运行时注解处理方法*/
public static void getHeroNameInfo(Hero hero) {
   try {
       Class<? extends Hero> clazz = hero.getClass();
       Field field = clazz.getDeclaredField("heroName");
       // Field isAnnotationPresent 判断一个属性是否被对应的注解修饰
       if (field.isAnnotationPresent(HeroName.class)) {
           //field.getAnnotation 获取属性的注解
           HeroName fruitNameAnno = field.getAnnotation(HeroName.class);
           hero.setHeroName("name = " +fruitNameAnno.value() +" alias = " + fruitNameAnno.alias());
       }
   } catch (NoSuchFieldException e) {
       e.printStackTrace();
   }
}

下面我们来运行下程序测试下:

public static void main(String[] args) {
   Hero hero = new Hero();
   getHeroNameInfo(hero);
   System.out.println("hero = " + hero);
}

运行结果:

hero = Hero{heroName='name = Spirit Walker alias = NB'}

3.2.3 编译时期的注解 RetentionPolicy.CLASS

  • 接下来到了编译时注解,这个注解类型,便是众多工具库中应用的注解类型,它不会影响运行时的效率问题,而是在编译期,或者打包过程中就生成了对应的代码,在运行时将会生效
  • 如我们常见的 ButterKnife 和 EventBus。
  • 编译时注解与运行时注解不同,编译时注解主要是帮助我们在编译器编译期使用注解处理器生成相应的代码,帮我们解放劳动力。

我们知道运行时注解是通过反射来解释对应注解并使注解生效的,那么编译时如何解释对应的注解呢?这里就需要用到注解处理器的知识了。

注解处理器(Annotation Processor)是javac的一个工具,它用来在编译时扫描和处理注解(Annotation)。你可以自定义注解,并注册相应的注解处理器(自定义的注解处理器需继承自AbstractProcessor)。

  • Java 中提供给我们了注解处理器实现方法,主要是通过实现一个名为 AbstractProcessor 的注解处理器基类。
  • 该抽象类要求我们必须实现 process方法来定义处理逻辑。
public class NameProcessor extends AbstractProcessor {
    
    //会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类如Elements, Types和Filer等
    @Override
    public synchronized void init(ProcessingEnvironment env){ }
    
   //返回最高所支持的java版本, 如返回 SourceVersion.latestSupported();
    @Override
    public SourceVersion getSupportedSourceVersion() { }
    
    //一个注解处理器可能会处理多个注解逻辑,这个方法将返回待处理的注解类型集合,返回值作为参数传递给 process 方法。
    @Override
    public Set<String> getSupportedAnnotationTypes() { }

    //process 函数就是我们处理待处理注解的地方了,我们需要在这里编写生成 java 文件的具体逻辑。 方法返回布尔值类型,表示注解是否已经处理完成。一般情况下我们返回 true 即可。
    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
}

注解处理器的处理步骤主要有以下:

  1. 编译器开始执行注解处理器
  2. process 方法中循环处理注解元素(Element),找到被该注解所修饰的类,方法,或者属性
  3. 拿上一步得到的备注注解修饰的类或者属性,方法,生成对应的辅助类,并写入 java 文件
  4. 生成 java 文件后就可以在运行时,在程序中获取并调用对应的辅助方法,如 ButterKnife.bind(this); 方法就是获取对应 Activity 的注解处理器生成的java 文件,并执行了构造函数

四 自定义一个编译时注解

  • 自定义编译时注解要比运行时注解要繁琐一些。下面我们来举一个简单的例子,意在说明编译时注解是如何工作的。
    在 Android 中为了实现一个编译时注解我们一般需要借助两个三方库:
  1. com.google.auto.service:auto-service:1.0-rc2 这是谷歌官方提供的一个注解处理注册插件可以帮助我们更方便的注册注解处理器,只需要在自定义的 Processor 类上方添加@AutoService(Processor.class)即可,不用自己动手执行注解处理器的注册工作(即编写 resource/META-INF/services/javax.annotation.processing.Processor文件)。
  2. 为了更方便的在 process 文件中生成 Java 类,需要依赖一个 Square 公司开源的 javapoet 库,com.squareup:javapoet:1.9.0 这个库中包装提供了一些好用的 API 帮助我们更快更准确的构建 .java 文件。当然你也可以自己手写拼接字符串然后写入文件(如果你能保证正确)。

仿照 ButterKnife 的实现,我们建立一个新的 Android project ,然后创建两个 Java Moudle,其中 processor 用来存放注解处理器,processor-lib 用来存放对应的注解,如下图所示:


image

在注解处理器存在的lib的 build.gradle 中添加依赖关系:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    compile project(':processor-lib')
    compile 'com.squareup:javapoet:1.9.0'
    compile 'com.google.auto.service:auto-service:1.0-rc3'
}

主 moudle 中也需要添加对 processor 和 processor -lib 的依赖:

dependencies {
    ....
   implementation project(':processor-lib')
   // 注意这里的注解处理器的依赖方式
   annotationProcessor project(':processor')
}

好了经过上述的准备我们终于能够编写我们的编译时注解了:

  1. 在 processor-lib 定义一个 Name 注解如下:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface Name {
    String name();
    String alias();
}
  1. 编写两个类使用我们定义的注解:
public class SBHero {
    @Name(name = "Spirit Walker", alias = "SB")
    private String heroName;
}

public class PAHero {
    @Name(name = "Phantom Assassin", alias = "PA")
    private String heroName;
}
  1. 在 processor 注解处理lib 下定义一个 NamePorcessor
// @AutoService(Processor.class) 帮助我们生成对应的注解处理器配置
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.wangshijia.www.processor.Name")
public class NamePorcessor extends AbstractProcessor {
    //文件写入工具类
    private Filer filer;
    //可以帮助我们在 gradle 控制台打印信息的类
    private Messager messager;
    // 元素操作的辅助类
    private Elements elementUtils;
    //自定义文件名的后缀
    private static final String SUFFIX = "AutoGenerate";

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
        elementUtils = processingEnv.getElementUtils();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * @return 你所需要处理的所有注解,该方法的返回值会被 process()方法所接收, 这里其实只有Name 注解,
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> set = new HashSet<>();
        set.add(Name.class.getCanonicalName());
        return set;
    }
    ... 
}
  1. 最后我们要编辑我们的 process 方法了,process 方法中一共进行了下面这几件事:
  • 遍历程序中所有被该注解修饰器处理注解修饰的元素 存放进创建的Map集合
  • 依次取出map 中的元素构建对应的类和方法
  • 构建对应的方法内容
  • 生成.java 文件 位置在 ~/app/build/generated/source/apt 目录下
 @Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

   String packageName= "";
   // 获得被该注解声明的元素
   Set<? extends Element> elememts = roundEnv.getElementsAnnotatedWith(Name.class);
   // 声明一个存放成员变量的列表
   List<VariableElement> fields;
   //key 对应包含注解修饰元素的类的全类名 vaule 代表所有被注解修饰的变量
   Map<String, List<VariableElement>> maps = new HashMap<>();
   
   // 遍历程序中所有被该注解修饰器处理注解修饰的元素
   for (Element ele : elememts) {
       //  ele.getKind() 获取注解修饰的成员的类型,判断该元素是否为成员变量
       if (ele.getKind() == ElementKind.FIELD) {
           VariableElement varELe = (VariableElement) ele;
           // 获取该元素封装类型
           TypeElement enclosingElement = (TypeElement) varELe.getEnclosingElement();
           // 拿到包含 enclosingElement 元素的类的名称 样式如 com.wangshijia.www.annotationapplication.Hero
           String key = enclosingElement.getQualifiedName().toString();
           messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + key);
           packageName = elementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
           fields = maps.get(key);
           if (fields == null) {
               maps.put(key, fields = new ArrayList<>());
           }
           fields.add(varELe);
       }
   }

   /*
    * maps 包含有所有被 @Name 修饰的类
    */
   for (String key : maps.keySet()) {
       List<VariableElement> elementFileds = maps.get(key);
       messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + key);
       messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + elementFileds);

       String className = key.substring(key.lastIndexOf(".") + 1);
       className += SUFFIX;
       // 创建 className 类
       TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className)
               .addModifiers(Modifier.PUBLIC, Modifier.FINAL);
       // 创建方法
       MethodSpec.Builder methodBuild = MethodSpec.methodBuilder("printNameAnnotation")
               .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
               .returns(void.class);
       //创建方法中的打印语句
       for (VariableElement e : elementFileds) {
           Name annotation = e.getAnnotation(Name.class);
           // 创建 printNameAnnotation 方法
           methodBuild
                   .addStatement("$T.out.println($S)", System.class, e.getSimpleName() + " = " + annotation.name())
                   .addStatement("$T.out.println($S)", System.class, e.getSimpleName() + " = " + annotation.alias());

       }

       //将方法中添加到类中
       MethodSpec printNameMethodSpec = methodBuild.build();
       TypeSpec classTypeSpec = classBuilder.addMethod(printNameMethodSpec).build();

       try {
           //构造的 java 文件 参数一 包名,参数二 上述构建的类描述 TypeSpec
           JavaFile javaFile = JavaFile.builder(packageName, classTypeSpec)
                   .addFileComment(" This codes are generated automatically. Do not modify!")
                   .build();
           javaFile.writeTo(filer);
       } catch (IOException exception) {
           exception.printStackTrace();
       }
   }

上述注释写的很详细了,这里希望不熟悉的朋友,自己动手实现下,才能更好的理解是如何构建对应的文件的。生成的文件位于指定目录下:


image
  1. 使用我们定义好的注解生成文件
  • 使用注解生成器生成的 java 文件和普通的类没什么区别
  • 通过编译后就放在上述文件夹中,我们可以正常调用我们构造类的方法,ButterKnife.bind(this) 实际上就是调用生成类的方法的过程。
  • 我们是一个简单的 demo 就不这么复杂的调用了。直接在 App 目录下的任意文件调用,如在一个 Activity 中:
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        PAHeroAutoGenerate.printNameAnnotation();
        SBHeroAutoGenerate.printNameAnnotation();
    }
}

五 Android 预置的注解

  • 日常开发中,注解能够帮助我们写出更好更优秀的代码,为了更好地支持 Android 开发,在已有的 android.annotation 基础上,Google 开发了 android.support.annotation 扩展包,共计50个注解,帮助开发者们写出更优秀的程序,这五十多种注解得以应用场景各不相同,常见的如 @IntDef @ColorInt @Nullable。

参考

夯实 Java 基础 - 注解
如何打造一个 Android 编译时注解框架

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容

  • 一、什么是注解? 注解对于开发人员来讲既熟悉又陌生,熟悉是因为只要你是做开发,都会用到注解(常见的@Overrid...
    _Justin阅读 1,351评论 0 10
  • 从JDK5开始,Java增加了Annotation(注解),Annotation是代码里的特殊标记,这些标记可以在...
    CarlosLynn阅读 554评论 0 2
  • 以前,『XML』是各大框架的青睐者,它以松耦合的方式完成了框架中几乎所有的配置,但是随着项目越来越庞大,『XML』...
    Java大生阅读 2,375评论 3 96
  • 这是一个最好的时代,这是一个人人都是自媒体的时代。 这是一个最坏的时代,这是一个自媒体意乱情迷的时代。 那么在这个...
    何鲸洛阅读 726评论 0 0
  • 华阳湖 石阶上 水上波澜风四起 凉哉 爽哉 广场里 灯月下 男女老少广场舞 欢哉 乐哉
    读书活阅读 373评论 0 0