Spring RestTemplate用法

类图

RestOperations 接口中定义了RestTemplate的各种操作,注释也说了,一般不怎么用这个类,但因为很容易在测试中Moked或者stubbed,测试还是很方便的。

HttpAccessorRestTemplate抽象类,默认的ClientHttpRequestFactory,采用JDK的HttpURLConnection构造

1
private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();

可以通过HttpAccessorsetRequestFactory指定ClientHttpRequestFactory,使用HttpComponentsClientHttpRequestFactory方式,底层使用HttpClient访问远程的Http服务,使用HttpClient可以配置连接池和证书等信息。

POM依赖

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

RestTemplate构造方法

代码

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
public RestTemplate() {
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(new StringHttpMessageConverter());
this.messageConverters.add(new ResourceHttpMessageConverter());
this.messageConverters.add(new SourceHttpMessageConverter<Source>());
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());

if (romePresent) {
this.messageConverters.add(new AtomFeedHttpMessageConverter());
this.messageConverters.add(new RssChannelHttpMessageConverter());
}

if (jackson2XmlPresent) {
this.messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
}
else if (jaxb2Present) {
this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
}

if (jackson2Present) {
this.messageConverters.add(new MappingJackson2HttpMessageConverter());
}
else if (gsonPresent) {
this.messageConverters.add(new GsonHttpMessageConverter());
}
}

public RestTemplate(ClientHttpRequestFactory requestFactory) {
this();
setRequestFactory(requestFactory);
}


public RestTemplate(List<HttpMessageConverter<?>> messageConverters) {
Assert.notEmpty(messageConverters, "At least one HttpMessageConverter required");
this.messageConverters.addAll(messageConverters);
}

共有3个构造方法,第一个无参构造方法,使用默认的HttpMessageConverter;第二个构造方法需要传入ClientHttpRequestFactory ,可以设置ClientHttpRequestFactory,第三个构造方法就是可以是这是多个HttpMessageConverter

上面提到RestTemplate的默认ClientHttpRequestFactorySimpleClientHttpRequestFactory,采用jdk的HttpURLConnection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static final int DEFAULT_CHUNK_SIZE = 4096;

private Proxy proxy;

private boolean bufferRequestBody = true;

private int chunkSize = DEFAULT_CHUNK_SIZE;

//默认连接超时是-1
private int connectTimeout = -1;

//默认读超时是-1
private int readTimeout = -1;

private boolean outputStreaming = true;

private AsyncListenableTaskExecutor taskExecutor;

从源码看的出超时时间都是-1,所以我们可以需要自定义SimpleClientHttpRequestFactory设置相关熟悉后,调用RestTemplate(ClientHttpRequestFactory requestFactory)生成RestTemplate

用Spring Boot构建的例子

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author Liu Hailin
* @create 2017-10-31 下午2:30
**/
@Configuration
@ConditionalOnProperty(prefix = "spring.rest-template",value = "enable",havingValue = "true")
@ConfigurationProperties(prefix = "spring.rest-template")
@Data
public class TemplateProptiesConfig {

private int connectionTimeout;
private int readTimeout;

}
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
/**
* @author Liu Hailin
* @create 2017-10-31 下午2:25
**/
@Configuration
@ConditionalOnBean(TemplateProptiesConfig.class)
@EnableConfigurationProperties(value = TemplateProptiesConfig.class)
public class TemplateClientConfig {

@Autowired
private TemplateProptiesConfig templateProptiesConfig;

@Bean
public ClientHttpRequestFactory createHttpRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout( templateProptiesConfig.getConnectionTimeout() );
factory.setReadTimeout( templateProptiesConfig.getReadTimeout() );
return factory;
}

@Bean
public RestTemplate createRestTemplate(ClientHttpRequestFactory factory) {
return new RestTemplate( factory );
}
}

application.yaml

1
2
3
4
5
6
7
8
9
server:
port: 8000
spring:
application:
name: restdemo
rest-template:
enable: true
connectionTimeout: 3000
readTimeout: 5000

好了,接下来,看一下怎么调用

我们在容器加载完成后,调用我们写的一个Rest Api。

代码

1
2
3
4
5
6
7
8
9
10
11
/**
* @author Liu Hailin
* @create 2017-10-31 下午1:55
**/
@RestController
public class TemplateDataController {
@GetMapping("/getdata")
public Result testData(){
return new Result( 0,"success" );
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @author Liu Hailin
* @create 2017-10-31 下午1:56
**/
@Data
@Builder
@AllArgsConstructor
public class Result {

private final int code;

private final String message;

@Override
public String toString() {
return "result{" +
"code='" + code + '\'' +
", message=" + message +
'}';
}
}

OK,我们知道spring中需要在应用启动或者重启后做一些工作,是通过ApplicationListener监听器来实现的,

在SpringBoot中也有,就是CommendLineRunnerApplicationRunner,区别看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Interface used to indicate that a bean should <em>run</em> when it is contained within
* a {@link SpringApplication}. Multiple {@link CommandLineRunner} beans can be defined
* within the same application context and can be ordered using the {@link Ordered}
* interface or {@link Order @Order} annotation.
* <p>
* If you need access to {@link ApplicationArguments} instead of the raw String array
* consider using {@link ApplicationRunner}.
*
* @author Dave Syer
* @see ApplicationRunner
*/
public interface CommandLineRunner {

/**
* Callback used to run the bean.
* @param args incoming main method arguments
* @throws Exception on error
*/
void run(String... args) throws Exception;

}

Interface used to indicate that a bean should run when it is contained within a {@link SpringApplication}.说明这两个类在SrpingAplication注册一个Bean就可以Run。

另外如果想使用更详细的命令行参数,就用ApplicationRunner,我们就用CommandLineRunner好了。

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
/**
* @author Liu Hailin
* @create 2017-10-31 上午11:38
**/
@SpringBootApplication
@Slf4j
public class RestApplication extends WebMvcConfigurerAdapter {

public static void main(String[] args) {
SpringApplication.run( RestApplication.class, args );
}

@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController( "/" ).setViewName( "home" );
}

@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker().suffix( ".ftl" );
registry.freeMarker().cache( false );
}

@Bean
public CommandLineRunner commandLineRunner(RestTemplate template) {
return runner -> {
ResponseEntity<Result> result = template.getForEntity( "http://localhost:8000/getdata", Result.class );
log.info( "==={}===", result.getBody() );
};
}
}

启动后结果

1
2
3
4
5
2017-10-31 16:02:36.253  INFO 63444 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8000 (http)
2017-10-31 16:02:36.379 INFO 63444 --- [nio-8000-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet'
2017-10-31 16:02:36.379 INFO 63444 --- [nio-8000-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started
2017-10-31 16:02:36.391 INFO 63444 --- [nio-8000-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 12 ms
2017-10-31 16:02:36.462 INFO 63444 --- [ main] com.lhl.study.RestApplication : ===result{code='0', message=success}===

注意:遇到问题java.lang.IllegalArgumentException: No converter found for return value of type: class com.lhl.study.controller.Result

原因:Result没有是getter/setter方法,加上@Data,因为Jackson需要有getter/setter,否则转换失败。

设置HttpMessageConverter

前面的API接口返回的是JSON数据,RestTemplate通过默认的HttpMessageConverter MappingJackson2HttpMessageConverter 转换数据。

默认情况下RestTemplate自动帮我们注册了一组HttpMessageConverter用来处理一些不同的contentType的请求。
StringHttpMessageConverter来处理text/plain;MappingJackson2HttpMessageConverter来处理application/json;MappingJackson2XmlHttpMessageConverter来处理application/xml。你可以在org.springframework.http.converter包下找到所有spring帮我们实现好的转换器。如果现有的转换器不能满足你的需求,你还可以实现org.springframework.http.converter.HttpMessageConverter接口自己写一个。

设置ResponseErrorHandler

RestTemplate默认的ErrorHandler是DefaultResponseErrorHandler

1
private ResponseErrorHandler errorHandler = new DefaultResponseErrorHandler();

比如调用第三方返回其业务的ResponseCode,如果本业务需要对特殊ResponseCode的处理。可以自己实现ResponseErrorHandler

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
/**
* Strategy interface used by the {@link RestTemplate} to determine
* whether a particular response has an error or not.
*
* @author Arjen Poutsma
* @since 3.0
*/
public interface ResponseErrorHandler {

/**
* Indicate whether the given response has any errors.
* <p>Implementations will typically inspect the
* {@link ClientHttpResponse#getStatusCode() HttpStatus} of the response.
* @param response the response to inspect
* @return {@code true} if the response has an error; {@code false} otherwise
* @throws IOException in case of I/O errors
*/
boolean hasError(ClientHttpResponse response) throws IOException;

/**
* Handle the error in the given response.
* <p>This method is only called when {@link #hasError(ClientHttpResponse)}
* has returned {@code true}.
* @param response the response with the error
* @throws IOException in case of I/O errors
*/
void handleError(ClientHttpResponse response) throws IOException;

}

要实现2个方法,hasError 和 handleError,handleError只有在hasError返回true的时候才会调用。

接下来看RestTemplate#handleResponse方法

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
/**
* Handle the given response, performing appropriate logging and
* invoking the {@link ResponseErrorHandler} if necessary.
* <p>Can be overridden in subclasses.
* @param url the fully-expanded URL to connect to
* @param method the HTTP method to execute (GET, POST, etc.)
* @param response the resulting {@link ClientHttpResponse}
* @throws IOException if propagated from {@link ResponseErrorHandler}
* @since 4.1.6
* @see #setErrorHandler
*/
protected void handleResponse(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
ResponseErrorHandler errorHandler = getErrorHandler();
boolean hasError = errorHandler.hasError(response);
if (logger.isDebugEnabled()) {
try {
logger.debug(method.name() + " request for \"" + url + "\" resulted in " +
response.getRawStatusCode() + " (" + response.getStatusText() + ")" +
(hasError ? "; invoking error handler" : ""));
}
catch (IOException ex) {
// ignore
}
}
if (hasError) {
errorHandler.handleError(response);
}
}

hasError是true的时候,会执行errorHandler.handleError(response);

OK,具体执行再赘述。

完成我们的Handler如下:

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
/**
* @author Liu Hailin
* @create 2017-10-31 下午4:51
**/
@Slf4j
public class CustomErrorHandler implements ResponseErrorHandler {

private ThreadLocal<Result> requestResult = new ThreadLocal<Result>();

/**
* 是线程安全的
*
* @see <a href="https://stackoverflow.com/questions/20956385/jackson-objectmapper-jsongenerator-is-it-thread-safe">
* Jackson ObjectMapper & JsonGenerator - is it thread-safe?</a>
* Jackson's ObjectMapper creates a new JsonGenerator on each request for serialization. In that sense, it is
* guaranteed to be thread safe. The only thing that I can see that might cause the behavior you are seeing is if
* your CustomSerializer has some instance fields that it is sharing and is doing some kind of internal
* synchronization.
*/
private ObjectMapper objectMapper = new ObjectMapper();

/**
* 我们要特殊处理的code,这里可以做成可配置
*
* 如果泛型Object对象代替,要重写hashcode & equals方法,Integer不用处理,原因看其equals方法。
*
* 最好是枚举
*/
@Setter
private List<Integer> bizErrorCodes = new ArrayList<Integer>();

private ResponseErrorHandler defaultResponseErrorHandler = new DefaultResponseErrorHandler();

@Override
public boolean hasError(ClientHttpResponse response) throws IOException {

//判断是否是系统级错误
//多说一句,如果应用统一异常处理后,应该不会有系统异常,这里做一些冗余处理,防止出错。
if (defaultResponseErrorHandler.hasError( response )) {
return Boolean.TRUE;
}

//如果不是系统异常,那一定会返回我们的Resut对象
Result result = objectMapper.readValue( response.getBody(), Result.class );
/**
* 调用equals方法,这里要注意,前面有说明
*/
if (bizErrorCodes.contains( result.getCode() )) {
return Boolean.TRUE;
}

return Boolean.FALSE;
}

@Override
public void handleError(ClientHttpResponse response) throws IOException {

Result result = objectMapper.readValue( response.getBody(), Result.class );

log.error( "发生异常:{}", result );
}
}

其中bizErrorCodes集合通过@Setter设置

稍做修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
public ResponseErrorHandler createErrorHandler() {
CustomErrorHandler handler = new CustomErrorHandler();
List<Integer> codes = new ArrayList<>();
codes.add( -1 );
codes.add( -2 );
handler.setBizErrorCodes( codes );
return handler;
}
@Bean
public RestTemplate createRestTemplate(ClientHttpRequestFactory factory, ResponseErrorHandler handler) {
RestTemplate template = new RestTemplate( factory );
template.setErrorHandler( handler );
return template;
}

在RestTemplate中设置ErrorHadder为自定义Handler。

启动执行:

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
2017-10-31 18:58:12.200 ERROR 64057 --- [           main] o.s.boot.SpringApplication               : Application startup failed

java.lang.IllegalStateException: Failed to execute CommandLineRunner
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:735) [spring-boot-1.5.8.RELEASE.jar:1.5.8.RELEASE]
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:716) [spring-boot-1.5.8.RELEASE.jar:1.5.8.RELEASE]
at org.springframework.boot.SpringApplication.afterRefresh(SpringApplication.java:703) [spring-boot-1.5.8.RELEASE.jar:1.5.8.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:304) [spring-boot-1.5.8.RELEASE.jar:1.5.8.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1118) [spring-boot-1.5.8.RELEASE.jar:1.5.8.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1107) [spring-boot-1.5.8.RELEASE.jar:1.5.8.RELEASE]
at com.lhl.study.RestApplication.main(RestApplication.java:24) [classes/:na]
Caused by: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:8000/getdata": stream is closed; nested exception is java.io.IOException: stream is closed
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:666) ~[spring-web-4.3.12.RELEASE.jar:4.3.12.RELEASE]
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:613) ~[spring-web-4.3.12.RELEASE.jar:4.3.12.RELEASE]
at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:312) ~[spring-web-4.3.12.RELEASE.jar:4.3.12.RELEASE]
at com.lhl.study.RestApplication.lambda$commandLineRunner$0(RestApplication.java:41) [classes/:na]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:732) [spring-boot-1.5.8.RELEASE.jar:1.5.8.RELEASE]
... 6 common frames omitted
Caused by: java.io.IOException: stream is closed
at sun.net.www.protocol.http.HttpURLConnection$HttpInputStream.ensureOpen(HttpURLConnection.java:3348) ~[na:1.8.0_131]
at sun.net.www.protocol.http.HttpURLConnection$HttpInputStream.read(HttpURLConnection.java:3373) ~[na:1.8.0_131]
at com.fasterxml.jackson.core.json.ByteSourceJsonBootstrapper.ensureLoaded(ByteSourceJsonBootstrapper.java:522) ~[jackson-core-2.8.10.jar:2.8.10]
at com.fasterxml.jackson.core.json.ByteSourceJsonBootstrapper.detectEncoding(ByteSourceJsonBootstrapper.java:129) ~[jackson-core-2.8.10.jar:2.8.10]
at com.fasterxml.jackson.core.json.ByteSourceJsonBootstrapper.constructParser(ByteSourceJsonBootstrapper.java:246) ~[jackson-core-2.8.10.jar:2.8.10]
at com.fasterxml.jackson.core.JsonFactory._createParser(JsonFactory.java:1271) ~[jackson-core-2.8.10.jar:2.8.10]
at com.fasterxml.jackson.core.JsonFactory.createParser(JsonFactory.java:810) ~[jackson-core-2.8.10.jar:2.8.10]
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2924) ~[jackson-databind-2.8.10.jar:2.8.10]
at com.lhl.study.errorhandler.CustomErrorHandler.handleError(CustomErrorHandler.java:72) ~[classes/:na]
at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:700) ~[spring-web-4.3.12.RELEASE.jar:4.3.12.RELEASE]
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:653) ~[spring-web-4.3.12.RELEASE.jar:4.3.12.RELEASE]
... 10 common frames omitted

???WTF,Caused by: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:8000/getdata": stream is closed; nested exception is java.io.IOException: stream is closed

为什么会这样?

我们分析一下:

因为业务需要需要在hasError读取了一次Response,

1
2
//如果不是系统异常,那一定会返回我们的Resut对象
Result result = objectMapper.readValue( response.getBody(), Result.class );

因为response.getBody返回的是InputStream,只能读取一次。

后面不管是否成功失败,都会出现IO异常,因为不能再次读取,这么个大坑,怎么处理呢?

先看一个类:BufferingClientHttpResponseWrapper

1
2
3
4
5
6
7
@Override
public InputStream getBody() throws IOException {
if (this.body == null) {
this.body = StreamUtils.copyToByteArray(this.response.getBody());
}
return new ByteArrayInputStream(this.body);
}

这个类可以满足我们需要,因为在本地它保存了一份拷贝,但是这个类是final的,只能在Spring内部使用,至少思路是对的,保存一份拷贝。

再看一个BufferingClientHttpRequestFactory 这个类对ClientHttpRequestFactory封装了一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Create a buffering wrapper for the given {@link ClientHttpRequestFactory}.
* @param requestFactory the target request factory to wrap
*/
public BufferingClientHttpRequestFactory(ClientHttpRequestFactory requestFactory) {
super(requestFactory);
}

@Override
protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory)
throws IOException {

ClientHttpRequest request = requestFactory.createRequest(uri, httpMethod);
if (shouldBuffer(uri, httpMethod)) {
return new BufferingClientHttpRequestWrapper(request);
}
else {
return request;
}
}

BufferingClientHttpRequestWrapper包装了request

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
final class BufferingClientHttpRequestWrapper extends AbstractBufferingClientHttpRequest {

private final ClientHttpRequest request;


BufferingClientHttpRequestWrapper(ClientHttpRequest request) {
this.request = request;
}


@Override
public HttpMethod getMethod() {
return this.request.getMethod();
}

@Override
public URI getURI() {
return this.request.getURI();
}

@Override
protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
this.request.getHeaders().putAll(headers);
StreamUtils.copy(bufferedOutput, this.request.getBody());
ClientHttpResponse response = this.request.execute();
return new BufferingClientHttpResponseWrapper(response);
}

}

Ok,到这里明白了,executeInternal方法用BufferingClientHttpResponseWrapper包装了response,所有它可以实现前面我们多次读取。

回归到我们自己代码,修改一下

1
2
3
4
5
6
7
8
 @Bean
public RestTemplate createRestTemplate(ClientHttpRequestFactory factory, ResponseErrorHandler handler) {
BufferingClientHttpRequestFactory bufferingClientHttpRequestFactory = new BufferingClientHttpRequestFactory(
factory );
RestTemplate template = new RestTemplate( bufferingClientHttpRequestFactory );
template.setErrorHandler( handler );
return template;
}

执行后,没有问题,不贴出日志。

参考:https://stackoverflow.com/questions/13182519/spring-rest-template-usage-causes-eofexception

自定义header

1
2
3
4
5
6
7
8
9
HttpHeaders headers = new HttpHeaders();
headers.add( "auth-token", "123456" );
headers.setContentType( MediaType.APPLICATION_JSON );

RequestData requestData = RequestData.builder().name( "liuhailin" ).age( 30 ).build();

HttpEntity<RequestData> entity = new HttpEntity<RequestData>( requestData, headers );

ResponseEntity<Result> resultPost=template.postForEntity("http://localhost:8000/postdata", entity,Result.class );

为我们以后在请求中加入权限验证token,提供可能性。

提交Form数据

1
2
3
4
5
6
7
8
9
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

MultiValueMap<String, String> map= new LinkedMultiValueMap<String, String>();
map.add("email", "[email protected]");

HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map, headers);

ResponseEntity<String> response = restTemplate.postForEntity( url, request , String.class );

集成HttpClient

之前使用默认JDK的HttpURLConnection,下面我们集成HttpClient;

看一个类HttpComponentsClientHttpRequestFactory 实现了ClientHttpRequestFactory

OK,我们只要把之前的BufferingClientHttpRequestFactory 替换成HttpComponentsClientHttpRequestFactory就好了。

上代码

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package com.lhl.study.config;

import java.util.ArrayList;
import java.util.List;

import com.lhl.study.errorhandler.CustomErrorHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.BufferingClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestTemplate;

/**
* @author Liu Hailin
* @create 2017-10-31 下午2:25
**/
@Configuration
@ConditionalOnBean(TemplateProptiesConfig.class)
@EnableConfigurationProperties(value = TemplateProptiesConfig.class)
@Slf4j
public class TemplateClientConfig {

@Autowired
private TemplateProptiesConfig templateProptiesConfig;



@Bean(name = "httpclient")
@ConditionalOnClass(HttpClient.class)
public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory() {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
//SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial( null, new TrustStrategy() {
// @Override
// public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
// return true;
// }
//} ).build();
//httpClientBuilder.setSSLContext( sslContext );
//HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
//SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory( sslContext,
// hostnameVerifier );
//Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
// .register( "http", PlainConnectionSocketFactory
// .getSocketFactory() ).register( "https", sslConnectionSocketFactory ).build();// 注册http和https请求
//PoolingHttpClientConnectionManager poolingHttpClientConnectionManager
// = new PoolingHttpClientConnectionManager( socketFactoryRegistry );// 开始设置连接池
//poolingHttpClientConnectionManager.setMaxTotal( 200 ); // 最大连接数200
//poolingHttpClientConnectionManager.setDefaultMaxPerRoute( 20 ); // 同路由并发数20
//httpClientBuilder.setConnectionManager( poolingHttpClientConnectionManager );
//httpClientBuilder.setRetryHandler( new DefaultHttpRequestRetryHandler( 2, true ) );// 重试次数
HttpClient httpClient = httpClientBuilder.build();
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory
= new HttpComponentsClientHttpRequestFactory( httpClient );// httpClient连接配置
clientHttpRequestFactory.setConnectTimeout( 20000 );// 连接超时
clientHttpRequestFactory.setReadTimeout( 20000 );// 数据读取超时时间
clientHttpRequestFactory.setConnectionRequestTimeout( 200 );// 连接不够用的等待时间
return clientHttpRequestFactory;
}



@Bean
public ResponseErrorHandler createErrorHandler() {
CustomErrorHandler handler = new CustomErrorHandler();
List<Integer> codes = new ArrayList<>();
codes.add( -1 );
codes.add( -2 );
handler.setBizErrorCodes( codes );
return handler;
}

@Bean
@ConditionalOnBean(HttpComponentsClientHttpRequestFactory.class)
public RestTemplate createRestTemplateWithHttpClient(@Qualifier("httpclient") ClientHttpRequestFactory factory,
ResponseErrorHandler handler) {
RestTemplate template = new RestTemplate( factory );
template.setErrorHandler( handler );
return template;
}


@Bean(name="defatultFactory")
public ClientHttpRequestFactory createHttpRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout( templateProptiesConfig.getConnectionTimeout() );
factory.setReadTimeout( templateProptiesConfig.getReadTimeout() );
return factory;
}

@Bean
@ConditionalOnMissingBean(RestTemplate.class)
public RestTemplate createRestTemplate(@Qualifier("defatultFactory") ClientHttpRequestFactory factory,
ResponseErrorHandler handler) {
BufferingClientHttpRequestFactory bufferingClientHttpRequestFactory = new BufferingClientHttpRequestFactory(
factory );
RestTemplate template = new RestTemplate( bufferingClientHttpRequestFactory );
template.setErrorHandler( handler );
return template;
}
}

在spring boot中如果用条件注解的话,一定要注意@bean的顺序。题外话,希望后面有时间可以专门介绍。

OK,我们执行一下,发现还是会出现IO异常,跟我们之前遇到的一样

那么同样我们用BufferingClientHttpRequestFactory包装一下,并不是直接替换

修改一下

1
2
3
4
5
6
7
8
9
@Bean
@ConditionalOnBean(HttpComponentsClientHttpRequestFactory.class)
public RestTemplate createRestTemplateWithHttpClient(@Qualifier("httpclient") ClientHttpRequestFactory factory,ResponseErrorHandler handler) {
BufferingClientHttpRequestFactory bufferingClientHttpRequestFactory = new BufferingClientHttpRequestFactory(
factory );
RestTemplate template = new RestTemplate( bufferingClientHttpRequestFactory );
template.setErrorHandler( handler );
return template;
}

OK,再次执行,没有问题。

另外上面注释的地方是关于Httpclient的一些配置,可以搜一下具体的意义。

URI 模板

在URI上面用{}占位,后面通过uriVariables的数组或者map指定。

1
String result = restTemplate.getForObject("http://example.com/hotels/{hotel}/bookings/{booking}", String.class, "42", "21");
1
2
3
4
Map<String, String> vars = new HashMap<String, String>();
vars.put("hotel", "42");
vars.put("booking", "21");
String result = restTemplate.getForObject("http://example.com/hotels/{hotel}/bookings/{booking}", String.class, vars);

设置HTTPS

添加依赖,并使用httpclient的SSLConnectionSocketFactory

1
2
3
4
5
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.3</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean
public RestTemplate restTemplate() throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;
SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom()
.loadTrustMaterial(null, acceptingTrustStrategy)
.build();
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext);

CloseableHttpClient httpClient = HttpClients.custom()
.setSSLSocketFactory(csf)
.build();
HttpComponentsClientHttpRequestFactory requestFactory =
new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(httpClient);
return new RestTemplate(requestFactory);
}

异步调用AsyncRestTemplate

设置拦截器(ClientHttpRequestInterceptor)

坚持技术分享,您的支持将鼓励我继续创作!