javax.json.JsonPatch 在 Spring Webflux 微服务中不支持 @RequestBody
javax.json.JsonPatch unsupported as @RequestBody in Spring Webflux Microservice
我目前正在摆弄一个 Spring Webflux 微服务,并尝试实现一个 @PatchMapping,它使用一个 JsonPatch 对象作为 @RequestBody,如下所示:
@PatchMapping("/{id}", consumes = ["application/json-patch+json"])
fun updateFriend( @PathVariable("id") id: Long,
@Valid @RequestBody jsonPatch: JsonPatch): Mono<UserFriends> {
// apply JsonPatch code here...
}
我使用 Postman 请求测试了端点,如下所示:
PATCH <microservice-uri>/friends/1
Content-Type = application/json-patch+json
RequestBody =
[
{
"op": "replace",
"path": "friends/11/since",
"value": "<sample value>"
}
]
这是我遇到的异常:
org.springframework.core.codec.CodecException: Type definition error: [simple type, class javax.json.JsonPatch]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `javax.json.JsonPatch` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (io.netty.buffer.ByteBufInputStream); line: 1, column: 1]
at org.springframework.http.codec.json.AbstractJackson2Decoder.processException(AbstractJackson2Decoder.java:211) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ HTTP PATCH "/friends/1" [ExceptionHandlingWebHandler]
<Additional stacktraces here ...>
我很清楚,我需要告诉我的微服务如何将 RequestBody 转换为 JsonPatch。但在失败了 3 天后,我决定寻求帮助。
在多次尝试失败后,我找到了 WebFluxConfigurer
并尝试覆盖 configureArgumentResolvers
。我在为 JsonPatch 编写 HandlerMethodArgumentResolver
时卡住了。
class JsonPatchResolver : HandlerMethodArgumentResolver {
override fun supportsParameter(parameter: MethodParameter): Boolean {
// Don't know what to implement here
}
override fun resolveArgument(parameter: MethodParameter, bindingContext: BindingContext, exchange: ServerWebExchange): Mono<Any> {
// Don't know how to create a JsonPatch from the given arguments
}
}
有人可以指出我正确的方向或告诉我我做错了什么吗?
非常感谢您!
编辑
这是我的 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>friend-info-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>friend-info-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
<kotlin.version>1.3.72</kotlin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-patch</artifactId>
<version>1.12</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr353</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>
<dependency>
<groupId>javax.json</groupId>
<artifactId>javax.json-api</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
编辑 2
以下不起作用:
class JsonPatchArgumentResolver : HandlerMethodArgumentResolver {
override fun supportsParameter(parameter: MethodParameter): Boolean {
// this doesn't event get called when debugging, so I suspect the handler isn't registered correctly
return parameter.parameterType == JsonPatch::class
}
override fun resolveArgument(parameter: MethodParameter, bindingContext: BindingContext, exchange: ServerWebExchange): Mono<Any> {
return exchange.request.body.toMono().map {
jacksonObjectMapper().readValue(it.asInputStream(), JsonPatch::class.java)
}
}
}
在我的申请中 class 我正在注册它(可能是错误的,我不知道):
@SpringBootApplication
@EnableReactiveMongoRepositories
@EnableWebFlux
@Configuration
class FriendInfoServiceApplication : WebFluxConfigurer {
override fun configureArgumentResolvers(configurer: ArgumentResolverConfigurer) {
configurer.addCustomResolver(JsonPatchArgumentResolver())
}
}
fun main(args: Array<String>) {
runApplication<FriendInfoServiceApplication>(*args)
}
编辑 3
在调试时我发现 configureArgumentResolvers
get 被调用了,所以也许我对 supportsParameter
?
做错了什么
编辑 4
我尝试从 THIS 博客 post 复制代码。科特林等价物应该是:
@Component
class JsonPatchHttpMessageConverter : AbstractHttpMessageConverter<JsonPatch>() {
@Throws(HttpMessageNotReadableException::class)
protected override fun readInternal(clazz: Class<out JsonPatch>, inputMessage: HttpInputMessage): JsonPatch {
try {
Json.createReader(inputMessage.body).use { reader -> return Json.createPatch(reader.readArray()) }
} catch (e: Exception) {
throw HttpMessageNotReadableException(e.message!!, inputMessage)
}
}
@Throws(HttpMessageNotWritableException::class)
protected override fun writeInternal(jsonPatch: JsonPatch, outputMessage: HttpOutputMessage) {
throw NotImplementedError("The write Json patch is not implemented")
}
protected override fun supports(clazz: Class<*>): Boolean {
return JsonPatch::class.java.isAssignableFrom(clazz)
}
}
并将 objectMapper 添加到应用程序 class 中,如下所示:
@SpringBootApplication
@EnableReactiveMongoRepositories
@EnableWebFlux
@Configuration
class FriendInfoServiceApplication {
@Bean
fun objectMapper(): ObjectMapper {
val objectMapper = ObjectMapper()
objectMapper.registerModule(JSR353Module())
return objectMapper
}
}
不幸的是,当尝试调用 PATCH 端点时,结果是相同的异常。
我是您提到的博客 post 的作者。我不是 Webflux 方面的专家,但在这种情况下,您必须将 JacksonConverter 重新实现为 webFlux 表单。
于是变成了这样:
import org.springframework.core.ResolvableType
import org.springframework.core.io.buffer.DataBuffer
import org.springframework.core.io.buffer.DataBufferUtils
import org.springframework.http.MediaType
import org.springframework.http.ReactiveHttpInputMessage
import org.springframework.http.codec.HttpMessageReader
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import javax.json.Json
import javax.json.JsonPatch
class JsonPatchHttpMessageConverter : HttpMessageReader<JsonPatch> {
override fun getReadableMediaTypes(): List<MediaType> {
return listOf(MediaType.valueOf("application/json-patch+json"))
}
override fun canRead(elementType: ResolvableType, mediaType: MediaType?): Boolean {
return MediaType.valueOf("application/json-patch+json").includes(mediaType)
}
override fun read(elementType: ResolvableType, message: ReactiveHttpInputMessage, hints: Map<String, Any>): Flux<JsonPatch> {
//TODO implement the same mono logic here
return Flux.empty();
}
override fun readMono(elementType: ResolvableType, message: ReactiveHttpInputMessage, hints: Map<String, Any>): Mono<JsonPatch> {
return DataBufferUtils.join(message.body).map { buffer: DataBuffer ->
//TODO error handling
val reader = Json.createReader(buffer.asInputStream())
Json.createPatch(reader.readArray())
}
}
}
请注意,它尚未完全实现,您必须进行错误处理并实现 read(elementType: ResolvableType, message: ReactiveHttpInputMessage, hints: Map<String, Any>): Flux<JsonPatch>
。
现在您必须注册这个自定义解码器。在 spring webFlux 中,您可以通过创建 WebFluxConfigurer bean 来实现:
@Bean
fun webFluxConfigurer(): WebFluxConfigurer {
return object : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
configurer.customCodecs().register(JsonPatchHttpMessageConverter())
}
}
}
最后是路由器和处理程序:
@Configuration
class GreetingRouter {
@Bean
fun route(greetingHandler: GreetingHandler): RouterFunction<ServerResponse> {
return RouterFunctions
.route(RequestPredicates.POST("/hello")
.and(RequestPredicates.accept(MediaType.valueOf("application/json-patch+json"))), HandlerFunction { request: ServerRequest? -> greetingHandler.hello(request!!) })
}
}
@Component
class GreetingHandler {
fun hello(request: ServerRequest): Mono<ServerResponse> {
return request.bodyToMono(JsonPatch::class.java)
.flatMap { jsonPatch: JsonPatch ->
ServerResponse.ok().contentType(MediaType.valueOf("application/json-patch+json"))
.body(BodyInserters.fromValue("Received: $jsonPatch"))
}
}
}
最后,您可以使用 curl 调用此端点,例如:
curl -X POST \
http://localhost:8080/hello \
-H 'content-type: application/json-patch+json' \
-d '[
{
"op":"replace",
"path":"/email",
"value":"email@email.com"
}
]'
我目前正在摆弄一个 Spring Webflux 微服务,并尝试实现一个 @PatchMapping,它使用一个 JsonPatch 对象作为 @RequestBody,如下所示:
@PatchMapping("/{id}", consumes = ["application/json-patch+json"])
fun updateFriend( @PathVariable("id") id: Long,
@Valid @RequestBody jsonPatch: JsonPatch): Mono<UserFriends> {
// apply JsonPatch code here...
}
我使用 Postman 请求测试了端点,如下所示:
PATCH <microservice-uri>/friends/1
Content-Type = application/json-patch+json
RequestBody =
[
{
"op": "replace",
"path": "friends/11/since",
"value": "<sample value>"
}
]
这是我遇到的异常:
org.springframework.core.codec.CodecException: Type definition error: [simple type, class javax.json.JsonPatch]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `javax.json.JsonPatch` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (io.netty.buffer.ByteBufInputStream); line: 1, column: 1]
at org.springframework.http.codec.json.AbstractJackson2Decoder.processException(AbstractJackson2Decoder.java:211) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ HTTP PATCH "/friends/1" [ExceptionHandlingWebHandler]
<Additional stacktraces here ...>
我很清楚,我需要告诉我的微服务如何将 RequestBody 转换为 JsonPatch。但在失败了 3 天后,我决定寻求帮助。
在多次尝试失败后,我找到了 WebFluxConfigurer
并尝试覆盖 configureArgumentResolvers
。我在为 JsonPatch 编写 HandlerMethodArgumentResolver
时卡住了。
class JsonPatchResolver : HandlerMethodArgumentResolver {
override fun supportsParameter(parameter: MethodParameter): Boolean {
// Don't know what to implement here
}
override fun resolveArgument(parameter: MethodParameter, bindingContext: BindingContext, exchange: ServerWebExchange): Mono<Any> {
// Don't know how to create a JsonPatch from the given arguments
}
}
有人可以指出我正确的方向或告诉我我做错了什么吗?
非常感谢您!
编辑 这是我的 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>friend-info-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>friend-info-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
<kotlin.version>1.3.72</kotlin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-patch</artifactId>
<version>1.12</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr353</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>
<dependency>
<groupId>javax.json</groupId>
<artifactId>javax.json-api</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
编辑 2 以下不起作用:
class JsonPatchArgumentResolver : HandlerMethodArgumentResolver {
override fun supportsParameter(parameter: MethodParameter): Boolean {
// this doesn't event get called when debugging, so I suspect the handler isn't registered correctly
return parameter.parameterType == JsonPatch::class
}
override fun resolveArgument(parameter: MethodParameter, bindingContext: BindingContext, exchange: ServerWebExchange): Mono<Any> {
return exchange.request.body.toMono().map {
jacksonObjectMapper().readValue(it.asInputStream(), JsonPatch::class.java)
}
}
}
在我的申请中 class 我正在注册它(可能是错误的,我不知道):
@SpringBootApplication
@EnableReactiveMongoRepositories
@EnableWebFlux
@Configuration
class FriendInfoServiceApplication : WebFluxConfigurer {
override fun configureArgumentResolvers(configurer: ArgumentResolverConfigurer) {
configurer.addCustomResolver(JsonPatchArgumentResolver())
}
}
fun main(args: Array<String>) {
runApplication<FriendInfoServiceApplication>(*args)
}
编辑 3
在调试时我发现 configureArgumentResolvers
get 被调用了,所以也许我对 supportsParameter
?
编辑 4 我尝试从 THIS 博客 post 复制代码。科特林等价物应该是:
@Component
class JsonPatchHttpMessageConverter : AbstractHttpMessageConverter<JsonPatch>() {
@Throws(HttpMessageNotReadableException::class)
protected override fun readInternal(clazz: Class<out JsonPatch>, inputMessage: HttpInputMessage): JsonPatch {
try {
Json.createReader(inputMessage.body).use { reader -> return Json.createPatch(reader.readArray()) }
} catch (e: Exception) {
throw HttpMessageNotReadableException(e.message!!, inputMessage)
}
}
@Throws(HttpMessageNotWritableException::class)
protected override fun writeInternal(jsonPatch: JsonPatch, outputMessage: HttpOutputMessage) {
throw NotImplementedError("The write Json patch is not implemented")
}
protected override fun supports(clazz: Class<*>): Boolean {
return JsonPatch::class.java.isAssignableFrom(clazz)
}
}
并将 objectMapper 添加到应用程序 class 中,如下所示:
@SpringBootApplication
@EnableReactiveMongoRepositories
@EnableWebFlux
@Configuration
class FriendInfoServiceApplication {
@Bean
fun objectMapper(): ObjectMapper {
val objectMapper = ObjectMapper()
objectMapper.registerModule(JSR353Module())
return objectMapper
}
}
不幸的是,当尝试调用 PATCH 端点时,结果是相同的异常。
我是您提到的博客 post 的作者。我不是 Webflux 方面的专家,但在这种情况下,您必须将 JacksonConverter 重新实现为 webFlux 表单。
于是变成了这样:
import org.springframework.core.ResolvableType
import org.springframework.core.io.buffer.DataBuffer
import org.springframework.core.io.buffer.DataBufferUtils
import org.springframework.http.MediaType
import org.springframework.http.ReactiveHttpInputMessage
import org.springframework.http.codec.HttpMessageReader
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import javax.json.Json
import javax.json.JsonPatch
class JsonPatchHttpMessageConverter : HttpMessageReader<JsonPatch> {
override fun getReadableMediaTypes(): List<MediaType> {
return listOf(MediaType.valueOf("application/json-patch+json"))
}
override fun canRead(elementType: ResolvableType, mediaType: MediaType?): Boolean {
return MediaType.valueOf("application/json-patch+json").includes(mediaType)
}
override fun read(elementType: ResolvableType, message: ReactiveHttpInputMessage, hints: Map<String, Any>): Flux<JsonPatch> {
//TODO implement the same mono logic here
return Flux.empty();
}
override fun readMono(elementType: ResolvableType, message: ReactiveHttpInputMessage, hints: Map<String, Any>): Mono<JsonPatch> {
return DataBufferUtils.join(message.body).map { buffer: DataBuffer ->
//TODO error handling
val reader = Json.createReader(buffer.asInputStream())
Json.createPatch(reader.readArray())
}
}
}
请注意,它尚未完全实现,您必须进行错误处理并实现 read(elementType: ResolvableType, message: ReactiveHttpInputMessage, hints: Map<String, Any>): Flux<JsonPatch>
。
现在您必须注册这个自定义解码器。在 spring webFlux 中,您可以通过创建 WebFluxConfigurer bean 来实现:
@Bean
fun webFluxConfigurer(): WebFluxConfigurer {
return object : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
configurer.customCodecs().register(JsonPatchHttpMessageConverter())
}
}
}
最后是路由器和处理程序:
@Configuration
class GreetingRouter {
@Bean
fun route(greetingHandler: GreetingHandler): RouterFunction<ServerResponse> {
return RouterFunctions
.route(RequestPredicates.POST("/hello")
.and(RequestPredicates.accept(MediaType.valueOf("application/json-patch+json"))), HandlerFunction { request: ServerRequest? -> greetingHandler.hello(request!!) })
}
}
@Component
class GreetingHandler {
fun hello(request: ServerRequest): Mono<ServerResponse> {
return request.bodyToMono(JsonPatch::class.java)
.flatMap { jsonPatch: JsonPatch ->
ServerResponse.ok().contentType(MediaType.valueOf("application/json-patch+json"))
.body(BodyInserters.fromValue("Received: $jsonPatch"))
}
}
}
最后,您可以使用 curl 调用此端点,例如:
curl -X POST \
http://localhost:8080/hello \
-H 'content-type: application/json-patch+json' \
-d '[
{
"op":"replace",
"path":"/email",
"value":"email@email.com"
}
]'