为什么要使用RestTemplate?
随着微服务的广泛使用,在实际的开发中,客户端代码中调用RESTful接口也越来越常见。在系统的遗留代码中,你可能会看见有一些代码是使用HttpURLConnection
来调用RESTful接口的,类似于下面这样:
URL url = ...
// 打开连接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
try {
conn.setRequestMethod("POST");
conn.setDoInput(true);
conn.setDoOutput(true);
conn.connect();
// 发送数据...
BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(conn.getOutputStream(), "utf-8"));
bw.write(str);
// 接收数据 ...
BufferedReader br = new BufferedReader(
new InputStreamReader(conn.getInputStream(), "utf-8"));
String line = null;
while ((line = br.readLine()) != null) {
...
}
} finally {
conn.disconnect();
}
从上面的代码可以看出,使用HttpURLConnection
调用RESTful接口是比较麻烦的,假如要调用30个接口,每个接口都使用类似于上面的代码 进行调用,那简直是一场灾难(写这么多无聊的样板代码,内心绝对是崩溃的)。有人可能会想,将常用的RESTful操作(例如GET、POST、DELETE)封装成工具类,再调用不是也可以吗!这样做确实可行,但是要封装成通用的工具类不是那么简单的(需要一定的经验)。调用RESTful接口,还有另外一种选择:Apache HttpComponents
。关于如何调用,可以参考我之前写过的一篇文章:学习HttpClient,从两个小例子开始。虽然使用它发送HTTP请求确实挺方便的,但是使用它调用RESTful接口好像也挺麻烦的!
直到我遇到了RestTemplate
,世界顿时都明亮了,腰也不酸了,腿也不疼了,一下子就对接好了10个RESTful接口。写的代码可能变成是这样子的:
RestTemplate template = ...
// 请求地址
String url = ...
// 请求参数
UserParams request = ...
// 执行POST请求
User u = template.postForObject(url, request, User.class);
...
上面的调用代码变的简洁了很多,是不是很爽啊!RestTemplate是spring提供用来调用RESTful接口的类,里面提供了大量便捷的方法,如下:
执行不同的请求,只需找到这种请求方式所对应的方法就行,上例中的postForObject
就是发送的POST请求。如果上面的请求没有找到对应的方法,可以使用更加通用的exchange
和execute
方法。
RestTemplate的可扩展性也很强(下面列出比较常用的几种方式):
- RestTemplate默认使用的是JDK中的HttpURLConnection实现的,如果你想要切换到其他的HTTP库,例如,Apache HttpComponents, Netty和OkHttp,只需要调用
setRequestFactory
方法来进行设置,甚至可以使用自己实现的类型,只需要继承自ClientHttpRequestFactory
。(一般情况下,使用默认的实现就好) - RestTemplate默认使用的是
DefaultResponseErrorHandler
响应错误处理器,你可以调用setErrorHandler
来定制自己的响应错误处理器。 - 客户端请求拦截器:
ClientHttpRequestInterceptor
,实现这个接口,并在org.springframework.web.client.RestTemplate#setInterceptors(java.util.List)
中进行注册,能对请求头和请求体的内容进行修改,并能对响应的内容进行修改。(下面将会讲解) - RestTemplate中的
doExecute
方法,所有请求方式最终都会执行这个方法,重写此方法能完成一些必要的操作。
RestTemplate的妙用
考虑这样一个场景:假如说A部门开发的用户相关的微服务接口,提供给B部门来使用,在使用时需要在请求头中加入基本的认证头信息,认证头的格式如下:
Authorization=Basic {token}
token的格式是:base64(username:password)。假如username是zfx,密码是123,结果是先拼接用户名和密码:zfx:123
,再用utf-8格式获取其字节码,进行base64编码的结果为:emZ4OjEyMw==
假如要调用A部门的接口,根据id来获取用户的信息,代码如下:
String userId = "11";
String url = "http://127.0.0.1:8080/v1/users/{id}";
RestTemplate restTemplate = new RestTemplate();
// 基本的认证头信息
String username = "zfx";
String password = "123";
String token = Base64Utils.encodeToString(
(username + ":" + password).getBytes(StandardCharsets.UTF_8));
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Basic " + token);
HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
ResponseEntity<User> exchange = restTemplate.exchange(url, HttpMethod.GET,
requestEntity, User.class, userId);
User result = exchange.getBody();
首先先创建RestTemplate
的实例,在实际的开发中,最好不要每次都创建RestTemplate的实例,最好在spring中以单例的方式来配置,要使用的地方直接注入。用base64来编码了用户名和密码,然后在请求头中进行设置。restTemplate.exchange执行请求,并获取返回的结果。上面的代码其实并不难,但是这样写会不会有啥问题呢!假如说,叫你对接A部门的根据机构id来获取机构信息的接口,你岂不是又要把上面的代码再重新复制一下吗?这样不是很麻烦,代码会很臃肿。
spring提供了ClientHttpRequestInterceptor这个类,适合用来处理这种情况,加入基本的认证头信息,可以使用spring提供的这个类:BasicAuthorizationInterceptor。使用配置的方式来配置RestTemplate:
@Configuration
public class RestTemplateConfig {
@Value("${zfx.username}")
private String username;
@Value("${zfx.password}")
private String password;
@Bean("basicAuthRestTemplate")
public RestTemplate createBasicAuthRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(new BasicAuthorizationInterceptor(username, password));
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
}
上面的代码在创建basicAuthRestTemplate时,会加入基本的认证头信息的拦截器,来设置基本认证头信息。
再次调用上面的接口时,代码可以这样写:
@Autowired
RestTemplate basicAuthRestTemplate;
...
String userId = "11";
String url = "http://127.0.0.1:8080/v1/users/{id}";
User result = basicAuthRestTemplate.getForObject(url, User.class, userId);
代码一下子简洁了这么多,假如说要调用根据机构id来获取机构信息的接口呢?如下:
@Autowired
RestTemplate basicAuthRestTemplate;
...
String orgId = "11";
String url = "http://127.0.0.1:8080/v1/orgs/{id}";
Org result = basicAuthRestTemplate.getForObject(url, Org.class, orgId);
代码一下子简洁了很多,对接这些接口的程序员不用再关心认证是怎么一回事,认证这件事对于他们完全是透明的,他们只需要专注于编写他们自己的逻辑代码就可以了。ClientHttpRequestInterceptor的实现也很简单,代码如下:
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
String token = Base64Utils.encodeToString(
(this.username + ":" + this.password).getBytes(StandardCharsets.UTF_8));
request.getHeaders().add("Authorization", "Basic " + token);
return execution.execute(request, body);
}
在intercept方法中加入了基本的认证头信息。
假如说,有一天认证方式变了,改成OAuth2.0了,那么我们只需要实现自己的请求拦截器就行了,如下:
public class BearerAuthorizationIntercept
implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
String token = 这些写获取token的逻辑;
request.getHeaders().add("Authorization", "Bearer " + token);
return execution.execute(request, body);
}
}
然后配置restTemplate:
@Bean("bearerAuthRestTemplate")
public RestTemplate createBearerAuthRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(new BearerAuthorizationIntercept());
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
那么只需要注入bearerAuthRestTemplate,就能使用他了:
@Autowired
RestTemplate bearerAuthRestTemplate;
...
String userId = "11";
String url = "http://127.0.0.1:8080/v1/users/{id}";
User result = bearerAuthRestTemplate.getForObject(url, User.class, userId);