My Note Pad

エンジニアリングや日々の雑感を書いていきます

AWS SAMでローカル環境でS3とDynamoDBを扱うLambdaを実行する

この記事は2018年10月26日にQiitaに投稿した記事を移行したものです

この投稿について

Serverlessconf Tokyo 2018で色々と刺激を受け、Lambdaに取り組んでみようと思い、色々と試す上でLambdaをローカル環境で開発や動作確認をするのに色々迷う部分が多かったので、メモとして残したものです。

動作環境

以下のものを使用しています。

# Pythonはvenv
$ python --version
Python 3.6.6
$ aws --version
aws-cli/1.16.24 Python/3.7.0 Darwin/18.0.0 botocore/1.12.14
$ sam --version
SAM CLI, version 0.6.0

SAMプロジェクトの作成

sam initコマンドを使用して空のプロジェクトを作成します。

  • -r はruntime
  • -n はプロジェクト名

を指定しています。

$ sam init -r python3.6 -n sam-s3-lambda-app

無事に成功すると以下のようなパッケージ構成が作成されます。

$ cd sam-s3-lambda-app
$ tree .
.
├── README.md
├── hello_world
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-37.pyc
│   │   └── app.cpython-37.pyc
│   └── app.py
├── requirements.txt
├── template.yaml
├── tests
│   └── unit
│       ├── __init__.py
│       ├── __pycache__
│       │   ├── __init__.cpython-37.pyc
│       │   └── test_handler.cpython-37.pyc
│       └── test_handler.py

hello_worldの動作確認

まずは自動的に作成された、hello_world関数を実行してみます。

# ライブラリのインストール
$ pip install -r requirements.txt -t hello_world/build/
$ cp hello_world/*.py hello_world/build/
# イベントの生成
$ sam local generate-event apigateway aws-proxy > api_event.json
# テスト実行
$ sam local invoke HelloWorldFunction --event api_event.json
2018-10-25 18:31:13 Invoking app.lambda_handler (python3.6)

Fetching lambci/lambda:python3.6 Docker container image......
.
.
.

{"statusCode": 200, "body": "{\"message\": \"hello world\", \"location\": \"IP Address\"}"}

無事に動作確認ができました。

実際にS3とDynamoDBを扱う部分を実装

LocalStackコンポーネントの作成

今回はS3とDynamoDBをエミュレートするために、LocalStackを使用しました。 https://github.com/localstack/localstack

LocalStack自体はdockerで立ち上げます。

以下のようにdocker-compose.ymlファイルを作成します。

version: "3.3"

services:
  localstack:
    container_name: localstack
    image: localstack/localstack
    ports:
      - "4569:4569"
      - "4572:4572"
    environment:
      - SERVICES=dynamodb,s3
      - DEFAULT_REGION=ap-northeast-1
      - DOCKER_HOST=unix:///var/run/docker.sock

s3とdynamodbがそれぞれ使用するポートはこちら

  • s3:4572
  • dynamodb:4569

LocalStackの起動 ※初回はイメージのダウンロードがあるため、時間がかかります。

$ docker-compose up

LocalStack用credentialの作成

LocalStack用にcredential情報を追加します。

[localstack]
aws_access_key_id = dummy
aws_secret_access_key = dummy
[profile localstack]
region = ap-northeast-1
output = json

LocalStack上にDynamoDBのテーブルを作成

AWSのドキュメントを参考にテーブルを作成します。 https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Tools.CLI.html

$ aws dynamodb create-table \
>     --table-name Music \
>     --attribute-definitions \
>         AttributeName=Artist,AttributeType=S \
>         AttributeName=SongTitle,AttributeType=S \
>     --key-schema AttributeName=Artist,KeyType=HASH AttributeName=SongTitle,KeyType=RANGE \
>     --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 \
>     --endpoint-url http://localhost:4569 --profile localstack

$ aws dynamodb list-tables --endpoint-url http://localhost:4569 --profile localstack
TABLENAMES      Music

LocalStack上のS3にテストデータを配置

LocakStackのS3にバケットの作成と、実ファイルの配置を行います。

# bucketの作成
$ aws s3 mb s3://music/ --endpoint-url=http://localhost:4572 --profile localstack
make_bucket: music
# テストデータのput
$ aws s3 cp ./testdata.json s3://music/rawdata/testdata.json --endpoint-url=http://localhost:4572 --profile localstack
upload: ./testdata.json to s3://music/rawdata/testdata.json  

テストデータは以下の内容で作成しました。

{
        "Artist": "Acme Band",
        "SongTitle": "Happy Day",
        "AlbumTitle": "Songs About Life"
}

関数のベースを作成

まずは依存関係にboto3を追加します。 boto3PythonでS3やDynamoDBなどのリソースを扱うためのライブラリです。 https://github.com/boto/boto3

$ pip install boto3
$ pip freeze > requirements.txt

関数の作成 今回はs3_dynamoという関数にしました。

$ mkdir s3_dynamo
$ touch s3_dynamo/__init__.py s3_dynamo/app.py

SAMのテンプレートのResourcesセクションに、作成した関数を追記します。

Resources:
    S3DynamoFunction:
      Type: AWS::Serverless::Function
      Properties:
          CodeUri: s3_dynamo/build/
          Handler: app.lambda_handler
          Runtime: python3.6

ローカル実行用のプロファイルとして、以下のファイルも合わせて作成しました。

{
  "S3DynamoFunction": {
    "AWS_SAM_LOCAL": true
  }
}

S3イベントの作成

SAM CLIの機能で各Lambda関数のイベントを生成できるようになっているので、S3をputするイベント定義を作成します。 https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/test-sam-cli.html

わかりにくいですが、 --keyにはS3のバケット配下のパスを渡します。

$ sam local generate-event s3 put --bucket music --key rawdata/testdata.json --region ap-northeast-1 > s3_event.json 

出力されたイベントファイルがこちら。 awsRegion,bucket.name,object.key が合っていれば正しくLambda側で判定できそうです。

{
  "Records": [
    {
      "eventVersion": "2.0",
      "eventSource": "aws:s3",
      "awsRegion": "ap-northeast-1",
.
.省略
.
      "s3": {
        "s3SchemaVersion": "1.0",
        "configurationId": "testConfigRule",
        "bucket": {
          "name": "music",
          "ownerIdentity": {
            "principalId": "EXAMPLE"
          },
          "arn": "arn:aws:s3:::music"
        },
        "object": {
          "key": "rawdata/testdata.json",
          "size": 1024,
          "eTag": "0123456789abcdef0123456789abcdef",
          "sequencer": "0A1B2C3D4E5F678901"
        }
      }
    }
  ]
}

S3からの読み込み処理を実装

まずはS3からファイルを読み取れることを確認するために、以下のようにS3からファイルを読んで標準出力に表示する部分だけを実装します。

import os
import json
import boto3
import pprint
import urllib.parse

if os.getenv("AWS_SAM_LOCAL"):
    dynamodb = boto3.resource(
        'dynamodb',
        endpoint_url='http://host.docker.internal:4569/'
    )
    s3 = boto3.client(
        's3',
        endpoint_url='http://host.docker.internal:4572/'
    )
else:
    dynamodb = boto3.resource('dynamodb')
    s3 = boto3.client('s3')


def lambda_handler(event, context):
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
    print("[bucket]: " + bucket + " [key]: " + key)

    try:
        response = s3.get_object(Bucket=bucket, Key=key)
        d = json.loads(response['Body'].read())
        pprint.pprint(d)
    except Exception as e:
        print(e)
        raise e

env-local.jsonを使用して実行するため、今回はS3 / DynamoDBとしては以下の定義が使われます。 sam localで実行する場合もdocker内部から実行されるようで、ホスト名はhost.docker.internalを使っています。

    dynamodb = boto3.resource(
        'dynamodb',
        endpoint_url='http://host.docker.internal:4569/'
    )
    s3 = boto3.client(
        's3',
        endpoint_url='http://host.docker.internal:4572/'
    )

実際に実行してみます。

$ pip install -r requirements.txt -t s3_dynamo/build
$ cp s3_dynamo/*.py s3_dynamo/build/
$ sam local invoke S3DynamoFunction --event s3_event.json --profile localstack

# result
[bucket]: music [key]: rawdata/testdata.json
{'AlbumTitle': 'Songs About Life',
 'Artist': 'Acme Band',
 'SongTitle': 'Happy Day'}

無事にbuket、keyが渡され、S3ファイルの中身を表示することができています。

DynamoDBへputするコードの実装

以下が最終型です。

import os
import json
import boto3
import pprint
import urllib.parse

if os.getenv("AWS_SAM_LOCAL"):
    dynamodb = boto3.resource(
        'dynamodb',
        endpoint_url='http://host.docker.internal:4569/'
    )
    s3 = boto3.client(
        's3',
        endpoint_url='http://host.docker.internal:4572/'
    )
else:
    dynamodb = boto3.resource('dynamodb')
    s3 = boto3.client('s3')

table = dynamodb.Table('Music')


def lambda_handler(event, context):
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
    print("[bucket]: " + bucket + " [key]: " + key)

    try:
        response = s3.get_object(Bucket=bucket, Key=key)
        d = json.loads(response['Body'].read())
        pprint.pprint(d)

        table.put_item(
            Item=d
        )
    except Exception as e:
        print(e)
        raise e

わかりにくいですが、以下のコードを追加しています。

table = dynamodb.Table('Music')
.
.
.
        table.put_item(
            Item=d
        )

LambdaのTimeoutを伸ばす

デフォルトではLambdaの実行時間上限は3秒になっているので、適当に伸ばしておきます。

Globals:
    Function:
        Timeout: 100

結果確認

再度実行してみます。

$ cp s3_dynamo/*.py s3_dynamo/build/
$ sam local invoke S3DynamoFunction --event s3_event.json --profile localstack

AWS CLIでMusicテーブルの中身を確認

$ aws dynamodb scan --table-name Music --endpoint-url http://localhost:4569 --profile localstack
1       1
ALBUMTITLE      Songs About Life
ARTIST  Acme Band
SONGTITLE       Happy Day

無事にDynamoDBのテーブルにデータがputされています。

おわりに

今までLambdaのコンソール上で短いコードを書いて実行するといったことはやったことがありましたが、実際にローカルに開発環境を用意して実行してみるという部分は初めてだったので、ハマりどころや分からない部分も多くありました。 実際にSAMを使用してAWS上にデプロイするにはCloudFormationを使うなど、もう少し学ぶところがありそうだという印象でした。 Serverlessのメリットを享受するために、これから吸収していければと思います。

Spring Boot 1.4系から2.0系へのマイグレーションでやったこと

これは2018年08月07日にQiitaに投稿した記事を移行したものです

はじめに

前回は、SpringBoot 1.5からSpring Boot 2.0へのバージョンアップを行いました。 今回は別のプロジェクトで、1.4から2.0に上げた際にハマった事などを書いていきます。

yuki10.hatenablog.com

今回主に苦労したのは、

  • 問題の原因が 1.4->1.5の部分なのか、1.5->2.0の部分なのかの切り分けに時間がかかった
  • Thymeleafの影響が大きいのでテストが大変
  • SpringSessionでのシリアライズ/デシリアライズまわり
  • 本番デプロイ時の対策

あたりです。

Spring Bootのバージョンを上げる

spring-boot-starter-parentのバージョンを更新

pom.xmlでSpringBootのバージョンを指定します。

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.3.RELEASE</version>
    <relativePath />
  </parent>

HikariPCの依存関係を削除

元々HikariPCを使用していたので、依存関係から削除します。

    <dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    </dependency>

コンパイルしてエラーを潰していく

SpringBootServletInitializerが見つからない

1.png

SpringBootServletInitializer のパッケージが変わっているので、再importする

org.springframework.boot.context.embedded.が見つからない

2.png

パッケージが org.springframework.boot.web.servlet. に変わっているので再importする

DataSourceBuilderが見つからない

3.png

パッケージが変わっているので再importする

WebMvcConfigurerAdapterの非推奨化

extends WebMvcConfigurerAdapterimplements WebMvcConfigurer に変更する

@EnableWebMvcSecurityの非推奨化

@EnableWebSecurity に変更する。

https://docs.spring.io/spring-security/site/docs/current/reference/html/mvc.html

org.apache.velocity.appが見つからない

4.png

velocity から mustache に移行する。

-    <dependency>
-   <groupId>org.apache.velocity</groupId>
-   <artifactId>velocity</artifactId>
-    </dependency>
+    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter-mustache</artifactId>
+    </dependency>

velocityのテンプレートファイル *.vm を mustacheのテンプレートファイル *.mustache に置換する。 ※ここは application.properties などで変更可

mustacheのテンプレートの形式に合わせて

${hoge}
↓
{{hoge}}

こんな感じで全部置き換える

shellとかで一括置換してもいいけど、IntelliJのリファクタでやると呼び出し元コードも発見して教えてくれるので数が少なければIntelliJのリファクタがいいかもしれない。

org.jsonが見つからない

5.png

以下の依存関係を追加する

  <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-json</artifactId>
    </dependency>

以下のパッケージで importしなおす org.springframework.boot.configurationprocessor.json.JSONObject

SpringApplication.runでエラー

6.png

SpringApplication.runの引数が変更になっている

以下のように修正

        public static void main(String[] args) {
-               Object[] objects = { HogeApplication.class, FugaService.class };
-               SpringApplication.run(objects, args);
+               final SpringApplication application = new SpringApplication(HogeApplication.class, FugaService.class);
+               application.run(args);
        }

ついでに以下のクラスを継承する

SpringBootServletInitializer

JPAのメソッド変更まわりの対応

7.png

黙々と直していく

https://spring.io/blog/2017/06/20/a-preview-on-spring-data-kay#improved-naming-for-crud-repository-methods

AutoConfigureTestDatabaseが見つからない

8.png

import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureTestDatabase; から import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; にパッケージを変更

Thymeleafのマイグレーション

https://www.thymeleaf.org/doc/articles/thymeleaf3migration.html

th:substitutebyを th:replaceに置き換える

こんな感じで機械的

find src/main/resources -type f -name "*.html" -print | xargs sed -i  -e 's/th:substituteby/th:replace/g'

linkタグのcss読み込みのtype="text/css"を削除

こんな感じで機械的

find src/main/resources -type f -name "*.html" -print | xargs sed -i  -e 's@type=\"text/css\"@@g'

inline="text" / inline="inline"を削除

一応中身を見ながら、削除していく。

Scriptタグ内のthymeleafが展開されない

こういうやつ

<script>(window.dataLayer || (window.dataLayer = [])).push(<span th:remove="tag" th:utext="${hoge}"/>)</script>

こんな風に修正する

<script type="text/javascript" th:inline="javascript">/*<![CDATA[*/
     (window.dataLayer || (window.dataLayer = [])).push(/*[(${hoge})]*/)
 /*]]>*/</script>

その他

SpringBootでは@EnableWebMvcがついていたら削除する

SessionScope

SpringSecurityの onAuthenticationSuccess 実行時に @SessionScope を参照できなくてエラー Error creating bean with name 'user': Scope 'session' is not active for the current thread;

以下のようなコンフィグファイルを作成しておいておく

package jp.hoge;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextListener;

import javax.servlet.annotation.WebListener;

@Configuration
@WebListener
public class WebRequestContextListener extends RequestContextListener {
}

Hibernate SaveAndFlushメソッドでエラー

java.sql.SQLSyntaxErrorException: Table 'hoge.hibernate_sequence' doesn't exist
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:536)
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:513)
    at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:115)
    at com.mysql.cj.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:1983)
    at com.mysql.cj.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1826)
    at com.mysql.cj.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:1923)
    at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)

GenerationType を変更する

-    @GeneratedValue(strategy=GenerationType.AUTO)
+    @GeneratedValue(strategy=GenerationType.IDENTITY)

実行中にHibernateエラー

org.springframework.dao.InvalidDataAccessResourceUsageException: error performing isolated work; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: error performing
 isolated work
        at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:242)
        at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:225)
        at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:527)
        at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
        at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
        at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:153)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
        at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:135)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
        at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
        at org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.invoke(SurroundingTransactionDetectorMethodInterceptor.java:61)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
        at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
        at com.sun.proxy.$Proxy127.save(Unknown Source)
        at jp.hoge.service.FugaService.execute(FugaService.java:218)
        at jp.hoge.controller.FugaController.execute(FugaController.java:101)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)

HibernateのGenerator mappingsが変更になっている。

application.propertiesに以下の設定を追加することで対処

spring.jpa.hibernate.use-new-id-generator-mappings=false

STGなどへのデプロイ後

プロファイルの読み込みエラー

logback-spring.xml<springProfile> でプロファイルが正しく読み込めずにエラー executableJarの場合、-Dspring-boot.run.profiles=環境名 のように定義すればいい。

今回のプロジェクトではtomcatにwarをデプロイするので、 application.propertiesspring-boot.run.profiles=環境名 みたいに定義してやる。

実際のところは、pom.xmlでwarファイル生成時にプロファイルを分けているので、以下のように定義しています。

spring-boot.run.profiles=${spring.profiles.active}
<profiles>
        <profile>
            <id>local</id>
            <properties>
                <spring.profiles.active>local</spring.profiles.active>
            </properties>
        </profile>
        <profile>
            <id>stg</id>
            <properties>
                <spring.profiles.active>stg</spring.profiles.active>
            </properties>
        </profile>
</profiles>

ビルド時にプロファイル

mvn package -Pstg

という感じで、 application.properties に埋め込んでいる。

SpringSecurity経由でのログイン後にSession取得後のUserオブジェクトのメンバ変数が全てnullになっている

Userのメンバ変数にアクセスしようとしてNullPointerExepotionが発生 とはいえ、Redisの中身を見てもシリアライズされているため読めない・・・

※Memberは @SessionScope

一旦、以下のような感じでJSONに変換してRedisに登録してくれるSerializerを作成する

@ConditionalOnProperty(name = "spring.session.store-type", havingValue = "redis", matchIfMissing = false)
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 20 * 24 * 60 * 60) //default=1800 -> 20days
@Configuration
public class HttpSessionConfig implements BeanClassLoaderAware {

   @Autowired
   private LettuceConnectionFactory lettuceConnectionFactory;

   private ClassLoader classLoader;

   @Bean
   public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
    final ObjectMapper mapper = new ObjectMapper()
            .registerModules(SecurityJackson2Modules.getModules(classLoader))
            .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
    return new GenericJackson2JsonRedisSerializer(mapper);
  }

      // Elasticache用
      @Bean
      public static ConfigureRedisAction configureRedisAction() {
          return ConfigureRedisAction.NO_OP;
      }

    @Override
    public void setBeanClassLoader(final ClassLoader classLoader) {
        this.classLoader = classLoader;
    }
}

そうするとlogin完了後からControllerに遷移する際に、全フィールドnullの状態でRedisにセットされていた。

ログイン後のControllerで User オブジェクト内の idがnullかどうか判定し、nullであれば詰め直すように対応。 ログイン以降は問題なくセッションを扱えている。

本当はJSON形式でセッション情報を持てるようにしたかったものの、Userクラスの構造が複雑なネスト構造になっていたため、時間との兼ね合いで断念・・・ Userクラスに情報をもたせすぎ・・・・

本番デプロイ時の対応

SpringSession のバージョンアップに伴い、内部的にシリアライズ・デシリアライズで扱う SerialVersionUID が変わっているらしいので、バージョンアップしたコードをデプロイすると、既にログイン中のユーザーはセッション情報をデシリアライズできなくて詰む。

なので、デシリアライズするときの対応を入れる。

https://sdqali.in/blog/2016/11/02/handling-deserialization-errors-in-spring-redis-sessions/

少し記事が古く、依存ライブラリが変更になっているので、以下の通り変更している。

- public class HttpSessionConfig {
+ public class HttpSessionConfig extends RedisHttpSessionConfiguration {

+   @Autowired
+   RedisTemplate<Object, Object> redisTemplate;
+   @Bean
+   @Override
+   public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
+       return super.springSessionRepositoryFilter(new SafeDeserializationRepository<>(sessionRepository, redisTemplate));
+   }
public class SafeDeserializationRepository<S extends Session> implements SessionRepository<S> {
    private final SessionRepository<S> delegate;
    private final RedisTemplate<Object, Object> redisTemplate;
     private static final String BOUNDED_HASH_KEY_PREFIX = "spring:session:sessions:";
     public SafeDeserializationRepository(SessionRepository<S> delegate,
            RedisTemplate<Object, Object> redisTemplate) {
        this.delegate = delegate;
        this.redisTemplate = redisTemplate;
    }
     @Override
    public S createSession() {
        return delegate.createSession();
    }
     @Override
    public void save(S session) {
        delegate.save(session);
    }
     @Override
    public S findById(String id) {
        try {
            return delegate.findById(id);
        } catch(SerializationException e) {
            log.info("Deleting non-deserializable session with key {}", id);
            redisTemplate.delete(BOUNDED_HASH_KEY_PREFIX + id);
            return null;
        }
    }
     @Override
    public void deleteById(String id) {
        delegate.deleteById(id);
    }
}

これで既存のユーザーは、一度ログアウトさせられ、再ログインで新しいSessionデータをRedisに保存し、以降はそちらを参照する。

まとめ

大体、上記の対応で本番環境で動作するようになりました。 (他にもある気がするけど)

また、本来であればTomcatは8.5系以上にしなければならないのですが、8.0で問題なさそうであったため、Tomcatのバージョンアップは見送りました。

SpringBootのメジャーバージョンをいくつか飛ばしてしまうと、とてもつらいのでバージョンアップはこまめにやろう・・・!!

参考ページ

Spring Boot 1.5系から2.0系へのマイグレーションでやったこと

これは2018年07月01日にQiitaに投稿した記事を移行したものです

はじめに

こちらでKotlin対応を行ったプロジェクトに対して、SpringBoot 2.0.2にアップデートを行いました。 ここではそのときに発生した作業などについて書いていきます。

※Spring Boot2.0からはJava8以降が必須になっています。

また、このプロジェクトはREST APIのサーバーとして動作しているため、Thymeleafのマイグレーションなどは回避することができました。 別途、Spring MVCのプロジェクトをマイグレーションしているので、そのあたりは別で書こうと思います。

以下のサイトや記事を主に参考にさせていただきました。

まずはSpring Bootのバージョンアップ

pom.xmlを修正してSpring Bootのバージョンを上げていきます。

変更点を抜粋

    <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
 -    <version>1.5.8.RELEASE</version>
 +    <version>2.0.2.RELEASE</version>
      <relativePath />
    </parent>

・・・

      <dependency>
        <!-- https://docs.spring.io/spring-session/docs/current/reference/html5/guides/java-redis.html -->
        <groupId>org.springframework.session</groupId>
 -      <artifactId>spring-session</artifactId>
 +      <artifactId>spring-session-data-redis</artifactId>
      </dependency>

・・・

      <!-- https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide#jackson--json-support -->
 +    <dependency>
 +      <groupId>org.springframework.boot</groupId>
 +      <artifactId>spring-boot-starter-json</artifactId>
 +    </dependency>

・・・
      <!- 元々HikariPCを使用していたので依存関係を削除 -->
      <!-- https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide#configuring-a-datasource -->
 -    <dependency>
 -      <groupId>com.zaxxer</groupId>
 -      <artifactId>HikariCP</artifactId>
 -    </dependency>

・・・

      <!-- ここはテストが終わったら削除 ->
      <!-- https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide#before-you-start -->
 +    <dependency>
 +      <groupId>org.springframework.boot</groupId>
 +      <artifactId>spring-boot-properties-migrator</artifactId>
 +      <scope>runtime</scope>
 +    </dependency>

ApplicationクラスのWebMvcConfigurerAdapterを使わないようにする

- public class XxxApplication extends WebMvcConfigurerAdapter {
+ public class XxxApplication implements WebMvcConfigurer{

ビルド->修正->再ビルド

pom.xmlの設定が終わったら、ビルドして修正を繰り返していきます。

JPA RepositoryのfindOneでコンパイルエラー

まずは以下のようなエラーが発生しました。

Error:(41, 41) Kotlin: Type inference failed: fun <S : #{テーブル名}!> findOne(p0: Example<S!>!): Optional<S!>!
cannot be applied to
(Long)
Error:(41, 49) Kotlin: Type mismatch: inferred type is Long but Example<(???..???)>! was expected

これはSpring DataのCRUDメソッドが変更になっていることに起因します。 https://spring.io/blog/2017/06/20/a-preview-on-spring-data-kay#improved-naming-for-crud-repository-methods

1行目のエラーは、findOneからfindByIdに変更する。 2行目のエラーは、findByIdの戻り値がJavaのOptionalになっているので、Optionalからgetする。

同様に、save, deleteの引数もListを渡すものからEntityを渡すものに変わっているため、代わりに saveAll, deleteAll メソッドを呼ぶようにする。 もちろん、1レコードのみが影響を受けるのであれば、savedeleteを使う方がいいので、マイグレーションに割ける時間と相談で。

ConfigureRedisActionを解決できない

Error:(13, 53) java: パッケージorg.springframework.session.data.redis.configは存在しません

順番が前後していますが、ElastiCacheを利用するために設定している以下の定義で、ConfigureRedisActionが解決できなくなっていました。 これはspring-session-data-redisというパッケージに切り出されたためです。

https://docs.spring.io/spring-session/docs/current/reference/html5/guides/java-redis.html

   @Bean
    public static ConfigureRedisAction configureRedisAction() {
        return ConfigureRedisAction.NO_OP;
    }

修正内容はpom.xmlspring-sessionの部分を参照。 また、lettuceの依存関係を追加していないのは、元々spring-boot-starter-data-redisが依存関係に含まれているためです。

org.hibernate.validator.constraints.NotBlankなどの非推奨化

これに合わせて、javax.validation.constraints.NotEmptyなどを使うように置き換えています。

jdbcのautoconfigureでエラー

Error:(7, 51) java: シンボルを見つけられません
  シンボル:   クラス DataSourceBuilder
  場所: パッケージ org.springframework.boot.autoconfigure.jdbc

これは複数のデータソースを扱っているために発生しているようでした。

hoge.datasource.driver-class-name        = com.mysql.jdbc.Driver
hoge.datasource.url                    = jdbc:mysql://127.0.0.1:33306/hoge
hoge.datasource.username                = hoge
hoge.datasource.password                = fuga
# その他は省略
# 上記x複数データソース分

元々は以下のように設定されていました

 @Configuration
 @EnableTransactionManagement
 @EnableJpaRepositories(entityManagerFactoryRef = "hogeEntityManagerFactory", transactionManagerRef = "hogeTransactionManager", basePackages = {
        "jp.xxx.data.hoge.repository" })
 public class HogeDatabaseConfig extends AbstractDatabaseConfig {
 
    @Bean(name = "hogeDataSource")
    @ConfigurationProperties(prefix = "hoge.datasource")
    public DataSource hogeDataSource() {
        return DataSourceBuilder.create().build();
    }
 
    @Bean(name = "hogeEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean hogeEntityManagerFactory(EntityManagerFactoryBuilder builder,
        return builder.dataSource(hogeDataSource)
                .packages("jp.xxx.data.hoge.entity").persistenceUnit("hoge")
                .properties(buildProperties()).build();
    }
 
    @Bean(name = "hogeTransactionManager")
    public PlatformTransactionManager hogeTransactionManager(
            @Qualifier("hogeEntityManagerFactory") EntityManagerFactory hogeEntityManagerFactory) {
        return new JpaTransactionManager(hogeEntityManagerFactory);
    }
 }

が、以下のように修正しました。 この部分についてはもう少しうまいやり方があるんじゃないかなーとは思っています。

 @Configuration
 @ConfigurationProperties(prefix = "hoge.datasource")
 @EnableTransactionManagement
 @EnableJpaRepositories(entityManagerFactoryRef = "hogeEntityManagerFactory", transactionManagerRef = "hogeTransactionManager", basePackages = {
        "jp.xxx.data.hoge.repository" })
 public class HogeDatabaseConfig extends AbstractDatabaseConfig {
 
    @Autowired
    private Environment env;

    @Bean(name = "hogeDataSource")
    public DataSource hogeDataSource() {
        final DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(env.getProperty("hoge.datasource.driver-class-name"));
        dataSource.setUrl(env.getProperty("hoge.datasource.url"));
        dataSource.setUsername(env.getProperty("hoge.datasource.username"));
        dataSource.setPassword(env.getProperty("hoge.datasource.password"));
        return dataSource;
    }
 
    @Bean(name = "hogeEntityManagerFactory")
    public EntityManagerFactory hogeEntityManagerFactory(
            @Qualifier("hogeDataSource") DataSource hogeDataSource) {
        final LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
        factoryBean.setDataSource(hogeDataSource);
        factoryBean.setPersistenceUnitName("hoge");
        factoryBean.setPackagesToScan("jp.xxx.data.hoge.entity");
        factoryBean.setJpaPropertyMap(buildProperties());
        factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        factoryBean.afterPropertiesSet();
        return factoryBean.getNativeEntityManagerFactory();
    }
 
    @Bean(name = "hogeTransactionManager")
    public PlatformTransactionManager hogeTransactionManager(
            @Qualifier("hogeEntityManagerFactory") EntityManagerFactory hogeEntityManagerFactory) {
        return new JpaTransactionManager(hogeEntityManagerFactory);
    }
 }

ここでTomcatは起動

Tomcatは起動したものの、以下のエラーログが流れていました。

java.lang.ClassCastException: jp.xxx.service.HogeService$Fuga cannot be cast to org.springframework.core.io.support.ResourceRegion
    at org.springframework.http.converter.ResourceRegionHttpMessageConverter.writeResourceRegionCollection(ResourceRegionHttpMessageConverter.java:182)
    at org.springframework.http.converter.ResourceRegionHttpMessageConverter.writeInternal(ResourceRegionHttpMessageConverter.java:139)
    at org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:102)
    at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:272)
    at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:180)
    at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:82)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:119)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:877)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:783)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:866)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:635)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)

データソースにはPostgreSQLもあるので発生しているようです。

これはhibernate.propertiesを作成して配置しておくことで回避しました。

hibernate.jdbc.lob.non_contextual_creation = true

ここまでで起動までにエラーは発生しなくなりました。

突然のClassCastException

java.lang.IllegalArgumentException: Parameter specified as non-null is null: method jp.xxx.controller.HogeController.createFuga, parameter form
    at jp.xxx.controller.HogeController.createCampaign(HogeController.kt)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:209)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:136)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:877)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:783)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974)
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:877)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:661)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)

formの引数の型をHogeFormからHogeForm?のnull許容型に変換して対応しました。

日付型の変換エラー

Failed to convert value of type 'java.lang.String[]' to required type 'java.util.Date'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.util.Date] for value '2018-06-11T00:00:00'; nested exception is java.lang.IllegalArgumentException

ここは少しハマって原因がよくわからない部分ではあったのですが、データを受け取っていたオブジェクトがKotlinのDataクラスだったものを、普通のKotlinクラスにすることで動作しました。

PUTだとformの値が渡らない

原因を調べる時間がなかったのでPUTしている部分をPOSTに書き換えました・・・

REST Controllerの戻り値の型がList<Object>だとJSONに変換できずにエラーになる。

エラーログ取り忘れ。

元々はこんな実装 そもそもListで返すなと・・・

    @GetMapping("/hoge")
    public List<Object> hoge() {
        return hogeService.hoges();
    }

戻りの型が結構複雑だったので、Controller側でJSONにして返すようにしました。

    @GetMapping("/hoge")
    public String hoge() throws JsonProcessingException {
        final List<Object> hoges = hogeService.hoges();
        final ObjectMapper mapper = new ObjectMapper();
        return mapper.writeValueAsString(hoges);
    }

以上で、マイグレーションが完了し、移行させることができました。

SpringBootをKubernetes上で動かしてみる

これは2018年02月26日にQiitaに投稿した記事を移行したものです

about

SoftwareDesign 2018/3号を読んでいてKubernetesに少し興味を持ったので試してみました。 色々ハマりどころがあるかと思いましたが、環境構築〜デプロイ、スケールまで割りとすんなりと進めることができました。

環境

インストール

VirtualBox

$ brew cask install virtualbox

Hish Sierraだとインストール中に止まってしまうことがあります。 その場合はシステム環境設定 > セキュリティとプライバシーOracleがブロックされたみたいなものが表示されているので「許可」するとインストールできます。 (スクリーンショット撮り忘れ)

minikube

$ brew cask install minikube

動作確認

minikubeの起動

$ minikube start
Starting local Kubernetes v1.8.0 cluster...
Starting VM...
Getting VM IP address...
Moving files into cluster...
Setting up certs...
Connecting to cluster...
Setting up kubeconfig...
Starting cluster components...
Kubectl is now configured to use the cluster.
Loading cached images from config file.

VirtualBox上でminikubeが作成されて起動していることが確認できます

スクリーンショット 2018-02-24 17.28.48.png

デプロイメントの作成

# deployment
$ kubectl run hello-minikube --image=k8s.gcr.io/echoserver:1.4 --port=8080
deployment "hello-minikube" created

$ kubectl expose deployment hello-minikube --type=NodePort
service "hello-minikube" exposed

$ kubectl get pod
NAME                              READY     STATUS    RESTARTS   AGE
hello-minikube-7844bdb9c6-88hl4   1/1       Running   0          2m

この状態でダッシュボードを確認してみると

# ダッシュボードの起動
$ minikube dashboard

デプロイメントとポッドが作成される。 スクリーンショット 2018-02-24 17.35.13.png

# URLの確認
$ minikube service hello-minikube --url

$ curl $(minikube service hello-minikube --url)
CLIENT VALUES:
client_address=172.17.0.1
command=GET
real path=/
・
・
・
# サービスの削除
$ kubectl delete service hello-minikube
service "hello-minikube" deleted

# デプロイメントの削除
$ kubectl delete deployment hello-minikube
deployment "hello-minikube" deleted

# minikube終了
$ minikube stop
Stopping local Kubernetes cluster...
Machine stopped.

Spring BootのDocker環境を用意する

今回は公式チュートリアルの完成版を利用します。 https://spring.io/guides/gs/spring-boot-docker/

スターターをclone

$ git clone https://github.com/spring-guides/gs-spring-boot-docker.git
# 完成版を使う
$ cd gs-spring-boot-docker/complete
$ ./gradlew build docker
・
・
・
:processTestResources NO-SOURCE
:testClasses
:test
:check
:build
:dockerClean UP-TO-DATE
:dockerPrepare
:docker

BUILD SUCCESSFUL

Total time: 1 mins 37.278 secs

動かしてみる

$ docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
springio/gs-spring-boot-docker   latest              d486fdb38288        4 minutes ago       116MB

$ docker run -p 8080:8080 -t springio/gs-spring-boot-docker

docker上でSpringBootが起動しました。

スクリーンショット 2018-02-24 21.11.02.png

kubernetes上で動かす

環境変数の設定

$ minikube start
$ eval $(minikube docker-env)
# この状態では一時的にローカルPCからdockerへの接続などができなくなりますが、ターミナルを再起動すれば設定が消えて元にもどります。

サービスの設定ファイルを以下の内容で作成する。

apiVersion: v1
kind: Service
metadata:
  name: hellojavakubernetes
  labels:
    app: hellojavakubernetes
    tier: backend
spec:
  type: NodePort
  ports:
    # the port that this service should serve on
  - port: 8080
  selector:
    app: hellojavakubernetes
    tier: backend

サービスの登録

$ kubectl create -f kubernetes-service.yaml

hellojavakubernetesサービスが登録されました。

スクリーンショット 2018-02-24 21.43.07.png

デプロイメント設定ファイルを以下の内容で作成する。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: hellojavakubernetes
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: hellojavakubernetes
        tier: backend
    spec:
      containers:
      - name: hellojavakubernetes
        image: springio/gs-spring-boot-docker
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        ports:
        - containerPort: 8080

デプロイしてみる。

$ kubectl create -f kubernetes-deployment.yaml
deployment "hellojavakubernetes" created

$ kubectl get deployment
NAME                  DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
hellojavakubernetes   1         1         1            1           3m

hellojavakubernetesというデプロイメントが作成され、ポッドも作成されています。

スクリーンショット 2018-02-24 21.54.37.png

URLを調べる。

$ minikube service hellojavakubernetes --url
http://192.168.99.100:31830

表示されたURLにアクセスする スクリーンショット 2018-02-24 22.04.06.png

正しく表示されました。 ここまで詰まることなく進めることができました。

アプリケーションのスケーリング

せっかくなのでスケーリングしてみます。

$ kubectl get deployments
NAME                  DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
hellojavakubernetes   1         1         1            1           17h

# replicasを指定する
$ kubectl scale --replicas=2 -f kubernetes-deployment.yaml 
deployment "hellojavakubernetes" scaled

# deploymentsの数が2に増えました。
$ kubectl get deployments
NAME                  DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
hellojavakubernetes   2         2         2            2           17h

ポッドの数が2つに増えていることがわかります。

スクリーンショット 2018-02-25 15.48.27.png

続いて設定ファイルから変更してみます。 上の方で作成したkubernetes-deployment.yamlreplicas3に変更します。

# 設定の適用
$ kubectl apply -f kubernetes-deployment.yaml 
Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
deployment "hellojavakubernetes" configured

$ kubectl get deployments
NAME                  DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
hellojavakubernetes   3         3         3            3           17h

こちらも無事スケールできました。

後始末

終了する

$ kubectl delete service,deployment hellojavakubernetes
$ minikube stop
$ minikube delete

本番で運用するにはまだまだ調べることが多そうですがKubernetes自体はかなり完成されているなという印象でした。

参考ページ

[minikube] https://github.com/kubernetes/minikube

[Spring Boot with Docker] https://spring.io/guides/gs/spring-boot-docker/

[getting started] https://www.bluefyre.io/getting-started-springboot-kubernetes/

Spring Boot (Kotlin) はじめの一歩

これは2018年01月30日にQiitaに投稿した記事を移行したものです

目的

Spring Bootを学習するきっかけが欲しいと友人にたのまれたので、 実際にペアプロしながら進めていくための資料として作成しました。

Spring Boot (Kotlin)入門 - MVC - Rest - DB Access

使用するもの

  • JDK 8
  • IntelliJ Idea
  • Kotlin
  • Gradle
  • Spring Boot
  • Doma2 (JPAのほうが最初の設定は楽だった・・)

IntelliJ Ideaの代わりにSTS + Kotlin PluginでもOKですが、手順は記載していません。 ※Mavenの場合、dependencyなどをpom用に読み替えてください。 ※Doma2のEntity / Dao実装はKotlinではなくJavaになっています。

環境構築

IntelliJ Ideaのインストール

こちらから https://www.jetbrains.com/idea/

Spring Bootプロジェクトの雛形を作成する

Spring Initializrで雛形を作成する https://start.spring.io/

以下の内容で生成します。

  • Gradle Project
  • Kotlin
  • Spring Boot 1.5.9
  • Dependencies => Web

SpringInitializer.PNG

Group / Artifactはお好みで

雛形のzipファイルがダウンロードされるので、適当な場所に展開しておきます。

プロジェクトを開く

IntelliJを起動してImport Project

Idea.PNG

展開したプロジェクトの雛形の中にあるbuild.gradleを指定する。

import.PNG

Use auto-importにチェックを入れてOK

use_auto_import.PNG

ビルドが始まるので、synced successfullyになればOK

build.PNG

View -> Tool Window -> Gradleを選択してGradleのツールウィンドウを開く。 Gradleツールウィンドウ内のdemo -> Tasks -> application -> bootRun を右クリックして Run 'demo[boot Run]'で起動する

bootRun.PNG

14:44:18: Executing task 'bootRun'...

:compileKotlin
:compileJava NO-SOURCE
:copyMainKotlinClasses
:processResources
:classes
:findMainClass
:bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.9.RELEASE)

・
・
・

s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2018-01-20 14:44:28.125  INFO 13432 --- [           main] com.example.demo.DemoApplicationKt       : Started DemoApplicationKt in 2.45 seconds (JVM running for 2.75)

※この時点ではControllerなどが無いので、何もできない。

Spring Boot起動中にIntelliJでコード修正が反映されるようにbuild.gradleの設定

plugin定義に以下を追加する

 apply plugin: 'idea'
 idea {
   module {
     outputDir file('build/classes/main')
     testOutputDir file('build/classes/test')
   }
 }
 if(project.convention.findPlugin(JavaPluginConvention)) {
    // Change the output directory for the main and test source sets back to the old path
    sourceSets.main.output.classesDir = new File(buildDir, "classes/main")
    sourceSets.test.output.classesDir = new File(buildDir, "classes/test")
 }

一番下のdependenciesに以下を追加する

    compile("org.springframework.boot:spring-boot-starter-thymeleaf")
    compile("org.springframework.boot:spring-boot-devtools")
  • thymeleaf => テンプレートエンジンのthymeleafを使う
  • spring-boot-devtools => オートリロードを有効にする

オートリロードの設定

Setting -> Build -> Compilerを開き、Build project automaticallyにチェックを入れてApply

buildProjectAutomatically.PNG

Windowsの場合:Sthif + Ctrl -> A Macの場合:Command + Ctrl + A でウィンドウを開き、Registry..で検索

開いたウィンドウでcompiler.automake.allow.when.app.runningにチェックを入れる

compiler.PNG

ThymeleafでHTMLを返す

Controllerの作成

com.example.demoの下にcontrollerパッケージを作成する。 controllerパッケージの下に、GreetingController.ktを作成する。

package com.example.demo.controller

import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam

@Controller
@RequestMapping("greeting")
class GreetingController {

    @GetMapping("/hello")
    fun hello(
            @RequestParam(value = "name", required = false, defaultValue = "world") name: String,
            model: Model): String {
        model.addAttribute("name", name)
        return "greeting"
    }
}

html(Thymeleaf)の作成

src/main/resources/templatesの下にgreeting.htmlを作成する

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" http-equiv="Content-Type" content="text/html" />
    <title>Getting Started: Saving Web Content</title>
</head>
<body>
    <p th:text="'Hello, ' + ${name} + '!'" />
</body>
</html>

動作確認

再度Spring Bootを実行する

Webブラウザhttp://localhost:8080/greeting/helloにアクセス

helloworld.PNG

http://localhost:8080/greeting/hello?name=hogeにしてみると、 表示がHello, Hoge!に変わる。

DB環境準備

Virtual Box / Vagrantのインストール

※Windows10 Homeで作業したためDocker for Windowsが使えないので仕方なくVirtual Box / Vagrantを使っています。 Windows10 ProやMacの方はDockerで構築する方が楽なので、VirtualBox / Vagrantの章は飛ばしてdocker-composeからはじめてOKです

Virtual Box https://www.virtualbox.org/

Vagrant https://www.vagrantup.com/downloads.html

プラグインのインストール

$ vagrant plugin install vagrant-vbguest

CentOS7のBoxをインストール・起動

cd [任意のディレクトリ]
vagrant init centos/7

Vagrantfileが生成されるので、ポートフォワード・IPアドレスバインディングを設定する

# 以下の設定を追加する
  config.vm.network "forwarded_port", guest: 3306, host: 3306
  config.vm.network "private_network", ip: "192.168.33.10"
vagrant up

# 終わったら↓でrunningになっていることを確認
vagrant status

# vagrantに接続する
vagrant ssh

Vagrant上のCentOS7にDockerをインストール

https://docs.docker.com/engine/installation/linux/docker-ce/centos/

# インストール確認
docker -v
# docker起動
sudo systemctl start docker
# 動作確認
sudo docker run hello-world
# vagrant起動時にdockerが起動するように
sudo systemctl enable docker

一般ユーザーでもdockerコマンドを使えるようにする

sudo gpasswd -a $USER docker
sudo systemctl restart docker

# 一度再ログインする
exit

続いてdocker-composeをインストールする ※Docker for Mac (Windows)の場合、同時にインストールされるのでスキップしてOK

https://docs.docker.com/compose/install/#install-compose

$ sudo curl -L https://github.com/docker/compose/releases/download/1.18.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose

$ sudo chmod +x /usr/local/bin/docker-compose

$ docker-compose --version

docker-compose.ymlの作成

db:
  image: mysql:5.7
  ports:
    - "3306:3306"
  environment:
    MYSQL_ROOT_PASSWORD: root
  volumes_from:
    - data

data:
  image: busybox:1
  volumes:
    - /var/lib/mysql:/var/lib/mysql # macの場合、適当なディレクトリを指定する

イメージの作成

$ docker-compose up -d
$ docker-compose ps
[vagrant@localhost ~]$ docker-compose ps
     Name                  Command             State            Ports
------------------------------------------------------------------------------
vagrant_data_1   sh                            Exit 0
vagrant_db_1     docker-entrypoint.sh mysqld   Up       0.0.0.0:3306->3306/tcp

mysqlへの接続確認

$ docker exec -it vagrant_db_1 bash
# mysql -uroot -proot

# 適当にデータベースを作成しておく
mysql> create database test;

ホストマシン(Windows / Mac)からMySQLに接続してみる。

設定方法はクライアント次第だけどこんな感じでつながるはず ※Vagrantを使わない場合はHostは172.0.0.1

項目 設定値
Host 192.168.33.10
Database test
User root
Password root

適当にテーブルを作っておく

CREATE TABLE anything(
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(255),
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

プロジェクトにDoma2を設定する

# repositoriesに↓を追加
    maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }

# compileKotlinの前に↓を追加
processResources.destinationDir = compileJava.destinationDir
compileJava.dependsOn processResources

# dependenciesに↓を追加
    compile group: 'mysql', name: 'mysql-connector-java', version: '6.0.6'
    compile('org.seasar.doma.boot:doma-spring-boot-starter:1.0.2')

application.propertiesの設定

# JDBC
spring.datasource.url=jdbc:mysql://192.168.33.10:3306/test
# vagrantを使わない場合はこちら
# spring.datasource.url=jdbc:mysql://172.0.0.1:3306/test
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

# DOMA
doma.dialect=mysql
doma.naming=snake_lower_case

DBアクセスの実装

src/main配下にjavaディレクトリを作成 src/main/java 配下に com.example.demo.entity, com.example.demo.daoパッケージを作成する。

以下のクラスを作成していく

package com.example.demo.entity;

import org.seasar.doma.Entity;
import org.seasar.doma.GeneratedValue;
import org.seasar.doma.GenerationType;
import org.seasar.doma.Id;
import org.seasar.doma.Table;

import java.sql.Timestamp;

@Entity
@Table(name = "anything")
public class AnythingEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Integer id;

    public String name;

    public Timestamp createdAt;

    public Timestamp updatedAt;
}
package com.example.demo.dao;

import com.example.demo.entity.AnythingEntity;
import org.seasar.doma.Dao;
import org.seasar.doma.Insert;
import org.seasar.doma.Select;
import org.seasar.doma.boot.ConfigAutowireable;

import java.util.List;

@ConfigAutowireable
@Dao
public interface AnythingDao {

    @Select
    List<AnythingEntity> selectAll();

    @Insert
    int insert(AnythingEntity anything);
}

===ここからkotlin配下===

com.example.demo.serviceパッケージを作成する

以下のクラスを作成する

package com.example.demo.service

import com.example.demo.dao.AnythingDao
import com.example.demo.entity.AnythingEntity
import org.springframework.stereotype.Service

@Service
class AnythingService(
        val anythingDao: AnythingDao
) {

    fun findAll(): List<AnythingEntity> {
        return this.anythingDao.selectAll()
    }

    fun insert(anything: AnythingEntity): Int {
        return this.anythingDao.insert(anything)
    }
}
package com.example.demo.controller

import com.example.demo.entity.AnythingEntity
import com.example.demo.service.AnythingService
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("anything")
class AnythingController(
        val anythingService: AnythingService
) {

    @GetMapping("/findAll")
    fun findAll(): List<AnythingEntity> {
        return this.anythingService.findAll()
    }

    // 本当はGetにするべきではないですが、面倒なので・・・
    @GetMapping("/insert")
    fun insert(@RequestParam(value = "name", required = false, defaultValue = "doma") name: String): String {

        val entity = AnythingEntity()
        entity.name = name
        this.anythingService.insert(entity)

        return "success"
    }
}

sqlファイルを作成する

SELECT
  *
FROM
  anything

動作確認

curlかブラウザで以下を実行 http://localhost:8080/anything/insert http://localhost:8080/anything/insert?name=fuga http://localhost:8080/anything/findAll

以下のような結果が帰ってくればOK ※上の2行でDBへのインサート、最後の行でインサートした結果を取得しています。

[{"id":1,"name":"doma","createdAt":1516459862000,"updatedAt":1516459862000},{"id":2,"name":"fuga","createdAt":1516459895000,"updatedAt":1516459895000}]

まとめ

今回はSpring Bootで開発を始める第一歩を書きました。 Doma2を使ったのは自分が触ってみたかったからです。 余裕があったら続編を書くかもしれません。

最終的なコードはこちら

https://github.com/Yuki10Kobayashi/SpringBootDoma2Demo

Javaで書かれたSpring BootプロジェクトをKotlin対応した話

これは2017年12月18日にQiitaに投稿した記事を移行したものです。

背景

某社のエンジニアチームで使用技術のモダナイズを進めており、「Java / SpringBootで書かれたシステムをどうせならKotlinで書きたいよね」という話が出てきたので、Kotlinで開発できるように対応を進めています。

最初からKotlinで始める(Gradle)といった記事はよく見かけますが、Javaで書かれている途中からKotlinに対応する(Maven)という記事があまり見つからなかったので、手探りで対応していきました。

既存の環境

基本的にはSpringBootではREST APIのみを実装しています。

Kotlin対応させる方針

  • 並行でJavaでの開発が進んでいるため、全コードをJavaからKotlinに変換するのは今回は見送り
  • Controller / ServiceはKotlinで書けるようにする
  • JPAなどのデータアクセス層はJavaのまま残しておく
  • 最終的には全部Kotlinにしたい

また、今回の作業はIntelliJ上で行いました。

移行作業

pom.xmlの設定

※今回の投稿に関係ある部分を中心に記載しています。

  • Kotlinバージョンの定義
  <properties>
...
     <java.version>1.8</java.version>
+    <kotlin.version>1.2.10</kotlin.version>
+    <kotlin.compiler.incremental>true</kotlin.compiler.incremental>
  </properties>
  • Kotlinの依存ライブラリを追加
  <dependencies>
・・・
+    <dependency>
+      <groupId>org.jetbrains.kotlin</groupId>
+      <artifactId>kotlin-stdlib</artifactId>
+      <version>${kotlin.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.jetbrains.kotlin</groupId>
+      <artifactId>kotlin-reflect</artifactId>
+      <version>${kotlin.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.jetbrains.kotlin</groupId>
+      <artifactId>kotlin-test</artifactId>
+      <version>${kotlin.version}</version>
+      <scope>test</scope>
+    </dependency>
  </dependencies>
  • ビルド設定

元々maven-compiler-pluginは使用していませんでしたが、JavaとKotlinのどちらでも開発ができるように、パッケージをそれぞれ

  • src/main/java
  • src/main/kotlin
  • scr/test/java
  • src/test/kotlin

で分けため、maven-compiler-pluginを使用しています。

また、Springで使用する各種アノテーションを使用できるようにするため、compilerPluginsとしてspringを定義しています。 ドキュメントにも記載されていますが、springプラグインを定義するとall-openの定義は不要になるようです。

  <build>
     <finalName>${project.name}</finalName>
-    <sourceDirectory>src/main/java</sourceDirectory>
-    <testSourceDirectory>src/test/java</testSourceDirectory>
    <resources>
      <resource>
        <directory>${resources.directory}</directory>
      </resource>
      <resource>
        <directory>src/main/resources</directory>
      </resource>
     </resources>
     <testResources>
       <testResource>
         <directory>src/test/resources</directory>
       </testResource>
     </testResources>
     <plugins>
       <plugin>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-maven-plugin</artifactId>
         <configuration>
           <executable>true</executable>
         </configuration>
       </plugin>
+      <plugin>
+        <artifactId>kotlin-maven-plugin</artifactId>
+        <groupId>org.jetbrains.kotlin</groupId>
+        <version>${kotlin.version}</version>
+        <configuration>
+          <compilerPlugins>
+            <plugin>spring</plugin>
+          </compilerPlugins>
+        </configuration>
+        <executions>
+          <execution>
+            <id>compile</id>
+            <goals> <goal>compile</goal> </goals>
+            <configuration>
+              <sourceDirs>
+                <sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
+                <sourceDir>${project.basedir}/src/main/java</sourceDir>
+              </sourceDirs>
+            </configuration>
+          </execution>
+          <execution>
+            <id>test-compile</id>
+            <goals> <goal>test-compile</goal> </goals>
+            <configuration>
+              <sourceDirs>
+                <sourceDir>${project.basedir}/src/test/kotlin</sourceDir>
+                <sourceDir>${project.basedir}/src/test/java</sourceDir>
+              </sourceDirs>
+            </configuration>
+          </execution>
+        </executions>
+        <dependencies>
+          <dependency>
+            <groupId>org.jetbrains.kotlin</groupId>
+            <artifactId>kotlin-maven-allopen</artifactId>
+            <version>${kotlin.version}</version>
+          </dependency>
+        </dependencies>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.5.1</version>
+        <executions>
+          <!-- Replacing default-compile as it is treated specially by maven -->
+          <execution>
+            <id>default-compile</id>
+            <phase>none</phase>
+          </execution>
+          <!-- Replacing default-testCompile as it is treated specially by maven -->
+          <execution>
+            <id>default-testCompile</id>
+            <phase>none</phase>
+          </execution>
+          <execution>
+            <id>java-compile</id>
+            <phase>compile</phase>
+            <goals> <goal>compile</goal> </goals>
+          </execution>
+          <execution>
+            <id>java-test-compile</id>
+            <phase>test-compile</phase>
+            <goals> <goal>testCompile</goal> </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
  </build>

これでIntelliJ上でKotlinを書く準備が整いました。

STS(Eclipse)の対応

ここまでの対応で普段STSを使用しているメンバーに確認してもらったところ、うまく動作せずに少しハマったのでSTSでの対応方法も記載します。

  • EclipseマーケットプレイスからKotlin Plugin for Eclipseをインストールします。

  • src/main/kotlinがビルドパスとして認識されていないので、Projects > Properties > Java Build PathのSourceからAdd Fileでkotlinのフォルダをチェック。 src/test/kotlinも同様

buildPath.png

  • プロジェクトを右クリックし、Configure Kotlin > Add Kotlin Natureを実行

下図では、既にAdd Kotlin Nature済みのため選択できなくなっていますが、実行前であれば選択できます。

AddKotlinNature.png

これでSTS上でもコンパイルが通り、Kotlinでの開発が行えるようになりました。

Kotlinを書く

JavaコードからKotlinコードへコンバート

IntelliJのコンバート機能が優秀なので、基本的にはIntelliJでコンバートしました。 一気にまとめては怖いので、1コードもしくは数コードづつ変換しました。

変換したいコードを選択して、Code > Convert Java File to Kotlin FileでKotlinのコードにコンバートします。

ConvertToKotlin.png

※たまにおかしな変換をするので都度手で修正します。

また、パッケージがsrc/main/java配下のままであるため、src/main/kotlin配下の同パッケージ以下に移動します。 これもIntelliJRefactor > Move...でいい感じにリファクタリングしてくれます。

DI(Autowiredなど)

たとえば以下のコードは

@Service
public class HogeService {
    @Autowired
    private HogeRepository hogeRepository;
}

Kotlinではlateinitでも宣言できますが、今回はconstructor injectionで定義するようにしました。

@Service
class HogeService(
    private val hogeRepository: HogeRepository
) {
}

Lombokまわり

Lombokを使っていると、Kotlinにコンバートした際にコンパイルエラーが出ることがありました。

基本的には、Kotlinにコンバートするクラス、Kotlinから参照するクラスは、Delombokすることで解消させました。

  • Delombok

IntelliJlombokプラグインを使用します。 Refactor > DelombokからLombok化を解除してくれます。

Delombok.png

loggerの宣言を省略して、いきなりログ出力するコードを書くことができます。 Kotlinにした場合、以下のコードでlog変数が解決できずコンパイルエラーとなってしまいます。

@Service
@Slf4j
public class HogeService {
    public void hoge() {
        log.info("hoge");
    }
}

kotlinではcompanion objectでロガーを初期化することで利用できるようになります。

@Service
class HogeService {
    companion object {
        private val log = LoggerFactory.getLogger(HogeService::class.java)
    }
}

アノテーションでのDIの方法などを試されている方もいたので、楽にできるよう検討したいと思います。 http://saiya-moebius.hatenablog.com/entry/2017/11/08/033932

以下のようなLombok化されたJavaのコードをKotlinから読み、hogeを取得しようとしてもprivateでアクセスでいないと言われてしまいます。 この場合、上に書いたようにDelombok化して対応しました。 (全部KotlinになればそもそもLombok使わなくても・・)

@Entity
@Data
@EqualsAndHashCode(callSuper = false)
@ToString(callSuper = true)
public class Hoge extends AbstractEntity {
    @Column(name = "hoge")
    private String hoge;
}

今後について

現時点で一部のコードはKotlin化して動作するようになりました。 しかし、実際にコードを書いている途中で問題が起こることが何度かあったので、今後も問題が発生する可能性があります。 そこでの対処方などはどんどん追記していければなと思います。

※Kotlin楽しい!!

IntelliJ Ideaでインポートのワイルドカードを無効化する

この記事はQiitaに2016年11月18日に投稿した記事を移行したものです。

はじめに

IntelliJのバージョンを最新の2016に上げたところ、でJavaのimportをIntelliJに行わせようとすると(option + return)、ワイルドカードでインポートされるようになってしまった。 コードレビューで指摘を受けてしまった。

import java.util.*;
import java.util.stream.Collectors;

ワイルドカードを使わないようにする方法

以下を参考にしました。 http://stackoverflow.com/questions/3348816/intellij-never-use-wildcard-imports

Preferences -> Editor -> Code & Style -> JavaJavaコードスタイルの設定ページを開く。

Importsタブを開くと、以下のような設定画面が表示される。

f:id:yuki10k:20200328222641p:plain

ワイルドカードを使わないようにするには、以下の3点を設定する。

  • Use single class importにチェックを入れる
  • Class count to use import with '*': を999などの大きな値にする
  • Names count to use static import with '*': を999などの大きな値にする

これでワイルドカードでのインポートがされないようになりました。