Servlets - fidransky/kiv-pia-labs GitHub Wiki
TOTD:
- Java web app low-level basics
- blocking (Servlet) vs. non-blocking (Reactive) stack
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.
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 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.
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.
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 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.
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.
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.
Servlet API provides three key-value stores, each holding attributes for a different time period:
- context
- session
- 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.
Generally, request processing consists of three main steps:
- read request from network (input)
- actual processing (e.g. write data to DB, request data from other web app, RPC, ...)
- 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 |
… |
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
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>
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>
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.
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/
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);
}
}
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.");
}
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.