Saturday, July 3, 2021

Creating pure JAX-RS web applications

How simple can a JAX-RS application be?

It is extremely simple to create a pure JAX-RS application. What's difficult is finding examples of how to do it.

There are no end of examples showing you how to create an application starting with a Maven archetype or an IDE and a JAX-RS implementation. But what you'll end up with is an enormous amount of cruft in your project directory tree and in your war file. You'll probably also end up with non-standard code using the 'helpful' extensions added to whatever implementation the tutorial author used, because they based their tutorial on one written by the JAX-RS implementation authors, who want to showcase their extensions to the standard.

This is not that. This tutorial shows how small a JAX-RS application can be, and how you don't need to download the universe to build and package it.

So what do you need?

  • A JDK. Java8+.
  • A text editor.
  • The JAX-RS api jar.
  • curl for downloading software and testing.

Assumptions

It is assumed you have a JDK installed and in your path and that JAVA_HOME points to it, and you have curl downloaded and in your path. You'd be a masochist to not use an IDE. But you don't need that for packaging the application.

Other than editing the source code this will be demonstrated entirely with the Windows 10 command shell. It would be easier with bash.

Project directory layout

This is very simple. A src directory for source files, a lib directory for the jax-rs-api jar, and war directory to build the war file from.


C:\Users\dajta\src\testwebapp>mkdir src\net\gweeps\webapp\model
C:\Users\dajta\src\testwebapp>mkdir src\net\gweeps\webapp\resources
C:\Users\dajta\src\testwebapp>mkdir lib
C:\Users\dajta\src\testwebapp>mkdir war\WEB-INF\classes

Download the JAX-RS api jar. I googled "jax-rs 2.1.1 api jar" to find it.

C:\Users\dajta\src\testwebapp>curl -s https://repo1.maven.org/maven2/javax/ws/rs/javax.ws.rs-api/2.1.1/javax.ws.rs-api-2.1.1.jar --output lib\javax.ws.rs-api-2.1.1.jar

Now the source files. There is an Application subclass to signal the war file contains a JAX-RS webapp, a test resource that provides a URI for requests, and a POJO representing some type of object in the application's data model.

src\net\gweeps\webapp\TestApplication.java


package net.gweeps.webapp;

// Nothing here about a JAX-RS implementation - this is pure JAX-RS API.
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

/**
 * This class tells the runtime environment to look for
 * JAX-RS resources in the war file.
 *
 * <p>The fact that none of the superclass methods are over-ridden
 * causes the runtime environment scan for the resources.</p>
 *
 * <p>The specification says this is optional but recommended
 * behaviour for the runtime.</p>
 */
@ApplicationPath("api")
public class TestApplication extends Application {
}

src\net\gweeps\webapp\resources\TestResource.java


package net.gweeps.webapp.resources;

import java.util.logging.Logger;

// Nothing here about a JAX-RS implementation - this is pure JAX-RS API.
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

// The model POJO
import net.gweeps.webapp.model.Test;

/**
 * The main entry point for the test resource.
 *
 * <p>This is available at /testapp/api/test.</p>
 */
@Path("test")
public class TestResource {

    private static final Logger logger = Logger.getLogger(TestResource.class.getName());

    /**
     * An example sub-resource method.
     *
     * <p>The result is an instance of the POJO Test, encoded in JSON. This is to show how
     * JAX-RX just knows how to do this without and great amount of coniguration.</p>
     *
     * @param id a request id.
     * @return a {@link Test} object representing the model's recommendations, encoded in JSON.
     */
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Test get(@QueryParam("id") String id) {
        logger.info(String.format("Received request id: %s", id));

        Test response = new Test(id, "A test object.");

        logger.info(String.format("Reponse for request id: %d, %s", id, response.toString()));
        return response;
    }
}

src\net\gweeps\webapp\model\Test.java


package net.gweeps.webapp.model;

import java.util.ArrayList;
import java.util.List;

/**
 * A POJO representing some object in the application model.
 *
 * <p>Note there are no annotations regarding JSON here.</p>
 */
public class Test {
	private String name;
	private String description;
	private List<String> details = new ArrayList<>();

	public Test(String name, String description) {
		this.name = name;
		this.description = description;
		details.add("Detail 1.");
		details.add("Detail 2.");
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public List<String> getDetails() {
		return details;
	}
}

Compilation and packaging

To compile and package the application requires using javac and jar.

The java compiler will not compile everything under a particular directory, it wants a list of source files. The easiest way to do this in the Windows 10 command prompt is to use the dir command and the @ argument for javac:


C:\Users\dajta\src\testwebapp>dir /s /B *.java >sourcefiles.txt
C:\Users\dajta\src\testwebapp>javac -cp lib\javax.ws.rs-api-2.1.1.jar -sourcepath src -d war\WEB-INF\classes @sourcefiles.txt
C:\Users\dajta\src\testwebapp>cd war
C:\Users\dajta\src\testwebapp\war>jar cMf ..\testapp.war *
C:\Users\dajta\src\testwebapp>cd ..
C:\Users\dajta\src\testwebapp>jar tf testapp.war
WEB-INF/
WEB-INF/classes/
WEB-INF/classes/net/
WEB-INF/classes/net/gweeps/
WEB-INF/classes/net/gweeps/webapp/
WEB-INF/classes/net/gweeps/webapp/model/
WEB-INF/classes/net/gweeps/webapp/model/Test.class
WEB-INF/classes/net/gweeps/webapp/resources/
WEB-INF/classes/net/gweeps/webapp/resources/TestResource.class
WEB-INF/classes/net/gweeps/webapp/TestApplication.class

No XML files to be seen, or even a manifest.

Running the application

A simple way to run the application is to download either TomEE or Wildfly, copy the war file to the appropriate directory, and start the server.

TomEE


C:\Users\dajta\src\testwebapp>curl -O https://downloads.apache.org/tomee/tomee-8.0.6/apache-tomee-8.0.6-webprofile.zip

You must use version 8 of TomEE so you get a Jakarta EE 8 implementation. Jakarta EE 9 introduces breaking changes in the JAX-RS API package names. Awesome.

Unzip TomEE in place, and copy the war file into it:


C:\Users\dajta\src\testwebapp>copy testapp.war apache-tomee-webprofile-8.0.6\webapps

Run TomEE and the application will be deployed:


03-Jul-2021 13:44:58.844 INFO [main] org.apache.openejb.config.AppInfoBuilder.build Enterprise application "C:\Users\dajta\src\testwebapp\apache-tomee-webprofile-8.0.6\webapps\testapp" loaded.
03-Jul-2021 13:44:58.848 INFO [main] org.apache.openejb.assembler.classic.Assembler.createApplication Assembling app: C:\Users\dajta\src\testwebapp\apache-tomee-webprofile-8.0.6\webapps\testapp
03-Jul-2021 13:44:59.021 INFO [main] org.apache.openejb.assembler.classic.Assembler.createApplication Deployed Application(path=C:\Users\dajta\src\testwebapp\apache-tomee-webprofile-8.0.6\webapps\testapp)
...
03-Jul-2021 13:44:59.328 INFO [main] org.apache.openejb.server.cxf.rs.CxfRsHttpListener.logEndpoints REST Application: http://localhost:8080/testapp/api      -> net.gweeps.webapp.TestApplication@12cd9150
03-Jul-2021 13:44:59.331 INFO [main] org.apache.openejb.server.cxf.rs.CxfRsHttpListener.logEndpoints      Service URI: http://localhost:8080/testapp/api/test -> Pojo net.gweeps.webapp.resources.TestResource
03-Jul-2021 13:44:59.331 INFO [main] org.apache.openejb.server.cxf.rs.CxfRsHttpListener.logEndpoints               GET http://localhost:8080/testapp/api/test ->      Testget(String)
03-Jul-2021 13:44:59.348 INFO [main] sun.reflect.DelegatingMethodAccessorImpl.invoke Deployment of web application archive [C:\Users\dajta\src\testwebapp\apache-tomee-webprofile-8.0.6\webapps\testapp.war] has finished in [699] ms

WidlFly

I used WidlFly 23, the latest release at the time of writing. It still has Jakarta EE 8, rather than Jarkarta EE 9.


C:\Users\dajta\src\testwebapp>curl -O https://download.jboss.org/wildfly/23.0.2.Final/wildfly-23.0.2.Final.zip    

Unzip WidlFly in place, and copy the war file into it:


C:\Users\dajta\src\testwebapp>copy testapp.war wildfly-23.0.2.Final\standalone\deployments

Run WidlFly and the application will be deployed:


16:57:26,851 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-7) WFLYSRV0027: Starting deployment of "testapp.war" (runtime-name: "testapp.war")
...
16:57:28,212 INFO  [org.jboss.resteasy.resteasy_jaxrs.i18n] (ServerService Thread Pool -- 79) RESTEASY002225: Deploying javax.ws.rs.core.Application: class net.gweeps.webapp.TestApplication
...
16:57:28,276 INFO  [org.wildfly.extension.undertow] (ServerService Thread Pool -- 79) WFLYUT0021: Registered web context: '/testapp' for server 'default-server'
16:57:28,320 INFO  [org.jboss.as.server] (ServerService Thread Pool -- 45) WFLYSRV0010: Deployed "testapp.war" (runtime-name : "testapp.war")

Testing

Use curl to connect to it and marvel at the fact the POJO was converted to JSON with no more effort than marking the sub-resource method with an annotation.


C:\Users\dajta\src\testwebapp>curl localhost:8080/testapp/api/test
{"description":"A test object.","details":["Detail 1.","Detail 2."]}