tomcat7-maven-plugin: Invalid byte tag in constant pool: 19

I use tomcat7-maven-plugin to spin up a Tomcat 7 container where I can run my web application. When I added dependencies for log4j2 (version 2.11.0) to my project, I got the error:

org.apache.tomcat.util.bcel.classfile.ClassFormatException:
Invalid byte tag in constant pool: 19

Apparently, log4j2 is a multi-release jar and older versions of Tomcat can’t handle that. So I needed to upgrade my Tomcat maven plugin.

Solution: Update your Tomcat

But how do you update your Tomcat? Its information page shows that it hasn’t been updated for a while, the latest version being 2.2 which runs 7.0.47 by default. Maven Central, on the other hand, shows that the latest version at the moment of this writing is 7.0.86. That’s the version we want.

Change your pom.xml in the following way:

<project>
  <properties>
    <tomcat.version>7.0.86</tomcat.version>
  </properties>
  <plugins>
    <plugin>
      <plugin>
        <groupId>org.apache.tomcat.maven</groupId>
        <artifactId>tomcat7-maven-plugin</artifactId>
        <version>2.2</version>
        <configuration>
          <path>/</path>
          <port>7777</port>
        </configuration>
        <dependencies>
          <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>${tomcat.version}</version>
          </dependency>
          <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-util</artifactId>
            <version>${tomcat.version}</version>
          </dependency>
          <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-coyote</artifactId>
            <version>${tomcat.version}</version>
          </dependency>
          <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-api</artifactId>
            <version>${tomcat.version}</version>
          </dependency>
          <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
            <version>${tomcat.version}</version>
          </dependency>
          <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-dbcp</artifactId>
            <version>${tomcat.version}</version>
          </dependency>
          <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-servlet-api</artifactId>
            <version>${tomcat.version}</version>
          </dependency>
          <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jsp-api</artifactId>
            <version>${tomcat.version}</version>
          </dependency>
          <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jasper</artifactId>
            <version>${tomcat.version}</version>
          </dependency>
          <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jasper-el</artifactId>
            <version>${tomcat.version}</version>
          </dependency>
          <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-el-api</artifactId>
            <version>${tomcat.version}</version>
          </dependency>
          <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-catalina</artifactId>
            <version>${tomcat.version}</version>
          </dependency>
          <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-tribes</artifactId>
            <version>${tomcat.version}</version>
          </dependency>
          <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-catalina-ha</artifactId>
            <version>${tomcat.version}</version>
          </dependency>
          <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-annotations-api</artifactId>
            <version>${tomcat.version}</version>
          </dependency>
          <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-juli</artifactId>
            <version>${tomcat.version}</version>
          </dependency>
          <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-logging-juli</artifactId>
            <version>${tomcat.version}</version>
          </dependency>
          <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-logging-log4j</artifactId>
            <version>${tomcat.version}</version>
          </dependency>
        </dependencies>
      </plugin>
    </plugin>
  </plugins>
</project>

How to write a custom appender in log4j2?

/* package declaration, imports... */

@Plugin(name = "CustomListAppender",
        category = Core.CATEGORY_NAME,
        elementType = Appender.ELEMENT_TYPE,
        printObject = true)
public final class CustomListAppender extends AbstractAppender {

    // for storing the log events
    private List<LogEvent> events = new ArrayList<>();

    protected CustomListAppender(
            String name,
            Filter filter,
            Layout<? extends Serializable> layout,
            boolean ignoreExceptions) {
        super(name, filter, layout, ignoreExceptions);
    }

    @Override
    public void append(LogEvent event) {
        if (event instanceof MutableLogEvent) {
            events.add(((MutableLogEvent) event).createMemento());
        } else {
            events.add(event);
        }
    }

    public List<LogEvent> getEvents() {
        return events;
    }

    @PluginFactory
    public static CustomListAppender createAppender(
            @PluginAttribute("name") String name,
            @PluginElement("Layout") Layout<? extends Serializable> layout,
            @PluginElement("Filter") Filter filter) {
        if (name == null) {
            LOGGER.error("No name provided for TestLoggerAppender");
            return null;
        }

        if (layout == null) layout = PatternLayout.createDefaultLayout();

        return new CustomListAppender(name, filter, layout, true);
    }
}

Our CustomListAppender extends AbstractAppender, because that implements a lot of the methods from the Appender interface for us that we would otherwise have to implement ourselves.

The @Plugin annotation identifies this class a plugin that should be picked up by the PluginManager:

  • The name attribute defines the name of the appender that can be used in the configuration.
  • The category attribute should be "Core", because “Core plugins are those that are directly represented by an element in a configuration file, such as an Appender, Layout, Logger or Filter” (source). And we are creating an appender.
  • The elementType attribute defines which type of element in the Core category this plugin should be. In our case, "appender".
  • The printObject attribute defines whether our custom plugin class defines a useful toString() method. We do, because the AbstractAppender class we’re extending is taking care of that for us.

We implement the Appender#append(LogEvent) method to add each event to our events list. If the LogEvent happens to be mutable, we must take care to create an immutable copy of the event, otherwise subsequent log events will overwrite it (we will get a list of, say, three log events that are all referencing the same object). We also add a simple getter method to retrieve all log events.

For the PluginManager to create our custom plugin, it needs a way to instantiate it. log4j2 uses a factory method for that, indicated by the annotation @PluginFactory. An appender contains attributes, such as a name, and other elements, such as layouts and filters. To allow for these, we use the corresponding annotations @PluginAttribute to indicate that a parameter represents an attribute, and @PluginElement to indicate that a parameter represents an element.

To log errors that might occur during this setup, we can make use of the StatusLogger. This logger is available as LOGGER, and is defined in one of the parents of our custom plugin, AbstractLifeCycle. (The level of log messages that should be visible can be adjusted in the <Configuration status="warn" ...> element.)

Configuration

Configuration:
  packages: com.relentlesscoding.logging.plugins
  status: warn
  appenders:
    Console:
      name: STDOUT
    CustomListAppender:
      name: MyVeryOwnListAppender

  Loggers:
    logger:
      -
        name: com.relentlesscoding.logging
        level: info
        AppenderRef:
          ref: MyVeryOwnListAppender
    Root:
      level: error
      AppenderRef:
        ref: STDOUT

The packages attribute on the Configuration element indicates the package that should be scanned by the PluginManager for custom plugins during initialization.

How to use our custom list appender?

private CustomListAppender appender;

@Before
public void setupLogging() {
    LoggerContext context = LoggerContext.getContext(false);
    Configuration configuration = context.getConfiguration();
    appender = configuration.getAppender("MyVeryOwnListAppender");
    appender.getEvents().clear();
}

When we run tests now, we are able to see all logged events by calling appender.getEvents(). Before each test, we take care to clear the list of the previous log statements.

Unit test log4j2 log output

Sometimes you want to test if certain log output gets generated when certain events happen in your application. Here is how I unit test that using log4j2 (version 2.11.0).

Use LoggerContextRule to get to your ListAppender quickly

If you are using JUnit 4, then the quickest solution would be one that is used by log4j2 itself:

import org.apache.logging.log4j.junit.LoggerContextRule;
/* other imports */

public class LogEventTest {
    private static ListAppender appender;

    @ClassRule
    public static LoggerContextRule init = new LoggerContextRule("log4j2-test.yaml");

    @BeforeClass
    public static void setupLogging() {
        appender = init.getListAppender("List");
    }

    @Before
    public void clearAppender() {
        appender.clear();
    }

    @Test
    public void someMethodShouldLogAnError() {
        // setup test and invoke logic
        List<LogEvent> logEvents = appender.getEvents();
        List<String> errors = logEvents.stream()
                .filter(event -> event.getLevel().equals(Level.ERROR))
                .map(event -> event.getMessage().getFormattedMessage())
                .collect(Collectors.toList());

        // we logged at least one event of level error
        assertThat(errors.size(), is(greaterThanOrEqualTo(1)));

        // log event message should contain "wrong" for example
        assertThat(errors, everyItem(containsString("wrong")));
    }
}

The LoggerContextRule provides methods that come in handy while testing. Here we use the getListAppender(...) method to get access to an appender that uses a list to store all log events. Before each test, we clear the list, so we have a clean slate for new log events. The test invokes the code-under-test, requests the log events from the appender and filters them so that we only have the error log events left. Then we that at least one error log message was captured and that it contains the word “wrong”.

Use the LoggerContext

Instead of using the class rule (which lets you conveniently pass it the file name of the configuration), you could also use the LoggerContext:

@BeforeClass
public static void setupLogging() {
    LoggerContext context = LoggerContext.getContext(false);
    Logger logger = context.getLogger("com.relentlesscoding");
    appender = (ListAppender) logger.getAppenders().get("List");
}

This might be your only option if you are working with JUnit 5 (and eventually you will want to migrate to that). In JUnit 5 we can’t use LoggerContextRule anymore, because @Rules don’t longer exist (they were replaced with an extension mechanism that works differently and log4j2 doesn’t provide such an extension currently).

Create a working configuration

To get the examples working, we need to define an appender called “List” and a logger in our log4j2 configuration.

log4j2-test.yaml

Configuration:
  status: warn
  name: TestConfig
  appenders:
    Console:
      name: STDOUT
    List:
      name: List

  Loggers:
    logger:
      -
        name: com.relentlesscoding
        AppenderRef:
          ref: List
    Root:
      level: info AppenderRef:
        ref: STDOUT

This configuration will send log events occurring in the package com.relentlesscoding and sub-packages to the appender with the name “List” (which is of type ListAppender). This configuration is defined in YAML, but you can use XML, JSON or the properties format as well.

Maven dependencies

To get the LoggerContextRule in JUnit 4 working, you need the following:

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.11.0</version>
    <type>test-jar</type>
</dependency>

To get the YAML log4j2 configuration working, you need:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.5</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-yaml</artifactId>
    <version>2.9.5</version>
</dependency>

JMockit fakes

If you are working with microservices, you may find that you are having a lot of dependencies on other services. If you want your tests to be autonomous, that is, running in isolation of those dependencies, you can either use a tool like WireMock, or you can use a mocking framework.

Recently, I came across JMockit, a mocking framework for Java. To mock external dependencies, you can use so-called “fakes”.

Say you use a service that knows the credentials of the customers of your e-store, and you make requests to this service by using a driver provided by this service, say CredentialsHttpService. In your development environment (and Jenkins), you don’t have access to a running service. A solution to this would be a fake implementation of CredentialsHttpService, where we would mock the methods that we actually call from our code.

public class CredentialsHttpService {
    ...
    public Optional<CustomerAccount> getCustomerAccount(String id) {
        // does HTTP request to service
    }
    ...
}

In our test code, we can now implement the fake:

public class ServiceTest {
    @Tested
    Service service;

    @Test
    void test() {
        new MockUp<CredentialsHttpService>() {
            final static AtomicInteger counter = new AtomicInteger();

            @Mock
            Optional<CustomerAccount> getCustomerAccount(String id) {
                return Optional.of(CustomerAccount.builder()
                                        .accountNumber(counter.incrementAndGet())
                                        .build());
            }
        }

        service.doFoo();  // eventually invokes our fake
                          // getCustomerAccount() implementation
    }
}

The class to be faked is the type parameter of the MockUp class. The fake CredentialsHttpService will only mock the methods that are annotated with @Mock. All other methods of the faked class will have their real implementation. Mocked methods are not restricted by access modifiers: the method may have a private, protected, package-private or public access modifier, as long as the method name and number of parameters and parameter types are the same.

There is a lot of other things that fakes can do in JMockit, see the documentation.