本ハンズオンで、次の図のような簡易家計簿のAPIサーバーをSpring WebFlux.fnを使って実装します。 あえてSpring BootもDependency Injectionも使わないシンプルなWebアプリとして実装します。
ハンズオンコンテンツ
- はじめに
- 簡易家計簿Moneygerプロジェクトの作成
- YAVIによるValidationの実装
- R2DBCによるデータベースアクセス
- Web UIの追加
- 例外ハンドリングの改善 👈
- 収入APIの実装
- Spring Bootアプリに変換
- GraalVMのSubstrateVMでNative Imageにコンパイル
例外ハンドリングの改善
現在の実装では、ExpenditureHandler
で実装した例外ハンドリング以外は適切にハンドリングされません。
例えば次のリクエストを送ってみてください。レスポンスボディがなく500エラーが返ります。
$ curl localhost:8080/expenditures -d "{\"unitPrice\":\"foo\"}" -H "Content-Type: application/json" -v
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /expenditures HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 19
>
* upload completely sent off: 19 out of 19 bytes
< HTTP/1.1 500 Internal Server Error
< content-length: 0
<
* Connection #0 to host localhost left intact
ErrorResponseExceptionHandler
の作成
適切に例外ハンドリングするためにWebExceptionHandler
クラスを実装します。
ErrorResponseExceptionHandler
クラスを作成して、次のコードを記述してください。
package com.example.error;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.List;
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
import static org.springframework.http.MediaType.APPLICATION_JSON;
public class ErrorResponseExceptionHandler implements WebExceptionHandler {
private final Logger log = LoggerFactory.getLogger(ErrorResponseExceptionHandler.class);
private ServerResponse.Context context = new ServerResponse.Context() {
private List<HttpMessageWriter<?>> messageWriters = Collections.singletonList(new EncoderHttpMessageWriter<>(new Jackson2JsonEncoder()));
@Override
public List<HttpMessageWriter<?>> messageWriters() {
return this.messageWriters;
}
@Override
public List<ViewResolver> viewResolvers() {
return Collections.emptyList();
}
};
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
log.warn(ex.getMessage(), ex);
final HttpStatus status = (ex instanceof ResponseStatusException) ? ((ResponseStatusException) ex).getStatus() : INTERNAL_SERVER_ERROR;
return ServerResponse.status(status)
.contentType(APPLICATION_JSON)
.bodyValue(new ErrorResponseBuilder()
.withStatus(status)
.withMessage(ex.getMessage())
.build())
.flatMap(response -> response.writeTo(exchange, this.context));
}
}
App
クラスのhandlerStrategies
メソッドを次のように変更してください。
public static HandlerStrategies handlerStrategies() {
return HandlerStrategies.empty()
.codecs(configure -> {
configure.registerDefaults(true);
ServerCodecConfigurer.ServerDefaultCodecs defaults = configure
.defaultCodecs();
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()
.dateFormat(new StdDateFormat())
.build();
defaults.jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper));
defaults.jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper));
})
// ↓追加
.exceptionHandler(new ErrorResponseExceptionHandler())
.build();
}
App
クラスのmain
メソッドを実行して、次のリクエストを送り、正しくレスポンスが返ることを確認してください。
$ curl localhost:8080/expenditures -d "{\"unitPrice\":\"foo\"}" -H "Content-Type: application/json" -v
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /expenditures HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 19
>
* upload completely sent off: 19 out of 19 bytes
< HTTP/1.1 400 Bad Request
< Content-Type: application/json
< Content-Length: 598
<
* Connection #0 to host localhost left intact
{"status":400,"error":"Bad Request","message":"400 BAD_REQUEST \"Failed to read HTTP message\"; nested exception is org.springframework.core.codec.DecodingException: JSON decoding error: Cannot deserialize value of type `int` from String \"foo\": not a valid Integer value; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `int` from String \"foo\": not a valid Integer value\n at [Source: (io.netty.buffer.ByteBufInputStream); line: 1, column: 14] (through reference chain: com.example.expenditure.ExpenditureBuilder[\"unitPrice\"])"}
Spring Bootでは
WebExceptionHandler
がAuto Configureされるので、このような設定は不要です。
Cloud Foundryにデプロイ
ビルドしてcf push
してください。
./mvnw clean package -DskipTests=true
cf push
Kubernetesにデプロイ
$ pack build <image-name> --builder cloudfoundry/cnb:bionic --publish
# 例: pack build making/moneyger --builder cloudfoundry/cnb:bionic --publish
または
$ ./mvnw clean package -DskipTests=true
$ pack build <image-name> -p target/moneyger-1.0.0-SNAPSHOT.jar --builder cloudfoundry/cnb:bionic --publish
# 例: pack build making/moneyger -p target/moneyger-1.0.0-SNAPSHOT.jar --builder cloudfoundry/cnb:bionic --publish
を実行して、
kbld -f moneyger.yml | kubectl apply -f -
を実行してください。