【工作经历】-对接第三方平台

524 阅读3分钟

1. springboot自带restTemplate简述

springboot自带restTemplate缺陷:

  1. 底层使用jdk自带的URLConnection
  2. 当第三方接口返回http状态码为40几的时候,会抛出异常

基于以上原因,自定义一下restTemplate

  1. 底层改用OKHttp
  2. 拦截40几异常处理

2. 自定义springboot-starter,改写restTemplate

  1. 依赖引入

依赖的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>
  1. 自定义配置类
@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;

}
  1. 自定义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);
    }
}
  1. 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,并且重试一次当前请求的接口。整体流程图如下:

  1. 接口请求层面aop拦截处理流程:

  2. 定时任务刷新token

每隔23小时请求一次登录接口,获取最新的token,存入自营平台redis

  1. 相关代码片段
  1. 重试机制
 @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();
    }
}