Spring Framework之再探Core Container(中)

知秋o 2019-08-23 16:10:15
原文地址:https://segmentfault.com/a/1190000020133458

特别说明

这是一个由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表达式是无法进行返回类型检查的。基于此,我们可以很好去使用这个特性,仅需要一个supplier实例(注:无须使用T.class进行类型限定)就可以知道所得到的组件类型。即通过supplier 实例创建的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的配置。

声明:该文章系转载,转载该文章的目的在于更广泛的传递信息,并不代表本网站赞同其观点,文章内容仅供参考。

本站是一个个人学习和交流平台,网站上部分文章为网站管理员和网友从相关媒体转载而来,并不用于任何商业目的,内容为作者个人观点, 并不代表本网站赞同其观点和对其真实性负责。

我们已经尽可能的对作者和来源进行了通告,但是可能由于能力有限或疏忽,导致作者和来源有误,亦可能您并不期望您的作品在我们的网站上发布。我们为这些问题向您致歉,如果您在我站上发现此类问题,请及时联系我们,我们将根据您的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。