Spring Boot - Controllerのエンドポイントプレフィックスを動的に設定する

# 1. やりたいこと

- 外部Jarとして取り込まれる
- 外部Jar内にController(RestController)を含む
- Controllerのエンドポイントプレフィックスを取り込むアプリケーション側で決めることができる
- 例えば「/api/demo」⇒「/webapi/demo」など

# 2. バージョンや条件など

- Java 8
- Spring Boot 1.5.10.RELEASE

# 3. やったこと

### RequestMappingHandlerMappingを継承したクラス

```java: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)で何かしらの値を設定します。
&nbsp;&nbsp;&nbsp;&nbsp; 継承元のAbstractHandlerMapping に以下の設定があるためです。
&nbsp;&nbsp;&nbsp;&nbsp; `private int order = Integer.MAX_VALUE;  // default: same as non-Ordered`

(2) 処理対象のBeanであるか判定をします。
&nbsp;&nbsp;&nbsp;&nbsp; BeanのクラスにApiがアノテーションされているかを判定します。
&nbsp;&nbsp;&nbsp;&nbsp; デフォルトでは以下のようにControllerもしくはRequestMappingがアノテーションされているかを判定しています。

```java

	/**
	 * {@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

```java: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

```java: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は付けません。
&nbsp;&nbsp;&nbsp;&nbsp; デフォルトのRequestMappingHandlerMappingの対象となるためです。

# 4. Github

- https://github.com/ShigeoTejima/spring-boot-demo-externalapi

おわり。

# 追記(2018/8/15)

- Spring Boot 2.0.3.RELEASE でも動きました。
- 上記の@Apiは@ResponseBodyが付いているため、RestControllerのような動きになります。
- @ResponseBodyを付けなければ、Controllerのように動きます。例えば次のようなAnnotationを用意します。

```java: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の内容も動作に合わせて変更します。

おわり。