SpringWebClient不读取超媒体链接

x6yk4ghg  于 2021-07-13  发布在  Java
关注(0)|答案(1)|浏览(388)

我正在阅读一个使用spring的webclient的带有超媒体链接和oauth2身份验证的外部api。在访问api时,json数据被正确地转换为模型对象,但是如果模型对象扩展了spring hateoas representationmodel,则提供的hal链接将被忽略,或者如果模型对象扩展了entitymodel,则提供nullpointerexception。我怀疑hypermediawebclientcustomizer有问题,但到目前为止无法解决。
我尝试在一个testcase中用一个traverson客户机读取json。如果我用绝对uri替换相对uri,用application/hal+json头替换application/json头,基本上就可以工作了。我将继续讨论traverson,但是除了这两个问题之外,traverson还需要restemplate(本例中为oauth2restemplate),这在我们的spring版本中不再可用。
如果配置有问题或者还有什么问题,你有什么想法吗?
这是我的配置:
依赖项(部分)

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.5.RELEASE</version>
        <relativePath/>
    </parent>

[...]

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-hateoas</artifactId>
        </dependency>

    [...]

        <!-- swagger dependencies -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>3.0.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>3.0.0</version>
        </dependency>

    [...]

    </dependencies>

应用程序配置

@SpringBootApplication
@EnableScheduling
@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
public class MyApplication extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(MyApplication.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

网络客户端配置

@Configuration
@Slf4j
public class WebClientConfig {

    private static final String REGISTRATION_ID = "myapi";

    @Bean
    ReactiveClientRegistrationRepository getRegistration(
            @Value("${spring.security.oauth2.client.provider.myapi.token-uri}") String tokenUri,
            @Value("${spring.security.oauth2.client.registration.myapi.client-id}") String clientId,
            @Value("${spring.security.oauth2.client.registration.myapi.client-secret}") String clientSecret
    ) {
        ClientRegistration registration = ClientRegistration
                .withRegistrationId(REGISTRATION_ID)
                .tokenUri(tokenUri)
                .clientId(clientId)
                .clientSecret(clientSecret)
                //.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                .build();
        return new InMemoryReactiveClientRegistrationRepository(registration);
    }

    @Bean
    WebClientCustomizer hypermediaWebClientCustomizer(HypermediaWebClientConfigurer configurer) {
        return webClientBuilder -> {
            configurer.registerHypermediaTypes(webClientBuilder);
        };
    }

    @Bean
    public WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations,
                               WebClient.Builder webClientBuilder){
        InMemoryReactiveOAuth2AuthorizedClientService clientService =
                new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrations);
        AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager =
                new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, clientService);
        ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
                new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        oauth.setDefaultClientRegistrationId(REGISTRATION_ID);

        webClientBuilder
                .defaultHeaders(header -> header.setBearerAuth("TestToken"))
                .filter(oauth);

        if (log.isDebugEnabled()) {
            webClientBuilder
                    .filter(logRequest())
                    .filter(logResponse());
        }

        return webClientBuilder.build();
    }

    private ExchangeFilterFunction logRequest() {
        return (clientRequest, next) -> {
            log.info("Request: {} {}", clientRequest.method(), clientRequest.url());
            clientRequest.headers()
                    .forEach((name, values) -> values.forEach(value -> log.info("{}={}", name, value)));
            return next.exchange(clientRequest);
        };
    }

    private ExchangeFilterFunction logResponse() {
        return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
            clientResponse.headers().asHttpHeaders()
                    .forEach((name, values) -> values.forEach(value -> log.info("{}={}", name, value)));
            return Mono.just(clientResponse);
        });
    }
}

示例模型对象

public class Person extends EntityModel<Person> {

    @JsonProperty("person_id")
    private String personId;

    private String name;

    @JsonProperty("external_reference")
    private String externalReference;

    @JsonProperty("custom_properties")
    private List<String> customProperties;

    [...]

}

webclient使用示例

return webClient.get()
                .uri(baseUrl + URL_PERSONS + "/" + id)
                .exchange()
                .flatMap(clientResponse -> clientResponse.bodyToMono(Person.class));

来自外部api的示例json(链接部分给了我一个具有上述模型的npe,stacktrace如下,或者如果我让person扩展representationmodel,它就丢失了)

{
  "_links": {
    "self": {
      "href": "/api/v1/persons/2f75ab34ea48cab4d4354e4a"
    },
    "properties": {
      "href": "/api/v1/persons/2f75ab34ea48cab4d4354e4a/properties"
    },
    [...]
  },
  "person_id": "2f75ab34ea48cab4d4354e4a",
  "name": "Jim Doyle",
  "external_reference": "1006543",
  "custom_properties": null,
  [...]
}

具有entitymodel的npe堆栈跟踪

org.springframework.core.codec.DecodingException: JSON decoding error: (was java.lang.NullPointerException); nested exception is com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: net.bfgh.api.myapi.model.Person["_links"])

    at org.springframework.http.codec.json.AbstractJackson2Decoder.processException(AbstractJackson2Decoder.java:215)
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    |_ checkpoint ⇢ Body from GET http://127.0.0.1:52900 [DefaultClientResponse]
Stack trace:
        at org.springframework.http.codec.json.AbstractJackson2Decoder.processException(AbstractJackson2Decoder.java:215)
        at org.springframework.http.codec.json.AbstractJackson2Decoder.decode(AbstractJackson2Decoder.java:173)
        at org.springframework.http.codec.json.AbstractJackson2Decoder.lambda$decodeToMono$1(AbstractJackson2Decoder.java:159)
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:118)

    [...]

Caused by: java.lang.NullPointerException
    at com.fasterxml.jackson.databind.deser.SettableAnyProperty.deserialize(SettableAnyProperty.java:153)
    at com.fasterxml.jackson.databind.deser.SettableAnyProperty.deserializeAndSet(SettableAnyProperty.java:134)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownVanilla(BeanDeserializerBase.java:1576)
    ... 52 more

导致上述错误的testcase

@Autowired
    private WebClient webClient;

    @Value(value = "classpath:json-myapi/person.json")
    private Resource personJson;

    @Before
    public void init() throws IOException {
        mockWebServer = new MockWebServer();
        mockWebServer.start(52900);
        mockBaseUrl = "http://" + mockWebServer.getHostName() + ":" + mockWebServer.getPort();
        [...]
    }

    @Test
    public void testSinglePersonJsonToHypermediaModel() throws IOException {
        MockResponse mockResponse = new MockResponse()
                .addHeader("Content-Type", "application/json") // API's original content type, but also tried setting application/hal+json here
                .setBody(new String(personJson.getInputStream().readAllBytes()));
        mockWebServer.enqueue(mockResponse);

        Person model = webClient.get().uri(mockBaseUrl).exchange()
                .flatMap(clientResponse -> clientResponse.bodyToMono(Person.class)).block();
        Assertions.assertThat(model).isNotNull();
        Assertions.assertThat(model.getName()).isEqualTo("Jim Doyle");
        [...]
        Assertions.assertThat(model.getLinks().hasSize(7)).isTrue();
        [...]
    }
lxkprmvk

lxkprmvk1#

似乎内容头hal+json是缺少的部分,尽管我确信我以前尝试过这个。在这两者之间修复之前,可能还有其他问题。至少测试用例现在正在处理这个问题:

MockResponse mockResponse = new MockResponse()
                .addHeader("Content-Type", "application/hal+json") //      <-- hal+json! 
                .setBody(new String(personJson.getInputStream().readAllBytes()));

相关问题