Why does SLF4J with slf4j-nop output the StaticLoggerBinder warning when using a Groovy script with @Grab

531 views Asked by At

I have a Groovy script which specifies dependencies using the Grape @Grab annotation. The dependencies specified are spring-web to use RestTemplate, and a dependency on slf4j-nop to avoid the Failed to load class "org.slf4j.impl.StaticLoggerBinder" warning.

#!/usr/bin/env groovy

@Grab('org.springframework:spring-web:5.3.18')
@Grab('org.slf4j:slf4j-nop:1.7.36')
import org.springframework.web.client.RestTemplate

new RestTemplate().getForObject('http://www.example.com', String)

However, despite this, I am still getting the SLF4J warning:

$ ./restTemplateLoggingTest.groovy 
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

Given that it's a script, it's important that it not output extraneous noise, as the script output may be used and manipulated programmatically.

What can I do to prevent this logging warning from being output when I run my script?

2

There are 2 answers

0
M. Justin On BEST ANSWER

Experimentation has shown that attaching the dependencies to the system classloader using @GrabConfig(systemClassLoader=true) causes the logs to no longer be emitted:

#!/usr/bin/env groovy

@GrabConfig(systemClassLoader=true)
@Grab('org.springframework:spring-web:5.3.18')
@Grab('org.slf4j:slf4j-nop:1.7.36')
import org.springframework.web.client.RestTemplate

new RestTemplate().getForObject('http://www.example.com', String)

I don't know for sure why this is the cause, though I have some vague guesses.

Note that despite addressing the issue, this isn't a use that's described by the Javadocs for GrabConfig#systemClassLoader:

Set to true if you want to use the system classloader when loading the grape. This is normally only required when a core Java class needs to reference the grabbed classes, e.g. for a database driver accessed using DriverManager.

3
Matteo Mazza On

short version:

as per M. Justin answer, add @GrabConfig(systemClassLoader=true) to load slf4j-nop on the systemClassLoader.
However keep reading below if you plan to use this in a production environment.

long version:

from the link in the warning message:

Failed to load class org.slf4j.impl.StaticLoggerBinder This warning message is reported when the org.slf4j.impl.StaticLoggerBinder class could not be loaded into memory. This happens when no appropriate SLF4J binding could be found on the class path.

If we look at the classpath of each class loader in the chain, we can see where spring and slf4j jars are. We can do so adding a bit of code at the end of the provided snippet:

@Grab('org.springframework:spring-web:5.3.18')
@Grab('org.slf4j:slf4j-nop:1.7.36')
import org.springframework.web.client.RestTemplate

new RestTemplate().getForObject('http://www.example.com', String)

def printClassPath(classLoader) {
  println classLoader.class
  try{
    classLoader.getURLs().each {url-> println "- ${url.toString()}" }
    if (classLoader.parent) { printClassPath(classLoader.parent) }
  } catch(Exception e){
    println "$classLoader ignored"
  }
}
printClassPath this.class.classLoader

the output would be something like:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

- [removed lines for readability]
class groovy.lang.GroovyClassLoader
- file:/Users/[...]/.groovy/grapes/org.slf4j/slf4j-nop/jars/slf4j-nop-1.7.36.jar
- file:/Users/[...]/.groovy/grapes/org.slf4j/slf4j-api/jars/slf4j-api-1.7.36.jar
- file:/Users/[...]/.groovy/grapes/org.springframework/spring-web/jars/spring-web-5.3.18.jar
- [removed lines for readability]
class org.codehaus.groovy.tools.RootLoader
- file:/Users/[...]/bin/groovy-3.0.13/lib/slf4j-api-1.7.32.jars
- [removed lines for readability]

we can see that spring-web (and its dependencies) are present in groovy.lang.GroovyClassLoader classpath. However, slf4j-api is also present in the parent class loader classpath (org.codehaus.groovy.tools.RootLoader).

Typically classloaders delegate their parents to load a class. Only if the parent fails the classloader tries to load the class from its classpath. implications:

  • when a spring-web class tries to access slf4j-api, it is the RootLoader that loads the class (not GroovyClassLoader)
  • when slf4j is initialized, it looks for implementation jars in its classloader classpath (RootLoader) that has no knowledge of slf4j-nop
  • slf4j defaults to NOP

When we add the additional configuration @GrabConfig(systemClassLoader=true) systemClassLoader doc, we instruct to put the grapes dependencies on the RootLoader as well:

Set to true if you want to use the system classloader when loading the grape. This is normally only required when a core Java class needs to reference the grabbed classes, e.g. for a database driver accessed using DriverManager.

@GrabConfig(systemClassLoader=true)
@Grab('org.springframework:spring-web:5.3.18')
@Grab('org.slf4j:slf4j-nop:1.7.36')
import org.springframework.web.client.RestTemplate

new RestTemplate().getForObject('http://www.example.com', String)

def printClassPath(classLoader) {
  println classLoader.class
  try{
    classLoader.getURLs().each {url-> println "- ${url.toString()}" }
    if (classLoader.parent) { printClassPath(classLoader.parent) }
  } catch(Exception e){
    println "$classLoader ignored"
  }
}
printClassPath this.class.classLoader

output:

- [removed lines for readability]
class groovy.lang.GroovyClassLoader
- file:/Users/[...]/.groovy/grapes/org.slf4j/slf4j-nop/jars/slf4j-nop-1.7.36.jar
- file:/Users/[...]/.groovy/grapes/org.slf4j/slf4j-api/jars/slf4j-api-1.7.36.jar
- file:/Users/[...]/.groovy/grapes/org.springframework/spring-web/jars/spring-web-5.3.18.jar
- [removed lines for readability]
class org.codehaus.groovy.tools.RootLoader
- file:/Users/[...]/bin/groovy-3.0.13/lib/slf4j-api-1.7.32.jar
- file:/Users/[...]/.groovy/grapes/org.slf4j/slf4j-nop/jars/slf4j-nop-1.7.36.jar
- file:/Users/[...]/.groovy/grapes/org.slf4j/slf4j-api/jars/slf4j-api-1.7.36.jar
- file:/Users/[...]/.groovy/grapes/org.springframework/spring-web/jars/spring-web-5.3.18.jar
- [removed lines for readability]

slf4j-api now can find an SLF4J binding on the classpath.

WARNING note: it's worth mentioning that there are now 2 versions of slf4j-api in the RootLoader and wether slf4j-api-1.7.32 or slf4j-api-1.7.36 is loaded into memory is not necessarily known. It may depend on classloader implementaiton, on the jvm implementation or even the OS.
Also if the choice of which classes are loaded in your environment is deterministic, it remains an implementation detail that may change on different systems or with a platform upgrade.
I would not suggest to keep 2 versions of the same classes into the same classpath for a reliable production environment.