SpringBootでホットデプロイ

SpringBootの開発用のdevtoolsモジュールを導入するとホットデプロイができるらしい。

build.gradle

~省略~
dependencies {
    developmentOnly 'org.springframework.boot:spring-boot-devtools' // hot deploy
~省略~

application.yml

~省略~
spring:
  devtools:
    livereload:
      enabled: true
    restart:
      enabled: true
~省略~

Advanced Encryption Standard (AES)

Advanced Encryption Standard (AES)

Advanced Encryption Standard (AES) は、DESに代わる新しい標準暗号となる共通鍵暗号アルゴリズム。 DES(Data Encryption Standard)は56ビットの鍵長が短すぎて安全でない為、新たに鍵長は128ビット・192ビット・256ビットの3つが利用できるAESが策定された。

暗号利用モード

AESは固定長のデータ(ブロックと呼ぶ)を単位として処理するブロック暗号で、ブロック長よりも長いメッセージを暗号化するメカニズム(暗号利用モード)を指定する必要がある。

暗号利用モードの種類

  • Electronic Codebook (ECB)
  • Cipher Block Chaining (CBC)
  • Propagating Cipher Block Chaining (PCBC)
  • Cipher Feedback (CFB)
  • Output Feedback (OFB)

ECB

鍵のみで暗号化する。同一鍵で同一の平文を暗号化すると同一の暗号文になる。
暗号文の比較によって解析しやすい為、安全性が低く、他のモードが必要となった。

// 暗号化キーのオブジェクト生成
SecretKeySpec key = new SecretKeySpec(byteKey, "AES");

// Cipherオブジェクト生成
// 暗号化アルゴリズム/暗号利用モード/パディング方式
// 暗号利用モード・・・ブロック長よりも長いメッセージを暗号化するメカニズム
// パディング方式・・・ロック長に満たない場合の補完の方式
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");

// Cipherオブジェクトの初期化
cipher.init(Cipher.ENCRYPT_MODE, key);

// 暗号化の結果格納
byte[] byteResult = cipher.doFinal(byteText);

CBC

鍵と初期化ベクトルを元に暗号化する。
初期化ベクトルが異なれば同一鍵で同一の平文を暗号化しても同一の暗号文にならない(鍵と初期化ベクトルが同一なら同一の暗号文になる)。
鍵は非公開である必要があるが、初期化ベクトルは公開しても問題ない。
また、復号化する際は暗号化時に利用した初期化ベクトルと同じものを利用する必要がある。

// 暗号化キーと初期化ベクトルのオブジェクト生成
SecretKeySpec key = new SecretKeySpec(byteKey, "AES");
IvParameterSpec iv = new IvParameterSpec(byteIv);

// Cipherオブジェクト生成
// 暗号化アルゴリズム/暗号利用モード/パディング方式
// 暗号利用モード・・・ブロック長よりも長いメッセージを暗号化するメカニズム
// パディング方式・・・ロック長に満たない場合の補完の方式
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

// Cipherオブジェクトの初期化
cipher.init(Cipher.ENCRYPT_MODE, key, iv);

// 暗号化の結果格納
byte[] byteResult = cipher.doFinal(byteText);

@ConditionalOnExpressionを使う

@Scheduledでタスクを作成したけど、設定ファイルで起動する/しないを切り替えたい。

@ConditionalOnExpressionを利用すればできるらしい。

SleepTasks.java

~省略~

@Component
@RequiredArgsConstructor
@ConditionalOnExpression("${tasks.sleep-tasks.task-on}")
public class SleepTasks {
    private static final Logger LOGGER = LoggerFactory.getLogger(SleepTasks.class);

    @Value("${tasks.sleep-tasks.max-sleep-millisecond}")
    private long MAX_SLEEP_MILLISECOND;

    @Value("${tasks.sleep-tasks.sleep-millisecond}")
    private long SLEEP_MILLISECOND;

    private final StringRedisTemplate redisTemplate;

    private final DataLinkageBL dataLinkageBL;

    private int skipCount = 0;

    @Scheduled(initialDelayString = "${tasks.sleep-tasks.initial-delay}", fixedRateString = "${tasks.sleep-tasks.fixed-rate}")
    public void doSomething() {
~省略~

application.yml

・task-onがtrueなら実行される。falseなら実行されない。

~省略~
tasks:
  sleep-tasks:
    initial-delay: 1000
    fixed-rate: 1000
    sleep-millisecond: 1000
    max-sleep-millisecond: 10000
    task-on: false
~省略~

他にもリソースの有無で動作を切り替えたりいろいろ用意されているらしい

【参考】

条件で動作するアノテーションを使う | Korean-Man in Tokyo

@PostConstructと@PreDestroyを使う

アプリ起動時と停止時に何かしらの処理を行いたい。

コンポーネントを用意して、メソッドに@PostConstructと@PreDestroyを付与すれば簡単にできるらしい。

ApplicationListner.java

package com.example.demo;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.springframework.stereotype.Component;

@Component
public class ApplicationListner {

    @PostConstruct
    public void init() {
        System.out.println("################################");
        System.out.println("# アプリケーションが起動しました");
        System.out.println("################################");
    }

    @PreDestroy
    public void destoroy() {
        System.out.println("################################");
        System.out.println("# アプリケーションが終了します");
        System.out.println("################################");
    }

}

SpringBootのキャッシングをRedisで使ってみる

例えば、DBから取得した結果を返却するというメソッドがあるとする。 このメソッドに@Cacheableを付与すると、検索結果をキャッシュに格納してくれて次回呼び出し時はキャッシュで保持しているデータを返却してくれる。 基本的にキャッシュにデータがある限りDBへのアクセスは行わず、キャッシュが消されると再びDBにアクセスして再度キャッシュする。 マスタデータの保持なんかで便利そう。

準備

build.gradle

・下記を追加

 implementation 'org.springframework.boot:spring-boot-starter-cache:2.5.4'
    implementation 'org.springframework.boot:spring-boot-starter-data-redis:2.5.4'

application.yml

・キャッシュプロバイダーはRedisを使用する

  cache:
    type: redis
  redis:
    host: localhost
    port: 16379
    database: 0
    password: 
    timeout: 3000

SampleApplication.java

・@EnableCachingでキャッシュを有効化

@SpringBootApplication
@EnableScheduling
@EnableCaching
public class SampleApplication {

UserCacheObject.java

・キャッシュ用オブジェクト

package sample.service.cache;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;

import sample.service.common.utils.JsonUtils;
import sample.service.domain.repository.UserRepository;
import sample.service.domain.repository.model.User;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Component
public class UserCacheObject {

    private final UserRepository userRepository;

    @Cacheable(cacheNames = "sample-cache", key = "#key")
    public String findByUserId(Long userId, String key) {
        User user = userRepository.findById(userId);

        return JsonUtils.convertToJson(user);
    }
}

UserCacheBL.java

ビジネスロジックインタフェース

package sample.service.domainbl;

import sample.service.domain.repository.model.User;

public interface UserCacheBL {
    User findById(Long userId);
}

UserCacheBLImpl.java

ビジネスロジック実装クラス

package sample.service.domainbl.impl;

import org.springframework.stereotype.Component;

import sample.service.cache.UserCacheObject;
import sample.service.common.utils.JsonUtils;
import sample.service.domain.repository.model.User;
import sample.service.domainbl.UserCacheBL;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Component
public class UserCacheBLImpl implements UserCacheBL {

    private final UserCacheObject userCacheObject;

    @Override
    public User findById(Long userId) {
        String json = userCacheObject.findByUserId(userId, "User::" + userId);
        return JsonUtils.convertFromJson(json, User.class);
    }

}

ScheduledTasks.java

・スケジュールタスクでビジネスロジックを呼び出す

package sample.service.common.tasks;

import java.util.Date;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import sample.service.common.utils.DateUtils;
import sample.service.domain.repository.model.User;
import sample.service.domainbl.UserCacheBL;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class ScheduledTasks {
    private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTasks.class);

    private final UserCacheBL userCacheBL;

    @Scheduled(initialDelay = 10000, fixedRate = 1000)
    public void doSomething() {
        User user = userCacheBL.findById(5L);
        LOGGER.info("### User : {}", user);

        LOGGER.info("### This time is : {}", DateUtils.format(new Date()));
    }
}

結果

こんな感じでRedisに格納された。

127.0.0.1:6379> keys *
1) "sample-cache::User::1"
127.0.0.1:6379> get sample-cache::User::1
"\xac\xed\x00\x05t\x00\xcf{\"id\":1,\"userName\":\"Jack\",\"mailAddress\":\"jack@hoge.example.jp\",\"password\":\"8bb0cf6eb9b17d0f7d22b456f121257dc1254e1f01665370476383ea776df414\",\"registTime\":1629500509000,\"updateTime\":1629500509000,\"deleted\":0}"
127.0.0.1:6379> del sample-cache::User::1
(integer) 1
127.0.0.1:6379> keys *
1) "sample-cache::User::1"

Spring Sessionを使う

Spring Sessionを使ってHttpSessionをRedisで管理してみる

セットアップ

build.gradle

  • dependenciesにspring-boot-starter-data-redisとspring-session-data-redisを追加する
plugins {
    id 'org.springframework.boot' version '2.5.0'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    compileOnly 'org.projectlombok:lombok'
    implementation 'org.slf4j:slf4j-api'
    implementation 'ch.qos.logback:logback-core'
    implementation 'ch.qos.logback:logback-classic'
    implementation 'com.google.guava:guava:30.1.1-jre'
    implementation 'org.apache.commons:commons-lang3'
    implementation 'commons-beanutils:commons-beanutils:1.9.2'
    implementation 'com.amazonaws:aws-java-sdk-dynamodb:1.12.9'
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.4'
    implementation 'org.springframework:springloaded:1.2.8.RELEASE'
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    implementation 'org.springframework.session:spring-session-data-redis'
    runtimeOnly 'mysql:mysql-connector-java' 
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}

/src/main/resources/application.yml

  • spring.session.store-type、spring.redis.host=localhost、spring.redis.portの定義を追加
server:
  servlet:
    context-path: /sample
spring:
  messages:
    basename: "application-messages,ValidationMessages"
    encoding: "UTF-8"
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:33060/sample
    username: mysql-app
    password: mysql-app
  thymeleaf:
    cache: false
  session:
    store-type: redis    ←追加
  redis:
    host: localhost    ←追加
    port: 16379    ←追加
mybatis:
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    org:
      springframework: WARN
    com:
      example:
        demo:
          domain:
            mapper: DEBUG

/src/main/java/com/example/demo/web/controller/SessionCheckController.java

package com.example.demo.web.controller;

import java.net.UnknownHostException;
import java.time.LocalDateTime;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.web.dto.form.SessionInfo;

@RestController
public class SessionCheckController {

    private final SessionInfo sessionInfo;

    @Autowired
    public SessionCheckController(SessionInfo sessionInfo) {
        this.sessionInfo = sessionInfo;
    }

    @GetMapping("/session/accept")
    public String start(HttpServletRequest request) throws UnknownHostException {
        if (sessionInfo.getStartedAt() == null) {
            sessionInfo.setClientIp(request.getRemoteAddr());
            sessionInfo.setStartedAt(LocalDateTime.now());
            sessionInfo.setMessage("Session start.");
        } else {
            sessionInfo.setMessage("Session is continuing.");
        }
        return String.format("[Message: %s] [Host: %s] [Started At: %s]", sessionInfo.getMessage(),
                sessionInfo.getClientIp(), sessionInfo.getStartedAt());
    }

    @GetMapping("/session/invalidate")
    public String invalidate(HttpSession session) {
        session.invalidate();
        return "Session invalidate.";
    }
}

/src/main/java/com/example/demo/web/dto/form/SessionInfo.java

package com.example.demo.web.dto.form;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.SessionScope;

@Component
@SessionScope
public class SessionInfo implements Serializable {

    /**
    * SerialVersionUID.
    */
    private static final long serialVersionUID = -6228456085202936272L;

    private String clientIp;
    private String startedAt;
    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getClientIp() {
        return clientIp;
    }

    public void setClientIp(String host) {
        this.clientIp = host;
    }

    public String getStartedAt() {
        return startedAt;
    }

    public void setStartedAt(String startedAt) {
        this.startedAt = startedAt;
    }

    public void setStartedAt(LocalDateTime startedAt) {
        this.startedAt = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(startedAt);
    }

}

/src/main/java/com/example/demo/WebConfiguration.java

  • springSessionDefaultRedisSerializerメソッドを追加。オブジェクトをJSON形式に変換してRedisに登録する
package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.example.demo.interceptor.RequestHandlerInterceptor;

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

    @Autowired
    private RequestHandlerInterceptor requestHandlerInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(requestHandlerInterceptor);
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

実行結果

f:id:samehada3:20210709141058p:plain
Another Redis Desktop

AWS SDK for Java でDynamoDBを操作する

build.gradle

plugins {
    id 'org.springframework.boot' version '2.5.0'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    compileOnly 'org.projectlombok:lombok'
    implementation 'org.slf4j:slf4j-api'
    implementation 'ch.qos.logback:logback-core'
    implementation 'ch.qos.logback:logback-classic'
    implementation 'com.google.guava:guava:30.1.1-jre'
    implementation 'org.apache.commons:commons-lang3'
    implementation 'commons-beanutils:commons-beanutils:1.9.2'
    implementation 'com.amazonaws:aws-java-sdk-dynamodb:1.12.9'
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.4'
    runtimeOnly 'mysql:mysql-connector-java' 
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}

登録/取得/更新/検索のサンプル

package com.example.demo.dynamodb;

import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;

import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AWSCredentialsProviderChain;
import com.amazonaws.auth.ClasspathPropertiesFileCredentialsProvider;
import com.amazonaws.auth.EC2ContainerCredentialsProviderWrapper;
import com.amazonaws.auth.EnvironmentVariableCredentialsProvider;
import com.amazonaws.auth.profile.ProfileCredentialsProvider;
import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.dynamodbv2.document.Index;
import com.amazonaws.services.dynamodbv2.document.Item;
import com.amazonaws.services.dynamodbv2.document.ItemCollection;
import com.amazonaws.services.dynamodbv2.document.QueryOutcome;
import com.amazonaws.services.dynamodbv2.document.Table;
import com.amazonaws.services.dynamodbv2.document.spec.QuerySpec;
import com.amazonaws.services.dynamodbv2.document.utils.ValueMap;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.GetItemRequest;
import com.amazonaws.services.dynamodbv2.model.GetItemResult;
import com.amazonaws.services.dynamodbv2.model.PutItemRequest;
import com.amazonaws.services.dynamodbv2.model.PutItemResult;
import com.amazonaws.services.dynamodbv2.model.ReturnValue;

public class SampleDynamoDB {

    public static void main(String[] args) {
        final String token = putItem();
        getItem(token);
        update();
        search();
    }

    /**
    * 登録
    */
    private static String putItem() {
        final String token = UUID.randomUUID().toString();
        final Date now = new Date();
        // TTLを設定するフィールドは秒単位
        // https://aws.amazon.com/jp/premiumsupport/knowledge-center/ttl-dynamodb/
        final long expires = (now.getTime() / 1000) + (60 * 30); // 有効期間30分

        final Map<String, AttributeValue> item = new HashMap<>();
        item.put("authToken", new AttributeValue().withS(token));
        item.put("userId", new AttributeValue().withN("456"));
        item.put("createTime", new AttributeValue().withN(String.valueOf(now.getTime())));
        item.put("deleted", new AttributeValue().withBOOL(false));
        item.put("expires", new AttributeValue().withN(String.valueOf(expires)));

        final PutItemRequest putItemRequest = new PutItemRequest().withTableName("AuthTokenTable").withItem(item)
                .withReturnValues(ReturnValue.NONE);

        final AmazonDynamoDB dynamoDB = getDynamoDB();
        final PutItemResult result = dynamoDB.putItem(putItemRequest);
        System.out.println(result);

        return token;
    }

    /**
    * 取得
    */
    private static void getItem(String token) {
        final AmazonDynamoDB dynamoDB = getDynamoDB();
        final GetItemResult getItemResult = dynamoDB.getItem(new GetItemRequest().withTableName("AuthTokenTable")
                .withKey(Collections.singletonMap("authToken", new AttributeValue().withS(token)))
                .withConsistentRead(true));
        if (getItemResult.getItem() != null && !getItemResult.getItem().isEmpty()) {
            final Map<String, AttributeValue> item = getItemResult.getItem();
            final AttributeValue expires = item.get("expires");

            if (expires != null && expires.getN() != null && !expires.getN().isEmpty()) {
                final long l = Long.parseLong(expires.getN() + "000");
                final Date date = new Date(l);
                System.out.println(date);
            }
        }
    }

    /**
    * 更新
    */
    private static void update() {
        final String token = "a14c5498-349b-42a6-b737-3f512878ab80";
        final Date now = new Date();
        // TTLを設定するフィールドは秒単位
        // https://aws.amazon.com/jp/premiumsupport/knowledge-center/ttl-dynamodb/
        final long expires = (now.getTime() / 1000) + (60 * 30); // 有効期間30分

        final AmazonDynamoDB dynamoDB = getDynamoDB();
        final GetItemResult getItemResult = dynamoDB.getItem(new GetItemRequest().withTableName("AuthTokenTable")
                .withKey(Collections.singletonMap("authToken", new AttributeValue().withS(token)))
                .withConsistentRead(true));
        if (getItemResult.getItem() != null && !getItemResult.getItem().isEmpty()) {
            final Map<String, AttributeValue> item = getItemResult.getItem();

            // TTLを延長する場合
            item.put("expires", new AttributeValue().withN(String.valueOf(expires)));

            // 削除する場合
            item.put("deleted", new AttributeValue().withBOOL(true));

            final PutItemRequest putItemRequest = new PutItemRequest().withTableName("AuthTokenTable").withItem(item)
                    .withReturnValues(ReturnValue.NONE);
            final PutItemResult result = dynamoDB.putItem(putItemRequest);
            System.out.println(result);
        }
    }

    /**
    * 検索(GlobalSecondaryIndex)
    */
    private static void search() {
        final Long userId = 123L;

        final AmazonDynamoDB dynamoDB = getDynamoDB();
        final DynamoDB db = new DynamoDB(dynamoDB);
        Table table = db.getTable("AuthTokenTable");
        Index index = table.getIndex("UserIdIndex");

        QuerySpec spec = new QuerySpec().withKeyConditionExpression("userId = :userId")
                .withValueMap(new ValueMap().withLong(":userId", userId));
        ItemCollection<QueryOutcome> items = index.query(spec);

        Iterator<Item> iterator = items.iterator();
        while (iterator.hasNext()) {
            Item token = (Item) iterator.next();
            System.out.println(token);
        }
    }

    private static AmazonDynamoDB getDynamoDB() {
        final AWSCredentialsProvider providers = new AWSCredentialsProviderChain(
                new EnvironmentVariableCredentialsProvider(), new ClasspathPropertiesFileCredentialsProvider(),
                new ProfileCredentialsProvider(), new EC2ContainerCredentialsProviderWrapper());
        final String endpoint = "http://localhost:8000";
        final String region = "ap-northeast-1";
        final AmazonDynamoDBClientBuilder builder = AmazonDynamoDBClientBuilder.standard();
        builder.setCredentials(providers);
        builder.setEndpointConfiguration(new EndpointConfiguration(endpoint, region));
        return builder.build();
    }

}