Spring 启动慢?提速利器 SpringFu 来了

Stella981
• 阅读 487

Spring 启动慢?提速利器 SpringFu 来了

函数是应用在Serverless世界里的一种极轻量形态,每个函数通常专注提供单一功能的服务。它们相互串联,井然有序,时而成群结队的出现,在完成既定任务后又很快的消失,此起彼伏,辉映成趣,将云的灵巧与飘逸发挥得淋漓尽致。

与此同时,扎根在中后台辛勤耕耘了十多年的Spring,早已是一位心宽体阔、中年发福了的明星大叔。倘若将他邀请到函数的舞台上,那就成了绝佳的娱乐新闻题材。其实Spring本尊也确曾出演过一部收视率不太高的“古装情景喜剧”《Spring Cloud Function 1.0》,尽管使出了反应式(Reactive)架构、云平台集成等等经典绝技,无奈臃肿的身材难以跟上轻快的舞步,被Nodejs和Python等晚辈小生调戏得窘态频现。于是,Spring立下宏志,即刻减肥,不瘦30斤,不回江湖。如今,Spring终于功成归来,不仅习得新技能Functional Bean,还为社区带来了两位新伙伴:SpringInit和SpringFu。

根据非官方实验数据,将SpringMVC应用改造为SpringFu结构,启动时间大约能缩短50%。这点提速当然不算瞩目,然而SpringFu的关键价值在于,它使Spring框架能够摆脱JVM动态特性束缚,从而适配AOT编译模式。倘若搭配上GraalVM编译器,应用启动速度就能直线下降到原先的大约1%,彻底甩掉“起跑永远慢半拍”的帽子。

一 Spring 5.0的创新

关于SpringFu的故事,还得从Spring 5.0版本说起。

纵观Spring的演化过程,Bean对象的定义方式是一条脉络清晰的主线。早期“SSH时代”的Spring框架只能使用xml文件定义Bean,写一个Java项目,一半是xml文件。即便如此,Spring 1.0依然带领着这支“尖括号加代码”大军击溃了更加笨拙繁琐的EJB框架。平定天下以后,Spring内部开始了自我进化,Spring 2.0推出了基于注解的Bean定义机制,逐步替换过去的xml文件。最终在SpringBoot项目的强力助攻下,注解全面取代xml,完成大国一统。眼看太平盛世刚刚到来,可就在最近几年里,Spring社区又刮起了一股更年轻的“去注解”风潮。

这股风潮的兴起与云原生以及GraalVM项目蓬勃发展有着不可忽视的联系。了解过AOT编译的读者应该知道,反射、动态代理等运行时特性都是妨碍Java代码进行AOT编译的首要因素,然而基于注解的Bean扫描过程恰恰大量依赖了这些Java动态特性。这就相当于说,曾经代表着先进生产力的Spring注解,现在正在成为制约Spring继续演进的束脚石。

事已至此,Spring自个也不含糊。“注解不要也罢,直接暴露接口”,2017年底,正赶在Spring 5.0新品发布会的节骨眼上,Spring当机立断的拿出了布局未来的一张牌:Functional Bean。

发展才是硬道理。

这项肩负重任的Functional Bean究竟是何神技呢?不瞒您说,其实关键的修改就是加了一个成员方法。

对于Spring系统而言,IoC容器是其中最核心的部分,在代码上就是BeanFactory和各种ApplicationContext。这当中,最功绩显赫的要数能从xml文件装载Bean的ClassPathXmlApplicationContext,和能从代码上下文扫描Bean的
AnnotationConfigApplicationContext,它俩的光辉事迹早已在网上被广泛传诵,无需多述。比较有意思的地方在于,若从继承关系来看,这两位老爷子顶多也就算得上是远房亲戚,虽然都继承自ApplicationContext,在中间隔却了好几代血缘。而与Functional Bean相关的“改进”发生在AnnotationConfigApplicationContext的父类,也就是GenericApplicationContext类型里。

Spring 启动慢?提速利器 SpringFu 来了

在Spring的历史上,GenericApplicationContext并不是一位经常抛头露面的IoC容器。据文档记载,一直到Spring 5.0之前,GenericApplicationContext的适用场景都是配合其他DefinitionReader对象完成非标准来源的Bean注册,比如从properties文件加载Bean定义。它有一个registerBeanDefinition()方法,但实际的代码实现却只是将传入的Bean定义直接转交给代管的beanFactory对象。

就是这么个低调的小副手,在Spring 5.0里忽然被赋予了如同AnnotationConfigApplicationContext一样独立加载和操控Bean的权力,就像78岁高龄的前副总统被提名大选,虽是情理之中,却也有些意料之外。新增加的方法叫做registerBean(),共有6种重载,其中前5种都是最后1种的参数简化版本,因此本质上就是一个方法,其完整定义如下:

<T> void registerBean(String beanName, Class<T> beanClass, Supplier<T> supplier, BeanDefinitionCustomizer... customizers)

一共4个参数:

  • beanName:给Bean取的名字
  • beanClass:注册Bean的类型
  • supplier:关键参数,用于生成Bean对象
  • customizers:可选参数,对生成的Bean对象进行配置

其中supplier和customizers参数所属类型都是只有一个方法的接口,在Java 8以上版本里,这种接口参数可以直接传入Lambda函数作为匿名类来使用。例如下面这个Bean定义:

context.registerBean("myService", MyService.class, 
    () -> new MyService(),   // supplier参数,定义Bean对象的创建方法
    (bean) -> bean.setLazyInit(true)   // customizers参数,配置Bean
);

用Lambda函数定义Bean,没有注解,没有反射,没有动态特性,这就是Functional Bean。

由于AnnotationConfigApplicationContext本身也是一种GenericApplicationContext,因此在代码里访问它一点也不困难。比如继承ApplicationContextInitializer接口,然后通过其提供的initialize()回调方法参数拿到GenericApplicationContext对象。或者在Spring容器里的任意地方用@Autowired注解直接拿到全局GenericApplicationContext对象等等。然而对于早已习惯了“Spring == 各种注解”的现有开发者来说,相比写@Component、@Configuration等等注解,要自己获取应用容器,再调用registerBean()方法来注册Bean,适应成本实在有点高。即使在最适用的Serverless函数场景下,愿意折腾的开发者早就投奔了Nodejs阵营,不愿折腾的开发者继续Spring注解将就用,这种“丑陋”的Bean注册方式即便官方博客多次宣传,在发布过后的近一年里依然几乎无人问津。

眼看不温不火的Spring Cloud Function 2.0同样难以扛起推广Functional Bean的大旗,Spring此时亟待一位像当年SpringBoot那样席卷全球的网红节目来反转自己在Serverless战线上一度低迷的票房。为此,一个崭新的项目,SpringFu出现在了大家的视线里。

二 SpringFu vs SpringMVC

SpringFu项目的发起人Deleuze先生来自法国,已经为Pivotal公司的Spring团队效力超过6年,我猜他大概是位中国迷。根据项目作者的阐述,Fu有三种含义,首先是Functional的前两个字母缩写,其次是来源于单词Kong-Fu(功夫),最后则是谐音中文的“赋”(函数声明式的定义写起来像是有节奏韵律的诗歌)。

这款项目的设计也正如其名所述,将Spring Bean的定义过程编制得如同有行云流水般的顺滑。来看个例子:

public class Application {
    public static void main (String[] args) {
        JafuApplication jafu = webApplication(app -> app.beans(def -> def
            .bean(DemoHandler.class)
            .bean(DemoService.class))
            .enable(webMvc(server -> server
                .port(server.profiles().contains("test") ? 8181 : 8080)
                .router(router -> {
                    DemoHandler handler = server.ref(DemoHandler.class);
                    router.GET("/", handler::hello)
                          .GET("/api", handler::json);
                }).converters(converter -> converter
                    .string()
                    .jackson()))));
        jafu.run(args);
    }
}

相比我们印象中Spring项目里东一个@Service西一个@Controller的松散型结构,基于SpringFu编写的代码非常紧凑,信息密度极高。事实上SpringFu的大部分核心能力依然直接来自于Spring的各个子项目,但它与SpringMVC项目用各种注解区分不同Bean的方式完全不同,SpringFu让用户显式的调用不同的注册接口来将所需的不同Bean对象注册到Spring上下文容器,整个机制完全不依赖反射和其他Java动态特性。因此只要用户自己没有故意使用Java动态语法,采用SpringFu编写的程序就能天然支持GraalVM的AOT编译,生成启动速度极快的二进制文件。其效果要比提供大量运行时信息给GraalVM编译器更简洁而显著,这也是SpringFu能带来近百倍提速的主要原因。

为了更直观的感受SpringFu这种声明式代码的独特Feeling,下面以SpringMVC的几种典型注解为线索,对比一下二者的差异。

首先是普通的Bean定义,在SpringMVC通过@Configuration类注解和@Bean方法注解来表示。

@Configuration
public class MyConfiguration {

    @Bean
    public Foo foo() {
        return new Foo();
    }

    @Bean
    public Bar bar(Foo foo) {
        return new Bar(foo);
    }
}

在SpringFu里对应的是ConfigurationDsl类型的beans方法,该方法接收一个Consumer接口对象作为参数,按照惯例,Consumer接口的实现通常采用Lambda方法来定义,因而在代码中不会显式的见到BeanDefinitionDsl的身影。

ConfigurationDsl config = beans(def -> def
    .bean(Foo.class)
    .bean(Bar.class) // 隐含使用构造函数注入其他Bean
)

在实际的代码里,一般也极少出现单独的ConfigurationDsl,它总是以Consumer的形式出现,并且隐藏在Lambda方法里面被传递给需要它的对象。

像SpringMVC中使用@Component、@Service等注解的地方,在SpringFu里的处理方法与前例是相同的,只需将原类型的注解去掉,然后通过beans()方法来注册。例如下面这两个定义:

@Component
public class XxComponent {
    // ...
}

@Service
public class YyService {
    // ...
}

在SpringFu里大致是这个样子:

public class XxComponent {
    // ...
}

public class YyService {
    // ...
}

beans(def -> def
    .bean(XxComponent.class)
    .bean(YyService.class)
)

稍有特殊的是@Controller注解(以及@RestController注解),由于它是业务请求的入口,与API的路由息息相关,因此在SpringFu中有专门的DSL类型与之对应,比如WebMvcServerDsl和WebFluxServerDsl,它们提供诸如port()、router()等方法来定义与HTTP监听相关的属性。例如这两个SpringMVC的接口:

@RestController
@RequestMapping("/api/demo")
public class MyController {

    @Autowired
    private MyService myService;

    @GetMapping("/")
    public List<Data> findAll() {
        return myService.findAll();
    }

    @GetMapping("/{id}")
    public Data findOne(@PathVariable Long id) {
        return myService.findById(id);
    }
}

在SpringFu里,通常将处理请求的入口类命名为Handler(而不是Controller,这种命名更符合函数定义的惯例),如果将上述代码“直译”为SpringFu结构,大概会长这样:

public class MyHandler {

    private MyService myService;
    public MyHandler(MyService myService) {
        this.myService = myService;
    }

    public List<Data> findAll() {
        return myService.findAll();
    }

    public Data findOne(ServerRequest request) {
        val id = request.pathVariable("id");
        return myService.findById(id);
    }
}

router(r -> {
    MyHandler handler = server.ref(MyHandler.class);
    r.GET("/", handler::findAll)
     .GET("/{id}", handler::findOne);
}

实际更符合惯例的SpringFu写法则是将MyHandler类型本身也匿名掉,这样上述代码可以进一步精简成:

router(r -> {
    MyService myService = server.ref(MyService.class);
    r.GET("/", myService::findAll)
     .GET("/{id}", request -> {
         return myService.findById(request.pathVariable("id"));
     });
}

这种流畅的声明式代码对于天生小巧的Serverless函数十分契合,尤其搭配上同样简洁的Kotlin语言时,往往仅需一个文件就能完成许多中等复杂程度函数的定义。

不过从SpringFu的业务范围来看,它的目标并非替代SpringMVC。在Spring布局的Functional Bean蓝图里,SpringFu更像是一支精锐的突击小分队,专攻以Serverless函数为典型的新型轻应用场景。对于Spring框架的传统优势领域,中后台大型服务而言,把所有Bean集中定义在一个地方毕竟过于理想。在通往Serverless的道路上,其实SpringFu并不孤单,它还有一个姐妹项目叫SpringInit,该项目立足于通过编译期代码增强,将用户编写的SpringMVC注解偷偷换成Functional Bean的方式注册,从而实现大型JVM服务的Serverless适配。而这个同样异想天开的项目作者正是大名鼎鼎的SpringBoot和SpringCloud项目创始人Dave Syer。由于篇幅所限,本文不对SpringInit项目再做展开,有兴趣的同学可移步Github围观。

三 源码解读

在设计之初,SpringFu就是奔着DSL(特定领域语言)的思路去的,项目分为JaFu(Java-Fu)和KoFu(Kotlin-Fu)两个部分,分别对应了符合Java和Kotlin语言语法的新型高效Spring服务编写方式。由于开发人手不足,其中JaFu子项目在0.1.0版本后曾暂停过一段时间,代码也从仓库里移除了,后来社区呼声强烈,在0.3.0版本里又再次复出。两者的本质原理大差不差,以下的源码分析以JaFu的最新发布版本v0.4.3作为参考。

项目结构十分简洁,一共3个模块:

autoconfigure-adapter:公共的ApplicationContextInitializer对象

jafu:SpringFu的Java DSL实现

kofu:SpringFu的Kotlin DSL实现

其中autoconfigure-adapter模块被jafu和kofu共用,它实现了许多ApplicationContextInitializer对象,这些对象用于在SpringBoot初始化过程中通过Functional Bean机制预注册某些系统的Bean。这当中,有些是为了从而加速服务的启动过程,比如ServletWebServerInitializer中注册的TomcatServletWebServerFactoryCustomizer,在这里直接注册会比让SpringBoot自己去扫描快许多;有些是为了改变服务行为,比如在JacksonJsonConverterInitializer会注册一个名称为mappingJackson2HttpMessageConverter的Bean,它会影响json对象通过HTTP接口返回时的序列化方式。总之这个模块属于针对函数场景的Spring定制优化,涉及很多Spring内部细节,我们点到为止。

在jafu模块下的源文件并不多,放在顶层目录最显眼位置的JaFu.java是用户程序的发动机,提供了进入SpringFu世界的三种入口方法application()、webApplication()和reactiveWebApplication()。它们负责创建出Spring的ApplicationContext容器,然后将其包装成一个匿名的JafuApplication对象返回,在之后在各种DSL里传递的context成员都是这个容器的引用。

几种入口的区别在于创建的ApplicationContext容器类型,application()生成的是原始的GenericApplicationContext类型容器(能够提供Functional Bean所需的registerBean()方法的最基础容器类型),而webApplication()和reactiveWebApplication()生成的是功能更丰富的ServletWebServerApplicationContext和ReactiveWebServerApplicationContext容器,它俩都来自SpringBoot项目,从这里已经可以看出,SpringFu麻雀虽小,却是站在巨人肩膀上起飞的。

在入口方法上还有一个关键细节,是创建JafuApplication对象时候要接收一个ApplicationDsl类型的参数。这个ApplicationDsl是SpringFu整套DSL机制的总指挥舱,里面琳琅满目的装载着其他所有DSL元素。

至此,SpringFu的外观轮廓已经出来了。任何SpringFu程序的最外层代码都可以概括成下面这种三部曲模式:

application(         // 构造,也可以是webApplication()或reactiveWebApplication()
    ApplicationDsl   // 配置
).run()              // 启动

第一步“构造”完成,接下来是内容最丰富的一个部分:“配置”。

从继承关系来看,ApplicationDsl是一种ConfigurationDsl,而ConfigurationDsl以及其他各种DSL元素都来自AbstractDsl。

Spring 启动慢?提速利器 SpringFu 来了

在所有类型中,孤零零的LoggingDsl是唯一没有继承AbstractDsl的漏网之鱼。作为SpringFu项目里最简单的DSL元素,去掉空行和注释,LoggingDsl类型的有效代码只有20行。不过,即便如此特立独行,在LoggingDsl身上依然保留着一项与其他DSL元素相同的特征:构造方法接收以自身类型为模板的Consumer对象作为参数。

LoggingDsl(Consumer<LoggingDsl> dsl) {
    dsl.accept(this);
}

构造函数里只有一行dsl.accept(this),这行代码在所有DSL元素里都会出现。只是在继承了AbstractDsl类型的DSL元素中,它是被放在实现initialize抽象方法的地方,而LoggingDsl类型没有继承过来的initialize方法,就直接将它摆在构造函数里了。“把自己传递给构造方法接收的Consumer对象”,关于DSL的这个神秘行为,我们在后面讲“启动”的环节里再来解释。

回到ApplicationDsl上来,这个类型只是在ConfigurationDsl的基础上,通过一个MessageSourceInitializer对象额外注册了几个Spring自用的Bean,主要功能都是直接继承自ConfigurationDsl。再看ConfigurationDsl类型,这里有几个比较常用的方法:

  • configurationProperties(Class clazz):注册属性配置类,相当于@ConfigurationProperties注解。
  • logging(Consumer dsl) : 提供函数的输出日志配置。
  • beans(Consumer dsl) :提供定义Bean的地方,相当于@Configuration注解。
  • enable(Consumer configuration) → 为其他DSL提供扩展能力的万能配置入口,比如增加Web监听。

为了符合流式声明结构的要求,这些方法都返回ConfigurationDsl类型,并且使用使用return this让下一个配置可以串联起来。然后就可以写出像下面这样的配置代码:

conf -> conf
    .beans(...)
    .logging(...)
    .enable(...);

beans()方法接收的是一个消费BeanDefinitionDsl的匿名函数,这个DSL提供bean()方法,其效果类似SpringMVC程序里的@Bean注解,但在内部会通过GenericApplicationContext的registerBean()方法直接注册Bean到IoC容器,没有反射和扫描的过程。logging()方法接收一个消费LoggingDsl的匿名函数,后者提供level()方法,可以动态调整任意包路径的输出日志级别。enable()方法需要结合其他DSL元素一起使用,其用途非常广泛,比如开头示例里的webMvc()方法会返回一个WebMvcServerDsl对象,可以配置HTTP监听、路由等属性并通过该DSL对象自动注册相关的Bean到Spring上下文。

在整个ApplicationDsl对象定义完以后,就进入到最后的一个环节“启动”了。前面提到过,application()接收一个ApplicationDsl对象,会返回一个JafuApplication对象。接下来就要调用这个返回对象里的点火器方法run()。

SpringFu的DSL是声明式的,开发者通过ApplicationDsl对象定义的所有配置信息,此时都还藏在ApplicationDsl自己的肚子里,真正的Spring容器里面依然空空如也。在JafuApplication的run()方法里,SpringFu创建出由SpringBoot框架封装的SpringApplication应用对象,并将传入的ApplicationDsl对象指定为该应用对象的initializer,然后调用应用对象的run()。这之后就进入了SpringBoot的剧本,SpringApplication会完成Spring运行所需的所有前序工作,然后调用所有initializer对象的initialize()方法,包括此前传入的ApplicationDsl对象。

在ApplicationDsl的initialize()方法里,首先通过super.initialize(context)调用祖父类型AbstractDsl的initialize()方法,将传入的context容器引用保留下来。然后执行dsl.accept(this)进入构造时传入的回调方法。在接下来的beans()和enable()方法里,又会显式的调用子级DSL元素的initialize()方法,从而将这个初始化过程一级一级的迭代下去。就像是层层嵌套的递归调用,直到所有子级元素都构造完毕。至此,初始化过程结束,程序返回到SpringBoot的启动流程。

从源码不难得出结论,SpringFu程序的本质就是规避了JVM动态特性的SpringBoot程序。在卸掉过去spring-boot-starter-web和spring-boot-starter-webflux沉重的外壳之后,换上了一身轻便的战袍。

看似离经叛道,实则一脉相承。

四 总结

SpringFu的到来是Spring面向Serverless时代的一次主动出击,颠覆自己,重获新生。为了让Spring在Serverless函数的舞台上也能轻盈起舞,SpringFu选择了一条前人从未走过的路,将Spring不符合AOT编译的东西统统去掉,做极简主义的减法。

事实证明,没有负担的SpringFu能够跑得更快、飞得更高。

随着云原生渐渐渗入到开发者日常的方方面面,相信在前往Serverless的旅途上,我们终将再次遇见Spring那高挑的身影,因为他早就抵达了这里,迎接着大家的到来。

原文链接

本文为阿里云原创内容,未经允许不得转载。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这