日常工作中肯定会遇到服务之间的调用,尤其是现在都是微服务的架构,所以总结一下restTemplate的最常用的用法以及自己踩过的坑。
restTemplate底层调用的是Execute方法,而Execute底层调用的是doExecute,它是基于http协议的,底层还是httpClient 的使用。
/** * Execute the given method on the provided URI. * <p>The {@link ClientHttpRequest} is processed using the {@link RequestCallback}; * the response with the {@link ResponseExtractor}. * @param url the fully-expanded URL to connect to * @param method the HTTP method to execute (GET, POST, etc.) * @param requestCallback object that prepares the request (can be {@code null}) * @param responseExtractor object that extracts the return value from the response (can be {@code null}) * @return an arbitrary object, as returned by the {@link ResponseExtractor} */ @Nullable protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException { Assert.notNull(url, "URI is required"); Assert.notNull(method, "HttpMethod is required"); ClientHttpResponse response = null; try { ClientHttpRequest request = createRequest(url, method); if (requestCallback != null) { requestCallback.doWithRequest(request); } response = request.execute(); handleResponse(url, method, response); return (responseExtractor != null ? responseExtractor.extractData(response) : null); } catch (IOException ex) { String resource = url.toString(); String query = url.getRawQuery(); resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource); throw new ResourceAccessException("I/O error on " + method.name() + " request for /"" + resource + "/": " + ex.getMessage(), ex); } finally { if (response != null) { response.close(); } } } 复制代码
我们一般都是用的restTepmlate的exchange方法,这个方法比较灵活,可以接受可变参数,重载方法也有很多。 当然 restTemplate还有其他很多方法,而且遵循restFul风格,像PUT POST GET PATCH DELETE 等都有对应的方法,按需使用。这里就不贴源码了。
然后就贴一个使用案例代码上来:
public YourResponse sampleRestTepmlate (YourRequest request) throws Exception { UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(this.serviceUrl); builder.path("urlpath"); log.info("url : {}, request : {}", builder.toUriString(), JsonUtils.toJson(request)); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("headername","headervalue"); headers.add("anotherway", "value"); HttpEntity<YourRequest> requestEntity = new HttpEntity<>(request, headers); ResponseEntity<YourResponse> responseEntity = null; try { responseEntity = restTemplate.exchange(builder.toUriString(), HttpMethod.POST, requestEntity, YourResponse.class); return responseEntity.getBody(); } catch (Exception e) { log.error("exception:{}",e.getMessage()); } } 复制代码
这里就要说一下我遇到的坑了。 在使用restTemplate的时候,当你的call没有成功返回200的时候,比如返回400 500之类的,restTemplate里面有一个DefaultResponseErrorHandler,他会自动拦截住这些httpstatus 为400 500的response然后给你抛出一个异常。这就意味着,当你也想拿到带有错误信息的response的时候,他不会给你!它会给你抛出exception并且只是给你返回一个简单的类似500 Internal error! WTF!
贴上这段坑爹的代码:
/** * Handle the error in the given response with the given resolved status code. * <p>This default implementation throws a {@link HttpClientErrorException} if the response status code * is {@link org.springframework.http.HttpStatus.Series#CLIENT_ERROR}, a {@link HttpServerErrorException} * if it is {@link org.springframework.http.HttpStatus.Series#SERVER_ERROR}, * and a {@link RestClientException} in other cases. * @since 5.0 */ protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException { switch (statusCode.series()) { case CLIENT_ERROR: throw new HttpClientErrorException(statusCode, response.getStatusText(), response.getHeaders(), getResponseBody(response), getCharset(response)); case SERVER_ERROR: throw new HttpServerErrorException(statusCode, response.getStatusText(), response.getHeaders(), getResponseBody(response), getCharset(response)); default: throw new UnknownHttpStatusCodeException(statusCode.value(), response.getStatusText(), response.getHeaders(), getResponseBody(response), getCharset(response)); } } 复制代码
遇到了坑就不要害怕,这个问题可以这么解决:
1.不用restTemplate去请求,可以采用httpClient底层去实现
2.重写handleError方法,自定义ErrorHandle继承DefaultResponseErrorHandler
在已经写完实现之后,我选择方式2 : )
@Builder @Slf4j public class MyErrorHandle extends DefaultResponseErrorHandler { @Override public void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException { int status = statusCode.value(); if (status == 200 || status == 400 || status == 500) { //do what u want to do } else { super.handleError(response,statusCode); } } } 复制代码
然后在初始化restTemplate的时候调用setErrorHandle方法就可以了。
restTemplate.setErrorHandler(YourErrorHandle). 复制代码
至于方式一这里不提了。
有的时候当我们调用对方的server时,基于https 的协议是需要导入证书的,那我们该怎么把证书融入到restTemplate中呢?(又一个坑)
@Bean public RestTemplate buildRestTemplateWithinSSl(@Value("${service.connectTimeout}") int connectTimeout, @Value("${service.readTimeout}") int readTimeout, @Value("${service.sslFilePath}") String filePath, @Value("${service.sslpassword}") String sslPassword) throws Exception{ RestTemplate template = restTemplateBuilder.setConnectTimeout(connectTimeout).setReadTimeout(readTimeout).build(); String workingDirectory = BeanUtility.getWorkingDirectory(); SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(new File(workingDirectory + "/" + filePath), sslPassword.toCharArray()).build(); SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory).build(); HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient); template.setRequestFactory(factory); return template; } 复制代码
相当于重新给RequestFactory值,构造一个已经带有ssl证书的factory给他。
这里注意两个地方:
SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); 复制代码
这里有个参数是NoopHostnameVerifier.INSTANCE, 这里是可以无视ip的,也就是ip或者域名形式都可以。 (适用于对方给我提供证书和 ip地址,试了半天死活不通的情况。。)
第二个就是一个工具类的使用,我相信很多时候new file的时候很容易被路径绕晕。
String workingDirectory = BeanUtility.getWorkingDirectory(); 复制代码
这个工具类获得的路径不用你去担心,只要你的jks文件和你的jar包同级就行。管他什么环境什么路径,很方便。
本地调试证书导入jdk就行。
记录下导入证书的方法:
keytool -import -alias {别名} -file {路径/证书名}.cer -keystore "{jdk路径/jre/lib/security/cacerts}" -storepass {password} -trustcacerts 复制代码
删除证书:
keytool -delete -alias {别名} -keystore "C:/Program Files/Java/jdk1.7.0_25/jre/lib/security/cacerts" -storepass {password} 复制代码
查看所有安装证书列表
keytool -list -v -keystore "C:/Users/1580977/Downloads/jdk1.8.0_101/jre/lib/security/cacerts" -storepass {password} >> C:/Desktop/abcd.txt 复制代码
生成jks文件 (没有默认生存,有就导入)
keytool -import -alias {别名} -file {证书名}.cer -keystore {命名}.jks 复制代码