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 ~省略~
他にもリソースの有無で動作を切り替えたりいろいろ用意されているらしい
【参考】
@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(); } }
実行結果
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(); } }