From 3c28622ebc81444974859685b14a663697612e23 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 10 Jul 2019 12:05:40 +0200 Subject: [PATCH] Add support for reactor-tools debug agent The `reactor-tools` dependency now brings a new Reactor Debug Agent which instruments loaded classes for better Reactor stacktraces. This commit removes the `spring.reactor.stacktrace-mode.enabled` configuration property since the related Reactor Hook is about to be removed. As a replacement, we're introducing `spring.reactor.debug-agent.enabled` which tells whether the Reactor Debug Agent should be loaded, given that the `reactor-tools` dependency is available. This option is enabled by default, since adding the dependency on classpath is a strong signal already. Fixes gh-17128 --- .../core/ReactorCoreAutoConfiguration.java | 48 ------------- .../reactor/core/ReactorCoreProperties.java | 65 ----------------- ...itional-spring-configuration-metadata.json | 8 +++ .../main/resources/META-INF/spring.factories | 1 - .../ReactorCoreAutoConfigurationTests.java | 69 ------------------- spring-boot-project/spring-boot/pom.xml | 5 ++ .../DebugAgentEnvironmentPostProcessor.java | 59 ++++++++++++++++ ...itional-spring-configuration-metadata.json | 7 ++ .../main/resources/META-INF/spring.factories | 3 +- ...bugAgentEnvironmentPostProcessorTests.java | 48 +++++++++++++ .../reactor/InstrumentedFluxProvider.java} | 19 ++++- 11 files changed, 145 insertions(+), 187 deletions(-) delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/core/ReactorCoreAutoConfiguration.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/core/ReactorCoreProperties.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/reactor/core/ReactorCoreAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/reactor/DebugAgentEnvironmentPostProcessor.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/DebugAgentEnvironmentPostProcessorTests.java rename spring-boot-project/{spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/core/package-info.java => spring-boot/src/test/java/org/springframework/boot/reactor/InstrumentedFluxProvider.java} (60%) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/core/ReactorCoreAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/core/ReactorCoreAutoConfiguration.java deleted file mode 100644 index 27109b1538..0000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/core/ReactorCoreAutoConfiguration.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2012-2019 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.autoconfigure.reactor.core; - -import reactor.core.publisher.Flux; -import reactor.core.publisher.Hooks; -import reactor.core.publisher.Mono; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for Reactor Core. - * - * @author Brian Clozel - * @author Eddú Meléndez - * @since 2.0.0 - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ Mono.class, Flux.class }) -@EnableConfigurationProperties(ReactorCoreProperties.class) -public class ReactorCoreAutoConfiguration { - - @Autowired - protected void initialize(ReactorCoreProperties properties) { - if (properties.isDebug()) { - Hooks.onOperatorDebug(); - } - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/core/ReactorCoreProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/core/ReactorCoreProperties.java deleted file mode 100644 index 3a5537508d..0000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/core/ReactorCoreProperties.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2012-2019 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.autoconfigure.reactor.core; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; - -/** - * Properties for Reactor Core. - * - * @author Brian Clozel - * @since 2.0.0 - */ -@ConfigurationProperties(prefix = "spring.reactor") -public class ReactorCoreProperties { - - /** - * Whether Reactor should collect stacktrace information at runtime. - */ - private boolean debug; - - private final StacktraceMode stacktraceMode = new StacktraceMode(); - - public boolean isDebug() { - return this.debug; - } - - public void setDebug(boolean debug) { - this.debug = debug; - } - - public StacktraceMode getStacktraceMode() { - return this.stacktraceMode; - } - - public class StacktraceMode { - - @DeprecatedConfigurationProperty(replacement = "spring.reactor.debug") - @Deprecated - public boolean isEnabled() { - return isDebug(); - } - - @Deprecated - public void setEnabled(boolean enabled) { - setDebug(enabled); - } - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 8eda844d5b..76c77d4abb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -775,6 +775,14 @@ "name": "spring.rabbitmq.listener.type", "defaultValue": "simple" }, + { + "name": "spring.reactor.stacktrace-mode.enabled", + "description": "Whether Reactor should collect stacktrace information at runtime.", + "defaultValue": false, + "deprecation": { + "replacement": "spring.reactor.debug-agent.enabled" + } + }, { "name": "spring.security.filter.dispatcher-types", "defaultValue": [ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 91f0a50965..e791c007e0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -100,7 +100,6 @@ org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\ org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\ org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\ org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\ -org.springframework.boot.autoconfigure.reactor.core.ReactorCoreAutoConfiguration,\ org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration,\ org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration,\ org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/reactor/core/ReactorCoreAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/reactor/core/ReactorCoreAutoConfigurationTests.java deleted file mode 100644 index 14dda20cc7..0000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/reactor/core/ReactorCoreAutoConfigurationTests.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2012-2019 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.autoconfigure.reactor.core; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Hooks; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.assertj.AssertableApplicationContext; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.boot.test.context.runner.ContextConsumer; -import org.springframework.test.util.ReflectionTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ReactorCoreAutoConfiguration}. - * - * @author Stephane Nicoll - */ -class ReactorCoreAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(ReactorCoreAutoConfiguration.class)); - - @BeforeEach - @AfterEach - void resetDebugFlag() { - Hooks.resetOnOperatorDebug(); - } - - @Test - void debugOperatorIsDisabledByDefault() { - this.contextRunner.run(assertDebugOperator(false)); - } - - @Test - void debugOperatorIsSetWithProperty() { - this.contextRunner.withPropertyValues("spring.reactor.debug=true").run(assertDebugOperator(true)); - } - - @Test - @Deprecated - void debugOperatorIsSetWithDeprecatedProperty() { - this.contextRunner.withPropertyValues("spring.reactor.stacktrace-mode.enabled=true") - .run(assertDebugOperator(true)); - } - - private ContextConsumer assertDebugOperator(boolean expected) { - return (context) -> assertThat(ReflectionTestUtils.getField(Hooks.class, "GLOBAL_TRACE")).isEqualTo(expected); - } - -} diff --git a/spring-boot-project/spring-boot/pom.xml b/spring-boot-project/spring-boot/pom.xml index 9a0435b577..61876b5682 100644 --- a/spring-boot-project/spring-boot/pom.xml +++ b/spring-boot-project/spring-boot/pom.xml @@ -76,6 +76,11 @@ reactor-netty true + + io.projectreactor + reactor-tools + true + io.netty netty-tcnative-boringssl-static diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/reactor/DebugAgentEnvironmentPostProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/reactor/DebugAgentEnvironmentPostProcessor.java new file mode 100644 index 0000000000..4a2c8ba17a --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/reactor/DebugAgentEnvironmentPostProcessor.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2019 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.reactor; + +import reactor.tools.agent.ReactorDebugAgent; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.ClassUtils; + +/** + * {@link EnvironmentPostProcessor} to enable the Reactor Debug Agent if available. + *

+ * The debug agent is enabled by default, unless the + * {@code "spring.reactor.debug-agent.enabled"} configuration property is set to false. We + * are using here an {@link EnvironmentPostProcessor} instead of an auto-configuration + * class to enable the agent as soon as possible during the startup process. + * + * @author Brian Clozel + * @since 2.2.0 + */ +public class DebugAgentEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { + + private static final String REACTOR_DEBUGAGENT_CLASS = "reactor.tools.agent.ReactorDebugAgent"; + + private static final String DEBUGAGENT_ENABLED_CONFIG_KEY = "spring.reactor.debug-agent.enabled"; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + if (ClassUtils.isPresent(REACTOR_DEBUGAGENT_CLASS, null)) { + Boolean agentEnabled = environment.getProperty(DEBUGAGENT_ENABLED_CONFIG_KEY, Boolean.class); + if (agentEnabled != Boolean.FALSE) { + ReactorDebugAgent.init(); + } + } + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index e63fac9499..1f8debfb82 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -719,6 +719,13 @@ "sourceType": "org.springframework.boot.context.config.ConfigFileApplicationListener", "description": "Unconditionally activate the specified comma-separated list of profiles (or list of profiles if using YAML)." }, + { + "name": "spring.reactor.debug-agent.enabled", + "type": "java.lang.Boolean", + "sourceType": "org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor", + "description": "Whether the Reactor Debug Agent should be enabled when reactor-tools is present.", + "defaultValue": true + }, { "name": "trace", "type": "java.lang.Boolean", diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories index 5312bed388..cbaf16b4ab 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories @@ -34,7 +34,8 @@ org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener org.springframework.boot.env.EnvironmentPostProcessor=\ org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\ org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\ -org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor +org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor,\ +org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor # Failure Analyzers org.springframework.boot.diagnostics.FailureAnalyzer=\ diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/DebugAgentEnvironmentPostProcessorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/DebugAgentEnvironmentPostProcessorTests.java new file mode 100644 index 0000000000..626139c2ba --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/DebugAgentEnvironmentPostProcessorTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2019 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.reactor; + +import org.junit.jupiter.api.Test; +import reactor.core.Scannable; +import reactor.core.publisher.Flux; + +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DebugAgentEnvironmentPostProcessor}. + * + * @author Brian Clozel + */ +class DebugAgentEnvironmentPostProcessorTests { + + static { + MockEnvironment environment = new MockEnvironment(); + DebugAgentEnvironmentPostProcessor postProcessor = new DebugAgentEnvironmentPostProcessor(); + postProcessor.postProcessEnvironment(environment, null); + } + + @Test + void enablesReactorDebugAgent() { + InstrumentedFluxProvider fluxProvider = new InstrumentedFluxProvider(); + Flux flux = fluxProvider.newFluxJust(); + assertThat(Scannable.from(flux).stepName()) + .startsWith("Flux.just ⇢ at org.springframework.boot.reactor.InstrumentedFluxProvider.newFluxJust"); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/core/package-info.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/InstrumentedFluxProvider.java similarity index 60% rename from spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/core/package-info.java rename to spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/InstrumentedFluxProvider.java index 780e22a653..2ec6e0c03b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/core/package-info.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/InstrumentedFluxProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * Copyright 2012-2019 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. @@ -14,7 +14,20 @@ * limitations under the License. */ +package org.springframework.boot.reactor; + +import reactor.core.publisher.Flux; + /** - * Auto-configuration for Reactor Core. + * Utility class that should be instrumented by the reactor debug agent. + * + * @author Brian Clozel + * @see DebugAgentEnvironmentPostProcessorTests */ -package org.springframework.boot.autoconfigure.reactor.core; +class InstrumentedFluxProvider { + + Flux newFluxJust() { + return Flux.just(1); + } + +}