Maven WAR packaging
last modified November 13, 2026
In this article we build a web application with Maven. We package a
Jakarta servlet application into a WAR file, look at its layout, see why
the servlet API has the provided scope, and run the
application with the Jetty Maven plugin. The examples use Maven 3.10, WAR
plugin 3.5.1, Jetty 12.1, Servlet 6.1, and Java 25.
A WAR (web application archive) is a ZIP file with a layout
defined by the Jakarta Servlet specification. It is deployed to a servlet
container, such as Tomcat or Jetty, or to an application server. The
container provides the HTTP server and the servlet API; the WAR contains
the application code, its libraries, and static files. Projects with
war packaging use the Maven WAR plugin instead of the JAR
plugin in the package phase.
The project
Our application greets users. A form sends a name to a servlet, which answers with an HTML fragment.
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>greeting</artifactId>
<version>1.0.0</version>
<packaging>war</packaging>
<properties>
<maven.compiler.release>25</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.15.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.5.1</version>
</plugin>
<plugin>
<groupId>org.eclipse.jetty.ee11</groupId>
<artifactId>jetty-ee11-maven-plugin</artifactId>
<version>12.1.14</version>
<configuration>
<webApp>
<contextPath>/greeting</contextPath>
</webApp>
<httpConnector>
<port>8085</port>
</httpConnector>
</configuration>
</plugin>
</plugins>
</build>
</project>
The packaging element is set to war.
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.1.0</version>
<scope>provided</scope>
</dependency>
The servlet API is needed to compile the servlet. It has the
provided scope, because the container supplies it at runtime.
Commons Text, which escapes the name, is a normal dependency.
<groupId>org.eclipse.jetty.ee11</groupId> <artifactId>jetty-ee11-maven-plugin</artifactId> <version>12.1.14</version>
The Jetty plugin runs the application in an embedded Jetty server. Jetty
12 has a separate plugin for each Jakarta EE version; the
ee11 plugin supports Servlet 6.1. The application is served
under the /greeting context path on port 8085.
package com.example.greeting;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.text.StringEscapeUtils;
import java.io.IOException;
@WebServlet("/hello")
public class GreetingServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
var name = req.getParameter("name");
if (name == null || name.isBlank()) {
name = "guest";
}
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().printf("<p>Hello, %s!</p>%n",
StringEscapeUtils.escapeHtml4(name));
}
}
The @WebServlet annotation maps the servlet to the
/hello path; the container finds annotated classes when it
deploys the application. The servlet reads the name
parameter and escapes it with StringEscapeUtils.escapeHtml4,
so the user cannot inject HTML into the page.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Greeting</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<form action="hello">
<input name="name" placeholder="Your name">
<button>Greet</button>
</form>
</body>
</html>
Static files go into src/main/webapp, which becomes the root
of the application. The form submits the name to the servlet with a GET
request. The directory also contains css/style.css.
Building the WAR
We package the application.
The WAR plugin first assembles the application in the
target/greeting-1.0.0 directory, called an exploded WAR, and
then zips it. The content of src/main/webapp is at the root,
the compiled classes are in WEB-INF/classes, and the runtime
dependencies are in WEB-INF/lib, including Commons Lang,
which Commons Text depends on. The servlet API is not there. The container
never serves files from WEB-INF to clients.
$ mvn package
--- war:3.5.1:war (default-war) @ greeting ---
Packaging webapp
Assembling webapp [greeting] in
[/home/user/project/greeting/target/greeting-1.0.0]
Copying webapp resources [/home/user/project/greeting/src/main/webapp]
Building war: /home/user/project/greeting/target/greeting-1.0.0.war
BUILD SUCCESS
$ unzip -l target/greeting-1.0.0.war
Archive: target/greeting-1.0.0.war
Length Date Time Name
--------- ---------- ----- ----
0 1980-02-01 00:00 META-INF/
99 1980-02-01 00:00 META-INF/MANIFEST.MF
0 1980-02-01 00:00 WEB-INF/
0 1980-02-01 00:00 WEB-INF/classes/
0 1980-02-01 00:00 WEB-INF/classes/com/
0 1980-02-01 00:00 WEB-INF/classes/com/example/
0 1980-02-01 00:00 WEB-INF/classes/com/example/greeting/
0 1980-02-01 00:00 WEB-INF/lib/
0 1980-02-01 00:00 css/
1482 1980-02-01 00:00 WEB-INF/classes/com/example/greeting/GreetingServlet.class
713862 1980-02-01 00:00 WEB-INF/lib/commons-lang3-3.20.0.jar
264873 1980-02-01 00:00 WEB-INF/lib/commons-text-1.15.0.jar
47 1980-02-01 00:00 css/style.css
296 1980-02-01 00:00 index.html
1758 1980-02-01 00:00 META-INF/maven/com.example/greeting/pom.xml
54 1980-02-01 00:00 META-INF/maven/com.example/greeting/pom.properties
--------- -------
982471 16 files
$ ls target target/greeting-1.0.0
target:
classes
generated-sources
greeting-1.0.0
greeting-1.0.0.war
maven-archiver
maven-status
target/greeting-1.0.0:
css
index.html
META-INF
WEB-INF
The provided scope
To see the effect of the provided scope, we change the scope
of the servlet API to compile.
The API JAR now gets into WEB-INF/lib. In the best case, the
container ignores it; in the worst case, two copies of the servlet classes
of different versions clash, and the application fails with
ClassCastException or LinkageError errors. The
same applies to every API that the container implements, such as
Jakarta Persistence or Jakarta REST in application servers.
$ mvn -q clean package && unzip -l target/greeting-1.0.0.war | grep WEB-INF/lib/
0 1980-02-01 00:00 WEB-INF/lib/
713862 1980-02-01 00:00 WEB-INF/lib/commons-lang3-3.20.0.jar
264873 1980-02-01 00:00 WEB-INF/lib/commons-text-1.15.0.jar
398424 1980-02-01 00:00 WEB-INF/lib/jakarta.servlet-api-6.1.0.jar
Running with Jetty
The jetty:run goal compiles the project and starts Jetty
with the application. It runs until it is stopped with
Ctrl+C; we start it in one terminal and use
curl in another.
The run goal does not build a WAR. It serves the static
files directly from src/main/webapp and the classes from
target/classes, so changes to HTML and CSS files are visible
without a restart. With the scan parameter set to a number of
seconds, the plugin also redeploys the application when classes change.
The servlet answers under the context path, and the name is escaped.
$ mvn jetty:run
--- jetty:12.1.14:run (default-cli) @ greeting ---
Classes = /home/user/project/greeting/target/classes
Context path = /greeting
web.xml file = null
Webapp directory = /home/user/project/greeting/src/main/webapp
Started oejs.ServerConnector@473847fb{HTTP/1.1, (http/1.1)}{0.0.0.0:8085}
Started oejs.Server@1ab53860{STARTING}[12.1.14,sto=0] @457ms
$ curl -s localhost:8085/greeting/ | head -4
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
$ curl -s 'localhost:8085/greeting/hello?name=Lucy'
<p>Hello, Lucy!</p>
$ curl -s 'localhost:8085/greeting/hello?name=<b>Lucy</b>'
<p>Hello, <b>Lucy</b>!</p>
The deployment descriptor
Older applications configure servlets in
WEB-INF/web.xml, the deployment descriptor. Since Servlet 3.0
it is optional, and since WAR plugin 3.0 a missing web.xml
no longer fails the build. It is still the place for settings that have no
annotation, such as error pages.
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_1.xsd"
version="6.1">
<display-name>Greeting</display-name>
<error-page>
<error-code>404</error-code>
<location>/not-found.html</location>
</error-page>
</web-app>
The descriptor declares Servlet 6.1 and shows the
not-found.html page for 404 errors. The page is a static
file in src/main/webapp. Servlets annotated with
@WebServlet keep working, because the descriptor does not set
metadata-complete.
This time we start the server with the jetty:run-war goal,
which runs the build up to the package phase and deploys the
packaged WAR, just like a standalone container does.
A request for a missing page returns the 404 status with our error page.
To deploy the same file to a standalone server, it is enough to copy
target/greeting-1.0.0.war to its deployment directory, for
example webapps in Tomcat 11; the context path is then
derived from the file name.
$ mvn jetty:run-war
Building war: /home/user/project/greeting/target/greeting-1.0.0.war
--- jetty:12.1.14:run-war (default-cli) @ greeting ---
War = /home/user/project/greeting/target/greeting-1.0.0.war
Started oejs.ServerConnector@7b13ff02{HTTP/1.1, (http/1.1)}{0.0.0.0:8085}
Started oejs.Server@6abe62bb{STARTING}[12.1.14,sto=0] @608ms
$ curl -si localhost:8085/greeting/missing
HTTP/1.1 404 Not Found
<body><p>The page does not exist.</p></body>
$ curl -s 'localhost:8085/greeting/hello?name=Lucy'
<p>Hello, Lucy!</p>
Useful options
| Parameter | Description |
|---|---|
warSourceDirectory | web application sources, src/main/webapp by default |
webResources | additional resources, optionally filtered |
packagingExcludes | files left out of the WAR |
failOnMissingWebXml | require web.xml, false by default |
warName | name of the WAR file |
attachClasses | also create a JAR with the classes |
overlays | merge other WARs into this one |
The finalName element in the build section sets
the base name of the WAR, for example greeting instead of
greeting-1.0.0, which gives a version-independent context
path in a standalone container.
Source
In this article we have built a web application into a WAR file with Maven and run it with the Jetty Maven plugin.
Author
List all Java tutorials.