RESTful Web Service using SpringMVC入門
SpringMVC使ってRestサーバ作ってみる。
覚えたてのGradleベースでやる。
環境は下記。
>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つがある。
- URLの拡張子
- 例)HTML形式の要求
- 例)spreadsheet形式の要求
- リクエスト(クエリ)パラメータ
- 例)spreadsheet形式の要求
- http://myserver/myapp/accounts/list?format=xls
- http://myserver/myapp/accounts/list?mediaType=xls
- 例)spreadsheet形式の要求
- Acceptヘッダ
- 例)JSON形式の要求
- Accept: applicatino/json
- 例)JSON形式の要求
上記の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」という方針をとっていて
- Path extension
- 拡張子
- Parameter
- リクエスト(クエリ)パラメータ
- 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は以下。
- ByteArrayHttpMessageConverter
- converts byte arrays
- StringHttpMessageConverter
- converts Strings
- デフォルトでメディアタイプ「text/*」をサポートし、「Content-Type: text/plain」でレスポンスを返却
- ResourceHttpMessageConverter
- converts org.springframework.core.io.Resource for any type of octet stream
- SourceHttpMessageConverter
- converts javax.xml.transform.Source
- FormHttpMessageConverter
- converts form data to/from a MultiValueMap
. - デフォルトでメディアタイプ「application/x-www-form-urlencoded」をサポートし、MultiValueMap
のデータ形式でレスポンスを返却
- converts form data to/from a MultiValueMap
- Jaxb2RootElementHttpMessageConverter
- converts Java objects to/from XML (added only if JAXB2 is present on the classpath)
- MappingJackson2HttpMessageConverter
- converts JSON (added only if Jackson 2 is present on the classpath)
- デフォルトでメディアタイプ「application/json」をサポートし、Jackson2のObjectMapperでデータを読み書き
- MappingJacksonHttpMessageConverter
- converts JSON (added only if Jackson is present on the classpath)
- デフォルトでメディアタイプ「application/json」をサポートし、JacksonのObjectMapperでデータを読み書き
- AtomFeedHttpMessageConverter
- converts Atom feeds (added only if Rome is present on the classpath)
- デフォルトでメディアタイプ「application/atom+xml」をサポート
- 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」して下記のいずれかの方法でアクセスする。
- 「http://localhost:8080/SpringMVC/greeting.xml」
- 「http://localhost:8080/SpringMVC/greeting?format=xml」
- HTTPヘッダを「Accept: application/xml」にして「http://localhost:8080/SpringMVC/greeting」
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で投げたとしてもコンテンツネゴシエーションで受け取るメディアタイプを指定できる。