From 634933b3e5c5f83b2f6318d61deca070110e3ccb Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 8 Sep 2025 14:52:00 +0200 Subject: [PATCH] Polish nullability annotations See gh-46926 --- .../boot/json/JsonValueWriter.java | 7 +-- .../springframework/boot/json/JsonWriter.java | 47 ++++++++++--------- ...tendedLogFormatStructuredLogFormatter.java | 13 +++-- .../LogstashStructuredLogFormatter.java | 7 ++- ...ticCommonSchemaStructuredLogFormatter.java | 5 +- ...tendedLogFormatStructuredLogFormatter.java | 9 ++-- .../LogstashStructuredLogFormatter.java | 7 +-- .../boot/logging/structured/ContextPairs.java | 42 +++++++++++------ .../ElasticCommonSchemaProperties.java | 1 + .../GraylogExtendedLogFormatProperties.java | 2 + .../springframework/boot/util/LambdaSafe.java | 19 ++++---- .../SampleJsonMembersCustomizer.java | 7 ++- 12 files changed, 103 insertions(+), 63 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/json/JsonValueWriter.java b/core/spring-boot/src/main/java/org/springframework/boot/json/JsonValueWriter.java index 2bda55d8633..1f4ba0f28f8 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/json/JsonValueWriter.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/json/JsonValueWriter.java @@ -149,7 +149,7 @@ class JsonValueWriter { } } - private boolean canWriteAsArray(Iterable iterable) { + private boolean canWriteAsArray(Iterable iterable) { return !(iterable instanceof Path); } @@ -349,7 +349,8 @@ class JsonValueWriter { // Lambda isn't detected with the correct nullability @SuppressWarnings({ "unchecked", "NullAway" }) private @Nullable V processValue(@Nullable V value, ValueProcessor valueProcessor) { - return (V) LambdaSafe.callback(ValueProcessor.class, valueProcessor, this.path, new Object[] { value }) + return (V) LambdaSafe + .callback(ValueProcessor.class, valueProcessor, this.path, new @Nullable Object[] { value }) .invokeAnd((call) -> call.processValue(this.path, value)) .get(value); } @@ -389,7 +390,7 @@ class JsonValueWriter { private int index; - private Set names = new HashSet<>(); + private final Set names = new HashSet<>(); private ActiveSeries(Series series) { this.series = series; diff --git a/core/spring-boot/src/main/java/org/springframework/boot/json/JsonWriter.java b/core/spring-boot/src/main/java/org/springframework/boot/json/JsonWriter.java index 78b2e12561f..d98ebe47877 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/json/JsonWriter.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/json/JsonWriter.java @@ -29,6 +29,7 @@ import java.util.function.Predicate; import java.util.function.Supplier; import java.util.function.UnaryOperator; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.springframework.boot.json.JsonValueWriter.Series; @@ -118,9 +119,9 @@ public interface JsonWriter { * Return a new {@link JsonWriter} instance that appends the given suffix after the * JSON has been written. * @param suffix the suffix to write, if any - * @return a new {@link JsonWriter} instance that appends a suffixafter the JSON + * @return a new {@link JsonWriter} instance that appends a suffix after the JSON */ - default JsonWriter withSuffix(String suffix) { + default JsonWriter withSuffix(@Nullable String suffix) { if (!StringUtils.hasLength(suffix)) { return this; } @@ -221,7 +222,7 @@ public interface JsonWriter { * @param supplier a supplier of the value * @return the added {@link Member} which may be configured further */ - public Member add(String name, Supplier supplier) { + public Member add(String name, Supplier<@Nullable V> supplier) { Assert.notNull(supplier, "'supplier' must not be null"); return add(name, (instance) -> supplier.get()); } @@ -233,7 +234,7 @@ public interface JsonWriter { * @param extractor {@link Extractor} to extract the value * @return the added {@link Member} which may be configured further */ - public Member add(String name, Extractor extractor) { + public Member add(String name, Extractor extractor) { Assert.notNull(name, "'name' must not be null"); Assert.notNull(extractor, "'extractor' must not be null"); return addMember(name, extractor); @@ -268,7 +269,7 @@ public interface JsonWriter { * @param value the member value * @return the added {@link Member} which may be configured further */ - public Member from(V value) { + public Member from(@Nullable V value) { return from((instance) -> value); } @@ -279,7 +280,7 @@ public interface JsonWriter { * @param supplier a supplier of the value * @return the added {@link Member} which may be configured further */ - public Member from(Supplier supplier) { + public Member from(Supplier<@Nullable V> supplier) { Assert.notNull(supplier, "'supplier' must not be null"); return from((instance) -> supplier.get()); } @@ -323,7 +324,7 @@ public interface JsonWriter { this.jsonProcessors.valueProcessors().add(valueProcessor); } - private Member addMember(@Nullable String name, Extractor extractor) { + private Member addMember(@Nullable String name, Extractor extractor) { Member member = new Member<>(this.members.size(), name, ValueExtractor.of(extractor)); this.members.add(member); return member; @@ -396,7 +397,7 @@ public interface JsonWriter { * @param extractor an function used to extract the value to test * @return a {@link Member} which may be configured further */ - public Member whenNotNull(Function extractor) { + public Member whenNotNull(Function<@Nullable T, ?> extractor) { Assert.notNull(extractor, "'extractor' must not be null"); return when((instance) -> Objects.nonNull(extractor.apply(instance))); } @@ -417,7 +418,8 @@ public interface JsonWriter { * @return a {@link Member} which may be configured further */ public Member whenNotEmpty() { - return whenNot(ObjectUtils::isEmpty); + Predicate<@Nullable T> isEmpty = ObjectUtils::isEmpty; + return whenNot(isEmpty); } /** @@ -425,7 +427,7 @@ public interface JsonWriter { * @param predicate the predicate to test * @return a {@link Member} which may be configured further */ - public Member whenNot(Predicate predicate) { + public Member whenNot(Predicate<@Nullable T> predicate) { Assert.notNull(predicate, "'predicate' must not be null"); return when(predicate.negate()); } @@ -448,7 +450,7 @@ public interface JsonWriter { * @return a {@link Member} which may be configured further */ @SuppressWarnings("unchecked") - public Member as(Extractor extractor) { + public Member as(Extractor extractor) { Assert.notNull(extractor, "'adapter' must not be null"); Member result = (Member) this; result.valueExtractor = this.valueExtractor.as(extractor::extract); @@ -688,7 +690,7 @@ public interface JsonWriter { * @param the member type */ @FunctionalInterface - interface ValueExtractor { + interface ValueExtractor { /** * Represents a skipped value. @@ -722,7 +724,7 @@ public interface JsonWriter { * @param extractor the extractor to use * @return a new {@link ValueExtractor} */ - default ValueExtractor as(Extractor extractor) { + default ValueExtractor as(Extractor extractor) { return (instance) -> apply(extract(instance), extractor); } @@ -946,7 +948,7 @@ public interface JsonWriter { * @param existingName the existing and possibly already processed name. * @return the new name */ - String processName(MemberPath path, String existingName); + @Nullable String processName(MemberPath path, String existingName); /** * Factory method to create a new {@link NameProcessor} for the given operation. @@ -969,7 +971,7 @@ public interface JsonWriter { * @param the value type */ @FunctionalInterface - interface ValueProcessor { + interface ValueProcessor { /** * Process the value at the given path. @@ -977,7 +979,7 @@ public interface JsonWriter { * @param value the value being written (may be {@code null}) * @return the processed value */ - T processValue(MemberPath path, T value); + @Nullable T processValue(MemberPath path, @Nullable T value); /** * Return a new processor from this one that only applied to members with the @@ -1018,7 +1020,8 @@ public interface JsonWriter { * type. */ default ValueProcessor whenInstanceOf(Class type) { - return when(type::isInstance); + Predicate<@Nullable T> isInstance = type::isInstance; + return when(isInstance); } /** @@ -1028,7 +1031,7 @@ public interface JsonWriter { * @return a new {@link ValueProcessor} that only applies when the predicate * matches */ - default ValueProcessor when(Predicate predicate) { + default ValueProcessor when(Predicate<@Nullable T> predicate) { return (name, value) -> (predicate.test(value)) ? processValue(name, value) : value; } @@ -1040,7 +1043,7 @@ public interface JsonWriter { * @param action the action to apply * @return a new {@link ValueProcessor} instance */ - static ValueProcessor of(Class type, UnaryOperator action) { + static ValueProcessor of(Class type, UnaryOperator<@Nullable T> action) { return of(action).whenInstanceOf(type); } @@ -1051,7 +1054,7 @@ public interface JsonWriter { * @param action the action to apply * @return a new {@link ValueProcessor} instance */ - static ValueProcessor of(UnaryOperator action) { + static ValueProcessor of(UnaryOperator<@Nullable T> action) { Assert.notNull(action, "'action' must not be null"); return (name, value) -> action.apply(value); } @@ -1065,14 +1068,14 @@ public interface JsonWriter { * @param the result type */ @FunctionalInterface - interface Extractor { + interface Extractor { /** * Extract from the given value. * @param value the source value (never {@code null}) * @return an extracted value or {@code null} */ - @Nullable R extract(T value); + @Nullable R extract(@NonNull T value); } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/GraylogExtendedLogFormatStructuredLogFormatter.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/GraylogExtendedLogFormatStructuredLogFormatter.java index c3c49792f9a..f07b46e6ba4 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/GraylogExtendedLogFormatStructuredLogFormatter.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/GraylogExtendedLogFormatStructuredLogFormatter.java @@ -18,7 +18,8 @@ package org.springframework.boot.logging.log4j2; import java.math.BigDecimal; import java.util.Set; -import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Predicate; import java.util.regex.Pattern; import org.apache.commons.logging.Log; @@ -37,6 +38,7 @@ import org.springframework.boot.json.WritableJson; import org.springframework.boot.logging.StackTracePrinter; import org.springframework.boot.logging.structured.CommonStructuredLogFormat; import org.springframework.boot.logging.structured.ContextPairs; +import org.springframework.boot.logging.structured.ContextPairs.Joiner; import org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties; import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter; import org.springframework.boot.logging.structured.StructuredLogFormatter; @@ -91,12 +93,15 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure members.add("_process_thread_name", LogEvent::getThreadName); GraylogExtendedLogFormatProperties.get(environment).jsonMembers(members); members.add("_log_logger", LogEvent::getLoggerName); + Predicate<@Nullable ReadOnlyStringMap> mapIsEmpty = (map) -> map == null || map.isEmpty(); members.from(LogEvent::getContextData) - .whenNot(ReadOnlyStringMap::isEmpty) + .whenNot(mapIsEmpty) .usingPairs(contextPairs.flat(additionalFieldJoiner(), GraylogExtendedLogFormatStructuredLogFormatter::addContextDataPairs)); + Function<@Nullable LogEvent, @Nullable Object> getThrownProxy = (event) -> (event != null) + ? event.getThrownProxy() : null; members.add() - .whenNotNull(LogEvent::getThrownProxy) + .whenNotNull(getThrownProxy) .usingMembers((thrownProxyMembers) -> throwableMembers(thrownProxyMembers, extractor)); } @@ -140,7 +145,7 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure contextPairs.add((contextData, pairs) -> contextData.forEach(pairs::accept)); } - private static BinaryOperator<@Nullable String> additionalFieldJoiner() { + private static Joiner additionalFieldJoiner() { return (prefix, name) -> { name = prefix + name; if (!FIELD_NAME_VALID_PATTERN.matcher(name).matches()) { diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/LogstashStructuredLogFormatter.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/LogstashStructuredLogFormatter.java index cbd2d70254f..82985179071 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/LogstashStructuredLogFormatter.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/LogstashStructuredLogFormatter.java @@ -21,6 +21,7 @@ import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Set; import java.util.TreeSet; +import java.util.function.Predicate; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Marker; @@ -61,13 +62,15 @@ class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter mapIsEmpty = (map) -> map == null || map.isEmpty(); members.from(LogEvent::getContextData) - .whenNot(ReadOnlyStringMap::isEmpty) + .whenNot(mapIsEmpty) .usingPairs(contextPairs.flat("_", LogstashStructuredLogFormatter::addContextDataPairs)); + Predicate<@Nullable Set> collectionIsEmpty = CollectionUtils::isEmpty; members.add("tags", LogEvent::getMarker) .whenNotNull() .as(LogstashStructuredLogFormatter::getMarkers) - .whenNot(CollectionUtils::isEmpty); + .whenNot(collectionIsEmpty); members.add("stack_trace", LogEvent::getThrownProxy).whenNotNull().as(extractor::stackTrace); } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatter.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatter.java index e458a77e42d..0aa703ed98b 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatter.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatter.java @@ -20,6 +20,7 @@ import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.TreeSet; +import java.util.function.Function; import ch.qos.logback.classic.pattern.ThrowableProxyConverter; import ch.qos.logback.classic.spi.ILoggingEvent; @@ -77,7 +78,9 @@ class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogF pairs.addMapEntries(ILoggingEvent::getMDCPropertyMap); pairs.add(ILoggingEvent::getKeyValuePairs, keyValuePairExtractor); })); - members.add().whenNotNull(ILoggingEvent::getThrowableProxy).usingMembers((throwableMembers) -> { + Function<@Nullable ILoggingEvent, @Nullable Object> getThrowableProxy = (event) -> (event != null) + ? event.getThrowableProxy() : null; + members.add().whenNotNull(getThrowableProxy).usingMembers((throwableMembers) -> { throwableMembers.add("error").usingMembers((error) -> { error.add("type", ILoggingEvent::getThrowableProxy).as(IThrowableProxy::getClassName); error.add("message", ILoggingEvent::getThrowableProxy).as(IThrowableProxy::getMessage); diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/GraylogExtendedLogFormatStructuredLogFormatter.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/GraylogExtendedLogFormatStructuredLogFormatter.java index d0d1c5fd532..ff1a2c316f8 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/GraylogExtendedLogFormatStructuredLogFormatter.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/GraylogExtendedLogFormatStructuredLogFormatter.java @@ -18,7 +18,7 @@ package org.springframework.boot.logging.logback; import java.math.BigDecimal; import java.util.Set; -import java.util.function.BinaryOperator; +import java.util.function.Function; import java.util.regex.Pattern; import ch.qos.logback.classic.pattern.ThrowableProxyConverter; @@ -37,6 +37,7 @@ import org.springframework.boot.json.WritableJson; import org.springframework.boot.logging.StackTracePrinter; import org.springframework.boot.logging.structured.CommonStructuredLogFormat; import org.springframework.boot.logging.structured.ContextPairs; +import org.springframework.boot.logging.structured.ContextPairs.Joiner; import org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties; import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter; import org.springframework.boot.logging.structured.StructuredLogFormatter; @@ -100,8 +101,10 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure pairs.addMapEntries(ILoggingEvent::getMDCPropertyMap); pairs.add(ILoggingEvent::getKeyValuePairs, keyValuePairExtractor); })); + Function<@Nullable ILoggingEvent, @Nullable Object> getThrowableProxy = (event) -> (event != null) + ? event.getThrowableProxy() : null; members.add() - .whenNotNull(ILoggingEvent::getThrowableProxy) + .whenNotNull(getThrowableProxy) .usingMembers((throwableMembers) -> throwableMembers(throwableMembers, extractor)); } @@ -128,7 +131,7 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure members.add("_error_message", ILoggingEvent::getThrowableProxy).as(IThrowableProxy::getMessage); } - private static BinaryOperator<@Nullable String> additionalFieldJoiner() { + private static Joiner additionalFieldJoiner() { return (prefix, name) -> { name = prefix + name; if (!FIELD_NAME_VALID_PATTERN.matcher(name).matches()) { diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogstashStructuredLogFormatter.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogstashStructuredLogFormatter.java index 3bcbde7df80..d4927b0c4a8 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogstashStructuredLogFormatter.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogstashStructuredLogFormatter.java @@ -24,6 +24,7 @@ import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.function.Function; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.pattern.ThrowableProxyConverter; @@ -76,9 +77,9 @@ class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter event) - .whenNotNull(ILoggingEvent::getThrowableProxy) - .as(extractor::stackTrace); + Function<@Nullable ILoggingEvent, @Nullable Object> getThrowableProxy = (event) -> (event != null) + ? event.getThrowableProxy() : null; + members.add("stack_trace", (event) -> event).whenNotNull(getThrowableProxy).as(extractor::stackTrace); } private static String asTimestamp(Instant instant) { diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java index 977c417446c..7c94f9d5ccf 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java @@ -21,10 +21,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; -import java.util.function.BinaryOperator; import java.util.function.Consumer; import java.util.function.Function; -import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; @@ -69,8 +67,7 @@ public class ContextPairs { * @param pairs callback to add all the pairs * @return a {@link BiConsumer} for use with the {@link JsonWriter} */ - public BiConsumer> flat(BinaryOperator<@Nullable String> joiner, - Consumer> pairs) { + public BiConsumer> flat(Joiner joiner, Consumer> pairs) { return (!this.include) ? none() : new Pairs<>(joiner, pairs)::flat; } @@ -89,7 +86,7 @@ public class ContextPairs { }; } - private BinaryOperator<@Nullable String> joinWith(String delimiter) { + private Joiner joinWith(String delimiter) { return (prefix, name) -> { StringBuilder joined = new StringBuilder(prefix.length() + delimiter.length() + name.length()); joined.append(prefix); @@ -101,6 +98,22 @@ public class ContextPairs { }; } + /** + * Joins a prefix and a name. + */ + @FunctionalInterface + public interface Joiner { + + /** + * Joins the given prefix and name. + * @param prefix the prefix + * @param name the name + * @return the joined result or {@code null} + */ + @Nullable String join(String prefix, String name); + + } + /** * Callback used to add pairs. * @@ -108,11 +121,11 @@ public class ContextPairs { */ public class Pairs { - private final BinaryOperator<@Nullable String> joiner; + private final Joiner joiner; private final List>> addedPairs; - Pairs(BinaryOperator<@Nullable String> joiner, Consumer> pairs) { + Pairs(Joiner joiner, Consumer> pairs) { this.joiner = joiner; this.addedPairs = new ArrayList<>(); pairs.accept(this); @@ -120,11 +133,11 @@ public class ContextPairs { /** * Add pairs from map entries. - * @param the map key type * @param the map value type * @param extractor the extractor used to provide the map */ - public void addMapEntries(Function> extractor) { + @SuppressWarnings("NullAway") // Doesn't detect lambda with correct nullability + public void addMapEntries(Function> extractor) { add(extractor.andThen(Map::entrySet), Map.Entry::getKey, Map.Entry::getValue); } @@ -133,9 +146,8 @@ public class ContextPairs { * @param elementsExtractor the extractor used to provide the iterable * @param pairExtractor the extractor used to provide the name and value * @param the element type - * @param the value type */ - public void add(Function> elementsExtractor, PairExtractor pairExtractor) { + public void add(Function> elementsExtractor, PairExtractor pairExtractor) { add(elementsExtractor, pairExtractor::getName, pairExtractor::getValue); } @@ -147,7 +159,7 @@ public class ContextPairs { * @param nameExtractor the extractor used to provide the name * @param valueExtractor the extractor used to provide the value */ - public void add(Function> elementsExtractor, Function nameExtractor, + public void add(Function> elementsExtractor, Function nameExtractor, Function valueExtractor) { add((item, pairs) -> { Iterable elements = elementsExtractor.apply(item); @@ -186,7 +198,7 @@ public class ContextPairs { for (int i = 0; i < nameParts.size() - 1; i++) { Object existing = destination.computeIfAbsent(nameParts.get(i), (key) -> new LinkedHashMap<>()); if (!(existing instanceof Map)) { - String common = nameParts.subList(0, i + 1).stream().collect(Collectors.joining(".")); + String common = String.join(".", nameParts.subList(0, i + 1)); throw new IllegalStateException( "Duplicate nested pairs added under '%s'".formatted(common)); } @@ -196,12 +208,12 @@ public class ContextPairs { Assert.state(previous == null, () -> "Duplicate nested pairs added under '%s'".formatted(name)); })); }); - result.forEach(pairs::accept); + result.forEach(pairs); } private BiConsumer joining(BiConsumer pairs) { return (name, value) -> { - name = this.joiner.apply(ContextPairs.this.prefix, (name != null) ? name : ""); + name = this.joiner.join(ContextPairs.this.prefix, (name != null) ? name : ""); if (StringUtils.hasLength(name)) { pairs.accept(name, value); } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ElasticCommonSchemaProperties.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ElasticCommonSchemaProperties.java index c645857e3c9..44bd93efe2f 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ElasticCommonSchemaProperties.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ElasticCommonSchemaProperties.java @@ -82,6 +82,7 @@ public record ElasticCommonSchemaProperties(Service service) { static final Service NONE = new Service(null, null, null, null); + @SuppressWarnings("NullAway") // Doesn't detect lambda with correct nullability void jsonMembers(Members members) { members.add("service").usingMembers((service) -> { service.add("name", this::name).whenHasLength(); diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/GraylogExtendedLogFormatProperties.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/GraylogExtendedLogFormatProperties.java index 8494c29ae90..41abfca8163 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/GraylogExtendedLogFormatProperties.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/GraylogExtendedLogFormatProperties.java @@ -58,6 +58,7 @@ public record GraylogExtendedLogFormatProperties(@Nullable String host, Service * Add {@link JsonWriter} members for the service. * @param members the members to add to */ + @SuppressWarnings("NullAway") // Doesn't detect lambda with correct nullability public void jsonMembers(JsonWriter.Members members) { members.add("host", this::host).whenHasLength(); this.service.jsonMembers(members); @@ -90,6 +91,7 @@ public record GraylogExtendedLogFormatProperties(@Nullable String host, Service return new Service(version); } + @SuppressWarnings("NullAway") // Doesn't detect lambda with correct nullability void jsonMembers(JsonWriter.Members members) { members.add("_service_version", this::version).whenHasLength(); } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/util/LambdaSafe.java b/core/spring-boot/src/main/java/org/springframework/boot/util/LambdaSafe.java index 93cff8e8b84..5542f295dd5 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/util/LambdaSafe.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/util/LambdaSafe.java @@ -70,7 +70,7 @@ public final class LambdaSafe { * @return a {@link Callback} instance that can be invoked. */ public static Callback callback(Class callbackType, C callbackInstance, A argument, - Object @Nullable ... additionalArguments) { + @Nullable Object @Nullable ... additionalArguments) { Assert.notNull(callbackType, "'callbackType' must not be null"); Assert.notNull(callbackInstance, "'callbackInstance' must not be null"); return new Callback<>(callbackType, callbackInstance, argument, additionalArguments); @@ -108,13 +108,13 @@ public final class LambdaSafe { private final A argument; - private final Object @Nullable [] additionalArguments; + private final @Nullable Object @Nullable [] additionalArguments; private Log logger; private Filter filter = new GenericTypeFilter<>(); - LambdaSafeCallback(Class callbackType, A argument, Object @Nullable [] additionalArguments) { + LambdaSafeCallback(Class callbackType, A argument, @Nullable Object @Nullable [] additionalArguments) { this.callbackType = callbackType; this.argument = argument; this.additionalArguments = additionalArguments; @@ -175,11 +175,11 @@ public final class LambdaSafe { } private boolean startsWithArgumentClassName(String message) { - Predicate startsWith = (argument) -> startsWithArgumentClassName(message, argument); - return startsWith.test(this.argument) || additonalArgumentsStartsWith(startsWith); + Predicate<@Nullable Object> startsWith = (argument) -> startsWithArgumentClassName(message, argument); + return startsWith.test(this.argument) || additionalArgumentsStartsWith(startsWith); } - private boolean additonalArgumentsStartsWith(Predicate startsWith) { + private boolean additionalArgumentsStartsWith(Predicate<@Nullable Object> startsWith) { if (this.additionalArguments == null) { return false; } @@ -243,7 +243,7 @@ public final class LambdaSafe { private final C callbackInstance; private Callback(Class callbackType, C callbackInstance, A argument, - Object @Nullable [] additionalArguments) { + @Nullable Object @Nullable [] additionalArguments) { super(callbackType, argument, additionalArguments); this.callbackInstance = callbackInstance; } @@ -343,7 +343,8 @@ public final class LambdaSafe { * @param additionalArguments any additional arguments * @return if the callback matches and should be invoked */ - boolean match(Class callbackType, C callbackInstance, A argument, Object @Nullable [] additionalArguments); + boolean match(Class callbackType, C callbackInstance, A argument, + @Nullable Object @Nullable [] additionalArguments); /** * Return a {@link Filter} that allows all callbacks to be invoked. @@ -365,7 +366,7 @@ public final class LambdaSafe { @Override public boolean match(Class callbackType, C callbackInstance, A argument, - Object @Nullable [] additionalArguments) { + @Nullable Object @Nullable [] additionalArguments) { ResolvableType type = ResolvableType.forClass(callbackType, callbackInstance.getClass()); if (type.getGenerics().length != 1) { return true; diff --git a/smoke-test/spring-boot-smoke-test-structured-logging/src/main/java/smoketest/structuredlogging/SampleJsonMembersCustomizer.java b/smoke-test/spring-boot-smoke-test-structured-logging/src/main/java/smoketest/structuredlogging/SampleJsonMembersCustomizer.java index d7140ad8be5..9379e487cf4 100644 --- a/smoke-test/spring-boot-smoke-test-structured-logging/src/main/java/smoketest/structuredlogging/SampleJsonMembersCustomizer.java +++ b/smoke-test/spring-boot-smoke-test-structured-logging/src/main/java/smoketest/structuredlogging/SampleJsonMembersCustomizer.java @@ -16,6 +16,10 @@ package smoketest.structuredlogging; +import java.util.function.UnaryOperator; + +import org.jspecify.annotations.Nullable; + import org.springframework.boot.json.JsonWriter.Members; import org.springframework.boot.json.JsonWriter.ValueProcessor; import org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizer; @@ -24,8 +28,9 @@ public class SampleJsonMembersCustomizer implements StructuredLoggingJsonMembers @Override public void customize(Members members) { + UnaryOperator<@Nullable String> formatted = "!!%s!!"::formatted; members.applyingValueProcessor( - ValueProcessor.of(String.class, "!!%s!!"::formatted).whenHasUnescapedPath("process.thread.name")); + ValueProcessor.of(String.class, formatted).whenHasUnescapedPath("process.thread.name")); } }