やりたいこと
前回のSpringでバージョニングされたAPIのエンドポイントを定義する(1)では、
- クラスに
@ApiVersion
が付与できない -
RequestMappingInfo
の生成が雑
という問題点がありました。
今回はこれを解消します。
前回のコード
コントローラ
@RestController
public class DemoController {
@GetMapping("/items/{id}")
@ApiVersion("1.0")
public Item1 getItem(@PathVariable Integer id) {
return Item1.builder()
.id(id)
.name("バッグ")
.build();
}
@GetMapping("/items/{id}")
@ApiVersion(greaterThan = "1.0")
public Item2 getItem(@PathVariable Long id) {
return Item2.builder()
.id(id)
.name("バッグ")
.price(10.9)
.build();
}
@GetMapping("/users/{id}")
@ApiVersion(supported = { "1.0", "3.1" })
public User getUser(@PathVariable Long id) {
return User.builder()
.id(id)
.name("Kenny")
.build();
}
}
複合的なRequestMappingInfo
の生成
コントローラにもメソッドにも@ApiVersion
を付与するので複合的なRequestMappingInfo
が生成されなければいけません。
これを自前で実装するのは手間なので、もう一度RequestMappingHandlerMapping
クラスを読み直しました。#getMappingForMethod(Method, Class)
から追っていくと、もっとオーバーライドするのにふさわしいメソッドがありました。
#createRequestMappingInfo(RequestMapping, RequestCondition)
です。
#getMappingForMethod(Method, Class)
の実装をやめて、こちらの実装に切り替えました。
@Override
protected RequestMappingInfo createRequestMappingInfo(RequestMapping requestMapping,
RequestCondition<?> customCondition) {
if (customCondition instanceof ApiVersionRequestCondition) {
return RequestMappingInfo.paths(Arrays.stream(requestMapping.path())
// RequestMappingから得られる全てのパスパターンに
// バージョン文字列のパターンを付与する
.map(path -> "/{version:\\d+\\.\\d+}/" + StringUtils.removeStart(path, "/"))
.toArray(String[]::new))
.methods(requestMapping.method())
.params(requestMapping.params())
.headers(requestMapping.headers())
.consumes(requestMapping.consumes())
.produces(requestMapping.produces())
.mappingName(requestMapping.name())
.customCondition(customCondition)
.options(getBuilderConfiguration())
.build();
}
return super.createRequestMappingInfo(requestMapping, customCondition);
}
これで自前で空っぽのRequestMappingInfo
を作ってオリジナルと繋げて・・・みたいなことはしなくて済むのでコード的にはスッキリしました。
コントローラに@ApiVersion
を仕込む
今回は仮に、全てのEndpointで0.5
のバージョンをサポートするというふうに変更してみます。
@RestController
+ @ApiVersion("0.5")
public class DemoController {
// ・・・
}
よし、これでOK。
と思ったが、ダメでした。
クラス側に付与した@ApiVersion
が考慮されないので、Spring側のコードを見てみます。
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
RequestCondition<?> condition = (element instanceof Class ?
getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}
ここであるように、AnnotatedElement
がクラスになるのですが、@RequestMapping
が付与されていないとそもそもnull
となるコードになっています。
よって、コントローラ側をこんな風に修正しました。
@RestController
+ @RequestMapping
@ApiVersion("0.5")
public class DemoController {
// ・・・
}
これで全てのEndpointでバージョン0.5もマッピングされるようになりました。
まとめ
- もうちょっとちゃんとコード読む