diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/DeferredLog.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/DeferredLog.java index 668fe82727..81b440ec65 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/DeferredLog.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/DeferredLog.java @@ -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 lines = new ArrayList<>(); + private final Supplier 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 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 { + + private final List lines = new ArrayList<>(); + + void add(Supplier destinationSupplier, LogLevel level, Object message, Throwable throwable) { + this.lines.add(new Line(destinationSupplier, level, message, throwable)); + } + + void clear() { + this.lines.clear(); + } + + @Override + public Iterator iterator() { + return this.lines.iterator(); + } + + } + + static class Line { + + private final Supplier 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 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; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/DeferredLogFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/DeferredLogFactory.java new file mode 100644 index 0000000000..4341285d90 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/DeferredLogFactory.java @@ -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 destination); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/DeferredLogs.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/DeferredLogs.java new file mode 100644 index 0000000000..902c02ade3 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/DeferredLogs.java @@ -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 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 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(); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/DeferredLogFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/DeferredLogFactoryTests.java new file mode 100644 index 0000000000..dcba31b1db --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/DeferredLogFactoryTests.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/DeferredLogTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/DeferredLogTests.java index 9824b61632..64f75ad501 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/DeferredLogTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/DeferredLogTests.java @@ -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 lines = (List) 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); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/DeferredLogsTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/DeferredLogsTests.java new file mode 100644 index 0000000000..6dc1d88a5b --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/DeferredLogsTests.java @@ -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); + } + +}