A simple web servlet in Jersey
Russell Bateman |
Servlets are so devilishly nightmarish to get right in IntelliJ IDEA (I found them pretty challenging in Eclipse too). I hope that this one will just work and can be used as a starting point. It almost did, but not quite—why having to play around to get working induced me to lay it out (for you now and for me next time).
This is not a tutorial to explain the theory behind writing servlets, JAX-RS (Jersey) servlets or ReST servlets. Its purpose is to give you what you need to reach the first story of the building of writing servlets. So many tutorials on line advertise getting you up and running, but, in the end, do not succeed. I know because, over the years, I have probably tried all of them and I end up lighting my hair on fire because of HTTP Status 404. I hope instead that this one will work as advertised.
The tool stack we'll use is illustrated by the colorful icons above:
I always set up a project (if, from scratch) this way. Where you put yours is up to you. I put my own, non-commercial ones under ~/dev.
~/dev/melissa-data $ mkdir -p src/main/java/com/windofkeltia/servlet ~/dev/melissa-data $ mkdir -p src/main/resources ~/dev/melissa-data $ mkdir -p src/main/webapp/WEB-INF
russ@gondolin ~/dev/melissa-data $ mvn --version Apache Maven 3.3.9 Maven home: /usr/share/maven Java version: 11.0.2, vendor: Oracle Corporation Java home: /home/russ/dev/jdk-11.0.2 Default locale: en_US, platform encoding: UTF-8 OS name: "linux", version: "4.13.0-36-generic", arch: "amd64", family: "unix"
Here's pom.xml. In green, the dependencies that makes this Jersey-based. Please note that you cannot implement this project to run under higher versions of Java language level (see setting for the maven-compiler-plugin below) simply by bumping to, say, Java 11. You will get a status code in your browser at start-up of 500.
<?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>com.windofkeltia.melissa-data</groupId> <artifactId>melissa-data</artifactId> <version>1.0.0</version> <packaging>war</packaging> <!-- To build the WAR file, do mvn clean package. --> <properties> <wokprefix>windofkeltia</wokprefix> <wokversion>0</wokversion> <wokrelease>0</wokrelease> <tomcat.version>9.0.7</tomcat.version> <jersey2.version>2.27</jersey2.version> <jersey-bundle.version>1.19.1</jersey-bundle.version> <jaxrs.version>2.0.1</jaxrs.version> <servlet-api.version>4.0.0</servlet-api.version> <slf4j.version>1.7.7</slf4j.version> <log4j.version>1.2.17</log4j.version> <junit.version>4.12</junit.version> <maven-compiler-plugin.version>3.2</maven-compiler-plugin.version> <maven-war-plugin.version>3.2.0</maven-war-plugin.version> <maven-surefire-plugin.version>2.21.0</maven-surefire-plugin.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>com.sun.jersey</groupId> <artifactId>jersey-bundle</artifactId> <version>${jersey-bundle.version}</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-servlet-api</artifactId> <version>${tomcat.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>${servlet-api.version}</version> </dependency> <!-- Various other dependencies --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>${slf4j.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>${log4j.version}</version> </dependency> </dependencies> <build> <finalName>melissa-data</finalName> <pluginManagement> </pluginManagement> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> <version>${maven-compiler-plugin.version}</version> </plugin> <!-- what builds a WAR file --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>${maven-war-plugin.version}</version> <configuration> <webResources> <resource> <!-- this is relative to the same directory as pom.xml --> <directory>src/main/webapp</directory> </resource> </webResources> <!-- Magic for maintaining a build timestamp in MANIFEST.MF: --> <archive> <manifest> <addDefaultImplementationEntries>true</addDefaultImplementationEntries> <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries> </manifest> <manifestEntries> <Build-Time>${maven.build.timestamp}</Build-Time> </manifestEntries> </archive> </configuration> </plugin> <!-- how to echo out properties and their definitions --> <plugin> <artifactId>maven-antrun-plugin</artifactId> <version>1.8</version> <executions> <execution> <phase>compile</phase> <configuration> <target> <!-- <echoproperties /> --> </target> </configuration> <goals> <goal>run</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>${maven-surefire-plugin.version}</version> <configuration> <systemPropertyVariables> <catalina.home>${project.build.directory}</catalina.home> <buildDirectory>${project.build.directory}</buildDirectory> </systemPropertyVariables> </configuration> </plugin> </plugins> </build> </project>
<?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_4_0.xsd" version="4.0"> <display-name>melissa-data</display-name> <servlet> <servlet-name>melissa-data</servlet-name> <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class> <init-param> <param-name>com.sun.jersey.config.property.packages</param-name> <param-value>com.windofkeltia.servlet</param-value> </init-param> <init-param> <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name> <param-value>true</param-value> </init-param> </servlet> <listener> <listener-class>com.windofkeltia.servlet.ServletInitializer</listener-class> </listener> <servlet-mapping> <servlet-name>melissa-data</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app>
org.apache.catalina.core.ContainerBase.[Catalina].level=INFO org.apache.catalina.core.ContainerBase.[Catalina].handlers=java.util.logging.ConsoleHandler
package com.windofkeltia.servlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @Path( "" ) public class MelissaDataServlet extends HttpServlet { private String message; private static final int STATUS_500 = Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(); @GET @Produces( MediaType.TEXT_PLAIN ) public String getStatusInPlainText( @Context HttpServletRequest request ) { return MelissaDataStatus.getStatusInPlainText( MelissaDataStatus.readManifest( request.getServletContext() ) ); } @POST @Consumes( { MediaType.TEXT_XML, MediaType.APPLICATION_XML } ) @Produces( { MediaType.TEXT_XML, MediaType.APPLICATION_XML } ) public Response postRequest( @Context HttpServletRequest request, @Context HttpHeaders header ) { return null; // ServiceImpl.post( request, header ); TODO: implement this after GET works! } }
package com.windofkeltia.servlet; import javax.servlet.ServletContext; import java.io.IOException; import java.util.Calendar; import java.util.jar.Attributes; import java.util.jar.Manifest; import static java.util.Objects.nonNull; public class MelissaDataStatus { private static final String COPYRIGHT = "Copyright "; private static final String COPY_PLAIN = "(c) "; private static final String COMPANY = "by Wind of Keltia, LLC."; private static final String RIGHTS = "Proprietary and confidential. All rights reserved."; private static final String PURPOSE = "Delivers Melissa Data from its server to HTTP clients."; protected static String getStatusInPlainText( final String MANIFEST ) { StringBuilder sb = new StringBuilder(); sb.append( "The Melissa Data service is up.\n\n" ); sb.append( MANIFEST ).append( '\n' ); sb.append( PURPOSE ).append( "\n\n" ); sb.append( COPYRIGHT ).append( COPY_PLAIN ).append( getYearRange() ).append( ' ' ).append( COMPANY ).append( '\n' ); sb.append( RIGHTS ).append( '\n' ); return sb.toString(); } private static String getYearRange() { final String FIRST_YEAR = "2018"; // just pretending I've been at this for a couple of years int current = Calendar.getInstance().get( Calendar.YEAR ); String YEAR_RANGE = FIRST_YEAR; if( current > Integer.parseInt( FIRST_YEAR ) ) YEAR_RANGE += "-" + current; return YEAR_RANGE; } protected static String readManifest( ServletContext servletContext ) { StringBuilder sb = new StringBuilder(); boolean htmlBreaks = false; try { Manifest manifest = new Manifest( servletContext.getResourceAsStream( "/META-INF/MANIFEST.MF" ) ); Attributes attributes = manifest.getMainAttributes(); String line; if( isNotEmpty( line = attributes.getValue( "Manifest-Version" ) ) ) sb.append( " Manifest-Version: " ).append( line ).append( '\n' ); if( isNotEmpty( line = attributes.getValue( "Implementation-Title" ) ) ) sb.append( " Implementation-Title: " ).append( line ).append( '\n' ); if( isNotEmpty( line = attributes.getValue( "Implementation-Version" ) ) ) sb.append( " Implementation-Version: " ).append( line ).append( '\n' ); // this is something like "Built-By: russ" or buildbot, etc. // if( !isEmpty( line = attributes.getValue( "Built-By" ) ) ) // sb.append( " Built-By: " ).append( line ).append( '\n' ); if( isNotEmpty( line = attributes.getValue( "Specification-Title" ) ) ) sb.append( " Specification-Title: " ).append( line ).append( '\n' ); if( isNotEmpty( line = attributes.getValue( "Implementation-Vendor-Id" ) ) ) sb.append( "Implementation-Vendor-Id: " ).append( line ).append( '\n' ); if( isNotEmpty( line = attributes.getValue( "Build-Time" ) ) ) sb.append( " Build-Time: " ).append( line ).append( '\n' ); if( isNotEmpty( line = attributes.getValue( "Created-By" ) ) ) sb.append( " Created-By: " ).append( line ).append( '\n' ); if( isNotEmpty( line = attributes.getValue( "Build-Jdk" ) ) ) sb.append( " Build-JDK: " ).append( line ).append( '\n' ); if( isNotEmpty( line = attributes.getValue( "Specification-Version" ) ) ) sb.append( " Specification-Version: " ).append( line ).append( '\n' ); return sb.toString(); } catch( IOException ex ) { return "Application build timestamp unavailable"; } } private static boolean isNotEmpty( final String string ) { return( nonNull( string ) && string.length() > 0 ); } }
package com.windofkeltia.servlet; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import org.apache.log4j.Logger; /** * Initialize when the application starts. The way you get this code called * is by adding this to web/WEB-INF/web.xml: * * <pre> * <listener> * <listener-class> * com.windofkeltia.servlet.ServletInitializer * </listener-class> * </listener> * </pre> * @author Russell Bateman */ public class ServletInitializer implements ServletContextListener { private static final Logger log = Logger.getLogger( ServletInitializer.class ); /** * This method is called when the servlet context is initialized (when the * web application is deployed). You can initialize servlet context-related * data here. We use this listener to ensure that we're all up and running * before any of the services get registered. */ @Override public void contextInitialized( ServletContextEvent event ) { ServletContext context = event.getServletContext(); MelissaDataServlet instance = new MelissaDataServlet(); final String COPYRIGHT_AND_STATUS = MelissaDataStatus.readManifest( context ); log.info( "#############################################################" ); log.info( "Initializing melissa-data application context..." ); log.info( MelissaDataStatus.getStatusInPlainText( COPYRIGHT_AND_STATUS ) ); } /** * This method is invoked when the Servlet Context (the web application) is * undeployed or the (Tomcat) server shuts down. */ @Override public void contextDestroyed( ServletContextEvent event ) { log.info( "Discarded melissa-data application context!" ); } }
In the Project pane, right-click on the project name, melissa-data, and choose Add Framework Support..., then click JAX RESTful Web Services. As far as I can tell, this only gets you com.sun.jersey:jersey-bundle-x.y.z.jar in pom.xml and we've already put that there, but this is how it's supposed to be done.
In short, make them look like this. I'm using port 6060 because everybody uses port 8080. The port you pick for JMX might conflict too.
/home/russ/dev/apache-tomcat-9.0.7/bin/catalina.sh run NOTE: Picked up JDK_JAVA_OPTIONS: --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED [2020-08-12 12:31:56,066] Artifact melissa-data:war exploded: Waiting for server connection to start artifact deployment... 12-Aug-2020 12:31:56.752 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version: Apache Tomcat/9.0.7 12-Aug-2020 12:31:56.754 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server built: Apr 3 2018 19:53:05 UTC 12-Aug-2020 12:31:56.754 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server number: 9.0.7.0 12-Aug-2020 12:31:56.754 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Name: Linux 12-Aug-2020 12:31:56.755 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Version: 4.13.0-36-generic 12-Aug-2020 12:31:56.755 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Architecture: amd64 12-Aug-2020 12:31:56.755 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Java Home: /home/russ/dev/jdk-11.0.2 12-Aug-2020 12:31:56.755 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Version: 11.0.2+9 ... 12-Aug-2020 12:31:56.950 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 129 ms Connected to server [2020-08-12 12:31:57,200] Artifact melissa-data:war exploded: Artifact is being deployed, please wait... log4j:WARN No appenders could be found for logger (com.windofkeltia.servlet.ServletInitializer). log4j:WARN Please initialize the log4j system properly. log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info. [2020-08-12 12:31:57,733] Artifact melissa-data:war exploded: Artifact is deployed successfully [2020-08-12 12:31:57,733] Artifact melissa-data:war exploded: Deploy took 533 milliseconds 12-Aug-2020 12:32:06.921 INFO [ContainerBackgroundProcessor[StandardEngine[Catalina]]] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/home/russ/dev/apache-tomcat-9.0.7/webapps/manager] 12-Aug-2020 12:32:06.951 INFO [ContainerBackgroundProcessor[StandardEngine[Catalina]]] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/home/russ/dev/apache-tomcat-9.0.7/webapps/manager] has finished in [31] ms
If this goes belly-up before Tomcat even runs your application, you can debug that. Follow the discussion here: Troubleshooting start-up/launch errors to figure out where IntelliJ IDEA puts Tomcat's stuff.
The Melissa Data service is up. Manifest-Version: 1.0 Implementation-Version: 1.0.0 Implementation-Vendor-Id: com.windofkeltia.melissa-data Build-Time: 2020-08-12T18:29:25Z Created-By: IntelliJ IDEA Build-JDK: 11.0.2 Specification-Version: 1.0.0 Delivers Melissa Data from its server to HTTP clients. Copyright (c) 2018-2020 by Wind of Keltia, LLC. Proprietary and confidential. All rights reserved.
This </> RESTED Client (or Insomnia—it's insanely great) is what you'll use if you do serious work on a servlet. A browser will easily display anything your servlet does for HTTP GET, however, this is not so for HTTP POST and other verbs.
Of course, I'm leaving out IDEA-created files, read-mes, tests, etc. to make this as simple yet informative as possible.
~/dev $ tree melissa-data melissa-data ├── pom.xml └── src └── main ├── java │ └── com │ └── windofkeltia │ └── servlet │ ├── MelissaDataServlet.java │ ├── MelissaDataStatus.java │ └── ServletInitializer.java ├── resources │ └── logging.properties └── webapp └── WEB-INF └── web.xml
$ mvn clean package
.
.
.
[ERROR] The goal you specified requires a project to execute but there is no POM in this directory \
(/home/russ/dev/melissa-data/src/main/java/com/windofkeltia/servlet). Please verify you invoked \
Maven from the correct directory. -> [Help 1]
Did you forget that you must be in the project root to invoke Maven for building the server? Maybe you were down below the project root subdirectory messing about somewhere.
$ mvn clean package
.
.
.
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-war-plugin:3.2.0:war (default-war) on project \
melissa-data: Execution default-war of goal org.apache.maven.plugins:maven-war-plugin:3.2.0:war failed: \
basedir /home/russ/dev/melissa-data/web does not exist -> [Help 1]
The problem is in pom.xml with maven-war-plugin's web-resources configuration. The default usually created is <directory>web</directory> as if you wanted to put web.xml on the path ${PROJECT-ROOT}/web. You need the following path instead if you created web.xml on the path src/main/webapp/WEB-INF/web.xml:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>${maven-war-plugin.version}</version> <configuration> <webResources> <resource> <!-- this is web.xml's parent subdirectory relative to pom.xml (project root): --> <directory>src/main/webapp</directory> </resource> </webResources> . . .
In the end, it doesn't matter where you put web.xml and IntelliJ IDEA as well as Eclipse web-application projects have waffled over the years as to a preferred location. Whatever the case, Maven's WAR-building plug-in must be able to find it.
You want to keep something to distribute with your servlet, let's say some documentation (or something else, but documentation will be our example). In this case, perform a GET on my servlet thus:
GET http://hostname:port/servlet-name/documentation/doc-name
The important tidbits you probably don't know how to do (as you're reading this at all) are highlighted here. Because the documentation itself is kept in HTML/CSS, doing this from a browser yields the documentation page (in the browser—duh).
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; @Path( "/documentation" ) public class Documentation { private static final String BASIC_PATHNAME = "/WEB-INF/classes/documents/doc-name.html"; @Path( "/doc-name" ) @GET @Produces( MediaType.TEXT_HTML ) public String getIxmlBasicDocumentationInHtml( @Context HttpServletRequest request ) throws IOException { // open file on path documents/ixml.html and spew out... ServletContext context = request.getServletContext(); InputStream inputStream = context.getResourceAsStream( BASIC_PATHNAME ); return readFromStream( inputStream ); } private static String readFromStream( InputStream inputStream ) throws IOException { List< String > lines = readLines( inputStream ); StringBuilder contents = new StringBuilder(); for( String line : lines ) contents.append( line ).append( '\n' ); return contents.toString(); } private static List< String > readLines( InputStream inputStream ) throws IOException { List< String > lines = new ArrayList<>(); try( BufferedReader br = new BufferedReader( new InputStreamReader( inputStream ) ) ) { for( String line = br.readLine(); line != null; line = br.readLine() ) lines.add( line ); } return lines; } }
Let's use the Melissa Data service as an example. Let's say that, in preparation for your splash page being in HTML and appearing in the browser, you decided to link to some documentation on how to use your service. You are maintaining this documentation in your IDE on the path, src/main/resources/doc/howto.html. It will be deployed as part of your service without you having to do anything special. Users will be able to reach it directly via http://localhost:8080/melissa-data/doc/howto.html.
Let's say your splash page is in HTML (instead of, as in our example much higher above, plain text). Here it is, but we'll put a link inside for your user to click relatively from the splash page without you needing to figure out what the domain and port are. Here's how:
<html> <head> <title> Melissa Data </title> </head> <body> <p> The Melissa Data service is up. </p> <pre> Manifest-Version: 1.0 Implementation-Version: 1.0.0 Implementation-Vendor-Id: com.windofkeltia.melissa-data Build-Time: 2020-08-12T18:29:25Z Created-By: IntelliJ IDEA Build-JDK: 11.0.2 Specification-Version: 1.0.0 </pre> <p> Delivers Melissa Data from its server to HTTP clients. </p> <p> <a href="/melissa-data/doc/howto">Click here for documentation.</a> </p> <p> Copyright (c) 2018-2020 by Wind of Keltia, LLC.<br /> Proprietary and confidential. All rights reserved. </p> </body> </html>
Once running, it will look like this:
The Melissa Data service is up.
Manifest-Version: 1.0 Implementation-Version: 1.0.0 Implementation-Vendor-Id: com.windofkeltia.melissa-data Build-Time: 2020-08-12T18:29:25Z Created-By: IntelliJ IDEA Build-JDK: 11.0.2 Specification-Version: 1.0.0
Delivers Melissa Data from its server to HTTP clients.
Click here for documentation.
Copyright (c) 2018-2020 by Wind of Keltia, LLC.
Proprietary and confidential. All rights reserved.
The utility of this note is that it gives you a full example of usage and spares you having to google around to figure out how to do a relative URL in HTML from the page of your content (the page your user is on).