Functional Bean Registrations in Spring Cloud Function

Engineering | Dave Syer | October 22, 2018 | ...

Spring Cloud Function has a couple of new features in 2.0 (still in milestone phase), and possibly the most dramatic is the ability to go "fully functional". This is made possible by changes in Spring Boot 2.1 together with Spring Framework 5.1, and it means a different way of thinking about bean definitions in Spring applications, but also significant improvements in startup performance.

AWS Cost Savings

It’s always good to start with a picture, especially if it tells a story. Here’s a graph that shows the improvement in Spring Cloud Function 2.0 over 1.0, comparing the cost of cold starts in AWS:

Memory Cost

The x-axis is memory in MB, and the y-axis is cost of a cold start in GBsec. The most dramatic effect is for low memory containers, where the cost is almost 4 times lower with 2.0. The "Custom" function is even faster (10x over Spring Cloud Function 1.0) - it’s a custom AWS runtime using Spring Cloud Function with functional beans. The origin of the improvement is in dramatically shorter startup times, which in turn come from using the functional form of bean definitions in the application. Josh made a video about functional bean registration a while ago if you need an introduction (it’s on YouTube). Now let’s have a closer look at how it works in Spring Cloud Function.

Comparing Functional with Traditional Bean Definitions

Here’s a Spring Cloud Function application from version 1.0, with the familiar @Configuration and @Bean declaration style:

@SpringBootApplication
public class DemoApplication {

  @Bean
  public Function<String, String> uppercase() {
    return value -> value.toUpperCase();
  }

  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }

}

You can run it in AWS Lambda (for instance) by jarring it up with all its dependencies and uploading it to Amazon. There is also support for Azure Functions and Apache OpenWhisk in the project. Other serverless providers, e.g. Oracle Fn and Riff, maintain their own bindings.

You can also run the app above in its own HTTP server just by including spring-cloud-function-starter-web on the classpath. Running the main method would expose an endpoint that you can use to ping that uppercase function:

$ curl localhost:8080 -d foo
FOO

The web adapter in 1.0 was implemented using Spring MVC, so you needed a Servlet container. In Spring Cloud Function 2.0 you can also use Webflux and the default server is netty (even though you can still use Servlet containers if you want to) - just include the spring-cloud-starter-function-webflux dependency instead. The functionality is the same, and the user application code can be used in both.

In 2.0 though, the user application code can be recast into "functional" form, like this:

@SpringBootConfiguration
public class DemoApplication
    implements ApplicationContextInitializer<GenericApplicationContext> {

  public static void main(String[] args) {
    FunctionalSpringApplication.run(DemoApplication.class, args);
  }

  public Function<String, String> uppercase() {
    return value -> value.toUpperCase();
  }

  @Override
  public void initialize(GenericApplicationContext context) {
    context.registerBean("demo", FunctionRegistration.class,
        () -> new FunctionRegistration<>(uppercase())
            .type(FunctionType.from(String.class).to(String.class)));
  }

}

The main differences are:

  • The main class is an ApplicationContextInitializer.

  • The @Bean methods have been converted to calls to context.registerBean()

  • The @SpringBootApplication has been replaced with @SpringBootConfiguration to signify that we are not enabling Spring Boot autoconfiguration, and yet still marking the class as an "entry point".

  • The SpringApplication from Spring Boot has been replaced with a FunctionalSpringApplication from Spring Cloud Function (it’s a subclass).

The business logic beans that you register in a Spring Cloud Function app are of type FunctionRegistration. This is a wrapper that contains both the function and information about the input and output types. In the @Bean form of the application that information can be derived reflectively, but in a functional bean registration some of it is lost unless we use a FunctionRegistration.

An alternative to using an ApplicationContextInitializer and FunctionRegistration is to make the application itself implement Function (or Consumer or Supplier). Example (equivalent to the above):

@SpringBootConfiguration
public class DemoApplication implements Function<String, String> {

  public static void main(String[] args) {
    FunctionalSpringApplication.run(DemoApplication.class, args);
  }

  @Override
  public String apply(String value) {
    return value.toUpperCase();
  }

}

It would also work to add a separate, standalone class of type Function and register it with the SpringApplication using an alternative form of the run() method. The main thing is that the generic type information is available at runtime through the class declaration.

The app runs in its own HTTP server if you add spring-cloud-starter-function-webflux (it won’t work with the MVC starter at the moment because the functional form of the embedded Servlet container hasn’t been implemented). The app also runs just fine in AWS Lambda or Azure Functions, and the improvements in startup time are dramatic (as shown in the graph above). Here are the startup times in another graph (startup time in seconds on the y-axis):

Memory Startup Time

Testing Functional Applications

Spring Cloud Function 2.0 also has some utilities for integration testing that will be very familiar to Spring Boot users. For example, here is an integration test for the HTTP server wrapping the app above:

@RunWith(SpringRunner.class)
@FunctionalSpringBootTest
@AutoConfigureWebTestClient
public class FunctionalTests {

	@Autowired
	private WebTestClient client;

	@Test
	public void words() throws Exception {
		client.post().uri("/").body(Mono.just("foo"), String.class)
                    .exchange().expectStatus().isOk()
                          .expectBody(String.class).isEqualTo("FOO");
	}

}

This test is almost identical to the one you would write for the @Bean version of the same app - the only difference is the @FunctionalSpringBootTest annotation, instead of the regular @SpringBootTest. All the other pieces, like the @Autowired WebTestClient, are standard Spring Boot features.

Functional Bean Definitions in Mainstream Spring Boot Applications

Spring Boot works just fine with functional bean registrations - Spring Cloud Function builds and runs on Spring Boot - but some of the most useful features in Spring Boot, the autoconfigurations, are all coded in the non-functional style. Most Spring Cloud Function apps have a relatively small scope compared to the whole of Spring Boot, so we are able to adapt it to these functional bean definitions easily. If you step outside that limited scope, you can extend your Spring Cloud Function app by switching back to @Bean style configuration, or by using a hybrid approach. It will take a little longer to extend the same kind of features to the rest of the Spring Boot ecosystem, but it is something we are working on quite actively. Please take Spring Cloud Function 2.0 for a spin and provide us with some feedback if you have a moment - the GA release will be coming soon.

Get the Spring newsletter

Stay connected with the Spring newsletter

Subscribe

Get ahead

VMware offers training and certification to turbo-charge your progress.

Learn more

Get support

Tanzu Spring offers support and binaries for OpenJDK™, Spring, and Apache Tomcat® in one simple subscription.

Learn more

Upcoming events

Check out all the upcoming events in the Spring community.

View all