由于 request.getSession() 为空,使用 webClient 时出现随机 NullPointerException / onErrorDropped

random NullPointerException / onErrorDropped using webClient, due to request.getSession() being null

我有一个 Spring Boot (2.5) 应用程序,我需要在其中对远程系统(我存储非规范化视图的 Solr 实例)进行 REST 调用,我可以在其中创建或更新记录。

我真的不关心我得到的响应(有时远程系统响应很慢),所以我在 createIndexForTicket / updateIndexForTicket 中进行这样的异步调用:

public MyService(WebClient webClient, String solrUpdateUrl) {
    this.webClient = webClient;
    this.solrUpdateUrl = solrUpdateUrl;
}

  public void createIndexForTicket(TicketIndex ticketIndex) {
    // build the request
    var createRequest = webClient.post()
        .uri(solrUpdateUrl);

    triggerRequest(createRequest, ticketIndex,"creation");

    log.info("payload sent, creating index for ticket {} : {}",ticketIndex.getUserFriendlyTicketId(),ticketIndex);
  }

  
  public void updateIndexForTicket(TicketIndex ticketIndex) {
    // build the request
    var updateRequest = webClient.put()
        .uri(solrUpdateUrl + "/" + ticketIndex.getInternalTicketId());

    triggerRequest(updateRequest, ticketIndex,"update");

    log.info("payload sent, updating index for ticket {} : {}",ticketIndex.getUserFriendlyTicketId(),ticketIndex);
  }

  private static void triggerRequest(RequestBodySpec requestToSolr,
                                                                 TicketIndex ticketIndex,
                                                                  String action) {

    requestToSolr.bodyValue(ticketIndex)
        .retrieve()
        .onStatus(HttpStatus::is2xxSuccessful,
                  resp -> logSuccess(ticketIndex,action))
        .bodyToMono(String.class)
        .doOnError(t ->
            log.error("problem while performing a "+action+", "
                + "calling Solr for ticket "+ticketIndex.getUserFriendlyTicketId(),t))
        .subscribe();
  }

它在大多数情况下工作正常。但我注意到我有时会收到 Operator called default onErrorDropped 错误,堆栈跟踪如下:

reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.NullPointerException
Caused by: java.lang.NullPointerException: null
    at org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository.saveAuthorizedClient(HttpSessionOAuth2AuthorizedClientRepository.java:63)
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    |_ checkpoint ⇢ Request to PUT https://myRemoteSolrSystem/services/v2/tickets/dGlja2V0aW5nLXNlcnZpY2UxNDEzNzM1 [DefaultWebClient]
Stack trace:
        at org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository.saveAuthorizedClient(HttpSessionOAuth2AuthorizedClientRepository.java:63)
        at org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository.saveAuthorizedClient(AuthenticatedPrincipalOAuth2AuthorizedClientRepository.java:92)
        at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager.lambda$new[=12=](DefaultOAuth2AuthorizedClientManager.java:126)
        at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager.authorize(DefaultOAuth2AuthorizedClientManager.java:184)
        at org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.lambda$authorizeClient(ServletOAuth2AuthorizedClientExchangeFilterFunction.java:552)
        at reactor.core.publisher.MonoSupplier.call(MonoSupplier.java:86)
        at reactor.core.publisher.FluxSubscribeOnCallable$CallableSubscribeOnSubscription.run(FluxSubscribeOnCallable.java:227)
        at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:68)
        at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:28)
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
        at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
        at java.base/java.lang.Thread.run(Thread.java:834)

查看源代码,我发现这导致 spring-security-oauth2-client 5.5.1,在 HttpSessionOAuth2AuthorizedClientRepository.saveAuthorizedClient

@Override
public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal,
        HttpServletRequest request, HttpServletResponse response) {
    Assert.notNull(authorizedClient, "authorizedClient cannot be null");
    Assert.notNull(request, "request cannot be null");
    Assert.notNull(response, "response cannot be null");
    Map<String, OAuth2AuthorizedClient> authorizedClients = this.getAuthorizedClients(request);
    authorizedClients.put(authorizedClient.getClientRegistration().getRegistrationId(), authorizedClient);
    request.getSession().setAttribute(this.sessionAttributeName, authorizedClients);
}

l.63,出现异常的地方是最后一个:

request.getSession().setAttribute(this.sessionAttributeName, authorizedClients);

所以它看起来像 request.getSession() returns 空...但我不知道为什么,我找不到模式。有时我从同一个线程连续触发 2 个调用,一个成功而另一个不成功。有时都失败,有时都成功。还有一次,我只触发了一个调用,但它失败了,而另一个线程同时或多或少地做了类似的事情,并且它起作用了。

被注入的 webClient 是这样构建的:

@Bean
@Primary
WebClient servletWebClient(ClientRegistrationRepository clientRegistrations,
                         OAuth2AuthorizedClientRepository authorizedClients) {

var oauth = new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);

oauth.setDefaultClientRegistrationId("keycloak");

return WebClient.builder()
    .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
    .apply(oauth.oauth2Configuration())
    .build();
}

关于我做错了什么,或者我可以尝试更好地理解正在发生的事情的任何提示?

谢谢

这是似乎有效的解决方法:

声明一个线程执行器:

 private final ExecutorService solrRequestExecutor = Executors.newSingleThreadExecutor();

然后,通过它进行异步调用:

private void triggerRequest(RequestBodySpec requestToSolr,
                                                             TicketIndex ticketIndex,
                                                              String action) {

// performing calls to Solr asynchronously
solrRequestExecutor.submit(
    () ->
requestToSolr.bodyValue(ticketIndex)
    .retrieve()
    .onStatus(HttpStatus::is2xxSuccessful,
              resp -> logSuccess(ticketIndex,action))
    .bodyToMono(String.class)
    .doOnError(t ->
        log.error("problem while performing a "+action+", "
            + "calling Solr for ticket "+ticketIndex.getUserFriendlyTicketId(),t))
    .block());
}

由于这不再在同一线程中执行,因此必须正确配置 webClient,否则我们会收到 servletRequest cannot be null 错误。见

我仍然不确定为什么原始代码会随机失败...它是否仅在远程端点也是反应性端点时才有效?