SpringFu Solves Slow Spring Startup

By Jinji

Introduction

Functions are a lightweight form of applications in the Serverless field. Each function provides a single service. They are connected in series and order and sometimes appear in groups and pairs. After completing the established tasks, they disappear quickly. They appear as needed, fully embodying the charm of the cloud.

Spring is a “star” in the world of application development, and has been in the middle and backend for more than ten years.” If Spring is invited to the function stage, it will become excellent entertainment news. Spring was once used in the lesser-known Spring Cloud Function 1.0. The “star” showed its classic skills, including reactive architecture and cloud platform integration. However, its overweight scale could not keep up with the powerful skills. Comparing younger generations, such as Node.js and Python, was awkward, so Spring made a great ambition to “lose weight” immediately. Now, Spring has returned with new achievements. It has not only acquired the new skill of Functional Bean, but it also brings two new members to the community, SpringInit and SpringFu.

According to unofficial experimental data, transforming the SpringMVC application into the SpringFu structure can shorten the startup time by about 50%. The speed increase is insignificant, but it enables the Spring framework to get rid of the dynamic feature constraints of JVM to adapt to the AOT compilation mode. Together with the GraalVM compiler, the application startup time can plummet to about 1% of the original time, getting over the slow startup problems.

1. Innovations in Spring 5.0

The rise of this trend is closely related to the rapid development of cloud-native and GraalVM. People familiar with the AOT compilation know runtime features, such as reflection and dynamic proxy, are the primary obstacles to the AOT compilation of Java codes. However, the annotation-based Bean scanning process relies heavily on these Java dynamic features. Spring annotations, which once represented advanced productive forces, are becoming the critical stumbling block for the continuous evolution of Spring.

Then, Spring chose to give up annotations and directly expose interfaces. At the end of 2017, Spring launched Functional Bean for future developments in the product release of Spring 5.0. Development is the last word.

What is this Functional Bean with heavy responsibilities? Honestly, the key modification is to add a member method.

The IoC container is the core of the Spring system, which refers to the BeanFactory and various ApplicationContext in codes. Among them, the most remarkable two are ClassPathXmlApplicationContext and AnnotationConfigApplicationContext. The ClassPathXmlApplicationContext can load Bean from xml files, while the AnnotationConfigApplicationContext can scan Bean from the code context. Their glorious practices are widely spread across the Internet, so we will not explain further. Interestingly, from the perspective of inheritance, the two are distant relatives. Although they both inherit ApplicationContext, they have been related to each other across several generations. The Functional Bean improvements occur in the superclass of AnnotationConfigApplicationContext, which is the GenericApplicationContext type.

In Spring’s history, GenericApplicationContext is not a common IoC container. According to the document, GenericApplicationContext applied to Bean registration from non-standard sources. For example, it loaded Bean definitions from the properties file. It completed such works in conjunction with other DefinitionReader objects before Spring 5.0. It has a registerBeanDefinition() method, but the actual code implementation is only to transfer the passed Bean definition directly to the managed BeanFactory object.

However, this lesser-known container was suddenly given the power to load and control Bean as independently as AnnotationConfigApplicationContext in Spring 5.0. The newly added method is called registerBean() with six reloads. The first five are simplified versions of the last one. Therefore, it is essentially one method. Its complete definition is listed below:

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

There are four parameters:

  • beanName: the name of Bean
  • beanClass: the registration type of Bean
  • supplier: a key parameter for generating the Bean object
  • customizers: an optional parameter for configuring the generated Bean object

The types of the supplier and customizers are APIs with only one method. In Java 8 and later, such interface parameters can be directly passed to a Lambda function as anonymous classes. For example, the following is a Bean definition:

context.registerBean("myService", MyService.class, 
() -> new MyService(), // The supplier parameter for defining the creation method of the Bean object.
(bean) -> bean.setLazyInit(true) // The customizers parameter for configuring Bean object.
);

A Bean definition through Lambda function requires no annotation, reflection, or dynamic features. This is Functional Bean.

Since AnnotationConfigApplicationContext is a kind of GenericApplicationContext, it is not difficult to access it in the codes. For example, it can inherit the ApplicationContextInitializer interface and obtain the GenericApplicationContext object through the provided parameters of the initialize() callback method. It can also use the @Autowired annotation anywhere in the Spring container to obtain the global GenericApplicationContext object. However, some developers are accustomed to the idea that Spring is equivalent to various annotations. The adaptation cost is a bit high compared with writing annotations such as @Component and @Configuration. They need to obtain the application container by themselves and call the registerBean() method to register Bean. Even in the most applicable Serverless function scenario, hard-working developers have already turned to Node.js. Other developers continue to use Spring annotations. This Bean registration method drew limited attention in the first year of its release, despite many publications.

Spring Cloud Function 2.0 is also not popular, so it is difficult to promote Functional Bean. Therefore, Spring urgently needs another welcome project, such as Spring Boot, to reverse the gloomy situation in the Serverless field. For this reason, a brand-new project, SpringFu, appeared.

2. SpringFu vs. SpringMVC

As its name suggests, the design of this project compiles the definition process of Spring Bean smoothly. Let’s take a look at an example:

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);
}
}

Unlike the loose structure of the Spring project with @Service and @Controller everywhere, the code written based on SpringFu is very compact with high information density. Most of the core capabilities of SpringFu still come directly from Spring’s subprojects. However, SpringFu is a different SpringMVC project that uses various annotations to distinguish differences. SpringFu allows users to explicitly call different registration interfaces to register different required Bean objects to the Spring context container. The entire mechanism is independent of reflection and other Java dynamic features. Therefore, programs written with SpringFu can naturally support the AOT compilation of GraalVM if there is no deliberate use of Java dynamic syntax. The AOT compilation generates binary files that have extremely fast startup speeds. This process is simpler and more remarkable than providing a large amount of runtime information to the GraalVM compiler. This is the main reason why SpringFu can achieve a speed increase by nearly a hundred times.

The following uses several typical annotations of SpringMVC to compare SpringFu and SpringMVC.

Firstly, a common Bean definition is expressed through the @Configuration class annotation and @Bean method annotation in SpringMVC.

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

This method is corresponding to the beans() method of the ConfigurationDsl type in SpringFu. The beans() method receives a Consumer interface object as the parameter. By convention, the implementation of a Consumer interface is usually defined by the Lambda method. Therefore, no BeanDefinitionDsl is seen explicitly in the codes.

ConfigurationDsl config = beans(def -> def
.bean(Foo.class)
.bean(Bar.class) // Use constructors implicitly to inject other Bean.
)

In the code, a single ConfigurationDsl seldom appears. It usually appears as a Consumer and is hidden in the Lambda method to be passed to the corresponding objects.

Annotations, such as @Component and @Service, are used in SpringMVC. It is the same for SpringFu. SpringFu removes the original annotations and registers them through the beans() method. For example, the following is the definition of @Component and @Service in SpringMVC:

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

In SpringFu, they are registered as the following:

public class XxComponent {
// ...
}
public class YyService {
// ...
}
beans(def -> def
.bean(XxComponent.class)
.bean(YyService.class)
)

The @Controller annotation and @RestController annotation are slightly special. They are the entry of business requests and closely related to API routing. Therefore, there are specific corresponding Domain-Specific Language (DSL) types in SpringFu, namely, WebMvcServerDsl and WebFluxServerDsl. These two provide methods, such as port() and router(), to define attributes related to HTTP listening. The following section shows the definition of these two in SpringMVC:

@RestController
@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);
}
}

In SpringFu, the entry class that processes requests is usually named Handler instead of Controller, which is more in line with the function definition convention. If the code above is translated directly into a SpringFu structure, it looks like this:

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);
}

It is more conventional to anonymize the MyHandler type in SpringFu. So, the code above can be simplified further:

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

This smooth declarative code is very suitable for naturally small-sized Serverless functions. Together with the simple Kotlin language, one file can complete the definition of many functions with medium complexity.

However, from the business scope of SpringFu, its goal is not to replace SpringMVC. In the Functional Bean blueprint of the Spring layout, SpringFu plays the role of an elite assault team, specializing in new light application scenarios like Serverless functions. After all, for the traditional dominating fields of Spring framework and large services in the middle and backends, it is too ideal to define all the Beans in one place. SpringFu is not alone on the road to Serverless. It has a sister project called SpringInit. This project replaces the user-written SpringMVC annotations with Functional Bean registration through code enhancements during compilation. Thus, it achieves the Serverless adaptation of large JVM services. The author of this incredible project is the well-known founder of Spring Boot and SpringCloud, Dave Syer. This article will not introduce the SpringInit project, but if you are interested, please check the GitHub documents.

3. Source Code Interpretation

The project structure is simple, with three modules:

  • autoconfigure-adapter: public ApplicationContextInitializer object
  • jafu: Java DSL implementation of SpringFu
  • kofu: Kotlin DSL implementation of SpringFu

The autoconfigure-adapter module is shared by jafu and kofu. It implements many ApplicationContextInitializer objects used to pre-register Beans from some systems through the Functional Bean mechanism during Spring Boot initialization. Some of these Beans speed up the service startup process. For example, for the TomcatServletWebServerFactoryCustomizer registered in ServletWebServerInitializer, it would be much faster to register directly than to have it scanned by Spring Boot. Some of them are used to change the service behavior. For example, the JacksonJsonConverterInitializer registers a Bean named mappingJackson2HttpMessageConverter, which affects the serialization method of the JSON object when returned through the HTTP interface. In short, this module is a customized Spring optimization module for function scenarios and involves many internal Spring details. As a result, it will not be described in detail in this article.

There are not many source files in the jafu module. The JaFu.java is placed in the most prominent position in the top-level directory. It is the engine of user programs and provides three entry methods to enter the SpringFu called application(), webApplication(), and reactiveWebApplication(). They create the Spring ApplicationContext container and return the container by wrapping it into an anonymous JafuApplication object. Then, the container is referred to as context members passed in various DSLs.

The difference between these entry methods is the created container types of ApplicationContext. The application() generates the original GenericApplicationContext type container, which is the most basic container that can provide the registerBean() method required by Functional Bean. The webApplication() and reactiveWebApplication() generate more functional ServletWebServerApplicationContext and ReactiveWebServerApplicationContext containers, both of which are from the Spring Boot project. SpringFu is small, but it can do many things.

An ApplicationDsl type parameter is received when creating a JafuApplication object. This ApplicationDsl is the general command cabin of the SpringFu DSL mechanism, which is full of other DSL elements.

Now, the outline of the SpringFu has formed. The outermost code of any SpringFu program can be summarized into the following trilogy mode:

application(         // Writing. The method can also be webApplication() or reactiveWebApplication().
ApplicationDsl // Configuring
).run() // Running

After the code is written, the next step is the most complex part, configuration. In terms of inheritance, ApplicationDsl is a type of ConfigurationDsl, and ConfigurationDsl and other DSL elements are from AbstractDsl.

Among all DSL types, LoggingDsl is the only one that does not inherit AbstractDsl. As the simplest DSL element of the SpringFu project, only 20 lines of valid code of the LoggingDsl type are available after removing blank lines and comments. However, despite its uniqueness, LoggingDsl still has the same features as other DSL elements. The constructor receives a Consumer object based on its own type as the parameter.

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

The constructor has only one line of dsl.accept(this), and this line of code appears in all DSL elements. In the DSL elements inherited from the AbstractDsl type, it is placed where the initialize abstract method is implemented. The LoggingDsl type does not have an inherited initialize method, so it directly places this line of code in the constructor. This mysterious behavior of DSL passes itself to the Consumer object received by the constructor. This behavior will be explained in the code running section.

Returning to ApplicationDsl, this DSL type registers several additional Beans for Spring through a MessageSourceInitializer object, based on the ConfigurationDsl. The main functions are all directly inherited from the ConfigurationDsl. Here are several common methods of the ConfigurationDsl type:

  • configurationProperties(Class clazz): It is the class of registration attribution configuration equivalent to the @ConfigurationProperties annotation.
  • logging(Consumer dsl): It provides the output log configuration for functions.
  • beans(Consumer dsl): It provides the place to define Beans, which is equivalent to the @Configuration annotation.
  • enable(Consumer configuration): It provides a universal configuration entry with scaling capabilities for other DSL types, such as web listening.

These methods all return the ConfigurationDsl type to meet the requirements of the streaming declaration structure and use "return this" to connect with the next configuration method. Thus, you can write the following configuration codes:

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

The beans() method receives an anonymous function that consumes BeanDefinitionDsl. This DSL provides the bean() method, and its effect is similar to the @Bean annotation in the SpringMVC program. However, the Bean is directly registered to the IoC container through the registerBean() method of GenericApplicationContext internally, without reflection and scanning. The logging() method receives an anonymous function that consumes LoggingDsl. LoggingDSl provides the level() method that dynamically adjusts the output log level for any package path. The enable() method needs to be used in conjunction with other DSL elements with variable usages. For example, the preceding webMvc() method returns a WebMvcServerDsl object, which can configure HTTP listening, routing, and other attributes. The webMvc() method can also automatically register the relevant Bean to the Spring context through the DSL object.

After all ApplicationDsl objects are defined, it goes to the final step of running code. As mentioned earlier, the application() method receives an ApplicationDsl object and returns a JafuApplication object. Next, it calls the igniter method called run() in this return object.

In SpringFu, DSL is declarative. All the configuration information that developers define by using the ApplicationDsl object is still hidden in ApplicationDsl. The Spring container is still empty. In the run() method of JafuApplication, SpringFu creates a SpringApplication application object encapsulated by the Spring Boot framework. Next, it specifies the passed ApplicationDsl object as the initializer of the application object and then calls the run() method of the application object. After that, it is time for the Spring Boot. The SpringApplication completes all the preliminary work required for running Spring and then calls the initialize() method of all the initializer objects, including the ApplicationDsl object passed earlier.

In the initialize() method of ApplicationDsl, the initialize() method of AbstractDsl is called through super.initialize(context) to retain the reference of the passed context container. Then, the callback method passed in during the dsl.accept(this) construction is executed. In the beans() and enable() methods, the initialize() method of the sub-level DSL elements is explicitly called to continue the iteration of this initialization process. It is like nested recursive calls until all sub-level elements are constructed. After that, the initialization process ends, and the program returns to the Spring Boot startup process.

From the source code analysis, we noticed the SpringFu program is a SpringBoot program that avoids the dynamic features of JVM. After removing the complicated processes of spring-boot-starter-web and spring-boot-webflux, it is simpler and quicker. They seem different but are similar to each other.

4. Summary

Facts have proved that SpringFu, which has no burden, can run faster and perform better.

As cloud-native gradually infiltrates all aspects of developers’ daily work, Spring will be used again on the journey to Serverless. Spring started this journey long ago, awaiting everyone’s arrival.

Original Source:

Follow me to keep abreast with the latest technology news, industry insights, and developer trends.