Feignでレスポンスボディによる例外処理を行う
REST APIクライアントのSpring Cloud Feignを使う場合に異常系のステータスコードをハンドリングする方法を調べたのでメモです。
デフォルトの振る舞い
APIが400番台や500番台のステータスコードを返した場合、FeignはFeignException
をスローします。これは下記の記事に書いた通りです。
FeignException
にはステータスコードが含まれるので、例外オブジェクトのstatus
プロパティで例外処理ができます。しかし、レスポンスボディで例外処理するのはこの仕組みではできません。
https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/FeignException.java
public class FeignException extends RuntimeException { private static final long serialVersionUID = 0; private int status; //(中略) public static FeignException errorStatus(String methodKey, Response response) { String message = format("status %s reading %s", response.status(), methodKey); try { if (response.body() != null) { String body = Util.toString(response.body().asReader()); message += "; content:\n" + body; } } catch (IOException ignored) { // NOPMD } return new FeignException(response.status(), message); } }
レスポンスボディによる例外処理
レスポンスボディの内容を元に例外処理を行いたい場合を想定します。例えば、バリデーションエラーの時にステータスコード400でエラー理由がレスポンスボディに入るといった場合です。
このような場合、既存のFeignException
ではレスポンスボディを読み取れないので、ErrorDecoder
を使う必要があります。ErrorDecoder
を使うと、APIが400番台や500番台を返した場合にどのような例外を発生させるか設定できます。詳しくは Custom error handling · OpenFeign/feign Wiki · GitHub に説明があります。
ここでは、ErrorDecoder
でレスポンスボディをPOJOに変換してみましょう。Spring Cloud Feignに含まれるSpringDecoder
を使うと、Springで用意されているConvertでレスポンスボディをオブジェクトに変換できます。
import feign.Response import feign.codec.Decoder import feign.codec.ErrorDecoder class ErrorResponseDecoder implements ErrorDecoder { // Spring Cloud FeignのSpringDecoderが注入される @Inject Decoder decoder @Override Exception decode(String methodKey, Response response) { def errorResponse = decoder.decode(response, ErrorResponse) as ErrorResponse new ErrorResponseException(response.status(), errorResponse) } } // レスポンスボディの型 @Immutable class ErrorResponse { int code String message } // レスポンスボディを含む例外クラス class ErrorResponseException extends FeignException { final ErrorResponse errorResponse def ErrorResponseException(int status, ErrorResponse errorResponse1) { super(status, errorResponse1.toString()) errorResponse = errorResponse1 } }
ErrorDecoder
を有効にするにはFeignのConfigurationを使います。
@Configuration class HelloClientConfiguration { @Bean ErrorDecoder errorDecoder() { new ErrorResponseDecoder() } } @FeignClient(name = 'hello', url = '${hello.service.url}', configuration = HelloClientConfiguration) interface HelloClient { //... }
上記の仕組みを使うと、ErrorResponseException
をキャッチすることでレスポンスボディによる例外処理ができます。
レスポンスボディのパススルー
外部APIのエラーレスポンスをそのままクライアントに返したい場合は、@ControllerAdvice
で共通処理を入れるとよいでしょう。
@Slf4j @ControllerAdvice class ResponseStatusPropagation { @ExceptionHandler(ErrorResponseException) ResponseEntity handleErrorResponseException(ErrorResponseException e) { log.error("Handled error response from API", e) def cause = e.cause as ErrorResponseException ResponseEntity .status(cause.status()) .contentType(MediaType.APPLICATION_JSON) .body(cause.errorResponse) } }
Feignのエラーハンドリングに関する記事がとても少ないので書いてみました。ご参考まで。
Jenkins PipelineでGitリポジトリにpushする
JenkinsのジョブでGitリポジトリにブランチやタグをpushしたい場合があります。Jenkinsfileでどのように実装するか調べてみました。
実装例
お急ぎの方は下記のコードを参考にしてください。
def userRemoteConfig = scm.userRemoteConfigs.head() withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: userRemoteConfig.credentialsId, usernameVariable: 'GIT_USER', passwordVariable: 'GIT_PASSWORD']]) { def url = userRemoteConfig.url.replace('://', "://${env.GIT_USER}:${env.GIT_PASSWORD}@") sh "git push $url --tags" }
PipelineでSCMにGitが指定されている必要があります。また、上記は git push
しか書いていませんが、実際にはユーザ名やメールアドレスを設定したり、 git add
したりするコードも必要です。
どのように実現しているのか
Jenkins PipelineのJenkinsfileでは以下のコードをよく使います。
node {
stage('Fetch') {
checkout scm
}
}
では、ここに出てくる scm
変数とは何者なんでしょうか。調べてみたらCloudbeeの記事に書いてありました。
SCMにGitを指定した場合、 scm
変数は GitSCM
クラスのインスタンスになります。GitSCM
クラスには以下のメソッドが用意されています。
public List<UserRemoteConfig> getUserRemoteConfigs() {...}
ここでいう UserRemoteConfig
クラス こそがGitの接続情報になります。UserRemoteConfig
クラスには以下のプロパティが定義されています。
- name
- refspec
- url
- credentialsId
ということは、GitSCM
クラス→UserRemoteConfig
クラス→credentialsIdプロパティ→withCredentialsの流れでGitの接続情報を取得できます。
ただし、Jenkins Pipelineの標準では scm
変数のプロパティに対するアクセスは禁止されています。このため、Jenkinsの設定から scm
変数のプロパティに対するアクセスを許可する必要があります。これは「Jenkinsの設定」で指定できます。
いつの日になるか分かりませんが、PipelineにGitアクセス機能が追加されるのが待ち遠しいですね。
Swagger YAMLのバリデーションをCIで回す
Swagger YAMLを書いていると間違いに気づかないことがよくあります。リポジトリにpushした時にJenkinsやCircle CIなどで構文チェックできるとミスを防げるます。そこで、GradleのプラグインにSwagger YAMLのバリデーションを追加してみました。
使い方
build.gradleに下記を書きます。
plugins { id 'org.hidetake.swagger.generator' version '1.6.0' } validateSwagger { inputFile = file('petstore.yaml') }
下記のようにvalidateSwagger
タスクを実行すると、YAMLのバリデーションが実行されます。
% ./gradlew validateSwagger :validateSwagger
YAMLに間違いがあると、下記のようなエラーを表示してくれます。
:validateSwagger FAILED FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':validateSwagger'. > Invalid Swagger YAML: /Users/hidetake/repo/gradle-swagger-generator-plugin/acceptance-test/validator/petstore.yaml --- - level: "error" schema: loadingURI: "http://swagger.io/v2/schema.json#" pointer: "/definitions/parametersList/items" instance: pointer: "/paths/~1pets~1{petId}/get/parameters/0" domain: "validation" keyword: "oneOf" message: "instance failed to match exactly one schema (matched 0 out of 2)" matched: 0 nrSchemas: 2 (以下略)
仕組み
Swaggerで公式に用意されているJSONスキーマでYAMLを検証しています。
https://github.com/OAI/OpenAPI-Specification/blob/master/schemas/v2.0/schema.json
Gradleプラグインについて
Gradle Swagger Generator Pluginはバリデーションだけでなくコード生成やドキュメント生成の機能も提供しています。詳しい使い方はREADMEを参照してください。