Sneak peek at spring-cloud-function serverless project
spring-cloud-function
umbrella project. It's basically a Spring's approach to serverless (I prefer the term function-as-a-service) programming. Function<T, R>
becomes the smallest building block in a Spring application. Functions defined as Spring beans are automatically exposed e.g. via HTTP in RPC style. Just a quick example how it looks:@SpringBootApplicationThe implementation of
public class FaasApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(FaasApplication.class, args);
}
@Bean
Function<Long, Person> personById(PersonRepository repo) {
return repo::findById;
}
}
@Component
interface PersonRepository {
Person findById(long id);
Mono<Person> findByIdAsync(long id);
}
PersonRepository
is irrelevant here. This is a valid Spring Boot application. But once you put spring-cloud-function
dependency, beans of Function
type come alive:compile 'org.springframework.cloud:spring-cloud-function-web:1.0.0.M5'At this point each
Function
(as well as Supplier
and Consumer
) bean is exposed via HTTP API. I'm using HTTPie as a command-line client:$ echo 42 | http -v :8080/personById Content-type:text/plainThe response:
POST /personById HTTP/1.1
Content-Length: 3
Content-type: text/plain
...
42
HTTP/1.1 200Notice how
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
{
"id": 42,
"name": "Bob"
}
personById
bean of type Function
turned into an HTTP endpoint. The Flux
version called peopleById
is even more interesting:@BeanIt allows processing a stream of input
Function<Flux<Long>, Flux<Person>> peopleById(PersonRepository repo) {
return ids -> ids.flatMap(repo::findByIdAsync);
}
Long
values and produce a stream of corresponding people. Remember that flatMap
used in the implementation may not preserve order!$ echo '[42,43]' | http -v :8080/peopleByIdThis returns an array of results:
POST /peopleById HTTP/1.1
Content-Type: application/json
...
[
42,
43
]
HTTP/1.1 200Honestly, this whole serverless thing looks like RPC over HTTP so far. Or to put it more gently, a slightly simpler way of registering HTTP endpoints. Indeed, but this is just the beginning. First, let's define few more functions:
Content-Type: application/json
Transfer-Encoding: chunked
[
{
"id": 42,
"name": "Bob"
},
{
"id": 43,
"name": "Alice"
}
]
@BeanThe stub implementations are fine for the purpose of this exercise. Now we can call
Function<Flux<Person>, Flux<Car>> carOfPerson() {
return flux -> flux.map(p ->
new Car("Honda",
"FOO-123",
p.getName() + " <" + p.getId() + ">"));
}
@Bean
Function<Flux<Car>, Flux<String>> describe() {
return flux -> flux.map(c ->
c.getLicensePlate() + " (" + c.getModel() +
") owned by " + c.getOwnerName());
}
carOfPerson
function remotely via HTTP:$ http -v :8080/carOfPerson id=42 name=BobPOSTing a person (an argument to
POST /carOfPerson HTTP/1.1
Content-Type: application/json
{
"id": "42",
"name": "Bob"
}
carOfPerson
function) yields Car
as a response:HTTP/1.1 200POSTing many instances of
Content-Type: application/json
Transfer-Encoding: chunked
[
{
"licensePlate": "FOO-123",
"model": "Honda",
"ownerName": "Bob <42>"
}
]
Person
would obviously return many instances of Car
. What about calling describe(Car)
returning String
?$ echo '{"licensePlate": "FOO-123", "model": "Honda", "ownerName": "Bob [42]"}' | \This returns a one-element array of strings:
http -v :8080/describe
POST /describe HTTP/1.1
Content-Type: application/json
{
"licensePlate": "FOO-123",
"model": "Honda",
"ownerName": "Bob <42>"
}
HTTP/1.1 200As a side-note, HTTPie makes working with JSON very convenient. Rather than piping the output of
Content-Type: application/json
Transfer-Encoding: chunked
[
"FOO-123 (Honda) owned by Bob <42>"
]
echo
command, you can use this handy syntax:$ http -v :8080/describe \Cool, but back to serverless.
licensePlate='FOO-123' \
model=Honda \
ownerName='Bob <42>'
POST /describe HTTP/1.1
Content-Type: application/json
{
"licensePlate": "FOO-123",
"model": "Honda",
"ownerName": "Bob <42>"
}
Composing functions server-side
Calling individual functions is nice, but we can compose many functions, piping the result of one function to input of another:$ echo '[42, 43]' | \This correctly returns an array of strings (
http :8080/peopleById | \
http :8080/carOfPerson | \
http :8080/describe
Flux<Long>
| Flux<Person>
| Flux<Car>
| Flux<String>
)., However, we make numerous network round trips. A much better approach is to compose functions on the server side (wait, so there is a server in serverless?!?)$ echo '[42, 43]' | http -v :8080/peopleById,carOfPerson,describePassing two IDs of
POST /peopleById,carOfPerson,describe HTTP/1.1
Content-Type: application/json
[
42,
43
]
Person
and then composing (piping) three functions, basicallydescribe . peopleById . carOfPersonor (if you're not the Haskell type of guy 😉)
ids -> describe(carOfPerson(peopleById(ids)))The response is expected:
HTTP/1.1 200OK, what we saw so far isn't particularly impressive. I'd rather use semi-standard JSON-RPC if that's all the library has to offer. But
Content-Type: application/json
Date: Tue, 17 Apr 2018 14:13:59 GMT
Transfer-Encoding: chunked
[
"FOO-123 (Honda) owned by Bob [42]",
"FOO-123 (Honda) owned by Bob [43]"
]
spring-cloud-function
offers another great feature: hot-deployment of functions.Deploying and compiling of functions at runtime
First, add the following dependency:compile 'org.springframework.cloud:spring-cloud-function-compiler:1.0.0.M5'Then expose the built-in
CompilerController
:import org.springframework.cloud.function.compiler.app.CompilerControllerAt this point we can POST raw Java code snippets to our application, which will be compiled to bytecode and saved for later execution:
@Bean
public CompilerController compilerController() {
return new CompilerController();
}
$ echo 's -> s.length()' | \Underneath
http -v :8080/function/len \
inputType==String \
outputType==Integer
POST /function/len?inputType=String&outputType=Integer HTTP/1.1
s -> s.length()
HTTP/1.1 200
spring-cloud-function-compiler
compiles this Java code snippet statically. For example, type mismatch error is properly reported (notice the outputType
query parameter):$ echo 's -> s.length()' | \You can also try to define more complex functions:
http :8080/function/len \
inputType==String \
outputType==Long
HTTP/1.1 500
org.springframework.cloud.function.compiler.java.CompilationFailedException: ==========
return (Function<String,Long> & java.io.Serializable) s -> s.length()
^^
ERROR:incompatible types: bad return type in lambda expression
int cannot be converted to java.lang.Long
$ echo "x -> java.util.stream.IntStream \This one defines
.range(2, x + 1) \
.mapToObj(java.math.BigInteger::valueOf) \
.reduce(java.math.BigInteger.ONE, java.math.BigInteger::multiply)" \
| http :8080/function/factorial \
inputType==Integer \
outputType==java.math.BigInteger
factorial
function from int
to BigInteger
. Fully qualified class names are necessary.Summary
The project is still under active development and it's definitely not production ready. Also, the documentation is still not complete. However, it already has some support for serverless platforms like AWS lambda. I'm not an enthusiast when it comes to function-as-a-service deployment pattern, but it's good that Spring makes an effort to support this paradigm. I'm looking forward to next milestones!Tags: HTTP, faas, httpie, serverless, spring