Java注解的小半生

745 阅读15分钟

Java注解的小半生

本篇文章主要讲解的内容包括:

  1. 什么是注解
  2. 注解的构成
  3. 注解的应用 其实对于刚开始学习注解的人来说,应当首要理解注解存在的意义和定位,这样在我们开发过程中,也能充分地发挥其中的作用!

至于为什么叫注解的小半生,大家看到后面就知道了!

一、什么是注解

按照惯例先看一看百度的解释:

从JDK5开始,Java增加对元数据的支持,也就是注解,注解与注释是有一定区别的,可以把注解理解为代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。

(一)基础理解

其实这个解释是非常好的,也是我个人非常喜欢的。一开始学习注解的时候我也是百度文章,大家都统一的告诉我,注解和注释相近,然后开始比较,然后我就陷入了注解即补充说明的功能,请大家先摆脱这种想法。

  1. 注解是一种标记

首先注解可以理解是一种标记,注意!注解是标记,它没有执行作用!注解是标记,它没有执行作用!注解是标记,它没有执行作用! 重要的事情说三遍哈~ 其实我就想说的是别再傻傻的点开注解代码然后一个字一个字看他的函数源码怎么执行的,看不到的!打开注解你只会发现一些属性内容,并没有具体函数执行相关的代码,因为它的定位就是标记,标记本身是不会有任何功能的!

@Override注解

  1. 注解需要被发现然后对该标记处理

回想什么是标记,标记就是一种定位,像我们在代码中打断点、在停车位置放雪糕筒、在学校里穿校服等等其实就是打标记。有人就会说这标记有作用哪里没有作用呢?它告诉了我们很多信息呀!其实标记要起作用需要的是关注标记的人发现标记然后响应相关事件。代码中打断点起作用是因为我们要debug并开启debug,对于其他开发者没有人会去care断点在哪,因为用不上;放雪糕筒是开车的人发现了然后相应的避开它,如果你是路人放个雪糕筒对于你而言其实没有任何存在感;对于校服是为了校内相关人员知道你是学生对你管控,如果你是外人,穿校服其实和根本无关。

所以说,注解要想起作用,那么我们需要写一个程序去发现它并作出对应的响应,如果我们不去写程序对其识别和响应,那么其实注解的存在就是形同虚设! 下面就是关于@Component的源代码及其处理关键代码,在定义中只有value这一属性,而在某个类中通过registerDefaultFilters()中获取注解所在的类,然后将其加入bean容器中

@Component注解

@Component搜寻

(二)注解的定义

  • @interface 首先定义注解的写法就是需要@interface标记这是注解类型,这里并不是写interface也不是class。(但其实网上很多人都通过解析字节码发现本质注解还是接口的定义,当然这个仅供了解。
  • 元注解 元注解的意思就是描述注解的注解,元注解只能加注解的上方!这个读起来挺拗口,听起来好像也不怎么了解,其实就是指当前你自定义注解的一些注解自身固有属性,没关系知道你还听不懂,下面例子过一遍就能知道大概啥意思了。
@Target()
作用:标记当前注解只能放置在哪些位置上,比如我们常见的,注解放在类(ElementType.TYPE)上面,放在方法(ElementType.METHOD)上,放在参数(ElementType.PARAMETER)前等等。

@Documented
作用:在生成javadoc文档时,意味着会在文档中出现注解内容,对我们来说作用不大。

@Inherited
作用:被当前注解标识的类它被继承后,注解也能跟着给继承。

@RetentionPolicy
作用:标记注解保留的最长寿命,只有三种类型,仅存活在.java文件(RetentionPolicy.SOURCE)、存活至.class文件(RetentionPolicy.CLASS文件)、一直存活在运行过程之中(RetentionPolicy.RUNTIME)。

在自定义注解的时候我们就可以按需添加元注解,按需描述清楚我们的注解是如何的,那么系统也会按我们所给元注解(其实也是标记)来进行相应的处理。

  • 注解属性 细心的同学会发现上面的@Override、@Component源代码里面其实都没有定义属性变量,都是在定义方法呀,其实大家继续看其他的注解发现都是如此。对的对于注解而言,所谓定义属性其实就是像写方法一样去定义,我们前面说过了注解本身是不具有方法执行的功能,所以我们其实也没法在注解里写任何关于方法实现的内容。 而注解属性其实就像是荧光笔,颜色就是它的属性,正是因为有颜色的差异,能让“标记”发挥更大的作用,所以有时候我们可以在这个注解“标记”中添加一些属性进行填写,让“标记”传达的信息更加全面!

我们都知道一般我们在类中写属性是这样的格式

public String name;

在注解是这么写

String name();

  • public等可见性关键字在注解中写是无效的,写了也提示冗余的
  • 属性类型即注解中“返回类型关键字”
  • 属性变量名即注解中“方法名”

如果你就这么定义注解,那么我们每次写注解的时候都必须填上注解的name属性,如果我们想要非必须填写,那么我们就要加上默认值

String name() default "this is default name";

二、注解实例分析

我们来几个实际使用的例子给大家看看,起码我们看注解源码的时候也知道注解的大概使用

  • @RequestMapping
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
   String name() default "";
   @AliasFor("path")
   String[] value() default {};
   @AliasFor("value")
   String[] path() default {};
   RequestMethod[] method() default {};
   String[] params() default {};
   String[] headers() default {};
   String[] consumes() default {};
   String[] produces() default {};

这个注解相信大家也是非常熟悉了,当然如果你是小小白没用过也没关系,我们只是想让大家知道怎么正确用注解,以及我们可以去做些什么配置,我可以添加什么信息进去。

@Target({ElementType.TYPE, ElementType.METHOD})意味着@RequestMapping注解只能放置在类上、方法上

@Retention(RetentionPolicy.RUNTIME)意味着@RequestMapping注解能一直存活在运行过程中

@Documented文档中会保留这些这些注解供查看

@Mapping我们上面没讲,他不是元注解,即他不是原生用来描述注解的,其实这个是框架自定义注解,作用在注解上,所以如果大家遇到没见过的注解不要慌,很大可能就是第三方自己定义的,其实查看源代码你会发现你又看懂了,这里就不过多解读啦,我相信大家有能力解读

@Target({ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Mapping {
}

String name() default "";可以添加name属性,默认为""

@AliasFor("path")框架自定义属性,而且它还传了path这个属性值进去(这个注解生效的作用就是可以重命名)

String[] value() default {};可以添加value属性,默认为""

@AliasFor("value")框架自定义属性

String[] path() default {};可以添加path属性,默认为[]

RequestMethod[] method() default {};可以添加method属性,默认为[],注意这里的RequestMethod是注解类型的

String[] params() default {};可以添加params属性,默认为[]

String[] headers() default {};可以添加headers属性,默认为[]

String[] consumes() default {};可以添加consumes属性,默认为[]

String[] produces() default {};可以添加produces属性,默认为[]

使用方式:

我们可以直接加一个注解然后不带任何参数,因为这里我们看了一圈全都有默认值

@RequestMapping
public class controller(){
}

我们可以携带一些属性值传入进去,覆盖默认值

@RequestMapping(value = "/index")//意思覆盖value属性默认值"",传入的是"/index"
public class controller(){
}

同时如果我们想要覆盖value值,其实可以不写名字,这个只对value生效

@RequestMapping("/index")//意思覆盖value属性默认值"",传入的是"/index"
public class controller(){
}

但是当我们想要传入多个值的时候,就必须显示的展示所有值

@RequestMapping(value = "/index", consumes = {"text/plain", "application/*"})//意思覆盖value属性默认值"",传入的是"/index",同时覆盖consumes值,传入了两个值,通过{}传入数组
public class controller(){
}

三、注解的利用

对的这里我特定强调了利用这个词,是想告诉大家注解这个“标记”是需要另外的“发现”然后处理,所以相比于使用可能利用更容易体会到注解的定位。

在进行自定义之前我们要先想想有什么需求是需要我们去打标记然后识别自动化做一些处理的工作。 就目前来看注解其实主要分为两种使用方式,这是由他的“寿命”所决定的。

  • 注解如果能保留到RUNTIME运行时,那么通常我们可能是用反射去针对注解做一些工作。
  • 注解保留到CLASS阶段,其实这个和SOURCE阶段差不多,比较少专门留到这才能做的,上网查也没有搜寻到,我个人想法就是能用到的只能是ASM技术
  • 注解保留到SOURCE阶段,那么我们通常是用注解处理器去做一些工作。

其实要想在这里讲完所以注解的处理,是比较难的,这是一个非常庞大的体系,而我学习整个注解体系是因为实习工作中刚好遇到,所以稀里哗啦的全都学了一下,发现非常庞大,为此我在这先介绍其中的一些技术,也让大家好入门而不要过于迷茫。

(一)前置知识

首先理解我们上面说的反射、ASM技术、注解处理器,我们要知道我们先的java源代码文件经历了什么样的一个过程

java编译大致流程

  • .java => .class 这个过程是编译过程,其实推荐大家可以大概学一学字节码文件的结构,学完再看这些过程就好像没那么的难懂了。 首先是词法分析,即提取所有词,语法分析,将.java文件构建成一个AST抽象语法树,顾名思义就是像一颗树,具有层级结构,然后就是注解处理,对于一些注解的我们可以自定义预先处理,这个过程中可能会改变抽象树、新增文件,所以就有可能新第一轮从头开始,接着就是语法分析,就是从整体上下文来分析语法正不正确,最后就是存入磁盘生成字节码文件。
  • .class载入内存 这个过程由类加载器帮忙载入,存入内存中,内存的分配是我们需要了解的,方法去是针对于一些常量、静态量、类信息,然后堆存储构建的对象,程序计数器用来指向下一条指令地址,虚拟机栈就是针对于中途产生的各种变量存储,本地方法栈是针对native方法使用的。其中重点我们要知道我们创建对象其实就是利用了方法区已经存好的类信息进行创建。

(二)反射

反射是很多新手很难理解的一项内容,其实这些内容的理解最好的方法是就是跟着敲代码感受整个过程,当然也有些人感受了一遍发现还是反射好像特别愚蠢大费周章的去做这些事,而且尤其特别抽象复杂。其实反射的本质就是从已有的类信息出发处理各式各样的逻辑,我们在过去的创建对象过程只是简单的new,这是一个“类整体”生产出“对象整体”的过程。

但是反射就是让我们可以拥有类信息的每一个元素,拥有它的所有属性、方法等等信息,并且单独判断处理,正是因为类内在信息的差异化,我们可以对某些特征的类进行特定的处理,就像是针对某些属性批量处理了,分析的粒度变小了。

反射大量存在框架中,反射的灵活性创造了无限的可能!也正是如此,对于注解的处理其实也能理解,我们可以通过反射获取类中的注解信息,如果有我们想要的注解,我们可以获取并提取关键信息,然后对之进行必要特别的处理,比如方法上有我们定义的注解,我们就在这个方法执行之前打印日志,记录日志等等。

(三)ASM

ASM其实和本篇注解关系不大,但是既然提及了也拓展讲讲。ASM针对的是.class文件载入内存的过程,它的作用其实就是直接修改字节码文件,ASM使用难度比较大,因为你要直接操作字节码文件,你必须熟悉字节码结构。直接处理机字节码相当于凭空产生了代码。那么说回到注解也是如此,我们也可以识别到注解后进行想要的处理。

(四)APT & AST

APT全称是Annotation Procerss Tool,关看这个名字就知道和注解关系很大,中文名叫注解处理器,其实就是我们上图中.java文件转.class文件中的一个注解处理步骤,这个步骤的增加也是jdk1.5增加的,他的出现就是让我们可以在编译阶段就可以直接对注解处理,不仅仅是运行时的反射方式。

注解处理器的大概过程就是识别注解所在的元素,然后我们可以根据元素的判断做些逻辑,但其实APT在大众的玩法其实就是更多用来生产java文件,它有提供Filer类供我们在处理注解的过程中生成我们想要新的java文件,比如比较出名的Mybatis-Plus组件可以一键生成mapper、service层代码,太香了。

mybatis-plus自动生成代码

但其实很多时候我们并不是想要去生成多一个java文件,而是单纯的想修改逻辑(在原先我查询资料的时候其实我一直没有查到可以修改逻辑的,从头到尾只有识别和生成文件,而没有提及能修改文件,某一天突然间看到AST+APT),这时候就要提及AST抽象语法树,它是在语法分析过程中构建的,后面才进行注解处理,但是呢在注解处理的过程我们是可以获取抽象树的,所以就有一种方式就是在注解处理的过程中我们不进行java文件的生成,我们是在过程中获取抽象树,对抽象树修改,这样就达到了修改的作用!这个也是目前很多组件的玩法,最经典的是Lombok!引入Lombok就可以帮我们生成getter、setter等等就是这个原理。(题外话:Lombok在使用前都要安装插件,其实是为了编译器能够在我们写代码的过程中不报错,甚至还带有提醒,不然其实就算我们写了处理,只要我们不编译就不会添加代码,那我们在打代码的过程中如果使用到了后面添加的代码,那必然报错,所以Lombok安装插件就是解决了这个问题,这也算是解决了我一个大的疑惑————为啥引入组件还要安装插件!)

Lombok使用

四、总结

其实目前的注解能在开发中帮我们非常大的忙,就像springboot的@SpringbootApplication注解,简简单单的一个单词,其实帮了我们大忙,设定了包扫描的路径,然后让大量的注解得以找到和被扫描到,然后用反射注册bean,其次就是开启了自动配置,简化了配置。

本文其实只介绍了注解的小半生,因为实际应用复杂而庞大,所以就不再这里展开讲述,当然如果大家还感兴趣,响应热烈,我也考虑在个人号(掘金搜索—— 一知半解 )中继续拓展。