ぺーぺーSEのブログ

備忘録・メモ用サイト。

RESTful Web Service using SpringMVC入門

SpringMVC使ってRestサーバ作ってみる。
覚えたてのGradleベースでやる。

tanakakns.hatenablog.com


環境は下記。

>gradle -v

------------------------------------------------------------
Gradle 2.1
------------------------------------------------------------

Build time:   2014-09-08 10:40:39 UTC
Build number: none
Revision:     e6cf70745ac11fa943e19294d19a2c527a669a53

Groovy:       2.3.6
Ant:          Apache Ant(TM) version 1.9.3 compiled on December 23 2013
JVM:          1.8.0_20 (Oracle Corporation 25.20-b23)
OS:           Windows 7 6.1 amd64

(1)プロジェクト用ディレクトリ作成
   ここでは「SpringMVC」にした。
(2)「gradle init --type java-library」でもたたいてひな形作成
(3)build.gradleを下記のように修正

■build.gradle

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'war'
apply plugin: 'tomcat'

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'org.gradle.api.plugins:gradle-tomcat-plugin:1.2.4'
    }
}

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.springframework:spring-webmvc:4.1.0.RELEASE'
    compile 'javax.servlet:javax.servlet-api:3.1.0'
    runtime 'com.fasterxml.jackson.core:jackson-databind:2.4.2'

    def tomcatVersion = '8.0.12'
    tomcat "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}",
           "org.apache.tomcat.embed:tomcat-embed-logging-juli:${tomcatVersion}"
    tomcat("org.apache.tomcat.embed:tomcat-embed-jasper:${tomcatVersion}") {
        exclude group: 'org.eclipse.jdt.core.compiler', module: 'ecj'
    }
}

なんか会社でSpringBoot使えることが一生無い気がするのであえて使わない方向で。

(4)「gradle eclipse」してからEclipseでコーディング

■hello.Greeting.java

package hello;

public class Greeting {

    private final long id;
    private final String content;

    public Greeting(long id, String content) {
        this.id = id;
        this.content = content;
    }

    public long getId() {
        return id;
    }

    public String getContent() {
        return content;
    }
}

■hello.GreetingController.java

package hello;

import java.util.concurrent.atomic.AtomicLong;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetingController {

    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();

    @RequestMapping("/greeting")
    public Greeting greeting(@RequestParam(value="name", required=false, defaultValue="World") String name) {
        return new Greeting(counter.incrementAndGet(),
                            String.format(template, name));
    }
}

■hello.AppConfig.java

package hello;

import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

}

XML-Based Configurationじゃなくて、Java-Based Configurationにしてみた。

■hello.WebMvcConfig.java

package hello;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
@ComponentScan(basePackages = { "hello" })
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
	@Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.ignoreAcceptHeader(true).defaultContentType(MediaType.APPLICATION_JSON);
    }
}

MVC用の設定もJava-Based Configurationにしてみた。

■hello.WebAppInitializer.java

package hello;

import java.util.Set;

import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

public class WebAppInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) {
        // Create the 'root' Spring application context
        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
        rootContext.register(AppConfig.class);
        rootContext.refresh();

        // Manage the lifecycle of the root application context
        servletContext.addListener(new ContextLoaderListener(rootContext));

        // Create the dispatcher servlet's SpringMVC application context
        AnnotationConfigWebApplicationContext mvcContext = new AnnotationConfigWebApplicationContext();
        mvcContext.register(WebMvcConfig.class);

        mvcContext.setParent(rootContext);

        // Register and map the dispatcher servlet
        ServletRegistration.Dynamic dispatcherServlet = servletContext.addServlet("dispatcherServlet", new DispatcherServlet(mvcContext));
        dispatcherServlet.setLoadOnStartup(1);
        Set<String> mappingConflicts = dispatcherServlet.addMapping("/");

        // Check the servlet mappings
        if (!mappingConflicts.isEmpty()) {
          for (String s : mappingConflicts) {
            System.out.println("[ERROR] Mapping conflict: " + s);
          }
          throw new IllegalStateException(
              "'webservice' cannot be mapped to '/'");
        }

    }
}

web.xmlの代わり。Servlet 3系っぽいっしょ。

(5)「gradle clean build
(6)「gradle tomcatRunWar
(7)ブラウザか何かで「http://localhost:8080/SpringMVC/greeting」へアクセス

動かなかったらごめん。
現状ではJSONのみ対応。

コンテンツネゴシエーション

SpringMVCではクライアントからサーバへコンテンツのフォーマットを要求する方法として以下の3つがある。

上記の3つとも存在しない場合のデフォルトのレスポンスのボディ形式は下記のように設定する。

■Java-Based Configuration

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

  @Override
  public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    configurer.defaultContentType(MediaType.APPLICATION_XML); // ←ココ!
  }
}

■XML-Based Configuration

<bean id="contentNegotiationManager"
           class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
  <property name="defaultContentType" value="application/xml" />
</bean>

<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager" />

SpringMVCでは「PPA Strategy」という方針をとっていて

  1. Path extension
    • 拡張子
  2. Parameter
    • リクエスト(クエリ)パラメータ
  3. Accept header
    • Acceptヘッダ

の順でレスポンスの形式を決定する。

今回のアプリで上記のコンテンツネゴシエーションできるようにする設定は以下。

■hello.WebMvcConfig.java

package hello;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
@ComponentScan(basePackages = { "hello" })
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
	@Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer
            .favorPathExtension(true)                       // URIの拡張子でコンテンツネゴシエーションを有効に(デフォルト:true)
            .favorParameter(true)                           // クエリパラメータでコンテンツネゴシエーションを有効に(デフォルト:false)
            .parameterName("format")                        // クエリパラメータでコンテンツネゴシエーションを可能にするパラメータ名を指定
            .defaultContentType(MediaType.APPLICATION_JSON)
            .mediaType("xml", MediaType.APPLICATION_XML);   // 後述の「XML対応」で必要
    }
}

SpringMVCではMessage Convertersがレスポンスのオブジェクトを対応したフォーマットの電文に変換する。
JAX-RSでいうEntity Providerに対応するのかな。
参考①によると、デフォルトのMessage Convertersは以下。

  1. ByteArrayHttpMessageConverter
    • converts byte arrays
  2. StringHttpMessageConverter
    • converts Strings
    • デフォルトでメディアタイプ「text/*」をサポートし、「Content-Type: text/plain」でレスポンスを返却
  3. ResourceHttpMessageConverter
    • converts org.springframework.core.io.Resource for any type of octet stream
  4. SourceHttpMessageConverter
    • converts javax.xml.transform.Source
  5. FormHttpMessageConverter
    • converts form data to/from a MultiValueMap.
    • デフォルトでメディアタイプ「application/x-www-form-urlencoded」をサポートし、MultiValueMapのデータ形式でレスポンスを返却
  6. Jaxb2RootElementHttpMessageConverter
    • converts Java objects to/from XML (added only if JAXB2 is present on the classpath)
  7. MappingJackson2HttpMessageConverter
    • converts JSON (added only if Jackson 2 is present on the classpath)
    • デフォルトでメディアタイプ「application/json」をサポートし、Jackson2のObjectMapperでデータを読み書き
  8. MappingJacksonHttpMessageConverter
    • converts JSON (added only if Jackson is present on the classpath)
    • デフォルトでメディアタイプ「application/json」をサポートし、JacksonのObjectMapperでデータを読み書き
  9. AtomFeedHttpMessageConverter
    • converts Atom feeds (added only if Rome is present on the classpath)
    • デフォルトでメディアタイプ「application/atom+xml」をサポート
  10. RssChannelHttpMessageConverter
    • converts RSS feeds (added only if Rome is present on the classpath)
    • デフォルトでメディアタイプ「application/rss+xml」をサポート

デフォルトと言っても6〜10は対応するクラス(ライブラリ)をクラスパス配下に置かないといけないみたい。
今回の例でいうところの「runtime 'com.fasterxml.jackson.core:jackson-databind:2.4.2'」(build.gradle)は7に対応してるのかな。
Message Converterのカスタマイズについても参考①。

参考①
http://www.baeldung.com/spring-httpmessageconverter-rest

XML対応

現状ではJSON形式のみのレスポンスなのでXML形式でもレスポンス可能にする。
下記のように修正。

■build.gradle

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'war'
apply plugin: 'tomcat'

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'org.gradle.api.plugins:gradle-tomcat-plugin:1.2.4'
    }
}

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.springframework:spring-webmvc:4.1.0.RELEASE'
    compile 'javax.servlet:javax.servlet-api:3.1.0'
    runtime 'com.fasterxml.jackson.core:jackson-databind:2.4.2'
    runtime 'com.sun.xml.bind:jaxb-impl:2.2.7' // 追加行

    def tomcatVersion = '8.0.12'
    tomcat "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}",
           "org.apache.tomcat.embed:tomcat-embed-logging-juli:${tomcatVersion}"
    tomcat("org.apache.tomcat.embed:tomcat-embed-jasper:${tomcatVersion}") {
        exclude group: 'org.eclipse.jdt.core.compiler', module: 'ecj'
    }
}

■hello.Greeting.java

package hello;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Greeting {

    private long id;
    private String content;

    protected Greeting(){}

    public Greeting(long id, String content) {
        this.id = id;
        this.content = content;
    }

    public long getId() {
        return id;
    }

    public String getContent() {
        return content;
    }

    @XmlElement
    public void setId(long id) {
        this.id = id;
    }

    @XmlElement
    public void setContent(String content) {
        this.content = content;
    }
}

@XmlRootElement」および「@XmlElement」およびセッターを追加した。

■hello.WebMvcConfig.java
先述のコンテンツネゴシエーションのところで、「configure」に対して「mediaType」メソッドでXMLを追加したのが効く。

gradle clean build」、「gradle tomcatRunWar」して下記のいずれかの方法でアクセスする。


アノテーションについて

SpringMVCのあのてーしょんについては下記を参照。
http://d.hatena.ne.jp/tanakakns/20130705/1372998820


POSTしてみる

これまではGETばかりだったが今度はPOSTしてみる。
意図はクライアントから投げた電文を受け取れるか見たいだけ。
下記のようにソースを編集。

■hello.GreetingController.java

package hello;

import java.util.concurrent.atomic.AtomicLong;

import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetingController {

    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();

    @RequestMapping(value="/greeting", method=RequestMethod.GET) // methodを追加
    public Greeting greetingGet(@RequestParam(value="name", required=false, defaultValue="World") String name) {
        return new Greeting(counter.incrementAndGet(),
                            String.format(template, name));
    }

    @RequestMapping(value="/greeting", method=RequestMethod.POST)  // 新規メソッド
    public Greeting greetingPost(@RequestBody Greeting greeting) { //
        return greeting;                                           //
    }                                                              //
}

POSTメソッドで「http://localhost:8080/SpringMVC/greeting」へHTTPヘッダ「Content-Type: application/json」を付与して下記の電文を投げるとそのまま返ってくる。

{"id":100,"content":"Hello, World!"}

ちなみに「http://localhost:8080/SpringMVC/greeting.xml」へ投げるとXML形式で帰ってくる。
JSONで投げたとしてもコンテンツネゴシエーションで受け取るメディアタイプを指定できる。