Blog-Archiv

Sonntag, 5. November 2023

Java Servlet with Filter via Annotations

The Java servlet specification supports annotations since version 3.0. In this article I want to show how to set up a servlet via annotations. There is no more web.xml (what a relief!), except when you need to give your filters a sort-order.

Besides the annnotations I played around with url-patterns and page-forwards and the relation between them. I used as many explicit names and constants as possible to make these relations clear.

Application Specification

There are two servlets, one for log-in, the other displays date and time and provides a log-out button. A filter intercepts every request and checks if log-in has been done. If not, the log-in page is shown. For simplicity there is just one text field and a fixed user name "fri". Once you log in with another name, you will be notified about the correct name.

As of the Maven artifact name and url-patterns given below, the application is loadable via following URLs:

  • http://localhost:8080/servletViaAnnotations/
  • http://localhost:8080/servletViaAnnotations/log-in
  • http://localhost:8080/servletViaAnnotations/date-time

Maven Project and Source

Here are the project's directories and files. I recommend to first build this structure, then compile it with Maven, then import it into Eclipse as "Maven Project".

Click onto the expand controls to see the sources.

servletViaAnnotations
pom.xml
The only dependency is servlet-api 4.0. Note the <failOnMissingWebXml> property due to the absence of web.xml.
<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>fri</groupId>
  <artifactId>servletViaAnnotations</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>
  
  <name>Minimal Servlet with Annotations</name>

  <properties>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
    <!-- without this, Eclipse complains about "Dynamic Web Module 4.0 requires Java 1.8 or newer" -->
    
    <failOnMissingWebXml>false</failOnMissingWebXml>
    <!-- without this, Eclipse complains about missing web.xml -->
  </properties>
    
  <dependencies>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
      <scope>provided</scope>
    </dependency>
    
  </dependencies>

</project>
src
main
java
fri
testServlet40
AuthenticationFilter.java
Does not allow to display date/time without log-in. Manages a fake authentication via a HTML form parameter.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package fri.testServlet40;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;

@WebFilter(
    urlPatterns = "/*"
)
public class AuthenticationFilter implements Filter
{
    private static final String THE_ONE_AND_ONLY = "fri";
    
    @Override
    public void doFilter(
            ServletRequest request, 
            ServletResponse response, 
            FilterChain chain)
        throws IOException, ServletException
    {
        final String userName = request.getParameter(LoginServlet.USERNAME_FIELDNAME);
        
        if (isAuthenticated(userName)) {
            chain.doFilter(request, response); // call other filters and finally servlet
        }
        else {
            request.setAttribute( // display error in LoginServlet
                LoginServlet.LOGIN_ERROR_MESSAGE, 
                (userName != null) 
                    ? "Wrong name: '"+escapeHtml(userName)+"', must be '"+THE_ONE_AND_ONLY+"'"
                    : null
            );
            request.getRequestDispatcher(LoginServlet.RELATIVE_URL_PATTERN).forward(request, response);
        }
    }

    private boolean isAuthenticated(String userName) {
        return THE_ONE_AND_ONLY.equals(userName);
    }
    
    private String escapeHtml(String userName) {
        return userName
                .replace("&", "&amp;")
                .replace("<", "&lt;")
                .replace(">", "&gt;")
                .replace("\"", "&quot;")
                .replace("'", "&#39");
    }

}
LoginServlet.java
Shows a login text-field, a "Log In" button, and optionally an error message.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package fri.testServlet40;

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

@WebServlet(
    urlPatterns = "/"+LoginServlet.RELATIVE_URL_PATTERN
)
public class LoginServlet extends HttpServlet
{
    public static final String RELATIVE_URL_PATTERN = "log-in";
    
    public static final String USERNAME_FIELDNAME = "userName";
    
    public static final String LOGIN_ERROR_MESSAGE = "LoginErrorMessage";
    
    @Override
    protected void doGet(
            HttpServletRequest request, 
            HttpServletResponse response)
        throws ServletException, IOException 
    {
        final Object errorMessage = request.getAttribute(LOGIN_ERROR_MESSAGE);
        final String errorHtml = (errorMessage != null ? "<div>Error: "+errorMessage+"</div>" : "");
        
        response.setContentType("text/html;charset=UTF-8");
        final String html = 
                "<html><body>"+
                "  <form method='POST' action='"+TimeServlet.RELATIVE_URL_PATTERN+"'>"+
                "    Your Name: <input name='"+USERNAME_FIELDNAME+"' type='text'/>"+
                "    <input type='submit' value='Log In'/>"+
                "  </form>"+
                   errorHtml+
                "</body></html>";
        final PrintWriter out = response.getWriter();
        out.println(html);
        out.close();
    }

    @Override
    protected void doPost(
            HttpServletRequest request, 
            HttpServletResponse response) 
        throws ServletException, IOException 
    {
        doGet(request, response);
    }
}
TimeServlet.java
Displays current date/time and a "Log Out" button.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package fri.testServlet40;

import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(
    urlPatterns = "/"+TimeServlet.RELATIVE_URL_PATTERN
)
public class TimeServlet extends HttpServlet
{
    public static final String RELATIVE_URL_PATTERN = "date-time";
    
    @Override
    protected void doGet(
            HttpServletRequest request, 
            HttpServletResponse response)
        throws ServletException, IOException 
    {
        final Calendar now = Calendar.getInstance();

        response.setContentType("text/html;charset=UTF-8");
        final String html = 
                "<html><body>"+
                "  <div>Date: "+date(now)+"</div>"+
                "  <div>Time: "+time(now)+"</div>"+
                "  <form method='GET' action='"+LoginServlet.RELATIVE_URL_PATTERN+"'>"+
                "    <input type='submit' value='Log Out'/>"+
                "  </form>"+
                "</body></html>";
        final PrintWriter out = response.getWriter();
        out.println(html);
        out.close();
    }

    @Override
    protected void doPost(
            HttpServletRequest request, 
            HttpServletResponse response) 
        throws ServletException, IOException 
    {
        doGet(request, response);
    }


    private String date(Calendar now) {
        return new SimpleDateFormat("yyyy-MM-dd").format(now.getTime());
    }
    
    private String time(Calendar now) {
        return new SimpleDateFormat("HH:mm:ss").format(now.getTime());
    }
}

There are only Java classes, no more XML except the Maven pom.xml. You can drive such a servlet with any web-server that supports servlet 3.0. It will search for annotations in all Java classes additionally to reading the web.xml deployment descriptor. This is expensive but seems to work well e.g. in Tomcat, despite the fact that there is no component-scan directive like Spring has, which would restrict the time-consuming class-scanning to certain packages.

Explanations


AuthenticationFilter

The @WebFilter annotation on line 11 marks the class as servlet-filter. Its url-pattern is rendered as annotation-attribute urlPatterns, which can be an array of strings. The authentication-filter is interested in every HTTP-request, so it declares the wildcard "/*". Remember that a filter always is called before any servlet.

As soon as a request arrives, the filter reads the USERNAME_FIELDNAME parameter from it (line 25). This is sent by the login-page via a POST parameter in LoginServlet on line 35.

If the input name is not "fri" (line 27 and 42), the request-dispatcher for the LoginServlet's RELATIVE_URL_PATTERN is loaded, and the request gets forwarded to it (line 37). Before that, an error message is built together that renders the wrong and the right log-in name (line 33). Mind that you must escape every user input to avoid XSS attacks (line 34 and 45). The error message is passed to the LoginServlet via a request attribute LOGIN_ERROR_MESSAGE (line 31 and 32).
In case the input name is "fri", the request is dispatched to the servlet that the LoginServlet specifies on line 34 in its form POST action: the TimeServlet's RELATIVE_URL_PATTERN.

Thus the filter uses constants from LoginServlet, but it doesn't know the TimeServlet.

LoginServlet

The LoginServlet is marked by the @WebServlet annotation on line 11. It declares its own url-pattern on line 12. Mind that the servlet specification demands a leading slash "/" for url-patterns, but forwarding to other servlets works via relative paths, as shown e.g. in AuthenticationFilter on line 37.

Note that I specify the url-pattern in the annotation via a Java constant of the class below. This is important to make clear how all these names relate to each other. For example, servlet-names are needed just when filters relate to them, so here are none (although the @WebServlet annotation allows it).

The RELATIVE_URL_PATTERN on line 16 enables others to forward to this servlet without hardcoding strings. The USERNAME_FIELDNAME on line 18 is the name of the request-parameter that carries the user-name. This builds upon an HTML form mechanism, thus you see the constant used on line 35 in the login-form, and in AuthenticationFilter on line 25.

On ine 28, the optional error message coming from AuthenticationFilter is retrieved. Starting from line 33 the HTML for the login form is built together. The POST to the TimeServlet is secified on line 34, the name of the HTML input-field that represents the authentiction request parameter is on line 35. From line 40 on the response is sent to the client browser.

Both servlets in this example support GET and POST HTTP methods, simply by delegating POST to GET. The LoginServlet launches a POST request to not render the user-input in the URL address, which would be a security leak. Thus the forward page will hold POST data, and if you click the browser's "Reload" button, you would have to confirm that POST data get sent again. If you set the method on line 34 to GET, this confirmation would not be displyed. (Supporting both methods makes it easy to switch!)

TimeServlet

The TimeServlet has its @WebServlet annotation on line 13, and its url-pattern below.

It build its output starting from line 30. It displays date and time, and a "Log Out" button below. That button does a GET call to the LoginServlet. If you then click browser "Back", you will see the TimeServlet again, because the browser kept the POST form data for it, thus it looks like you are still logged in. This is just an example servlet, do not use this authentication implementation in a real application!


Conclusion

Using servlets without some framework like JSP or Velocity is tiresome, I would not recommend such. Having HTML source inside Java classes is hardly maintainable. This example is not a realistic application. But it is interesting to uncover the original servlet mechanisms from the end of last century :-)




Freitag, 3. November 2023

A Minimal Java Servlet with Filters

I took out a servlet filter that had no filter-mapping from a web.xml of a (very old) web-app, and - BOOM! - the app did not work any more.

One would assume that a filter without filter-mapping should be ignored by the web-container, but I could not verify this in the servlet 4.0 specification. I couldn't believe that removing a filter without filter-mapping can affect a web-app in any way, so I wrote a minimal servlet application to try this out. Environment was Tomcat 9, Servlet 4.0, Maven 3.6.3.

Objectives of this article are

  1. showing how a minimal Java servlet looks like
  2. trying out whether a filter without filter-mapping is applied or ignored by the web-container.

Application Specification

The servlet outputs ">Hallo Welt<" into the browser, where "Hallo" is determined by one filter, and "Welt" by another filter. So, if I remove the filter-mapping of the "Welt" filter, the output should be ">Hallo null<". This has to be proven.

Maven Project

I recommend to first create the directories and files of the project, then compile it with Maven, then import it as "Maven Project" into Eclipse.

I named the Maven artifact testServlet40, which designates the servlet's context-path. I also named the root directory of the project testServlet40. Thus the web-app is available in a web-browser using this address:

  • http://localhost:8080/testServlet40

The file pom.xml describes the Maven project object model (dependencies).
The file web.xml is the Servlet deloyment descriptor that controls the interaction between the servlet-container (Tomcat, Jetty, ...) and the web-app (servlet).

Following are the project folders and source files. Click on the expand controls to see collapsed sources.

testServlet40
pom.xml
Depends on servlet-api only, because I don't even use JSP here:
<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>fri</groupId>
  <artifactId>testServlet40</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>Minimal Servlet 4.0 Example3</name>

  <properties>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
    <!-- without this, Eclipse complains about "Dynamic Web Module 4.0 requires Java 1.8 or newer" -->
  </properties>
    
  <dependencies>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
      <scope>provided</scope>
    </dependency>
    
  </dependencies>

</project>
src
main
java
fri
testServlet40
HelloWorldServlet.java
Builds together two request attribute values that were set by filters, see lines 23 and 27:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package fri.testServlet40;

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class HelloWorldServlet extends HttpServlet
{
    public static final String HELLO_ATTRIBUTENAME = "hello";
    public static final String WORLD_ATTRIBUTENAME = "world";
    
    @Override
    protected void doGet(
            HttpServletRequest request, 
            HttpServletResponse response)
        throws ServletException, IOException 
    {
        System.err.println("HelloWorldServlet start request on contextPath '"+request.getContextPath()+"'");
        
        final Object hello = request.getAttribute(HELLO_ATTRIBUTENAME);
        final Object world = request.getAttribute(WORLD_ATTRIBUTENAME);
        
        final PrintWriter out = response.getWriter();
        out.println(">"+hello+" "+world+"<");
        out.close();
        
        System.err.println("HelloWorldServlet end request on contextPath '"+request.getContextPath()+"'");
    }
}
HelloFilter.java
Sets the "hello" request attribute value for the servlet, see line 21:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package fri.testServlet40;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class HelloFilter implements Filter
{
    @Override
    public void doFilter(
            ServletRequest request, 
            ServletResponse response, 
            FilterChain chain)
        throws IOException, ServletException
    {
        System.err.println("HelloFilter start");
        
        request.setAttribute(HelloWorldServlet.HELLO_ATTRIBUTENAME, "Hallo");
        
        chain.doFilter(request, response);
        
        System.err.println("HelloFilter end");
    }
}
WorldFilter.java
Sets the "world" request attribute value for the servlet, see line 21:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package fri.testServlet40;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class WorldFilter implements Filter
{
    @Override
    public void doFilter(
            ServletRequest request, 
            ServletResponse response, 
            FilterChain chain)
        throws IOException, ServletException
    {
        System.err.println("WorldFilter start");
        
        request.setAttribute(HelloWorldServlet.WORLD_ATTRIBUTENAME, "Welt");
        
        chain.doFilter(request, response);
        
        System.err.println("WorldFilter end");
    }
}
webapp
WEB-INF
web.xml
Binds together the servlet and its filters. Mind that the servlet-name is not what appears in the URL address, moreover the URL consists of context-path (basename of .war file, Maven artifact name) and the url-pattern defined here:
<?xml version="1.0"?>
<web-app version="4.0"
        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">

    <servlet>
        <servlet-name>helloworld-servlet</servlet-name>
        <servlet-class>fri.testServlet40.HelloWorldServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>helloworld-servlet</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

    <!-- Filters are called in the order they appear here -->
    
    <filter>
        <filter-name>world-filter</filter-name>
        <filter-class>fri.testServlet40.WorldFilter</filter-class>
    </filter>

    <filter>
        <filter-name>hello-filter</filter-name>
        <filter-class>fri.testServlet40.HelloFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>world-filter</filter-name>
        <servlet-name>helloworld-servlet</servlet-name>
    </filter-mapping>

    <filter-mapping>
        <filter-name>hello-filter</filter-name>
        <servlet-name>helloworld-servlet</servlet-name>
    </filter-mapping>

</web-app>

Running the Application

You can compile and pack the web-application by this Maven commandline:

cd testServlet40
mvn install

Then deploy the created testServlet40-0.0.1-SNAPSHOT.war file into your favorite web-server and call the URL

  • http://localhost:8080/testServlet40

in your favorite web-browser.
On the server console you will see this:

WorldFilter start
HelloFilter start
HelloWorldServlet start request on contextPath '/testServlet40'
HelloWorldServlet end request on contextPath '/testServlet40'
HelloFilter end
WorldFilter end

This proves that filters are called in the order they appear in web.xml: first WorldFilter, then HelloFilter. Then the servlet is called, and afterwards the filters can do cleanup, but in reverse order.

Note that any filter can prevent the execution of other filters and the servlet by not calling chain.doFilter(request, response).

Result

In the browser I can see the expected result:

>Hallo Welt<

When I remove the filter-mapping of WorldFilter in web.xml and pack and deploy the web-app again, the output changes:

>Hallo null<

Thus it is true that a filter without filter-mapping is ignored.