import org.springframework.boot.SpringApplication; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.logging.LoggingApplicationListener; import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.logging.logback.LogbackLoggingSystem; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEvent; import org.springframework.context.event.GenericApplicationListener; import org.springframework.core.ResolvableType; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; import static org.springframework.boot.logging.LoggingApplicationListener.CONFIG_PROPERTY; /** * Load {@literal xxx-slog-logback.xml} using {@literal logging.config} if {@literal logging.config} is not set and no * logback configuration file is found. *

* Unfortunately, Spring does not allow a "starter" to ship a logback configuration file that would be loaded by default * if none is provided by the application. By running this listener just before {@link LoggingApplicationListener}, we * are able to inspect the logback configuration and use {@literal logging.config} to pass our own if none is detected. *

* This is hacky, but does the job. Getting ride of this listener would mean that we cannot ship our default * configuration with the starter and require from our user to create a {@literal logback.xml} file (which would include * our configuration). Doable but not a great developer experience. */ public class SLogApplicationListener implements GenericApplicationListener { private static final Class[] EVENT_TYPES = {ApplicationEnvironmentPreparedEvent.class}; private static final Class[] SOURCE_TYPES = {SpringApplication.class, ApplicationContext.class}; @Override public int getOrder() { return LoggingApplicationListener.DEFAULT_ORDER - 1; } @Override public boolean supportsEventType(ResolvableType eventType) { return isAssignableFrom(eventType.getRawClass(), EVENT_TYPES); } @Override public boolean supportsSourceType(Class sourceType) { return isAssignableFrom(sourceType, SOURCE_TYPES); } private boolean isAssignableFrom(Class type, Class... supportedTypes) { return Arrays.stream(supportedTypes).anyMatch(supportedType -> supportedType.isAssignableFrom(type)); } @Override public void onApplicationEvent(ApplicationEvent event) { if (event instanceof ApplicationEnvironmentPreparedEvent) { ApplicationEnvironmentPreparedEvent aepEvent = (ApplicationEnvironmentPreparedEvent) event; overrideLogbackConfig(aepEvent); } } private void overrideLogbackConfig(ApplicationEnvironmentPreparedEvent event) { ConfigurableEnvironment env = event.getEnvironment(); String logConfig = env.getProperty(CONFIG_PROPERTY); if (StringUtils.hasLength(logConfig)) { return; // Never override user supplied logging.config } // Must be super careful to not initialize the LoggingSystem (ctor does nothing) ClassLoader classLoader = event.getSpringApplication().getClassLoader(); LogbackLoggingSystem logbackLoggingSystem = getLogbackLoggingSystem(classLoader); if (logbackLoggingSystem == null) { return; // Logback is not active, don't touch anything } if (isLogbackConfPresent(logbackLoggingSystem)) { return; // A config file at well known location has been found, don't touch anything } setConfig(env.getPropertySources()); } private LogbackLoggingSystem getLogbackLoggingSystem(ClassLoader classLoader) { LoggingSystem loggingSystem = LoggingSystem.get(classLoader); if (loggingSystem instanceof LogbackLoggingSystem) { return (LogbackLoggingSystem) loggingSystem; } return null; } private boolean isLogbackConfPresent(LogbackLoggingSystem loggingSystem) { Method method = ReflectionUtils.findMethod(LogbackLoggingSystem.class, "getSpringInitializationConfig"); method.setAccessible(true); try { String ret = (String) method.invoke(loggingSystem); return ret != null; } catch (IllegalAccessException | InvocationTargetException e) { return false; } } private void setConfig(MutablePropertySources sources) { MapPropertySource mapPropertySource = new MapPropertySource( "xxx-slog-logging-config", Collections.singletonMap(CONFIG_PROPERTY, "classpath:xxx-slog-logback.xml")); sources.addLast(mapPropertySource); } }