最近遇到一个棘手的问题,有个需求需要写一些调用外部API接口的方法,因为没有SDK,所以我只能手动的去调用,框架采用的是springboot,Java语言编写,但是却出现了当调用异常,包括参数错误,token无效等直接抛出异常400,并没有获取到API接口的异常信息,只有我们自己抛出的异常信息。这样接口的调用方肯定不知道接口调用异常原因。
一、RestTemple实现API调用
我采用的使用springboot的restTemple来去调用接口,具体的实现是先定义一个controller接口,然后再在service层进行逻辑处理。
@Autowired
private RestTemplate restTemplate=new RestTemplate();
//service层代码
public String updateProduct(ProductParam productParam) {
//spliceupdateProductUrl方法是拼接接口参数
String url = spliceupdateProductUrl(productParam,"--此处填写API的接口地址--");
//调用restTemple的get请求方法,直接返回response地址
String response = restTemplate.getForObject(url,String.class);
return response;
}
成功的结果:我们要求都是返回这种格式
{"message":"","code":0,"data":{}}
失败的结果:
{
"timestamp": "2020-05-29T07:44:38.031+0000",
"status": 500,
"error": "Internal Server Error",
"message": "400 Bad Request,
"path": "/updateProduct"
}
直接抛出异常,message是调用接口的异常信息,这里缺少一部分说明,为什么我会知道呢?因为我试过直接在postman上调用API的时候,相同的url和参数,postman是有说明的,如下:
{"message":"111 is an invalid id","code":1000,"data":2014}
二、追踪源代码寻找问题
我首先是通过debug追踪源码的方式去找问题所在:
第一步:RestTemple.class
第二步:RestTemple.class
第三步:RestTemple.class
第四步:RestTemple.class
第五步:RestTemple.class
第六步:ResponseErrorHandler.class
第七步:ResponseErrorHandler.class的默认处理器,继续追踪
第八步:DefaultResponseErrorHandler.class
第九步:DefaultResponseErrorHandler.class,这里我发现一个问题,算是机缘巧合,我发现spring不同版本存在Restemple的
DefaultResponseErrorHandler源码存在差异,在下面这个方法的差异
2.2.1.RELEASE版本的DefaultResponseErrorHandler.class
2.2.6.RELEASE版本的DefaultResponseErrorHandler.class
三、尝试解决问题,不断调试
从我们不断的追踪源码发现了一些问题,这个时候我们可以升级一下spring的版本,再次运行上述代码
成功的结果:我们要求都是返回这种格式
{"message":"","code":0,"data":{}}
失败的结果:
{
"timestamp": "2020-05-29T07:44:38.031+0000",
"status": 500,
"error": "Internal Server Error",
"message": "400 Bad Request: [{"message":"111 is an invalid id","code":1000,"data":2014}],
"path": "/updateProduct"
}
这个时候我们已经可以打印出了api接口的message,但是发现返回的结果是抛出的异常信息,默认的errorhandler是通过封装接口的异常信息到自己的异常中,然后再抛出异常,因此,我们可以选择捕获异常的message。
捕获异常:
@Override
public String updateProduct(ProductParam productParam) {
String url = spliceupdateProductUrl(productParam,"--api接口url--");
try {
String response = restTemplate.getForObject(url,String.class);
return response;
}catch (Exception e){
return e.getMessage();
}
}
抛出异常运行结果:
400 Bad Request: [{"message":"111 is an invalid id","code":1000,"data":2014}]
这个时候我们只要json字符串怎么办?
{"message":"111 is an invalid id","code":1000,"data":2014}
两种处理方法:
一 自定义异常处理器
就可以看到我们上面发现的自定义异常处理器了,我们可以自定义异常处理器,重写几个debug的时候走过的DefaultErrorHandle的方法,来自定义Message,不仅如此,我们也可以自定义一些其他的操作,例如编码格式转换等等。
debug找到打印信息的地方,进入getErrorMessage方法看:
、
第一步:写一个异常处理器
public class ReturnErrorJsonHandler extends DefaultResponseErrorHandler {
//调用父类方法就行
public boolean hasError(ClientHttpResponse response) throws IOException {
return super.hasError(response);
}
//重写该方法调用重写的getErrorMessage方法
public void handleError(ClientHttpResponse response) throws IOException {
HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
if (statusCode == null) {
byte[] body = this.getResponseBody(response);
String message = this.getErrorMessage( body, this.getCharset(response));
throw new UnknownHttpStatusCodeException(message, response.getRawStatusCode(), response.getStatusText(), response.getHeaders(), body, this.getCharset(response));
} else {
this.handleError(response, statusCode);
}
}
//重写该方法,让它返回我们要的格式
private String getErrorMessage( @Nullable byte[] responseBody, @Nullable Charset charset) {
if (ObjectUtils.isEmpty(responseBody)) {
return "[no body]";
} else {
charset = charset == null ? StandardCharsets.UTF_8 : charset;
int maxChars = 200;
if (responseBody.length < maxChars * 2) {
//主要是这里
String message= new String(responseBody, charset);
//Unicode 转换为汉字
return UnicodeToString(message);
} else {
try {
Reader reader = new InputStreamReader(new ByteArrayInputStream(responseBody), charset);
CharBuffer buffer = CharBuffer.allocate(maxChars);
reader.read(buffer);
reader.close();
buffer.flip();
return buffer.toString() + "... (" + responseBody.length + " bytes)";
} catch (IOException var9) {
throw new IllegalStateException(var9);
}
}
}
}
/**
* 将 Unicode 转换为汉字
* */
public String UnicodeToString(String str) {
Pattern pattern = Pattern.compile("(\\\\u(\\p{XDigit}{4}))");
Matcher matcher = pattern.matcher(str);
char ch;
while (matcher.find()) {
ch = (char) Integer.parseInt(matcher.group(2), 16);
str = str.replace(matcher.group(1), ch + "");
}
return str;
}
//重写该方法调用重写的getErrorMessage方法
protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
String statusText = response.getStatusText();
HttpHeaders headers = response.getHeaders();
byte[] body = this.getResponseBody(response);
Charset charset = this.getCharset(response);
String message = this.getErrorMessage(body, charset);
switch(statusCode.series()) {
case CLIENT_ERROR:
throw HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset);
case SERVER_ERROR:
throw HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset);
default:
throw new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body, charset);
}
}
}
uincode码和uincode转换为汉字参考链接:
第二步:RestTemple放入自定义处理器
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate=new RestTemplate();
restTemplate.setErrorHandler(new ReturnErrorJsonHandler());
return restTemplate;
}
二 捕获HttpClientErrorException
运行结果:现在即使是异常我们也可以直接抛出json数据了
{"message":"111 is an invalid id","code":1000,"data":2014}
四、总结
每当我们解决一个问题,都需要去做一个总结,从这次解决的手段来看,如果你不想要升级springboot的版本,你可能使用第二种方法,因为旧版本的异常类是没有封装message的。同时,我们发现,会看源码真的很重要,特别有利于我们去解决很多问题,debug追踪源码是很好的一种方法,发现问题,找到问题在寻求解决问题的方式。