diff --git a/build.gradle b/build.gradle index 68525a4230..75002c0143 100644 --- a/build.gradle +++ b/build.gradle @@ -64,6 +64,8 @@ configure(allprojects) { project -> dependency "io.reactivex:rxjava-reactive-streams:1.2.1" dependency "io.reactivex.rxjava2:rxjava:2.2.17" + dependency "io.projectreactor.tools:blockhound:1.0.2.RELEASE" + dependency "com.caucho:hessian:4.0.62" dependency "com.fasterxml:aalto-xml:1.2.2" dependency("com.fasterxml.woodstox:woodstox-core:6.0.3") { diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index 94327d342e..d88536a3fe 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -43,6 +43,7 @@ dependencies { compile(files(objenesisRepackJar)) compile(project(":spring-jcl")) compileOnly(project(":kotlin-coroutines")) + compileOnly("io.projectreactor.tools:blockhound") optional("net.sf.jopt-simple:jopt-simple") optional("org.aspectj:aspectjweaver") optional("org.jetbrains.kotlin:kotlin-reflect") @@ -60,6 +61,7 @@ dependencies { testCompile("javax.xml.bind:jaxb-api") testCompile("com.fasterxml.woodstox:woodstox-core") testCompile(project(":kotlin-coroutines")) + testCompile("io.projectreactor.tools:blockhound") testFixturesImplementation("com.google.code.findbugs:jsr305") testFixturesImplementation("io.projectreactor:reactor-test") testFixturesImplementation("org.assertj:assertj-core") diff --git a/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java b/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java index 48c7512e62..9b237c9a09 100644 --- a/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java +++ b/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -29,12 +29,15 @@ import io.reactivex.Flowable; import kotlinx.coroutines.CompletableDeferredKt; import kotlinx.coroutines.Deferred; import org.reactivestreams.Publisher; +import reactor.blockhound.BlockHound; +import reactor.blockhound.integration.BlockHoundIntegration; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import rx.RxReactiveStreams; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ReflectionUtils; /** @@ -354,4 +357,32 @@ public class ReactiveAdapterRegistry { ); } } + + + /** + * {@code BlockHoundIntegration} for spring-core classes. + *

Whitelists the following: + *

+ * @since 5.2.4 + */ + public static class SpringCoreBlockHoundIntegration implements BlockHoundIntegration { + + @Override + public void applyTo(BlockHound.Builder builder) { + + // Avoid hard references potentially anywhere in spring-core (no need for structural dependency) + + builder.allowBlockingCallsInside( + "org.springframework.core.LocalVariableTableParameterNameDiscoverer", "inspectClass"); + + String className = "org.springframework.util.ConcurrentReferenceHashMap$Segment"; + builder.allowBlockingCallsInside(className, "doTask"); + builder.allowBlockingCallsInside(className, "clear"); + builder.allowBlockingCallsInside(className, "restructure"); + } + } + } diff --git a/spring-core/src/main/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration b/spring-core/src/main/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration new file mode 100644 index 0000000000..5a35c69bac --- /dev/null +++ b/spring-core/src/main/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration @@ -0,0 +1,15 @@ +# Copyright 2002-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. + +org.springframework.core.ReactiveAdapterRegistry$SpringCoreBlockHoundIntegration \ No newline at end of file diff --git a/spring-core/src/test/java/org/springframework/core/SpringCoreBlockHoundIntegrationTests.java b/spring-core/src/test/java/org/springframework/core/SpringCoreBlockHoundIntegrationTests.java new file mode 100644 index 0000000000..2d8648335c --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/SpringCoreBlockHoundIntegrationTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-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.core; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import reactor.blockhound.BlockHound; +import reactor.core.scheduler.Schedulers; + +import org.springframework.tests.sample.objects.TestObject; +import org.springframework.util.ConcurrentReferenceHashMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests to verify the spring-core BlockHound integration rules. + * + * @author Rossen Stoyanchev + * @since 5.2.4 + */ +public class SpringCoreBlockHoundIntegrationTests { + + + @BeforeAll + static void setUp() { + BlockHound.install(); + } + + + @Test + void blockHoundIsInstalled() { + assertThatThrownBy(() -> testNonBlockingTask(() -> Thread.sleep(10))) + .hasMessageContaining("Blocking call!"); + } + + @Test + void localVariableTableParameterNameDiscoverer() { + testNonBlockingTask(() -> { + Method setName = TestObject.class.getMethod("setName", String.class); + String[] names = new LocalVariableTableParameterNameDiscoverer().getParameterNames(setName); + assertThat(names).isEqualTo(new String[] {"name"}); + }); + } + + @Test + void concurrentReferenceHashMap() { + int size = 10000; + Map map = new ConcurrentReferenceHashMap<>(size); + + CompletableFuture future1 = new CompletableFuture<>(); + testNonBlockingTask(() -> { + for (int i = 0; i < size / 2; i++) { + map.put("a" + i, "bar"); + } + }, future1); + + CompletableFuture future2 = new CompletableFuture<>(); + testNonBlockingTask(() -> { + for (int i = 0; i < size / 2; i++) { + map.put("b" + i, "bar"); + } + }, future2); + + CompletableFuture.allOf(future1, future2).join(); + assertThat(map).hasSize(size); + } + + private void testNonBlockingTask(NonBlockingTask task) { + CompletableFuture future = new CompletableFuture<>(); + testNonBlockingTask(task, future); + future.join(); + } + + private void testNonBlockingTask(NonBlockingTask task, CompletableFuture future) { + Schedulers.parallel().schedule(() -> { + try { + task.run(); + future.complete(null); + } + catch (Throwable ex) { + future.completeExceptionally(ex); + } + }); + } + + + @FunctionalInterface + private interface NonBlockingTask { + + void run() throws Exception; + } + +}