怠惰系エンジニアのメモ帳

勉強した内容をメモしていきます。解説ブログではないので悪しからず。

【Spring】ConfigurationPropertiesアノテーションを注釈したクラスのsetterはpublicにする

@ConfigurationProperties を注釈したクラスは、setter/getterが必要。

@Component
@ConfigurationProperties(prefix = "aws")
public class AwsSetting {

    private String region;

    public void setRegion(String region) {
        this.region = region;
    }

    public String getRegion() {
        return this.region;
    }
}

setter/getterのスコープは public にしていることが多いが、試しにパッケージプライベートにしてみたら値は設定されなかった。
protected もダメ。

publicでないと設定されないみたい。

【SpringMVC】Apache commons-fileuploadを使用して、マルチパートリクエストをストリーミングする

モチベーション

SpringMVCで作成されたアプリケーショで、 クライアントからアップロードされたファイル(マルチパート)をストリーミングして処理したかった。
というのも、Springでマルチパートのデータを受け取る場合は、ハンドラメソッドの引数に MultipartFile を取れば良いが、ローカルストレージに一時ファイルを作成してしまう。 (MultipartFileが参照するのはクライアントから送られてきたデータではなく、ローカルストレージに作成されたファイル。) そのため、大容量ファイルが送られてきた際は、一時的とはいえストレージを消費することになるため、多重で大量のファイルが送られてきた場合には容量不足になる可能性がある。

これを避けるためには、クライアントから送られてきたマルチパートデータをストリーミング処理すればよい。

環境

  • Spring MVC
    • 5.0.6RELEASE
  • Apache commons-fileupload
    • 1.3.3

準備

1. Apache commons-fileupload を依存関係に追加する

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.3</version>
</dependency>

2. マルチパートリクエストを無効にする

無効にしないと、ローカルストレージに一時ファイルを作成してしまうため。

  • application.properties
spring.servlet.multipart.enabled=false

コード

流れとしては

  1. マルチパートリクエストであるかを判定する
  2. リクエストコンテキストから、マルチパートのデータを取得する。
  3. マルチパートのデータから、入力ストリームを取得して処理する。

となる。

@PostMapping("/upload")
public void streaming(final HttpServletRequest request) {

    // 1. マルチパートのリクエストであるかを判定
    if (!ServletFileUpload.isMultipartContent(request)) {
        throw new RuntimeException("request is not multipart.");
    }

    // 2. リクエストコンテキストから、マルチパートのデータを取得する。
    final ServletFileUpload servletFileUpload = new ServletFileUpload();
    final FileItemIterator fileItemIterator = servletFileUpload.getItemIterator(request);

    // 3. マルチパートのデータから、入力ストリームを取得して処理する。
    while (fileItemIterator.hasNext()) {
        final FileItemStream itemStream = fileItemIterator.next();
        final InputStream is = itemStream.openStream();
        this.uploadService.execute(is); // ストリームを消費するサービスの想定
    }
}

また、FileItemStreamInputStream 以外にも

  • フィールド名
  • ファイル名
  • Content-type

が取得できる。
マルチパートのデータにはファイル以外も含まれている場合があるので、フィールド名やContent-typeでファイル判別を行う方が良さそう。

【Java】AESを利用した暗号化・復号を標準APIで行う

結論

  • javax.crypto.Cipher を使用して暗号化・復号を行う
  • 鍵は javax.crypto.spec.SecretKeySpec を使用
  • 暗号利用モードが「初期ベクトル」を必要とする方式の場合、javax.crypto.spec.IvParameterSpec を使用。
  • 最終的には、SecretKeySpecIvParameterSpec を元に Cipher を初期化する。

AESについて

  • 共通鍵暗号方式アルゴリズム
  • ブロック暗号である
    • ある一定のデータ長で区切って、それを「ブロック」という単位で処理する。
    • なお、「ブロック暗号」とは別に「ストリーム暗号」もある。
  • 鍵の長さは128bit、192bit、256bitの3種
    • Javaの場合は128bitがデフォルト

コード

続きを読む

Log4j2からElasticsearchにログを流し込む

ログをファイルに出力するのではなく、Elasticsearchに流し込む方法が知りたかったので調べてみました。
「 Spring(SpringBoot)アプリケーションから出力されるログをElasticsearchに流し込む」のがゴールになります。
ログ実装には Log4j2 を用います。(Logbackでもいいのですが、お仕事では Log4j2 が採用されているので。。。)

この方法が正しいわけではないと思うので、ご指摘等いただけると幸いです。

結論

データ収集用のミドルウェア Logstash を経由することで Elasticsearch にログを流し込めました。

www.elastic.co www.elastic.co

なお、ログ可視化のために Kibana を使いました。

www.elastic.co

環境

今回 Elasticsearch, Logstash, Kibana の環境(この3つで ELK Stack と呼ばれてるっぽい)はDockerを利用して構築しています。

  • Docker for Mac(version 18.03.0-ce, build 0520e24)
    • Elasticsearch : docker.elastic.co/elasticsearch/elasticsearch:6.2.4
    • Logstash : docker.elastic.co/logstash/logstash:6.2.2
    • Kibana : docker.elastic.co/kibana/kibana:6.2.4
  • Spring Boot 1.5.12
    • Log4j2は依存関係に spring-boot-starter-log4j2 を追加して使用。
    • なお、log4j2 を使用する場合 spring-boot-starter-logging を依存関係から除外してください。

準備

1. ELK StackをDockerで用意する

docker-compose.yml と、各種設定ファイルを用意する。(全体的なフォルダ構成は最後に記載)

docker-compose.yml

version: '2'
services:

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:6.2.4
    container_name: elasticsearch
    ports:
      - "9200:9200"
      - "9300:9300"
    environment:
      - discovery.type=single-node

  logstash:
    image: docker.elastic.co/logstash/logstash:6.2.2
    container_name: logstash
    ports:
      - "5044:5044"
    volumes:
      - ./logstash/config/:/usr/share/logstash/config/
      - ./logstash/pipeline/:/usr/share/logstash/pipeline/
    links:
      - elasticsearch

  kibana:
    image: docker.elastic.co/kibana/kibana:6.2.4
    container_name: kibana
    ports:
      - "5601:5601"
    volumes:
      - ./kibana/config/kibana.yml:/usr/share/kibana/config/kibana.yml
    links:
      - elasticsearch
  • イメージはDockerHubではなく、Elastic社から取得しています。
    • ※なんかDockerHubのは非推奨らしい

elasticsearch.yml

用意はしているんですが、ボリュームをマウントしてないので意味ないですね。 参考程度に。

network.host: 0.0.0.0
cluster.name: elasticsearch
discovery.type: single-node

logstash.yml

http.host: "0.0.0.0"
path.config: /usr/share/logstash/pipeline
xpack.monitoring.elasticsearch.url: http://elasticsearch:9200
xpack.monitoring.elasticsearch.username: logstash_system
xpack.monitoring.elasticsearch.password: changeme

logstash.conf

input {
  tcp {
    mode => "server"
    host => "0.0.0.0"
    port => 5044
  }
}
output {
  elasticsearch {
    hosts => "elasticsearch:9200"
    index => "logstash-%{+YYYY.MM.dd}"
    document_type => "logs"
  }
}
  • サーバーモードで入力を受け付け、Elasticsearchに出力します。

2. Log4j2の設定

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="OFF">

    <Properties>
        <Property name="loglayout">[%d{yyyy-MM-dd HH:mm:ss.SSS}],%-5p,%t,%c:%m%n</Property>
    </Properties>

    <Appenders>
        <Console name="CONSOLE" target="SYSTEM_OUT">
            <PatternLayout pattern="${loglayout}"/>
        </Console>

        <Socket name="LOG_STASH" host="localhost" port="5044" >
            <JsonLayout compact="true" eventEol="true" />
        </Socket>
    </Appenders>

    <Loggers>
        <Root level="info">
            <AppenderRef ref="CONSOLE" />
            <AppenderRef ref="LOG_STASH" />
        </Root>
    </Loggers>

</Configuration>

ELKを立ち上げ、ログを流し込む

docker-compose up で、ELKを立ち上げます。

$ docker-compose up
Creating network "docker_default" with the default driver
Creating elasticsearch ... done
Creating kibana        ... done
Creating logstash      ... done
Attaching to elasticsearch, kibana, logstash
...

起動するまで少し時間がかかるのでしばらく待って、localhost:5601 にアクセスします。 ※ Kitematic を立ち上げておくと、各コンテナの状態が分かりやすいのでオススメです。

Kibanalocalhost:5601) にアクセスして、以下のような画面が表示されれば準備OKです。

f:id:tk5_21:20180421235025p:plain

大丈夫だとは思いますが、念のため、docker-compose ps で全てのコンテナが稼働しているか確認しておいてください。 ※もしくは Kitematic で確認

$ docker-compose ps
    Name                   Command               State                       Ports                     
-------------------------------------------------------------------------------------------------------
elasticsearch   /usr/local/bin/docker-entr ...   Up      0.0.0.0:9200->9200/tcp, 0.0.0.0:9300->9300/tcp
kibana          /bin/bash /usr/local/bin/k ...   Up      0.0.0.0:5601->5601/tcp                        
logstash        /usr/local/bin/docker-entr ...   Up      0.0.0.0:5044->5044/tcp, 9600/tcp    

ELKが立ち上がったら、アプリケーションを実行してログを出力します。
SpringBootの起動時、エラーが出ていなければ Logstash にログが転送されているはずです。

SpringBootが起動できたら Kibana を確認してみましょう。

f:id:tk5_21:20180422000730p:plain

Elasticsearch にログが転送されていれば、上記のような画面が表示されます。
「index pattern」には logstash.conf で指定したindexと同じパターンになるような文字列(今回は logstash-2018.04.21)を入力し、「Next step」を押下します。

f:id:tk5_21:20180422000735p:plain

「Time Filter field name」は @timestamp でいいです。 「Create index pattern」を押下後、サイドメニューの「Discover」をクリックします。

f:id:tk5_21:20180422000744p:plain

SpringBootの起動ログが表示されていればOKです。 Elasticsearch にログを流し込めてます。

まとめ

  • Log4j2SocketAppender を使用して、Logstash にログを送信する。
  • Logstash から Elasticsearch にデータ転送できる。

フォルダ構成(一部省略)

./
├── docker
│   ├── docker-compose.yml
│   ├── elasticsearch
│   │   └── config
│   │       └── elasticsearch.yml
│   ├── kibana
│   │   └── config
│   │       └── kibana.yml
│   └── logstash
│       ├── config
│       │   └── logstash.yml
│       └── pipeline
│           └── logstash.conf
├── pom.xml
├── src
   └── main
       ├── java
       └── resources
           ├── application.yml
           └── log4j2.xml

所感

とりあえず、Elasticsearch にログを流し込む、という目的は達成されましたが ログをそのまま送信しているので、セキュリティはガバガバ。。。

あと、Elasticsearch 以外に Logstash とか Kibana とか知れたのは大きいですね。 (使えるわけではないけど)

余談

ELKのDockerイメージは既にあって、イメージサイズも小さい

これも試したみたんですが、Logstash にログを転送するにはSSL(TLS)接続が必要だったので一旦諦めました。(ネットワークよく分からん。。。)
この辺詳しい場合は、こちらを使用するほうが楽かも。

Logback用のlogback-elasticsearch-appenderなるものがGitHubにある

Logbackの場合はコレ使えば直接流し込めるのかな?

【SpringWebFlux + Doma2】T型をFlux<T>に変換するCollector

SpringWebFlux + Doma2 で開発していて、Daoの戻り値を Flux<T> にしたかったので Flux<T> に変換する Collector 作った話。
当たり前だが、DomaのDaoメソッドは戻り値を Flux<T> で返せない。

@Dao
public interface PersonDao {
    @Select
    Flux<Person> findAll(); // これダメ
}

かと言って、List<Person>Stream<Person> で受け取りたくない。
となると、Domaのコレクト検索機能(もしくはストリーム検索)を利用して Flux<T> に変換する。

@Dao
public interface PersonDao {
    @Select(strategy = COLLECT)
    <R> R findAll(final Collector<Person, ?, R> collector);
}

Domaはこのコレクト検索が強い。良い。
で、作った Collector が以下。

public class PublisherCollectors {
    public static <T> Collector<T, ?, Flux<T>> toFlux() {
        return Collector.<T, List<T>, Flux<T>>of(
                ArrayList::new, 
                List::add,
                (right, left) -> { right.addAll(left); return right; },
                Flux::fromIterable
        );
    }
}
PersonDao personDao = // ...
Flux<Person> fluxPerson = personDao.findAll(PublisherCollectors.toFlux());

一旦 List に全ての要素を突っ込んで、最後に Flux に変換する方法をとった。
※本当は頭からケツまで Flux で処理したかったが、上手くいかなかった。

ちなみにストリーム検索を利用して Flux<T> に変換する場合は、

@Dao
public interface PersonDao {
    @Select(strategy = STREAM)
    <R> R findAll(final Function<Stream<Person>, R> function);
}
Flux<Person> fluxPerson = personDao.findAll(Flux::fromStream);

とする。

最後に

頭から Flux で処理できる方法や、良いライブラリがあったら教えていただけると嬉しいです。