WebアプリやHTTP通信のテストコード用に、簡易なサーバが欲しいと思いまして。
以下の2つを使うことを考えてみます。
このあたりを、組み込みTomcatとJDKに付いているHttpServerを使って作ってみたいと思います。
また、目的が目的なので、テストコードと組み合わせて考えます。
Servletなどを動かすためのサーバー
これは、作成しているWebアプリケーション自身をデプロイして動かすことを考えます。
この時、web.xmlを読ませるようにしておきます。
※単純に、後で方法を探すのが面倒なので
Mavenで使うpom.xmlは、このように構成。
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.littlewings</groupId> <artifactId>webapp-test-embedded-tomcat-jdkserver</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <failOnMissingWebXml>false</failOnMissingWebXml> </properties> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> <version>8.0.28</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <version>8.0.28</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-logging-juli</artifactId> <version>8.0.28</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.2.0</version> <scope>test</scope> </dependency> </dependencies> </project>
簡単なServletも用意します。
src/main/java/org/littlewings/web/HelloServlet.java
package org.littlewings.web; import java.io.IOException; import java.util.Optional; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String name = Optional.ofNullable(request.getParameter("name")).orElse("World"); response.getWriter().write("Hello " + name + "!!"); } }
web.xmlも用意。
src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app 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/web-app_3_1.xsd" version="3.1"> <servlet> <servlet-name>HelloServlet</servlet-name> <servlet-class>org.littlewings.web.HelloServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>HelloServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> </web-app>
では、組み込みTomcatをテストコードで使うことを考えます。単純に、組み込みTomcatのラッパーを用意。
src/test/java/org/littlewings/web/EmbeddedTomcat.java
package org.littlewings.web; import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import javax.servlet.ServletException; import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; import org.apache.catalina.startup.Tomcat; public class EmbeddedTomcat { private Tomcat tomcat; private boolean temporaryBaseDir = false; private String baseDir; private int port; private Context context; protected EmbeddedTomcat(Tomcat tomcat) { this.tomcat = tomcat; } public static EmbeddedTomcat create() { return new EmbeddedTomcat(new Tomcat()); } public static EmbeddedTomcat create(Tomcat tomcat) { return new EmbeddedTomcat(tomcat); } public EmbeddedTomcat port(int port) { tomcat.setPort(port); this.port = port; return this; } public EmbeddedTomcat temporaryBaseDir() { temporaryBaseDir = true; return baseDir(createTemporaryPath("tomcat", Integer.toString(port))); } public EmbeddedTomcat baseDir(String path) { tomcat.setBaseDir(path); baseDir = path; return this; } public EmbeddedTomcat webapp(String contextPath, String docBase) { try { if (context == null) { context = tomcat.addWebapp(contextPath, docBase); } else { throw new IllegalArgumentException("Only support, one webapp."); } return this; } catch (ServletException e) { throw new IllegalArgumentException(e); } } public EmbeddedTomcat start() { try { tomcat.start(); return this; } catch (LifecycleException e) { throw new IllegalStateException(e); } } public EmbeddedTomcat await() { tomcat.getServer().await(); return this; } public void shutdown() { try { tomcat.stop(); tomcat.destroy(); if (temporaryBaseDir) { Files.walkFileTree(Paths.get(baseDir), new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path directory, IOException e) throws IOException { Files.delete(directory); return FileVisitResult.CONTINUE; } }); } } catch (LifecycleException | IOException e) { throw new IllegalStateException(e); } } public static String toAbsolutePath(String path) { return Paths.get(path).toAbsolutePath().toString(); } public static String createTemporaryPath(String prefix, String suffix) { try { File tempDir = File.createTempFile(prefix, suffix); tempDir.delete(); tempDir.mkdir(); tempDir.deleteOnExit(); return tempDir.getAbsolutePath(); } catch (IOException e) { throw new UncheckedIOException(e); } } }
TomcatのdocBaseは一時領域にもできて、その場合は終了時に削除するようにしています。Webアプリケーションは、ひとつのみで考えています。この時に、src/main/webappとかをWebアプリケーションのdocBaseとして設定します。
実際に、このコードを使ったテストコード。
src/test/java/org/littlewings/web/HelloServletTest.java
package org.littlewings.web; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URI; import java.nio.charset.StandardCharsets; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public class HelloServletTest { private static EmbeddedTomcat tomcat; @BeforeClass public static void setUpClass() { tomcat = EmbeddedTomcat .create() .port(8080) .temporaryBaseDir() .webapp("/", EmbeddedTomcat.toAbsolutePath("src/main/webapp")) .start(); } @AfterClass public static void tearDownClass() { tomcat.shutdown(); } @Test public void testServlet1() throws IOException { HttpURLConnection conn = (HttpURLConnection) URI .create("http://localhost:8080/hello?name=Tomcat") .toURL() .openConnection(); try (InputStreamReader isr = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8); BufferedReader reader = new BufferedReader(isr)) { assertThat(reader.readLine()) .isEqualTo("Hello Tomcat!!"); } finally { conn.disconnect(); } } @Test public void testServlet2() throws IOException { HttpURLConnection conn = (HttpURLConnection) URI .create("http://localhost:8080/hello") .toURL() .openConnection(); try (InputStreamReader isr = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8); BufferedReader reader = new BufferedReader(isr)) { assertThat(reader.readLine()) .isEqualTo("Hello World!!"); } finally { conn.disconnect(); } } }
静的ファイルなどを読むための、簡易的なHTTPサーバー
今度は、HTMLファイルなどを静的に読み込むサーバーを書いてみます。
こちらは、JDKに付属しているHttpServerを使います。
こんな感じで実装してみました。
src/test/java/org/littlewings/web/StaticHttpServer.java
package org.littlewings.web; import java.io.IOException; import java.io.OutputStream; import java.io.UncheckedIOException; import java.net.InetSocketAddress; import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.concurrent.Executor; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; public class StaticHttpServer { private HttpServer httpServer; private String contextPath; private HttpHandler handler; protected StaticHttpServer(HttpServer httpServer, String contextPath, String docRoot) { this.httpServer = httpServer; this.contextPath = contextPath; handler = defaultHandler(docRoot); } public static StaticHttpServer create(int port, String contextPath, String docRoot) { try { return new StaticHttpServer(HttpServer.create(new InetSocketAddress(port), 0), contextPath, docRoot); } catch (IOException e) { throw new UncheckedIOException(e); } } public StaticHttpServer executor(Executor executor) { httpServer.setExecutor(executor); return this; } public StaticHttpServer handler(HttpHandler handler) { this.handler = handler; return this; } protected HttpHandler defaultHandler(String docRoot) { Path docRootPath = Paths.get(docRoot); return exchange -> { URI uri = exchange.getRequestURI(); String path = uri.getPath(); String relativePath = path.substring(1); Path requestPath = docRootPath.resolve(relativePath); if (Files.exists(requestPath) && requestPath.startsWith(docRoot)) { byte[] bytes = Files.readAllBytes(requestPath); exchange.getResponseHeaders().add("Content-Type", "text/html;charset=utf-8"); exchange.sendResponseHeaders(200, bytes.length); try (OutputStream os = exchange.getResponseBody()) { for (byte b : bytes) { os.write(b); } } } else { byte[] messageBinary = ("Request[" + uri + "], Not Found.").getBytes(StandardCharsets.UTF_8); exchange.sendResponseHeaders(404, messageBinary.length); try (OutputStream os = exchange.getResponseBody()) { for (byte b : messageBinary) { os.write(b); } } } }; } public StaticHttpServer start() { httpServer.createContext(contextPath, handler); httpServer.start(); return this; } public void shutdown() { shutdown(0); } public void shutdown(int waitSeconds) { httpServer.stop(waitSeconds); } }
ドキュメントルートやコンテキストパス、ポートを指定して起動します。Content-Typeはtext/html決め打ちですが、拡張子などである程度動的に変えてもいいでしょう。
とりあえず、簡易に、簡易に。
こちらを使った、テストコード。
src/test/java/org/littlewings/web/StaticHttpServerTest.java
package org.littlewings.web; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URI; import java.nio.charset.StandardCharsets; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public class StaticHttpServerTest { private static StaticHttpServer httpServer; @BeforeClass public static void setUpClass() { httpServer = StaticHttpServer .create(10000, "/", "src/main/webapp") .start(); } @AfterClass public static void tearDownClass() { httpServer.shutdown(); } @Test public void testExistContents() throws IOException { HttpURLConnection conn = (HttpURLConnection) URI .create("http://localhost:10000/WEB-INF/web.xml") .toURL() .openConnection(); try (InputStreamReader isr = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8); BufferedReader reader = new BufferedReader(isr)) { assertThat(conn.getHeaderField("Content-Type")) .isEqualTo("text/html;charset=utf-8"); assertThat(reader.readLine()) .isEqualTo("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); } finally { conn.disconnect(); } } @Test(expected = FileNotFoundException.class) public void testNotFound() throws IOException { HttpURLConnection conn = (HttpURLConnection) URI .create("http://localhost:10000/WEB-INF/notFound.xml") .toURL() .openConnection(); try (InputStreamReader isr = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8); BufferedReader reader = new BufferedReader(isr)) { assertThat(reader.readLine()) .isEqualTo("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); } finally { conn.disconnect(); } } }
こんな感じで。
個人的に、ちょっと活用すると思います。