在 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 定义无效