乐趣区

Spring-Framework之再探Core-Container中

特别说明

这是一个由 simviso 团队进行的关于 Spring Framework 5.2 版本内容分享的翻译文档,分享者是 Spring Framework 5.2 项目 leader。
视频第一集:https://www.bilibili.com/vide…

视频第二集:https://www.bilibili.com/vide…

视频翻译文字版权归 simviso 所有,未经授权,请勿转载

参与人员名单:

顺带推荐一个专业的程序员后端微信群的圈子:

1. GenericApplicationContext

出于多种目的,特别是在 GenericApplicationContext 这里,我们专门提供了 Kotlin 扩展。你不需要特意引入它们(Kotlin 支持),不需要额外的步骤,它们已经成为 Spring Framework 核心中的一部分。so,无论你使用的是 Spring context 5.0、5.1、5. 2 中任何版本,你都会自动获得带有 Kotlin 扩展的 GenericApplicationContext。如果你选择使用 Kotlin 来进行开发,那么它们就会被 Kotlin 编译器检测编译。

来看这个 GenericApplicationContext,它的处理方式是不是看起来很熟悉,但它是使用 Kotlin 来写的。可以看到,图中下面的和上面的版本明显有一些区别。举个例子来讲,我们撇开 Bar.class,然后来说这里该如何去创建一个 bar 实例。我们只需要拥有一个 Supplier 实例即可,so, 我们来看 registerBean 仅仅需要一个基于构造器调用的 supplier 实例,没有其他。

理由其实很简单,因为这并不是 Java 里的 Lambda 表达式,而是一个 Kotlin 函数。它实际上调用了一个由 Kotlin 实现的 registerBean 重载函数。在我们的 Kotlin 扩展中,Kotlin 扩展函数是基于一个元数据模型(T::class.java)
一个反射模型(BeanDefinitionCustomizer)进行设计的。当我们来问一个 Kotlin 函数,你会返回什么,它预先已经知道了,而此时 Java 8 lambda 表达式是无法进行返回类型检查的。基于此,我们可以很好去使用这个特性,仅需要一个 supply 实例(注:无须使用 T.class 进行类型限定)就可以知道所得到的组件类型。即通过 suplier 实例创建的 bean 的类型。

我们可以通过 Kotlin 中一些其他函数 API 来提高开发体验。下面的这个版本基本上只是使用了一点语法糖,有一点点的语法差异。这是另外一种应用 Kotlin 语言特性来实现目标 API 的方式。这不是一个正式的 Gradle 风格,只是有一点 DSL(领域专用语言)的风格。感觉有一点像 Gradle 构建工具,Kotlin 风格的 Gradle 构建工具。

so,这里通过一种不一样的风格来表达 Generic(这里指 GenericApplicationContext),但这只是 Kotlin 语言的一种变体,我们这里用它来实现我们的目的,没有什么特别的。

2. 性能调优与 GraalVM


ok,让我们开始转向另一个重要的话题。对于 Spring Framework 5,我们一直致力于不断调整并提高它的性能

当然我们也在努力改善提高开箱即用的性能。虽然和我们的目标相关,但努力的方向有点不一样。我们试图在代码库中避免一些性能很差的东西来减少不必要的开销。同时我们也有尝试为你提供带 hook 的设施,一种你可以用来调整性能的机制。如果你知道的话,你就可以根据它做出最具体的假设,这些假设无法通过通用的框架代码来实现。

在 5.2 中最明显的例子就是注解处理。最初,在 5.1 版本中,我们主要通过对现有代码进行修订,但在 5.2 中我们选择完全重新实现。遗憾的是,在 java 中注解的处理是一个相当复杂的事情。如果你之前有做过这样的事情,你也许就知道我的意思,这也是框架日常所做的绝大多数事情。在一个应用程序的代码中,你几乎不需要去写如何查找一个注解。也就是说你只需要声明注解,框架会负责对它们进行正确的查找。这个过程其实非常复杂,效率又很低。主要原因在于它们是由 Java 来实现的。我们为了做到最好,在这个方面我们尽量避免反射,避免通过代理。然而不幸的是,注解实例是通过 Java 代理实现的,主要也是基于 jdk 自身。因此,我们竭尽所能,从开始就避免使用反射。

关于 AnnotationUtils 和 AnnotatedElementUtils 这两个 API 基本上是相同的,你在使用 SpringBoot 的过程中自然会使用到它们(我们会使用注解,那就会使用到这些工具类)。你很少会亲自去使用它们,但是基本上它们对你来讲是透明的。(也就是你看不到,你只需要关心使用什么注解,不需要关心注解背后的实现)如果你使用了其他的 Spring 项目例如 Spring Integration,Spring Batch,那你就可以从这个透明特性里面得到很明显的好处。

在 Spring 5.2 中有一个名为 MergedAnnotations 的 API,它对 Spring 声明过的注解层次结构的内省非常有用。你可能感觉到 Spring 的注解模型很复杂了。这里有个元数据注解模型,你可以在此之上覆写它的属性。你不需要做很复杂的事情,你可以很轻易地通过这些选项获取到。因此我们引入了一种全新的 API,它可以使所有的内部检查变得非常简单直接,并且十分高效。

我们的精力更多放在了对于组件中可存在注解和不可存在注解的注册上面。你可以通过编程规范来告诉容器某些注解类型只能存在于特定的组件类里面。换句话讲,在这种特定的组件类和特定的组件包中,根本就不可能有这些注解类型的使用。我们不需要在这些地方去查找这些注解。在这些地方,你根本就找不到它们的。

这些假设我们很难在程序中自己通过代码来实现。正常情况下,注解可以被应用于任何地方,这是一个常识。可能由于一些规定,你需要在你或团队的代码库中服从一些约定,即特定的地方只能用一些特定的注解类型。如果你告诉我们这些约定,我们就可以在运行代码时减少这些注解产生的性能开销。对此我们已经在 5.2 中做了大量的工作,也就是通过这些信息在注解查找的时候尽量跳过它。以此来整合出一个 Java 标准的索引排列。

我们在启动的时候并没有索引,我们只有这些类文件。在运行时类索引并不指向类文件(指向的是 JVM 里面的 class 字节码)。我们只能通过两种途径对注解进行内省。如果我们在构建时想要获取额外信息的话,就可以通过像 Jandex 的索引或者是一个自定义的索引排列一样来达到目的(通过索引来 存储一些关键信息)。就好像你在其他的基础架构中所使用的索引一样。在启动时如果通过加载这样的一个索引来提取信息,这个信息可能是 Spring ApplicationContext 相关的内容,通过这个索引我们就能立马获取到这个信息。这些在我们的 BootStrap 代码中都有提供配置可进行调整。我们也会在 SpringBoot 中会对引用排列进行重新评估,尤其是这个东西它是不是已经可以被 SpringBoot 自动使用。如果在使用时,发现了一个 Jandex 索引,SpringBoot 会自动识别评估并应用它。

关于这块,接下来的路还很长,但在七月份我们会将我们这些想法放到 SpringBoot 中。最重要的是如果你使用了这些功能(索引排列支持),那么它将会是你整个架构的一个热点(很明显会大量的用到,因为解决了很多痛点)。同时,你可以对这些可用功能进行调整以避免不必要的开销。

今天我们另一个主题则是 GraalVM,它是最近比较火的一个话题。Spring 框架对它的支持已经有一段时间了(即将 Spring Boot Application 封装成一个 GraalVM Native Image 在上面运行),现在这部分仍然处于实验阶段。目前,我们在 github 上已经简单创建了一个基于 GraalVM 19GA 版本的 wiki。通过它,人们可以进行有针对性的讨论,今年晚些时候,我们也会对其进行专门的讨论。

GraalVM Native Image 它到底是什么东西?它是一个很特别的部署架构。它并不是我们常见的 JVM,两者完全不一样。它没有任何动态类加载,也没有任何动态内省。很直接的讲,它并没有你想的那么与众不同。GraalVM 同样支持反射。你只需要给 Native Image 工具提供配置文件,这样它就能预先将正确的信息内置进 Native Image。所以,在运行时你不能做任何动态反射,但是你可以将你需要用到反射的地方进行提前配置。在这个基于 GraalVM 的定制版的 Spring Framework application 中我们对 prototypes 进行了实验,得到了比索引排列更好的效果。

拿我们之前很熟悉的函数式 Bean Registration(前面 ppt 中的例子)。通过内联的 Supplier 注册 Bean 的过程可以很自然地在 GraalVM 上运行,你不需要去做什么。在这里我们需要讨论的是在 GraalVM 中,基于注解的组件模型需要进行一些额外的工作,你需要提前告诉 GraalVM 中的 Native Image,你所要操作的组件类型以及内省。

我们当下的目标基本还是为 GraalVM 做准备,我们已经避免了一些不必要的反射点,同时也重制了一些代码,例如可以自动跳过那些没用的以及对 GraalVM 没有任何意义的工作,以提升开箱即用的体验。它们其实已经在 5.1 中出现了不少,在 5.2 中尤甚。

附带说明(wiki 文档)里面也提及如何使用 GraalVM 19 早期采用版本中的 Native Image 工具。在我们 Spring Framework 5.3 下一次迭代中,主要目标在于提升开箱即用的性能体验。通过整合写开箱即用的配置和构建工具,
你可以很轻易的构建一个用于部署的基于 Spring 的 Application GraalVM Native Image。目前而言,任重道远。毋庸置疑,我们现在也不清楚这个工具未来会是怎样的,但我们已经和 Oracle 团队在 GraalVM 上紧密合作了相当长的一段时间。自从基于 Spring 的 Native Image 可以在 GraalVM 上进行部署,两者结合的优点也已经体现出来了。为了可以在 GraalVM 上运行,我们已经做了相当一些改进。在对 GraalVM 19 使用时我们给出了反馈意见,之后我们会提供更多的反馈,以期望这些反馈会体现在 GraalVM 20 上。

从前面所讲的这两种方式来看,我们可以发现具有开箱即用功能的索引排列在当下可能是更优的选择。这个是我们当前维护的 wiki 页面。这基本上也是目前现有的状态,我也在 wiki 上列出了我们的一些前进方向。

当然,我们选择将基于 Spring 的应用程序通过 Native Image 的方式进行部署的主要理由,就是 Native Image 基于一种完全不同的内存消耗模型以及更加快速的启动方式。通过使用 Native Image,我们能获取大量好处,同时也得进行一些取舍。你会牺牲一些性能,即在构建的的时候,你需要在生成 Native Image 上花很多时间。对我来说,这不是个简单的决定。如果你真的需要性能上的调优,从我的观点而言,你可以选择我们所提供的首选方案(即索引排列)。

接下来有一些建议。如果你为了性能提升去选择优化特定的应用或者架构组织,你就必须在你的代码或你的组件模型结构上做出一定的妥协。在此我们提供了一系列的透明特性来让你们获得好处,同时将它们做的尽可能透明。当然你需要去找到你所要用到的配置选项进行配置。例如,如果你使用代码的形式去注册,这样会显得很臃肿。但如果你能明确声明哪些组件你不需要,那么在启动的过程中就能减少这些组件类的加载,以此来提高性能。这需要你进行很多的微调。相应的在 Spring 5.2 中,我们提供了一些特定的设施。例如一些配置类,你可以选择将 proxyBeanMethods 设定为 false 来避免在运行时创建 CGLIB 的子类。如果你的配置类不会出现一个 Bean 方法调用另一个 Bean 方法,否则导致它们会被重定向到容器(会通过代理类进行调用)。如果你的 Bean 彼此独立,不会互相调用,那么你可以在一个特定的配置类里面设置 proxyBeanMethods 为 false 来避免在运行时生成代理子类。我们已经将上面的功能整合进了 Spring Framework5.2 和 Spring Boot 2 中。

另一个有点奇怪的建议就是,我想我更喜欢基于接口的代理。每个人都习惯使用代理来映射到目标类,也就意味着在运行时会创建 CGLIB 的代理子类。但是实际上基于良好的旧的接口代理,在启动时创建的效率更高,并且它们可以在 GraalVM 上做到开箱即用。Spring 总是在基于接口的代理和基于类的代理之间进行默认使用选择(对 Spring Boot 同样如此)。你可以使用任何一种 AOP 的对应实现,你也可以明确的指出你想要使用的代理类型。如果你的组件模型结构是使用接口代理来实现的,那么这将会对性能产生一个极大的提升。这对转向 GraalVM 有特别的意义,因为在这里我们不需要考虑 CGLIB 的配置。

退出移动版