📝 需要支持特性
- 每次请求都支持单独的超时时间配置(因为每个URL对应的超时时间配置不一样)
- 支持请求参数拦截对其加密后再发起请求,如果响应参数也有加密的情况,也要对其解密
之前有看过SpringCloud的源码,Ribbon在实现负载均衡策略上的方式对我印象深刻,本次的实现思路和Ribbon在restTemplate上的扩展方式基本一致。
😅 思路
开发使用基础架构提供的RestTemplate → 发起请求 → RestTemplate自定义拦截器 → 加签逻辑 → 发起请求
如何让开发使用我们增强过的RestTemplate的实例呢?
🤪 使用的技术点
1、Spring提供的@Qualifier注解
1.1 @Qualifier注解
学习过Spring基础的同学都会很熟悉这个注解,使用这个注解的大多数的目的是为了区分开我们注入的具体bean。例如:
@Data
public class User{
private String name;
private String sex;
}
@Bean("user1")
User user1() {
return new User("1", "woman");
}
@Bean("user2")
User user2() {
return new User("2", "man");
}容器里有两个user的bean,当我们想指定某一个bean注入到我们的服务里时,可以使用@Qualifier注解
public class UserService{
@Autowired
@Qualifier(value = "user2")
User user;
public String getUserName(){
return user.getName();
}
}这个是我们常用的方式,其实@Qualifier注解还有一些比较特殊且好用的技巧
- 注入map和list
上面的两个User的bean实例对象还可以注入到map和list中
@Autowired(required = false)
private List<User> users = Collections.emptyList();
@Autowired(required = false)
private Map<String,User> userMap = new HashMap<>();写两个测试用例看下结果:
@RequestMapping("/users")
public String users(){
return JSON.toJSONString(users);
}
@RequestMapping("/userMap")
public String userMap(){
return JSON.toJSONString(userMap);
}结果:
[
{
"name":"1",
"sex":"woman"
},
{
"name":"2",
"sex":"man"
}
]
{
"user1":{
"name":"1",
"sex":"woman"
},
"user2":{
"name":"2",
"sex":"man"
}
}- 给bean打上@Qualifier修饰符,指定注入
@Bean("user1")
@Qualifier("valid")
User user1() {
return new User("1", "woman");
}
@Qualifier("invalid")
@Bean("user2")
User user2() {
return new User("2", "man");
}我们把测试的controller也加上这个修饰符,看下注入结果是否不一样
@Autowired(required = false)
@Qualifier("valid")
private List<User> users = Collections.emptyList();
@Autowired(required = false)
@Qualifier("invalid")
private Map<String,User> userMap = new HashMap<>();
@RequestMapping("/list")
public Object listUsers() {
return users;
}
@RequestMapping("/map")
public Object mapUsers() {
return userMap;
}再分别请求,结果:
[
{
"name":"1",
"sex":"woman"
}
]
{
"user2":{
"name":"2",
"sex":"man"
}
}这里可以发现@Qualifier修饰符还有过滤的作用,而SpringCloud中Ribbon获取RestTemplate就是采用了这个小技巧。
1.2 从Ribbon中学到的技巧
在使用springcloud ribbon客户端负载均衡的时候,可以给RestTemplate bean 加一个@LoadBalanced注解,就能让这个RestTemplate在请求时拥有客户端负载均衡的能力
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}看下LoadBalanced源码,发现其中并没有什么特殊的地方:
package org.springframework.cloud.client.loadbalancer;
import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient
* @author Spencer Gibb
*/
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}其实主要发挥作用的就是@Qualifier注解了,@Qualifier支持继承,LoadBalanced 有了@Qualifier能力。
搜索@LoadBalanced注解的使用地方,发现只有一处使用了,
在LoadBalancerAutoConfiguration这个自动装配类中:
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
final List<RestTemplateCustomizer> customizers) {
return new SmartInitializingSingleton() {
@Override
public void afterSingletonsInstantiated() {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
}
};
}
@Autowired(required = false)
private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
@Bean
@ConditionalOnMissingBean
public LoadBalancerRequestFactory loadBalancerRequestFactory(
LoadBalancerClient loadBalancerClient) {
return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
}
@Configuration
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return new RestTemplateCustomizer() {
@Override
public void customize(RestTemplate restTemplate) {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
}
};
}
}这段自动装配的代码的含义不难理解,就是利用了RestTemplate的拦截器,使用RestTemplateCustomizer对所有标注了@LoadBalanced的RestTemplate Bean添加了一个LoadBalancerInterceptor拦截器,而这个拦截器的作用就是对请求的URI进行转换获取到具体应该请求哪个服务实例ServiceInstance。
有以上的知识,我们可以知道:
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();restTemplates这个list能拿到所有加上@LoadBalanced注解的RestTemplate的bean。
1.3 实践
- 新建一个注解 @HPS
package com.envconfig.common.hps;
import java.lang.annotation.*;
import org.springframework.beans.factory.annotation.Qualifier;
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface HPS {
}- 提供一个专门支持加解签的RestTemplate
@Bean
@HPS
RestTemplate restTemplate() {
//bean增强 ...
return new RestTemplate();
}- 开发同学使用
@RestController
public class HpsTestController {
@Autowired
@HPS
private RestTemplate restTemplate;
@RequestMapping("/test")
public String test(){
return restTemplate.getForObject("https://www.baidu.com", String.class);
}
}2、RestTemplate的扩展
通过使用@HPS注解,我们已经可以让开发同学使用增强后的restTemplate了,那如何对restTemplate进行增强呢?
其实从Ribbon的源码LoadBalancerAutoConfiguration中,我们可以学习到如何去做。
LoadBalancerAutoConfiguration中注入了RestTemplateCustomizer,将自定义的customize逻辑注入到restTemplate中。
RestTemplate的扩展点就是RestTemplateCustomizer,我们可以在其中魔改拿到的restTemplate。
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return new RestTemplateCustomizer() {
@Override
public void customize(RestTemplate restTemplate) {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
}
};
}在customize中,ribbon注入了自定义的拦截器负责负载均衡,那我们也在此自定义一个拦截器对请求参数进行加签即可。
- 自定义
HpsRestTemplateCustomizer
public class HpsRestTemplateCustomizer implements RestTemplateCustomizer, ClientHttpRequestInterceptor {
public static final Logger log = LoggerFactory.getLogger(HpsRestTemplateCustomizer.class);
public static final String ALGORITHM__MD5 = "MD5"; // 签名算法, 默认MD5
public HpsRestTemplateCustomizer() {
}
@Override
public void customize(RestTemplate restTemplate) {
List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
List<ClientHttpRequestInterceptor> list = new ArrayList<>(interceptors);
list.add(this);
restTemplate.setInterceptors(list);
}
@Override
public ClientHttpResponse intercept(
HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution)
throws IOException {
Map<String, Object> reqParamsMap = new HashMap<>();
reqParamsMap = getUrlQueryMap(httpRequest);
Map<String, Object> postParamsMap = getQueryMap(httpRequest, bytes);
reqParamsMap.putAll(postParamsMap);
Map<String, Object> hpsSignedParamsMap = toHpsSignedParamsMap(config, reqParamsMap);
if ("GET".equalsIgnoreCase(httpRequest.getMethodValue())){
httpRequest = 加密GET请求参数;
}else if("POST".equalsIgnoreCase(httpRequest.getMethodValue())) {
bytes = 加密POST请求参数;
}
return clientHttpRequestExecution.execute(httpRequest, bytes);
public static Map<String, Object> getUrlQueryMap(HttpRequest request) {
String urlQueryString = request.getURI().getQuery();
Map<String, Object> queryMap = new HashMap<>();
String[] arrSplit;
if (urlQueryString == null) {
return queryMap;
} else {
//每个键值为一组
arrSplit = urlQueryString.split("[&]");
for (String strSplit : arrSplit) {
String[] arrSplitEqual = strSplit.split("[=]");
//解析出键值
if (arrSplitEqual.length > 1) {
queryMap.put(arrSplitEqual[0], arrSplitEqual[1]);
} else {
if (!"".equals(arrSplitEqual[0])) {
queryMap.put(arrSplitEqual[0], "");
}
}
}
}
return queryMap;
}
public static Map<String, Object> getQueryMap(HttpRequest request, byte[] body) {
String reqBodyStr = "";
if (body != null && request.getHeaders().getContentType() != null) {
String contentType =
request.getHeaders().getContentType().toString().toLowerCase();
if (contentType.contains("application/x-www-form-urlencoded")
|| contentType.contains("application/json")
|| contentType.contains("text/xml")) {
String charset = !contentType.contains("gbk") && !contentType.contains("gb2312") ? "utf-8" : "gbk";
reqBodyStr = bodyToString(body, charset);
}
}
Map<String, Object> reqMap = new HashMap<>();
if (StringUtils.isNotBlank(reqBodyStr)){
reqMap = JsonUtil.toMap(reqBodyStr);
}
return reqMap;
}
private static String bodyToString(byte[] body, String charset) {
try {
return new String(body, charset);
} catch (Exception e) {
return new String(body);
}
}
}- 将
HpsRestTemplateCustomizer注入容器,并增强hpsRestTemplates
@Configuration
@ConditionalOnClass(RestTemplate.class)
public class HpsAutoConfiguration {
//获取被HPS注解修饰过的restTemplate
@HPS
@Autowired(required = false)
private Map<String, RestTemplate> hpsRestTemplates = Collections.emptyMap();
@Bean
public SmartInitializingSingleton hpsRestTemplateInitializer(HpsRestTemplateCustomizer hpsRestTemplateCustomizer) {
return new SmartInitializingSingleton() {
@Override
public void afterSingletonsInstantiated() {
//增强被HPS注解修饰过的restTemplate
Map<String, RestTemplate> merchantRestTemplates = hpsRestTemplates;
merchantRestTemplates.forEach((merchantName, restTemplate) -> {
if ("hpsRestTemplate".equals(merchantName)) {
//这里是为了避免将开发自定义的restTemplate也增强了
hpsRestTemplateCustomizer.customize(restTemplate);
}
});
}
};
}
@Bean
@ConditionalOnMissingBean(name = "hpsRestTemplateCustomizer")
public HpsRestTemplateCustomizer hpsRestTemplateCustomizer(HpsMerchantConfig config) {
return new HpsRestTemplateCustomizer(config);
}
}3、每次请求都支持单独的超时时间配置
支持加解密的RestTemplate只有一个,但是需要根据不用的请求url有不同的超时时间,那如何进行设置呢?
思路是:
1、在需要自定义 RequsetConfig 的场景,将 RequsetConfig 存储在 ThreadLocal 中
2、在请求时,将自定义的RequsetConfig从ThreadLocal取出来注入到restTemplate
那现在就是寻找RestTemplate对配置的扩展点了,走查了一下午代码发现了HttpComponentsClientHttpRequestFactory,支持我们设置超时时间。
而他默认的SimpleClientHttpRequestFactory则不支持。
HttpComponentsClientHttpRequestFactory#createRequest源码:
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
HttpClient client = this.getHttpClient();
HttpUriRequest httpRequest = this.createHttpUriRequest(httpMethod, uri);
this.postProcessHttpRequest(httpRequest);
HttpContext context = this.createHttpContext(httpMethod, uri);
if (context == null) {
context = HttpClientContext.create();
}
if (((HttpContext)context).getAttribute("http.request-config") == null) {
RequestConfig config = null;
if (httpRequest instanceof Configurable) {
config = ((Configurable)httpRequest).getConfig();
}
if (config == null) {
config = this.createRequestConfig(client);
}
if (config != null) {
((HttpContext)context).setAttribute("http.request-config", config);
}
}
return (ClientHttpRequest)(this.bufferRequestBody ? new HttpComponentsClientHttpRequest(client, httpRequest, (HttpContext)context) : new HttpComponentsStreamingClientHttpRequest(client, httpRequest, (HttpContext)context));
}其中createHttpContext的结果HttpContext就是配置超时时间的地方,但是他默认的createHttpContext是直接返回null的(版本是spring-web-5.2.1.RELEASE)
@Nullable
protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) {
return null;
}那没办法我们只能继承这个类,自定义一个出来 HpsRequestContextFactory
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.protocol.HttpContext;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import java.net.URI;
public class HpsRequestContextFactory extends HttpComponentsClientHttpRequestFactory {
@Override
protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) {
//从ThreadLocal中获取到请求配置 requestConfig
RequestConfig requestConfig = RequestTimeOutConfigHolder.get();
if (requestConfig != null) {
HttpContext context = HttpClientContext.create();
context.setAttribute(HttpClientContext.REQUEST_CONFIG, requestConfig);
return context;
}
return null;
}
}- 将注入到容器中,生成一个支持动态修改超时时间的restTemplate
@Bean
@ConditionalOnMissingBean(name = {"hpsRequestContextFactory"})
HpsRequestContextFactory hpsRequestContextFactory() {
return new HpsRequestContextFactory();
}
@Bean
@HPS
@ConditionalOnMissingBean(name = {"hpsRestTemplate"})
RestTemplate hpsRestTemplate(HpsRequestContextFactory hpsRequestContextFactory) {
return new RestTemplate(clientHttpRequestFactory);
}
- 开发使用
@RestController
public class HpsTestController {
@Autowired
@HPS
private RestTemplate restTemplate;
@RequestMapping("/test2")
public String test(){
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(timeout).build();
RequestConfigHolder.bind(requestConfig);
return restTemplate.getForObject("https://www.baidu.com", String.class);
}
}🤗 总结归纳
这次扩展主要使用了Spring提供的@Qualifier修饰符和RestTemplate的扩展点。其实接到这个需求的时候,我就基本有了思路,很庆幸之前看过SpringCloud的源码。
沉淀是一件非常重要的事情,书到用时方恨少,只有当我们有足够的知识储备后,我们才能写出更优秀的代码,思考到更好的解决方案。
📎 参考文章
引用文章