Spring WebClient调用API返回MonoFlatMapMany及证书异常排查
Spring WebClient调用HTTPS接口异常修复方案
问题复现
通过Postman调用目标接口可正常获取如下响应:
{ "loginAccounts": [ { "name": "Oracle", "accountId": "16485679", "baseUrl": "https://demo.docusign.net/restapi/v2.1/accounts/16485679", "isDefault": "true", "userName": "Mahima Pandey", "userId": "e48dbc73-97df-42de-be9f-ea49c8b24712", "email": "mahima.pandey@oracle.com", "siteDescription": "" } ] }
使用如下WebClient代码调用时无法得到正确结果:
public static Flux<LoginAccounts> retrieveDetails(){ Flux<LoginAccounts> loginAccounts=null; WebClient webclient = WebClient.create(); try { loginAccounts = webclient.get().uri(DocusignConstants.LOGIN_INFO_URL) .header("Accept", "application/json") .header("Authorization", "Bearer eyJ0eX...........") .retrieve() .bodyToFlux(LoginAccounts.class); System.out.println(loginAccounts); } catch(WebClientResponseException ex) { System.out.println(ex); } return loginAccounts; }
运行现象:
- 直接打印返回的
loginAccounts对象仅输出MonoFlatMapMany - 调用
loginAccounts.toStream().forEach(data -> System.out.println(data));遍历结果时抛出异常,核心报错为org.springframework.web.reactive.function.client.WebClientRequestException,嵌套异常为javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target,提示无法找到目标地址有效证书路径。
原有POJO定义如下:
@data @Noargsconstructor @Allargsconstructor public class LoginAccounts { private String name; private String accountId; private String baseUrl; private Boolean isDefault; private String userName; private String userId; private String email; private String siteDescription; }
问题根因
- SSL信任库差异:Postman自带独立证书信任库,会自动信任公开合法证书;JVM默认使用自身
jre/lib/security/cacerts作为信任库,未导入目标域名证书链时就会抛出SSL握手失败异常。 - 响应结构不匹配:接口最外层是包装对象,
loginAccounts是包装对象内部的数组字段,直接用bodyToFlux(LoginAccounts.class)无法正确解析嵌套结构。 - 响应式API使用错误:WebClient返回的
Flux/Mono是惰性执行流,仅拿到流对象不触发订阅时不会实际发起请求,直接打印对象只会得到流的内部实现类名称,不会返回实际响应;同时响应式流的异常在订阅执行时才会抛出,外层try-catch无法捕获。 - POJO定义错误:Lombok注解大小写不符合规范,
@data/@Noargsconstructor/@Allargsconstructor均为无效注解,不会生成getter/setter、构造方法;且接口返回的isDefault是字符串类型的"true",直接定义为Boolean类型会触发反序列化类型不匹配错误。
修复步骤
1. 修正POJO类定义
新增外层响应包装类,修正Lombok注解写法,调整字段类型适配接口返回值:
// 接口最外层包装对象 @Data @NoArgsConstructor @AllArgsConstructor public class LoginAccountResponse { private List<LoginAccount> loginAccounts; } // 单条账户数据实体,类名用单数更符合语义 @Data @NoArgsConstructor @AllArgsConstructor public class LoginAccount { private String name; private String accountId; private String baseUrl; // 接口返回字符串类型的"true"/"false",先用String接收,需要布尔值时可自行转换 private String isDefault; private String userName; private String userId; private String email; private String siteDescription; }
2. 解决SSL证书信任问题
开发测试环境可临时配置跳过SSL校验快速验证(生产环境严禁使用该配置,必须将目标域名正式证书导入JVM信任库):
// 开发测试用:构建跳过SSL校验的HTTP客户端 HttpClient httpClient = HttpClient.create() .secure(sslSpec -> { try { sslSpec.sslContext( SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .build() ); } catch (SSLException e) { throw new RuntimeException(e); } }); // 基于自定义客户端构建WebClient实例 WebClient webClient = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) .build();
生产环境正确处理方式:导出目标域名的完整证书链,使用
keytool命令将证书导入JVM的cacerts信任库,保留默认SSL校验逻辑。
3. 修正接口调用逻辑
调整反序列化目标类型,使用响应式原生操作符处理流和异常:
public static Flux<LoginAccount> retrieveDetails(){ // 开发测试环境用跳过SSL校验的客户端,生产环境替换为正常证书配置的WebClient HttpClient httpClient = HttpClient.create() .secure(sslSpec -> { try { sslSpec.sslContext( SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .build() ); } catch (SSLException e) { throw new RuntimeException(e); } }); WebClient webclient = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) .build(); return webclient.get().uri(DocusignConstants.LOGIN_INFO_URL) .header("Accept", "application/json") .header("Authorization", "Bearer 替换为你的有效Token") .retrieve() // 先解析为外层包装对象 .bodyToMono(LoginAccountResponse.class) // 拆出内部账户列表转为Flux流 .flatMapMany(resp -> Flux.fromIterable(resp.getLoginAccounts())) // 用响应式操作符处理异常,不要在外层写try-catch .doOnError(WebClientResponseException.class, ex -> { System.out.println("请求失败,状态码:" + ex.getStatusCode()); System.out.println("错误响应:" + ex.getResponseBodyAsString()); }); } // 调用方法时必须主动订阅,才会实际发起HTTP请求 public static void main(String[] args) { retrieveDetails() .subscribe(account -> { // 在这里处理拿到的账户数据 System.out.println("用户名:" + account.getUserName()); System.out.println("账户ID:" + account.getAccountId()); System.out.println("接口基地址:" + account.getBaseUrl()); }); }
4. 注意事项
- 不要直接调用
toStream()遍历Flux,会阻塞当前线程,违背响应式编程非阻塞设计原则,优先使用subscribe()、map()、flatMap()等非阻塞操作符处理数据。 - 可以直接复制Postman中生成的完整请求头参数到代码中,避免Token、Content-Type等参数拼写错误、Token过期等问题。
内容的提问来源于stack exchange,提问作者Mahima
相关产品推荐
相关产品推荐

