spring-bootjava.util.logging

Can't override java.util.logging.LogManager in a Spring Boot web application: Getting java.lang.ClassNotFoundException on already loaded class


I am trying to override java.util.logging.LogManager with my own configuration:

class CloudwatchHandler is an implementation of Handler and includes this init() method:

public static void init() {
    final String julConfigFile = System.getProperty("java.util.logging.config.file");
    if(julConfigFile != null) {
        try (InputStream is = new FileInputStream(julConfigFile)) {
            LogManager logManager = LogManager.getLogManager();
            logManager.reset();
            logManager.readConfiguration(is);
            Logger logger = Logger.getLogger(CloudwatchHandler.class.getName());
            logger.info("LOADED");
        } catch (SecurityException | IOException e) {
            System.err.println(Instant.now() + ": Failed to initialize JUL.");
            e.printStackTrace(System.err);
            throw new RuntimeException(e);
        }
    }
    else {
        System.err.println(Instant.now() + ": java.util.logging.config.file was not specified");
    }

}

Application main class

public static void main(String[] args) {
    CloudwatchHandler.init();
    SpringApplication.run(MyApp.class, args);
}

Error

Can't load log handler "mypackage.CloudwatchHandler"
java.lang.ClassNotFoundException: mypackage.CloudwatchHandler
java.lang.ClassNotFoundException: mypackage.CloudwatchHandler
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
        at java.logging/java.util.logging.LogManager.createLoggerHandlers(LogManager.java:1005)
        at java.logging/java.util.logging.LogManager$4.run(LogManager.java:975)
        at java.logging/java.util.logging.LogManager$4.run(LogManager.java:971)
        at java.base/java.security.AccessController.doPrivileged(AccessController.java:318)
        at java.logging/java.util.logging.LogManager.loadLoggerHandlers(LogManager.java:971)
        at java.logging/java.util.logging.LogManager.initializeGlobalHandlers(LogManager.java:2424)
        at java.logging/java.util.logging.LogManager$RootLogger.accessCheckedHandlers(LogManager.java:2526)
        at java.logging/java.util.logging.Logger.getHandlers(Logger.java:2090)
        at java.logging/java.util.logging.Logger.log(Logger.java:977)
        at java.logging/java.util.logging.Logger.doLog(Logger.java:1007)
        at java.logging/java.util.logging.Logger.log(Logger.java:1030)
        at java.logging/java.util.logging.Logger.info(Logger.java:1803)
        at mypackage.CloudwatchHandler.init(CloudwatchHandler.java:51)
        ... main ...

The really crazy thing about this exception is that the class causing the ClassNotFoundException is actually a caller in the current stack frame, as seen in the stack trace. So clearly it has been FOUND or it couldn't be running.

What's causing this and how can I fix it? I just want to load my own log handler.

Spring Boot version is 2.6.3.


Solution

  • ClassNotFoundException can occur if the Handler is not deployed to load in the system class loader as that is what the LogManager uses to find handlers.

    Update your test case and try again:

    public static void main(String[] args) throws Exception {
        System.out.println(ClassLoader.getSystemClassLoader());
        System.out.println(Thread.currentThread().getContextClassLoader());
        System.out.println(CloudwatchHandler.class.getClassLoader());
    
        //This is what CloudwatchHandler.init(); triggers
        Class.forName(CloudwatchHandler.class.getName(), true, Thread.currentThread().getContextClassLoader());
    
       //This is what the LogManager is doing
       Class.forName(CloudwatchHandler.class.getName(), true, ClassLoader.getSystemClassLoader());
    
    
       //Force load the root handlers.
       Logger.getLogger("").getHandlers();
    
       CloudwatchHandler.init();
       SpringApplication.run(MyApp.class, args);
    }
    

    If it is that the handler is deployed in the context class loader and not in the system classloader then you need to change how you package the handler so it is visible to the system classloader. The java.util.logging.config.class option is one part of the LogManager that will try loading classes via context classloader which is what will be able to see your classes. For this option you move the contents of your init method to a new class and have the constructor perform the action. On the command line then set the value to the FQCN of the new config class.