JAX-RSをやってみる (12) - MVC -
公式ドキュメント: https://jersey.java.net/documentation/latest/user-guide.html#mvc
参考: http://qiita.com/opengl-8080/items/f4c25ad671e8a6dac743
jersey-mvcなAPIを使えばViewableをぶん投げたりする事によりテンプレートエンジンによりレスポンスを出したりする事も出来る。※現時点で公式でサポートされているテンプレートエンジンがfreemarkerとmustacheとjsp
っていう事でやってみる
その前に
jersey-mvc-jspなJSPをレンダリングする仕組みを使う場合にテスト書くとエラーを引き起こす。※freemarkerとmustacheでは起きない(HttpServletRequest等をマッピングしない為かと)
っていう事でthymeleafを使ってテンプレートエンジンによりレスポンスを送信する仕組みのTemplateProcessorを作りながらやる
依存性
apply plugin: "java"
apply plugin: "jetty"
apply plugin: "eclipse"
repositories {
mavenCentral()
}
dependencies {
compile "org.slf4j:slf4j-simple:1.7.7"
compile "org.thymeleaf:thymeleaf:+"
providedCompile "org.glassfish.jersey.containers:jersey-container-servlet:2.9.+"
providedCompile "org.glassfish.jersey.ext:jersey-mvc:2.9.+"
// 他のを使う場合には以下を使えば良い
//compile "org.glassfish.jersey.ext:jersey-mvc-freemarker:2.9.+"
//compile "org.glassfish.jersey.ext:jersey-mvc-mustache:2.9.+"
//compile "org.glassfish.jersey.ext:jersey-mvc-jsp:2.9.+"
testCompile "org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-bundle:2.9.+"
// テスト時にMock系を使う為
testCompile "org.springframework:spring-test:+"
testCompile "org.hamcrest:hamcrest-all:+"
}
んでまずThymeleafなテンプレートエンジンを使ってレスポンスを処理するのを作る
ThymeleafFeature.java
package sample;
import javax.ws.rs.ConstrainedTo;
import javax.ws.rs.RuntimeType;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.Feature;
import javax.ws.rs.core.FeatureContext;
import org.glassfish.jersey.server.mvc.MvcFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ConstrainedTo(RuntimeType.SERVER)
public class ThymeleafFeature implements Feature {
@Override
public boolean configure(FeatureContext context) {
final Configuration config = context.getConfiguration();
if (!config.isRegistered(ThymeleafTemplateProcessor.class)) {
context.register(ThymeleafTemplateProcessor.class);
if (!config.isRegistered(MvcFeature.class)) {
// これしないとViewableMessageBodyWriter周りが作用しないので
context.register(MvcFeature.class);
}
}
return true;
}
}
ThymeleafTemplateProcessor.java
package sample;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Inject;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import org.glassfish.jersey.server.mvc.Viewable;
import org.glassfish.jersey.server.mvc.spi.AbstractTemplateProcessor;
import org.jvnet.hk2.annotations.Optional;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
public class ThymeleafTemplateProcessor extends AbstractTemplateProcessor<String> {
@Inject
HttpServletRequest request;
@Inject
HttpServletResponse response;
@Inject
ServletContext servletContext;
@Inject
public ThymeleafTemplateProcessor(Configuration config, @Optional ServletContext servletContext) {
super(config, servletContext, "thymeleaf", "html");
this.servletContext = servletContext;
}
@SuppressWarnings("unchecked")
@Override
public void writeTo(String templateReference, Viewable viewable,
MediaType mediaType, MultivaluedMap<tring, Object> httpHeaders,
OutputStream out) throws IOException {
try(Writer writer = new OutputStreamWriter(out)) {
Object o = viewable.getModel();
Map<String, Object> stash;
if (o instanceof Map) {
stash = (Map<String, Object>)o;
} else {
stash = new HashMap<String, Object>();
// @ErrorTemplate時はパラメーターがExceptionで返ってくる
if (o instanceof Exception) {
stash.put("error", o);
} else {
// 返ってきた値がMapでない場合にitなキーで参照出来るように
stash.put("it", o);
}
}
WebContext context = new WebContext(request, response, servletContext);
context.setVariables(stash);
TemplateEngine engine = new TemplateEngine();
engine.setTemplateResolver(new ClassLoaderTemplateResolver());
engine.process(templateReference, context, writer);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected String resolve(String templatePath, Reader reader) throws Exception {
return templatePath;
}
}
@Injectで注入するのが正しいかは微妙。mvc-jspな奴はProvider<Ref<HttpServletRequest>>的な感じなのを注入しているが、これだとどうもテスト時の注入でAbstractBinder使っても上手く作用してくれないので
んまぁてな感じでThymeleafを使ってテンプレートエンジンによるレスポンスの処理なところはこれだけ。設定しないと使えませんが
web.xml
MVCによる処理を利用する場合においてはweb.xmlでfilter周りを設定しないといけないみたいな制約がある模様なので
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<filter>
<filter-name>jersey</filter-name>
<filter-class>org.glassfish.jersey.servlet.ServletContainer</filter-class>
<init-param>
<param-name>javax.ws.rs.Application</param-name>
<param-value>sample.SampleApplication</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>jersey</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
な感じで
beans.xml
glassfishとかで動かす場合に
<?xml version="1.0" ?>
<beans
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
version="1.1"
bean-discovery-mode="all">
</beans>
となっているとデプロイエラーを起こすので、bean-discovery-mode="annotated"にしておく
SampleApplication.java
package sample;
import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.mvc.MvcFeature;
@ApplicationPath("/")
public class SampleApplication extends ResourceConfig {
public SampleApplication() {
// 設定しておかないとエラーになる
property(MvcFeature.TEMPLATE_BASE_PATH, "templates");
// ThymeleafTemplateProcessorをregisterするFeatureをregister
register(ThymeleafFeature.class);
packages("sample");
}
}
あとはリソースで使うだけ。
Home.java
package sample.controller;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import org.glassfish.jersey.server.mvc.ErrorTemplate;
import org.glassfish.jersey.server.mvc.Template;
import org.glassfish.jersey.server.mvc.Viewable;
@Path("/sample")
public class Home {
public String name;
/* Template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<p th:text="${it.name}" />
</body>
</html>
*/
@Path("test1")
@GET
public Viewable test1(@QueryParam("name") @DefaultValue("hoge") String name) {
this.name = name;
return new Viewable("/test1.html", this);
}
/* Template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<p th:text="${it}" />
</body>
</html>
*/
@Path("test2")
@GET
@Template(name = "/test2.html")
public String test2() {
// Stringで返しているのでitとして参照出来るようになる
return "test2";
}
/* Template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<p th:text="${message}" />
</body>
</html>
*/
@Path("test3")
@GET
@Template(name = "/test3.html")
public Map<String, Object> test3() {
// Map型で返しているのでその内部のデータがそのままマッピングされる。
Map<String, Object> data = new HashMap<String, Object>(1);
data.put("message", "hello world");
return data;
}
/* Template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<span th:text="|error: ${error.message}|" />
</body>
</html>
*/
@Path("test4")
@GET
@ErrorTemplate(name = "/error.html")
public void error() throws Exception {
throw new Exception("hoge");
}
}
んまぁという感じで大体どういうレスポンスになるかは想像出来るのはないかと
HomeTest.java
テストする際にHttpServletRequest等の@Injectは行われないのでエラーになる。なのでSpring Framework Testingに入ってるMockHttpServletRequest等をAbstractBinderを使って差し込む的な事をする事で対処出来る模様
package sample.controller;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.Application;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;
import sample.SampleApplication;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;
public class HomeTest extends JerseyTest {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
@Override
protected Application configure() {
return new SampleApplication()
.register(new AbstractBinder() {
@Override
protected void configure() {
bind(request).to(HttpServletRequest.class);
bind(response).to(HttpServletResponse.class);
bind(new MockServletContext()).to(ServletContext.class);
}
});
}
@Test
public void test1() {
String response = target("/sample/test1").request().get(String.class);
assertThat(response, containsString("hoge"));
}
@Test
public void test2() {
String response = target("/sample/test2").request().get(String.class);
assertThat(response, containsString("test2"));
}
@Test
public void test3() {
String response = target("/sample/test3").request().get(String.class);
assertThat(response, containsString("hello world"));
}
@Test
public void test4() {
String response = target("/sample/test4").request().get(String.class);
assertThat(response, containsString("error: hoge"));
}
}
終わり。まぁテスト出来ないとか難がちょいとあったけど、リソースメソッドでViewableを返す事でテンプレートエンジンを利用してレスポンスを出したり出来る仕組みを利用するのであればこれ使えば良い
あとテスト動いてもglassfishとかじゃ動かないとかあるあるなのでちゃんと確認した方が良い。ちなみにそういうテストをサポートするTestContainerFactoryがあるっぽいので