Spring BootをAWS Lambdaで動かす

2016.05.06

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

JavaでAWS Lambdaを開発する際にフレームワークを使用したいと思い、Spring BootがAWS Lambdaで動くのかを試してみました。今回行ったのは

  • @Componentを付与したクラスを読み込む
  • 定義ファイル(application.yml)を読み込む

ことです。

※追記
@Autowiredで@Componentのインスタンスを取得する改良版を書きました。
Spring BootをAWS Lambdaで動かす – (2)HanderクラスをApplication Contextとして指定する

手順について

1.build.gradle

まずはGradleで必要なものをインポートします。以下のようになります。

build.gradle
buildscript {
	ext {
		springBootVersion = '1.3.3.RELEASE'
	}
	repositories {
		mavenCentral()
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 
	}
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'spring-boot' 

jar {
	baseName = 'LambdaSpringBootSample'
	version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
	mavenCentral()
}


dependencies {
	compile('org.springframework.boot:spring-boot-starter')
	compile('com.amazonaws:aws-lambda-java-core:1.1.0')
	compile('org.projectlombok:lombok:1.16.8')
	testCompile('org.springframework.boot:spring-boot-starter-test') 
}


eclipse {
	classpath {
		 containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
		 containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
	}
}

build.gradleのベースはSpring Initializrで作成したプロジェクトのものです。これに「com.amazonaws:aws-lambda-java-core」「org.projectlombok:lombok」をライブラリとして追加します。

2.Applicationクラス

このクラスもSpring Initializrで作成したプロジェクトにある、「@SpringBootApplication」が付与されたクラスをベースとしています。Spring Boot単体であれば、このクラスがアプリケーションのエントリーポイントとなるのですが、今回はAWS Lambda上で動かすため後述する「handleRequest」メソッドがエントリーポイントとなります。そのため、このクラスの「main」メソッドが実行されないことを前提にプログラムを作成する必要があります。

Application.java
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Application {
    private static ConfigurableApplicationContext context = null;
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    
    public static <T> T getBean(Class<T> clazz){
        return getContext().getBean(clazz);
    }
    
    private static ConfigurableApplicationContext getContext(){
        if(context == null){
            String args[] = new String[1];
            args[0] = "dummy";
            context = SpringApplication.run(Application.class, args);
        }
        return context;
    }
}

上記が今回作成したApplicationクラスとなります。15行目以降が新たに追加した処理となります。「getContext」メソッドはSpringのApplicationContextを生成して返却しています。「getBean」メソッドは先の「getContext」を呼び出し、引数で指定されたクラスをBeanとして返却します。後述する@Componentを付与したクラスは、この「getBean」メソッドを通してインスタンスを生成します。

3.RequestHandler

AWS Lambdaを実行した際にエントリーポイントとなる「handleRequest」メソッドを実装するクラスです。

LambdaFunctionHandler.java
package com.example;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

public class LambdaFunctionHandler implements RequestHandler<Object, Object>{

    private SampleBean sampleBean;
    private ConfigReader configReader;
    
    @Override
    public Object handleRequest(Object input, Context context) {
        context.getLogger().log("-----start.-----");
        
        sampleBean = Application.getBean(SampleBean.class);
        String inputValue = sampleBean.sampleMethod(input.toString());
        context.getLogger().log(inputValue);
        
        configReader = Application.getBean(ConfigReader.class);
        context.getLogger().log("Region = " + configReader.getRegion());
        context.getLogger().log("Bucket = " + configReader.getBucket());
        context.getLogger().log("file = " + configReader.getFile());
        
        context.getLogger().log("-----end.-----");
        return "success.";
    }

}

@Componentを付与した「SampleBean」「ConfigReader」の2つのクラスのインスタンスを、Applicationクラスの「getBean」メソッドを通じて取得しています(15、19行目)。これらのクラスについては後述します。AWS Lambdaでなければインスタンス変数の宣言時(8、9行目)に@Autowiredを付与することでインスタンスを取得できますが、AWS Lambda上で実行する際にはできないようです。

それ以外は通常のAWS Lambdaのプログラムと変わりはないです。

4.SampleBean

先の「RequestHandler」にてインスタンスを取得したクラスです。以下のようになります。

SampleBean.java
package com.example;

import org.springframework.stereotype.Component;

@Component
public class SampleBean {
    public String sampleMethod(String input){
        return "Called sampleMethod : arg = " + input;
    }
}

このクラスは@Componentを使用してコンポーネント化しており、メソッドも受け取った引数の文字列を加えて返却しているだけです。

5.定義ファイルの読み込み

定義ファイル(application.yml)の読み込みです。定義ファイルを読み込む@ConfigurationPropertiesを使用するクラス(Settings)と、定義値を文字列として返却するクラス(ConfigReader)の2つに分けました。

また起動時にバナーや起動情報が標準出力されないよう設定しています。

application.yml
settings:
  region: ap-northeast-1
  s3:
    bucket: test-bucket
    file: testfile.txt
spring:
  main:
    show-banner: false
    log-startup-info: false

今回は上記のようなymlに定義した値を読み込みます。

Settings.java
package com.example;

import java.util.Map;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import lombok.Data;

@Component
@ConfigurationProperties(prefix = "settings")
@Data
public class Settings {
    private String region;
    private Map<String, String> s3;
}

@ConfigurationPropertiesを使用し、上記のapplication.ymlに定義した値を読み込む為のクラスです。lombokの@Dataを使用し、メンバ変数のGetter、Setterを自動的に作成しています。詳細はSpring Bootで定義ファイル(yaml)を参照するを参考にしてください。

ConfigReader.java
package com.example;

import java.util.Map;

import org.springframework.stereotype.Component;

@Component
public class ConfigReader {
    private Settings settings = null;
     
    public String getRegion(){
        return getSettings().getRegion();
    }
     
    public String getBucket(){
        Map<String, String> map = getSettings().getS3();
        return map.get("bucket");
    }
     
    public String getFile(){
        Map<String, String> map = getSettings().getS3();
        return map.get("file");
    }
    
    // SettingsのBeanを取得する
    // このクラスのコンストラクタで行うと、Lambda実行時にタイムアウトする
    private Settings getSettings(){
        if(settings == null){
            settings = Application.getBean(Settings.class);
        }
        return settings;
    }
}

上記のSettingsクラスを使用し、定義ファイルの定義値を文字列として返却するクラスです。Settingsクラスも@Componentとして宣言したため、Applicationクラスの「getBean」メソッドを使用してインスタンスを取得しています。このインスタンスの取得ですが、クラスのコンストラクタ内で行うと、Lambdaの実行時にタイムアウトするようです。今回はインスタンスを取得するための「getSettings」メソッドを用意し、そのメソッドを通じてSettingsクラスのインスタンスを使用しています。

実行結果

最後に実行結果です。Gradleのビルドを行って作成したjarをAWS Lambdaに配置して実行します。jar内にapplication.ymlも組み込まれ、以下のような実行結果がCloud Watch Logに出るかと思います。

START RequestId: 91c52efb-119f-11e6-a9bb-a7da05f32c35 Version: $LATEST 
-----start.-----
(中略)

Called sampleMethod : arg = {key3=value3, key2=value2, key1=value1}
Region = ap-northeast-1
Bucket = test-bucket
file = testfile.txt
-----end.-----

まとめ

あまり例は無いかとは思いますが、Spring BootをAWS Lambdaで動かすことができました。@Autowiredが使えないことが残念ではありますが・・・。何かの参考になれば幸いです。

参考サイト

Creating AWS Lambda using java and spring framework
Spring Bootで起動/終了時のコンソール出力を抑制する