Servlets - fidransky/kiv-pia-labs GitHub Wiki

TOTD:

  • Java web app low-level basics
  • blocking (Servlet) vs. non-blocking (Reactive) stack

Why?

Why are we learning servlets?

Because they are there. Whenever you implement a RESTful service or a web application using ThymeLeaf, JSP, JSF or any other templating engine, these are always based on the Servlet API. When using Spring MVC, there is an embedded Tomcat (or other Servlet Container) serving your requests. And when you encounter an exception, it is always good to have at least a basic understanding of all the listeners/filters/servlets that you may find in the stack trace.

Terms and theory

Context & Application

When you deploy a Web Application to a Servlet Container, a Context is created for it containing and effectively isolating all your application code and data. It is possible to do cross-context calls, but it must be properly configured on both contexts within the Servlet Container to allow it. Usually, your application/context is defined by a URL prefix within the Servlet Container. Anything after the prefix is passed to the context and being handled according to the mapping rules defined there. See mapping below.

Servlet

Servlet is a basic functional unit. It can serve any HTTP method. The basic java interface is jakarta.servlet.Servlet where every request is served using the service() method. For your convenience, you may use the jakarta.servlet.http.HttpServlet abstract class, which defines separate methods to handle each HTTP method. These methods are doGet(), doPost()… You get the idea.

Filter & Filter chain

For each request there is a chain of filters. These should not contain any business logic, but may serve to do authentication, authorization, content (both, data and metadata) modification and many other tasks. It is important to never break the chain (call the chain.doFilter() method) unless you really mean to.

Essentially, filters in Java work the same way and serve the same purpose as middlewares in Node.js.

Listener

There are many life cycles defined within the Servlet API and for each there is a Listener interface you need to implement to be notified about the events.

  • jakarta.servlet.ServletRequestListener - allows you to be notified about the request's creation and destruction,
  • jakarta.servlet.ServletContextListener - the same for the context - meaning you will be notified when your application is started and stopped,
  • jakarta.servlet.http.HttpSessionListener, jakarta.servlet.http.HttpSessionAttributeListener and many other can be used.

Dispatcher

Dispatcher is the process inside the Servlet Container responsible for calling the right filters and servlets whenever a request is made. It uses filter and servlet mappings (see below) to do it automatically, but you can also use it programatically. It is important to understand the difference between REQUEST, FORWARD, INCLUDE and other types of mapping. All types are described in the jakarta.servlet.DispatcherType enum.

REQUEST is being used when the initial request is being processed, other types are there to process "calls" within the context of your application. Inside your servlet you may for example ask the dispatcher to FORWARD the request to some other URI.

By default, all forwards and includes are being handled within the same context.

Mapping

Servlets mappings are served with all Dispatcher Types described above. A servlet can be mapped to multiple URI patterns.

Filter mappings are more complicated. You may map a filter to multiple URI patterns, but you can also map it to specific servlet names in which case the filter inherits mappings from the servlets.

Filter specific is also a possibility to specify a set of Dispatcher Types for which the filter should be activated.

In practice, only REQUEST is being used.

Deployment Descriptor aka web.xml

Historically, all Java web apps had to contain a web.xml file where all Servlets, Filters, Listeners and Mappings are configured. Starting with Servlet API 3.0, all mentioned can be configured via Java annotations. Still, a web.xml file is needed for other things such as welcome file or error handling configuration.

Attributes

Servlet API provides three key-value stores, each holding attributes for a different time period:

  1. context
  2. session
  3. request

All three stores can be used to store any Java object but each one is useful for different purposes. In summary, we can say that:

  • Context attributes are meant for infra-structure, such as shared connection pools.
  • Session attributes are meant for contextual information, such as user identification.
  • Request attributes are meant for specific request info, such as query results.

Servlet vs. Reactive stack

Generally, request processing consists of three main steps:

  1. read request from network (input)
  2. actual processing (e.g. write data to DB, request data from other web app, RPC, ...)
  3. write response to network (output)

Traditionally, all steps are done on a single thread which is blocked from processing further requests until the current one is finished. To process multiple requests simultaneously but avoid the cost of thread management, servers typically maintain thread pools (in Apache Tomcat, 200 threads by default). However, this solution doesn't scale very well and it's prone to DOS (denial of service) attacks.

To solve these problems, one must abandon the traditional sequential logic associated with imperative programming, in favor of asynchronous APIs, and to learn to react to events they generate (therefore the reactive stack). This is how the event loop concept (popularised by Node.js) works.

In Servlet API, a few improvements have been added over the years to enable reactive request processing:

year Servlet API feature
1997 1.0 Initial version
2009 3.0 Async Servlet
2013 3.1 Servlet Non-Blocking I/O

Practice

1. Create a new Maven project

In Intellij IDEA, go to File > New > New project… to create a new project:

  • Language: Java
  • Build system: Maven
  • JDK: 21

In Advanced Settings:

  • GroupId: cz.zcu.kiv.pia
  • ArtifactId: pia-labs

2. Add Servlet API dependency

Adding the jakarta.servlet:jakarta.servlet-api dependency allows our project to use Servlet API classes and to be deployed to a Servlet Container.

Add the dependency to the dependencies section of your pom.xml file:

<dependency>
	<groupId>jakarta.servlet</groupId>
	<artifactId>jakarta.servlet-api</artifactId>
	<version>6.0.0</version>
	<scope>provided</scope>
</dependency>

3. Configure Maven packaging

Maven packages projects as a jar file by default. Changing this configuration to war makes Maven package our project so that it can be deployed to a Servlet Container. Set <packaging> to war in your pom.xml file:

<packaging>war</packaging>

Maven uses org.apache.maven.plugins:maven-war-plugin plugin to actually do the packaging. Add the plugin to the build > plugins section of the pom.xml file:

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-war-plugin</artifactId>
	<version>3.4.0</version>
</plugin>

Now, Maven packages our project correctly but the resulting war file's name (e.g. pia-labs-1.0-SNAPSHOT.war) is not very practical because the Servlet Container deploys war files to a Context matching given war file's name (here, http://localhost:8080/pia-labs-1.0-SNAPSHOT). Change the final name to match the artifactId by setting finalName in the POM's build section:

<finalName>${project.artifactId}</finalName>

Finally, we need to change maven-war-plugin's configuration so that it doesn't fail when web.xml file doesn't exist in the plugin's configuration section:

<failOnMissingWebXml>false</failOnMissingWebXml>

4. Finally implement something

While adding a web.xml file is often no longer necessary, we'll still need it for some tasks today. Create the file in src/main/webapp/WEB-INF/ directory:

<?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>KIV-PIA Labs</display-name>
</web-app>

Next, build and package the project using Maven:

mvn package

After Maven successfully finishes the build, explore the results in target/ directory and deploy the pia-labs.war file to Apache Tomcat running in Docker:

https://github.com/fidransky/tomcat/pkgs/container/tomcat

After the deployment, open http://localhost:8080/pia-labs/. Since we haven't implemented any request processing yet, Tomcat returns 404 Not Found. Let's fix that - create an index.html file in src/main/webapp/ directory:

<h1>It works!</h1>

Build and deploy the app again, then refresh the page to see that It works! 🎉

Note: index.html is one of the predefined welcome file names. To use some other file name, you'd need to configure welcome-file-list in web.xml first.

4.1 Servlet

Create a HelloWorldServlet class in cz.zcu.kiv.pia.labs package of your project, extending jakarta.servlet.http.HttpServlet and annotated with jakarta.servlet.annotation.WebServlet annotation with urlPatterns attribute set to /hello:

package cz.zcu.kiv.pia.labs;

import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(urlPatterns = "/hello")
public class HelloWorldServlet extends HttpServlet {
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
		res.setContentType("text/html");

		// use res.getWriter() instead of writing directly to res.getOutputStream()
		PrintWriter out = res.getWriter();
		out.println("<h1>Hello World!</h1>");
	}
}

Expand the servlet to display a value of a request parameter named from. When present, display

Hello World from …!

Otherwise, just display

Hello World!

var from = req.getParameter("from");
var builder = new StringBuilder("Hello World");
if (from != null) {
	builder.append(" from ").append(from);
}
builder.append("!");

See other Servlet examples here: http://localhost:8080/examples/servlets/

4.2 Filter

Create a HelloWorldFilter class in cz.zcu.kiv.pia.labs package of your project, extending jakarta.servlet.http.HttpFilter and annotated with jakarta.servlet.annotation.WebFilter annotation with urlPatterns attribute set to /*:

package cz.zcu.kiv.pia.labs;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebFilter(urlPatterns = "/*")
public class HelloWorldFilter extends HttpFilter {
	@Override
	protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
		super.doFilter(req, res, chain);
	}
}

Filters can also be used for error handling. Change the HelloWorldServlet to throw an exception when from parameter is missing:

throw new RuntimeException("The 'from' parameter is missing.");

When an exception is thrown but not handled during request processing, Tomcat renders its own error page and sets response code to 500 Internal Server Error. To provide a custom error handling mechanism, we need to configure the error page in web.xml file:

<error-page>
	<location>/error</location>
</error-page>

Now, whenever an exception is thrown, Tomcat renders /error page instead of its own. Let's create a new ErrorServlet and map it to that URL:

package cz.zcu.kiv.pia.labs;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(urlPatterns = "/error")
public class ErrorServlet extends HttpServlet {
	@Override
	protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		Throwable exception = (Throwable) req.getAttribute(RequestDispatcher.ERROR_EXCEPTION);

		resp.setContentType("text/html");
		resp.setStatus(400);

		PrintWriter out = resp.getWriter();
		out.println("<h1>Error: " + exception.getMessage() + "</h1>");
	}
}

Finally, create a new ErrorFilter logging full exception stack trace to standard output:

package cz.zcu.kiv.pia.labs;

import jakarta.servlet.DispatcherType;
import jakarta.servlet.FilterChain;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebFilter(urlPatterns = "/*", dispatcherTypes = DispatcherType.ERROR)
public class ErrorFilter extends HttpFilter {
	@Override
	protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
		Throwable exception = (Throwable) req.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
		exception.printStackTrace(System.out);

		super.doFilter(req, res, chain);
	}
}

4.3 Listener

Create a HelloWorldListener class in cz.zcu.kiv.pia.labs package of your project, implementing jakarta.servlet.ServletContextListener interface and annotated with jakarta.servlet.annotation.WebListener annotation:

package cz.zcu.kiv.pia.labs;

import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;

@WebListener
public class HelloWorldListener implements ServletContextListener {
	@Override
	public void contextInitialized(ServletContextEvent sce) {
		// do nothing
	}
}

Make the listener implement ServletRequestListener interface and implement its methods to measure and log requests processing time:

private static final String REQUEST_START_ATTRIBUTE_NAME = "requestStartedDateTime";

@Override
public void requestInitialized(ServletRequestEvent sre) {
	sre.getServletRequest().setAttribute(REQUEST_START_ATTRIBUTE_NAME, LocalDateTime.now());
}

@Override
public void requestDestroyed(ServletRequestEvent sre) {
	final var request = sre.getServletRequest();
	final var started = (LocalDateTime) request.getAttribute(REQUEST_START_ATTRIBUTE_NAME);
	final var now = LocalDateTime.now();

	final var millis = ChronoUnit.MILLIS.between(started, now);
	System.out.println("Request took " + millis + " ms.");
}

5. Explore async and non-blocking I/O examples

Async Servlet

Servlet Non-Blocking I/O

Summary

We have learned about low-level Servlet API and implemented a simple app which does some request/response operations, handles errors and measures request processing time. We have also touched on differences between blocking (Servlet) and non-blocking (Reactive) stack.

Note: Throughout this lab, we have worked with Jakarta Servlet API. Until recent Java licensing changes, the same used to be called Java Servlet API. This change included moving all classes and interfaces from javax.servlet.* package to jakarta.servlet.* package. Apache Tomcat started to use Jakarta Servlet API in version 10 so all apps deployed to it had to be migrated to Jakarta Servlet API first.

Sources

⚠️ **GitHub.com Fallback** ⚠️