Add DeferredLogFactory support

Add a new `DeferredLogFactory` interface and `DeferredLogs`
implementation that can be used when a `DeferredLog` instance is needed
but the `switchOver` method should be handled elsewhere.

This interface has primarily been added so `EnvironmentPostProcessor`
classes will no longer need to implement `ApplicationEventListener`
just to switch over their logs.

Closes gh-22496
This commit is contained in:
Phillip Webb 2020-07-13 21:17:46 -07:00
parent 9e9eb90d09
commit 039fbdfa7c
6 changed files with 331 additions and 14 deletions

View File

@ -17,11 +17,15 @@
package org.springframework.boot.logging;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;
/**
* Deferred {@link Log} that can be used to store messages that shouldn't be written until
* the logging system is fully initialized.
@ -33,7 +37,29 @@ public class DeferredLog implements Log {
private volatile Log destination;
private final List<Line> lines = new ArrayList<>();
private final Supplier<Log> destinationSupplier;
private final Lines lines;
/**
* Create a new {@link DeferredLog} instance.
*/
public DeferredLog() {
this.destinationSupplier = null;
this.lines = new Lines();
}
/**
* Create a new {@link DeferredLog} instance managed by a {@link DeferredLogFactory}.
* @param destination the switch-over destination
* @param lines the lines backing all related deferred logs
* @since 2.4.0
*/
DeferredLog(Supplier<Log> destination, Lines lines) {
Assert.notNull(destination, "Destination must not be null");
this.destinationSupplier = destination;
this.lines = lines;
}
@Override
public boolean isTraceEnabled() {
@ -143,11 +169,15 @@ public class DeferredLog implements Log {
logTo(this.destination, level, message, t);
}
else {
this.lines.add(new Line(level, message, t));
this.lines.add(this.destinationSupplier, level, message, t);
}
}
}
void switchOver() {
this.destination = this.destinationSupplier.get();
}
/**
* Switch from deferred logging to immediate logging to the specified destination.
* @param destination the new log destination
@ -213,7 +243,7 @@ public class DeferredLog implements Log {
return destination;
}
private static void logTo(Log log, LogLevel level, Object message, Throwable throwable) {
static void logTo(Log log, LogLevel level, Object message, Throwable throwable) {
switch (level) {
case TRACE:
log.trace(message, throwable);
@ -235,7 +265,28 @@ public class DeferredLog implements Log {
}
}
private static class Line {
static class Lines implements Iterable<Line> {
private final List<Line> lines = new ArrayList<>();
void add(Supplier<Log> destinationSupplier, LogLevel level, Object message, Throwable throwable) {
this.lines.add(new Line(destinationSupplier, level, message, throwable));
}
void clear() {
this.lines.clear();
}
@Override
public Iterator<Line> iterator() {
return this.lines.iterator();
}
}
static class Line {
private final Supplier<Log> destinationSupplier;
private final LogLevel level;
@ -243,12 +294,17 @@ public class DeferredLog implements Log {
private final Throwable throwable;
Line(LogLevel level, Object message, Throwable throwable) {
Line(Supplier<Log> destinationSupplier, LogLevel level, Object message, Throwable throwable) {
this.destinationSupplier = destinationSupplier;
this.level = level;
this.message = message;
this.throwable = throwable;
}
Log getDestination() {
return this.destinationSupplier.get();
}
LogLevel getLevel() {
return this.level;
}

View File

@ -0,0 +1,63 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.logging;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Factory that can be used to create multiple {@link DeferredLog} instances that will
* switch over when appropriate.
*
* @author Phillip Webb
* @since 2.4.0
* @see DeferredLogs
*/
@FunctionalInterface
public interface DeferredLogFactory {
/**
* Create a new {@link DeferredLog} for the given destination.
* @param destination the ultimate log destination
* @return a deferred log instance that will switch to the destination when
* appropriate.
*/
default Log getLog(Class<?> destination) {
return getLog(() -> LogFactory.getLog(destination));
}
/**
* Create a new {@link DeferredLog} for the given destination.
* @param destination the ultimate log destination
* @return a deferred log instance that will switch to the destination when
* appropriate.
*/
default Log getLog(Log destination) {
return getLog(() -> destination);
}
/**
* Create a new {@link DeferredLog} for the given destination.
* @param destination the ultimate log destination
* @return a deferred log instance that will switch to the destination when
* appropriate.
*/
Log getLog(Supplier<Log> destination);
}

View File

@ -0,0 +1,95 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.logging;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.logging.DeferredLog.Line;
import org.springframework.boot.logging.DeferredLog.Lines;
/**
* A {@link DeferredLogFactory} implementation that manages a collection
* {@link DeferredLog} instances.
*
* @author Phillip Webb
* @since 2.4.0
*/
public class DeferredLogs implements DeferredLogFactory {
private final Lines lines = new Lines();
private final List<DeferredLog> loggers = new ArrayList<>();
/**
* Create a new {@link DeferredLog} for the given destination.
* @param destination the ultimate log destination
* @return a deferred log instance that will switch to the destination when
* appropriate.
*/
@Override
public Log getLog(Class<?> destination) {
return getLog(() -> LogFactory.getLog(destination));
}
/**
* Create a new {@link DeferredLog} for the given destination.
* @param destination the ultimate log destination
* @return a deferred log instance that will switch to the destination when
* appropriate.
*/
@Override
public Log getLog(Log destination) {
return getLog(() -> destination);
}
/**
* Create a new {@link DeferredLog} for the given destination.
* @param destination the ultimate log destination
* @return a deferred log instance that will switch to the destination when
* appropriate.
*/
@Override
public Log getLog(Supplier<Log> destination) {
synchronized (this.lines) {
DeferredLog logger = new DeferredLog(destination, this.lines);
this.loggers.add(logger);
return logger;
}
}
/**
* Switch over all deferred logs to their supplied destination.
*/
public void switchOverAll() {
synchronized (this.lines) {
for (Line line : this.lines) {
DeferredLog.logTo(line.getDestination(), line.getLevel(), line.getMessage(), line.getThrowable());
}
for (DeferredLog logger : this.loggers) {
logger.switchOver();
}
this.lines.clear();
}
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.logging;
import org.apache.commons.logging.Log;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link DeferredLogFactory}.
*
* @author Phillip Webb
*/
class DeferredLogFactoryTests {
private DeferredLogFactory factory = (supplier) -> this.log = supplier.get();
private Log log;
@Test
void getLogFromClassCreatesLogSupplier() {
this.factory.getLog(DeferredLogFactoryTests.class);
assertThat(this.log).isNotNull();
}
@Test
void getLogFromDestinationCreatesLogSupplier() {
Log log = mock(Log.class);
this.factory.getLog(log);
assertThat(this.log).isSameAs(log);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,11 +16,10 @@
package org.springframework.boot.logging;
import java.util.List;
import org.apache.commons.logging.Log;
import org.junit.jupiter.api.Test;
import org.springframework.boot.logging.DeferredLog.Lines;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
@ -171,21 +170,16 @@ class DeferredLogTests {
verifyNoInteractions(log2);
}
@SuppressWarnings("unchecked")
@Test
void switchTo() {
List<String> lines = (List<String>) ReflectionTestUtils.getField(this.deferredLog, "lines");
Lines lines = (Lines) ReflectionTestUtils.getField(this.deferredLog, "lines");
assertThat(lines).isEmpty();
this.deferredLog.error(this.message, this.throwable);
assertThat(lines).hasSize(1);
this.deferredLog.switchTo(this.log);
assertThat(lines).isEmpty();
this.deferredLog.info("Message2");
assertThat(lines).isEmpty();
verify(this.log).error(this.message, this.throwable);
verify(this.log).info("Message2", null);
}

View File

@ -0,0 +1,60 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.logging;
import org.apache.commons.logging.Log;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link DeferredLogs}.
*
* @author Phillip Webb
*/
class DeferredLogsTests {
@Test
void switchOverAllSwitchesLoggersWithOrderedOutput() {
Log log1 = mock(Log.class);
Log log2 = mock(Log.class);
DeferredLogs loggers = new DeferredLogs();
Log dlog1 = loggers.getLog(log1);
Log dlog2 = loggers.getLog(log2);
dlog1.info("a");
dlog2.info("b");
dlog1.info("c");
dlog2.info("d");
verifyNoInteractions(log1, log2);
loggers.switchOverAll();
InOrder ordered = inOrder(log1, log2);
ordered.verify(log1).info("a", null);
ordered.verify(log2).info("b", null);
ordered.verify(log1).info("c", null);
ordered.verify(log2).info("d", null);
verifyNoMoreInteractions(log1, log2);
dlog1.info("e");
dlog2.info("f");
ordered.verify(log1).info("e", null);
ordered.verify(log2).info("f", null);
}
}