javax. json .JsonPatch не поддерживается как @RequestBody в микросервисе Spring Webflux - PullRequest
0 голосов
/ 25 мая 2020

В настоящее время я возился с микросервисом Spring Webflux и пытаюсь реализовать @PatchMapping, который потребляет объект JsonPatch как @RequestBody, например: так:

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. Но после трех дней неудач я решил получить некоторую помощь.

После многих неудачных попыток я нашел WebFluxConfigurer и попытался заменить configureArgumentResolvers. Я застрял при написании HandlerMethodArgumentResolver для JsonPatch.

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
    }
}

Может кто-нибудь указать мне правильное направление или сказать, что я делаю не так?

Заранее большое спасибо!

EDIT Вот мой пом. 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>

EDIT 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)
        }
    }

}

И в моем классе приложения я регистрирую его (может быть, ошибаюсь, я не знаю):

@SpringBootApplication
@EnableReactiveMongoRepositories
@EnableWebFlux
@Configuration
class FriendInfoServiceApplication : WebFluxConfigurer {
    override fun configureArgumentResolvers(configurer: ArgumentResolverConfigurer) {
        configurer.addCustomResolver(JsonPatchArgumentResolver())
    }
}

fun main(args: Array<String>) {
    runApplication<FriendInfoServiceApplication>(*args)
}

EDIT 3 Во время отладки я обнаружил, что вызывается configureArgumentResolvers get, так что, возможно, я делаю что-то не так с supportsParameter?

EDIT 4 Я попытался скопировать код из ЭТО сообщение в блоге. Эквивалент kotlin должен быть:


@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 в класс Application следующим образом:

@SpringBootApplication
@EnableReactiveMongoRepositories
@EnableWebFlux
@Configuration
class FriendInfoServiceApplication {
    @Bean
    fun objectMapper(): ObjectMapper {
        val objectMapper = ObjectMapper()
        objectMapper.registerModule(JSR353Module())
        return objectMapper
    }
}

Результат, к сожалению, такое же исключение при попытке вызвать PATCH конечная точка.

1 Ответ

1 голос
/ 26 мая 2020

Я автор упомянутого вами сообщения в блоге. Я не эксперт в 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 вы можете сделать это, создав bean-компонент WebFluxConfigurer:

@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"
   }
]'
...