1. やりたいこと
- 外部Jarとして取り込まれる
- 外部Jar内にController(RestController)を含む
- Controllerのエンドポイントプレフィックスを取り込むアプリケーション側で決めることができる
- 例えば「/api/demo」⇒「/webapi/demo」など
2. バージョンや条件など
- Java 8
- Spring Boot 1.5.10.RELEASE
3. やったこと
RequestMappingHandlerMappingを継承したクラス
ApiRequestMappingHandlerMapping.java
package jp.gr.java_conf.pekokun.springbootdemo.externalapi.app;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
@Component
public class ApiRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
@Value("${demo.externalapi.prefix:api}")
private String prefix;
public ApiRequestMappingHandlerMapping() {
// (1)
setOrder(0);
}
@Override
protected boolean isHandler(Class<?> beanType) {
// (2)
return AnnotatedElementUtils.hasAnnotation(beanType, Api.class);
}
@Override
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
Api api = method.getDeclaringClass().getAnnotation(Api.class);
if (Objects.isNull(api)) {
// No-annotated Api, then do nothing.
return;
}
// (3)
List<String> patterns = mapping.getPatternsCondition().getPatterns().stream()
.map(p -> getPathMatcher().combine(prefix, p))
.collect(Collectors.toList());
// (4)
super.registerHandlerMethod(handler, method,
new RequestMappingInfo(
new PatternsRequestCondition(patterns.toArray(new String[patterns.size()])),
mapping.getMethodsCondition(),
mapping.getParamsCondition(),
mapping.getHeadersCondition(),
mapping.getConsumesCondition(),
mapping.getProducesCondition(),
mapping.getCustomCondition()));
}
}
(1) setOrder(int order)で何かしらの値を設定します。
継承元のAbstractHandlerMapping に以下の設定があるためです。
private int order = Integer.MAX_VALUE; // default: same as non-Ordered
(2) 処理対象のBeanであるか判定をします。
BeanのクラスにApiがアノテーションされているかを判定します。
デフォルトでは以下のようにControllerもしくはRequestMappingがアノテーションされているかを判定しています。
/**
* {@inheritDoc}
* Expects a handler to have a type-level @{@link Controller} annotation.
*/
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
(3) エンドポイントプレフィックスを付加したパターンを生成します。
(4) (3)で生成したパターンを用いてRequestMappingInfoを生成し、それを使って登録します。
対象のControllerにマークするためのAnnotation
Api.java
package jp.gr.java_conf.pekokun.springbootdemo.externalapi.app;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ResponseBody;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
@ResponseBody
public @interface Api {
}
対象のController
DemoRestController.java
package jp.gr.java_conf.pekokun.springbootdemo.externalapi.app;
import org.springframework.web.bind.annotation.GetMapping;
// (1)
@Api
public class DemoRestController {
@GetMapping(path = "demo")
public String get() {
return "DEMO";
}
}
(1) ControllerやRestController、RequestMappingは付けません。
デフォルトのRequestMappingHandlerMappingの対象となるためです。
4. Github
おわり。
追記(2018/8/15)
- Spring Boot 2.0.3.RELEASE でも動きました。
- 上記の@Apiは@ResponseBodyが付いているため、RestControllerのような動きになります。
- @ResponseBodyを付けなければ、Controllerのように動きます。例えば次のようなAnnotationを用意します。
View.java
package jp.gr.java_conf.pekokun.springbootdemo.externalapi.app;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.stereotype.Component;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface View {
}
- カスタムするRequestMappingHandlerMappingの内容も動作に合わせて変更します。
おわり。