Functional Bean Registrations in Spring Cloud Function
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:
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 tocontext.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 aFunctionalSpringApplication
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):
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.