我正在阅读一个使用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();
[...]
}
1条答案
按热度按时间lxkprmvk1#
似乎内容头hal+json是缺少的部分,尽管我确信我以前尝试过这个。在这两者之间修复之前,可能还有其他问题。至少测试用例现在正在处理这个问题: