在 Webflux 中使用 bean 验证时 BlockHound 抛出阻塞调用异常

Posted

技术标签:

【中文标题】在 Webflux 中使用 bean 验证时 BlockHound 抛出阻塞调用异常【英文标题】:BlockHound throws blocking call exception when using bean validation in Webflux 【发布时间】:2020-05-07 11:52:35 【问题描述】:

我使用 Webflux 在 Spring Boot 2.1.8 应用程序上测试了 BlockHound,我在 bean 验证中遇到了阻塞调用。为了确保这肯定不是由我们的逻辑引起的,我创建了一个带有一个端点的简单 Webflux 应用程序。

这是来自应用程序的简单控制器:

@RestController
@RequestMapping("/v1/test")
@Validated
class TestController 

    @PostMapping("/type", consumes = [MediaType.APPLICATION_JSON_VALUE])
    fun testPost(@PathVariable type: String, @Valid @RequestBody entry: TestEntry): Mono<TestEntry> 
        return Mono.just(TestEntry("$entry.data - $type"))
    


@JsonInclude(JsonInclude.Include.NON_NULL)
data class TestEntry(
    @field:NotNull val data: String?
)

在 main 方法中我运行 Block Hound JVM 代理:

@SpringBootApplication
class DemoApplication

fun main(args: Array<String>) 
    BlockHound.install()

    runApplication<DemoApplication>(*args)

向我的端点发送请求后,我得到了这个异常:

java.lang.Error: Blocking call! java.io.RandomAccessFile#readBytes
    at reactor.blockhound.BlockHound$Builder.lambda$new$0(BlockHound.java:196) ~[blockhound-1.0.1.RELEASE.jar:na]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    |_ checkpoint ⇢ HTTP POST "/v1/test/type1" [ExceptionHandlingWebHandler]
Stack trace:
        at reactor.blockhound.BlockHound$Builder.lambda$new$0(BlockHound.java:196) ~[blockhound-1.0.1.RELEASE.jar:na]
        at reactor.blockhound.BlockHound$Builder.lambda$install$6(BlockHound.java:318) ~[blockhound-1.0.1.RELEASE.jar:na]
        at reactor.blockhound.BlockHoundRuntime.checkBlocking(BlockHoundRuntime.java:46) ~[na:na]
        at java.base/java.io.RandomAccessFile.readBytes(RandomAccessFile.java) ~[na:na]
        at java.base/java.io.RandomAccessFile.read(RandomAccessFile.java:406) ~[na:na]
        at java.base/java.io.RandomAccessFile.readFully(RandomAccessFile.java:470) ~[na:na]
        at java.base/java.util.zip.ZipFile$Source.readFullyAt(ZipFile.java:1298) ~[na:na]
        at java.base/java.util.zip.ZipFile$ZipFileInputStream.initDataOffset(ZipFile.java:997) ~[na:na]
        at java.base/java.util.zip.ZipFile$ZipFileInputStream.read(ZipFile.java:1012) ~[na:na]
        at java.base/java.util.zip.ZipFile$ZipFileInflaterInputStream.fill(ZipFile.java:467) ~[na:na]
        at java.base/java.util.zip.InflaterInputStream.read(InflaterInputStream.java:159) ~[na:na]
        at java.base/java.io.InputStream.readNBytes(InputStream.java:490) ~[na:na]
        at java.base/java.util.jar.JarFile.getBytes(JarFile.java:805) ~[na:na]
        at java.base/java.util.jar.JarFile.checkForSpecialAttributes(JarFile.java:1005) ~[na:na]
        at java.base/java.util.jar.JarFile.isMultiRelease(JarFile.java:388) ~[na:na]
        at java.base/java.util.jar.JarFile.getEntry(JarFile.java:507) ~[na:na]
        at java.base/sun.net.www.protocol.jar.URLJarFile.getEntry(URLJarFile.java:131) ~[na:na]
        at java.base/sun.net.www.protocol.jar.JarURLConnection.connect(JarURLConnection.java:137) ~[na:na]
        at java.base/sun.net.www.protocol.jar.JarURLConnection.getInputStream(JarURLConnection.java:155) ~[na:na]
        at java.base/java.net.URL.openStream(URL.java:1117) ~[na:na]
        at java.base/java.lang.ClassLoader.getResourceAsStream(ClassLoader.java:1738) ~[na:na]
        at java.base/java.lang.Class.getResourceAsStream(Class.java:2651) ~[na:na]
        at org.springframework.core.LocalVariableTableParameterNameDiscoverer.inspectClass(LocalVariableTableParameterNameDiscoverer.java:94) ~[spring-core-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1705) ~[na:na]
        at org.springframework.core.LocalVariableTableParameterNameDiscoverer.doGetParameterNames(LocalVariableTableParameterNameDiscoverer.java:84) ~[spring-core-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.springframework.core.LocalVariableTableParameterNameDiscoverer.getParameterNames(LocalVariableTableParameterNameDiscoverer.java:72) ~[spring-core-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.springframework.core.PrioritizedParameterNameDiscoverer.getParameterNames(PrioritizedParameterNameDiscoverer.java:55) ~[spring-core-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.springframework.validation.beanvalidation.LocalValidatorFactoryBean$1.getParameterNames(LocalValidatorFactoryBean.java:325) ~[spring-context-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.hibernate.validator.internal.util.ExecutableParameterNameProvider.getParameterNames(ExecutableParameterNameProvider.java:37) ~[hibernate-validator-6.0.18.Final.jar:6.0.18.Final]
        at org.hibernate.validator.internal.metadata.aggregated.ParameterMetaData$Builder.build(ParameterMetaData.java:169) ~[hibernate-validator-6.0.18.Final.jar:6.0.18.Final]
        at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.findParameterMetaData(ExecutableMetaData.java:435) ~[hibernate-validator-6.0.18.Final.jar:6.0.18.Final]
        at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.build(ExecutableMetaData.java:388) ~[hibernate-validator-6.0.18.Final.jar:6.0.18.Final]
        at org.hibernate.validator.internal.metadata.aggregated.BeanMetaDataImpl$BuilderDelegate.build(BeanMetaDataImpl.java:788) ~[hibernate-validator-6.0.18.Final.jar:6.0.18.Final]
        at org.hibernate.validator.internal.metadata.aggregated.BeanMetaDataImpl$BeanMetaDataBuilder.build(BeanMetaDataImpl.java:648) ~[hibernate-validator-6.0.18.Final.jar:6.0.18.Final]
        at org.hibernate.validator.internal.metadata.BeanMetaDataManager.createBeanMetaData(BeanMetaDataManager.java:204) ~[hibernate-validator-6.0.18.Final.jar:6.0.18.Final]
        at org.hibernate.validator.internal.metadata.BeanMetaDataManager.getBeanMetaData(BeanMetaDataManager.java:166) ~[hibernate-validator-6.0.18.Final.jar:6.0.18.Final]
        at org.hibernate.validator.internal.engine.ValueContext.getLocalExecutionContext(ValueContext.java:78) ~[hibernate-validator-6.0.18.Final.jar:6.0.18.Final]
        at org.hibernate.validator.internal.engine.ValidatorImpl.validateReturnValueInContext(ValidatorImpl.java:1060) ~[hibernate-validator-6.0.18.Final.jar:6.0.18.Final]
        at org.hibernate.validator.internal.engine.ValidatorImpl.validateReturnValue(ValidatorImpl.java:306) ~[hibernate-validator-6.0.18.Final.jar:6.0.18.Final]
        at org.hibernate.validator.internal.engine.ValidatorImpl.validateReturnValue(ValidatorImpl.java:257) ~[hibernate-validator-6.0.18.Final.jar:6.0.18.Final]
        at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:122) ~[spring-context-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747) ~[spring-aop-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689) ~[spring-aop-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at com.example.demo.TestController$$EnhancerBySpringCGLIB$$ae3498ec.testPost(<generated>) ~[classes/:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
        at org.springframework.web.reactive.result.method.InvocableHandlerMethod.lambda$invoke$0(InvocableHandlerMethod.java:147) ~[spring-webflux-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:118) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1630) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.core.publisher.MonoZip$ZipCoordinator.signal(MonoZip.java:247) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.core.publisher.MonoZip$ZipInner.onNext(MonoZip.java:329) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onNext(MonoPeekTerminal.java:173) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onNext(FluxDefaultIfEmpty.java:92) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:192) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:67) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:73) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1630) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:144) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.core.publisher.FluxContextStart$ContextStartSubscriber.onNext(FluxContextStart.java:103) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.core.publisher.FluxContextStart$ContextStartSubscriber.onNext(FluxContextStart.java:103) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:287) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:330) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1630) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:145) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:136) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:252) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:136) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]
        at reactor.netty.channel.FluxReceive.terminateReceiver(FluxReceive.java:419) ~[reactor-netty-0.9.2.RELEASE.jar:0.9.2.RELEASE]
        at reactor.netty.channel.FluxReceive.drainReceiver(FluxReceive.java:209) ~[reactor-netty-0.9.2.RELEASE.jar:0.9.2.RELEASE]
        at reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:367) ~[reactor-netty-0.9.2.RELEASE.jar:0.9.2.RELEASE]
        at reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:363) ~[reactor-netty-0.9.2.RELEASE.jar:0.9.2.RELEASE]
        at reactor.netty.http.server.HttpServerOperations.onInboundNext(HttpServerOperations.java:461) ~[reactor-netty-0.9.2.RELEASE.jar:0.9.2.RELEASE]
        at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:89) ~[reactor-netty-0.9.2.RELEASE.jar:0.9.2.RELEASE]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:352) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at reactor.netty.http.server.HttpTrafficHandler.channelRead(HttpTrafficHandler.java:211) ~[reactor-netty-0.9.2.RELEASE.jar:0.9.2.RELEASE]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:352) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:438) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:326) ~[netty-codec-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:300) ~[netty-codec-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:253) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:352) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1422) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:931) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:700) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:635) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:552) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:514) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.util.concurrent.SingleThreadEventExecutor$6.run(SingleThreadEventExecutor.java:1050) ~[netty-common-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.43.Final.jar:4.1.43.Final]
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.43.Final.jar:4.1.43.Final]
        at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]

有没有办法以某种方式解除阻止这种验证?

编辑:

似乎休眠 bean 验证正在阻塞。我发现的解决方案之一是创建自定义spring Validator。在我的情况下,它会是这样的:

@Component
class TestEntryValidator: Validator 
    override fun validate(target: Any, errors: Errors) 
        ValidationUtils.rejectIfEmptyOrWhitespace(
            errors, "data", "field.required")
    

    override fun supports(clazz: Class<*>): Boolean 
        return TestEntry::class.javaObjectType.isAssignableFrom(clazz)
    

它的用法可能是这样的:

@RestController
@RequestMapping("/v1/test")
class TestController(val testSpringValidator: TestEntryValidator) 

    @PostMapping("/type", consumes = [MediaType.APPLICATION_JSON_VALUE])
    fun testPost(@PathVariable type: String, @RequestBody entry: TestEntry): Mono<TestEntry> 
        return Mono.fromCallable 
            val errors = BeanPropertyBindingResult(
                entry,
                TestEntry::class.java.name
            )
            testSpringValidator.validate(entry, errors)

            if (errors.allErrors.isEmpty()) 
                TestEntry("$entry.data - $type")
             else 
                throw ResponseStatusException(
                    HttpStatus.BAD_REQUEST,
                    errors.allErrors.toString()
                )
            
        
    


data class TestEntry(
    val data: String?
)

不幸的是,这迫使您添加一些额外的样板代码,但我目前还没有找到更好的东西。

【问题讨论】:

由于您没有发布任何有关正在使用的验证或任何内容的信息,因此没有人会帮助您。您正在使用的验证库正在从某处读取一些字节。所以你可能正在使用一个阻塞验证库。 我不使用任何自定义 bean 验证。我使用 spring-boot-starter-webflux 中包含的 hibernate-validator:6.0.17。有没有非阻塞的替代方案?我认为 Webflux 应该提供非阻塞库 OOTB。 发布您的完整代码,您是否正在阅读参数/属性 都是代码。我使用 spring initializr 创建了新的 Webflux 应用程序并添加了这个 ednpoint 和 blockhound 依赖项。 docs.jboss.org/hibernate/stable/validator/reference/en-US/… 解释了它,它会加载您可以存储在资源包中的自定义验证消息。如果有多个资源包,它将聚合它们。它可能首先加载一个默认包,然后加载包含所有错误消息的包 【参考方案1】:

我在我的 Spring Boot 应用程序中试验了相同的行为。好消息是它很容易解决。

当您使用验证注解时,当一个字段未通过约束时,要注意的消息是从文件加载的。那就是阻塞!

所以,您唯一需要做的就是提供一条消息来显示,而不是从文件中读取默认值。

例如,如果你想限制一个字符串字段不包含空值, 用这个

@NotBlank(message = "Field must not be blank")
private String mandatoryField;

不是这个

@NotBlank
private String mandatoryField;

就是这样!

【讨论】:

在每个注释中重复此消息看起来不是正确的解决方案。 当然,如果很多情况下文本完全一样,你可以将消息文本提取成一个常量 如果你需要 i18n 怎么办? 我认为您可以将消息(属性或 yaml 文件)外部化并动态加载正确的值

以上是关于在 Webflux 中使用 bean 验证时 BlockHound 抛出阻塞调用异常的主要内容,如果未能解决你的问题,请参考以下文章

在 WebFlux 中创建名为 requestMappingHandlerMapping 的 bean 时出错(没有 Spring Boot)

如何在 Spring Boot 和 Spring WebFlux 中使用“功能 bean 定义 Kotlin DSL”?

WebFilter bean 在安全的 Spring Boot Webflux 应用程序中调用了两次

如何使用 Spring Boot Security 修复 Spring Boot Webflux 应用程序中的“名称为 requestMappingHandlerAdapter 的 bean 定义无效

在 Spring WebFlux 中使用 Spring Security 实现身份验证的资源是啥

在身份验证 Spring Security + WebFlux 期间抛出和处理自定义异常