1. springboot自带restTemplate简述
springboot自带restTemplate缺陷:
- 底层使用jdk自带的URLConnection
- 当第三方接口返回http状态码为40几的时候,会抛出异常
基于以上原因,自定义一下restTemplate
- 底层改用OKHttp
- 拦截40几异常处理
2. 自定义springboot-starter,改写restTemplate
- 依赖引入
依赖的web,test包的作用域(scope)均为provide/test,打包的时候该依赖包不会打包到依赖该starter的项目中去,依赖该starter的包中需要自行引入web,test包
<dependencies>
<dependency>
<groupId>com.basic</groupId>
<artifactId>basic-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.basic</groupId>
<artifactId>basic-starter-redis</artifactId>
<version>1.0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 定制restTemplate需要的依赖 start -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.7.2</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>1.3.70</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- 定制restTemplate需要的依赖 end -->
</dependencies>
- 自定义配置类
@Getter
@Setter
@ConfigurationProperties(prefix = "ylwl.api")
public class PriceTagPropery {
// 网关地址
private String gatewayUrl;
private String userName;
private String password;
// 新增价签时用到的activekey,目前是固定值
private String activekey;
// token过期时间(单位: 秒)
private long expireTime;
}
- 自定义configuration类
angsiRestTemplate
@Configuration
@EnableConfigurationProperties(PriceTagPropery.class)
@ConditionalOnProperty(name = "ylwl.api.enable",havingValue = "true",matchIfMissing = true)
public class RestTemplateConfiguration {
/**
* 定制化restTemplate
*/
@Bean
@Primary
@ConditionalOnMissingBean(name = "angsiRestTemplate")
public RestTemplate angsiRestTemplate(RestTemplateBuilder builder){
// 改写底层,使用OKhttp
OkHttp3ClientHttpRequestFactory requestFactory = new OkHttp3ClientHttpRequestFactory();
RestTemplate restTemplate = builder.build();
restTemplate.setRequestFactory(requestFactory);
// 设置公共请求头信息
restTemplate.setInterceptors(Collections.singletonList(new UserAgentInterceptor()));
// 处理返回状态码非200时的异常
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
public boolean hasError(ClientHttpResponse response) throws IOException {
HttpStatus statusCode = response.getStatusCode();
return statusCode.series() == HttpStatus.Series.SERVER_ERROR;
}
});
return restTemplate;
}
}
UserAgentInterceptor
public class UserAgentInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
// 配置请求头信息
HttpHeaders requestHeaders = request.getHeaders();
requestHeaders.add(HttpHeaders.ACCEPT, "*/*");
requestHeaders.add(HttpHeaders.CONNECTION, "Keep-Alive");
requestHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
requestHeaders.add(HttpHeaders.USER_AGENT,
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36");
return execution.execute(request, body);
}
}
- spring.factories配置
META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
com.angsi.hart.common.config.RestTemplateConfiguration
3. 封装构建请求发送解析api工具类
get请求工具类方法
/**
* 云里物里api调用,get请求
*/
public YlwlResponse sendGet(String url, Map<String,Object> params,String token){
MultiValueMap<String, String> header = new LinkedMultiValueMap();
// 设置登录票据
if(StringUtils.isNotEmpty(token)){
header.add(HttpHeaders.AUTHORIZATION,token);
}
// 请求参数设置
if(!CollectionUtils.isEmpty(params)){
url = url + "?";
for(String key : params.keySet()){
Object value = params.get(key);
if(null != value){
url += key + "=" + params.get(key) + "&";
}
}
}
HttpEntity<MultiValueMap<String,Object>> requestEntity = new HttpEntity<>(header);
// 发送请求&结果封装
ResponseEntity<String> exchange = restTemplate.exchange(url,
HttpMethod.GET, requestEntity, String.class);
// 封装成自定义实体类
YlwlResponse response = new YlwlResponse(exchange.getStatusCodeValue(), exchange.getBody());
return response;
}
post请求工具类方法
/**
* 云里物里api调用,post请求
*/
public YlwlResponse sendPost(String url, Map<String,Object> params,String token){
MultiValueMap<String, String> header = new LinkedMultiValueMap();
// 设置登录票据
if(StringUtils.isNotEmpty(token)){
header.add(HttpHeaders.AUTHORIZATION,token);
}
// 请求参数设置
Map<String, Object> param = new LinkedHashMap<>();
if(!CollectionUtils.isEmpty(params)){
params.keySet().forEach(key -> {
param.put(key,params.get(key));
});
}
HttpEntity<Map<String,Object>> requestEntity = new HttpEntity<>(param, header);
// 发送请求得到响应
ResponseEntity<String> exchange = restTemplate.postForEntity(url, requestEntity, String.class);
// 封装成自定义实体类
YlwlResponse response = new YlwlResponse(exchange.getStatusCodeValue(), exchange.getBody());
return response;
}
4. 登录票据处理机制
背景:第三方提供了一个登陆接口,返回一个有效期为24小时的token,仅有token,并无token的创建时间,另外自己平台服务器和第三方平台服务器的时间上可能有出入,故此,我们这边获取到token后,不能让token的有效期为24小时,应该比这个时间短,可设置成23小时;另外,做一个重试机制,当请求第三方接口时,如果第三方的响应中的结果码为token失效时,应调用一次登录接口获取最新的token存入本地redis,并且重试一次当前请求的接口。整体流程图如下:
-
接口请求层面aop拦截处理流程:
-
定时任务刷新token
每隔23小时请求一次登录接口,获取最新的token,存入自营平台redis
- 相关代码片段
- 重试机制
@Around(value = "excudeService()")
public Object doAroud(ProceedingJoinPoint pjp) throws Throwable {
if (null != pjp.getArgs() && pjp.getArgs().length == 3 && !pjp.getArgs()[0].toString()
.contains(UrlConstant.LOGIN_URL)) {
// 非登录接口,做返回结果token校验,为失效token时,刷新token
try {
Object[] args = pjp.getArgs();
YlwlResponse response = (YlwlResponse) pjp.proceed(args);
log.info("ylwl api request url:{},param:{},token:{},response:{}", JSONObject.toJSONString(args[0]),
JSONObject.toJSONString(args[1]), JSONObject.toJSONString(args[2]),
JSONObject.toJSONString(response));
// 是否是token过期判断,token过期了,替换原来的token,重新请求一次
return retry(pjp, response);
} catch (Exception e) {
log.error("aspect errror happend:", e);
}
}
return pjp.proceed();
}
/**
* 响应结果判断是否需要重试
*/
public YlwlResponse retry(ProceedingJoinPoint pjp, YlwlResponse response) throws Throwable {
if (null != pjp.getArgs() && pjp.getArgs().length > 0 && !pjp.getArgs()[0].toString()
.contains(UrlConstant.LOGIN_URL)) {
// 非登录接口,校验返回结果是否是token过期,如果token过期了,重新获取token,并重新请求一次接口
if (HttpStatus.UNAUTHORIZED.value() == response.getCode()) {
// token过期,重新获取token
String token = ylwlAPI.refreshToken();
if (StringUtils.isNotEmpty(token)) {
// 用新的token重新请求
Object[] args = pjp.getArgs();
args[2] = token;
response = (YlwlResponse) pjp.proceed(args);
log.info("ylwl api request retry url:{},param:{},token:{},response:{}", JSONObject.toJSONString(args[0]),
JSONObject.toJSONString(args[1]), JSONObject.toJSONString(args[2]),
JSONObject.toJSONString(response));
}
}
}
return response;
}
2)定时任务刷新token
@Job(cron = "* * 0/23 * * ? *", description = "刷新登录票据缓存到redis")
@Slf4j
public class YlwlTokenJob implements SimpleJob {
@Autowired
private YlwlAPI ylwlAPI;
@Override
public void execute(ShardingContext shardingContext) {
// 调用刷新token接口
ylwlAPI.refreshToken();
}
}