はじめに
Lambda関数をJavaで実装するのは面倒くさい。
Spring Cloud Functionというのを使うと、その辺の煩雑さを良い感じにフレームワークが吸収してくれるらしい。
ということで、本当に良い感じに吸収されてサクッと書けるのかを試してみた。
ちなみに、Spring Cloud Functionはプロキシ統合していないサンプルは多くあれど、統合版はなかなか見つからなかったので、その検討の一助にもなれば。
構成
以下のようなMavenプロジェクトを作成する。
SpringCloudFunctionTest
├── pom.xml
├── serverless.yml
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── springcloudfunctiontest
│ │ ├── AWSLambdaHandler.java
│ │ ├── Hello.java
│ │ └── SpringCloudFunctionExampleApplication.java
│ └── resources
│ └── application.properties
└── test
└── java
└── com
└── springcloudfunctiontest
└── SpringCloudFunctionExampleApplicationTests.java
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 http://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.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.techprimers.serverless</groupId>
<artifactId>spring-cloud-function-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud-function-test</name>
<description>Demo project for Spring Boot with Spring Cloud Function</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-aws</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-events</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
<version>1.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot.experimental</groupId>
<artifactId>spring-boot-thin-layout</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<shadedArtifactAttached>true</shadedArtifactAttached>
<shadedClassifierName>aws</shadedClassifierName>
</configuration>
</plugin>
</plugins>
</build>
</project>
ハンドラのコード
通常のSpring Bootなアプリ同様、SpringApplication.run
するクラスを作る。
package com.springcloudfunctiontest;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringCloudFunctionExampleApplication {
public static void main(String[] args) {
SpringApplication.run(SpringCloudFunctionExampleApplication.class, args);
}
}
で、Lambdaのイベントハンドラを作る。
正直、このクラスが空っぽで良い理由がよく分からない……。
API Gatewayのバックエンドに置くので、InputとOutputにはそれぞれAPIGatewayProxyRequestEvent
とAPIGatewayProxyResponseEvent
を設定する。
package com.springcloudfunctiontest;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import org.springframework.cloud.function.adapter.aws.SpringBootRequestHandler;
public class AWSLambdaHandler extends SpringBootRequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
}
最後に、ビジネスロジックのクラスを作成する。
``APIGatewayProxyRequestEventと
APIGatewayProxyResponseEvent```の仕様はjavadocを確認しよう。
しかし、Spring Cloud Functionはマルチクラウドでの互換性がウリなのに、こんなインタフェースにしたら完全にAWS特化な実装になっちゃうよなぁ……。
最後のsetBodyのJSONの扱いが雑なのは気にしないように。
package com.springcloudfunctiontest;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import org.springframework.stereotype.Component;
import java.util.function.Function;
import java.util.Map;
@Component
public class Hello implements Function<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
@Override
public APIGatewayProxyResponseEvent apply(APIGatewayProxyRequestEvent input) {
Map<String, String> queryStringParameter = input.getQueryStringParameters();
String id = queryStringParameter.get("id");
String name = null;
if( id.equals("11111") ) {
name = "\"Taro\"";
} else {
name = "\"Nanashi\"";
}
APIGatewayProxyResponseEvent responseEvent = new APIGatewayProxyResponseEvent();
responseEvent.setStatusCode(200);
responseEvent.setBody("{\"name\":" + name + "}");
return responseEvent;
}
}
テストコード
テストコードも普通に書ける。
package com.springcloudfunctiontest;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringCloudFunctionExampleApplicationTests {
@Test
public void HelloTest1() {
Hello hello = new Hello();
APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent();
Map<String,String> queryStringParameter = new HashMap<>();
queryStringParameter.put("id", "11111");
request.setQueryStringParameters(queryStringParameter);
APIGatewayProxyResponseEvent response = hello.apply(request);
assertThat(response.getBody()).isEqualTo("{\"name\":\"Taro\"}");
}
@Test
public void HelloTest2() {
Hello hello = new Hello();
APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent();
Map<String,String> queryStringParameter = new HashMap<>();
queryStringParameter.put("id", "22222");
request.setQueryStringParameters(queryStringParameter);
APIGatewayProxyResponseEvent response = hello.apply(request);
assertThat(response.getBody()).isEqualTo("{\"name\":\"Nanashi\"}");
}
}
デプロイ
手でいちいちS3にアップロードしたり、パイプラインのビルド待ちしたりするのも面倒臭いので、正式版のアプリでないからServerless Frameworkでテキトーにデプロイしよう。
service: test
provider:
name: aws
runtime: java8
timeout: 30
region: ap-northeast-1
package:
artifact: target/spring-cloud-function-test-0.0.1-SNAPSHOT-aws.jar
functions:
hello:
handler: org.springframework.cloud.function.adapter.aws.SpringBootStreamHandler
events:
- http:
path: testapi
method: get
integration: lambda-proxy
request:
template:
application/json: '$input.json("$")'
これでデプロイすると、こんな感じでちゃんとRESTAPIが返されるぞ!
$ curl -X GET https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/testapi?id=11111; echo
{"name":"Taro"}
結論
うーん、Springフレームワークの恩恵を全然預かっていないので、普通のLambdaのイベントハンドラを作った時と手間としては変わらなかった気分。もっとフレームワークの機能をふんだんに使うようになったらありがたみがわかるのだろうか。
今回は、JavaでLambda関数を書くならやっぱりAPIGatewayProxyRequestEventを使うと実装が楽だね、ということと、Serverless Frameworkのデプロイがお手軽だね、と言うことは理解できた。
2022/3/17追記 DynamoDB StreamsでトリガされるLambdaの場合
DynamoDB StreamsについてもSpring Cloud Functionで呼び出せることを確認したのでメモ。
メイン処理
メイン処理はAPI Gatewayから変える必要がない。
ハンドラ
ハンドラは以下のように、DynanmoDBのイベントを受け取れるようにする。
package com.springcloudfunctiontest;
import org.springframework.cloud.function.adapter.aws.SpringBootRequestHandler;
import com.amazonaws.services.lambda.runtime.events.DynamodbEvent;
public class DynamoDbEventHandler extends SpringBootRequestHandler<DynamodbEvent, String> {
}
関数の実装
関数側も同様に、DynamoDBEventを受け付けられるようにしておく。これで、DynamoDB Streamsのイベントがapply
で通知され、input
でJSONの情報を取得できる。
package com.springcloudfunctiontest;
import java.util.function.Function;
import org.springframework.context.annotation.Configuration;
import com.amazonaws.services.lambda.runtime.events.DynamodbEvent;
@Configuration
public class Hello implements Function<DynamodbEvent, String> {
@Override
public String apply(DynamodbEvent input) {
System.out.println(input);
return "OK.";
}
}
Serverless Frameworkの設定
Serverless FrameworkでDynamoDBのイベントを作成することも可能。
気を付けなければいけないのは、ハンドラのクラスがAPI Gatewayと異なること。
今回はちゃんとハンドルしてほしいクラス(DynamoDbEventHandler
)のクラスパスを指定しよう。
# Welcome to Serverless!
#
# This file is the main config file for your service.
# It's very minimal at this point and uses default values.
# You can always add more config options for more control.
# We've included some commented out config examples here.
# Just uncomment any of them to get that config option.
#
# For full config options, check the docs:
# docs.serverless.com
#
# Happy Coding!
# https://serverless.com/framework/docs/providers/aws/guide/serverless.yml/
service: test
provider:
name: aws
runtime: java8
timeout: 30
region: ap-northeast-1
package:
artifact: build/libs/r-kubota_SpringCloudFunctionTest-aws.jar
functions:
hello:
handler: com.springcloudfunctiontest.DynamoDbEventHandler
events:
- stream:
type: dynamodb
arn:
Fn::GetAtt:
- MyDynamoDbTable
- StreamArn
resources:
Resources:
MyDynamoDbTable:
Type: "AWS::DynamoDB::Table"
Properties:
TableName: my-table
AttributeDefinitions:
- AttributeName: id
AttributeType: N
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
参考資料
DynamoDB Streams版を書くにあたり、「やってみた」レベルの理解度だと全然分からなくて、以下のサイトがかなり参考になった。感謝!