Spring MVC で Controller を動的に切り替える - RequestMappingHandlerMapping のサブクラス利用

Spring MVC では、基本的に @RequestMapping アノテーションで指定した URL パターンに合致する Controller のメソッドを実行し、org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping クラスがその処理を担っています。

そこで、試しに RequestMappingHandlerMapping のサブクラスを使って、実行対象の Controller (@RequestMapping のパス違い) を動的に切り替えるようにしてみました。

サンプルソースhttp://github.com/fits/try_samples/tree/master/blog/20150507/

はじめに

今回は、以下のように Query string (URL) の debug パラメータ有無によって、実行する Controller を切り替える処理を RequestMappingHandlerMapping のサブクラスで実現します。

URL 実行するメソッド
/sample/xxx SampleController.sample
/sample/xxx?debug DebugSampleController.sample

ただし、Controller を切り替えなくても他にやり様はいくらでもありますので、本件の実用性は低いと思います。

実装

Controller クラス

まずは Controller を 2つ用意します。

src/main/java/sample/controller/SampleController.java
package sample.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class SampleController {
    @RequestMapping("/sample/{id}")
    @ResponseBody
    public String sample(@PathVariable("id") String id) {
        return "sample: " + id + ", " + System.currentTimeMillis();
    }
}

2つ目には @RequestMapping パスの先頭に /debug を付けました。

src/main/java/sample/controller/DebugSampleController.java
package sample.controller;
・・・
@Controller
public class DebugSampleController {
    @RequestMapping("/debug/sample/{id}")
    @ResponseBody
    public String sample(@PathVariable("id") String id) {
        return "debug-sample: " + id + ", " + new Date();
    }
}

RequestMappingHandlerMapping サブクラス

次に、本題の RequestMappingHandlerMapping サブクラスを実装します。

とりあえず、下記のように実装すれば別の Controller を呼び出せます。

  • (1) lookupHandlerMethod メソッドをオーラーライドし lookupPath を変更
  • (2) HttpServletRequest.getServletPath() の値を (1) に合わせて変更

(1) を実施しただけでは内部的なパスのチェック処理に引っかかるので (2) も合わせて実施する必要があります。

下記では、Query string へ debug が付いていた場合に Controller を選出するパス (lookupPath) の先頭に /debug を追加するように実装し、getServletPath() の戻り値を変更するために HttpServletRequestWrapper のサブクラスを使っています。

src/main/java/sample/mapping/SampleRequestMappingHandlerMapping.java
package sample.mapping;

import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class SampleRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
    // (1)
    @Override
    protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
        return super.lookupHandlerMethod(
            changePath(lookupPath, request), 
            new SampleHttpServletRequest(request)
        );
    }

    // Controller を選出するパスを書き換える
    private String changePath(String path, HttpServletRequest request) {
        if (request.getParameter("debug") != null) {
            return "/debug" + path;
        }
        return path;
    }

    class SampleHttpServletRequest extends HttpServletRequestWrapper {
        public SampleHttpServletRequest(HttpServletRequest req) {
            super(req);
        }
        // (2) 
        @Override
        public String getServletPath() {
            return changePath(super.getServletPath(), this);
        }
    }
}

設定クラス

SampleRequestMappingHandlerMapping を適用するように Bean 定義を行います。

src/main/java/sample/config/WebConfig.java
package sample.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import sample.mapping.SampleRequestMappingHandlerMapping;

@Configuration
@EnableWebMvc
public class WebConfig {
    @Bean
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        return new SampleRequestMappingHandlerMapping();
    }
}

実行クラス

今回は Spring Boot を使って実行しますので、そのための実行クラスも用意します。

src/main/java/sample/Application.java
package sample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan
@EnableAutoConfiguration
public class Application {
    public static void main(String... args) {
        SpringApplication.run(Application.class, args);
    }
}

ビルド定義

Gradle のビルド定義ファイルは以下の通りです。 (Spring Boot 利用)

build.gradle
apply plugin: 'spring-boot'

def enc = 'UTF-8'
tasks.withType(AbstractCompile)*.options*.encoding = enc

buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.3.RELEASE'
    }
}

repositories {
    jcenter()
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web:1.2.3.RELEASE'
}

実行

bootRun タスクを実行し、Tomcat 上で Web アプリケーションを起動しておきます。

起動
> gradle bootRun

・・・
:bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.3.RELEASE)
・・・
2015-05-03 16:43:04.306  INFO 4860 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2015-05-03 16:43:04.308  INFO 4860 --- [           main] sample.Application                       : Started Application in 5.001 seconds (JVM running for 6.5)

/sample/xxx/sample/xxx?debug へアクセスすると、?debug の有無で実行結果 (実行対象 Controller) が変化する事を確認できました。

動作確認
$ curl http://localhost:8080/sample/abc
sample: abc, 1430639107957

$ curl http://localhost:8080/sample/abc?debug
debug-sample: abc, Sun May 03 16:45:12 JST 2015