本文收录于 kotlin入门潜修专题系列,欢迎学习交流。
创作不易,如有转载,还请备注。
注解
注解在编程语言中是一种特殊的机制、是用来描述代码的元数据。在我看来,注解首先简洁了代码,使得我们编码变得言简意赅;其次,注解解耦了系统,同时还让系统之间保持了一定的联系(比如不再像配置文件那样,和代码完全独立);最后,注解能及时有效的反馈错误(编译时报错)。
看到这里可能还是不太明白什么是注解,如果使用通俗的白话来表示一下注解是什么意思呢?
我们可以认为,注解就是一种注释,只不过这种注释是给两个角色看的:一个是我们,能够从注解中看出代码的意义来;另一个则是注解处理器(编译器),这个就是注解实际起作用的解释引擎,它会对注解信息做出解释,并执行相应的动作。比如基于注解的编译警告、依赖注入等等。注解处理引擎也是注解机制中最主要的存在。
如果还不明白,那就接着往下看。
java中的注解
java语言提供了很多内置的注解,比如我们常见的下面两种注解:
public class Test extends Thread {
@Override
public void run() {
super.run();
}
@Deprecated
public void m1() {
}
}
没错,我们复写的run方法上面的 @Override就是个注解,表示该方法是超类中的方法,我们在子类进行了复写。 @Deprecated注解则表示该方法不建议被使用了,以后可能会被废弃。
从代码可以看出来,注解的使用语法是:@+注解名。此外注解还可以接受参数,比如我们常见的SuppressWarnings注解(作用是抑制警告),示例如下:
public class Test extends Thread {
@Deprecated
public void m1() {
}
@SuppressWarnings("deprecation")
public static void main(String[] args) {
Test t = new Test();
t.m1();
}
}
正常来讲,因为m1方法使用了 @Deprecated
注解修饰,所以我们在main方法调用m1的时候,编译器会警告我们m1方法已经不建议使用,此时,如果我们在m1方法上添加 @SuppressWarnings("deprecation")这个注解,就可以禁止掉这个警告提示。
当然,这个只是SuppressWarnings注解完成的一个功能,实际上它还可以完成其他许多的功能,这里不再阐述,只需要关注注解是可以接收参数这个特性即可。
自定义java注解
前面提到的都是java自带的注解,如果有需要,我们也可以自定义注解,如果不熟悉自定义注解,则可以参考系统提供的注解写法,比如我们先来看下上个章节中的SuppressWarnings这个注解的定义,示例如下:
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
由上面代码可知,SuppressWarnings注解包含了一个参数,该参数类型是字符串数组,这就是注解入参的写法:我们只需要像定义方法一样定义需要的入参及其类型即可。此外,我们发现,在使用SuppressWarnings的时候,我们并没有指定参数的名称,而是直接传入了“deprecation”这个值,这是为什么?如果有多个参数的时候还能这么传入吗?
这是java中的一个默认语法,对于只有一个入参的注解,我们可以使用value进行命名,使用value进行命名的参数,在传参的时候可以省略其名称。比如上面的SuppressWarnings("deprecation")就是省略了value的缘故,其全称可以写为:SuppressWarnings(value="deprecation")。
注意,只能当注解只需要一个参数,且该参数被命名为value的时候,才能省略。
最后,我们发现在SuppressWarnings注解上面还有一些注解,比如Target注解、Retention注解,这些注解被称为元注解,只能用于修饰注解类型。
Target注解和Retention注解是我们在定义注解的时候常用的两个元注解。使用Target修饰的注解,用来表明我们自定义注解可以修饰的目标,比如可以用来修饰方法、成员变量、包等等,具体的修饰目标如下所示:
/** 可以用于修饰类, 接口 (包括注解类型), 或者枚举类 */
TYPE,
/** 用于修饰字段 (包括枚举常量) */
FIELD,
/** 用于修饰方法 */
METHOD,
/** 用于修饰形参 */
PARAMETER,
/** 用于修饰构造方法*/
CONSTRUCTOR,
/** 用于修饰本地变量 */
LOCAL_VARIABLE,
/** 用于修饰注解*/
ANNOTATION_TYPE,
/** 用于修饰包 */
PACKAGE,
/**
* 用于修饰类型参数,从jdk1.8开始支持
*/
TYPE_PARAMETER,
/**
* 可用于修饰各种类型
* @since 1.8
*/
TYPE_USE
而Retention注解则表示我们自定义注解的生效时机,它总共支持三个值,如下所示:
/**
* 只在源代码中生效,编译的时候会被擦除
*/
SOURCE,
/**
* 注解信息会被编译器编译到class文件中,但无法再运行时保持,这意味着我们无法再运行时采用反射来获取到这个注解,这个是所有注解的默认行为
*/
CLASS,
/**
* 注解信息会被编译到class文件中,而且在运行时保持了该注解信息,可以使用反射机制获取到该注解信息
*/
RUNTIME
结合上面的信息,我们演示下自定义注解的写法,如下所示:
public @interface MyAnnotation {
String firstParam();
int secondParam();
}
上面代码我们定义了一个接收两个入参的注解,其语法为权限修饰符 + @interface + 注解名。该注解信息同时会在编译期和运行期生效(参见上面阐述过的默认取值)。
乍看起来,注解的写法有点像接口的写法,形式上确实是这样,但需要注意以下几点:
- 同接口一样,注解只能使用包权限修饰符或者public权限修饰符进行修饰。
- 和接口不同的是,注解的关键字是@interface,并不是interface
那么,如何使用我们自定义的注解呢?这个很简单,其使用姿势与使用系统提供的注解姿势一致,如下所示:
@Retention(RetentionPolicy.RUNTIME)
public @MyAnnotation(firstParam = "param1", secondParam = 1)
public class Test {}
使用注解的姿势就是@+注解名,然后附带上注解需要的参数即可。
kotlin中的注解
了解完java中的注解后,再来看下kotlin中的注解。
首先,在kotlin中,同样为我们提供了几个常见的内置注解,这些注解位于kotlin.annotation包下,它们只能用于修饰注解类型,也就是元注解。介绍如下:
//注解的修饰目标,与java类似,但是有很多不同
@Target
//同java基本一致,定义有稍微不同
@Retention
//新注解,表示一个注解可以在同一个地方写多遍,
//在1.8版本之前的jvm,只支持Retention为Source的写法
@Repeatable
//用该注解修饰的注解,将会被视为api方法的一部分,
//在生成文档的时候将会保留注解相关信息
@MustBeDocumented
下面简单介绍下上面的几个元注解。
首先,来看下Target修饰的注解可以用于修饰哪些目标,如下所示:
/**用于修饰类,、接口、对象类(object)、注解类*/
CLASS,
/** 只能用于修饰注解类,实际上Target注解本身就是使用该类型修饰的,所以Target只能用于注解 */
ANNOTATION_CLASS,
/** 用于修饰泛型类型参数 (直接写本篇文章时,暂未支持) */
TYPE_PARAMETER,
/** 用于修饰属性*/
PROPERTY,
/** 用于修饰字段, 包括属性的后备字段 */
FIELD,
/** 用于修饰本地变量 */
LOCAL_VARIABLE,
/** 方法或者构造方法中的值参数 */
VALUE_PARAMETER,
/** 只能用于修饰构造方法(第一或者第二)*/
CONSTRUCTOR,
/** 用于修饰方法,不包括上面的构造方法 */
FUNCTION,
/**只能用于修饰getter属性 */
PROPERTY_GETTER,
/**只能用于修饰setter属性 */
PROPERTY_SETTER,
/**同java,用于修饰类型 */
TYPE,
/**用于修饰任意的表达式*/
EXPRESSION,
/** 用于修饰文件*/
FILE,
/** 用于修饰别名类型,从kotlin1.1开始支持 */
TYPEALIAS
然后来看下Retention修饰的注解可以修饰哪些目标,如下所示:
/** 源代码层次 */
SOURCE,
/** 存在于编译期,运行时不可见 */
BINARY,
/** 即存在于编译期,又能在运行时访问到,默认即是该值*/
RUNTIME
而对于Repeatable和MustBeDocumented注解都没有参数,因此只有单一的意义,此处略过。
那么kotlin中的注解写法和java中的注解有什么不同吗?这里通过代码展示下kotlin中注解的使用方式,注意代码中的注释:
//这里定义了一个注解,可被文档化,能修饰类、属性、
//构造方法、本地变量,除此之外不能修饰其他目标
//同时该注解的信息既在编译器保留,同时在运行时也能获取到
@MustBeDocumented
@Target(AnnotationTarget.CLASS, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.PROPERTY, AnnotationTarget.LOCAL_VARIABLE)
@Retention(AnnotationRetention.RUNTIME)
annotation class TestAnnotation(val value: String)
@TestAnnotation("class")//正确,可以修饰类
class Test {
@TestAnnotation("property"))//正确,可以修饰属性
private val t1 = "test"
@TestAnnotation("constructor"))//正确,可以修饰构造方法
constructor()
@TestAnnotation("method"))//!!!错误,不能修饰方法
fun m1() {
@TestAnnotation("local_variable"))//正确,可以修饰本地变量
val i = 1;
}
}
可见kotlin中的注解与java中的注解大同小异,但需要注意的是kotlin自定义注解的语法,不再是像java一样使用@interface来进行定义,而是使用annotation class这种语法。
kotlin精确注解
“精确注解”产生的原因,主要是kotlin注解与java层面上注解的不同而引起的,先来看个例子:
class Test( val param1: Int)
上面是一个极其简单的kotlin类声明,主构造方法接收一个Int类型的参数param1,那么这段代码生成的字节码是什么呢?来看下:
public final class Test {
// access flags 0x12
private final I param1
// access flags 0x11
public final getParam1()I
//...省略一些字节码
// access flags 0x1
public <init>(I)V
//...省略一些字节码
由字节码可知,对于上面的代码,kotlin为我们生成了构造方法,并为param1生成了一个属性成员以及一个公有的get方法,那么问题来了,如果我们在param1上加上注解,该注解到底会在哪儿生效呢?比如,下面一段代码:
//我们定义了一个注解TestAnnotation
annotation class TestAnnotation
//使用注解TestAnnotation修饰param1,此时注解作用的目标是什么?
class Test(@TestAnnotation val param1: Int)
上面代码中,注解TestAnnotation到底作用于param1对应的属性成员还是其对应的get方法呢?很简单,来看下字节码,如下所示:
//...省略部分字节码
public <init>(I)V
@LTestAnnotation;()
//...省略部分字节码
通过查看字节码,我们发现,TestAnnotation被编译到了构造方法中,即默认修饰的是构造方法中的参数,而不是属性和get方法!那么如何明确指定TestAnnotation注解在java层面上的修饰目标呢?这就引入了“精确注解”的概念,所谓“精确注解”是指,kotlin允许我们精确指定注解在java层面上的修饰目标。
比如,同样是上面的代码,我们想让注解实际作用域java层面上的get方法,我们就可以这么写:
class Test(@get:TestAnnotation val param1: Int)
其对应的字节码如下所示:
//...省略部分字节码
public final getParam1()I
@LTestAnnotation;()
//...省略部分字节码
通过上面字节码我们发现,实际上TestAnnotation注解已经最用于param1对应的get方法上了!这就是“精确注解”。
kotlin为我们提供了多个“精确注解”,当我们使用这些注解的时候,其相应的作用罗列如下:
—file:作用于文件
—property :用该值修饰的注解对java来说是不可见的
—field: 字段
—get : 用于修饰属性的getter
—set :用于修饰属性的setter
—receiver :用于修饰扩展属性以及方法的receiver
—param :用于修饰构造参数
—setparam :用于修饰属性的setter参数
—delegate :用于修饰委托属性
如果没有指定“精确注解”,那么kotlin编译器会根据注解上的target取值来决定修饰哪个目标,如果有多个目标,kotlin规定了他们的优先级:第一优先级是param;然后是property;最后是field。
注解背后的机制
本小节来结合Retention看下kotlin注解背后的原理,首先看下要分析的代码,如下所示:
@Target(AnnotationTarget.CLASS)
annotation class TestAnnotation(val value: String)
//修饰类Test
@TestAnnotation("class")
class Test {
}
其生成的字节码如下所示:
public abstract @interface TestAnnotation implements java/lang/annotation/Annotation {
@Lkotlin/annotation/Target;(allowedTargets={Lkotlin/annotation/AnnotationTarget;.CLASS})
@Ljava/lang/annotation/Retention;(value=Ljava/lang/annotation/RetentionPolicy;.RUNTIME)
@Ljava/lang/annotation/Target;(value={Ljava/lang/annotation/ElementType;.TYPE})
// access flags 0x401
public abstract value()Ljava/lang/String;
}
由上面字节码可以总结如下:
- kotlin注解实际上会被编译成实现java.lang.annotation.Annotation接口的一个抽象类。这个与java相一致,因为在java中只有实现了Annotation接口的才被当做注解对待(而且还不能是自己实现,必须是系统实现)!
- 注解的入参,实际上会被编译成抽象的公有方法。这些公有的抽象方法,就是用于获取注解参数的对外方法!比如我们想获取上述TestAnnotation注解的vlaue值,如下所示:
Test::class.annotations.forEach {
val anotation:TestAnnotation = it as TestAnnotation
println(anotation.value)//打印 'class'
}
其对应的字节码摘录如下所示:
L8
LINENUMBER 24 L8
ALOAD 5
INVOKEINTERFACE TestAnnotation.value ()Ljava/lang/String;//调用了value方法!!!
ASTORE 6
L9
- 如果我们不加任何元注解信息,则编译器会为我们加上java层面上的默认元注解(如默认的@Retention(RetentionPolicy.RUNTIME)
),但当我们显示使用kotlin语言指定Retention策略的时候,编译器则会同时提供kotlin层面的注解,如下所示:
//自定义注解信息
@Retention(AnnotationRetention.RUNTIME)
annotation class TestAnnotation(val value: String)
//生成的字节码
public abstract @interface TestAnnotation implements java/lang/annotation/Annotation {
//kotlin层面的注解信息
@Lkotlin/annotation/Retention;(value=Lkotlin/annotation/AnnotationRetention;.RUNTIME)
//java层面的注解信息
@Ljava/lang/annotation/Retention;(value=Ljava/lang/annotation/RetentionPolicy;.RUNTIME)
// access flags 0x401
public abstract value()Ljava/lang/String;
}
- 关于Retention注解,可以通过字节码来查看其取不同取值时的区别,比如我们定义如下代码:
@Retention(AnnotationRetention.CLASS)
annotation class TestAnnotation(val value: String)
//使用注解
@TestAnnotation("class")
class Test { }
//对应的字节码将会包含该注解信息,如下所示:
public final class Test {
@LTestAnnotation;(value="class")
}
下面我们改变Retention的策略,如下所示:
//将Retention的策略改成SOURCE
@Retention(AnnotationRetention.SOURCE)
annotation class TestAnnotation(val value: String)
那么,此时我们将不能在字节码中看到注解信息!关于RUNTIME机制这里无法从字节码中进一步验证,只能通过代码来演示在运行时获取注解信息,这个会在下节案例实战中进行阐述。
注解案例实战
前面巴拉巴拉一大堆注解的语法,看来看去怎么感觉注解没有啥用?如果单纯像上面那样自己定义注解,然后拿着自定义的注解去修饰下目标,确实没有什么用!那么注解存在的意义是什么?除了几个系统自带的注解外,我们自定义的注解又该如何工作?
这些问题正是我们本小节要阐述的问题,现在我们来写一个demo,演示下实战中注解的应用。
这个demo的目的就是完成依赖注入功能!没错,就是很多框架提供的功能,这里我们提供一个非常“简版”的kotlin实现。
首先,先来了解下什么是依赖注入。在写代码的时候,如果在类A中使用到类B的对象的时候,我们一般会直接在类A中进行new B()操作,这样就满足了我们的需求。这么做有什么弊端呢?
这种写法的弊端就是,A类需要对B对象的创建以及其生命周期负责,这样显然会带来系统的高耦合性。举个简单的例子,试想一下,如果此时B的构造方法变了,不再是无参的构造方法,那么是不是也需要去变更A类中的代码?如果有多个类依赖了B,是不是要逐个修改这些类中的代码?答案是显然的,这种牵一发而动全身的系统绝对不是我们想要的,因此有一种解耦的设计模式(思想)就出现了,它就是控制反转。
控制反转要解决的问题就是,不再将B的创建放到A类中,而是交给A类的使用者,比如我们可以为A类提供一个构造方法,该构造方法接收B类型的对象,这样我们就可以在A中使用由外界传入的B对象。再进一步考虑,为了减免外部构造的复杂性(即使用的时候,我们无需关注B对象的构建,也无需关注A类是不是需要B对象),我们可以提供一个“中间者”来完成B对象的构造、赋值功能,这样就大大解耦了系统,这个“中间者”就是我们demo要实现的功能,也是各大框架中所谓的“容器”。
控制反转已经理解了,那么上面提到的依赖注入是什么?其实上面的描述的具体操作就是依赖注入,控制反转是一种设计模式、一种思想,而依赖注入是这种模式的一种具体实现。依赖注入使得获取依赖对象的方式反转了。
白话一大堆,说的自己都觉得玄乎其玄,其实最好的例子就是看代码!想起一句话,代码面前了无秘密!
demo的场景是这样的,我们有一个画笔类Paint,以及一个颜色类Color,我们将用这两个类完成绘制功能。很显然,按照常规方案,我们只需要在画笔内部生成一个Color对象,然后完成绘制即可,示意如下:
//color类,用于获取画笔的颜色
class Color {
fun getColor(): String {
return "red"
}
}
//画笔
class Paint {
//这里我们直接在画笔类中生成了Color对象
private val color = Color();
//完成绘制
fun draw() {
println("draw with color: " +color.getColor())
}
}
//测试代码
fun main(args: Array<String>) {
val paint = Paint()
paint.draw()//打印 draw with color: red
}
上面就是我们常规的实现方案,但是根据上文分析可知,这种实现方式显然具有很强的耦合性,因此我们需要采用“控制反转”的思想来完成对Paint类中的color字段的赋值。
首先,我们定义一个注解,该注解位于test包下,如下所示:
package test
annotation class Inject //注解Inject,位于test包中
然后,我们抽象出来一个“中间者”,这个“中间者”的作用就是解析Inject注解、完成控制反转,如下所示:
//中间者Ioc类
class Ioc {
//定义了一个伴随对象,方便外部调用
companion object {
//inject方法接收两个参数,一个是宿主对象,一个是宿主对象的类类型
//这个宿主对象是相对于注入对象来讲的,比如在本例中,Paint类型的对象
//就是宿主对象,而Color类型对象就是被注入的对象
fun inject(obj: Any, clazz: KClass<out Any>) {
for (memberProperty in clazz.memberProperties) {//遍历成员变量
memberProperty.annotations.forEach {//读取成员变量上面的注解
if (it.annotationClass.qualifiedName.equals("test.Inject")) {//如果该成员变量使用了“Inject”注解修饰
memberProperty.isAccessible = true//修改访问权限为"public"
memberProperty.javaField?.set(obj, memberProperty.javaField?.type?.newInstance())//为该字段生成实例,并重新赋值
}
}
}
}
}
}
上面就是我们的中间者实现代码,代码中的注释已经比较详细,这里不再展开。下面看下如何使用:
//Color类,保持不变
class Color {
fun getColor(): String {
return "red"
}
}
//画笔类
class Paint {
//注意这里!我们不再直接生成Color对象,而是使用Inject注解
//进行了修饰,意思是交给我们的"中间者"来将其实例化
//由于无法立即赋值,所以需要使用lateinit关键字修饰,表明该字段会
//在合适的时候进行实例化
@Inject
private lateinit var color: Color
fun draw() {
println("draw with color: " +color.getColor())
}
}
//测试代码
fun main(args: Array<String>) {
val paint = Paint()
Ioc.inject(paint, paint::class)//这里我们将实例化的控制权交给了Ioc
paint.draw()//打印'draw with color: red'
}
至此,本篇文章已经阐述完毕。