Search code examples
javaspring-bootclassloaderlogback

Classes loaded by different Classloaders in Spring Boot and Logback


I ran into a wierd problem that I would like to understand. If someone has a solution that would be great, but I am searching actually for an untderstanding why this is happening:

I wrote a custom Logback Layout. I am extending ch.qos.logback.contrib.json.classic.JsonLayout and overriding addCustomDataToJsonMap. There I would like to add additional attributes if a certain type of argument is found in the logging events arguments list:

  protected void addCustomDataToJsonMap(Map<String, Object> map, ILoggingEvent event) {

    if (event.getArgumentArray() == null) {
      return;
    }

    for (Object argument : event.getArgumentArray()) {

      System.out.println(argument.getClass().getClassLoader()); // 1
      System.out.println(JsonAttribute.class.getClassLoader()); // 2

But the class of the object in the argument list (1) and the class of the static referenced one (2) are loaded by different classloaders as seen in the output:

org.springframework.boot.devtools.restart.classloader.RestartClassLoader@618157b2
sun.misc.Launcher$AppClassLoader@18b4aac2

Due to this I am not able to cast the object to the disired type and access its methods. The workaround I have in mind would be to access those via Reflecion, but I would rather use the real value.

I suppose that this is only a problem of my development environment, but as said in the first section I would really like to understand what is happening.

Edit:

As expected: When running the appliation in "production"-mode without the spring dev tools, the classes are loaded all by the same classloader.

Addendum:

The stacktrace of this function:

    at xxx.ExtendableJsonLayout.addCustomDataToJsonMap(ExtendableJsonLayout.java:26)
    at ch.qos.logback.contrib.json.classic.JsonLayout.toJsonMap(Unknown Source)
    at ch.qos.logback.contrib.json.classic.JsonLayout.toJsonMap(Unknown Source)
    at ch.qos.logback.contrib.json.JsonLayoutBase.doLayout(Unknown Source)
    at ch.qos.logback.core.encoder.LayoutWrappingEncoder.encode(LayoutWrappingEncoder.java:115)
    at ch.qos.logback.core.OutputStreamAppender.subAppend(OutputStreamAppender.java:230)
    at ch.qos.logback.core.OutputStreamAppender.append(OutputStreamAppender.java:102)
    at ch.qos.logback.core.UnsynchronizedAppenderBase.doAppend(UnsynchronizedAppenderBase.java:84)
    at ch.qos.logback.core.spi.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.java:51)
    at ch.qos.logback.classic.Logger.appendLoopOnAppenders(Logger.java:270)
    at ch.qos.logback.classic.Logger.callAppenders(Logger.java:257)
    at ch.qos.logback.classic.Logger.buildLoggingEventAndAppend(Logger.java:421)
    at ch.qos.logback.classic.Logger.filterAndLog_1(Logger.java:398)
    at ch.qos.logback.classic.Logger.error(Logger.java:526)
    at xxx.TestRunner.run(TestRunner.java:23)
    at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:813)
    at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:797)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:324)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248)
    at xxx.TestRunner.main(TestRunner.java:16)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49)

My logback configuration for the appender:

  <appender name="json" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
      <layout class="xxx.ExtendableJsonLayout">
        <jsonFormatter
          class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter">
          <prettyPrint>true</prettyPrint>
        </jsonFormatter>
        <timestampFormat>yyyy-MM-dd' 'HH:mm:ss.SSS</timestampFormat>
      </layout>
    </encoder>
  </appender>

The JsonAttribute class:

@AllArgsConstructor
@Getter
public class JsonAttribute {

  private String key;
  private Object value;

  public static JsonAttribute attr(String key, Object value) {
    return new JsonAttribute(key, value);
  }

  @Override
  public String toString() {
    return key + "=" + value;
  }


Solution

  • I assume you are using Spring DevTools. They create a separate classloader for quicker application restart. This classloader is supposed to load your classes and is thrown away on restart instead of restarting the whole application. See the DevTools docs.

    You should be able to configure it to exclude your custom Logback layout so that it is not reloaded on application restart (Sec. 20.2.6 in the linked docs).