An example of very lightweight RESTful web services in Java | Opensource.com

An example of very lightweight RESTful web services in Java

Explore lightweight RESTful services in Java through a full code example to manage a book collection.

Coding on a computer
x

Subscribe now

Get the highlights in your inbox every week.

Web services, in one form or another, have been around for more than two decades. For example, XML-RPC services appeared in the late 1990s, followed shortly by ones written in the SOAP offshoot. Services in the REST architectural style also made the scene about two decades ago, soon after the XML-RPC and SOAP trailblazers. REST-style (hereafter, Restful) services now dominate in popular sites such as eBay, Facebook, and Twitter. Despite the alternatives to web services for distributed computing (e.g., web sockets, microservices, and new frameworks for remote-procedure calls), Restful web services remain attractive for several reasons:

  • Restful services build upon existing infrastructure and protocols, in particular, web servers and the HTTP/HTTPS protocols. An organization that has HTML-based websites can readily add web services for clients interested more in the data and underlying functionality than in the HTML presentation. Amazon, for example, has pioneered making the same information and functionality available through both websites and web services, either SOAP-based or Restful.

  • Restful services treat HTTP as an API, thereby avoiding the complicated software layering that has come to characterize the SOAP-based approach to web services. For example, the Restful API supports the standard CRUD (Create-Read-Update-Delete) operations through the HTTP verbs POST-GET-PUT-DELETE, respectively; HTTP status codes inform a requester whether a request succeeded or why it failed.

  • Restful web services can be as simple or complicated as needed. Restful is a style—indeed, a very flexible one—rather than a set of prescriptions about how services should be designed and structured. (The attendant downside is that it may be hard to determine what does not count as a Restful service.)

  • For a consumer or client, Restful web services are language- and platform-neutral. The client makes requests in HTTP(S) and receives text responses in a format suitable for modern data interchange (e.g., JSON).

  • Almost every general-purpose programming language has at least adequate (and often strong) support for HTTP/HTTPS, which means that web-service clients can be written in those languages.

This article explores lightweight Restful services in Java through a full code example.

The Restful novels web service

The Restful novels web service consists of three programmer-defined classes:

  • The Novel class represents a novel with just three properties: a machine-generated ID, an author, and a title. The properties could be expanded for more realism, but I want to keep this example simple.
  • The Novels class consists of utilities for various tasks: converting a plain-text encoding of a Novel or a list of them into XML or JSON; supporting the CRUD operations on the novels collection; and initializing the collection from data stored in a file. The Novels class mediates between Novel instances and the servlet.
  • The NovelsServlet class derives from HttpServlet, a sturdy and flexible piece of software that has been around since the very early enterprise Java of the late 1990s. The servlet acts as an HTTP endpoint for client CRUD requests. The servlet code focuses on processing client requests and generating the appropriate responses, leaving the devilish details to utilities in the Novels class.

Some Java frameworks, such as Jersey (JAX-RS) and Restlet, are designed for Restful services. Nonetheless, the HttpServlet on its own provides a lightweight, flexible, powerful, and well-tested API for delivering such services. I'll demonstrate this with the novels example.

Deploy the novels web service

Deploying the novels web service requires a web server, of course. My choice is Tomcat, but the service should work (famous last words!) if it's hosted on, for example, Jetty or even a Java Application Server. The code and a README that summarizes how to install Tomcat are available on my website. There is also a documented Apache Ant script that builds the novels service (or any other service or website) and deploys it under Tomcat or the equivalent.

Tomcat is available for download from its website. Once you install it locally, let TOMCAT_HOME be the install directory. There are two subdirectories of immediate interest:

  • The TOMCAT_HOME/bin directory contains startup and stop scripts for Unix-like systems (startup.sh and shutdown.sh) and Windows (startup.bat and shutdown.bat). Tomcat runs as a Java application. The web server's servlet container is named Catalina. (In Jetty, the web server and container have the same name.) Once Tomcat starts, enter http://localhost:8080/ in a browser to see extensive documentation, including examples.

  • The TOMCAT_HOME/webapps directory is the default for deployed websites and web services. The straightforward way to deploy a website or web service is to copy a JAR file with a .war extension (hence, a WAR file) to TOMCAT_HOME/webapps or a subdirectory thereof. Tomcat then unpacks the WAR file into its own directory. For example, Tomcat would unpack novels.war into a subdirectory named novels, leaving novels.war as-is. A website or service can be removed by deleting the WAR file and updated by overwriting the WAR file with a new version. By the way, the first step in debugging a website or service is to check that Tomcat has unpacked the WAR file; if not, the site or service was not published because of a fatal error in the code or configuration.

  • Because Tomcat listens by default on port 8080 for HTTP requests, a request URL for Tomcat on the local machine begins:

    http://localhost:8080/

    Access a programmer-deployed WAR file by adding the WAR file's name but without the .war extension:

    http://locahost:8080/novels/

    If the service was deployed in a subdirectory (e.g., myapps) of TOMCAT_HOME, this would be reflected in the URL:

    http://locahost:8080/myapps/novels/

    I'll offer more details about this in the testing section near the end of the article.

As noted, the ZIP file on my homepage contains an Ant script that compiles and deploys a website or service. (A copy of novels.war is also included in the ZIP file.) For the novels example, a sample command (with % as the command-line prompt) is:

% ant -Dwar.name=novels deploy

This command compiles Java source files and then builds a deployable file named novels.war, leaves this file in the current directory, and copies it to TOMCAT_HOME/webapps. If all goes well, a GET request (using a browser or a command-line utility, such as curl) serves as a first test:

% curl http://localhost:8080/novels/

Tomcat is configured, by default, for hot deploys: the web server does not need to be shut down to deploy, update, or remove a web application.

The novels service at the code level

Let's get back to the novels example but at the code level. Consider the Novel class below:

Example 1. The Novel class

package novels;

import java.io.Serializable;

public class Novel implements Serializable, Comparable<Novel> {
    static final long serialVersionUID = 1L;
    private String author;
    private String title;
    private int id;

    public Novel() { }

    public void setAuthor(final String author) { this.author = author; }
    public String getAuthor() { return this.author; }
    public void setTitle(final String title) { this.title = title; }
    public String getTitle() { return this.title; }
    public void setId(final int id) { this.id = id; }
    public int getId() { return this.id; }

    public int compareTo(final Novel other) { return this.id - other.id; }
}

This class implements the compareTo method from the Comparable interface because Novel instances are stored in a thread-safe ConcurrentHashMap, which does not enforce a sorted order. In responding to requests to view the collection, the novels service sorts a collection (an ArrayList) extracted from the map; the implementation of compareTo enforces an ascending sorted order by Novel ID.

The class Novels contains various utility functions:

Example 2. The Novels utility class

package novels;

import java.io.IOException;
import java.io.File;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.nio.file.Files;
import java.util.stream.Stream;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.Collections;
import java.beans.XMLEncoder;
import javax.servlet.ServletContext; // not in JavaSE
import org.json.JSONObject;
import org.json.XML;

public class Novels {
    private final String fileName = "/WEB-INF/data/novels.db";
    private ConcurrentMap<Integer, Novel> novels;
    private ServletContext sctx;
    private AtomicInteger mapKey;

    public Novels() {
        novels = new ConcurrentHashMap<Integer, Novel>();
        mapKey = new AtomicInteger();
    }

    public void setServletContext(ServletContext sctx) { this.sctx = sctx; }
    public ServletContext getServletContext() { return this.sctx; }

    public ConcurrentMap<Integer, Novel> getConcurrentMap() {
        if (getServletContext() == null) return null; // not initialized
        if (novels.size() < 1) populate();
        return this.novels;
    }

    public String toXml(Object obj) { // default encoding
        String xml = null;
        try {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            XMLEncoder encoder = new XMLEncoder(out);
            encoder.writeObject(obj);
            encoder.close();
            xml = out.toString();
        }
        catch(Exception e) { }
        return xml;
    }

    public String toJson(String xml) { // option for requester
        try {
            JSONObject jobt = XML.toJSONObject(xml);
            return jobt.toString(3); // 3 is indentation level
        }
        catch(Exception e) { }
        return null;
    }

    public int addNovel(Novel novel) {
        int id = mapKey.incrementAndGet();
        novel.setId(id);
        novels.put(id, novel);
        return id;
    }

    private void populate() {
        InputStream in = sctx.getResourceAsStream(this.fileName);
        // Convert novel.db string data into novels.
        if (in != null) {
            try {
                InputStreamReader isr = new InputStreamReader(in);
                BufferedReader reader = new BufferedReader(isr);

                String record = null;
                while ((record = reader.readLine()) != null) {
                    String[] parts = record.split("!");
                    if (parts.length == 2) {
                        Novel novel = new Novel();
                        novel.setAuthor(parts[0]);
                        novel.setTitle(parts[1]);
                        addNovel(novel); // sets the Id, adds to map
                    }
                }
                in.close();
            }
            catch (IOException e) { }
        }
    }
}

The most complicated method is populate, which reads from a text file contained in the deployed WAR file. The text file contains the initial collection of novels. To open the text file, the populate method needs the ServletContext, a Java map that contains all of the critical information about the servlet embedded in the servlet container. The text file, in turn, contains records such as this:

Jane Austen!Persuasion

The line is parsed into two parts (author and title) separated by the bang symbol (!). The method then builds a Novel instance, sets the author and title properties, and adds the novel to the collection, which acts as an in-memory data store.

The Novels class also has utilities to encode the novels collection into XML or JSON, depending upon the format that the requester prefers. XML is the default, but JSON is available upon request. A lightweight XML-to-JSON package provides the JSON. Further details on encoding are below.

Example 3. The NovelsServlet class

package novels;

import java.util.concurrent.ConcurrentMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.beans.XMLEncoder;
import org.json.JSONObject;
import org.json.XML;

public class NovelsServlet extends HttpServlet {
    static final long serialVersionUID = 1L;
    private Novels novels; // back-end bean

    // Executed when servlet is first loaded into container.
    @Override
    public void init() {
        this.novels = new Novels();
        novels.setServletContext(this.getServletContext());
    }

    // GET /novels
    // GET /novels?id=1
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        String param = request.getParameter("id");
        Integer key = (param == null) ? null : Integer.valueOf((param.trim()));

        // Check user preference for XML or JSON by inspecting
        // the HTTP headers for the Accept key.
        boolean json = false;
        String accept = request.getHeader("accept");
        if (accept != null && accept.contains("json")) json = true;

        // If no query string, assume client wants the full list.
        if (key == null) {
            ConcurrentMap<Integer, Novel> map = novels.getConcurrentMap();
            Object[] list = map.values().toArray();
            Arrays.sort(list);

            String payload = novels.toXml(list);        // defaults to Xml
            if (json) payload = novels.toJson(payload); // Json preferred?
            sendResponse(response, payload);
        }
        // Otherwise, return the specified Novel.
        else {
            Novel novel = novels.getConcurrentMap().get(key);
            if (novel == null) { // no such Novel
                String msg = key + " does not map to a novel.\n";
                sendResponse(response, novels.toXml(msg));
            }
            else { // requested Novel found
                if (json) sendResponse(response, novels.toJson(novels.toXml(novel)));
                else sendResponse(response, novels.toXml(novel));
            }
        }
    }

    // POST /novels
    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response) {
        String author = request.getParameter("author");
        String title = request.getParameter("title");

        // Are the data to create a new novel present?
        if (author == null || title == null)
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));

        // Create a novel.
        Novel n = new Novel();
        n.setAuthor(author);
        n.setTitle(title);

        // Save the ID of the newly created Novel.
        int id = novels.addNovel(n);

        // Generate the confirmation message.
        String msg = "Novel " + id + " created.\n";
        sendResponse(response, novels.toXml(msg));
    }

    // PUT /novels
    @Override
    public void doPut(HttpServletRequest request, HttpServletResponse response) {
        /* A workaround is necessary for a PUT request because Tomcat does not
           generate a workable parameter map for the PUT verb. */

        String key = null;
        String rest = null;
        boolean author = false;

        /* Let the hack begin. */
        try {
            BufferedReader br =
                new BufferedReader(new InputStreamReader(request.getInputStream()));
            String data = br.readLine();
            /* To simplify the hack, assume that the PUT request has exactly
               two parameters: the id and either author or title. Assume, further,
               that the id comes first. From the client side, a hash character
               # separates the id and the author/title, e.g.,

                  id=33#title=War and Peace
            */

            String[] args = data.split("#");      // id in args[0], rest in args[1]
            String[] parts1 = args[0].split("="); // id = parts1[1]
            key = parts1[1];

            String[] parts2 = args[1].split("="); // parts2[0] is key
            if (parts2[0].contains("author")) author = true;
            rest = parts2[1];
        }
        catch(Exception e) {
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_INTERNAL_SERVER_ERROR));
        }

        // If no key, then the request is ill formed.
        if (key == null)
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));

        // Look up the specified novel.
        Novel p = novels.getConcurrentMap().get(Integer.valueOf((key.trim())));
        if (p == null) { // not found
            String msg = key + " does not map to a novel.\n";
            sendResponse(response, novels.toXml(msg));
        }
        else { // found
            if (rest == null) {
                throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));
            }
            // Do the editing.
            else {
                if (author) p.setAuthor(rest);
                else p.setTitle(rest);

                String msg = "Novel " + key + " has been edited.\n";
                sendResponse(response, novels.toXml(msg));
            }
        }
    }

    // DELETE /novels?id=1
    @Override
    public void doDelete(HttpServletRequest request, HttpServletResponse response) {
        String param = request.getParameter("id");
        Integer key = (param == null) ? null : Integer.valueOf((param.trim()));
        // Only one Novel can be deleted at a time.
        if (key == null)
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));
        try {
            novels.getConcurrentMap().remove(key);
            String msg = "Novel " + key + " removed.\n";
            sendResponse(response, novels.toXml(msg));
        }
        catch(Exception e) {
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_INTERNAL_SERVER_ERROR));
        }
    }

    // Methods Not Allowed
    @Override
    public void doTrace(HttpServletRequest request, HttpServletResponse response) {
        throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED));
    }

    @Override
    public void doHead(HttpServletRequest request, HttpServletResponse response) {
        throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED));
    }

    @Override
    public void doOptions(HttpServletRequest request, HttpServletResponse response) {
        throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED));
    }

    // Send the response payload (Xml or Json) to the client.
    private void sendResponse(HttpServletResponse response, String payload) {
        try {
            OutputStream out = response.getOutputStream();
            out.write(payload.getBytes());
            out.flush();
        }
        catch(Exception e) {
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_INTERNAL_SERVER_ERROR));
        }
    }
}

Recall that the NovelsServlet class above extends the HttpServlet class, which in turn extends the GenericServlet class, which implements the Servlet interface:

NovelsServlet extends HttpServlet extends GenericServlet implements Servlet

As the name makes clear, the HttpServlet is designed for servlets delivered over HTTP(S). The class provides empty methods named after the standard HTTP request verbs (officially, methods):

  • doPost (Post = Create)
  • doGet (Get = Read)
  • doPut (Put = Update)
  • doDelete (Delete = Delete)

Some additional HTTP verbs are covered as well. An extension of the HttpServlet, such as the NovelsServlet, overrides any do method of interest, leaving the others as no-ops. The NovelsServlet overrides seven of the do methods.

Each of the HttpServlet CRUD methods takes the same two arguments. Here is doPost as an example:

public void doPost(HttpServletRequest request, HttpServletResponse response) {

The request argument is a map of the HTTP request information, and the response provides an output stream back to the requester. A method such as doPost is structured as follows:

  • Read the request information, taking whatever action is appropriate to generate a response. If information is missing or otherwise deficient, generate an error.
  • Use the extracted request information to perform the appropriate CRUD operation (in this case, create a Novel) and then encode an appropriate response to the requester using the response output stream to do so. In the case of doPost, the response is a confirmation that a new novel has been created and added to the collection. Once the response is sent, the output stream is closed, which closes the connection as well.

More on the do method overrides

An HTTP request has a relatively simple structure. Here is a sketch in the familiar HTTP 1.1 format, with comments introduced by double sharp signs:

GET /novels              ## start line
Host: localhost:8080     ## header element
Accept-type: text/plain  ## ditto
...
[body]                   ## POST and PUT only

The start line begins with the HTTP verb (in this case, GET) and the URI (Uniform Resource Identifier), which is the noun (in this case, novels) that names the targeted resource. The headers consist of key-value pairs, with a colon separating the key on the left from the value(s) on the right. The header with key Host (case insensitive) is required; the hostname localhost is the symbolic address of the local machine on the local machine, and the port number 8080 is the default for the Tomcat web server awaiting HTTP requests. (By default, Tomcat listens on port 8443 for HTTPS requests.) The header elements can occur in arbitrary order. In this example, the Accept-type header's value is the MIME type text/plain.

Some requests (in particular, POST and PUT) have bodies, whereas others (in particular, GET and DELETE) do not. If there is a body (perhaps empty), two newlines separate the headers from the body; the HTTP body consists of key-value pairs. For bodyless requests, header elements, such as the query string, can be used to send information. Here is a request to GET the /novels resource with the ID of 2:

GET /novels?id=2

The query string starts with the question mark and, in general, consists of key-value pairs, although a key without a value is possible.

The HttpServlet, with methods such as getParameter and getParameterMap, nicely hides the distinction between HTTP requests with and without a body. In the novels example, the getParameter method is used to extract the required information from the GET, POST, and DELETE requests. (Handling a PUT request requires lower-level code because Tomcat does not provide a workable parameter map for PUT requests.) Here, for illustration, is a slice of the doPost method in the NovelsServlet override:

@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) {
   String author = request.getParameter("author");
   String title = request.getParameter("title");
   ...

For a bodyless DELETE request, the approach is essentially the same:

@Override
public void doDelete(HttpServletRequest request, HttpServletResponse response) {
   String param = request.getParameter("id"); // id of novel to be removed
   ...

The doGet method needs to distinguish between two flavors of a GET request: one flavor means "get all", whereas the other means get a specified one. If the GET request URL contains a query string whose key is an ID, then the request is interpreted as "get a specified one":

http://localhost:8080/novels?id=2  ## GET specified

If there is no query string, the GET request is interpreted as "get all":

http://localhost:8080/novels       ## GET all

Some devilish details

The novels service design reflects how a Java-based web server such as Tomcat works. At startup, Tomcat builds a thread pool from which request handlers are drawn, an approach known as the one thread per request model. Modern versions of Tomcat also use non-blocking I/O to boost performance.

The novels service executes as a single instance of the NovelsServlet class, which in turn maintains a single collection of novels. Accordingly, a race condition would arise, for example, if these two requests were processed concurrently:

  • One request changes the collection by adding a new novel.
  • The other request gets all the novels in the collection.

The outcome is indeterminate, depending on exactly how the read and write operations overlap. To avoid this problem, the novels service uses a thread-safe ConcurrentMap. Keys for this map are generated with a thread-safe AtomicInteger. Here is the relevant code segment:

public class Novels {
    private ConcurrentMap<Integer, Novel> novels;
    private AtomicInteger mapKey;
    ...

By default, a response to a client request is encoded as XML. The novels program uses the old-time XMLEncoder class for simplicity; a far richer option is the JAX-B library. The code is straightforward:

public String toXml(Object obj) { // default encoding
   String xml = null;
   try {
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      XMLEncoder encoder = new XMLEncoder(out);
      encoder.writeObject(obj);
      encoder.close();
      xml = out.toString();
   }
   catch(Exception e) { }
   return xml;
}

The Object parameter is either a sorted ArrayList of novels (in response to a "get all" request); or a single Novel instance (in response to a get one request); or a String (a confirmation message).

If an HTTP request header refers to JSON as a desired type, then the XML is converted to JSON. Here is the check in the doGet method of the NovelsServlet:

String accept = request.getHeader("accept"); // "accept" is case insensitive
if (accept != null && accept.contains("json")) json = true;

The Novels class houses the toJson method, which converts XML to JSON:

public String toJson(String xml) { // option for requester
   try {
      JSONObject jobt = XML.toJSONObject(xml);
      return jobt.toString(3); // 3 is indentation level
   }
   catch(Exception e) { }
   return null;
}

The NovelsServlet checks for errors of various types. For example, a POST request should include an author and a title for the new novel. If either is missing, the doPost method throws an exception:

if (author == null || title == null)
   throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));

The SC in SC_BAD_REQUEST stands for status code, and the BAD_REQUEST has the standard HTTP numeric value of 400. If the HTTP verb in a request is TRACE, a different status code is returned:

public void doTrace(HttpServletRequest request, HttpServletResponse response) {
   throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED));
}

Testing the novels service

Testing a web service with a browser is tricky. Among the CRUD verbs, modern browsers generate only POST (Create) and GET (Read) requests. Even a POST request is challenging from a browser, as the key-values for the body need to be included; this is typically done through an HTML form. A command-line utility such as curl is a better way to go, as this section illustrates with some curl commands, which are included in the ZIP on my website.

Here are some sample tests without the corresponding output:

% curl localhost:8080/novels/
% curl localhost:8080/novels?id=1
% curl --header "Accept: application/json" localhost:8080/novels/

The first command requests all the novels, which are encoded by default in XML. The second command requests the novel with an ID of 1, which is encoded in XML. The last command adds an Accept header element with application/json as the MIME type desired. The get one command could also use this header element. Such requests have JSON rather than the XML responses.

The next two commands create a new novel in the collection and confirm the addition:

% curl --request POST --data "author=Tolstoy&title=War and Peace" localhost:8080/novels/
% curl localhost:8080/novels?id=4

A PUT command in curl resembles a POST command except that the PUT body does not use standard syntax. The documentation for the doPut method in the NovelsServlet goes into detail, but the short version is that Tomcat does not generate a proper map on PUT requests. Here is the sample PUT command and a confirmation command:

% curl --request PUT --data "id=3#title=This is an UPDATE" localhost:8080/novels/
% curl localhost:8080/novels?id=3

The second command confirms the update.

Finally, the DELETE command works as expected:

% curl --request DELETE localhost:8080/novels?id=2
% curl localhost:8080/novels/

The request is for the novel with the ID of 2 to be deleted. The second command shows the remaining novels.

The web.xml configuration file

Although it's officially optional, a web.xml configuration file is a mainstay in a production-grade website or service. The configuration file allows routing, security, and other features of a site or service to be specified independently of the implementation code. The configuration for the novels service handles routing by providing a URL pattern for requests dispatched to this service:

<?xml version = "1.0" encoding = "UTF-8"?>
<web-app>
  <servlet>
    <servlet-name>novels</servlet-name>
    <servlet-class>novels.NovelsServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>novels</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>
</web-app>

The servlet-name element provides an abbreviation (novels) for the servlet's fully qualified class name (novels.NovelsServlet), and this name is used in the servlet-mapping element below.

Recall that a URL for a deployed service has the WAR file name right after the port number:

http://localhost:8080/novels/

The slash immediately after the port number begins the URI known as the path to the requested resource, in this case, the novels service; hence, the term novels occurs after the first single slash.

In the web.xml file, the url-pattern is specified as /*, which means any path that starts with /novels. Suppose Tomcat encounters a contrived request URL, such as this:

http://localhost:8080/novels/foobar/

The web.xml configuration specifies that this request, too, should be dispatched to the novels servlet because the /* pattern covers /foobar. The contrived URL thus has the same result as the legitimate one shown above it.

A production-grade configuration file might include information on security, both wire-level and users-roles. Even in this case, the configuration file would be only two or three times the size of the sample one.

Wrapping up

The HttpServlet is at the center of Java's web technologies. A website or web service, such as the novels service, extends this class, overriding the do verbs of interest. A Restful framework such as Jersey (JAX-RS) or Restlet does essentially the same by providing a customized servlet, which then acts as the HTTP(S) endpoint for requests against a web application written in the framework.

A servlet-based application has access, of course, to any Java library required in the web application. If the application follows the separation-of-concerns principle, then the servlet code remains attractively simple: the code checks a request, issuing the appropriate error if there are deficiencies; otherwise, the code calls out for whatever functionality may be required (e.g., querying a database, encoding a response in a specified format), and then sends the response to the requester. The HttpServletRequest and HttpServletResponse types make it easy to perform the servlet-specific work of reading the request and writing the response.

Java has APIs that range from the very simple to the highly complicated. If you need to deliver some Restful services using Java, my advice is to give the low-fuss HttpServlet a try before anything else.

Cheat Sheet cover image

Download our new curl cheat sheet. Curl is a fast and efficient way to pull the information you need from the internet without using a graphical interface.
Person drinking a hot drink at the computer

Java must include these benefits to compete in the era of cloud-native, microservices, and serverless applications.

Topics

About the author

Marty Kalin - I'm an academic in computer science (College of Computing and Digital Media, DePaul University) with wide experience in software development, mostly in production planning and scheduling (steel industry) and product configuration (truck and bus manufacturing). Details on books and other publications are available at Marty Kalin's hompage