安庆大理运城常德铜陵江西
投稿投诉
江西南阳
嘉兴昆明
铜陵滨州
广东西昌
常德梅州
兰州阳江
运城金华
广西萍乡
大理重庆
诸暨泉州
安庆南充
武汉辽宁

Android基础系列篇(一)注解的那些事儿

7月3日 小米粒投稿
  前言
  本系列文章主要是汇总了一下大佬们的技术文章,属于Android基础部分,作为一名合格的安卓开发工程师,咱们肯定要熟练掌握java和android,本期就来说说这些
  〔非商业用途,如有侵权,请告知我,我会删除〕
  DD一下:开发文档跟之前仍旧一样,需要的跟作者直接要。注解
  Annotation中文译过来就是注解、标释的意思,在Java中注解是一个很重要的知识点,但经常还是有点让新手不容易理解。
  我个人认为,比较糟糕的技术文档主要特征之一就是:用专业名词来介绍专业名词。比如:
  Java注解用于为Java代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。Java注解是从Java5开始添加到Java的。这是大多数网站上对于Java注解,解释确实正确,但是说实在话,我第一次学习的时候,头脑一片空白。这什么跟什么啊?听了像没有听一样。因为概念太过于抽象,所以初学者实在是比较吃力才能够理解,然后随着自己开发过程中不断地强化练习,才会慢慢对它形成正确的认识。
  我在写这篇文章的时候,我就在思考。如何让自己或者让读者能够比较直观地认识注解这个概念?是要去官方文档上翻译说明吗?我马上否定了这个答案。
  后来,我想到了一样东西墨水,墨水可以挥发、可以有不同的颜色,用来解释注解正好。
  不过,我继续发散思维后,想到了一样东西能够更好地代替墨水,那就是印章。印章可以沾上不同的墨水或者印泥,可以定制印章的文字或者图案,如果愿意它也可以被戳到你任何想戳的物体表面。
  但是,我再继续发散思维后,又想到一样东西能够更好地代替印章,那就是标签。标签是一张便利纸,标签上的内容可以自由定义。常见的如货架上的商品价格标签、图书馆中的书本编码标签、实验室中化学材料的名称类别标签等等。
  并且,往抽象地说,标签并不一定是一张纸,它可以是对人和事物的属性评价。也就是说,标签具备对于抽象事物的解释。
  所以,基于如此,我完成了自我的知识认知升级,我决定用标签来解释注解。1。注解的定义
  注解通过interface关键字进行定义。publicinterfaceTestAnnotation{}
  它的形式跟接口很类似,不过前面多了一个符号。上面的代码就创建了一个名字为TestAnnotaion的注解。
  你可以简单理解为创建了一张名字为TestAnnotation的标签。1。1注解的属性
  注解的属性也叫做成员变量。注解只有成员变量,没有方法。注解的成员变量在注解的定义中以无形参的方法形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。Target(ElementType。TYPE)Retention(RetentionPolicy。RUNTIME)publicinterfaceTestAnnotation{intid();Stringmsg();}
  上面代码定义了TestAnnotation这个注解中拥有id和msg两个属性。在使用的时候,我们应该给它们进行赋值。
  赋值的方式是在注解的括号内以value形式,多个属性之前用,隔开。TestAnnotation(id3,msghelloannotation)publicclassTest{}
  需要注意的是,在注解中定义属性时它的类型必须是8种基本数据类型外加类、接口、注解及它们的数组。
  注解中属性可以有默认值,默认值需要用default关键值指定。比如:Target(ElementType。TYPE)Retention(RetentionPolicy。RUNTIME)publicinterfaceTestAnnotation{publicintid()default1;publicStringmsg()defaultHi;}
  TestAnnotation中id属性默认值为1,msg属性默认值为Hi。它可以这样应用。TestAnnotation()publicclassTest{}
  因为有默认值,所以无需要再在TestAnnotation后面的括号里面进行赋值了,这一步可以省略。
  另外,还有一种情况。如果一个注解内仅仅只有一个名字为value的属性时,应用这个注解时可以直接接属性值填写到括号内。publicinterfaceCheck{Stringvalue();}
  上面代码中,Check这个注解只有value这个属性。所以可以这样应用。Check(hi)
  这和下面的效果是一样的Check(valuehi)
  最后,还需要注意的一种情况是一个注解没有任何属性。比如publicinterfacePerform{}
  那么在应用这个注解的时候,括号都可以省略。PerformpublicvoidtestMethod(){}2。自定义注解2。1注解如同标签
  之前某新闻客户端的评论有盖楼的习惯,于是乔布斯重新定义了手机、罗永浩重新定义了傻X就经常极为工整地出现在了评论楼层中,并且广大网友在相当长的一段时间内对于这种行为乐此不疲。这其实就是等同于贴标签的行为。在某些网友眼中,罗永浩就成了傻X的代名词。
  广大网友给罗永浩贴了一个名为傻x的标签,他们并不真正了解罗永浩,不知道他当教师、砸冰箱、办博客的壮举,但是因为傻x这样的标签存在,这有助于他们直接快速地对罗永浩这个人做出评价,然后基于此,罗永浩就可以成为茶余饭后的谈资,这就是标签的力量。
  而在网络的另一边,老罗靠他的人格魅力自然收获一大批忠实的拥泵,他们对于老罗贴的又是另一种标签。
  老罗还是老罗,但是由于人们对于它贴上的标签不同,所以造成对于他的看法大相径庭,不喜欢他的人整天在网络上评论抨击嘲讽,而崇拜欣赏他的人则会愿意挣钱购买锤子手机的发布会门票。
  我无意于评价这两种行为,我再引个例子。
  《奇葩说》是近年网络上非常火热的辩论节目,其中辩手陈铭被另外一个辩手马薇薇攻击说是站在宇宙中心呼唤爱,然后贴上了一个大大的标签鸡汤男,自此以后,观众再看到陈铭的时候,首先映入脑海中便是鸡汤男三个大字,其实本身而言陈铭非常优秀,为人师表、作风正派、谈吐举止得体,但是在网络中,因为娱乐至上的环境所致,人们更愿意以娱乐的心态来认知一切,于是鸡汤男就如陈铭自己所说成了一个撕不了的标签。
  我们可以抽象概括一下,标签是对事物行为的某些角度的评价与解释。
  到这里,终于可以引出本文的主角注解了。
  初学者可以这样理解注解:想像代码具有生命,注解就是对于代码中某些鲜活个体的贴上去的一张标签。简化来讲,注解如同一张标签。
  在未开始学习任何注解具体语法而言,你可以把注解看成一张标签。这有助于你快速地理解它的大致作用。如果初学者在学习过程有大脑放空的时候,请不要慌张,对自己说:
  注解,标签。注解,标签。2。2注解语法
  因为平常开发少见,相信有不少的人员会认为注解的地位不高。其实同classs和interface一样,注解也属于一种类型。它是在JavaSE5。0版本中开始引入的概念。2。3元注解
  元注解是什么意思呢?
  元注解是可以注解到注解上的注解,或者说元注解是一种基本注解,但是它能够应用到其它的注解上面。
  如果难于理解的话,你可以这样理解。元注解也是一张标签,但是它是一张特殊的标签,它的作用和目的就是给其他普通的标签进行解释说明的。
  元标签有Retention、Documented、Target、Inherited、Repeatable5种。Retention
  Retention的英文意为保留期的意思。当Retention应用到一个注解上的时候,它解释说明了这个注解的的存活时间。
  它的取值如下:RetentionPolicy。SOURCE注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。RetentionPolicy。CLASS注解只被保留到编译进行的时候,它并不会被加载到JVM中。RetentionPolicy。RUNTIME注解可以保留到程序运行的时候,它会被加载进入到JVM中,所以在程序运行时可以获取到它们。
  我们可以这样的方式来加深理解,Retention去给一张标签解释的时候,它指定了这张标签张贴的时间。Retention相当于给一张标签上面盖了一张时间戳,时间戳指明了标签张贴的时间周期。Retention(RetentionPolicy。RUNTIME)publicinterfaceTestAnnotation{}
  上面的代码中,我们指定TestAnnotation可以在程序运行周期被获取到,因此它的生命周期非常的长。Documented
  顾名思义,这个元注解肯定是和文档有关。它的作用是能够将注解中的元素包含到Javadoc中去。Target
  Target是目标的意思,Target指定了注解运用的地方。
  你可以这样理解,当一个注解被Target注解时,这个注解就被限定了运用的场景。
  类比到标签,原本标签是你想张贴到哪个地方就到哪个地方,但是因为Target的存在,它张贴的地方就非常具体了,比如只能张贴到方法上、类上、方法参数上等等。Target有下面的取值ElementType。ANNOTATIONTYPE可以给一个注解进行注解ElementType。CONSTRUCTOR可以给构造方法进行注解ElementType。FIELD可以给属性进行注解ElementType。LOCALVARIABLE可以给局部变量进行注解ElementType。METHOD可以给方法进行注解ElementType。PACKAGE可以给一个包进行注解ElementType。PARAMETER可以给一个方法内的参数进行注解ElementType。TYPE可以给一个类型进行注解,比如类、接口、枚举Inherited
  Inherited是继承的意思,但是它并不是说注解本身可以继承,而是说如果一个超类被Inherited注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解。说的比较抽象。代码来解释。InheritedRetention(RetentionPolicy。RUNTIME)interfaceTest{}TestpublicclassA{}publicclassBextendsA{}
  注解Test被Inherited修饰,之后类A被Test注解,类B继承A,类B也拥有Test这个注解。
  可以这样理解:
  老子非常有钱,所以人们给他贴了一张标签叫做富豪。
  老子的儿子长大后,只要没有和老子断绝父子关系,虽然别人没有给他贴标签,但是他自然也是富豪。
  老子的孙子长大了,自然也是富豪。
  这就是人们口中戏称的富一代,富二代,富三代。虽然叫法不同,好像好多个标签,但其实事情的本质也就是他们有一张共同的标签,也就是老子身上的那张富豪的标签。Repeatable
  Repeatable自然是可重复的意思。Repeatable是Java1。8才加进来的,所以算是一个新的特性。
  什么样的注解会多次应用呢?通常是注解的值可以同时取多个。
  举个例子,一个人他既是程序员又是产品经理,同时他还是个画家。interfacePersons{Person〔〕value();}Repeatable(Persons。class)interfacePerson{S}Person(roleartist)Person(rolecoder)Person(rolePM)publicclassSuperMan{}
  注意上面的代码,Repeatable注解了Person。而Repeatable后面括号中的类相当于一个容器注解。
  什么是容器注解呢?就是用来存放其它注解的地方。它本身也是一个注解。
  我们再看看代码中的相关容器注解。interfacePersons{Person〔〕value();}
  按照规定,它里面必须要有一个value的属性,属性类型是一个被Repeatable注解过的注解数组,注意它是数组。
  如果不好理解的话,可以这样理解。Persons是一张总的标签,上面贴满了Person这种同类型但内容不一样的标签。把Persons给一个SuperMan贴上,相当于同时给他贴了程序员、产品经理、画家的标签。
  我们可能对于Person(rolePM)括号里面的内容感兴趣,它其实就是给Person这个注解的role属性赋值为PM,大家不明白正常,马上就讲到注解的属性这一块。2。4Java预置的注解
  Java语言本身已经提供了几个现成的注解。Deprecated
  这个元素是用来标记过时的元素,想必大家在日常开发中经常碰到。编译器在编译阶段遇到这个注解时会发出提醒警告,告诉开发者正在调用一个过时的元素比如过时的方法、过时的类、过时的成员变量。publicclassHero{Deprecatedpublicvoidsay(){System。out。println(Notinghastosay!);}publicvoidspeak(){System。out。println(Ihaveadream!);}}
  定义了一个Hero类,它有两个方法say()和speak(),其中say()被Deprecated注解。然后我们在IDE中分别调用它们。
  可以看到,say()方法上面被一条直线划了一条,这其实就是编译器识别后的提醒效果。Override
  这个大家应该很熟悉了,提示子类要复写父类中被Override修饰的方法SuppressWarnings
  阻止警告的意思。之前说过调用被Deprecated注解的方法后,编译器会警告提醒,而有时候开发者会忽略这种警告,他们可以在调用的地方通过SuppressWarnings达到目的。SuppressWarnings(deprecation)publicvoidtest1(){HeroheronewHero();hero。say();hero。speak();}SafeVarargs
  参数安全类型注解。它的目的是提醒开发者不要用参数做一些不安全的操作,它的存在会阻止编译器产生unchecked这样的警告。它是在Java1。7的版本中加入的。SafeVarargsNotactuallysafe!staticvoidm(ListString。。。stringLists){Object〔〕arraystringLListIntegertmpListArrays。asList(42);array〔0〕tmpLSemanticallyinvalid,butcompileswithoutwarningsStringsstringLists〔0〕。get(0);Ohno,ClassCastExceptionatruntime!}
  上面的代码中,编译阶段不会报错,但是运行时会抛出ClassCastException这个异常,所以它虽然告诉开发者要妥善处理,但是开发者自己还是搞砸了。
  Java官方文档说,未来的版本会授权编译器对这种不安全的操作产生错误警告。FunctionalInterface
  函数式接口注解,这个是Java1。8版本引入的新特性。函数式编程很火,所以Java8也及时添加了这个特性。
  函数式接口(FunctionalInterface)就是一个具有一个方法的普通接口。比如FunctionalInterfacepublicinterfaceRunnable{WhenanobjectimplementinginterfacecodeRunnableisusedtocreateathread,startingthethreadcausestheobjectsrunmethodtobecalledinthatseparatelyexecutingthread。
  Thegeneralcontractofthemethodrunisthatitmaytakeanyactionwhatsoever。seejava。lang。Threadrun()publicabstractvoidrun();}code
  我们进行线程开发中常用的Runnable就是一个典型的函数式接口,上面源码可以看到它就被FunctionalInterface注解。
  可能有人会疑惑,函数式接口标记有什么用,这个原因是函数式接口可以很容易转换为Lambda表达式。这是另外的主题了,有兴趣的同学请自己搜索相关知识点学习。3。注解的使用3。1注解的应用
  上面创建了一个注解,那么注解的的使用方法是什么呢。TestAnnotationpublicclassTest{}
  创建一个类Test,然后在类定义的地方加上TestAnnotation就可以用TestAnnotation注解这个类了。
  你可以简单理解为将TestAnnotation这张标签贴到Test这个类上面。3。2注解的提取
  前面的部分讲了注解的基本语法,现在是时候检测我们所学的内容了。
  我通过用标签来比作注解,前面的内容是讲怎么写注解,然后贴到哪个地方去,而现在我们要做的工作就是检阅这些标签内容。形象的比喻就是你把这些注解标签在合适的时候撕下来,然后检阅上面的内容信息。
  要想正确检阅注解,离不开一个手段,那就是反射。3。3注解与反射
  注解通过反射获取。首先可以通过Class对象的isAnnotationPresent()方法判断它是否应用了某个注解publicbooleanisAnnotationPresent(C?extendsAnnotationannotationClass){}
  然后通过getAnnotation()方法来获取Annotation对象。publicAgetAnnotation(ClassannotationClass){}
  或者是getAnnotations()方法。publicAnnotation〔〕getAnnotations(){}
  前一种方法返回指定类型的注解,后一种方法返回注解到这个元素上的所有注解。
  如果获取到的Annotation如果不为null,则就可以调用它们的属性方法了。比如TestAnnotation()publicclassTest{publicstaticvoidmain(String〔〕args){booleanhasAnnotationTest。class。isAnnotationPresent(TestAnnotation。class);if(hasAnnotation){TestAnnotationtestAnnotationTest。class。getAnnotation(TestAnnotation。class);System。out。println(id:testAnnotation。id());System。out。println(msg:testAnnotation。msg());}}}
  程序的运行结果是:id:1msg:
  这个正是TestAnnotation中id和msg的默认值。
  上面的例子中,只是检阅出了注解在类上的注解,其实属性、方法上的注解照样是可以的。同样还是要假手于反射。TestAnnotation(msghello)publicclassTest{Check(valuehi)PerformpublicvoidtestMethod(){}SuppressWarnings(deprecation)publicvoidtest1(){HeroheronewHero();hero。say();hero。speak();}publicstaticvoidmain(String〔〕args){booleanhasAnnotationTest。class。isAnnotationPresent(TestAnnotation。class);if(hasAnnotation){TestAnnotationtestAnnotationTest。class。getAnnotation(TestAnnotation。class);获取类的注解System。out。println(id:testAnnotation。id());System。out。println(msg:testAnnotation。msg());}try{FieldaTest。class。getDeclaredField(a);a。setAccessible(true);获取一个成员变量上的注解Checkchecka。getAnnotation(Check。class);if(check!null){System。out。println(checkvalue:check。value());}MethodtestMethodTest。class。getDeclaredMethod(testMethod);if(testMethod!null){获取方法中的注解Annotation〔〕anstestMethod。getAnnotations();for(inti0;ians。i){System。out。println(methodtestMethodannotation:ans〔i〕。annotationType()。getSimpleName());}}}catch(NoSuchFieldExceptione){TODOAutogeneratedcatchblocke。printStackTrace();System。out。println(e。getMessage());}catch(SecurityExceptione){TODOAutogeneratedcatchblocke。printStackTrace();System。out。println(e。getMessage());}catch(NoSuchMethodExceptione){TODOAutogeneratedcatchblocke。printStackTrace();System。out。println(e。getMessage());}}}
  它们的结果如下:id:1msg:hellocheckvalue:himethodtestMethodannotation:Perform
  需要注意的是,如果一个注解要在运行时被成功提取,那么Retention(RetentionPolicy。RUNTIME)是必须的。3。4注解的使用场景
  我相信博文讲到这里大家都很熟悉了注解,但是有不少同学肯定会问,注解到底有什么用呢?
  对啊注解到底有什么用?
  我们不妨将目光放到Java官方文档上来。
  文章开始的时候,我用标签来类比注解。但标签比喻只是我的手段,而不是目的。为的是让大家在初次学习注解时能够不被那些抽象的新概念搞懵。既然现在,我们已经对注解有所了解,我们不妨再仔细阅读官方最严谨的文档。
  注解是一系列元数据,它提供数据用来解释程序代码,但是注解并非是所解释的代码本身的一部分。注解对于代码的运行效果没有直接影响。注解有许多用处,主要如下:提供信息给编译器:编译器可以利用注解来探测错误和警告信息编译阶段时的处理:软件工具可以用来利用注解信息来生成代码、Html文档或者做其它相应处理。运行时的处理:某些注解可以在程序运行的时候接受代码的提取值得注意的是,注解不是代码本身的一部分。
  如果难于理解,可以这样看。罗永浩还是罗永浩,不会因为某些人对于他傻x的评价而改变,标签只是某些人对于其他事物的评价,但是标签不会改变事物本身,标签只是特定人群的手段。所以,注解同样无法改变代码本身,注解只是某些工具的的工具。
  还是回到官方文档的解释上,注解主要针对的是编译器和其它工具软件(SoftWaretool)。
  当开发者使用了Annotation修饰了类、方法、Field等成员之后,这些Annotation不会自己生效,必须由开发者提供相应的代码来提取并处理Annotation信息。这些处理提取和处理Annotation的代码统称为APT(AnnotationProcessingTool)。
  现在,我们可以给自己答案了,注解有什么用?给谁用?给编译器或者APT用的。
  如果,你还是没有搞清楚的话,我亲自写一个好了。3。5亲手自定义注解完成项目
  我要写一个测试框架,测试程序员的代码有无明显的异常。
  程序员A:我写了一个类,它的名字叫做NoBug,因为它所有的方法都没有错误。我:自信是好事,不过为了防止意外,让我测试一下如何?程序员A:怎么测试?我:把你写的代码的方法都加上Jiecha这个注解就好了。程序员A:好的。importceshi。JpublicclassNoBug{JiechapublicvoidsuanShu(){System。out。println(1234567890);}Jiechapublicvoidjiafa(){System。out。println(1111);}Jiechapublicvoidjiefa(){System。out。println(11(11));}Jiechapublicvoidchengfa(){System。out。println(3x535);}Jiechapublicvoidchufa(){System。out。println(6060);}publicvoidziwojieshao(){System。out。println(我写的程序没有bug!);}}
  上面的代码,有些方法上面运用了Jiecha注解。
  这个注解是我写的测试软件框架中定义的注解。importjava。lang。annotation。Rimportjava。lang。annotation。RetentionPRetention(RetentionPolicy。RUNTIME)publicinterfaceJiecha{}
  然后,我再编写一个测试类TestTool就可以测试NoBug相应的方法了。importjava。lang。reflect。InvocationTargetEimportjava。lang。reflect。MpublicclassTestTool{publicstaticvoidmain(String〔〕args){TODOAutogeneratedmethodstubNoBugtestobjnewNoBug();Classclazztestobj。getClass();Method〔〕methodclazz。getDeclaredMethods();用来记录测试产生的log信息StringBuilderlognewStringBuilder();记录异常的次数interrornum0;for(Methodm:method){只有被Jiecha标注过的方法才进行测试if(m。isAnnotationPresent(Jiecha。class)){try{m。setAccessible(true);m。invoke(testobj,null);}catch(Exceptione){TODOAutogeneratedcatchblocke。printStackTrace();log。append(m。getName());log。append();log。append(haserror:);log。append(rcausedby);记录测试过程中,发生的异常的名称log。append(e。getCause()。getClass()。getSimpleName());log。append(r);记录测试过程中,发生的异常的具体信息log。append(e。getCause()。getMessage());log。append(r);}}}log。append(clazz。getSimpleName());log。append(has);log。append(errornum);log。append(error。);生成测试报告System。out。println(log。toString());}}
  测试的结果是:123456789011111103x515chufahaserror:causedbyArithmeticExceptionbyzeroNoBughas1error。
  提示NoBug类中的chufa()这个方法有异常,这个异常名称叫做ArithmeticException,原因是运算过程中进行了除0的操作。
  所以,NoBug这个类有Bug。
  这样,通过注解我完成了我自己的目的,那就是对别人的代码进行测试。
  所以,再问我注解什么时候用?我只能告诉你,这取决于你想利用它干什么用。3。6注解应用实例
  注解运用的地方太多了,如:JUnit这个是一个测试框架,典型使用方法如下:publicclassExampleUnitTest{TestpublicvoidadditionisCorrect()throwsException{assertEquals(4,22);}}
  Test标记了要进行测试的方法additionisCorrect()。
  还有例如ssm框架等运用了大量的注解。注解部分总结如果注解难于理解,你就把它类同于标签,标签为了解释事物,注解为了解释代码。注解的基本语法,创建如同接口,但是多了个符号。注解的元注解。注解的属性。注解主要给编译器及工具类型的软件用的。注解的提取需要借助于Java的反射技术,反射比较慢,所以注解使用时也需要谨慎计较时间成本。4。APT实现原理4。1SPI机制
  SPI是jdk内置的服务发现机制,全称叫ServiceProviderInterface。
  SPI的工作原理,就是ClassPath路径下的METAINFservices文件夹中,以接口的全限定名来命名文件名,文件里面写该接口的实现。然后再资源加载的方式,读取文件的内容(接口实现的全限定名),然后再去加载类。
  SPI可以很灵活的让接口和实现分离,让api提供者只提供接口,第三方来实现。
  这一机制为很多框架的扩展提供了可能,比如在Dubbo、JDBC、SpringBoot中都使用到了SPI机制。虽然他们之间的实现方式不同,但原理都差不多。今天我们就来看看,SPI到底是何方神圣,在众多开源框架中又扮演了什么角色。4。1。1JDK中的SPI
  我们先从JDK开始,通过一个很简单的例子来看下它是怎么用的。4。1。1。1、小栗子
  首先,我们需要定义一个接口,SpiServicepackagecom。dxz。jdk。publicinterfaceSpiService{voidprintln();}
  然后,定义一个实现类,没别的意思,只做打印。packagecom。dxz。jdk。publicclassSpiServiceImplimplementsSpiService{Overridepublicvoidprintln(){System。out。println(SPIDEMO);}}
  最后呢,要在resources路径下配置添加一个文件。文件名字是接口的全限定类名,内容是实现类的全限定类名,多个实现类用换行符分隔。
  文件内容就是实现类的全限定类名:4。1。1。2、测试
  然后我们就可以通过ServiceLoader。load方法拿到实现类的实例,并调用它的方法。publicstaticvoidmain(String〔〕args){ServiceLoaderSpiServiceloadServiceLoader。load(SpiService。class);IteratorSpiServiceiteratorload。iterator();while(iterator。hasNext()){SpiServiceserviceiterator。next();service。println();}}4。1。1。3、源码分析
  首先,我们先来了解下ServiceLoader,看看它的类结构。publicfinalclassServiceLoaderSimplementsIterableS{配置文件的路径privatestaticfinalStringPREFIXMETAINF加载的服务类或接口privatefinalClassS已加载的服务类集合privateLinkedHashMapString,SprovidersnewLinkedHashMap();类加载器privatefinalClassL内部类,真正加载服务类privateLazyIteratorlookupI}
  当我们调用load方法时,并没有真正的去加载和查找服务类。而是调用了ServiceLoader的构造方法,在这里最重要的是实例化了内部类LazyIterator,它才是接下来的主角。privateServiceLoader(ClassSsvc,ClassLoadercl){要加载的接口serviceObjects。requireNonNull(svc,Serviceinterfacecannotbenull);类加载器loader(clnull)?ClassLoader。getSystemClassLoader():访问控制器acc(System。getSecurityManager()!null)?AccessController。getContext():先清空providers。clear();实例化内部类LazyIteratorlookupIteratornewLazyIterator(service,loader);}
  查找实现类和创建实现类的过程,都在LazyIterator完成。当我们调用iterator。hasNext和iterator。next方法的时候,实际上调用的都是LazyIterator的相应方法。publicIteratorSiterator(){returnnewIteratorS(){publicbooleanhasNext(){returnlookupIterator。hasNext();}publicSnext(){returnlookupIterator。next();}。。。。。。。};}
  所以,我们重点关注lookupIterator。hasNext()方法,它最终会调用到hasNextServicez,在这里返回实现类名称。privateclassLazyIteratorimplementsIteratorS{ClassSClassLEnumerationURLIteratorSStringnextNprivatebooleanhasNextService(){第二次调用的时候,已经解析完成了,直接返回if(nextName!null){}if(configsnull){METAINFservices加上接口的全限定类名,就是文件服务类的文件METAINFservicescom。viewscenes。netsupervisor。spi。SPIServiceStringfullNamePREFIXservice。getName();将文件路径转成URL对象configsloader。getResources(fullName);}while((pendingnull)!pending。hasNext()){解析URL文件对象,读取内容,最后返回pendingparse(service,configs。nextElement());}拿到第一个实现类的类名nextNamepending。next();}}
  然后当我们调用next()方法的时候,调用到lookupIterator。nextService。它通过反射的方式,创建实现类的实例并返回。privateSnextService(){全限定类名StringcnnextNnextN创建类的Class对象C?cClass。forName(cn,false,loader);通过newInstance实例化Spservice。cast(c。newInstance());放入集合,返回实例providers。put(cn,p);}
  到这为止,已经获取到了类的实例。4。1。2JDBC中的应用
  我们开头说,SPI机制为很多框架的扩展提供了可能,其实JDBC就应用到了这一机制。
  在以前,需要先设置数据库驱动的连接,再通过DriverManager。getConnection获取一个Connection。Stringurljdbc:mysql:consult?serverTimezoneUTC;SSClass。forName(com。mysql。jdbc。Driver);ConnectionconnectionDriverManager。getConnection(url,user,password);
  而现在,设置数据库驱动连接,这一步骤就不再需要,那么它是怎么分辨是哪种数据库的呢?答案就在SPI。4。1。2。1加载
  下图mysqlDriver的实例。com。mysql。cj。jdbc。Driver就是Driver的实现。
  mysql驱动为例
  mysqlDriver实现类
  Driver接口上的一段注释。
  DriverManager将尝试加载尽可能多的驱动程序。
  我们把目光回到DriverManager类,它在静态代码块里面做了一件比较重要的事。很明显,它已经通过SPI机制,把数据库驱动连接初始化了。publicclassDriverManager{static{loadInitialDrivers();println(JDBCDriverManagerinitialized);}}
  接下来我们去DriverManger上看看是如何加载Driver接口的实现类的。publicclassDriverManager{LoadtheinitialJDBCdriversbycheckingtheSystempropertyjdbc。propertiesandthenusethe{codeServiceLoader}mechanismstatic{loadInitialDrivers();println(JDBCDriverManagerinitialized);}privatestaticvoidloadInitialDrivers(){AccessController。doPrivileged(newPrivilegedActionVoid(){publicVoidrun(){ServiceLoaderDriverloadedDriversServiceLoader。load(Driver。class);IteratorDriverdriversIteratorloadedDrivers。iterator();try{while(driversIterator。hasNext()){driversIterator。next();}}catch(Throwablet){}}});}
  在DriverManger类初始化的时候,调用loadInitialDrivers方法。
  具体过程还得看loadInitialDrivers,它在里面查找的是Driver接口的服务类,所以它的文件路径就是:
  METAINFservicesjava。sql。Driver
  在loadInitialDrivers方法中,privatestaticvoidloadInitialDrivers(){AccessController。doPrivileged(newPrivilegedActionVoid(){publicVoidrun(){很明显,它要加载Driver接口的服务类,Driver接口的包为:java。sql。Driver所以它要找的就是METAINFservicesjava。sql。Driver文件ServiceLoaderDriverloadedDriversServiceLoader。load(Driver。class);IteratorDriverdriversIteratorloadedDrivers。iterator();try{查到之后创建对象while(driversIterator。hasNext()){driversIterator。next();当调用next方法时,就会创建这个类的实例。它就完成了一件事,向DriverManager注册自身的实例。}}catch(Throwablet){Donothing}}});}
  这段代码是实现SPI的关键,真是这个ServiceLoader类去实现SPI的。那么下面就分析分析ServiceLoader的代码,看看是如何实现SPI的。packagejava。publicfinalclassServiceLoaderSimplementsIterableS{publicstaticSServiceLoaderSload(ClassSservice){ClassLoaderclThread。currentThread()。getContextClassLoader();returnServiceLoader。load(service,cl);}其中service就是要加载实现类对应的接口,loader就是用该加载器去加载对应的实现类publicstaticSServiceLoaderSload(ClassSservice,ClassLoaderloader){returnnewServiceLoader(service,loader);}}
  先调用ServiceLoader类的静态方法load,然后根据当前线程的上下文类加载器,创建一个ServiceLoader实例。privatestaticfinalStringPREFIXMETAINFpublicvoidreload(){providers。clear();lookupIteratornewLazyIterator(service,loader);}privateServiceLoader(ClassSsvc,ClassLoadercl){serviceObjects。requireNonNull(svc,Serviceinterfacecannotbenull);loader(clnull)?ClassLoader。getSystemClassLoader():acc(System。getSecurityManager()!null)?AccessController。getContext():reload();}
  创建ServiceLoader实例的时候,接着创建一个Iterator实现类。接下来这个Iterator分析的重点。基本所有的加载类的实现逻辑都在里面。
  其中ServiceLoader类中一个常量的定义是关键的。前面说过,我们service的实现类在放在哪,就是这里写死的常量路径。这里先介绍Iterator的变量,先大概有个印象。privateclassLazyIteratorimplementsIteratorS{service,loader前面介绍过了。ClassSClassLEnumerationURLIteratorSStringnextNpublicbooleanhasNext(){省略权限相关代码returnhasNextService();}privatebooleanhasNextService(){一开始nextName肯定为空if(nextName!null){}一开始configs也肯定为空if(configsnull){try{PREFIXMETAINFservices以mysql为例,就是METAINFservicesjava。sql。DriverStringfullNamePREFIXservice。getName();if(loadernull)configsClassLoader。getSystemResources(fullName);loader去加载这个classpath下文件。这里很有可能返回的是多个文件的资源,例如一个项目下既有mysql驱动,也有sqlserver驱动等所以返回的是一个枚举类型。elseconfigsloader。getResources(fullName);}catch(IOExceptionx){fail(service,Errorlocatingconfigurationfiles,x);}}while((pendingnull)!pending。hasNext()){if(!configs。hasMoreElements()){}然后根据加载出来的资源,解析一个文件中的内容。放到Iterator实现类中pendingparse(service,configs。nextElement());}这里next返回的就是文件一行的内容,一般一行对应一个接口的实现类。一个接口放多行,就可以有多个接口实现类中。nextNamepending。next();}}
  configs变量,就对应service文件。是个枚举,就是说可以定义多个service文件。
  pending变量:就对应configs中,service文件解析出来的一行有效内容,即一个实现类的全限定类名称。
  parse方法就是简单,不是重点。这里就略过了。就是读取service文件中读取,一行就是一个nextName,然后遇到就跳过后面的内容。所以service文件可以用作为注释。直到遇到空行,解析结束。
  LazyIterator类中的hasNext方法就分析完了。使用classLoader。getResources方法加载service文件。我看了下getResources方法,并一定是加载classpath下的资源,得根据classLoader来解决。不过绝大多数情况下,都是classpath的资源。这里为了好理解,就理解成classpath下的资源。
  接着分析LazyIteratornext方法。publicSnext(){删除权限相关代码returnnextService();}privateSnextService(){if(!hasNextService())thrownewNoSuchElementException();这个nextName前面分析过了StringcnnextNnextNC?try{加载类,且不初始化cClass。forName(cn,false,loader);}catch(ClassNotFoundExceptionx){fail(service,Providercnnotfound);}if(!service。isAssignableFrom(c)){fail(service,Providercnnotasubtype);}try{类型判断Spservice。cast(c。newInstance());最后放到ServiceLoader实例变量Map中,缓存起来,下次直接使用providers。put(cn,p);}catch(Throwablex){fail(service,Providercncouldnotbeinstantiated,x);}thrownewError();Thiscannothappen}
  next方法就比较简单了,根据前面解析出来的nextName(接口实现类的全限定名称),用Class。forName创建对应的Class对象。4。1。2。2创建Connection
  DriverManager。getConnection()方法就是创建连接的地方,它通过循环已注册的数据库驱动程序,调用其connect方法,获取连接并返回。privatestaticConnectiongetConnection(Stringurl,Propertiesinfo,C?caller)throwsSQLException{registeredDrivers中就包含com。mysql。cj。jdbc。Driver实例for(DriverInfoaDriver:registeredDrivers){if(isDriverAllowed(aDriver。driver,callerCL)){try{调用connect方法创建连接ConnectionconaDriver。driver。connect(url,info);if(con!null){return(con);}}catch(SQLExceptionex){if(reasonnull){}}}else{println(skipping:aDriver。getClass()。getName());}}}4。1。2。3扩展
  既然我们知道JDBC是这样创建数据库连接的,我们能不能再扩展一下呢?如果我们自己也创建一个java。sql。Driver文件,自定义实现类MySQLDriver,那么,在获取连接的前后就可以动态修改一些信息。
  还是先在项目resources下创建文件,文件内容为自定义驱动类com。jcc。java。spi。domyself。MySQLDriver
  我们的MySQLDriver实现类,继承自MySQL中的NonRegisteringDriver,还要实现java。sql。Driver接口。这样,在调用connect方法的时候,就会调用到此类,但实际创建的过程还靠MySQL完成。publicclassMySQLDriverextendsNonRegisteringDriverimplementsDriver{static{try{DriverManager。registerDriver(newMySQLDriver());}catch(SQLExceptione){e。printStackTrace();}}publicMySQLDriver()throwsSQLException{}OverridepublicConnectionconnect(Stringurl,Propertiesinfo)throwsSQLException{System。out。println(准备创建数据库连接。url:url);System。out。println(JDBC配置信息:info);重置配置info。setProperty(user,root);Connectionconnectionsuper。connect(url,info);System。out。println(数据库连接创建完成!connection。toString());}}
  这样的话,当我们获取数据库连接的时候,就会调用到这里。输出结果准备创建数据库连接。url:jdbc:mysql:consult?serverTimezoneUTCJDBC配置信息:{userroot,passwordroot}数据库连接创建完成!com。mysql。cj。jdbc。ConnectionImpl7cf10a6f4。1。3SpringBoot中的应用
  SpringBoot提供了一种快速的方式来创建可用于生产环境的基于Spring的应用程序。它基于Spring框架,更倾向于约定而不是配置,并且旨在使您尽快启动并运行。
  即便没有任何配置文件,SpringBoot的Web应用都能正常运行。这种神奇的事情,SpringBoot正是依靠自动配置来完成。
  说到这,我们必须关注一个东西:SpringFactoriesLoader,自动配置就是依靠它来加载的。4。1。3。1配置文件
  SpringFactoriesLoader来负责加载配置。我们打开这个类,看到它加载文件的路径是:METAINFspring。factories
  笔者在项目中搜索这个文件,发现有4个Jar包都包含它:Listitemspringboot2。1。9。RELEASE。jarspringbeans5。1。10。RELEASE。jarspringbootautoconfigure2。1。9。RELEASE。jarmybatisspringbootautoconfigure2。1。0。jar
  那么它们里面都是些啥内容呢?其实就是一个个接口和类的映射。在这里笔者就不贴了,有兴趣的小伙伴自己去看看。
  比如在SpringBoot启动的时候,要加载所有的ApplicationContextInitializer,那么就可以这样做:
  SpringFactoriesLoader。loadFactoryNames(ApplicationContextInitializer。class,classLoader)4。1。3。2加载文件
  loadSpringFactories就负责读取所有的spring。factories文件内容。privatestaticMapString,ListStringloadSpringFactories(NullableClassLoaderclassLoader){MultiValueMapString,Stringresultcache。get(classLoader);if(result!null){}try{获取所有spring。factories文件的路径EnumerationURLurlslassLoader。getResources(METAINFspring。factories);resultnewLinkedMultiValueMap();while(urls。hasMoreElements()){URLurlurls。nextElement();加载文件并解析文件内容UrlResourceresourcenewUrlResource(url);PropertiespropertiesPropertiesLoaderUtils。loadProperties(resource);for(Map。E?,?entry:properties。entrySet()){StringfactoryClassName((String)entry。getKey())。trim();for(StringfactoryName:StringUtils。commaDelimitedListToStringArray((String)entry。getValue())){result。add(factoryClassName,factoryName。trim());}}}cache。put(classLoader,result);}catch(IOExceptionex){thrownewIllegalArgumentException(Unabletoloadfactoriesfromlocation〔FACTORIESRESOURCELOCATION〕,ex);}}
  可以看到,它并没有采用JDK中的SPI机制来加载这些类,不过原理差不多。都是通过一个配置文件,加载并解析文件内容,然后通过反射创建实例。4。1。3。3参与其中
  假如你希望参与到SpringBoot初始化的过程中,现在我们又多了一种方式。
  我们也创建一个spring。factories文件,自定义一个初始化器。
  org。springframework。context。ApplicationContextInitializercom。youyouxunyin。config。context。MyContextInitializer
  然后定义一个MyContextInitializer类publicclassMyContextInitializerimplementsApplicationContextInitializer{Overridepublicvoidinitialize(ConfigurableApplicationContextconfigurableApplicationContext){System。out。println(configurableApplicationContext);}}4。1。4Dubbo中的应用
  我们熟悉的Dubbo也不例外,它也是通过SPI机制加载所有的组件。同样的,Dubbo并未使用Java原生的SPI机制,而是对其进行了增强,使其能够更好的满足需求。在Dubbo中,SPI是一个非常重要的模块。基于SPI,我们可以很容易的对Dubbo进行拓展。
  它的使用方式同样是在METAINFservices创建文件并写入相关类名。4。1。5sentinel中的应用
  通过SPI机制将METAINFOservcie下配置好的默认责任链构造这加载出来,然后调用其builder()方法进行构建调用链。publicfinalclassSlotChainProvider{privatestaticvolatileSlotChainBuilderslotChainBTheloadandpickprocessisnotthreadsafe,butitsokaysincethemethodshouldbeonlyinvokedvia{codelookProcessChain}in{linkcom。alibaba。csp。sentinel。CtSph}underlock。returnnewcreatedslotchainpublicstaticProcessorSlotChainnewSlotChain(){if(slotChainBuilder!null){returnslotChainBuilder。build();}ResolvetheslotchainbuilderSPI。slotChainBuilderSpiLoader。of(SlotChainBuilder。class)。loadFirstInstanceOrDefault();if(slotChainBuildernull){Shouldnotgothroughhere。RecordLog。warn(〔SlotChainProvider〕Wrongstatewhenresolvingslotchainbuilder,usingdefault);slotChainBuildernewDefaultSlotChainBuilder();}else{RecordLog。info(〔SlotChainProvider〕Globalslotchainbuilderresolved:{},slotChainBuilder。getClass()。getCanonicalName());}returnslotChainBuilder。build();}privateSlotChainProvider(){}}
  SpiLoader。of()publicstaticTSpiLoaderTof(ClassTservice){AssertUtil。notNull(service,SPIclasscannotbenull);AssertUtil。isTrue(service。isInterface()Modifier。isAbstract(service。getModifiers()),SPIclass〔service。getName()〕mustbeinterfaceorabstractclass);StringclassNameservice。getName();SpiLoaderTspiLoaderSPILOADERMAP。get(className);if(spiLoadernull){synchronized(SpiLoader。class){spiLoaderSPILOADERMAP。get(className);if(spiLoadernull){SPILOADERMAP。putIfAbsent(className,newSpiLoader(service));spiLoaderSPILOADERMAP。get(className);}}}returnspiL}Spi(isDefaulttrue)publicclassDefaultSlotChainBuilderimplementsSlotChainBuilder{OverridepublicProcessorSlotChainbuild(){ProcessorSlotChainchainnewDefaultProcessorSlotChain();ListProcessorSlotsortedSlotListSpiLoader。of(ProcessorSlot。class)。loadInstanceListSorted();for(ProcessorSlotslot:sortedSlotList){if(!(slotinstanceofAbstractLinkedProcessorSlot)){RecordLog。warn(TheProcessorSlot(slot。getClass()。getCanonicalName())isnotaninstanceofAbstractLinkedProcessorSlot,cantbeaddedintoProcessorSlotChain);}chain。addLast((AbstractLinkedProcessorS?)slot);}}}
  责任链同样是由spi机制加载出来的,上面的加载只会在第一次使用的时候加载,然后缓存到内从后,以后直接取即可。
  至此,SPI机制的实现原理就分析完了。虽然SPI我们日常开发中用的很少,但是至少了解了解还是有必要的。例如:一些框架实现中一般都会用到SPI机制。
  vert。x内部也是大量使用SPI4。2APT注解处理器4。2。1基础知识
  注解的保留时间分为三种:SOURCE只在源代码中保留,编译器将代码编译成字节码文件后就会丢掉CLASS保留到字节码文件中,但Java虚拟机将class文件加载到内存是不一定在内存中保留RUNTIME一直保留到运行时
  通常我们使用后两种,因为SOURCE主要起到标记方便理解的作用,无法对代码逻辑提供有效的信息。
  时间
  解析
  性能影响
  RUNTIME
  运行时
  反射
  有
  CLASS
  编译期
  APTJavaPoet
  无
  如上图,对比两种解析方式:运行时注解比较简单易懂,可以运用反射技术在程序运行时获取指定的注解信息,因为用到反射,所以性能会收到一定影响。编译期注解可以使用APT(AnnotationProcessingTool)技术,在编译期扫描和解析注解,并结合JavaPoet技术生成新的java文件,是一种更优雅的解析注解的方式,不会对程序性能产生太大影响。
  下面以BindView为例,介绍两种方式的不同使用方法。4。2。2运行时注解
  运行时注解主要通过反射进行解析,代码运行过程中,通过反射我们可以知道哪些属性、方法使用了该注解,并且可以获取注解中的参数,做一些我们想做的事情。
  首先,新建一个注解Target({ElementType。FIELD})Retention(RetentionPolicy。RUNTIME)publicinterfaceBindViewTo{intvalue()default1;需要绑定的viewid}
  然后,新建一个注解解析工具类AnnotationTools,和一般的反射用法并无不同:publicclassAnnotationTools{publicstaticvoidbindAllAnnotationView(Activityactivity){获得成员变量Field〔〕fieldsactivity。getClass()。getDeclaredFields();for(Fieldfield:fields){try{if(field。getAnnotations()!null){判断BindViewTo注解是否存在if(field。isAnnotationPresent(BindViewTo。class)){获取访问权限field。setAccessible(true);BindViewTogetViewTofield。getAnnotation(BindViewTo。class);获取ViewidintidgetViewTo。value();通过id获取View,并赋值该成员变量field。set(activity,activity。findViewById(id));}}}catch(Exceptione){}}}}
  在Activity中调用publicclassMainActivityextendsAppCompatActivity{BindViewTo(R。id。text)privateTextViewmTOverrideprotectedvoidonCreate(BundlesavedInstanceState){super。onCreate(savedInstanceState);setContentView(R。layout。activitymain);调用注解绑定,当前Activity中所有使用BindViewTo注解的控件将自动绑定AnnotationTools。bindAllAnnotationView(this);测试绑定是否成功mText。setTextColor(Color。RED);}}
  测试结果毫无意外,字体变成了红色,说明绑定成功。4。2。3编译期注解(APTJavaPoet)
  编译期注解解析需要用到APT(AnnotationProcessingTool)技术,APT是javac中提供的一种编译时扫描和处理注解的工具,它会对源代码文件进行检查,并找出其中的注解,然后根据用户自定义的注解处理方法进行额外的处理。APT工具不仅能解析注解,还能结合JavaPoet技术根据注解生成新的的Java源文件,最终将生成的新文件与原来的Java文件共同编译。
  APT实现流程如下:创建一个javalib作为注解解析库如aptprocessor在创建一个javalib作为注解声明库如aptannotation搭建两个lib和主项目的依赖关系实现AbstractProcessor编译和调用
  整个流程是固定的,我们的主要工作是继承AbstractProcessor,并且实现其中四个方法。下面一步一步详细介绍:
  4。2。3。创建解析库aptprocessorapplyplugin:javalibrarydependencies{implementationfileTree(dir:libs,include:〔。jar〕)compilecom。squareup:javapoet:1。9。0square开源的Java代码生成框架compilecom。google。auto。service:autoservice:1。0rc2Google开源的用于注册自定义注解处理器的工具implementationproject(:aptannotation)依赖自定义注解声明库}sourceCompatibility7targetCompatibility7
  4。2。3。创建注解库aptannotation
  声明一个注解BindViewTo,注意Retention不再是RUNTIME,而是CLASS。Target({ElementType。FIELD})Retention(RetentionPolicy。CLASS)publicinterfaceBindViewTo{intvalue()default1;}
  4。2。3。搭建主项目依赖关系dependencies{implementationfileTree(dir:libs,include:〔。jar〕)implementationproject(:aptannotation)依赖自定义注解声明库annotationProcessorproject(:aptprocessor)依赖自定义注解解析库(仅编译期)}
  这里需要解释一下,因为注解解析库只在程序编译期有用,没必要打包进APK。所以依赖解析库使用的关键字是annotationProcessor,这是google为gradle插件添加的特性,表示只在编译期依赖,不会打包进最终APK。这也是为什么前面要把注解声明和注解解析拆分成两个库的原因。因为注解声明是一定要编译到最终APK的,而注解解析不需要。
  4。2。3。实现AbstractProcessor
  这是最复杂的一步,也是完成我们期望工作的重点。首先,我们在aptprocessor中创建一个继承自AbstractProcessor的子类,重载其中四个方法:init()此处初始化一个工具类getSupportedSourceVersion()声明支持的Java版本,一般为最新版本getSupportedAnnotationTypes()声明支持的注解列表process()编译器回调方法,apt核心实现方法具体代码如下:SupportedSourceVersion(SourceVersion。RELEASE7)SupportedAnnotationTypes(com。xibeixue。aptannotation。BindViewTo)AutoService(Processor。class)publicclassBindViewProcessorextendsAbstractProcessor{privateElementsmElementUprivateHashMapString,BinderClassCreatormCreatorMapnewHashMap();init方法一般用于初始化一些用到的工具类,主要有processingEnvironment。getElementUtils();处理Element的工具类,用于获取程序的元素,例如包、类、方法。processingEnvironment。getTypeUtils();处理TypeMirror的工具类,用于取类信息processingEnvironment。getFiler();文件工具processingEnvironment。getMessager();错误处理工具Overridepublicsynchronizedvoidinit(ProcessingEnvironmentprocessingEnvironment){super。init(processingEnvironment);mElementUtilsprocessingEnv。getElementUtils();}获取Java版本,一般用最新版本也可以使用注解方式:SupportedSourceVersion(SourceVersion。RELEASE7)OverridepublicSourceVersiongetSupportedSourceVersion(){returnSourceVersion。latestSupported();}获取目标注解列表也可以使用注解方式:SupportedAnnotationTypes(com。xibeixue。aptannotation。BindViewTo)OverridepublicSetStringgetSupportedAnnotationTypes(){HashSetStringsupportTypesnewLinkedHashSet();supportTypes。add(BindViewTo。class。getCanonicalName());returnsupportT}编译期回调方法,apt核心实现方法包含所有使用目标注解的元素(Element)Overridepublicbooleanprocess(S?extendsTypeElementset,RoundEnvironmentroundEnvironment){扫描整个工程,找出所有使用BindViewTo注解的元素S?extendsElementelementsroundEnvironment。getElementsAnnotatedWith(BindViewTo。class);遍历元素,为每一个类元素创建一个Creatorfor(Elementelement:elements){BindViewTo限定了只能属性使用,这里强转为变量元素VariableElementVariableElementvariableElement(VariableElement)获取封装属性元素的类元素TypeElementTypeElementclassElement(TypeElement)variableElement。getEnclosingElement();获取简单类名StringfullClassNameclassElement。getQualifiedName()。toString();BinderClassCreatorcreatormCreatorMap。get(fullClassName);如果不存在,则创建一个对应的Creatorif(creatornull){creatornewBinderClassCreator(mElementUtils。getPackageOf(classElement),classElement);mCreatorMap。put(fullClassName,creator);}将需要绑定的变量和对应的viewid存储到对应的Creator中BindViewTobindAnnotationvariableElement。getAnnotation(BindViewTo。class);intidbindAnnotation。value();creator。putElement(id,variableElement);}每一个类将生成一个新的java文件,其中包含绑定代码for(Stringkey:mCreatorMap。keySet()){BinderClassCreatorbinderClassCreatormCreatorMap。get(key);通过javapoet构建生成Java类文件JavaFilejavaFileJavaFile。builder(binderClassCreator。getPackageName(),binderClassCreator。generateJavaCode())。build();try{javaFile。writeTo(processingEnv。getFiler());}catch(IOExceptione){e。printStackTrace();}}}}
  其中,BinderClassCreator是代码生成相关方法,具体代码如下:publicclassBinderClassCreator{publicstaticfinalStringParamNamerootVprivateTypeElementmTypeEprivateStringmPackageNprivateStringmBinderClassNprivateMapInteger,VariableElementmVariableElementsnewHashMap();parampackageElement包元素paramclassElement类元素publicBinderClassCreator(PackageElementpackageElement,TypeElementclassElement){this。mTypeElementclassEmPackageNamepackageElement。getQualifiedName()。toString();mBinderClassNameclassElement。getSimpleName()。toString()ViewB}publicvoidputElement(intid,VariableElementvariableElement){mVariableElements。put(id,variableElement);}publicTypeSpecgenerateJavaCode(){returnTypeSpec。classBuilder(mBinderClassName)public修饰类。addModifiers(Modifier。PUBLIC)添加类的方法。addMethod(generateMethod())构建Java类。build();}privateMethodSpecgenerateMethod(){获取全类名ClassNameclassNameClassName。bestGuess(mTypeElement。getQualifiedName()。toString());构建方法方法名returnMethodSpec。methodBuilder(bindView)public方法。addModifiers(Modifier。PUBLIC)返回void。returns(void。class)方法传参(参数全类名,参数名)。addParameter(className,ParamName)方法代码。addCode(generateMethodCode())。build();}privateStringgenerateMethodCode(){StringBuildercodenewStringBuilder();for(intid:mVariableElements。keySet()){VariableElementvariableElementmVariableElements。get(id);变量名称StringnamevariableElement。getSimpleName()。toString();变量类型StringtypevariableElement。asType()。toString();rootView。name(type)view。findViewById(id),注意原类中变量声明不能为private,否则这里是获取不到的StringfindViewCodeParamName。name(type)ParamName。findViewById(id);;code。append(findViewCode);}returncode。toString();}publicStringgetPackageName(){returnmPackageN}}
  4。2。3。编译和调用
  在MainActivity中调用,这里需要强调的是待绑定变量不能声明为private,原因在上面代码注释中已经解释了。publicclassMainActivityextendsAppCompatActivity{BindViewTo(R。id。text)publicTextViewmTOverrideprotectedvoidonCreate(BundlesavedInstanceState){super。onCreate(savedInstanceState);setContentView(R。layout。activitymain);这里的MainActivity需要先编译生成后才能调用newMainActivityViewBinding()。bindView(this);测试绑定是否成功mText。setTextColor(Color。RED);}}
  此时,build或rebuild工程(需要先注掉MainActivity的调用),会看到在generatedJava文件夹下生成了新的Java文件。
  上面的调用方式需要先编译一次才能使用,当有多个Activity时比较繁琐,而且无法做到统一。
  我们也可以选择另一种更简便的方法,即反射调用。新建工具类如下:publicclassMyButterKnife{publicstaticvoidbind(Activityactivity){Classclazzactivity。getClass();try{ClassbindViewClassClass。forName(clazz。getName()ViewBinding);MethodmethodbindViewClass。getMethod(bindView,activity。getClass());method。invoke(bindViewClass。newInstance(),activity);}catch(Exceptione){e。printStackTrace();}}}
  调用方式改为:OverrideprotectedvoidonCreate(BundlesavedInstanceState){super。onCreate(savedInstanceState);setContentView(R。layout。activitymain);通过反射调用MyButterKnife。bind(this);测试绑定是否成功mText。setTextColor(Color。RED);}
  此方式虽然也会稍微影响性能,但依然比直接使用运行时注解高效得多。4。2。4APT注解处理器总结
  说到底,APT是一个编译器工具,是一个非常好的从源码到编译期的过渡解析工具。虽然结合JavaPoet技术被各大框架使用,但是依然存在固有的缺陷,比如变量不能私有,依然要采用反射调用等,普通开发者可斟酌使用。
  个人认为APT有如下优点:配置方式,替换文件配置方式,改为代码内配置,提高程序内聚性代码精简,一劳永逸,省去繁琐复杂的格式化代码,适合团队内推广
  以上优点同时也是缺点,因为很多代码都在后台生成,会对新同学造成理解困难,影响其对整体架构的理解,增加学习成本。
  近期研究热修复和APT,发现从我们写完成代码,到代码真正执行,期间还真是有大把的空子可以钻啊,借图mark一下。
  4。3javac源码分析4。3。1javac概述
  我们都知道。java文件要首先被编译成。class文件才能被JVM认识,这部分的工作主要由Javac来完成,类似于Javac这样的我们称之为前端编译器;
  但是。class文件也不是机器语言,怎么才能让机器识别呢?就需要JVM将。class文件编译成机器码,这部分工作由JIT编译器完成;
  除了这两种编译器,还有一种直接把。java文件编译成本地机器码的编译器,我们称之AOT编译器。
  4。3。2javac的编译过程
  首先,我们先导一份javac的源码(基于openjdk8)出来,下载地址:https:hg。openjdk。java。netjdk8jdk8langtoolsarchivetip。tar。gz,然后将JDKSRCHOMElangtoolssrcshareclassescomsun目录下的源文件全部复制到工程的源码目录中,生成的目录如下:
  我们执行com。sun。tools。javac。Main的main方法,就和我们在命令窗口中使用javac命令一样:
  从SunJavac的代码来看,编译过程大致可以分为三个步骤:解析和填充符号表过程插入式注解处理器的注解处理过程分析和字节码生成过程
  这三个步骤所做的工作内容大致如下:
  这三个步骤之间的关系和交互顺序如下图所示,可以看到如果注解处理器在处理注解期间对语法树进行了修改,编译器将回到解析和填充符号表的过程进行重新处理,直到注解处理器没有再对语法树进行修改为止。
  Javac编译的入口是com。sun。tools。javac。main。JavaCompiler类,上述三个步骤的代码都集中在这个类的compile()和compile2()中:
  4。3。3javac编译器编译程序的步骤词法分析首先是读取源代码,找出这些字节中哪些是我们定义的语法关键词,如Java中的if、else、for等关键词语法分析的结果:从源代码中找出一些规范化的token流注:token是一种认证机制语法分析检查关键词组合在一起是不是Java语言规范,如if后面是不是紧跟着一个布尔表达式。语法分析的结果:形成一个符合Java语言规范的抽象语法树语义分析把一些难懂的、复杂的语法转化为更加简单的语法语义分析的结果:完成复杂语法到简单语法的简化,如将foreach语句转化成for循环结果,还有注解等。最后形成一个注解过后的抽象语法树,这颗语法树更接近目标语言的语法规则生成字节码通过字节码生成器生成字节码,根据经过注解的抽象语法树生成字节码,也就是将一个数据结构转化成另一个数据结构代码生成器的结果:生成符合Java虚拟机规范的字节码注:抽象语法树在计算机科学中,抽象语法树是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构
投诉 评论 转载

1秒丢了1100万今天,中证1000股指期货主力合约,上演了一秒跌停的乌龙指。集合竞价打到跌停,开盘第一秒恢复。可能是集合竞价期间挂错单子了,结果1秒损失了差不多1100多万吧。……中央政府30亿元人民币国债在澳门金融机构成功上市新华社澳门9月16日电(记者刘刚李寒芳)中央政府30亿元人民币国债16日在中华(澳门)金融资产交易股份有限公司(简称MOX)成功上市。这是中央政府继2019年之后,再次于……Android基础系列篇(一)注解的那些事儿前言本系列文章主要是汇总了一下大佬们的技术文章,属于Android基础部分,作为一名合格的安卓开发工程师,咱们肯定要熟练掌握java和android,本期就来说说这些……全场景生态引擎联想天禧首发,科技创新引领潮酷智生活昨天,2022联想创新科技大会正式举行。基于端边云网智的架构,联想发布了让生活更加潮酷的智能生态服务引擎联想天禧引擎,更通过自制综艺《你好,酷生活》并围绕家庭安防、照明、能源三……相由心生,年龄越大相貌上越显露出来岁月会在人的脸面上留下痕迹。皱纹会显现在脸颊额头,但还有一种内在的东西,会在脸面上显示出来。那就是,随着岁月的打磨,人会出现两极分化的状态。有的人经过自己的不断修养……我国工业互联网产业规模超万亿元走进浙江省杭州市临平区老板电器茅山智能制造基地智能工厂,生产线上只有设备状态指示灯、品质检验灯以及自动导向车闪着微光,284台自动化设备正自行运转。所有设备都基于‘5G工业互联……当亲人离世时,切莫做亲人的敌人叶曼先生说:当亲人弥留之际,千万不要对亲人说:我舍不得你。这句话就相当于固执的攀缘,亲人会舍不得走。能够放下而且安详的去世是对即将离世的亲人最好的安排!我们能为他做得最好……孩子脾胃不好,多用3个中医方法调理,绿色安全还能促进健康长高祖国医学云:小儿为纯阳之体,呼为纯阳,元气未散。意为3岁以下孩童,阳气旺盛、发育迅速,是人体生长发育最黄金的时期!但与此同时,小孩也有先天3不足,不论你的孩子出生时看起来……折叠屏适不适合玩游戏?看完ROG6天玑至尊版的实测,这才叫专折叠屏的横空出世也属于不鸣则已,一鸣惊人。在近两年时间里,大家都见证了后来者居上的折叠屏形态的手机。那么,风靡2022年的折叠屏手机,是否适合游戏玩家呢?有一说一,折叠屏手机可……我国核能发电占比将持续提高核能多用途应用范围不断扩大央视网消息:核能作为稳定可靠的清洁低碳能源,是我国能源向清洁化、低碳化转型的重要选项。《中国核能发展报告(2022)》蓝皮书显示,未来我国核能发电占比将持续提高,核能的多用途应……卡萨诺C罗已经到退役的时候了继续下去只能甘当替补北京时间9月28日傍晚,根据《镜报》报道,意大利名宿卡萨诺不相信C罗目前仍然能保持每年都赢得奖杯时的水平,也许是时候放弃了。在C罗2009年加盟皇马之前,卡萨诺曾为皇马效……RNG负债太多,已经卖不掉了,转会期爆料开始,Uzi迎来好消各位LPL的观众和英雄联盟召唤师大家好,这里是天下游戏汇。S12全球总决赛还没有结束,但现在各大赛区已经开始准备转会期的工作了,那些已经被淘汰的队伍肯定是要想好转会期策略……
孟美岐最新近照大变样,疑似整牙嘴巴微凸,舞蹈功底退步自嘲手残59岁李连杰公开无遮挡正面照,胡子花白难掩老态,眼球突出显疲每年都有,但今年追潮儿已风起云涌金地集团前10个月签约额1833。9亿元从未获得世界杯冠军,比利时队是如何排名世界前茅的?原神为600原石卷也就卷了,4个矿不至于吧?但满命优菈是真不海信电视机全国售后服务维修电话2022已更新(今日更新)国际乒联第43周排名,王楚钦升至第七创个人新高餐餐吃好,疾病不扰,营养专家来支招孩子过敏是怎么回事?出现这些症状,家长要警惕顺位暴跌!跌出乐透!模板韦德,全美第一后卫,首发都快打不上了蔡斌有多难?女排三大核心缺席世锦赛,七仙女仅剩下两人主打

友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找七猫云易事利