これは、なにをしたくて書いたもの?
Spring Cloud Functionを、ちょっと試してみようかな、と。
Spring Cloud Function
Spring Cloud Functionとは、関数(Function)を使ってロジックを実行する仕組みです。特定の実行環境に依存せず、
同じコードでWebのエンドポイント、ストリーム処理、タスクを実行できるようにできます。
DIなどの、Springの機能も使えます。
機能としては、こんな感じのようです。
- リアクティブ、命令型、およびそのハイブリッドのサポート
@FunctionalInterface
なセマンティクスなものであれば関数として扱う(POJO function)- 入出力の変換
- 関数の合成
- RESTサポート
- Spring Cloud Streamを介したストリーミング(Apache Kafka、RabbitMQなどを使って)
- JARへのパッケージング
- ターゲットとするプラットフォーム(AWS Lambdaなど)向けにパッケージする
- 各種FaaS向けのアダプター
- リアクティブな関数のサポートによる複数の入出力のマージ、結合、その他の操作
ドキュメントは、こちらです。
Spring Cloud Function Reference Documentation
今回は、簡単に関数を作ってREST APIとしてアクセスしてみます。プログラミングスタイルは、リアクティブ(Reactorを使う)で
いきます。
環境
今回の環境は、こちらです。
$ java --version openjdk 11.0.10 2021-01-19 OpenJDK Runtime Environment (build 11.0.10+9-Ubuntu-0ubuntu1.20.04) OpenJDK 64-Bit Server VM (build 11.0.10+9-Ubuntu-0ubuntu1.20.04, mixed mode, sharing) $ mvn --version Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 11.0.10, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-67-generic", arch: "amd64", family: "unix"
プロジェクトを作成する
まずは、Spring Bootプロジェクトを作成します。依存関係にSpring Cloud Functionと、WebFluxを選んでプロジェクトを作成。
$ curl -s https://start.spring.io/starter.tgz \ -d dependencies=cloud-function,webflux \ -d groupId=org.littlewings \ -d artifactId=getting-started \ -d packageName=org.littlewings.spring.cloudfuntion \ -d bootVersion=2.4.4 \ -d javaVersion=11 \ -d type=maven-project \ -d baseDir=getting-started | tar zxvf - $ cd getting-started
最初から入っているソースコードは削除しておきます。
$ rm src/main/java/org/littlewings/spring/cloudfuntion/DemoApplication.java src/test/java/org/littlewings/spring/cloudfuntion/DemoApplicationTests.java
生成されたpom.xml
は、こんな感じです。
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.4</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>org.littlewings</groupId> <artifactId>getting-started</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>11</java.version> <spring-cloud.version>2020.0.1</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-function-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
spring-cloud-starter-function-webflux
というものを使うのでは?と思ったのですが、中身を見るとspring-boot-starter-webflux
と
spring-cloud-function-web
を足したもののようなので、意味は同じですね。
関数を作成する
では、関数を作成していきましょう。Java 8の関数インターフェースを使っていきます。
Spring Cloud Function / Java 8 function support
そして、Webアプリケーションとして実行します。
Spring Cloud Function / Standalone Web Applications
今回は扱いませんが、ストリーム処理もできます、と。
Spring Cloud Function / Standalone Streaming Applications
作成したmainクラス+関数は、こちら。
src/main/java/org/littlewings/spring/cloudfuntion/App.java
package org.littlewings.spring.cloudfuntion; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @SpringBootApplication public class App { public static void main(String... args) { SpringApplication.run(App.class, args); } @Bean public Supplier<Mono<String>> hello() { return () -> Mono.just("Hello Spring Cloud Function!!"); } @Bean public Function<Mono<String>, Mono<String>> decorate() { return message -> message.map(m -> String.format("★★★%s★★★", m)); } @Bean public Consumer<Flux<String>> consume() { return message -> message.subscribe(m -> System.out.printf("Received: %s%n", m)); } }
Function
やSupplier
、Consumer
などをSpringのBeanとして登録すればOKです。
@Bean public Supplier<Mono<String>> hello() { return () -> Mono.just("Hello Spring Cloud Function!!"); } @Bean public Function<Mono<String>, Mono<String>> decorate() { return message -> message.map(m -> String.format("★★★%s★★★", m)); } @Bean public Consumer<Flux<String>> consume() { return message -> message.subscribe(m -> System.out.printf("Received: %s%n", m)); }
パッケージングして起動。
$ mvn package $ java -jar target/getting-started-0.0.1-SNAPSHOT.jar
URLのマッピングのルールはこちらに書いているので、これに沿ってアクセスしてみます。
Spring Cloud Function / Standalone Web Applications
Supplier
。
@Bean public Supplier<Mono<String>> hello() { return () -> Mono.just("Hello Spring Cloud Function!!"); }
Beanの名前が、そのままアクセスするパスになります。
$ curl -i localhost:8080/hello HTTP/1.1 200 OK timestamp: 1616161539041 Content-Type: text/plain;charset=UTF-8 Content-Length: 29 Hello Spring Cloud Function!!
Function
。
@Bean public Function<Mono<String>, Mono<String>> decorate() { return message -> message.map(m -> String.format("★★★%s★★★", m)); }
HTTPボディを入力として扱います。アクセスするパスは、こちらもBean名です。
$ curl -i -XPOST -H 'Content-Type: text/plain' localhost:8080/decorate -d 'Hello' HTTP/1.1 200 OK timestamp: 1616161556980 Content-Type: text/plain;charset=UTF-8 Content-Length: 23 ★★★Hello★★★
Content-Type
は重要で、今回のアクセス方法でtext/plain
を指定しない場合は、HTTPボディを認識できません。
$ curl -i -XPOST localhost:8080/decorate -d 'Hello' HTTP/1.1 200 OK timestamp: 1616161622371 Content-Type: text/plain;charset=UTF-8 Content-Length: 18 ★★★★★★
パスの一部を入力として与えることも可能です。
$ curl -i localhost:8080/decorate/hello HTTP/1.1 200 OK timestamp: 1616161636288 Content-Type: text/plain;charset=UTF-8 Content-Length: 23 ★★★hello★★★
Consumer
とリアクティブスタイルを組み合わせる場合は、入力はFlux
にする必要があるようです。また、サブスクライブする
必要があることにも注意です。
@Bean public Consumer<Flux<String>> consume() { return message -> message.subscribe(m -> System.out.printf("Received: %s%n", m)); }
Spring Cloud Function / Consumer
$ curl -i -XPOST -H 'Content-Type: text/plain' localhost:8080/consume -d 'Hello' HTTP/1.1 202 Accepted content-length: 0
サーバー側で出力される結果。サブスクライブしている部分ですね。
Received: Hello
ルーティングに関しては、他の方法もあるようですが、今回はパス。
Spring Cloud Function / Function Routing and Filtering
アクセスできることは確認できたので、ここまで書いた内容はコメントアウト。
@SpringBootApplication public class App { public static void main(String... args) { SpringApplication.run(App.class, args); } /* @Bean public Supplier<Mono<String>> hello() { return () -> Mono.just("Hello Spring Cloud Function!!"); } @Bean public Function<Mono<String>, Mono<String>> decorate() { return message -> message.map(m -> String.format("★★★%s★★★", m)); } @Bean public Consumer<Flux<String>> consume() { return message -> message.subscribe(m -> System.out.printf("Received: %s%n", m)); } */ }
入出力にJSONを使う
次は、入出力にJSONを使ってみましょう。お題は書籍で。
src/main/java/org/littlewings/spring/cloudfuntion/Book.java
package org.littlewings.spring.cloudfuntion; public class Book { String isbn; String title; int price; // getter/setterは省略 }
書籍データの保存先は、メモリ上にしておきます。
src/main/java/org/littlewings/spring/cloudfuntion/InMemoryBookRepository.java
package org.littlewings.spring.cloudfuntion; import java.util.Comparator; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @Component public class InMemoryBookRepository { ConcurrentMap<String, Book> storage = new ConcurrentHashMap<>(); public Mono<Void> insert(Book book) { return Mono.justOrEmpty(storage.put(book.getIsbn(), book)).then(); } public Flux<Book> findAll() { return Flux .fromIterable(storage.values().stream().sorted(Comparator.<Book>comparingInt(book -> book.getPrice()).reversed()).collect(Collectors.toList())); } public Mono<Book> find(String isbn) { return Mono.justOrEmpty(storage.get(isbn)); } }
関数の実装方法は、今回は@Component
を使って試してみましょう。単に興味本位のやり方です。
ついでに、DIできることも確認しておこうかな、と。
書籍を保存するConsumer
。@Component
に名前を指定することで、こちらの名前でアクセスできることも
確認してみようと思います。
src/main/java/org/littlewings/spring/cloudfuntion/BookInsertFunction.java
package org.littlewings.spring.cloudfuntion; import java.util.function.Consumer; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; @Component("insert") public class BookInsertFunction implements Consumer<Flux<Book>> { InMemoryBookRepository bookRepository; public BookInsertFunction(InMemoryBookRepository bookRepository) { this.bookRepository = bookRepository; } @Override public void accept(Flux<Book> books) { books.subscribe(b -> bookRepository.insert(b)); } }
先ほど作成したInMemoryBookRepository
を、コンストラクタインジェクションしています。
書籍ひとつを取得するFunction
。
src/main/java/org/littlewings/spring/cloudfuntion/BookFindFunction.java
package org.littlewings.spring.cloudfuntion; import java.util.function.Function; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; @Component("find") public class BookFindFunction implements Function<Mono<String>, Mono<Book>> { InMemoryBookRepository bookRepository; public BookFindFunction(InMemoryBookRepository bookRepository) { this.bookRepository = bookRepository; } @Override public Mono<Book> apply(Mono<String> isbn) { return isbn.flatMap(i -> bookRepository.find(i)); } }
全書籍を取得するSupplier
。
src/main/java/org/littlewings/spring/cloudfuntion/BookListFunction.java
package org.littlewings.spring.cloudfuntion; import java.util.function.Supplier; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; @Component("list") public class BookListFunction implements Supplier<Flux<Book>> { InMemoryBookRepository bookRepository; public BookListFunction(InMemoryBookRepository bookRepository) { this.bookRepository = bookRepository; } @Override public Flux<Book> get() { return bookRepository.findAll(); } }
パッケージングして起動。
$ mvn package $ java -jar target/getting-started-0.0.1-SNAPSHOT.jar
確認してみます。まずは、データの登録。
$ curl -i -XPOST -H 'Content-Type: application/json' localhost:8080/insert -d '{"isbn": "978-4798142470", "title": "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", "price": 4400}' HTTP/1.1 202 Accepted content-length: 0 $ curl -i -XPOST -H 'Content-Type: application/json' localhost:8080/insert -d '{"isbn": "978-4774182179", "title": "[改訂新版]Spring入門 - Javaフレームワーク・より良い設計とアーキテクチャ", "price": 4180}' HTTP/1.1 202 Accepted content-length: 0
1件取得。
$ curl -i localhost:8080/find/978-4798142470 HTTP/1.1 200 OK timestamp: 1616162572285 Content-Type: application/json Content-Length: 127 {"isbn":"978-4798142470","title":"Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発","price":4400}
OKですね。
$ curl -i localhost:8080/list HTTP/1.1 200 OK timestamp: 1616162591835 Content-Type: application/json Content-Length: 278 [{"isbn":"978-4798142470","title":"Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発","price":4400},{"isbn":"978-4774182179","title":"[改訂新版]Spring入門 - Javaフレームワーク・より良い設計とアーキテクチャ","price":4180}]
とりあえず、今回やってみたいことは確認できたかな、と。
まとめ
Spring Cloud Functionを試してみました。Function
、Supplier
、Consumer
インターフェースで、簡単に関数を
公開できることが確認できました。
最初は、Spring Data系のものと組み合わせて試してみようかなと思ったのですが、関数へのルーティングまわりに
戸惑ったところもあって、単純にアクセスするだけにとどめておきました。
次からは、もうちょっといろいろできるかな、と思います。