Jettyベースで作ったWebアプリケーションを１個のJARにまとめて配布する

# 目的

Java で動作するWebアプリケーションをJARファイル１つにまとめて配布することが目的です。簡単なのかと思いきや意外にコツが必要でした。

# Maven

## ディレクトリ構造

Mavenプロジェクトのディレクトリ構造は以下のようになっているものとします。標準的な構造かと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110265/a14384ca-238a-e7ea-119d-03d95c2631d1.png)

# Main クラス

```java
package nlp4j.servlet;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.webapp.WebAppContext;

public class HelloJettyAppMain {
	public static void main(String[] args) throws Exception {
		// Jettyサーバーをポート8080で作成
		Server server = new Server(8080);
		// WebAppContextを設定
		WebAppContext webapp = new WebAppContext();
		webapp.setContextPath("/hello-java-jettyapp");
		webapp.setResourceBase(HelloJettyAppMain.class.getClassLoader().getResource("webapp").toExternalForm());
		webapp.setWelcomeFiles(new String[] { "index.html", "index.jsp" });
		// デフォルトサーブレットを設定（静的ファイル用）
		ServletHolder defaultServlet = new ServletHolder("default", DefaultServlet.class);
		defaultServlet.setInitParameter("resourceBase", webapp.getResourceBase());
		defaultServlet.setInitParameter("dirAllowed", "true");
		webapp.addServlet(defaultServlet, "/");
		// HelloServletを追加
		webapp.addServlet(new ServletHolder(new HelloServlet1()), "/helloservlet1");
		webapp.addServlet(new ServletHolder(new HelloServlet2EventStream()), "/helloservlet2");
		webapp.addServlet(new ServletHolder(new HelloServlet3()), "/helloservlet3");
		// サーバーにWebAppContextを設定
		server.setHandler(webapp);
		System.out.println("http://localhost:8080/hello-java-jettyapp/index.html");
		// サーバーを起動
		server.start();
		server.join();
	}
}

```

# Servletクラス

サーブレットは通常のWebアプリケーションと変わらずお好みのものを作ればよいのですが、以下をサンプルとします。このサンプルではプロパティファイルを読み込んでいます。

```java
package nlp4j.servlet;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.Properties;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/helloservlet1")
public class HelloServlet1 extends HttpServlet {
	private static final long serialVersionUID = 1L;
	public HelloServlet1() {
		super();
	}
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		{ // Read resource properties file ...
			Properties prop = new Properties();
			try (InputStream inputStream = HelloServlet1.class.getResourceAsStream("/hello-java-jettyapp.properties")) {
				prop.load(inputStream);
				System.out.println("hello " + prop.getProperty("testkey", "xxx"));
			} catch (IOException e) {
				e.printStackTrace(System.err);
			}
		} // ... Read resource properties file
		response.getWriter().append("Served at: ").append(request.getContextPath()).append(" " + new Date());
	}
	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		doGet(request, response);
	}
}
```

以下ではEventStreamをサーブレットで実装しています。

```java
package nlp4j.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/helloservlet2")
public class HelloServlet2EventStream extends HttpServlet {
	private static final long serialVersionUID = 1L;
	public HelloServlet2EventStream() {
		super();
	}
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		response.setContentType("text/event-stream");
		response.setCharacterEncoding("UTF-8");
		PrintWriter writer = response.getWriter();
		for (int i = 0; i < 10; i++) {
			writer.write("data: " + "Message " + i + "\n\n");
			writer.flush();
			try {
				Thread.sleep(1000); // 1秒ごとにメッセージを送信
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		writer.close();
	}
	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		doGet(request, response);
	}
}
```

以下ではindex.htmlをincludeしています。

```java
package nlp4j.servlet;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/helloservlet3")
public class HelloServlet3 extends HttpServlet {
	private static final long serialVersionUID = 1L;
	public HelloServlet3() {
		super();
	}
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		// HTMLファイルのパスを指定
		String htmlFilePath = "/index3.html";
		// リクエストディスパッチャーを使用してHTMLファイルをインクルード
		RequestDispatcher dispatcher = request.getRequestDispatcher(htmlFilePath);
		dispatcher.include(request, response);
	}
	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		doGet(request, response);
	}
}
```

# pom.xml

pom.xmlは以下のようになります。これが意外にハマります。

「<packaging>war</packaging>」にしておくとWARファイルがビルドされます。

JARファイルとしてビルドしたいので

「<packaging>jar</packaging>」

とする必要があります。


```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.nlp4j</groupId>
	<artifactId>hello-java-webapp</artifactId>
	<version>1.0.0.0</version>
	<!-- jar にしておくとJARが出力される -->
	<packaging>jar</packaging>
	<name>hello-java-jettyapp</name>
	<properties>
		<java.version>1.8</java.version>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<maven.compiler.source>1.8</maven.compiler.source>
		<maven.compiler.target>1.8</maven.compiler.target>
	</properties>
	<build>
		<finalName>hello-java-jettyapp</finalName>
		<resources>
			<!-- デフォルトの resource 設定 -->
			<resource>
				<directory>${project.basedir}/src/main/resources</directory>
			</resource>
			<resource>
				<directory>${project.basedir}/src/main/webapp</directory>
				<includes>
					<include>**/*</include>
				</includes>
				<targetPath>webapp</targetPath>
			</resource>
			<!-- system scope の jar ファイルをリソースとして含める 設定 -->
			<resource>
				<directory>${project.basedir}</directory>
				<includes>
					<include>lib/*.jar</include>
				</includes>
			</resource>
		</resources>
		<plugins>
			<!-- Compiler plugin to specify Java version -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.9.0</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>
			<!-- Jar plugin to create an executable jar -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-jar-plugin</artifactId>
				<version>3.4.2</version>
				<configuration>
					<archive>
						<manifest>
							<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
							<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
							<mainClass>nlp4j.servlet.HelloJettyAppMain</mainClass>
						</manifest>
					</archive>
					<includes>
						<include>*/**</include>
					</includes>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-resources-plugin</artifactId>
				<version>3.2.0</version>
				<executions>
					<execution>
						<phase>process-resources</phase>
						<goals>
							<goal>resources</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
			<!-- Shade plugin to package dependencies into the jar -->
			<!-- Maven Shade Plugin で system スコープの jar ファイルもひとつにまとめる -->
			<!-- https://qiita.com/niwasawa/items/024de1ff01ef3df541cc -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-shade-plugin</artifactId>
				<version>3.2.1</version>
				<configuration>
					<!-- dependency-reduced-pom.xml を生成しない設定 -->
					<createDependencyReducedPom>false</createDependencyReducedPom>
				</configuration>
				<executions>
					<execution>
						<phase>package</phase>
						<goals>
							<goal>shade</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
			<!-- WAR -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-war-plugin</artifactId>
				<version>3.3.2</version>
				<configuration>
					<warName>hello-java-jettyapp</warName>
				</configuration>
			</plugin>

			<plugin>
				<groupId>org.eclipse.jetty</groupId>
				<artifactId>jetty-maven-plugin</artifactId>
				<version>9.4.44.v20210927</version>
				<configuration>
					<httpConnector>
						<port>8080</port>
					</httpConnector>
				</configuration>
			</plugin>
		</plugins>
	</build>
	<!-- dependencies -->
	<dependencies>
		<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>javax.servlet-api</artifactId>
			<version>3.1.0</version>
			<!-- scope>provided scope -->
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-webapp -->
		<dependency>
			<groupId>org.eclipse.jetty</groupId>
			<artifactId>jetty-server</artifactId>
			<version>9.4.51.v20230217</version>
		</dependency>
		<dependency>
			<groupId>org.eclipse.jetty</groupId>
			<artifactId>jetty-annotations</artifactId>
			<version>9.4.51.v20230217</version>
		</dependency>
		<dependency>
			<groupId>org.eclipse.jetty</groupId>
			<artifactId>jetty-webapp</artifactId>
			<version>9.4.51.v20230217</version>
		</dependency>


		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.11</version>
			<scope>test</scope>
		</dependency>
	</dependencies>
</project>

```

# mvn package

以下のような感じでビルドできます。

```
>mvn package
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------< org.nlp4j:hello-java-webapp >---------------------
[INFO] Building hello-java-jettyapp 1.0.0.0
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- resources:3.2.0:resources (default-resources) @ hello-java-webapp ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] Copying 1 resource
[INFO] Copying 5 resources to webapp
[INFO] Copying 0 resource
[INFO]
[INFO] --- resources:3.2.0:resources (default) @ hello-java-webapp ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] Copying 1 resource
[INFO] Copying 5 resources to webapp
[INFO] Copying 0 resource
[INFO]
[INFO] --- compiler:3.9.0:compile (default-compile) @ hello-java-webapp ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 4 source files to C:\xxx\projects\hello-java-jettyapp\target\classes
[INFO]
[INFO] --- resources:3.2.0:testResources (default-testResources) @ hello-java-webapp ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] Copying 0 resource
[INFO]
[INFO] --- compiler:3.9.0:testCompile (default-testCompile) @ hello-java-webapp ---
[INFO] Changes detected - recompiling the module!
[INFO]
[INFO] --- surefire:3.0.0-M8:test (default-test) @ hello-java-webapp ---
[INFO]
[INFO] --- jar:3.4.2:jar (default-jar) @ hello-java-webapp ---
[INFO] Building jar: C:\xxx\projects\hello-java-jettyapp\target\hello-java-jettyapp.jar
[INFO]
[INFO] --- shade:3.2.1:shade (default) @ hello-java-webapp ---
[INFO] Including javax.servlet:javax.servlet-api:jar:3.1.0 in the shaded jar.
[INFO] Including org.eclipse.jetty:jetty-server:jar:9.4.51.v20230217 in the shaded jar.
[INFO] Including org.eclipse.jetty:jetty-http:jar:9.4.51.v20230217 in the shaded jar.
[INFO] Including org.eclipse.jetty:jetty-util:jar:9.4.51.v20230217 in the shaded jar.
[INFO] Including org.eclipse.jetty:jetty-io:jar:9.4.51.v20230217 in the shaded jar.
[INFO] Including org.eclipse.jetty:jetty-annotations:jar:9.4.51.v20230217 in the shaded jar.
[INFO] Including org.eclipse.jetty:jetty-plus:jar:9.4.51.v20230217 in the shaded jar.
[INFO] Including org.eclipse.jetty:jetty-jndi:jar:9.4.51.v20230217 in the shaded jar.
[INFO] Including javax.annotation:javax.annotation-api:jar:1.3.2 in the shaded jar.
[INFO] Including org.ow2.asm:asm:jar:9.4 in the shaded jar.
[INFO] Including org.ow2.asm:asm-commons:jar:9.4 in the shaded jar.
[INFO] Including org.ow2.asm:asm-tree:jar:9.4 in the shaded jar.
[INFO] Including org.eclipse.jetty:jetty-webapp:jar:9.4.51.v20230217 in the shaded jar.
[INFO] Including org.eclipse.jetty:jetty-xml:jar:9.4.51.v20230217 in the shaded jar.
[INFO] Including org.eclipse.jetty:jetty-servlet:jar:9.4.51.v20230217 in the shaded jar.
[INFO] Including org.eclipse.jetty:jetty-security:jar:9.4.51.v20230217 in the shaded jar.
[INFO] Including org.eclipse.jetty:jetty-util-ajax:jar:9.4.51.v20230217 in the shaded jar.
[WARNING] Discovered module-info.class. Shading will break its strong encapsulation.
[WARNING] Discovered module-info.class. Shading will break its strong encapsulation.
[WARNING] Discovered module-info.class. Shading will break its strong encapsulation.
[INFO] Replacing original artifact with shaded artifact.
[INFO] Replacing C:\xxx\projects\hello-java-jettyapp\target\hello-java-jettyapp.jar with C:\xxx\projects\hello-java-jettyapp\target\hello-java-webapp-1.0.0.0-shaded.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  7.150 s
[INFO] Finished at: 2024-07-07T15:00:24+09:00
[INFO] ------------------------------------------------------------------------
```

# 結果

```
t>java -jar hello-java-jettyapp.jar
2024-07-07 15:00:53.522:INFO::main: Logging initialized @100ms to org.eclipse.jetty.util.log.StdErrLog
jar:file:/C:/xxx/projects/hello-java-jettyapp/target/hello-java-jettyapp.jar!/webapp
http://localhost:8080/hello-java-jettyapp/index.html
2024-07-07 15:00:53.575:INFO:oejs.Server:main: jetty-9.4.51.v20230217; built: 2023-02-17T08:19:37.309Z; git: b45c405e4544384de066f814ed42ae3dceacdd49; jvm 17.0.8+9-LTS-211
2024-07-07 15:00:53.713:WARN:oejw.StandardDescriptorProcessor:main: Duplicate mapping from / to default
2024-07-07 15:00:53.715:INFO:oejw.StandardDescriptorProcessor:main: NO JSP Support for /hello-java-jettyapp, did not find org.eclipse.jetty.jsp.JettyJspServlet
2024-07-07 15:00:53.720:INFO:oejs.session:main: DefaultSessionIdManager workerName=node0
2024-07-07 15:00:53.720:INFO:oejs.session:main: No SessionScavenger set, using defaults
2024-07-07 15:00:53.721:INFO:oejs.session:main: node0 Scavenging every 660000ms
2024-07-07 15:00:53.738:INFO:oejsh.ContextHandler:main: Started o.e.j.w.WebAppContext@327b636c{/hello-java-jettyapp,jar:file:/C:/xxx/projects/hello-java-jettyapp/target/hello-java-jettyapp.jar!/webapp,AVAILABLE}
2024-07-07 15:00:53.779:INFO:oejs.AbstractConnector:main: Started ServerConnector@3419866c{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}
2024-07-07 15:00:53.780:INFO:oejs.Server:main: Started @364ms
```

http://localhost:8080/hello-java-jettyapp/index.html
にアクセスすると以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110265/98f47b4c-0f49-1c69-21d4-696e9733985b.png)

http://localhost:8080/hello-java-jettyapp/img/
にアクセスすると、ファイル一覧も生成してくれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110265/544a5af4-9011-5e45-5d7f-14ba7fa6c10a.png)


---

以上.