『Microservices with Spring Boot 3 and Spring Cloud』の読書メモ

Spring Boot v3最新、java最新だとビルドが通らない箇所があり(要確認)
内容の理解を優先するため、上記は一旦スキップ。

OpenApiのドキュメントは、一旦最低限にする。

写経してた際に、コンストラクタがないため怒られる。
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `se.magnus.api.composite.product.ProductAggregate` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (org.springframework.core.io.buffer.DataBufferInputStream); line: 1, column: 2]
Springは、Jacksonなどのライブラリを使用してJSONのシリアライズおよびデシリアライズを行っており、デフォルトコンストラクタを必要としている。
@PostMapping(
value = "/product-composite",
consumes = "application/json")
void createProduct(@RequestBody ProductAggregate body);
- 修正内容
public class ProductAggregate {
private final int productId;
private final String name;
private final int weight;
private final List<RecommendationSummary> recommendations;
private final List<ReviewSummary> reviews;
private final ServiceAddresses serviceAddresses;
// 以下を追加
public ProductAggregate() {
productId = 0;
name = null;
weight = 0;
recommendations = null;
reviews = null;
serviceAddresses = null;
}
}

Chapter6の写経中...
- 統合APIのCreateが404
Chapter5->Chapter6 で 地味にパスの記載が変わっていて、少しだけハマる。
- 修正内容
productServiceUrl = "http://" + productServiceHost + ":" + productServicePort + "/product";
recommendationServiceUrl = "http://" + recommendationServiceHost + ":" + recommendationServicePort + "/recommendation";
reviewServiceUrl = "http://" + reviewServiceHost + ":" + reviewServicePort + "/review";

MongoDB へのクエリメモ
jsでクエリを記載する模様
- ログ抑制なし
docker-compose exec mongodb mongosh product-db --eval "db.products.find()"
Current Mongosh Log ID: 66af59b957fd7d28170c5cd5
Connecting to: mongodb://127.0.0.1:27017/product-db?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+1.8.0
Using MongoDB: 6.0.4
Using Mongosh: 1.8.0
For mongosh info see: https://docs.mongodb.com/mongodb-shell/
------
The server generated these startup warnings when booting
2024-08-04T10:10:30.627+00:00: Using the XFS filesystem is strongly recommended with the WiredTiger storage engine. See http://dochub.mongodb.org/core/prodnotes-filesystem
2024-08-04T10:10:31.108+00:00: Access control is not enabled for the database. Read and write access to data and configuration is unrestricted
2024-08-04T10:10:31.108+00:00: /sys/kernel/mm/transparent_hugepage/enabled is 'always'. We suggest setting it to 'never'
2024-08-04T10:10:31.108+00:00: vm.max_map_count is too low
------
------
Enable MongoDB's free cloud-based monitoring service, which will then receive and display
metrics about your deployment (disk utilization, CPU, operation statistics, etc).
The monitoring data will be available on a MongoDB website with a unique URL accessible to you
and anyone you share the URL with. MongoDB may use this information to make product
improvements and to suggest MongoDB products and deployment options to you.
To enable free monitoring, run the following command: db.enableFreeMonitoring()
To permanently disable this reminder, run the following command: db.disableFreeMonitoring()
------
[
{
_id: ObjectId("66af53af71194b485d9ab527"),
version: 0,
productId: 0,
name: 'string',
weight: 0,
_class: 'se.magnus.microservices.core.product.persistence.ProductEntity'
},
{
_id: ObjectId("66af53ca71194b485d9ab528"),
version: 0,
productId: 1,
name: 'string',
weight: 0,
_class: 'se.magnus.microservices.core.product.persistence.ProductEntity'
}
]
- ログ抑制あり
docker-compose exec mongodb mongosh recommendation-db --quiet --eval "db.recommendations.find()"
[
{
_id: ObjectId("66af53afaa9bff6cb96a9428"),
version: 0,
productId: 0,
recommendationId: 0,
author: 'string',
rating: 0,
content: 'string',
_class: 'se.magnus.microservices.core.recommendation.persistence.RecommendationEntity'
},
{
_id: ObjectId("66af53caaa9bff6cb96a9429"),
version: 0,
productId: 1,
recommendationId: 0,
author: 'string',
rating: 0,
content: 'string',
_class: 'se.magnus.microservices.core.recommendation.persistence.RecommendationEntity'
}
]

MongoDB のインデックスマイグレーションメモ(自動生成)
- 書籍
Auto Configuration
で記載
@Autowired
MongoOperations mongoTemplate;
@EventListener(ContextRefreshedEvent.class)
public void initIndicesAfterStartup() {
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext = mongoTemplate.getConverter().getMappingContext();
IndexResolver resolver = new MongoPersistentEntityIndexResolver(mappingContext);
IndexOperations indexOps = mongoTemplate.indexOps(ProductEntity.class);
resolver.resolveIndexFor(ProductEntity.class).forEach(e -> indexOps.ensureIndex(e));
}
- application.yml
appliction.yml
でも対応できる
spring.data.mongodb:
host: localhost
port: 27017
database: recommendation-db
auto-index-creation: true # <--- コレ

(MySQL へのクエリメモ)
docker-compose exec mysql mysql -uuser -p review-db -ppwd -e "select * from reviews"
mysql: [Warning] Using a password on the command line interface can be insecure.
+----+--------+---------+------------+-----------+---------+---------+
| id | author | content | product_id | review_id | subject | version |
+----+--------+---------+------------+-----------+---------+---------+
| 1 | string | string | 0 | 0 | string | 0 |
| 2 | string | string | 1 | 0 | string | 0 |
+----+--------+---------+------------+-----------+---------+---------+

ボイラープレートコードの削減のため、lombokを導入してみる

そろそろフォーマットされないコードが気持ち悪くなってきたので、spotless
を導入してみる

Chapter7 リアクティブマイクロサービスの開発 を読み進める。
MongoDBの1サービス(product-service
)のレポジトリの戻り値リアクティブ(Optinal->Mono)に変更し、レポジトリのテストがパスするように実装する。
- 依存関係をリアクティブに変更
- implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
+ implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'
- インターフェイスを変更
- public interface ProductRepository CrudRepository<ProductEntity, String> {
- Optional<ProductEntity> findByProductId(int productId);
+ public interface ProductRepository extends ReactiveCrudRepository<ProductEntity, String> {
+ Mono<ProductEntity> findByProductId(int productId);
- TestはStepVerifierを利用して修正する

次にAPIもリアクティブに変更する。
例外の取り扱い方法もメソッドチェーンで処理するように変更する。
- public Product createProduct(Product body) {
- try {
- ProductEntity entity = mapper.apiToEntity(body);
- ProductEntity newEntity = repository.save(entity).block();
- return mapper.entityToApi(newEntity);
- } catch (DuplicateKeyException dke) {
- throw new InvalidInputException("Duplicate key, Product Id: " + body.getProductId());
- }
+ public Mono<Product> createProduct(Product body) {
+ ProductEntity entity = mapper.apiToEntity(body);
+
+ return repository
+ .save(entity)
+ .log(LOG.getName(), FINE)
+ .onErrorMap(
+ DuplicateKeyException.class,
+ ex -> new InvalidInputException("Duplicate key, Product Id: " + body.getProductId()))
+ .map(mapper::entityToApi);
- public void deleteProduct(int productId) {
- repository.findByProductId(productId).blockOptional().ifPresent(repository::delete);
+ public Mono<Void> deleteProduct(int productId) {
+ return repository
+ .findByProductId(productId)
+ .log(LOG.getName(), FINE)
+ .flatMap(repository::delete);

次にFluxが戻り値のサービス(recommendation-service
)のレポジトリの戻り値リアクティブ(Optinal->Flux)に変更し、レポジトリのテストがパスするように実装する。
- tests
./gradlew clean :microservices:recommendation-service:test --tests "PersistenceTests"
Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.
You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
For more on this, please refer to https://docs.gradle.org/8.8/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
BUILD SUCCESSFUL in 7s
21 actionable tasks: 19 executed, 2 up-to-date
テストは、reactiveのapiをblockするように修正する。

次に該当のサービスのAPIをリアクティブに変更し、テストコードも修正する。
変更箇所は、Monoの場合と同様。
./gradlew :microservices:recommendation-service:test
Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.
You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
For more on this, please refer to https://docs.gradle.org/8.8/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
BUILD SUCCESSFUL in 938ms
15 actionable tasks: 15 up-to-date

つぎに、APIがノンブロッキングに対応してないリソース(postgres
)などをリアクティブAPIに変更します。方法としては、アプリケーションでスレッドプールを使用して、ブロッキングコードを非同期で実行する方式で処理します(Node.js
等のノンブロッキングI/Oの仕組みに似ている)。
アプリ設定クラスで、Schedulers.newBoundedElasticを使用してスレッドプールを作成します。
このスレッドプールは、ブロッキングなデータベース操作を非同期で実行するために使用します。
@Autowired
public ReviewServiceApplication(
@Value("${app.threadPoolSize:10}") Integer threadPoolSize,
@Value("${app.taskQueueSize:100}") Integer taskQueueSize) {
this.threadPoolSize = threadPoolSize;
this.taskQueueSize = taskQueueSize;
}
@Bean
public Scheduler jdbcScheduler() {
LOG.info("Creates a jdbcScheduler with thread pool size = {}", threadPoolSize);
return Schedulers.newBoundedElastic(threadPoolSize, taskQueueSize, "jdbc-pool");
}
- サービスクラスでの実装例
Mono.fromCallable
及びMono.fromRunnable
を使用して、ブロッキングなデータベース操作を非同期で実行しています。subscribeOn(jobScheduler)
を使うことで、先ほど設定したスレッドプールでこの処理が実行されます。
// Mono.fromCallable は、結果を返す処理を非同期で実行したい場合に使用
// データベース操作やファイル読み込みなど、何らかの結果を返す可能性がある処理を非同期で実行する際に使用します
@Override
public Mono<Review> createReview(Review body) {
return Mono.fromCallable(() -> internalCreateReview(body))
.log(LOG.getName(), FINE)
.subscribeOn(jobScheduler);
}
private Review internalCreateReview(Review body) {
ReviewEntity entity = mapper.apiToEntity(body);
ReviewEntity newEntity = repository.save(entity);
return mapper.entityToApi(newEntity);
}
// Mono.fromRunnable は、結果を返さない処理を非同期で実行したい場合に使用
// 削除処理やログ出力、シンプルなタスクの非同期実行に使用します。
@Override
public Mono<Void> deleteReviews(int productId) {
return Mono.fromRunnable(() -> internalDeleteReviews(productId))
.log(LOG.getName(), FINE)
.subscribeOn(jobScheduler)
.then();
}
private void internalDeleteReviews(int productId) {
LOG.debug(
"deleteReviews: tries to delete reviews for the product with productId: {}", productId);
repository.deleteAll(repository.findByProductId(productId));
}

つぎにマイクロサービスの統合APIのHTTP通信部分修正します。
HTTP通信部分は、Resttemplate
-> WebClient
に変更することで、非同期でAPIを呼び出せるようになり、システム全体の処理効率を向上させることができます。この変更は、特に、多数の外部APIを呼び出す場合に効果的です。
- before
@Override
public Mono<Product> getProduct(int productId) {
try {
String url = productServiceUrl + "/" + productId;
LOG.debug("Will call getProduct API on URL: {}", url);
Product product = restTemplate.getForObject(url, Product.class);
assert product != null;
LOG.debug("Found a product with id: {}", product.getProductId());
return Mono.just(product);
} catch (HttpClientErrorException ex) {
throw handleHttpClientException(ex);
}
}
- after
@Override
public Mono<Product> getProduct(int productId) {
String url = productServiceUrl + "/" + productId;
LOG.debug("Will call the getProduct API on URL: {}", url);
return webClient
.get()
.uri(url)
.retrieve()
.bodyToMono(Product.class)
.log(LOG.getName(), FINE)
.onErrorMap(WebClientResponseException.class, this::handleException);
}

つぎに、上記HTTP通信を統合する処理(統合APIの実装部分)を変更していきます。
修正後のコードは、複数の情報の取得処理を非同期で並列に実行しています(JavaScript
のPromise.all
に似ている)。
先ほどの説明同様、システム全体の処理効率を向上させることができ、特に、多数の外部APIを呼び出す場合に効果的です。
- before
同期/逐次的に処理し、結果を統合して返す。
@Override
public ProductAggregate getProduct(int productId) {
Product product = integration.getProduct(productId).block();
List<Recommendation> recommendations = integration.getRecommendations(productId);
List<Review> reviews = integration.getReviews(productId);
return createProductAggregate(
product, recommendations, reviews, serviceUtil.getServiceAddress());
}
- after
Mono.zip
を使用して、getProduct
、getRecommendations
、getReviews
の結果を並行して取得し、それらが全て揃った時点でProductAggregateオブジェクトを返戻する。
@Override
public Mono<ProductAggregate> getProduct(int productId) {
return Mono.zip(
integration.getProduct(productId),
integration.getRecommendations(productId).collectList(),
integration.getReviews(productId).collectList())
.map(
tuple ->
createProductAggregate(
tuple.getT1(), tuple.getT2(), tuple.getT3(), serviceUtil.getServiceAddress()))
.doOnError(ex -> LOG.warn("product get failed: {}", ex.toString()))
.log(LOG.getName(), FINE);
}