Blog-Archiv

Dienstag, 5. Dezember 2023

Java AspectJ Wildcards

It is a rare "pleasure" to get in touch with aspects in Java. Here is what I want to remember about it, without being an expert. I am doing this since the experts seem to be unable to provide quickly accessible explanations of these syntax constructs, in other words: what would we do without stackoverflow? Read through hundreds of AspectJ reference pages, just to find out that they forgot to document it?

Wildcards are expressions that describe something imprecisely, mostly character sequences. Lots of software bugs rise from such fuzzy expressions: "if you have a problem and try to solve it with regular expressions, then you have two problems". Same goes with aspects, do not use it just for fun, they will make life complicated.

Now here come the AspectJ wildcards, note that they have nothing to do with regular expressions. I refer to pointcut definitions that describe on which class and method an aspect's advice should work, something like:

@Pointcut( "execution(public * com.abc..dao..*(..))" )

Wildcards

*

The character '*' (asterisk) stands for any fully qualified Java class name, but only when it is used without any adjacent '.' (dot) character. It would match both MyClassWithoutPackageName or java.util.List.

..

The sequence ".." (dot dot) stands for any character sequence that starts and ends with a "." (dot), whereby it consumes the maximum of possible characters, including '.' (dots). It would match both .net. or .net.http. but not java.net. as the latter does not start with a '.' (dot). Typically this wildcard is used for Java package name fragments.

In context of methods, the sequence ".." (dot dot) has a different meaning. Here it stands for a list of zero or more parameters. The expression "foo(..)" would match all methods with name foo, having any number of parameters, and "foo(.., String)" would match all foo having a String as last parameter.

*..

The character '*' (asterisk) has a different meaning when used together with ".." (dot dot). Here it stands for zero or more characters except '.' (dot).

The sequence "*.." would match .net. or java.net. but not .net.URL as the latter does not end with a '.' (dot).

..*

The sequence "..*" would match .net. or .net.URL but not java.net. as the latter does not start with a '.' (dot).

*..*

The sequence "*..*" would match all of .net. or .net.http. or java.net. or .net.http.HttpClient or java.net.URL.

Note

AspectJ patterns seem to be not case-sensitive. Feel free to write java.util.map instead of java.util.Map. The violent psychopath that ends up maintaining your code will be delighted.




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.




Freitag, 15. September 2023

JUnit 5 Before and After Annotations Execution Order

JUnit has been a Java testing framework now for 25 years. In continuation of my year 2020 article about JUnit 4 execution order I will describe here how junit-jupiter-api 5.4.2 behaves, together with junit-platform-commons 1.4.2. It is about @BeforeEach and @AfterEach annotations, no others.

Questions will be: How is the execution order when ....

Same Annotation on Multiple Methods


 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
class SameAnnotationOnMultipleMethods
{
    @BeforeEach
    public void bbb() {
        System.err.println("@BeforeEach bbb()");
    }
    @BeforeEach
    public void aaa() {
        System.err.println("@BeforeEach aaa()");
    }
    @BeforeEach
    public void ccc() {
        System.err.println("@BeforeEach ccc()");
    }
    
    @AfterEach
    public void zzz() {
        System.err.println("@AfterEach zzz()");
    }
    @AfterEach
    public void xxx() {
        System.err.println("@AfterEach xxx()");
    }
    @AfterEach
    public void yyy() {
        System.err.println("@AfterEach yyy()");
    }
    
    @Test
    public void test() {
        System.err.println("SameAnnotationOnMultipleMethods: @Test test()");
    }
}

Output is:

@BeforeEach aaa()
@BeforeEach bbb()
@BeforeEach ccc()
SameAnnotationOnMultipleMethods: @Test test()
@AfterEach xxx()
@AfterEach yyy()
@AfterEach zzz()

The methods seem to get executed in alphabetical order, not as written in source. This applies to both @BeforeEach and @AfterEach.

Multiple Same Annotations on Multiple Methods


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class SameMultipleAnnotationsOnMultipleMethods
{
    @BeforeEach
    @AfterEach
    public void bbb() {
        System.err.println("@BeforeEach @AfterEach bbb()");
    }
    @BeforeEach
    @AfterEach
    public void aaa() {
        System.err.println("@BeforeEach @AfterEach aaa()");
    }
    @BeforeEach
    @AfterEach
    public void ccc() {
        System.err.println("@BeforeEach @AfterEach ccc()");
    }

    @Test
    public void test() {
        System.err.println("SameMultipleAnnotationsOnMultipleMethods: @Test test()");
    }
}

Output is:

@BeforeEach @AfterEach aaa()
@BeforeEach @AfterEach bbb()
@BeforeEach @AfterEach ccc()
SameMultipleAnnotationsOnMultipleMethods: @Test test()
@BeforeEach @AfterEach aaa()
@BeforeEach @AfterEach bbb()
@BeforeEach @AfterEach ccc()

Again the methods seem to get executed in alphabetical order.

An interesting variant of this ambiguity is when @BeforeEach and @AfterEach collide with a single @BeforeEach or @AfterEach:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class SameMultipleAnnotationsOnMultipleMethods
{
    @BeforeEach
    public void bbb() {
        System.err.println("@BeforeEach bbb()");
    }
    @BeforeEach
    @AfterEach
    public void aaa() {
        System.err.println("@BeforeEach @AfterEach aaa()");
    }
    @AfterEach
    public void ccc() {
        System.err.println("@AfterEach ccc()");
    }

    @Test
    public void test() {
        System.err.println("SameMultipleAnnotationsOnMultipleMethods: @Test test()");
    }
}

Output is:

@BeforeEach @AfterEach aaa()
@BeforeEach bbb()
SameMultipleAnnotationsOnMultipleMethods: @Test test()
@BeforeEach @AfterEach aaa()
@AfterEach ccc()

Again the execution order is alphabetical by method-name. The order is not connected to the fact that aaa() carries multiple annotations, as I found out by changing the method names.

Annotations in Inheritance

Super-class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
abstract class AbstractBeforeAndAfter
{
    @BeforeEach
    public void setUp() {
        System.err.println("AbstractBeforeAndAfter: @BeforeEach setUp()");
    }

    @BeforeEach
    @AfterEach
    public void bothInSuperclass() {
        System.err.println("AbstractBeforeAndAfter: @BeforeEach @AfterEach bothInSuperclass()");
    }

    @AfterEach
    public void tearDown() {
        System.err.println("AbstractBeforeAndAfter: @AfterEach tearDown()");
    }
}

Derived class:

 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
class AnnotationsInInheritance extends AbstractBeforeAndAfter
{
    @BeforeEach
    @Override
    public void setUp() {
        System.err.println("AnnotationsInInheritance: @BeforeEach @Override setUp()");
    }

    @BeforeEach
    @AfterEach
    public void bothInDerivedClass() {
        System.err.println("AnnotationsInInheritance: @BeforeEach @AfterEach bothInDerivedClass()");
    }

    @AfterEach
    @Override
    public void tearDown() {
        System.err.println("AnnotationsInInheritance: @AfterEach @Override tearDown()");
    }

    
    @Test
    public void test() {
        System.err.println("AnnotationsInInheritance: @Test test()");
    }
}

Mind that bothInDerivedClass() is not an override of bothInSuperClass()!

Output is:

AbstractBeforeAndAfter: @BeforeEach @AfterEach bothInSuperclass()
AnnotationsInInheritance: @BeforeEach @AfterEach bothInDerivedClass()
AnnotationsInInheritance: @BeforeEach @Override setUp()
AnnotationsInInheritance: @Test test()
AnnotationsInInheritance: @AfterEach @Override tearDown()
AnnotationsInInheritance: @BeforeEach @AfterEach bothInDerivedClass()
AbstractBeforeAndAfter: @BeforeEach @AfterEach bothInSuperclass()

Super-class @BeforeEach annotations come before derived class. Super-class @AfterEach annotations come after derived class.

Mind that "@AfterEach @Override tearDown()" here comes before "@BeforeEach @AfterEach bothInDerivedClass()". This contradicts the previous assumption that methods are executed in alphabetical order. The tearDown() method is executed first because it is an @Override of a method in super-class.

Missing Annotation in Override

This builds on the same super-class as the example above.
Assumption is that @AfterEach tearDown() in super-class was overridden, but the annotation was forgotten:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class MissingAnnotationInOverride extends AbstractBeforeAndAfter
{
    @Override
    // Mind that @AfterEach is missing here!
    public void tearDown() {
        System.err.println("MissingAnnotationInOverride: @Override tearDown()");
    }

    
    @Test
    public void test() {
        System.err.println("MissingAnnotationInOverride: @Test test()");
    }
}

Output is:

AbstractBeforeAndAfter: @BeforeEach @AfterEach bothInSuperclass()
AbstractBeforeAndAfter: @BeforeEach setUp()
MissingAnnotationInOverride: @Test test()
AbstractBeforeAndAfter: @BeforeEach @AfterEach bothInSuperclass()

The tearDown() was not executed.
That means when you override an annotated method and do not call it explicitly, the annotation in super-class will be ignored and the method will not be executed at all. In other words, these JUnit 5 annotations are not inherited from a super-class, they have to be repeated.

Mind that such does not apply to any annotation-framework, it depends on the framework's implementation. Although the annotation definition syntax has become part of the Java programming language, the semantic of an annotation is up to its implementation, and that is NOT part of the Java runtime library.

Conclusion

Adding the annotations feature to the Java programming-language opened the door for complexity that has nothing to do with object-oriented thinking. We need to learn and try out the semantics of each and every annotation we use.

For the discussed JUnit5 annotations, we need to remember:

  • Super-class annotations come first on "BeforeEach", come last on "AfterEach"
  • Alphabetical order of execution is not something you should build upon, this would just lead to confusion
  • The discussed JUnit-5 annotations will not be inherited from an overridden method, they must be repeated in the override

Hope this was helpful for some test developers!