Java教程:dubbo源码解析-SPI机制

Dubbo是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成。

Dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。

dubbo运行架构如下图示

节点角色说明

调用关系说明

  1. 服务容器负责启动,加载,运行服务提供者。
  2. 服务提供者在启动时,向注册中心注册自己提供的服务。
  3. 服务消费者在启动时,向注册中心订阅自己所需的服务。
  4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
  6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

关于dubbo 的特点分别有连通性、健壮性、伸缩性、以及向未来架构的升级性。特点的详细介绍也可以参考官方文档。

接下来逐步对dubbo各个模块的源码以及原理进行解析,目前dubbo框架已经交由Apache基金会进行孵化,被在github开源。

Dubbo 社区目前主力维护的有 2.6.x 和 2.7.x 两大版本,其中,

  • 2.6.x 主要以 bugfix 和少量 enhancements 为主,因此能完全保证稳定性
  • 2.7.x 作为社区的主要开发版本,得到持续更新并增加了大量新 feature 和优化,同时也带来了一些稳定性挑战

通过以下的这个命令签出最新的dubbo项目源码,并导入到IDEA中

可以看到Dubbo被拆分成很多的Maven项目,在后续课程中会介绍左边每个模块的大致作用。

在本次课程中,不仅讲解dubbo源码还会涉及到相关的基础知识,为了方便学员快速理解并掌握各个内容,已经准备好了相关工程,只需导入到IDEA中即可。对于工程中代码的具体作用,在后续课程会依次讲解

(1) 安装zookeeper

(2) 修改官网案例,配置zookeeper地址

(3) 启动服务提供者,启动服务消费者

通过如下图形可以大致的了解到,dubbo源码各个模块的相关作用:

模块说明:

  • dubbo-common 公共逻辑模块:包括 Util 类和通用模型。
  • dubbo-remoting 远程通讯模块:相当于 Dubbo 协议的实现,如果 RPC 用 RMI协议则不需要使用此包。
  • dubbo-rpc 远程调用模块:抽象各种协议,以及动态代理,只包含一对一的调用,不关心集群的管理。
  • dubbo-cluster 集群模块:将多个服务提供方伪装为一个提供方,包括:负载均衡, 容错,路由等,集群的地址列表可以是静态配置的,也可以是由注册中心下发。
  • dubbo-registry 注册中心模块:基于注册中心下发地址的集群方式,以及对各种注册中心的抽象。
  • dubbo-monitor 监控模块:统计服务调用次数,调用时间的,调用链跟踪的服务。
  • dubbo-config 配置模块:是 Dubbo 对外的 API,用户通过 Config 使用Dubbo,隐藏 Dubbo 所有细节。
  • dubbo-container 容器模块:是一个 Standlone 的容器,以简单的 Main 加载 Spring 启动,因为服务通常不需要 Tomcat/JBoss 等 Web 容器的特性,没必要用 Web 容器去加载服务。

图例说明:

  • 图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。
  • 图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。
  • 图中绿色小块的为扩展接口,蓝色小块为实现类,图中只显示用于关联各层的实现类。
  • 图中蓝色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调时链,紫色三角箭头为继承,可以把子类看作父类的同一个节点,线上的文字为调用的方法。
  • config 配置层:对外配置接口,以 ServiceConfig, ReferenceConfig 为中心,可以直接初始化配置类,也可以通过 spring 解析配置生成配置类
  • proxy 服务代理层:服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton, 以 ServiceProxy 为中心,扩展接口为 ProxyFactory
  • registry 注册中心层:封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactory, Registry, RegistryService
  • cluster 路由层:封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 Cluster, Directory, Router, LoadBalance
  • monitor 监控层:RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory, Monitor, MonitorService
  • protocol 远程调用层:封装 RPC 调用,以 Invocation, Result 为中心,扩展接口为 Protocol, Invoker, Exporter
  • exchange 信息交换层:封装请求响应模式,同步转异步,以 Request, Response 为中心,扩展接口为 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer
  • transport 网络传输层:抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel, Transporter, Client, Server, Codec
  • serialize 数据序列化层:可复用的一些工具,扩展接口为 Serialization, ObjectInput, ObjectOutput, ThreadPool

在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。如果大家想要学习 Dubbo 的源码,SPI 机制务必弄懂。接下来,我们先来了解一下 Java SPI 与 Dubbo SPI 的用法,然后再来分析 Dubbo SPI 的源码。

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。

Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。

首先,我们定义一个接口,名称为 Robot。

接下来定义两个实现类,分别为 OptimusPrime 和 Bumblebee。

接下来 META-INF/services 文件夹下创建一个文件,名称为 Robot 的全限定名 com.itheima.java.spi.Robot。文件内容为实现类的全限定的类名,如下:

做好所需的准备工作,接下来编写代码进行测试。

最后来看一下测试结果,如下:

从测试结果可以看出,我们的两个实现类被成功的加载,并输出了相应的内容。

调用过程

  • 应用程序调用ServiceLoader.load方法,创建一个新的ServiceLoader,并实例化该类中的成员变量
  • 应用程序通过迭代器接口获取对象实例,ServiceLoader先判断成员变量providers对象中(LinkedHashMap<String,S>类型)是否有缓存实例对象,如果有缓存,直接返回。 如果没有缓存,执行类的装载,

优点

使用 Java SPI 机制的优势是实现解耦,使得接口的定义与具体业务实现分离,而不是耦合在一起。应用进程可以根据实际业务情况启用或替换具体组件。

缺点

  • 不能按需加载。虽然 ServiceLoader 做了延迟载入,但是基本只能通过遍历全部获取,也就是接口的实现类得全部载入并实例化一遍。如果你并不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
  • 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
  • 多个并发多线程使用 ServiceLoader 类的实例是不安全的。
  • 加载不到实现类时抛出并不是真正原因的异常,错误很难定位。

Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。

与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。下面来演示 Dubbo SPI 的用法:

Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,配置内容如下。

在使用Dubbo SPI 时,需要在接口上标注 @SPI 注解。

通过 ExtensionLoader,我们可以加载指定的实现类,下面来演示 Dubbo SPI :

测试结果如下:

Dubbo SPI 除了支持按需加载接口实现类,还增加了 IOC 和 AOP 等特性,这些特性将会在接下来的源码分析章节中一一进行介绍。

上一章简单演示了 Dubbo SPI 的使用方法,首先通过 ExtensionLoader 的 getExtensionLoader 方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象。下面我们从 ExtensionLoader 的 getExtension 方法作为入口,对拓展类对象的获取过程进行详细的分析。

上面代码的逻辑比较简单,首先检查缓存,缓存未命中则创建拓展对象。下面我们来看一下创建拓展对象的过程是怎样的。

createExtension 方法的逻辑稍复杂一下,包含了如下的步骤:

  1. 通过 getExtensionClasses 获取所有的拓展类
  2. 通过反射创建拓展对象
  3. 向拓展对象中注入依赖
  4. 将拓展对象包裹在相应的 Wrapper 对象中

以上步骤中,第一个步骤是加载拓展类的关键,第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现。由于此类设计源码较多,这里简单的总结下ExtensionLoader整个执行逻辑:

Dubbo IOC 是通过 setter 方法注入依赖。Dubbo 首先会通过反射获取到实例的所有方法,然后再遍历方法列表,检测方法名是否具有 setter 方法特征。若有,则通过 ObjectFactory 获取依赖对象,最后通过反射调用 setter 方法将依赖设置到目标对象中。整个过程对应的代码如下:

在上面代码中,objectFactory 变量的类型为 AdaptiveExtensionFactory,AdaptiveExtensionFactory 内部维护了一个 ExtensionFactory 列表,用于存储其他类型的 ExtensionFactory。Dubbo 目前提供了两种 ExtensionFactory,分别是 SpiExtensionFactory 和 SpringExtensionFactory。前者用于创建自适应的拓展,后者是用于从 Spring 的 IOC 容器中获取所需的拓展。这两个类的类的代码不是很复杂,这里就不一一分析了。

Dubbo IOC 目前仅支持 setter 方式注入,总的来说,逻辑比较简单易懂。

在用Spring的时候,我们经常会用到AOP功能。在目标类的方法前后插入其他逻辑。比如通常使用Spring AOP来实现日志,监控和鉴权等功能。 Dubbo的扩展机制,是否也支持类似的功能呢?答案是yes。在Dubbo中,有一种特殊的类,被称为Wrapper类。通过装饰者模式,使用包装类包装原始的扩展点实例。在原始扩展点实现前后插入其他逻辑,实现AOP功能。

装饰者模式:在不改变原类文件以及不使用继承的情况下,动态地将责任附加到对象上,从而实现动态拓展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。

一般来说装饰者模式有下面几个参与者:

  • Component:装饰者和被装饰者共同的父类,是一个接口或者抽象类,用来定义基本行为
  • ConcreteComponent:定义具体对象,即被装饰者
  • Decorator:抽象装饰者,继承自Component,从外类来扩展ConcreteComponent。对于ConcreteComponent来说,不需要知道Decorator的存在,Decorator是一个接口或抽象类
  • ConcreteDecorator:具体装饰者,用于扩展ConcreteComponent

注:装饰者和被装饰者对象有相同的超类型,因为装饰者和被装饰者必须是一样的类型,这里利用继承是为了达到类型匹配,而不是利用继承获得行为。

Dubbo AOP 是通过装饰者模式完成的,接下来通过一个简单的案例来学习dubbo中AOP的实现方式。

首先定义一个接口

定义接口的实现类,也就是被装饰者

为了简单,这里省略了装饰者接口。仅仅定义一个装饰者,实现phone接口,内部配置增强逻辑方法

添加拓展点配置文件META-INF/dubbo/com.itheima.dubbo.Phone,内容如下

配置测试方法

具体执行效果如下

先调用装饰者增强,再调用目标方法完成业务逻辑。

通过测试案例,可以看到在Dubbo SPI中具有增强AOP的功能,我们只需要关注dubbo源码中这样一行代码就够了

我们知道在 Dubbo 中,很多拓展都是通过 SPI 机制 进行加载的,比如 Protocol、Cluster、LoadBalance、ProxyFactory 等。有时,有些拓展并不想在框架启动阶段被加载,而是希望在拓展方法被调用时,根据运行时参数进行加载,即根据参数动态加载实现类。如下所示:

这种在运行时,根据方法参数才动态决定使用具体的拓展,在dubbo中就叫做扩展点自适应实例。其实是一个扩展点的代理,将扩展的选择从Dubbo启动时,延迟到RPC调用时。Dubbo中每一个扩展点都有一个自适应类,如果没有显式提供,Dubbo会自动为我们创建一个,默认使用Javaassist。

自适应拓展机制的实现逻辑是这样的

  1. 首先 Dubbo 会为拓展接口生成具有代理功能的代码;
  2. 通过 javassist 或 jdk 编译这段代码,得到 Class 类;
  3. 通过反射创建代理类;
  4. 在代理类中,通过URL对象的参数来确定到底调用哪个实现类;

Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。为了方便更好的理解dubbo中的自适应,这里通过案例的形式来熟悉下Javassist的基本使用

通过以上代码,我们可以知道使用javassist可以方便的在运行时,按需动态的创建java对象,并执行内部方法。而这也是dubbo中动态编译的核心

在开始之前,我们有必要先看一下与自适应拓展息息相关的一个注解,即 Adaptive 注解。

从上面的代码中可知,Adaptive 可注解在类或方法上。

  • 标注在类上:Dubbo 不会为该类生成代理类。
  • 标注在方法上:Dubbo 则会为该方法生成代理逻辑,表示当前方法需要根据 参数URL 调用对应的扩展点实现。

dubbo中每一个扩展点都有一个自适应类,如果没有显式提供,Dubbo会自动为我们创建一个,默认使用Javaassist。 先来看下创建自适应扩展类的代码:

继续看createAdaptiveExtension方法

继续看getAdaptiveExtensionClass方法

继续看createAdaptiveExtensionClass方法,绕了一大圈,终于来到了具体的实现了。看这个createAdaptiveExtensionClass方法,它首先会生成自适应类的Java源码,然后再将源码编译成Java的字节码,加载到JVM中。

Compiler的代码,默认实现是javassist。

createAdaptiveExtensionClassCode()方法中使用一个StringBuilder来构建自适应类的Java源码。方法实现比较长,这里就不贴代码了。这种生成字节码的方式也挺有意思的,先生成Java源代码,然后编译,加载到jvm中。通过这种方式,可以更好的控制生成的Java类。而且这样也不用care各个字节码生成框架的api等。因为xxx.java文件是Java通用的,也是我们最熟悉的。只是代码的可读性不强,需要一点一点构建xx.java的内容。

不知道什么是Java注解?莫慌,十分钟一篇文章就能深度学习

不知道什么是Java注解?莫慌,十分钟一篇文章就能深度学习!

注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。

  • 编写文档: 通过代码里标识的元数据生成文档【生成文档doc文档】
  • 代码分析: 通过代码里标识的元数据对代码进行分析【使用反射】
  • 编译检查: 通过代码里标识的元数据让编译器能够实现基本的编译检查【Override等】

编写文档

首先,我们要知道Java中是有三种注释的,分别为单行注释、多行注释和文档注释。而文档注释中,也有@开头的元注解,这就是基于文档注释的注解。我们可以使用javadoc命令来生成doc文档,此时我们文档的内元注解也会生成对应的文档内容。这就是编写文档的作用。

代码分析

我们频繁使用之一,也是包括使用反射来通过代码里标识的元数据对代码进行分析的,此内容我们在后续展开讲解。

编译检查

至于在编译期间在代码中标识的注解,可以用来做特定的编译检查,它可以在编译期间就检查出“你是否按规定办事”,如果不按照注解规定办事的话,就会在编译期间飘红报错,并予以提示信息。可以就可以为我们代码提供了一种规范制约,避免我们后续在代码中处理太多的代码以及功能的规范。比如,@Override注解是在我们覆盖父类(父接口)方法时出现的,这证明我们覆盖方法是继承于父类(父接口)的方法,如果该方法稍加改变就会报错;@FunctionInterface注解是在编译期检查是否是函数式接口的,如果不遵循它的规范,同样也会报错。

  • @Override: 标记在成员方法上,用于标识当前方法是重写父类(父接口)方法,编译器在对该方法进行编译时会检查是否符合重写规则,如果不符合,编译报错。
  • @Deprecated: 用于标记当前类、成员变量、成员方法或者构造方法过时如果开发者调用了被标记为过时的方法,编译器在编译期进行警告。
  • @SuppressWarnings: 压制警告注解,可放置在类和方法上,该注解的作用是阻止编译器发出某些警告信息。

标记在成员方法上,用于标识当前方法是重写父类(父接口)方法,编译器在对该方法进行编译时会检查是否符合重写规则,如果不符合,编译报错。

这里解释一下@Override注解,在我们的Object基类中有一个方法是toString方法,我们通常在实体类中去重写此方法来达到打印对象信息的效果,这时候也会发现重写的toString方法上方就有一个@Override注解。如下所示:

于是,我们试图去改变重写后的toString方法名称,将方法名改为toStrings。你会发现在编译期就报错了!如下所示:

那么这说明什么呢?这就说明该方法不是我们重写其父类(Object)的方法。这就是@Override注解的作用。

用于标记当前类、成员变量、成员方法或者构造方法过时如果开发者调用了被标记为过时的方法,编译器在编译期进行警告。

我们解释@Deprecated注解就需要模拟一种场景了。假设我们公司的产品,目前是V1.0版本,它为用户提供了show1方法的功能。这时候我们为产品的show1方法的功能又进行了扩展,打算发布V2.0版本。但是,我们V1.0版本的产品需要抛弃吗?也就是说我们V1.0的产品功能还继续让用户使用吗?答案肯定是不能抛弃的,因为有一部分用户是一直用V1.0版本的。如果抛弃了该版本会损失很多的用户量,所以我们不能抛弃该版本。这时候,我们对功能进行了扩展后,发布了V2.0版本,我们给予用户的通知就可以了,也就是告知用户我们在V2.0版本中为功能进行了扩展。可以让用户自行选择版本。

但是,除了发布告知用户版本情况之外,我们还需要在原来版本的功能上给予提示,在上面的模拟场景中我们需要在show1方法上方加@Deprecated注解给予提示。通过这种方式也告知用户“这是旧版本时候的功能了,我们不建议再继续使用旧版本的功能”,这句话的意思也就正是给用户做了提示。用户也会这么想“奥,这版本的这个功能不好用了,肯定有新版本,又更好用的功能。我要去官网查一下下载新版本”,还会有用户这么想“我明白了,又更新出更好的功能了,但是这个版本的功能我已经够用了,不需要重新下载新版本了”。

那么我们怎么查看我上述所说的在功能上给予的提示呢?这时候我需要去创建一个方法,然后去调用show1方法,并查看调用时它是如何提示的。

图已经贴出来了,你是否发现的新旧版本功能的异同点呢?很明显,该方法中的提示是在调用的方法名上加了一道横线把该方法划掉了。这就体现了show1方法过时了,已经不建议使用了,我们为你提供了更好的。

回想起来,在我们的api中也会有方法是过时的,比如我们的Date日期类中的方法有很多都已经过时了。如下图:

如你所见,是不是有很多方法都过时了呢?那它的方法上是加了@Deprecated注解吗?来跟着我的脚步,我带你们看一下。

我们已经知道的Date类中的这些方法已经是过时的了,如果我们使用该方法并执行该程序的话。执行的过程中就会提示该方法已过时的内容,但是只是提示,并不影响你使用该方法。如下:

OK!这也就是@Deprecated注解的作用了。

压制警告注解,可放置在类和方法上,该注解的作用是阻止编译器发出某些警告信息,该注解为单值注解,只有 一个value参数,该参数为字符串数组类型,参数值常用的有如下几个。

  • unchecked:未检查的转化,如集合没有指定类型还添加元素
  • unused:未使用的变量
  • resource:有泛型未指定类型
  • path:该类路径,原文件路径中有不存在的路径
  • deprecation:使用了某些不赞成使用的类和方法
  • fallthrough:switch语句执行到底没有break关键字
  • rawtypes:没有写泛型,比如: List list = new ArrayList();
  • all:全部类型的警告

压制警告注解,顾名思义就是压制警告的出现。我们都知道,在Java代码的编写过程中,是有很多黄色警告出现的。但是我不知道你的导师是否教过你,程序员只需要处理红色的error,不需要理会黄色的warning。如果你的导师说过此问题,那是有原因的。因为在你学习阶段,我们认清处理红色的error即可,这样可以减轻你学习阶段在脑部的记忆内容。如果你刚刚加入学习Java的队列中,需要大脑记忆的东西就有太多了,也就是我们目前不需要额外记忆其他的东西,只记忆重点即可。至于黄色warning嘛,在你的学习过程中慢慢就会有所了解的,而不是死记硬背的。

那为了解释@SuppressWarnings注解,我们还使用上一个例子,因为在那个例子中就有黄色的warning出现。

而每一个黄色的warning都会有警告信息的。比如,这一个图中的警告信息,就告知你show2()方法没有被使用,简单来说,你创建的show2方法,但是你在代码中并没有调用过此方法。以后你便会遇到各种各样黄色的warning。然后, 我们就可以使用不同的注解参数来压制不同的注解。但是在该注解的参数中,提供了一个all参数可以压制全部类型的警告。而这个注解是需要加到类的上方,并赋予all参数,即可压制所有警告。如下:

我们加入注解并赋予all参数后,你会发现use方法和show2方法的警告没有了,实际上导Date包的警告还在,因为我们Date包导入到了该类中,但是我们并没有创建Date对象,也就是并没有写入Date在代码中,你也会发现那一行是灰色的,也就证明了我们没有去使用导入这个包的任何信息的说法,出现这种情况我们就需要把这个没有用的导包内容删除掉,使用Ctrl + X删除导入没有用到的包即可。还有一种办法就是在包的上方修饰压制警告注解,但是我认为在一个没有用的包上加压制注解是毫无意义的,所以,我们直接删除就好。

然后,我们还见到上图,注解那一行出现了警告信息提示。这一行的意思是冗余的警告压制。这就是说我们压制以下的警告并没有什么意义而造成的冗余,但是如果我们使用了该类并做了点什么的话,压制注解的冗余警告就会消失,毕竟我们使用了该类,此时就不会早场冗余了。

上述解释@SuppressWarnings注解也差不多就这些了。OK,继续向下看吧。持续为大家讲解。

@Repeatable 表明标记的注解可以多次应用于相同的声明或类型,此注解由Java8版本引入。我们知道注解是不能重复定义的,其实该注解就是一个语法糖,它可以重复多次使用,更适用于我们的特殊场景。

首先,我们先创建一个可以重复使用的注解。

你会发现注解要求传入的只是一个类对象,此类对象就需要传入另外一个注解,这里也就是另外一个注解容器的类对象。我们去创建一下。

其实,这两个注解的套用,就是将一个普通的注解封装了一个可重复使用的注解,来达到注解的复用性。最后,我们创建一下测试类,随后带你去看一下源码。

测试类,是一个工人测试类,该工人使用注解记录早中晚的工作时间。测试结果如下:

然后我们进入到源码一探究竟。

我们发现进入到源码后,就只看见一个返回值为类对象的抽象方法。这也就验证了该注解只是一个可实现重复性注解的语法糖而已。

注解可以根据注解参数分为三大类:

  • 标记注解: 没有参数的注解,仅用自身的存在与否为程序提供信息,如@Override注解,该注解没有参数,用于表示当前方法为重写方法。
  • 单值注解: 只有一个参数的注解,如果该参数的名字为value,那么可以省略参数名,如 @SuppressWarnings(value = “all”),可以简写为@SuppressWarnings(“all”)。
  • 完整注解: 有多个参数的注解。

说到@Override注解是一个标记注解,那我们进入到该注解的源码查看一下。从上往下看该注解源码,发现它继承了导入了java.lang.annotation.*,也就是有使用到该包的内容。然后下面就又是两个看不懂的注解,其实发现注解的定义格式是public修饰的@Interface,最终看到该注解中方法体并没有任何参数,也就是只起到标记作用。

在上面我们用到的@SuppressWarnings注解就是一个单值注解。那我们进入到它的源码看一下是怎么个情况。其实,和标记注解比较,它就多一个value参数而已,而这就是单值注解的必要条件,即只有一个参数。并且这一个参数为value时,我们可以省略value。

上述两个类型注解讲解完,至于完整注解嘛,这下就能更明白了。其中的方法体就是有多个参数而已。

格式: public @Interface 注解名 {属性列表/无属性}

注意: 如果注解体中无任何属性,其本质就是标记注解。但是与其标注注解还少了上边修饰的元注解。

如下,这就是一个注解。但是它与jdk自定义注解有点区别,jdk自定义注解的上方还有注解来修饰该注解,而那注解就叫做元注解。元注解我会在后面详细的说到。

这里我们的确不知道@Interface是什么,那我们就把自定义的这个注解反编译一下,看一下反编译信息。反编译操作如下:

反编译后的反编译内容如下:

首先,看过反编译内容后,我们可以直观的得知它是一个接口,因为它的public修饰符后面的关键字是interface。

其次,我们发现MyAnno这个接口是继承了java.lang.annotation包下的Annotation接口。

所以,我们可以得知注解的本质就是一个接口,该接口默认继承了Annotation接口。

既然,是继承的Annotation接口,那我们就去进入到这个接口中,看它定义了什么。以下是我抽取出来的接口内容。我们发现它看似很常见,其实它们不是很常用,作为了解即可。

最后,我们的注解中也是可以写有属性的,它的属性不同于普通的属性,它的属性是抽象方法。既然注解也是一个接口,那么我们可以说接口体中可以定义什么,它同样也可以定义,而它的修饰符与接口一样,也是默认被public abstract修饰。

而注解体中的属性也是有要求的。其属性要求如下:

属性的返回值类型必须是以下几种:

  • 基本数据类型
  • String类型
  • 枚举类型
  • 注解
  • 以上类型的数组
  • 注意: 在这里不能有void的无返回值类型和以上类型以外的类型

定义的属性,在使用时需要给注解中的属性赋值

  • 如果定义属性时,使用default关键字给属性默认初始化值,则使用注解时可以不为属性赋值,它取的是默认值。如果为它再次传入值,那么就发生了对原值的覆盖。
  • 如果只有一个属性需要赋值,并且属性的名称为value,则赋值时value可以省略,可以直接定义值
  • 数组赋值时,值使用{}存储值。如果数组中只有一个值,则可以省略{}。

属性返回值既然有以上几种,那么我就在这里写出这几种演示一下是如何写的。

首先,定义一个枚举类和另外一个注解备用。

其次,我们来定义上述几种类型,如下:

这里我们演示一下,首先,我们使用该注解来进行演示。

随后创建一个测试类,在类的上方写上注解,你会发现,注解的参数中会让你写这两个参数(int、String)。

此时,传参是这样来做的。格式为:名称 = 返回值类型参数。如下:

上述所说,如果使用default关键字给属性默认初始化值,就不需要为其参数赋值,如果赋值的话,就把默认初始化的值覆盖掉了。

当然还有一个规则,如果只有一个属性需要赋值,并且属性的名称为value,则赋值时value可以省略,可以直接定义值。那么,我们的num已经有了默认值,就可以不为它传值。我们发现,注解中定义的属性就剩下了一个value属性值,那么我们就可以来演示这个规则了。

这里,我并没有写属性名称value,而是直接为value赋值。如果我将num的default关键字修饰去掉呢,那意思也就是说在使用该注解时必须为num赋值,这样可以省略value吗?那我们看一下。

结果,就是我们所想的,它报错了,必须让我们给num赋值。其实想想这个规则也是很容易懂的,定义一个为value的值,就可以省略其value名称。如果定义多个值,它们可以省略名称就无法区分定义的是哪个值了,关键是还有数组,数组内定义的是多个值呢,对吧。

这里我们演示一下,上述的多种返回值类型是如何赋值的。这里我们定义这几个参数来看一下,是如何为属性赋值的。

num是一个int基本数据类型,即num = 1

value是一个String类型,即value = \”str\”

lamp是一个枚举类型,即lamp = Lamp.RED

myAnno2是一个注解类型,即myAnno2 = @MyAnno2

values是一个String类型数组,即values = {\”s1\”, \”s2\”, \”s3\”}

values是一个String类型数组,其数组中只有一个值,即values = \”s4\”

注意: 值与值之间是,隔开的;数组是用{}来存储值的,如果数组中只有一个值可以省略{};枚举类型是枚举名.枚举值

元注解就是用来描述注解的注解。一般使用元注解来限制自定义注解的使用范围、生命周期等等。

而在jdk的中java.lang.annotation包中定义了四个元注解,如下:

@Target 指定被修饰的注解的作用范围。其作用范围可以在源码中找到参数值。

由此可见,该注解体内只有一个value属性值,但是它的类型是一个ElementType数组。那我们进入到这个数组中继续查看。

进入到该数组中,你会发现它是一个枚举类,其中定义了上述表格中的各个属性。

了解了@Target的作用和属性值后,我们来使用一下该注解。首先,我们要先用该注解来修饰一个自定义注解,定义该注解的指定作用在类上。如下:

而你观察如下测试类,我们把注解作用在类上时是没有错误的。而当我们的注解作用在其他地方就会报错。这也就说明了,我们@Target的属性起了作用。

注意: 如果我们定义多个作用范围时,也是可以省略该参数名称了,因为该类型是一个数组,虽然能省略名称但是,我们还需要用{}来存储。

@Retention 指定了被修饰的注解的生命周期

注意: 我们常用的定义即是RetentionPolicy.RUNTIME,因为我们使用反射来实现的时候是需要从JVM中获取class类对象并操作类对象的。

首先,我们要了解反射的三个生命周期阶段,这部分内容我在Java反射机制中也是做了非常详细的说明,有兴趣的小伙伴可以去看看我写的Java反射机制,相信你在其中也会有所收获。

这里我再次强调一下这三个生命周期是源码阶段 – > class类对象阶段 – > Runtime运行时阶段。

那我们进入到源码,看看@Retention注解中是否有这些参数。

我们看到该注解中的属性只有一个value,而它的类型是一个RetentionPolicy类型,我们进入到该类型中看看有什么参数,是否与表格中的参数相同呢?

至于该注解怎么使用,其实是相同的,用法如下:

这就证明了我们的注解可以保留到Runtime运行阶段,而我们在反射中大多数是定义到Runtime运行时阶段的,因为我们需要从JVM中获取class类对象并操作类对象。

@Documented 指定了被修饰的注解是可以Javadoc等工具文档化

@Documented注解是比较好理解的,它是一个标记注解。被该标记注解标记的注解,生成doc文档时,注解是可以被加载到文档中显示的。

还拿api中过时的Date中的方法来说,在api中显示Date中的getYear方法是这样的。

正如你看到的,注解在api中显示了出来,证明该注解是@Documented注解修饰并文档化的。那我们就看看这个注解是否被@Documented修饰吧。

然后,我们发现该注解的确是被文档化了。所以在api中才会显示该注解的。如果不信,你可以自己使用javadoc命令来生成一下doc文档,看看被该注解修饰的注解是否存在。

@Inherited 指定了被修饰的注解修饰程序元素的时候是可以被子类继承的

首先进入到源码中,我们也可以清楚的知道,该注解也是一个标记注解。而且它也是被文档化的注解。

其次,我们去在自定义注解中,标注上@Inherited注解。

演示@Inherited注解,我需要创建两个类,同时两个类中有一层的继承关系。如下:

我们在Person类中标记了@MyAnno注解,由于该注解被@Inherited注解修饰,我们就可以得出继承于Person类的Student类也同样被@MyAnno注解标记了,如果你要获取该注解的值的话,肯定获取的也是父类上注解值的那个\”1\”。

自定义注解

Cat

准备好,上述代码后,我们就可以开始编写使用反射技术来解析注解的测试类。如下:

首先,我们先通过反射来获取注解中的methodName和className参数。

此时的打印结果证明我们已经成功获取到了该注解的两个参数。

注意: 获取类对象中的注解对象时,其原理实际上是在内存中生成了一个注解接口的子类实现对象并返回的字符串内容。如下:

继续编写我们后面的代码,代码完整版如下:

完整版代码

执行结果

执行后成功的调用了eat方法,并打印了猫吃鱼的结果,如下:

首先,我们在使用JDBC的时候是需要通过properties文件来获取配置JDBC的配置信息的,这次我们通过自定义注解来获取配置信息。其实使用注解并没有用配置文件好,但是我们需要了解这是怎么做的,获取方法也是鱼使用反射机制解析注解,所谓“万变不离其宗”,它就是这样的。

自定义注解

数据库连接工具类

为了代码的健全我也在里面加了properties文件获取连接的方式。

测试类

测试结果

为了证明获取的连接是由注解的配置信息获取到的连接,我将properties文件中的所有配置信息删除后测试的。

我不清楚小伙伴们是否了解,Junit单元测试。@Test是单元测试的测试方法上方修饰的注解。此注解的核心原理也是由反射来实现的。

作者:Ziph

原文链接:https://blog.csdn.net/weixin_44170221/article/details/106590823

Java注解最全详解(超级详细)

Java注解是一个很重要的知识点,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。

掌握好Java注解有利于学习框架底层实现。@mikechen

Java注解又称Java标注,是在 JDK5 时引入的新特性,注解(也被称为元数据)。

Java注解它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。

Java注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。

1.生成文档这是最常见的,也是java 最早提供的注解;

2.在编译时进行格式检查,如@Override放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出;

3.跟踪代码依赖性,实现替代配置文件功能,比较常见的是spring 2.5 开始的基于注解配置,作用就是减少配置;

4.在反射的 Class, Method, Field 等函数中,有许多于 Annotation 相关的接口,可以在反射中解析并使用 Annotation。

包括@Override、@Deprecated、@SuppressWarnings等,使用这些注解后编译器就会进行检查。

元注解是用于定义注解的注解,包括@Retention、@Target、@Inherited、@Documented、@Repeatable 等。元注解也是Java自带的标准注解,只不过用于修饰注解,比较特殊。

用户可以根据自己的需求定义注解。

JDK 中内置了以解:

如果试图使用 @Override 标记一个实际上并没有覆写父类的方法时,java 编译器会告警。

@SuppressWarnings 用于关闭对类、方法、成员编译时产生的特定警告。

1)抑制单类型的警告

2)抑制多类型的警告

3)抑制所有类型的警告

@SuppressWarnings 注解的常见参数值的简单说明:

@FunctionalInterface 用于指示被修饰的接口是函数式接口,在 JDK8 引入。

函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

元注解是java API提供的,是用于修饰注解的注解,通常用在注解的定义上:

@ Retention用来定义该注解在哪一个级别可用,在源代码中(SOURCE)、类文件中(CLASS)或者运行时(RUNTIME)。

@Retention 源码:

RetentionPolicy 是一个枚举类型,它定义了被 @Retention 修饰的注解所支持的保留级别:

@Documented:生成文档信息的时候保留注解,对类作辅助说明

@Documented 示例

@Target:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)

@Target源码:

ElementType 是一个枚举类型,它定义了被 @Target 修饰的注解可以应用的范围:

@Inherited:说明子类可以继承父类中的该注解

表示自动继承注解类型。 如果注解类型声明中存在 @Inherited 元注解,则注解所修饰类的所有子类都将会继承此注解。

@Repeatable 表示注解可以重复使用。

当我们需要重复使用某个注解时,希望利用相同的注解来表现所有的形式时,我们可以借助@Repeatable注解。以 Spring @Scheduled 为例:

如果不满足于文章详解,私信【架构】获取视频详解!

本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com

点赞 0
收藏 0

文章为作者独立观点不代本网立场,未经允许不得转载。