Skip to content

Improve documentation about swapping one technical starter for another #20408

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
mauromol opened this issue Mar 6, 2020 · 3 comments
Closed
Assignees
Labels
type: documentation A documentation update
Milestone

Comments

@mauromol
Copy link

mauromol commented Mar 6, 2020

I'm new to Spring Boot.
I created an application with https://start.spring.io/, using Gradle, Java 11, Spring Boot 2.2.5.
Reading the reference documentation here and here, I understand that to replace the Logback implementation with the Log4j2 one I should simply add the spring-boot-starter-log4j2 starter. So, my Gradle build script looks like this:

dependencies {
	implementation platform('org.springframework.boot:spring-boot-dependencies:2.2.5.RELEASE')
	implementation 'org.springframework.boot:spring-boot-starter'
	implementation 'org.springframework.boot:spring-boot-starter-log4j2'
}

Please note that I'm not using the Spring dependency plugin, but rather the builtin BOM support from recent Gradle.

However, when I start my application, I get the following:

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/C:/Users/mmolinari/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-slf4j-impl/2.12.1/14973e22497adaf0196d481fb99c5dc2a0b58d41/log4j-slf4j-impl-2.12.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/C:/Users/mmolinari/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.2.3/7c4f3c474fb2c041d8028740440937705ebb473a/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory]
Exception in thread "main" java.lang.ExceptionInInitializerError
	at it.dcssrl.myapp.CoreApplication.main(CoreApplication.java:25)
Caused by: org.apache.logging.log4j.LoggingException: log4j-slf4j-impl cannot be present with log4j-to-slf4j
	at org.apache.logging.slf4j.Log4jLoggerFactory.validateContext(Log4jLoggerFactory.java:49)
	at org.apache.logging.slf4j.Log4jLoggerFactory.newLogger(Log4jLoggerFactory.java:39)
	at org.apache.logging.slf4j.Log4jLoggerFactory.newLogger(Log4jLoggerFactory.java:30)
	at org.apache.logging.log4j.spi.AbstractLoggerAdapter.getLogger(AbstractLoggerAdapter.java:54)
	at org.apache.logging.slf4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:30)
	at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:363)
	at org.apache.commons.logging.LogAdapter$Slf4jAdapter.createLocationAwareLog(LogAdapter.java:130)
	at org.apache.commons.logging.LogAdapter.createLog(LogAdapter.java:91)
	at org.apache.commons.logging.LogFactory.getLog(LogFactory.java:67)
	at org.apache.commons.logging.LogFactory.getLog(LogFactory.java:59)
	at org.springframework.boot.SpringApplication.<clinit>(SpringApplication.java:196)
	... 1 more

Searching on the Internet I could not find that the documented way to do this is put in a completely different page of the reference documentation in the how-to guides. I just discovered it by chance by looking at the "similar issues" list suggested by GitHub, while creating this issue. If at least a hint/link from the previous paragraph to the how-to guide were present, it would have saved me quite a lot of time.

I was also wondering whether a dependency substitution could be a better alternative:

configurations.all {
	resolutionStrategy.dependencySubstitution.all { DependencySubstitution dependency ->
		if (dependency.requested instanceof ModuleComponentSelector && dependency.requested.module == 'spring-boot-starter-logging') {
			dependency.useTarget('org.springframework.boot:spring-boot-starter-log4j2', 'we use Log4j2 instead of Logback')
		}
	}
}

Gradle documentation, in fact, pushes against the use of generic "exclusions", which are poor in semantics.

Another glitch with the documentation about logging is that in the dedicated paragraph, after talking about the file rotation logging properties a note says:

Logging properties are independent of the actual logging infrastructure.

However, in a subsequent paragraph a table shows that most of the logging properties can be used only with the default Logback setup, including those regarding file rotation... So, I would not say they are so "independent" :-)

@wilkinsona
Copy link
Member

I understand that to replace the Logback implementation with the Log4j2 one I should simply add the spring-boot-starter-log4j2 starter.

The documentation says that spring-boot-starter-logging is the default starter and that spring-boot-starter-log4j2 is an alternative. Our hope was that this would point users in the direction of replace the former with the latter rather than just adding the latter. However, that hasn't been the case here. That may not be helped by the spring-boot-starter-logging dependency being transitive so it's not clear from looking at your build.gradle alone.

I think we could improve things by adding a paragraph beneath the table of technical starters that links to the how-to sections that describe how to swap one for another.

I was also wondering whether a dependency substitution could be a better alternative

I think this is a good idea, thank you. We could do the same in the section on using another web server as well.

@wilkinsona wilkinsona changed the title Improve documentation about the use of Log4j2 instead of Logback Improve documentation about swapping one technical starter for another Mar 6, 2020
@wilkinsona wilkinsona added type: documentation A documentation update and removed status: waiting-for-triage An issue we've not yet triaged labels Mar 6, 2020
@wilkinsona wilkinsona added this to the 2.2.x milestone Mar 6, 2020
@mauromol
Copy link
Author

mauromol commented Mar 6, 2020

The documentation says that spring-boot-starter-logging is the default starter and that spring-boot-starter-log4j2 is an alternative. Our hope was that this would point users in the direction of replace the former with the latter rather than just adding the latter. However, that hasn't been the case here. That may not be helped by the spring-boot-starter-logging dependency being transitive so it's not clear from looking at your build.gradle alone.

Exactly: spring-boot-starter-logging is a transitive dependency, required (and not optional) by other starters. It's a bit different from other alternative starters that the user is supposed to add exclusively. Here one of the two alternatives is a "default one", which is brought in transparently to the Boot user. Honestly, knowing about Boot magics, I expected that adding spring-boot-starter-log4j2 could in some way automatically "disable" the default logging starter. I'm not sure whether there's a way to express this with Maven metadata (optional, "conflucts-with", etc.), while I'm pretty sure it's possible with Gradle metadata, but I understand you need to keep compatibility with Maven as well. Now that I know this, I understand the sentence "Spring Boot also includes the following starters that can be used if you want to exclude or swap specific technical facets" above Table 3, but I wasn't smart enough to understand the whole picture at a first sight.

So yes, IMHO at least a hint/link to the how-to guide from those first documentation pages would be really useful.

@mauromol
Copy link
Author

@wilkinsona if you're interested in following the "dependency substitution" idea, please note that, as explained at gradle/gradle#12459 (comment), I just found that a fully working solution by now (Gradle 6.5) is to also include the dependency version in the target definition:

configurations.all {
  resolutionStrategy.dependencySubstitution.all { DependencySubstitution dependency ->
    if (dependency.requested instanceof ModuleComponentSelector && dependency.requested.module == 'spring-boot-starter-logging')
      dependency.useTarget("org.springframework.boot:spring-boot-starter-log4j2:$dependency.requested.version", 'we use Log4j2 instead of Logback')
  }
}

Otherwise although some things work, when Gradle tries to resolve the runtimeClasspath it will fail complaining that the target module version has not been specified. Here we're lucky that all Spring Boot components use the same version, so the above will request org.springframework.boot:spring-boot-starter-log4j2 to be the same version of the requested (and substituted) org.springframework.boot:spring-boot-starter-logging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: documentation A documentation update
Projects
None yet
Development

No branches or pull requests

3 participants