RestTemplate实践(及遇到的问题)

tech2022-08-29  113

点击上方 "程序员小乐"关注, 星标或置顶一起成长

每天凌晨00点00分, 第一时间与你相约

每日英文

A friend who understands your tears is much more valuable than a lot of friends who only know your smile.

一个懂你泪水的朋友,胜过一群只懂你笑容的朋友。

每日掏心话

生活本身是简单的,快乐也很简单,一切精彩都蕴含在朴素而简单生活中,想得太复杂,往往感受不到简单的快乐。

来自:duanxz | 责编:乐乐

链接:cnblogs.com/duanxz/p/3510622.html

程序员小乐(ID:study_tech)第 976 次推文  图源:百度

往日回顾:因用了Insert into select语句,美女同事被开除了!

     

   正文   

在微服务都是以HTTP接口的形式暴露自身服务的,因此在调用远程服务时就必须使用HTTP客户端。我们可以使用JDK原生的URLConnection、Apache的Http Client、Netty的异步HTTP Client, Spring的RestTemplate。但是,用起来最方便、最优雅的还是要属Feign了。这里介绍的是RestTemplate。

什么是RestTemplate?

RestTemplate是Spring提供的用于访问Rest服务的客户端,RestTemplate提供了多种便捷访问远程Http服务的方法,能够大大提高客户端的编写效率。

调用RestTemplate的默认构造函数,RestTemplate对象在底层通过使用java.net包下的实现创建HTTP 请求,可以通过使用ClientHttpRequestFactory指定不同的HTTP请求方式。

ClientHttpRequestFactory接口主要提供了两种实现方式

一种是SimpleClientHttpRequestFactory,使用J2SE提供的方式(既java.net包提供的方式)创建底层的Http请求连接。

一种方式是使用HttpComponentsClientHttpRequestFactory方式,底层使用HttpClient访问远程的Http服务,使用HttpClient可以配置连接池和证书等信息。

RestTemplate的核心之一 Http Client。

目前通过RestTemplate 的源码可知,RestTemplate 可支持多种 Http Client的http的访问,如下所示:

基于 JDK HttpURLConnection 的 SimpleClientHttpRequestFactory,默认。

基于 Apache HttpComponents Client 的 HttpComponentsClientHttpRequestFactory

基于 OkHttp3的OkHttpClientHttpRequestFactory。

基于 Netty4 的 Netty4ClientHttpRequestFactory。

其中HttpURLConnection 和 HttpClient 为原生的网络访问类,OkHttp3采用了 OkHttp3的框架,Netty4 采用了Netty框架。

xml配置的方式

请查看RestTemplate源码了解细节,知其然知其所以然!

RestTemplate默认是使用SimpleClientHttpRequestFactory,内部是调用jdk的HttpConnection,默认超时为-1

@Autowired RestTemplate simpleRestTemplate; @Autowired RestTemplate restTemplate;

基于jdk的spring的RestTemplate

<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans"        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"        default-autowire="byName" default-lazy-init="true">       <!--方式一、使用jdk的实现-->     <bean id="ky.requestFactory" class="org.springframework.http.client.SimpleClientHttpRequestFactory">         <property name="readTimeout" value="10000"/>         <property name="connectTimeout" value="5000"/>     </bean>       <bean id="simpleRestTemplate" class="org.springframework.web.client.RestTemplate">         <constructor-arg ref="ky.requestFactory"/>         <property name="messageConverters">             <list>                 <bean class="org.springframework.http.converter.FormHttpMessageConverter"/>                 <bean class="org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter"/>                 <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>                 <bean class="org.springframework.http.converter.StringHttpMessageConverter">                     <property name="supportedMediaTypes">                         <list>                             <value>text/plain;charset=UTF-8</value>                         </list>                     </property>                 </bean>             </list>         </property>     </bean> </beans>

使用Httpclient连接池的方式

<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans"        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"        default-autowire="byName" default-lazy-init="true">       <!--方式二、使用httpclient的实现,带连接池-->     <bean id="ky.pollingConnectionManager" class="org.apache.http.impl.conn.PoolingHttpClientConnectionManager">         <!--整个连接池的并发-->         <property name="maxTotal" value="1000" />         <!--每个主机的并发-->         <property name="defaultMaxPerRoute" value="1000" />     </bean>       <bean id="ky.httpClientBuilder" class="org.apache.http.impl.client.HttpClientBuilder" factory-method="create">         <property name="connectionManager" ref="ky.pollingConnectionManager" />         <!--开启重试-->         <property name="retryHandler">             <bean class="org.apache.http.impl.client.DefaultHttpRequestRetryHandler">                 <constructor-arg value="2"/>                 <constructor-arg value="true"/>             </bean>         </property>         <property name="defaultHeaders">             <list>                 <bean class="org.apache.http.message.BasicHeader">                     <constructor-arg value="User-Agent"/>                     <constructor-arg value="Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36"/>                 </bean>                 <bean class="org.apache.http.message.BasicHeader">                     <constructor-arg value="Accept-Encoding"/>                     <constructor-arg value="gzip,deflate"/>                 </bean>                 <bean class="org.apache.http.message.BasicHeader">                     <constructor-arg value="Accept-Language"/>                     <constructor-arg value="zh-CN"/>                 </bean>             </list>         </property>     </bean>       <bean id="ky.httpClient" factory-bean="ky.httpClientBuilder" factory-method="build" />       <bean id="ky.clientHttpRequestFactory" class="org.springframework.http.client.HttpComponentsClientHttpRequestFactory">         <constructor-arg ref="ky.httpClient"/>         <!--连接超时时间,毫秒-->         <property name="connectTimeout" value="5000"/>         <!--读写超时时间,毫秒-->         <property name="readTimeout" value="10000"/>     </bean>       <bean id="restTemplate" class="org.springframework.web.client.RestTemplate">         <constructor-arg ref="ky.clientHttpRequestFactory"/>         <property name="errorHandler">             <bean class="org.springframework.web.client.DefaultResponseErrorHandler"/>         </property>         <property name="messageConverters">             <list>                 <bean class="org.springframework.http.converter.FormHttpMessageConverter"/>                 <bean class="org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter"/>                 <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>                 <bean class="org.springframework.http.converter.StringHttpMessageConverter">                     <property name="supportedMediaTypes">                         <list>                             <value>text/plain;charset=UTF-8</value>                         </list>                     </property>                 </bean>             </list>         </property>     </bean>   </beans>

bean初始化+静态工具

线程安全的单例(懒汉模式)

搜索公众号程序员小乐回复关键字“offer”获取算法面试题和答案。

基于jdk的spring的RestTemplate

import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; import org.springframework.stereotype.Component; import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.RestTemplate;   import javax.annotation.PostConstruct; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List;   @Component @Lazy(false) public class SimpleRestClient {       private static final Logger LOGGER = LoggerFactory.getLogger(SimpleRestClient.class);       private static RestTemplate restTemplate;       static {         SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();         requestFactory.setReadTimeout(5000);         requestFactory.setConnectTimeout(5000);           // 添加转换器         List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();         messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));         messageConverters.add(new FormHttpMessageConverter());         messageConverters.add(new MappingJackson2XmlHttpMessageConverter());         messageConverters.add(new MappingJackson2HttpMessageConverter());           restTemplate = new RestTemplate(messageConverters);         restTemplate.setRequestFactory(requestFactory);         restTemplate.setErrorHandler(new DefaultResponseErrorHandler());           LOGGER.info("SimpleRestClient初始化完成");     }       private SimpleRestClient() {       }       @PostConstruct     public static RestTemplate getClient() {         return restTemplate;     }   }

使用Httpclient连接池的方式

import org.apache.http.Header; import org.apache.http.client.HttpClient; import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy; import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.message.BasicHeader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; import org.springframework.stereotype.Component; import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.RestTemplate;   import javax.annotation.PostConstruct; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit;   @Component @Lazy(false) public class RestClient {       private static final Logger LOGGER = LoggerFactory.getLogger(RestClient.class);       private static RestTemplate restTemplate;       static {         // 长连接保持30秒         PoolingHttpClientConnectionManager pollingConnectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);         // 总连接数         pollingConnectionManager.setMaxTotal(1000);         // 同路由的并发数         pollingConnectionManager.setDefaultMaxPerRoute(1000);           HttpClientBuilder httpClientBuilder = HttpClients.custom();         httpClientBuilder.setConnectionManager(pollingConnectionManager);         // 重试次数,默认是3次,没有开启         httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(2, true));         // 保持长连接配置,需要在头添加Keep-Alive         httpClientBuilder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());   //        RequestConfig.Builder builder = RequestConfig.custom(); //        builder.setConnectionRequestTimeout(200); //        builder.setConnectTimeout(5000); //        builder.setSocketTimeout(5000); // //        RequestConfig requestConfig = builder.build(); //        httpClientBuilder.setDefaultRequestConfig(requestConfig);           List<Header> headers = new ArrayList<>();         headers.add(new BasicHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36"));         headers.add(new BasicHeader("Accept-Encoding", "gzip,deflate"));         headers.add(new BasicHeader("Accept-Language", "zh-CN"));         headers.add(new BasicHeader("Connection", "Keep-Alive"));           httpClientBuilder.setDefaultHeaders(headers);           HttpClient httpClient = httpClientBuilder.build();           // httpClient连接配置,底层是配置RequestConfig         HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);         // 连接超时         clientHttpRequestFactory.setConnectTimeout(5000);         // 数据读取超时时间,即SocketTimeout         clientHttpRequestFactory.setReadTimeout(5000);         // 连接不够用的等待时间,不宜过长,必须设置,比如连接不够用时,时间过长将是灾难性的         clientHttpRequestFactory.setConnectionRequestTimeout(200);         // 缓冲请求数据,默认值是true。通过POST或者PUT大量发送数据时,建议将此属性更改为false,以免耗尽内存。         // clientHttpRequestFactory.setBufferRequestBody(false);           // 添加内容转换器         List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();         messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));         messageConverters.add(new FormHttpMessageConverter());         messageConverters.add(new MappingJackson2XmlHttpMessageConverter());         messageConverters.add(new MappingJackson2HttpMessageConverter());           restTemplate = new RestTemplate(messageConverters);         restTemplate.setRequestFactory(clientHttpRequestFactory);         restTemplate.setErrorHandler(new DefaultResponseErrorHandler());           LOGGER.info("RestClient初始化完成");     }       private RestClient() {       }       @PostConstruct     public static RestTemplate getClient() {         return restTemplate;     }   } @Configuration public class RestConfig {     @Bean     public RestTemplate restTemplate(){         RestTemplate restTemplate = new RestTemplate();         return restTemplate;     }     @Bean("urlConnection")     public RestTemplate urlConnectionRestTemplate(){         RestTemplate restTemplate = new RestTemplate(new SimpleClientHttpRequestFactory());         return restTemplate;     }     @Bean("httpClient")     public RestTemplate httpClientRestTemplate(){         RestTemplate restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());         return restTemplate;     }     @Bean("oKHttp3")     public RestTemplate OKHttp3RestTemplate(){         RestTemplate restTemplate = new RestTemplate(new OkHttp3ClientHttpRequestFactory());         return restTemplate;     } }

ErrorHolder

自定义的一个异常结果包装类

import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpServerErrorException;   public class ErrorHolder {       private HttpStatus statusCode;       private String statusText;       private String responseBody;       private HttpHeaders responseHeaders;       public ErrorHolder(HttpStatus statusCode, String statusText, String responseBody) {         this.statusCode = statusCode;         this.statusText = statusText;         this.responseBody = responseBody;     }       public ErrorHolder(String statusText) {         this.statusText = statusText;     }       public HttpStatus getStatusCode() {         return statusCode;     }       public void setStatusCode(HttpStatus statusCode) {         this.statusCode = statusCode;     }       public String getStatusText() {         return statusText;     }       public void setStatusText(String statusText) {         this.statusText = statusText;     }       public String getResponseBody() {         return responseBody;     }       public void setResponseBody(String responseBody) {         this.responseBody = responseBody;     }       public HttpHeaders getResponseHeaders() {         return responseHeaders;     }       public void setResponseHeaders(HttpHeaders responseHeaders) {         this.responseHeaders = responseHeaders;     }       public static ErrorHolder build(Exception exception) {         if (exception instanceof HttpServerErrorException) {             HttpServerErrorException e = (HttpServerErrorException) exception;             return new ErrorHolder(e.getStatusCode(), e.getStatusText(), e.getResponseBodyAsString());         }           if (exception instanceof HttpClientErrorException) {             HttpClientErrorException e = (HttpClientErrorException) exception;             return new ErrorHolder(e.getStatusCode(), e.getStatusText(), e.getResponseBodyAsString());         }           return new ErrorHolder(exception.getMessage());     } }

使用样例

api里面可以做自动的参数匹配:如:http://you domainn name/test?empNo={empNo},则下面方法的最后一个参数为数据匹配参数,会自动根据key进行查找,然后替换

API没有声明异常,注意进行异常处理

更多使用语法请查看API文档

ResponseEntity<List<KyArea>> result = RestClient.getClient().exchange(DIVIDE_PLATE_API, HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference<List<KyArea>>() {}, map("empNo", empNo)); List<KyArea> list = result.getBody();   ResponseEntity<KyArea> result = RestClient.getClient().exchange(DIVIDE_PLATE_API, HttpMethod.GET, HttpEntity.EMPTY, KyArea.class, map("empNo", empNo)); KyArea kyArea = result.getBody();

RestTemplate处理请求状态码为非200的返回数据

默认的 RestTemplate 有个机制是请求状态码非200 就抛出异常,会中断接下来的操作。如果不想中断对结果数据得解析,可以通过覆盖默认的 ResponseErrorHandler。

见下面的示例,示例中的方法中基本都是空方法,只要对hasError修改下,让他一直返回true,即是不检查状态码及抛异常了。

@Bean("sslRestTemplate")     public RestTemplate getRestTemplate() throws Exception {         RestTemplate sslRestTemplate = new RestTemplate(new HttpsClientRequestFactory());         ResponseErrorHandler responseErrorHandler = new ResponseErrorHandler() {             @Override             public boolean hasError(ClientHttpResponse clientHttpResponse) throws IOException {                 return true;             }             @Override             public void handleError(ClientHttpResponse clientHttpResponse) throws IOException {             }         };         sslRestTemplate.setErrorHandler(responseErrorHandler);         return sslRestTemplate;     }

或者,修改resttemplate的源码,把对应的源码文件拷贝到自己的项目中,但不推荐。

搜索公众号程序员小乐回复关键字“Java”获取Java面试题和答案。

RestTempate的访问的超时设置

例如,我用的是Httpclient的连接池,RestTemplate的超时设置依赖HttpClient的内部的三个超时时间设置。

HttpClient内部有三个超时时间设置:连接池获取可用连接超时,连接超时,读取数据超时:

1.setConnectionRequestTimeout从连接池中获取可用连接超时:设置从connect Manager获取Connection 超时时间,单位毫秒。

HttpClient中的要用连接时尝试从连接池中获取,若是在等待了一定的时间后还没有获取到可用连接(比如连接池中没有空闲连接了)则会抛出获取连接超时异常。

2.连接目标超时connectionTimeout,单位毫秒。

指的是连接目标url的连接超时时间,即客服端发送请求到与目标url建立起连接的最大时间。如果在该时间范围内还没有建立起连接,则就抛出connectionTimeOut异常。

如测试的时候,将url改为一个不存在的url:“http://test.com” ,超时时间3000ms过后,系统报出异常:   org.apache.commons.httpclient.ConnectTimeoutException:The host did not accept the connection within timeout of 3000 ms

3.等待响应超时(读取数据超时)socketTimeout ,单位毫秒。

连接上一个url后,获取response的返回等待时间 ,即在与目标url建立连接后,等待放回response的最大时间,在规定时间内没有返回响应的话就抛出SocketTimeout。

测试时,将socketTimeout 设置很短,会报等待响应超时。

我遇到的问题,restTemplate请求到一个高可用的服务是,返回的超时时间是设置值的2倍,是因为负载均衡器返回的重定向,导致httpClient底层认为没有超时,又请求一次,如果负载均衡器下有两个节点,就耗费connectionTimeout的双倍时间。

欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,欢迎转发分享给更多人。欢迎加入程序员小乐技术交流群,在后台回复“加群”或者“学习”即可。

猜你还想看

阿里、腾讯、百度、华为、京东最新面试题汇集

Spring MVC+Spring+Mybatis实现支付宝支付功能(附完整代码)

压缩20M文件从30秒到1秒的优化过程

如何破解“仅三天可见”的朋友圈?

关注订阅号「程序员小乐」,收看更多精彩内容

嘿,你在看吗?

最新回复(0)