diff --git a/docs/core/tracing.md b/docs/core/tracing.md index 883f8db86..a67460e79 100644 --- a/docs/core/tracing.md +++ b/docs/core/tracing.md @@ -419,5 +419,55 @@ Below is an example configuration needed for each test case. } ``` +## Advanced +### Lambda SnapStart priming +The Tracing utility integrates with AWS Lambda SnapStart to improve restore durations. To make sure the SnapStart priming logic of this utility runs correctly, you need an explicit reference to `TracingUtils` in your code to allow the library to register before SnapStart takes a memory snapshot. Learn more about what priming is in this [blog post](https://aws.amazon.com/blogs/compute/optimizing-cold-start-performance-of-aws-lambda-using-advanced-priming-strategies-with-snapstart/){target="_blank"}. + +Make sure to reference `TracingUtils` in your Lambda handler initialization code. This can be done by adding one of the following lines to your handler class: + +=== "Constructor" + + ```java hl_lines="7" + import software.amazon.lambda.powertools.tracing.Tracing; + import software.amazon.lambda.powertools.tracing.TracingUtils; + + public class MyFunctionHandler implements RequestHandler { + + public MyFunctionHandler() { + TracingUtils.prime(); // Ensure TracingUtils is loaded for SnapStart + } + + @Override + @Tracing + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) { + // ... + return something; + } + } + ``` + +=== "Static Initializer" + + ```java hl_lines="7" + import software.amazon.lambda.powertools.tracing.Tracing; + import software.amazon.lambda.powertools.tracing.TracingUtils; + + public class MyFunctionHandler implements RequestHandler { + + static { + TracingUtils.prime(); // Ensure TracingUtils is loaded for SnapStart + } + + @Override + @Tracing + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) { + // ... + return something; + } + } + ``` + +!!! note "Important: Direct TracingUtils reference required" + Using only the `@Tracing` annotation is not sufficient to trigger SnapStart priming. You must have a direct reference to `TracingUtils` in your code (as shown in the examples above) to ensure the CRaC hooks are properly registered. diff --git a/docs/utilities/serialization.md b/docs/utilities/serialization.md index b47bdbd91..28f846f58 100644 --- a/docs/utilities/serialization.md +++ b/docs/utilities/serialization.md @@ -472,3 +472,53 @@ to powertools.You can then use it to do your validation or in idempotency module } } ``` + +## Advanced + +### Lambda SnapStart priming + +The Serialization utility integrates with AWS Lambda SnapStart to improve restore durations. To make sure the SnapStart priming logic of this utility runs correctly, you need an explicit reference to `JsonConfig` in your code to allow the library to register before SnapStart takes a memory snapshot. Learn more about what priming is in this [blog post](https://aws.amazon.com/blogs/compute/optimizing-cold-start-performance-of-aws-lambda-using-advanced-priming-strategies-with-snapstart/){target="_blank"}. + +If you don't set a custom `JsonConfig` in your code yet, make sure to reference `JsonConfig` in your Lambda handler initialization code. This can be done by adding one of the following lines to your handler class: + +=== "Constructor" + + ```java hl_lines="7" + import software.amazon.lambda.powertools.utilities.JsonConfig; + import static software.amazon.lambda.powertools.utilities.EventDeserializer.extractDataFrom; + + public class MyFunctionHandler implements RequestHandler { + + public MyFunctionHandler() { + JsonConfig.get(); // Ensure JsonConfig is loaded for SnapStart + } + + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) { + Product product = extractDataFrom(input).as(Product.class); + // ... + return something; + } + } + ``` + +=== "Static Initializer" + + ```java hl_lines="7" + import software.amazon.lambda.powertools.utilities.JsonConfig; + import static software.amazon.lambda.powertools.utilities.EventDeserializer.extractDataFrom; + + public class MyFunctionHandler implements RequestHandler { + + static { + JsonConfig.get(); // Ensure JsonConfig is loaded for SnapStart + } + + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) { + Product product = extractDataFrom(input).as(Product.class); + // ... + return something; + } + } + ``` diff --git a/powertools-serialization/pom.xml b/powertools-serialization/pom.xml index 7e4e2af15..5ff9a40e4 100644 --- a/powertools-serialization/pom.xml +++ b/powertools-serialization/pom.xml @@ -47,6 +47,14 @@ com.fasterxml.jackson.core jackson-databind + + org.crac + crac + + + software.amazon.lambda + powertools-common + @@ -74,6 +82,11 @@ aws-lambda-java-tests test + + org.mockito + mockito-core + test + @@ -96,6 +109,24 @@ + + generate-classesloaded-file + + + + org.apache.maven.plugins + maven-surefire-plugin + + + -Xlog:class+load=info:classesloaded.txt + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + + + + + + generate-graalvm-files diff --git a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.java b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.java index fc0f083e5..145667e9f 100644 --- a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.java +++ b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.java @@ -27,11 +27,30 @@ import io.burt.jmespath.function.FunctionRegistry; import io.burt.jmespath.jackson.JacksonRuntime; import java.util.function.Supplier; +import org.crac.Context; +import org.crac.Core; +import org.crac.Resource; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; +import com.amazonaws.services.lambda.runtime.events.ActiveMQEvent; +import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; +import com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent; +import com.amazonaws.services.lambda.runtime.events.KafkaEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsFirehoseInputPreprocessingEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsStreamsInputPreprocessingEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent; +import com.amazonaws.services.lambda.runtime.events.RabbitMQEvent; +import com.amazonaws.services.lambda.runtime.events.SNSEvent; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.amazonaws.services.lambda.runtime.events.ScheduledEvent; +import software.amazon.lambda.powertools.common.internal.ClassPreLoader; import software.amazon.lambda.powertools.utilities.jmespath.Base64Function; import software.amazon.lambda.powertools.utilities.jmespath.Base64GZipFunction; import software.amazon.lambda.powertools.utilities.jmespath.JsonFunction; -public final class JsonConfig { +public final class JsonConfig implements Resource { private static final Supplier objectMapperSupplier = () -> JsonMapper.builder() // Don't throw an exception when json has extra fields you are not serializing on. @@ -61,6 +80,11 @@ public final class JsonConfig { private JmesPath jmesPath = new JacksonRuntime(configuration, getObjectMapper()); + // Static block to ensure CRaC registration happens at class loading time + static { + Core.getGlobalContext().register(get()); + } + private JsonConfig() { } @@ -103,6 +127,45 @@ public void addFunction(T function) { jmesPath = new JacksonRuntime(updatedConfig, getObjectMapper()); } + @Override + public void beforeCheckpoint(Context context) throws Exception { + // Preload classes first to ensure this always runs + ClassPreLoader.preloadClasses(); + + // Initialize key components + ObjectMapper mapper = getObjectMapper(); + getJmesPath(); + + // Prime common AWS Lambda event types with realistic events + primeEventType(mapper, APIGatewayProxyRequestEvent.class, + "{\"httpMethod\":\"GET\",\"path\":\"/test\",\"headers\":{\"Content-Type\":\"application/json\"},\"requestContext\":{\"accountId\":\"123456789012\"}}"); + primeEventType(mapper, APIGatewayV2HTTPEvent.class, + "{\"version\":\"2.0\",\"routeKey\":\"GET /test\",\"requestContext\":{\"http\":{\"method\":\"GET\"},\"accountId\":\"123456789012\"}}"); + primeEventType(mapper, SQSEvent.class, + "{\"Records\":[{\"messageId\":\"test-id\",\"body\":\"test message\",\"eventSource\":\"aws:sqs\"}]}"); + primeEventType(mapper, SNSEvent.class, + "{\"Records\":[{\"Sns\":{\"Message\":\"test message\",\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:test\"}}]}"); + primeEventType(mapper, KinesisEvent.class, + "{\"Records\":[{\"kinesis\":{\"data\":\"dGVzdA==\",\"partitionKey\":\"test\"},\"eventSource\":\"aws:kinesis\"}]}"); + primeEventType(mapper, ScheduledEvent.class, + "{\"source\":\"aws.events\",\"detail-type\":\"Scheduled Event\",\"detail\":{}}"); + + // Warm up JMESPath function registry + getJmesPath().compile("@").search(mapper.readTree("{\"test\":\"value\"}")); + } + + @Override + public void afterRestore(Context context) throws Exception { + // No action needed after restore + } + + private void primeEventType(ObjectMapper mapper, Class eventClass, String sampleJson) throws Exception { + // Deserialize sample JSON to the event class + Object event = mapper.readValue(sampleJson, eventClass); + // Serialize back to JSON to warm up both directions + mapper.writeValueAsString(event); + } + private static class ConfigHolder { private static final JsonConfig instance = new JsonConfig(); } diff --git a/powertools-serialization/src/main/resources/classesloaded.txt b/powertools-serialization/src/main/resources/classesloaded.txt new file mode 100644 index 000000000..b7836f94d --- /dev/null +++ b/powertools-serialization/src/main/resources/classesloaded.txt @@ -0,0 +1,74 @@ +java.lang.Object +java.io.Serializable +java.lang.Comparable +java.lang.CharSequence +java.lang.String +java.lang.Class +java.lang.Cloneable +java.lang.ClassLoader +java.lang.System +java.lang.Throwable +java.lang.Error +java.lang.Exception +java.lang.RuntimeException +com.fasterxml.jackson.databind.ObjectMapper +com.fasterxml.jackson.databind.JsonNode +com.fasterxml.jackson.databind.node.ObjectNode +com.fasterxml.jackson.databind.node.ArrayNode +com.fasterxml.jackson.databind.node.TextNode +com.fasterxml.jackson.databind.node.NumericNode +com.fasterxml.jackson.databind.node.BooleanNode +com.fasterxml.jackson.databind.node.NullNode +com.fasterxml.jackson.databind.json.JsonMapper +com.fasterxml.jackson.core.JsonFactory +com.fasterxml.jackson.core.JsonGenerator +com.fasterxml.jackson.core.JsonParser +com.fasterxml.jackson.core.JsonToken +com.fasterxml.jackson.databind.DeserializationFeature +com.fasterxml.jackson.databind.SerializationFeature +com.fasterxml.jackson.databind.MapperFeature +com.fasterxml.jackson.databind.JsonSerializer +com.fasterxml.jackson.databind.JsonDeserializer +com.fasterxml.jackson.databind.SerializerProvider +com.fasterxml.jackson.databind.DeserializationContext +com.fasterxml.jackson.annotation.JsonInclude +com.fasterxml.jackson.annotation.JsonInclude$Include +io.burt.jmespath.JmesPath +io.burt.jmespath.RuntimeConfiguration +io.burt.jmespath.RuntimeConfiguration$Builder +io.burt.jmespath.function.BaseFunction +io.burt.jmespath.function.FunctionRegistry +io.burt.jmespath.jackson.JacksonRuntime +software.amazon.lambda.powertools.utilities.JsonConfig +software.amazon.lambda.powertools.utilities.EventDeserializer +software.amazon.lambda.powertools.utilities.EventDeserializationException +software.amazon.lambda.powertools.utilities.jmespath.Base64Function +software.amazon.lambda.powertools.utilities.jmespath.Base64GZipFunction +software.amazon.lambda.powertools.utilities.jmespath.JsonFunction +com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent +com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent +com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent +com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse +com.amazonaws.services.lambda.runtime.events.ActiveMQEvent +com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent +com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerResponseEvent +com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent +com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent +com.amazonaws.services.lambda.runtime.events.KafkaEvent +com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsFirehoseInputPreprocessingEvent +com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsStreamsInputPreprocessingEvent +com.amazonaws.services.lambda.runtime.events.KinesisEvent +com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent +com.amazonaws.services.lambda.runtime.events.RabbitMQEvent +com.amazonaws.services.lambda.runtime.events.SNSEvent +com.amazonaws.services.lambda.runtime.events.SQSEvent +com.amazonaws.services.lambda.runtime.events.ScheduledEvent +org.slf4j.Logger +org.slf4j.LoggerFactory +java.util.function.Supplier +java.lang.ThreadLocal +java.util.Map +java.util.HashMap +java.util.List +java.util.ArrayList +java.util.concurrent.ConcurrentHashMap diff --git a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/JsonConfigCracTest.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/JsonConfigCracTest.java new file mode 100644 index 000000000..78ec80a0b --- /dev/null +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/JsonConfigCracTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * http://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 software.amazon.lambda.powertools.utilities; + +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.mockito.Mockito.mock; + +import org.crac.Context; +import org.crac.Resource; +import org.junit.jupiter.api.Test; + +class JsonConfigCracTest { + + JsonConfig config = JsonConfig.get(); + Context context = mock(Context.class); + + @Test + void testBeforeCheckpointDoesNotThrowException() { + assertThatNoException().isThrownBy(() -> config.beforeCheckpoint(context)); + } + + @Test + void testAfterRestoreDoesNotThrowException() { + assertThatNoException().isThrownBy(() -> config.afterRestore(context)); + } +} diff --git a/powertools-tracing/pom.xml b/powertools-tracing/pom.xml index 67de0be7d..3f4fb5a77 100644 --- a/powertools-tracing/pom.xml +++ b/powertools-tracing/pom.xml @@ -74,6 +74,10 @@ com.fasterxml.jackson.core jackson-annotations + + org.crac + crac + @@ -118,9 +122,32 @@ assertj-core test + + org.mockito + mockito-core + test + + + generate-classesloaded-file + + + + org.apache.maven.plugins + maven-surefire-plugin + + + -Xlog:class+load=info:classesloaded.txt + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + + + + + + generate-graalvm-files diff --git a/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/TracingUtils.java b/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/TracingUtils.java index 954ed7da4..91e3c5331 100644 --- a/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/TracingUtils.java +++ b/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/TracingUtils.java @@ -21,17 +21,33 @@ import com.amazonaws.xray.entities.Subsegment; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.function.Consumer; +import org.crac.Context; +import org.crac.Core; +import org.crac.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.amazon.lambda.powertools.common.internal.ClassPreLoader; /** * A class of helper functions to add additional functionality and ease * of use. */ -public final class TracingUtils { +public final class TracingUtils implements Resource { private static final Logger LOG = LoggerFactory.getLogger(TracingUtils.class); private static ObjectMapper objectMapper; + // Dummy instance to register TracingUtils with CRaC + private static final TracingUtils INSTANCE = new TracingUtils(); + + // Static block to ensure CRaC registration happens at class loading time + static { + Core.getGlobalContext().register(INSTANCE); + } + + private TracingUtils() { + // Private constructor for singleton pattern + } + /** * Put an annotation to the current subsegment with a String value. * @@ -192,4 +208,38 @@ public static void defaultObjectMapper(ObjectMapper objectMapper) { public static ObjectMapper objectMapper() { return objectMapper; } + + /** + * Prime TracingUtils for AWS Lambda SnapStart. + * This method has no side-effects and can be safely called to trigger SnapStart priming. + */ + public static void prime() { + // This method intentionally does nothing but ensures TracingUtils is loaded + // The actual priming happens in the beforeCheckpoint() method via CRaC hooks + } + + @Override + public void beforeCheckpoint(Context context) throws Exception { + // Preload classes first to ensure this always runs + ClassPreLoader.preloadClasses(); + + // Initialize key components + if (objectMapper == null) { + objectMapper = new ObjectMapper(); + } + + // Initialize X-Ray components by accessing them + AWSXRay.getGlobalRecorder(); + + // Warm up tracing utilities by calling key methods + serviceName(); + + // Initialize ObjectMapper for JSON serialization + objectMapper.writeValueAsString("dummy"); + } + + @Override + public void afterRestore(Context context) throws Exception { + // No action needed after restore + } } diff --git a/powertools-tracing/src/main/resources/classesloaded.txt b/powertools-tracing/src/main/resources/classesloaded.txt new file mode 100644 index 000000000..c93b8343a --- /dev/null +++ b/powertools-tracing/src/main/resources/classesloaded.txt @@ -0,0 +1,66 @@ +java.lang.Object +java.io.Serializable +java.lang.Comparable +java.lang.CharSequence +java.lang.String +java.lang.Class +java.lang.Cloneable +java.lang.ClassLoader +java.lang.System +java.lang.Throwable +java.lang.Error +java.lang.Exception +java.lang.RuntimeException +com.amazonaws.xray.AWSXRay +com.amazonaws.xray.entities.Entity +com.amazonaws.xray.entities.Subsegment +com.amazonaws.xray.entities.Segment +com.amazonaws.xray.entities.TraceID +com.amazonaws.xray.entities.TraceHeader +com.amazonaws.xray.strategy.sampling.SamplingStrategy +com.amazonaws.xray.strategy.sampling.LocalizedSamplingStrategy +com.amazonaws.xray.strategy.sampling.NoSamplingStrategy +com.amazonaws.xray.strategy.sampling.AllSamplingStrategy +com.amazonaws.xray.strategy.sampling.CentralizedSamplingStrategy +com.amazonaws.xray.strategy.ContextMissingStrategy +com.amazonaws.xray.strategy.LogErrorContextMissingStrategy +com.amazonaws.xray.strategy.RuntimeErrorContextMissingStrategy +com.amazonaws.xray.strategy.IgnoreErrorContextMissingStrategy +com.amazonaws.xray.contexts.LambdaSegmentContext +com.amazonaws.xray.contexts.SegmentContext +com.amazonaws.xray.contexts.ThreadLocalSegmentContext +com.amazonaws.xray.emitters.Emitter +com.amazonaws.xray.emitters.UDPEmitter +com.amazonaws.xray.listeners.SegmentListener +com.amazonaws.xray.plugins.Plugin +com.amazonaws.xray.plugins.ECSPlugin +com.amazonaws.xray.plugins.EC2Plugin +com.amazonaws.xray.plugins.EKSPlugin +software.amazon.lambda.powertools.tracing.TracingUtils +software.amazon.lambda.powertools.tracing.Tracing +software.amazon.lambda.powertools.tracing.CaptureMode +software.amazon.lambda.powertools.tracing.internal.LambdaTracingAspect +software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor +software.amazon.lambda.powertools.common.internal.LambdaConstants +com.fasterxml.jackson.databind.ObjectMapper +com.fasterxml.jackson.databind.JsonNode +com.fasterxml.jackson.databind.node.ObjectNode +com.fasterxml.jackson.databind.node.ArrayNode +com.fasterxml.jackson.databind.node.TextNode +com.fasterxml.jackson.databind.node.NumericNode +com.fasterxml.jackson.databind.node.BooleanNode +com.fasterxml.jackson.databind.node.NullNode +com.fasterxml.jackson.core.JsonFactory +com.fasterxml.jackson.core.JsonGenerator +com.fasterxml.jackson.core.JsonParser +com.fasterxml.jackson.core.JsonToken +com.fasterxml.jackson.databind.DeserializationFeature +com.fasterxml.jackson.databind.SerializationFeature +com.fasterxml.jackson.databind.MapperFeature +com.fasterxml.jackson.databind.JsonSerializer +com.fasterxml.jackson.databind.JsonDeserializer +com.fasterxml.jackson.databind.SerializerProvider +com.fasterxml.jackson.databind.DeserializationContext +org.slf4j.Logger +org.slf4j.LoggerFactory +org.slf4j.MDC diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/TracingUtilsCracTest.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/TracingUtilsCracTest.java new file mode 100644 index 000000000..4e4c3e8dc --- /dev/null +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/TracingUtilsCracTest.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * http://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 software.amazon.lambda.powertools.tracing; + +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.mockito.Mockito.mock; + +import java.lang.reflect.Field; +import org.crac.Context; +import org.crac.Resource; +import org.junit.jupiter.api.Test; + +class TracingUtilsCracTest { + + Context context = mock(Context.class); + + @Test + void testPrimeMethodDoesNotThrowException() { + assertThatNoException().isThrownBy(() -> TracingUtils.prime()); + } + + @Test + void testTracingUtilsLoadsSuccessfully() { + // Simply calling TracingUtils.prime() should trigger CRaC registration + TracingUtils.prime(); + // If we get here without exception, the test passes + } +} diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index 35aed5e26..de666170a 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -193,8 +193,20 @@ - - + + + + + + + + + + + + + +