在这里,我们将给大家分享关于SpringMVC中使用RestTemplate访问REST服务详解的知识,让您更了解springmvcrestful的本质,同时也会涉及到如何更有效地java–Sprin
在这里,我们将给大家分享关于Spring MVC中使用RestTemplate访问REST服务详解的知识,让您更了解spring mvc restful的本质,同时也会涉及到如何更有效地java – SpringBoot @WebMvcTest,自动装配RestTemplateBuilder、RestTemplate相关组件:ClientHttpRequestInterceptor【享学Spring MVC】、spring boot / cloud (八) 使用RestTemplate来构建远程调用服务、Spring Boot 入门 - 进阶篇(4)- REST访问(RestTemplate)的内容。
本文目录一览:- Spring MVC中使用RestTemplate访问REST服务详解(spring mvc restful)
- java – SpringBoot @WebMvcTest,自动装配RestTemplateBuilder
- RestTemplate相关组件:ClientHttpRequestInterceptor【享学Spring MVC】
- spring boot / cloud (八) 使用RestTemplate来构建远程调用服务
- Spring Boot 入门 - 进阶篇(4)- REST访问(RestTemplate)
Spring MVC中使用RestTemplate访问REST服务详解(spring mvc restful)
在Java Web开发中,我们通常需要通过GET、POST请求其他系统提供的服务。其中,JDK自带的HttpURLConnection、Apache HttpClient等方式都可以实现。当然,这些方式都有一个很明显的缺陷,那就是代码很繁琐。而Spring提供的RestTemplate封装了这些库的实现,可以让我们的HTTP请求更加简洁、直观。
在RestTemplate中定义了11个独立的操作,它们分别是:
方法 | 描述 |
delete() | 在特定的URL上对资源执行HTTP DELETE操作 |
exchange() | 在URL上执行特定的HTTP方法,返回的ResponseEntity包含了响应体所映射成的对象 |
execute() | 在URL上执行特定的HTTP方法,返回一个从响应体映射得到的对象 |
getForEntity() | 发送一个HTTP GET请求,返回的ResponseEntity包含了响应体所映射成的对象 |
getForObject() | 发送一个HTTP GET请求,返回根据响应体映射形成的对象 |
postForEntity() | POST数据到一个URL,返回的ResponseEntity包含了响应体所映射成的对象 |
postForLocation() | POST数据到一个URL,返回新创建资源的URL |
postForObject() | POST数据到一个URL,返回根据响应体映射形成的对象 |
put() | PUT资源到特定的URL |
headForHeaders() | 发送HTTP HEAD请求,返回包含特定资源URL的HTTP头 |
optionsForAllow() | 发送HTTP OPTIONS请求,返回对特定URL的Allow头信息 |
接下来,我将对常用的几个方法分别介绍。
(1)在项目中添加依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<!-- Jackson对自动解析JSON和XML格式的支持 --> <dependency> <groupId>com.fasterxml.jackson.jaxrs</groupId> <artifactId>jackson-jaxrs-json-provider</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency>
<!-- HttpClient --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency>
<!-- Fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.46</version> </dependency> |
(2)在项目中注入RestTemplate:
在注入RestTemplate的bean的时候,可以通过ClientHttpRequestFactory指定RestTemplate发起HTTP请求的底层实现所采用的类库。对此,ClientHttpRequestFactory接口主要提供了以下两种实现方法:
i)SimpleClientHttpRequestFactory:
也就是底层使用java.net包提供的方式创建Http连接请求。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
package cn.zifangsky.springbootdemo.config;
import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.ClientHttpRequestFactory; 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.web.client.RestTemplate;
@Configuration public class RestTemplateConfig {
/** * 返回RestTemplate * @param factory * @return */ @Bean public RestTemplate restTemplate(ClientHttpRequestFactory factory){ //消息转换器,一般情况下可以省略,只需要添加相关依赖即可 // 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 restTemplate = new RestTemplate(factory); // restTemplate.setMessageConverters(messageConverters);
return restTemplate; }
/** * ClientHttpRequestFactory接口的第一种实现方式,即: * SimpleClientHttpRequestFactory:底层使用java.net包提供的方式创建Http连接请求 * @return */ @Bean public SimpleClientHttpRequestFactory simpleClientHttpRequestFactory(){ SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setReadTimeout(5000); requestFactory.setConnectTimeout(5000);
return requestFactory; }
}
|
ii)HttpComponentsClientHttpRequestFactory(推荐使用):
也就是底层使用Httpclient连接池的方式创建Http连接请求。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
package cn.zifangsky.springbootdemo.config;
import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit;
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.conn.PoolingHttpClientConnectionManager; import org.apache.http.message.BasicHeader; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.ClientHttpRequestFactory; 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.web.client.RestTemplate;
@Configuration public class RestTemplateConfig {
/** * 返回RestTemplate * @param factory * @return */ @Bean public RestTemplate restTemplate(ClientHttpRequestFactory factory){ //消息转换器,Spring Boot环境可省略,只需要添加相关依赖即可 // 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 restTemplate = new RestTemplate(factory); // restTemplate.setMessageConverters(messageConverters);
return restTemplate; }
/** * ClientHttpRequestFactory接口的另一种实现方式(推荐使用),即: * HttpComponentsClientHttpRequestFactory:底层使用Httpclient连接池的方式创建Http连接请求 * @return */ @Bean public HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory(){ //Httpclient连接池,长连接保持30秒 PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
//设置总连接数 connectionManager.setMaxTotal(1000); //设置同路由的并发数 connectionManager.setDefaultMaxPerRoute(1000);
//设置header List<Header> headers = new ArrayList<Header>(); headers.add(new BasicHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.04")); headers.add(new BasicHeader("Accept-Encoding", "gzip, deflate")); headers.add(new BasicHeader("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3")); headers.add(new BasicHeader("Connection", "keep-alive"));
//创建HttpClient HttpClient httpClient = HttpClientBuilder.create() .setConnectionManager(connectionManager) .setDefaultHeaders(headers) .setRetryHandler(new DefaultHttpRequestRetryHandler(3, true)) //设置重试次数 .setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy()) //设置保持长连接 .build();
//创建HttpComponentsClientHttpRequestFactory实例 HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
//设置客户端和服务端建立连接的超时时间 requestFactory.setConnectTimeout(5000); //设置客户端从服务端读取数据的超时时间 requestFactory.setReadTimeout(5000); //设置从连接池获取连接的超时时间,不宜过长 requestFactory.setConnectionRequestTimeout(200); //缓冲请求数据,默认为true。通过POST或者PUT大量发送数据时,建议将此更改为false,以免耗尽内存 requestFactory.setBufferRequestBody(false);
return requestFactory; }
}
|
(3)使用getForObject()方法发起GET请求:
getForObject()方法实际上是对getForEntity()方法的进一步封装,二者用法类似。 唯一的区别在于getForObject()方法只返回所请求类型的对象, 而getForEntity()方法会返回请求的对象以及响应的Header、响应状态码等额外信息。
三个getForObject()方法的签名如下:
1 2 3 4 5 |
<T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException;
<T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;
<T> T getForObject(URI url, Class<T> responseType) throws RestClientException; |
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
package cn.zifangsky.SpringBootDemo.controller;
import java.util.HashMap; import java.util.Map;
import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.web.client.RestTemplate;
import cn.zifangsky.springbootdemo.config.RestTemplateConfig; import cn.zifangsky.springbootdemo.config.WebMvcConfig; import cn.zifangsky.springbootdemo.model.DemoObj;
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes={WebMvcConfig.class,RestTemplateConfig.class}) @WebAppConfiguration("src/main/resources") public class TestRestTemplate {
@Autowired private RestTemplate restTemplate;
/** * 测试最基本的Get请求 */ @Test public void testGetMethod1(){ DemoObj obj = restTemplate.getForObject("http://127.0.0.1:9090/rest/testJson2?id={1}&name={2}" , DemoObj.class , 1,"Tom");
System.out.println(obj); }
} |
注:本篇文章的单元测试请求的REST服务均来源于上一篇文章:https://www.zifangsky.cn/1215.html
上面代码设置请求参数使用了数字占位符,同时getForObject()方法的最后一个参数是一个可变长度的参数,用于一一替换前面的占位符。当然,除了这种方式之外,还可以使用Map来设置参数,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/** * 测试Get请求另一种设置参数的方式 */ @Test public void testGetMethod2(){ Map<String, String> uriVariables = new HashMap<String, String>(); uriVariables.put("var_id", "1"); uriVariables.put("var_name", "Tom");
DemoObj obj = restTemplate.getForObject("http://127.0.0.1:9090/rest/testJson2?id={var_id}&name={var_name}" , DemoObj.class , uriVariables);
System.out.println(obj); } |
运行单元测试之后,最后输出如下:
DemoObj [id=2, name=Tom Ret]
此外需要注意的是,由于上面代码只是简单的单元测试,因此请求URL就直接硬编码在代码中了。实际开发则需要将之配置到配置文件或者Zookeeper、Redis中。比如这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
package cn.zifangsky.springbootdemo.controller;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate;
import cn.zifangsky.springbootdemo.model.DemoObj;
@RestController @RequestMapping("/restTemplate") public class RestTemplateController {
@Value("${SERVER_URL}") private String SERVER_URL;
@Autowired private RestTemplate restTemplate;
@RequestMapping(path="/getDemoObj",produces={MediaType.APPLICATION_JSON_UTF8_VALUE}) public DemoObj getDemoObj(){ DemoObj obj = restTemplate.getForObject(SERVER_URL + "/rest/testXML?id={1}&name={2}" , DemoObj.class , 1,"Tom");
return obj; }
}
|
(4)使用getForEntity()方法发起GET请求:
三个getForEntity()方法的签名如下:
1 2 3 4 5 |
<T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) throws RestClientException;
<T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;
<T> ResponseEntity<T> getForEntity(URI url, Class<T> responseType) throws RestClientException; |
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/** * 测试Get请求返回详细信息,包括:响应正文、响应状态码、响应Header等 */ @Test public void testGetMethod3(){ ResponseEntity<DemoObj> responseEntity = restTemplate.getForEntity("http://127.0.0.1:9090/rest/testJson2?id={1}&name={2}" , DemoObj.class , 1,"Tom");
DemoObj body = responseEntity.getBody(); int statusCodeValue = responseEntity.getStatusCodeValue(); HttpHeaders headers = responseEntity.getHeaders();
System.out.println("responseEntity.getBody():" + body); System.out.println("responseEntity.getStatusCodeValue():" + statusCodeValue); System.out.println("responseEntity.getHeaders():" + headers); } |
运行单元测试之后,最后输出如下:
responseEntity.getBody():DemoObj [id=2, name=Tom Ret]
responseEntity.getStatusCodeValue():200
responseEntity.getHeaders():{Date=[Fri, 09 Feb 2018 06:22:28 GMT], Content-Type=[application/json;charset=utf-8], Transfer-Encoding=[chunked]}
(5)使用postForObject()方法发起POST请求:
在RestTemplate中,POST请求跟GET请求类似,也可以使用如下三个方法来请求:
1 2 3 4 5 6 7 |
<T> T postForObject(String url, Object request, Class<T> responseType, Object... uriVariables) throws RestClientException;
<T> T postForObject(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;
<T> T postForObject(URI url, Object request, Class<T> responseType) throws RestClientException; |
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
/** * 测试最基本的Post请求 */ @Test public void testPostMethod1(){ DemoObj request = new DemoObj(1l, "Tim");
DemoObj obj = restTemplate.postForObject("http://127.0.0.1:9090/rest/testJson1" , request, DemoObj.class);
System.out.println(obj); } |
运行单元测试之后,最后输出如下:
DemoObj [id=2, name=Tim Ret]
(6)使用postForEntity()方法发起POST请求:
三个postForEntity()方法的签名如下:
1 2 3 4 5 6 7 |
<T> ResponseEntity<T> postForEntity(String url, Object request, Class<T> responseType, Object... uriVariables) throws RestClientException;
<T> ResponseEntity<T> postForEntity(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;
<T> ResponseEntity<T> postForEntity(URI url, Object request, Class<T> responseType) throws RestClientException; |
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/** * 测试Post请求返回详细信息,包括:响应正文、响应状态码、响应Header等 */ @Test public void testPostMethod2(){ DemoObj request = new DemoObj(1l, "Tim");
ResponseEntity<DemoObj> responseEntity = restTemplate.postForEntity("http://127.0.0.1:9090/rest/testJson1" , request, DemoObj.class);
DemoObj body = responseEntity.getBody(); int statusCodeValue = responseEntity.getStatusCodeValue(); HttpHeaders headers = responseEntity.getHeaders();
System.out.println("responseEntity.getBody():" + body); System.out.println("responseEntity.getStatusCodeValue():" + statusCodeValue); System.out.println("responseEntity.getHeaders():" + headers); } |
运行单元测试之后,最后输出如下:
responseEntity.getBody():DemoObj [id=2, name=Tim Ret]
responseEntity.getStatusCodeValue():200
responseEntity.getHeaders():{Date=[Fri, 09 Feb 2018 06:32:02 GMT], Content-Type=[application/json;charset=utf-8], Transfer-Encoding=[chunked]}
(7)使用exchange()方法执行指定的HTTP请求:
exchange()方法跟上面的getForObject()、getForEntity()、postForObject()、postForEntity()等方法不同之处在于它可以指定请求的HTTP类型。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/** * 测试Exchange请求 */ @Test public void testExchange(){ //设置header HttpHeaders headers = new HttpHeaders(); headers.add("Content-Type", "application/x-zifangsky");
//设置参数 String requestBody = "1#Converter"; HttpEntity<String> requestEntity = new HttpEntity<String>(requestBody,headers);
ResponseEntity<String> responseEntity = restTemplate.exchange("http://127.0.0.1:9090/convert" , HttpMethod.POST, requestEntity, String.class);
System.out.println("responseEntity.getBody():" + responseEntity.getBody()); System.out.println("responseEntity.getHeaders():" + responseEntity.getHeaders()); } |
运行单元测试之后,最后输出如下:
responseEntity.getBody():{“id”:2,”name”:”Converter Ret”}
responseEntity.getHeaders():{Date=[Fri, 09 Feb 2018 06:42:29 GMT], Content-Type=[application/x-zifangsky], Transfer-Encoding=[chunked]}
参考:
- https://segmentfault.com/a/1190000011093597
- http://rensanning.iteye.com/blog/2362105
java – SpringBoot @WebMvcTest,自动装配RestTemplateBuilder
我在我的测试类中使用注释@WebMvcTest.
当我运行测试时,我收到此错误:
没有’org.springframework.boot.web.client.RestTemplateBuilder’类型的限定bean
我在我的项目中使用RestTemplate用于其他类,所以我在我的主类中定义了一个bean:
@Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { return builder.build(); }
为了使它工作,我必须以这种方式定义我的restTemplate bean:
@Bean public RestTemplate restTemplate() { return new RestTemplate(); }
这是注释@WebMvcTest的问题还是我错过了什么?
谢谢
解决方法
RestTemplate相关组件:ClientHttpRequestInterceptor【享学Spring MVC】
每篇一句
做事的人和做梦的人最大的区别就是行动力
前言
本文为深入了解Spring
提供的Rest调用客户端RestTemplate
开山,对它相关的一些组件做讲解。
Tips:请注意区分RestTemplate
和RedisTemplate
哦~
ClientHttpRequestFactory
它是个函数式接口,用于根据URI
和HttpMethod
创建出一个ClientHttpRequest
来发送请求~
ClientHttpRequest
它代表请求的客户端,该接口继承自HttpRequest
、HttpOutputMessage
,只有一个ClientHttpResponse execute() throws IOException
方法。其中Netty、HttpComponents、OkHttp3,HttpUrlConnection
对它都有实现~
// @since 3.0 RestTemplate这个体系都是3.0后才有的
@FunctionalInterface
public interface ClientHttpRequestFactory {
// 返回一个ClientHttpRequest,这样调用其execute()方法就可以发送rest请求了~
ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException;
}
它的继承树如下:
可以直观的看到,我们可以使用Apache
的HttpClient
、OkHttp3
、Netty4
都可,但这些都需要额外导包,默认情况下Spring
使用的是java.net.HttpURLConnection
。
HttpClient最新版本:4.5.10
OkHttp最新版本:4.1.1(虽然版本号是4,但是GAV还是3哦:com.squareup.okhttp3)
Netty最新版本:4.1.39.Final(它的5版本可以宣告已死)
Spring4.0
是新增了一个对异步支持的AsyncClientHttpRequestFactory
(Spring5.0后标记为已废弃):
// 在Spring5.0后被标记为过时了,被org.springframework.http.client.reactive.ClientHttpConnector所取代(但还是可用的嘛)
@Deprecated
public interface AsyncClientHttpRequestFactory {
// AsyncClientHttpRequest#executeAsync()返回的是ListenableFuture<ClientHttpResponse>
// 可见它的异步是通过ListenableFuture实现的
AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) throws IOException;
}
使用工厂创建ClientHttpRequest
,然后我们发请求就不用关心具体httpClient内部的细节了(可插拔使用二方库、三方库)
SimpleClientHttpRequestFactory
它是Spring
内置默认的实现,使用的是JDK内置的java.net.URLConnection
作为client客户端。
public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory {
private static final int DEFAULT_CHUNK_SIZE = 4096;
@Nullable
private Proxy proxy; //java.net.Proxy
private boolean bufferRequestBody = true; // 默认会缓冲body
// URLConnection''s connect timeout (in milliseconds).
// 若值设置为0,表示永不超时 @see URLConnection#setConnectTimeout(int)
private int connectTimeout = -1;
// URLConnection#setReadTimeout(int)
// 超时规则同上
private int readTimeout = -1;
//Set if the underlying URLConnection can be set to ''output streaming'' mode.
private boolean outputStreaming = true;
// 异步的时候需要
@Nullable
private AsyncListenableTaskExecutor taskExecutor;
... // 省略所有的set方法
@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
// 打开一个HttpURLConnection
HttpURLConnection connection = openConnection(uri.toURL(), this.proxy);
// 设置超时时间、请求方法等一些参数到connection
prepareConnection(connection, httpMethod.name());
//SimpleBufferingClientHttpRequest的excute方法最终使用的是connection.connect();
// 然后从connection中得到响应码、响应体~~~
if (this.bufferRequestBody) {
return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming);
} else {
return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming);
}
}
// createAsyncRequest()方法略,无非就是在线程池里异步完成请求
...
}
需要注意的是:JDK <1.8 doesn''t support getOutputStream with HTTP DELETE
,也就是说如果JDK
的版本低于1.8的话,那么Delete请求是不支持body体的。
Demo Show:
public static void main(String[] args) throws IOException {
SimpleClientHttpRequestFactory clientFactory = new SimpleClientHttpRequestFactory();
// ConnectTimeout只有在网络正常的情况下才有效,因此两个一般都设置
clientFactory.setConnectTimeout(5000); //建立连接的超时时间 5秒
clientFactory.setReadTimeout(5000); // 传递数据的超时时间(在网络抖动的情况下,这个参数很有用)
ClientHttpRequest client = clientFactory.createRequest(URI.create("https://www.baidu.com"), HttpMethod.GET);
// 发送请求
ClientHttpResponse response = client.execute();
System.out.println(response.getStatusCode()); //200 OK
System.out.println(response.getStatusText()); // OK
System.out.println(response.getHeaders()); //
// 返回内容 是个InputStream
byte[] bytes = FileCopyUtils.copyToByteArray(response.getBody());
System.out.println(new String(bytes, StandardCharsets.UTF_8)); // 百度首页内容的html
}
关于HttpURLConnection
的API使用,需注意如下几点:
-
HttpURLConnection
对象不能直接构造,需要通过URL类中的openConnection()
方法来获得 -
HttpURLConnection
的connect()函数,实际上只是建立了一个与服务器的TCP连接,并没有实际发送HTTP请求。HTTP请求实际上直到我们获取服务器响应数据(如调用getInputStream()、getResponseCode()等方法)时才正式发送出去1. 配置信息都需要在connect()方法执行之前完成
-
HttpURLConnection
是基于HTTP协议的,其底层通过socket通信实现。如果不设置超时(timeout),在网络异常的情况下,可能会导致程序僵死而不继续往下执行。==请务必100%设置== - HTTP正文的内容是通过OutputStream流写入的, 向流中写入的数据不会立即发送到网络,而是存在于内存缓冲区中,待流关闭时,根据写入的内容生成HTTP正文
- 调用getInputStream()方法时,返回一个输入流,用于从中读取服务器对于HTTP请求的返回信息。
-
HttpURLConnection.connect()
不是必须的。当我们需要返回值时,比如我们使用HttpURLConnection.getInputStream()方法的时候它就会自动发送请求了,所以完全没有必要调用connect()方法了(没必要先建立Tcp嘛~)。
使用哪一个底层http库?
我们知道HttpURLConnection
它在功能上是有些不足的(简单的提交参数可以满足)。绝大部分情况下Web站点的网页可能没这么简单,这些页面并不是通过一个简单的URL就可访问的,可能需要用户登录而且具有相应的权限才可访问该页面。在这种情况下,就需要涉及Session、Cookie的处理了,如果打算使用HttpURLConnection来处理这些细节,当然也是可能实现的,只是处理起来难度就大了。
这个时候,Apache开源组织提供了一个HttpClient
项目,可以用于发送HTTP请求,接收HTTP响应(包含HttpGet、HttpPost...等各种发送请求的对象)。
它不会缓存服务器的响应,不能执行HTML页面中嵌入的Javascript代码;也不会对页面内容进行任何解析、处理
因此,下面我就让Spring使用HttpClient为示例演示使用三方库:
1、导包
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.10</version>
</dependency>
Tips:Requires Apache HttpComponents 4.3
or higher, as of Spring 4.0.
2、案例使用
案例内容仅仅只需
把上例第一句话换成使用HttpComponentsClientHttpRequestFactory
它的实例,其余都不用变化即可成功看到效果。可以看看这个类它具体做了什么
// @since 3.1 3.1后出现的。
public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequestFactory, DisposableBean {
private HttpClient httpClient;
@Nullable
private RequestConfig requestConfig; // 这个配置就是可以配置超时等等乱七八糟client属性的类
private boolean bufferRequestBody = true;
//=========下面是构造函数们=========
public HttpComponentsClientHttpRequestFactory() {
// HttpClientBuilder.create().useSystemProperties().build();
// 所有若是这里,配置超时时间可以这么来设置也可:
// System.setProperty(”sun.net.client.defaultConnectTimeout”, “5000″);
this.httpClient = HttpClients.createSystem();
}
// 当然可以把你配置好了的Client扔进来
public HttpComponentsClientHttpRequestFactory(HttpClient httpClient) {
this.httpClient = httpClient;
}
... // 省略设置超时时间。。。等等属性的一些get/set
// 超时信息啥的都是保存在`RequestConfig`里的
@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
HttpClient client = getHttpClient(); // 拿到你指定的client)=(或者系统缺省的)
// switch语句逻辑:HttpMethod == GET --> HttpGet HEAD --> HttpHead ...
HttpUriRequest httpRequest = createHttpUriRequest(httpMethod, uri);
postProcessHttpRequest(httpRequest);
...
}
}
实际使用的是HttpClient
完成的请求。另外OkHttp3ClientHttpRequestFactory
使用的是okhttp3.OkHttpClient
发送请求;Netty4ClientHttpRequestFactory
使用的是io.netty.channel.EventLoopGroup
。此处就不一一例举了
Spring5.0以后,Netty4ClientHttpRequestFactory
过期了,建议使用org.springframework.http.client.reactive.ReactorClientHttpConnector
代替~
关于HttpURLConnection
、HttpClient
、OkHttpClient
的简单比较:
-
HttpURLConnection
:- 优点:JDK内置支持,java的标准类 - 缺点:API不够友好,什么都没封装,用起来太原始,不方便(这其实有时候也算优点,原始就证明好控~)
-
HttpClient
:- 优点:功能强大,API友好,使用率够高,几乎成为了实际意义上的标准(相当于对`HttpURLConnection`的封装) - 缺点:性能稍低(比`HttpURLConnection`低,但4.3后使用连接池进行了改善),API较臃肿,其实Android已经弃用了它~
-
OkHttpClient
:新一代的Http访问客户端- 优点:一个专注于**性能和易用性**的HTTP客户端(节约宽带,Android推荐使用),它设计的首要目标就是高效。提供了最新的 HTTP 协议版本 HTTP/2 和 SPDY 的支持。如果 HTTP/2 和 SPDY 不可用,OkHttp 会使用连接池来复用连接以提高效率 - 暂无。
关于Apache HttpClient
,Android
5.0之后已经废弃使用它了(API太多,太重),推荐使用更轻量的HttpUrlConnection
。(Java开发还是推荐用HttpClient
)
OkHttp
优点较多:支持SPDY,可以合并多个到同一个主机的请求;OkHttp实现的诸多技术如:连接池,gziping,缓存等;OkHttp 处理了很多网络疑难杂症:会从很多常用的连接问题中自动恢复。如果您的服务器配置了多个IP地址,当第一个IP连接失败的时候,OkHttp会自动尝试下一个IP;OkHttp是一个Java的HTTP+SPDY客户端开发包,同时也支持Android。默认情况下,OKHttp会自动处理常见的网络问题,像二次连接、SSL的握手问题。支持文件上传、下载、cookie、session、https证书等几乎所有功能。支持取消某个请求
综上所述,不管是Java还是Android,我推荐的自然都是OkHttp
(OkHttp使用Okio进行数据传输。都是Square公司自家的,Square公司还出了一个Retrofit库配合OkHttp战斗力翻倍)~~~
池化技术一般用于长连接,那么像Http这种适合连接池吗?
HttpClient 4.3以后中使用了PoolingHttpClientConnectionManager
连接池来管理持有连接,同一条TCP链路上,连接是可以复用的。HttpClient通过连接池的方式进行连接持久化(所以它这个连接池其实是tcp的连接池。它里面有一个很重要的概念:Route
的概念,代表一条线路。比如baidu.com是一个route,163.com是一个route...)。
连接池:可能是http请求,也可能是https请求
加入池话技术,就不用每次发起请求都新建一个连接(每次连接握手三次,效率太低)
AbstractClientHttpRequestFactoryWrapper
对其它ClientHttpRequestFactory
的一个包装抽象类,它有如下两个子类实现
InterceptingClientHttpRequestFactory
(重要)
Interceptor
拦截的概念,还是蛮重要的。它持有的ClientHttpRequestInterceptor
对于我们若想要拦截发出去的请求非常之重要(比如全链路压测中,可以使用它设置token之类的~)
// @since 3.1
public class InterceptingClientHttpRequestFactory extends AbstractClientHttpRequestFactoryWrapper {
// 持有所有的请求拦截器
private final List<ClientHttpRequestInterceptor> interceptors;
public InterceptingClientHttpRequestFactory(ClientHttpRequestFactory requestFactory, @Nullable List<ClientHttpRequestInterceptor> interceptors) {
super(requestFactory);
// 拦截器只允许通过构造函数设置进来,并且并没有提供get方法方法~
this.interceptors = (interceptors != null ? interceptors : Collections.emptyList());
}
// 此处返回的是一个InterceptingClientHttpRequest,显然它肯定是个ClientHttpRequest嘛~
@Override
protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory) {
return new InterceptingClientHttpRequest(requestFactory, this.interceptors, uri, httpMethod);
}
}
InterceptingClientHttpRequest
的execute()
方法的特点是:若存在拦截器,交给给拦截器去执行发送请求return nextInterceptor.intercept(request, body, this)
,否则就自己上。
ClientHttpRequestInterceptor
关于请求拦截器,Spring MVC内置了两个最基础的实现
==BasicAuthorizationInterceptor==:
// @since 4.3.1 但在Spring5.1.1后推荐使用BasicAuthenticationInterceptor
@Deprecated
public class BasicAuthorizationInterceptor implements ClientHttpRequestInterceptor {
private final String username;
private final String password;
// 注意:username不允许包含:这个字符,但是密码是允许的
public BasicAuthorizationInterceptor(@Nullable String username, @Nullable String password) {
Assert.doesNotContain(username, ":", "Username must not contain a colon");
this.username = (username != null ? username : "");
this.password = (password != null ? password : "");
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// 用户名密码连接起来后,用Base64对字节码进行编码~
String token = Base64Utils.encodeToString((this.username + ":" + this.password).getBytes(StandardCharsets.UTF_8));
// 放进请求头:key为`Authorization` 然后执行请求的发送
request.getHeaders().add("Authorization", "Basic " + token);
return execution.execute(request, body);
}
}
这个拦截器木有对body有任何改动,只是把用户名、密码帮你放进了请求头上。
需要注意的是:若你的header里已经存在了Authorization
这个key,这里也不会覆盖的,这会添加哦。但并不建议你有覆盖现象~
==BasicAuthenticationInterceptor==:
它是用来代替上类的。它使用标准的授权头来处理,参考HttpHeaders#setBasicAuth、HttpHeaders#AUTHORIZATION
public class BasicAuthenticationInterceptor implements ClientHttpRequestInterceptor {
private final String username;
private final String password;
// 编码,一般不用指定
@Nullable
private final Charset charset;
... // 构造函数略
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
HttpHeaders headers = request.getHeaders();
// 只有当请求里不包含`Authorization`这个key的时候,此处才会设置授权头哦
if (!headers.containsKey(HttpHeaders.AUTHORIZATION)) {
// 这个方法是@since 5.1之后才提供的~~~~~
// 若不包含此key,就设置标准的授权头(根据用户名、密码) 它内部也有这如下三步:
// String credentialsString = username + ":" + password;
// byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(charset));
// String encodedCredentials = new String(encodedBytes, charset);
// 注意:它内部最终还是调用set(AUTHORIZATION, "Basic " + encodedCredentials);这个方法的
headers.setBasicAuth(this.username, this.password, this.charset);
}
return execution.execute(request, body);
}
}
说明:这两个请求拦截器虽是Spring提供,但默认都是没有被"装配"的,所亲需要,请手动装配~
BufferingClientHttpRequestFactory
包装其它ClientHttpRequestFactory
,使得具有缓存的能力。若开启缓存功能(有开关可控),会使用BufferingClientHttpRequestWrapper
包装原来的ClientHttpRequest
。这样发送请求后得到的是BufferingClientHttpResponseWrapper
响应。
ResponseErrorHandler
用于确定特定响应是否有错误的策略接口。
// @since 3.0
public interface ResponseErrorHandler {
// response里是否有错
boolean hasError(ClientHttpResponse response) throws IOException;
// 只有hasError = true时才会调用此方法
void handleError(ClientHttpResponse response) throws IOException;
// @since 5.0
default void handleError(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
handleError(response);
}
}
继承树如下:
DefaultResponseErrorHandler
Spring
对此策略接口的默认实现,RestTemplate
默认使用的错误处理器就是它。
// @since 3.0
public class DefaultResponseErrorHandler implements ResponseErrorHandler {
// 是否有错误是根据响应码来的,所以请严格遵守响应码的规范啊
// 简单的说4xx和5xx都会被认为有错,否则是无错的 参考:HttpStatus.Series
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
int rawStatusCode = response.getRawStatusCode();
HttpStatus statusCode = HttpStatus.resolve(rawStatusCode);
return (statusCode != null ? hasError(statusCode) : hasError(rawStatusCode));
}
...
// 处理错误
@Override
public void handleError(ClientHttpResponse response) throws IOException {
HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
if (statusCode == null) {
throw new UnknownHttpStatusCodeException(response.getRawStatusCode(), response.getStatusText(), response.getHeaders(), getResponseBody(response), getCharset(response));
}
handleError(response, statusCode);
}
// protected方法,子类对它有复写
protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
String statusText = response.getStatusText();
HttpHeaders headers = response.getHeaders();
byte[] body = getResponseBody(response); // 拿到body,把InputStream转换为字节数组
Charset charset = getCharset(response); // 注意这里的编码,是从返回的contentType里拿的~~~
// 分别针对于客户端错误、服务端错误 包装为HttpClientErrorException和HttpServerErrorException进行抛出
// 异常内包含有状态码、状态text、头、body、编码等等信息~~~~
switch (statusCode.series()) {
case CLIENT_ERROR:
throw HttpClientErrorException.create(statusCode, statusText, headers, body, charset);
case SERVER_ERROR:
throw HttpServerErrorException.create(statusCode, statusText, headers, body, charset);
default:
throw new UnknownHttpStatusCodeException(statusCode.value(), statusText, headers, body, charset);
}
}
...
}
到这里就可以给大家解释一下,为何经常能看到客户端错误,然后还有状态码+一串信息了,就是因为这两个异常。
HttpClientErrorException:
public class HttpClientErrorException extends HttpStatusCodeException {
...
public static HttpClientErrorException create(
HttpStatus statusCode, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) {
switch (statusCode) {
case BAD_REQUEST:
return new HttpClientErrorException.BadRequest(statusText, headers, body, charset);
case UNAUTHORIZED:
return new HttpClientErrorException.Unauthorized(statusText, headers, body, charset);
case FORBIDDEN:
return new HttpClientErrorException.Forbidden(statusText, headers, body, charset);
case NOT_FOUND:
return new HttpClientErrorException.NotFound(statusText, headers, body, charset);
case METHOD_NOT_ALLOWED:
return new HttpClientErrorException.MethodNotAllowed(statusText, headers, body, charset);
case NOT_ACCEPTABLE:
return new HttpClientErrorException.NotAcceptable(statusText, headers, body, charset);
case CONFLICT:
return new HttpClientErrorException.Conflict(statusText, headers, body, charset);
case GONE:
return new HttpClientErrorException.Gone(statusText, headers, body, charset);
case UNSUPPORTED_MEDIA_TYPE:
return new HttpClientErrorException.UnsupportedMediaType(statusText, headers, body, charset);
case TOO_MANY_REQUESTS:
return new HttpClientErrorException.TooManyRequests(statusText, headers, body, charset);
case UNPROCESSABLE_ENTITY:
return new HttpClientErrorException.UnprocessableEntity(statusText, headers, body, charset);
default:
return new HttpClientErrorException(statusCode, statusText, headers, body, charset);
}
}
...
}
它针对不同的状态码HttpStatus
,创建了不同的类型进行返回,方便使用者控制,这在监控上还是蛮有意义的
BadRequest、Unauthorized、Forbidden...等等都是HttpClientErrorException
的子类
HttpServerErrorException
代码类似,略~
ExtractingResponseErrorHandler
继承自DefaultResponseErrorHandler
。在RESTful
大行其道的今天,Spring5.0
开始提供了此类。它将http错误响应利用HttpMessageConverter
转换为对应的RestClientException
// @since 5.0 它出现得还是很晚的。继承自DefaultResponseErrorHandler
// 若你的RestTemplate想使用它,请调用RestTemplate#setErrorHandler(ResponseErrorHandler)设置即可
public class ExtractingResponseErrorHandler extends DefaultResponseErrorHandler {
private List<HttpMessageConverter<?>> messageConverters = Collections.emptyList();
// 对响应码做缓存
private final Map<HttpStatus, Class<? extends RestClientException>> statusMapping = new LinkedHashMap<>();
private final Map<HttpStatus.Series, Class<? extends RestClientException>> seriesMapping = new LinkedHashMap<>();
// 构造函数、set方法给上面两个Map赋值。因为我们可以自己控制哪些状态码应该报错,哪些不应该了~
// 以及可以自定义:那个状态码抛我们自定义的异常,哪一系列状态码抛我们自定义的异常,这个十分的便于我们做监控
... // 省略构造函数和set方法。。。
// 增加缓存功能~~~ 否则在交给父类
@Override
protected boolean hasError(HttpStatus statusCode) {
if (this.statusMapping.containsKey(statusCode)) {
return this.statusMapping.get(statusCode) != null;
} else if (this.seriesMapping.containsKey(statusCode.series())) {
return this.seriesMapping.get(statusCode.series()) != null;
} else {
return super.hasError(statusCode);
}
}
// 这个它做的事:extract:提取
@Override
public void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
if (this.statusMapping.containsKey(statusCode)) {
extract(this.statusMapping.get(statusCode), response);
} else if (this.seriesMapping.containsKey(statusCode.series())) {
extract(this.seriesMapping.get(statusCode.series()), response);
} else {
super.handleError(response, statusCode);
}
}
private void extract(@Nullable Class<? extends RestClientException> exceptionClass, ClientHttpResponse response) throws IOException {
if (exceptionClass == null) {
return;
}
// 这里使用到了ResponseExtractor返回值提取器,从返回值里提取内容(本文是提取异常)
HttpMessageConverterExtractor<? extends RestClientException> extractor =
new HttpMessageConverterExtractor<>(exceptionClass, this.messageConverters);
RestClientException exception = extractor.extractData(response);
if (exception != null) { // 若提取到了异常信息,抛出即可
throw exception;
}
}
}
若你想定制请求异常的处理逻辑,你也是可以自定义这个接口的实现的,当然还是建议你通过继承DefaultResponseErrorHandler
来扩展~
ResponseExtractor
响应提取器:从Response
中提取数据。RestTemplate
请求完成后,都是通过它来从ClientHttpResponse
提取出指定内容(比如请求头、请求Body体等)~
它的直接实现似乎只有HttpMessageConverterExtractor
,当然它也是最为重要的一个实现,和HttpMessageConverter
相关。
在解释它之前,先看看这个:MessageBodyClientHttpResponseWrapper
,它的特点:它不仅可以通过实际读取输入流来检查响应是否有消息体,还可以检查其长度是否为0(即空)
// @since 4.1.5 它是一个访问权限是default的类,是对其它ClientHttpResponse的一个包装
class MessageBodyClientHttpResponseWrapper implements ClientHttpResponse {
private final ClientHttpResponse response;
// java.io.PushbackInputStream
@Nullable
private PushbackInputStream pushbackInputStream;
// 判断相应里是否有body体
// 若响应码是1xx 或者是204;或者getHeaders().getContentLength() == 0 那就返回false 否则返回true
public boolean hasMessageBody() throws IOException {
HttpStatus status = HttpStatus.resolve(getRawStatusCode());
if (status != null && (status.is1xxInformational() || status == HttpStatus.NO_CONTENT || status == HttpStatus.NOT_MODIFIED)) {
return false;
}
if (getHeaders().getContentLength() == 0) {
return false;
}
return true;
}
// 上面是完全格局状态码(ContentLength)来判断是否有body体的~~~这里会根据流来判断
// 如果response.getBody() == null,返回true
// 若流里有内容,最终就用new PushbackInputStream(body)包装起来~~~
public boolean hasEmptyMessageBody() throws IOException {
...
}
... // 其余接口方法都委托~
@Override
public InputStream getBody() throws IOException {
return (this.pushbackInputStream != null ? this.pushbackInputStream : this.response.getBody());
}
}
它的作用就是包装后,提供两个方法hasMessageBody、hasEmptyMessageBody
方便了对body体内容进行判断
// @since 3.0 泛型T:the data type
public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
// java.lang.reflect.Type
private final Type responseType;
// 这个泛型也是T,表示数据的Class嘛~
// 该calss有可能就是上面的responseType
@Nullable
private final Class<T> responseClass;
// 重要:用于消息解析的转换器
private final List<HttpMessageConverter<?>> messageConverters;
... // 省略构造函数
// 从ClientHttpResponse 里提取值
@Override
@SuppressWarnings({"unchecked", "rawtypes", "resource"})
public T extractData(ClientHttpResponse response) throws IOException {
MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);
// 若没有消息体(状态码不对 或者 消息体为空都被认为是木有)
if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {
return null;
}
// content-type若响应头header里没有指定,那默认是它MediaType.APPLICATION_OCTET_STREAM
MediaType contentType = getContentType(responseWrapper);
// 遍历所有的messageConverters,根据contentType 来选则一个消息转换器
// 最终return messageConverter.read((Class) this.responseClass, responseWrapper)
...
}
}
它的处理逻辑理解起来非常简单:利用contentType
找到一个消息转换器,最终HttpMessageConverter.read()
把消息读出来转换成Java对象。
它还有两个内部类的实现如下(都是RestTemplate
的私有内部类):
RestTemplate:
// 提取为`ResponseEntity` 最终委托给HttpMessageConverterExtractor完成的
private class ResponseEntityResponseExtractor<T> implements ResponseExtractor<ResponseEntity<T>> {
@Nullable
private final HttpMessageConverterExtractor<T> delegate;
public ResponseEntityResponseExtractor(@Nullable Type responseType) {
// 显然:只有请求的返回值不为null 才有意义~
if (responseType != null && Void.class != responseType) {
this.delegate = new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);
} else {
this.delegate = null;
}
}
// 数据提取。都是交给`delegate.extractData(response)`做了,然后new一个ResponseEntity出来包装进去
// 若木有返回值(delegate=null),那就是一个`ResponseEntity`实例,body为null
@Override
public ResponseEntity<T> extractData(ClientHttpResponse response) throws IOException {
if (this.delegate != null) {
T body = this.delegate.extractData(response);
return ResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).body(body);
}
else {
return ResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).build();
}
}
}
// 提取请求头
private static class HeadersExtractor implements ResponseExtractor<HttpHeaders> {
@Override
public HttpHeaders extractData(ClientHttpResponse response) {
return response.getHeaders();
}
}
UriTemplateHandler
这个组件它用于定义用变量扩展uri模板的方法。
// @since 4.2 出现较晚
// @see RestTemplate#setUriTemplateHandler(UriTemplateHandler)
public interface UriTemplateHandler {
URI expand(String uriTemplate, Map<String, ?> uriVariables);
URI expand(String uriTemplate, Object... uriVariables);
}
关于URI的处理,最终都是委托给UriComponentsBuilder
来完成。若对这块还存在一定疑问的,强烈强烈强烈 参考这里
推荐阅读
RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】
总结
本文介绍的组件是去理解RestTemplate
必备的组件们,属于开山篇。因为RestTemplate
使用频繁,并且经常需要调优,因此我寄希望大家也能对它做较为深入的了解,这也是我写本系列的目的,共勉。
== 若对Spring、SpringBoot、MyBatis等源码分析感兴趣,可加我wx:fsx641385712,手动邀请你入群一起飞 ==
== 若对Spring、SpringBoot、MyBatis等源码分析感兴趣,可加我wx:fsx641385712,手动邀请你入群一起飞 ==
spring boot / cloud (八) 使用RestTemplate来构建远程调用服务
spring boot / cloud (八) 使用RestTemplate来构建远程调用服务
##前言
上周因家里突发急事,请假一周,故博客没有正常更新
###RestTemplate介绍:
RestTemplate是spring框架中自带的rest客户端工具类,具有丰富的API,并且在spring cloud中,标记@LoadBalanced注解,可以实现客户端负载均衡的rest调用.
##思路
RestTemplate虽然提供了丰富的API,但是这些API过于底层,如果不稍加控制,让开发人员随意使用,那后续的代码也将会变的五花八门,难以维护.
同时,当系统规模大了之后,将会有更多的服务,并且服务之间的调用关系也将更加复杂,如果不进行管控治理的话,同样,项目同期也将越来越不可控,
最后,服务间调用也需要有明确的权限认证机制,最好是能通过配置的方式来明确,哪些服务可以调用那些服务.从而来把控项目的复杂度.
本文将从以下几点来提供一个解决问题的思路:
-
通过spring boot的@ConfigurationProperties机制来定义远程服务的元数据,从而实现权限认证的配置化
-
使用HandlerInterceptor来进行拦截,实现权限的验证
-
定义通用Rms类,来规范RestTemplate的使用
##实现
###1.实现权限配置
####1.定义Application元数据
public class ApplicationMeta implements Serializable {
//ID
private static final long serialVersionUID = 1L;
//服务ID
private String serviceId;
//私钥
private String secret;
//权限
private String purview;
//所有服务的调用权限(优先判定)
private Boolean all = false;
//禁止服务调用
private Boolean disabled = false;
//描述
private String description;
}
####2.定义Service元数据
public class ServiceMeta implements Serializable {
//ID
private static final long serialVersionUID = 1L;
//应用名称
private String owner;
//地址
private String uri;
//服务方法
private String method;
//是否HTTPS
private Boolean isHttps = false;
//描述
private String description;
####3.定义RmsProperties类
@Component
@ConfigurationProperties(prefix = "com.egridcloud.rms.properties")
public class RmsProperties implements Serializable {
//ID
private static final long serialVersionUID = 1L;
//应用清单(应用名称 : 应用地址)
private Map<String, ApplicationMeta> application;
//服务路径(服务编号 : 服务元数据)
private Map<String, ServiceMeta> service;
####4.在properties文件中进行配置
#定义了一个叫udf-demo(跟spring boot的应用ID一致),设置了私钥,以及可调用的服务
com.egridcloud.rms.properties.application.udf-demo.serviceId=127.0.0.1:8080
com.egridcloud.rms.properties.application.udf-demo.secret=ADSFHKW349546RFSGF
com.egridcloud.rms.properties.application.udf-demo.purview=FILE_3
com.egridcloud.rms.properties.application.udf-demo.all=false
com.egridcloud.rms.properties.application.udf-demo.disabled=false
com.egridcloud.rms.properties.application.udf-demo.description=sample application
#定义了一个叫FILE_3的服务,后续使用这个服务编号进行调用即可
com.egridcloud.rms.properties.service.FILE_3.owner=udf-demo
com.egridcloud.rms.properties.service.FILE_3.uri=/service/file/download
com.egridcloud.rms.properties.service.FILE_3.method=POST
com.egridcloud.rms.properties.service.FILE_3.isHttps=false
com.egridcloud.rms.properties.service.FILE_3.description=文件下载
###2.实现权限校验
####1.定义RmsAuthHandlerInterceptor拦截器
public class RmsAuthHandlerInterceptor implements HandlerInterceptor {
//环境标识
private static final String DEV_PROFILES = "dev";
//配置
@Autowired
private RmsProperties rmsProperties;
//环境变量
@Autowired
private Environment env;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
.......
}
}
####2.完善preHandle方法-取出认证信息
String rmsApplicationName = request.getHeader(Constant.HEADER_RMS_APPLICATION_NAME_CODE);
if (StringUtils.isBlank(rmsApplicationName)) {
rmsApplicationName = request.getParameter(Constant.HEADER_RMS_APPLICATION_NAME_CODE);
}
//获取认证信息(sign)
String rmsSign = request.getHeader(Constant.HEADER_RMS_SIGN_CODE);
if (StringUtils.isBlank(rmsSign)) {
rmsSign = request.getParameter(Constant.HEADER_RMS_SIGN_CODE);
}
//获取认证信息(服务代码)
String rmsServiceCode = request.getHeader(Constant.HEADER_SERVICE_CODE_CODE);
if (StringUtils.isBlank(rmsServiceCode)) {
rmsServiceCode = request.getParameter(Constant.HEADER_SERVICE_CODE_CODE);
}
//获取请求地址
String url = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE).toString();
//获取请求方法
String method = request.getMethod();
####3.完善preHandle方法-校验
//判断环境(开发环境无需校验)
if (!DEV_PROFILES.equals(env.getProperty("spring.profiles.active"))) {
//判断是否缺少认证信息
if (StringUtils.isBlank(rmsApplicationName) || StringUtils.isBlank(rmsSign)
|| StringUtils.isBlank(rmsServiceCode)) {
throw new AuthException("missing required authentication parameters (rmsApplicationName , rmsSign)");
}
//判断systemTag是否有效
if (!this.rmsProperties.getApplication().containsKey(rmsApplicationName)) {
throw new AuthException("unrecognized systemTag:" + rmsApplicationName);
}
//获得应用元数据
ApplicationMeta applicationMeta = rmsProperties.getApplication().get(rmsApplicationName);
//获得secret
String secret = applicationMeta.getSecret();
//计算sign
String sign = Constant.sign(rmsApplicationName, secret);
//比较sign
if (!rmsSign.equals(sign)) {
throw new AuthException("sign Validation failed");
}
//判断是否有调用所有服务的权限
if (!applicationMeta.getAll()) {
//判断是否禁止调用所有服务权限
if (applicationMeta.getDisabled()) {
throw new PermissionException(rmsApplicationName + " is disabled");
}
//判断是否有调用该服务的权限
if (applicationMeta.getPurview().indexOf(rmsServiceCode) == -1) {
throw new PermissionException("no access to this servoceCode : " + rmsServiceCode);
}
//判断服务元数据是否存在
if (!rmsProperties.getService().containsKey(rmsServiceCode)) {
throw new PermissionException("service code not exist");
}
//获得服务元数据
ServiceMeta serviceMeta = rmsProperties.getService().get(rmsServiceCode);
//比较url和method的有效性
if (!serviceMeta.getUri().equals(url) || !serviceMeta.getMethod().equals(method)) {
throw new PermissionException("url and method verification error");
}
}
}
####4.定义RmsConfig类
@Configuration
@ConfigurationProperties(prefix = "com.egridcloud.rms.config")
@Validated
public class RmsConfig {
//RMS扫描路径
@NotNull
private String rmsPathPatterns;
.........
}
####5.定义RmsConfig类-注册bean
@Bean
@LoadBalanced
RestTemplate restTemplate(ClientHttpRequestFactory requestFactory) {
return new RestTemplate(requestFactory);
}
@Bean
public RmsAuthHandlerInterceptor rmsAuthHandlerInterceptor() {
return new RmsAuthHandlerInterceptor();
}
@Bean
public WebMvcConfigurer rmsAuthConfigurer() { //NOSONAR
return new WebMvcConfigurerAdapter() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
String[] rmsPathPatternsArray = rmsPathPatterns.split(",");
registry.addInterceptor(rmsAuthHandlerInterceptor()).addPathPatterns(rmsPathPatternsArray);
super.addInterceptors(registry);
}
};
}
####6.在properties文件中进行配置
#拦截路径
com.egridcloud.rms.config.rmsPathPatterns=/service/**
###3.实现Rms类
####1.定义rms类
@Component
public class Rms {
//应用名称
@Value("${spring.application.name}")
private String springApplicationName;
//restTemplate
@Autowired
private RestTemplate restTemplate;
//配置
@Autowired
private RmsProperties rmsProperties;
####2.定义rms类-call方法
public <I, O> ResponseEntity<O> call(String serviceCode, I input, String uriParam,
ParameterizedTypeReference<O> responseType, Map<String, ?> uriVariables) {
//客户端权限验证
verification(serviceCode);
//构建请求路径
String path = getRmsUrl(serviceCode);
//获得请求方法
String method = getRmsMethod(serviceCode);
//拼装路径参数
if (StringUtils.isNotBlank(uriParam)) {
path += uriParam;
}
//构建请求头
HttpHeaders httpHeaders = buildSystemTagHeaders(serviceCode);
//构建请求消息体
HttpEntity<I> requestEntity = new HttpEntity<>(input, httpHeaders);
//请求并且返回
LOGGER.info("rms url : {} , method : {} ", path, method);
return restTemplate.exchange(path, HttpMethod.resolve(method), requestEntity, responseType,
uriVariables != null ? uriVariables : new HashMap<String, String>());
}
####3.定义rms类-其他方法
//构建请求头
private HttpHeaders buildSystemTagHeaders(String serviceCode) {
String secret = rmsProperties.getApplication().get(springApplicationName).getSecret();
HttpHeaders headers = new HttpHeaders();
headers.add(Constant.HEADER_RMS_APPLICATION_NAME_CODE, springApplicationName);
headers.add(Constant.HEADER_RMS_SIGN_CODE, Constant.sign(springApplicationName, secret));
headers.add(Constant.HEADER_SERVICE_CODE_CODE, serviceCode);
return headers;
}
//客户端验证
private void verification(String serviceCode) {
ApplicationMeta applicationMeta = rmsProperties.getApplication().get(springApplicationName);
if (!applicationMeta.getAll()) {
if (applicationMeta.getDisabled()) {
throw new PermissionException(springApplicationName + " is disabled");
}
if (applicationMeta.getPurview().indexOf(serviceCode) == -1) {
throw new PermissionException("no access to this servoceCode : " + serviceCode);
}
}
}
//获得请求方法
private String getRmsMethod(String serviceCode) {
return rmsProperties.getService().get(serviceCode).getMethod();
}
//构造url
private String getRmsUrl(String serviceCode) {
//获取服务元数据
ServiceMeta serviceMeta = rmsProperties.getService().get(serviceCode);
//构建请求路径
StringBuilder url =
new StringBuilder(serviceMeta.getIsHttps() ? Constant.HTTPS : Constant.HTTP);
url.append(rmsProperties.getApplication().get(serviceMeta.getOwner()).getServiceId());
url.append(serviceMeta.getUri());
return url.toString();
}
//计算sign
public static String sign(String rmsApplicationName, String secret) {
final String split = "_";
StringBuilder sb = new StringBuilder();
sb.append(rmsApplicationName).append(split).append(secret).append(split)
.append(new SimpleDateFormat(DATA_FORMAT).format(new Date()));
return DigestUtils.md5Hex(sb.toString());
}
###4.客户端调用
//获得文件信息
ResponseEntity<RestResponse<FileInfo>> fileInfo = rms.call("FILE_4", fileParam, null,
new ParameterizedTypeReference<RestResponse<FileInfo>>() {
}, null);
##结束
这样,规范了远程服务的调用,只关心接口编号和接口的入参和出参,能够增加沟通效率,并且也有了轻量级的服务治理机制,服务间的调用更可控,到最后,配置文件一拉出来一清二楚.
想获得最快更新,请关注公众号
Spring Boot 入门 - 进阶篇(4)- REST访问(RestTemplate)
经常需要发送一个GET/POST请求到其他系统(REST API),通过JDK自带的HttpURLConnection、Apache HttpClient、Netty 4、OkHTTP 2/3都可以实现。
HttpClient的使用:http://rensanning.iteye.com/blog/1550436
Spring的RestTemplate封装了这些库的实现,使用起来更简洁。
RestTemplate包含以下几个部分:
- HttpMessageConverter 对象转换器
- ClientHttpRequestFactory 默认是JDK的HttpURLConnection
- ResponseErrorHandler 异常处理
- ClientHttpRequestInterceptor 请求拦截器
Java代码
- @Service
- public class AccountService {
- @Autowired
- private RestTemplate restTemplate;
- // ...
- }
(1)发送GET请求(getForObject()、getForEntity()、exchange())
Java代码
- // 1-getForObject()
- User user1 = this.restTemplate.getForObject(uri, User.class);
- // 2-getForEntity()
- ResponseEntity<User> responseEntity1 = this.restTemplate.getForEntity(uri, User.class);
- HttpStatus statusCode = responseEntity1.getStatusCode();
- HttpHeaders header = responseEntity1.getHeaders();
- User user2 = responseEntity1.getBody();
- // 3-exchange()
- RequestEntity requestEntity = RequestEntity.get(new URI(uri)).build();
- ResponseEntity<User> responseEntity2 = this.restTemplate.exchange(requestEntity, User.class);
- User user3 = responseEntity2.getBody();
(2)发送POST请求(postForObject()、postForEntity()、exchange())
Java代码
- // 1-postForObject()
- User user1 = this.restTemplate.postForObject(uri, user, User.class);
- // 2-postForEntity()
- ResponseEntity<User> responseEntity1 = this.restTemplate.postForEntity(uri, user, User.class);
- // 3-exchange()
- RequestEntity<User> requestEntity = RequestEntity.post(new URI(uri)).body(user);
- ResponseEntity<User> responseEntity2 = this.restTemplate.exchange(requestEntity, User.class);
(3)设置HTTP Header信息
Java代码
- // 1-Content-Type
- RequestEntity<User> requestEntity = RequestEntity
- .post(new URI(uri))
- .contentType(MediaType.APPLICATION_JSON)
- .body(user);
- // 2-Accept
- RequestEntity<User> requestEntity = RequestEntity
- .post(new URI(uri))
- .accept(MediaType.APPLICATION_JSON)
- .body(user);
- // 3-Other
- RequestEntity<User> requestEntity = RequestEntity
- .post(new URI(uri))
- .header("Authorization", "Basic " + base64Credentials)
- .body(user);
(4)异常处理
捕获HttpServerErrorException
Java代码
- int retryCount = 0;
- while (true) {
- try {
- responseEntity = restTemplate.exchange(requestEntity, String.class);
- break;
- } catch (HttpServerErrorException e) {
- if (retryCount == 3) {
- throw e;
- }
- retryCount++;
- }
- }
自定义异常处理
Java代码
- public class CustomErrorHandler extends DefaultResponseErrorHandler {
- @Override
- public void handleError(ClientHttpResponse response) throws IOException {
- }
- }
Java代码
- @Configuration
- public class RestClientConfig {
- @Bean
- public RestTemplate restTemplate() {
- RestTemplate restTemplate = new RestTemplate();
- restTemplate.setErrorHandler(new CustomErrorHandler());
- return restTemplate;
- }
- }
(5)设置超时时间
Java代码
- @Configuration
- public class RestClientConfig {
- @Bean
- public RestTemplate restTemplate() {
- return new RestTemplate(clientHttpRequestFactory());
- }
- private ClientHttpRequestFactory clientHttpRequestFactory() {
- HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
- factory.setReadTimeout(2000);
- factory.setConnectTimeout(2000);
- return factory;
- }
- }
(6)设置连接池
Java代码
- @Configuration
- public class RestClientConfig {
- @Bean
- public ClientHttpRequestFactory httpRequestFactory() {
- return new HttpComponentsClientHttpRequestFactory(httpClient());
- }
- @Bean
- public RestTemplate restTemplate() {
- return new RestTemplate(httpRequestFactory());
- }
- @Bean
- public HttpClient httpClient() {
- Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory> create()
- .register("http", PlainConnectionSocketFactory.getSocketFactory())
- .register("https", SSLConnectionSocketFactory.getSocketFactory())
- .build();
- PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
- connectionManager.setMaxTotal(5);
- connectionManager.setDefaultMaxPerRoute(5);
- RequestConfig requestConfig = RequestConfig.custom()
- .setSocketTimeout(8000)
- .setConnectTimeout(8000)
- .setConnectionRequestTimeout(8000)
- .build();
- return HttpClientBuilder.create()
- .setDefaultRequestConfig(requestConfig)
- .setConnectionManager(connectionManager)
- .build();
- }
- }
(7)发送文件
Java代码
- MultiValueMap<String, Object> multiPartBody = new LinkedMultiValueMap<>();
- multiPartBody.add("file", new ClassPathResource("/tmp/user.txt"));
- RequestEntity<MultiValueMap<String, Object>> requestEntity = RequestEntity
- .post(uri)
- .contentType(MediaType.MULTIPART_FORM_DATA)
- .body(multiPartBody);
(8)下载文件
Java代码
- // 小文件
- RequestEntity requestEntity = RequestEntity.get(uri).build();
- ResponseEntity<byte[]> responseEntity = restTemplate.exchange(requestEntity, byte[].class);
- byte[] downloadContent = responseEntity.getBody();
- // 大文件
- ResponseExtractor<ResponseEntity<File>> responseExtractor = new ResponseExtractor<ResponseEntity<File>>() {
- @Override
- public ResponseEntity<File> extractData(ClientHttpResponse response) throws IOException {
- File rcvFile = File.createTempFile("rcvFile", "zip");
- FileCopyUtils.copy(response.getBody(), new FileOutputStream(rcvFile));
- return ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()).body(rcvFile);
- }
- };
- File getFile = this.restTemplate.execute(targetUri, HttpMethod.GET, null, responseExtractor);
(9)Spring Boot的RestTemplateBuilder
全局设置
Java代码
- @Component
- public class CustomRestTemplateCustomizer implements RestTemplateCustomizer {
- @Override
- public void customize(RestTemplate restTemplate) {
- new RestTemplateBuilder()
- .detectRequestFactory(false)
- .basicAuthorization("username", "password")
- .uriTemplateHandler(new OkHttp3ClientHttpRequestFactory())
- .errorHandler(new CustomResponseErrorHandler())
- .configure(restTemplate);
- }
- }
单独设置
Java代码
- @Service
- public class MyRestClientService {
- private RestTemplate restTemplate;
- public MyRestClientService(RestTemplateBuilder restTemplateBuilder) {
- this.restTemplate = restTemplateBuilder
- .basicAuthorization("username", "password")
- .setConnectTimeout(3000)
- .setReadTimeout(5000)
- .rootUri("http://api.example.com/")
- .errorHandler(new CustomResponseErrorHandler())
- .additionalMessageConverters(new CustomHttpMessageConverter())
- .uriTemplateHandler(new OkHttp3ClientHttpRequestFactory())
- .build();
- }
- public String site() {
- return this.restTemplate.getForObject("http://rensanning.iteye.com/", String.class);
- }
- }
参考:
http://qiita.com/kazuki43zoo/items/7cf3c8ca4f6f2283cefb
http://terasolunaorg.github.io/guideline/5.1.0.RELEASE/ja/ArchitectureInDetail/RestClient.html
- 查看图片附件
程序猿必关注这个技术,风头正劲,仅4个月薪酬35K+
大数据技术与运用的成熟,应用集中于互联网、金融、医疗、新能源、通信和房地产等行业。整理平均薪资情况和大数据学习大纲供查看
关于Spring MVC中使用RestTemplate访问REST服务详解和spring mvc restful的介绍已经告一段落,感谢您的耐心阅读,如果想了解更多关于java – SpringBoot @WebMvcTest,自动装配RestTemplateBuilder、RestTemplate相关组件:ClientHttpRequestInterceptor【享学Spring MVC】、spring boot / cloud (八) 使用RestTemplate来构建远程调用服务、Spring Boot 入门 - 进阶篇(4)- REST访问(RestTemplate)的相关信息,请在本站寻找。
本文标签: