Spring OpenFeign动态URL实战(下篇)

文摘   职场   2023-05-09 11:35   江苏  

    OpenFeign的设计宗旨是简化Java客户端的开发。在restTemplate的基础上做了进一步的封装来帮助我们定义和实现依赖服务接口的定义。在OpenFeign的协助下,我们只需创建一个接口并使用注解的方式进行配置即可完成对服务提供方的接口绑定,自动封装服务调用客户端的开发工作。

  1. 背景

    本篇主要接Feign的一小步,我的一大步(上篇),上下文对程序来说很重要,墙裂建议看官看一下,具体讲述Open Feign的实战过程,顺便解决了工作中遇到的问题;所有代码在本人写文章时为方便读者理解、节省篇幅,已做改动。


  2. 依葫芦画瓢

    如果你当前的项目框架采用的是spring cloud,那会方便很多。按如下步骤试一下吧:

    a.引入maven坐标

    <dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-starter-openfeign</artifactId>    <version>2.2.0.RELEASE</version></dependency>

    b.创建一个外部Client接口

    @FeignClient(name = "proxyServiceFeign")public interface ProxyServiceFeign {    /**     * post     * @param uri uri     * @param obj 可以是你自己定义的任何对象,能传参就行     */    @RequestLine("POST")    String post(URI uri,Object obj);
    //高德接口都是GET请求 目前我只用了定义的get方法 @RequestLine("GET") String get(URI uri);}
  3. c.在你业务代码处注入Client并初始化

    @Servicepublic class RestApiServiceImpl implements RestApiService {    private Logger log = LoggerFactory.getLogger(this.getClass());    //所有高德接口的前缀:https://restapi.amap.com/v3/    @Value("${restapi.amap.v3_url}")    private String V3_ROOT;        private final ProxyServiceFeign proxyServiceFeign;    // 这里很重要    public RestApiServiceImpl() {        proxyServiceFeign = Feign.builder().target(Target.EmptyTarget.create(ProxyServiceFeign.class));    }    }

    d.调用前解析请求参数、拼接URL,组装正确的URI

        /**     * 直接重定向到第三方 高德对应的接口,并且保存结果     * @param request       当前请求     * @param keys          当前可用的高德key集合      * @return     */    @Override    public BaseResponseEntity<JsonNode> redirectRequest2ThirdPart(HttpServletRequest request, Collection<String> keys){        log.info("redirectRequest2ThirdPart param:{}",request.getQueryString());        //当前项目维护的高德key,写在配置文件中        keys = CollectionUtils.isEmpty(keys) ? restKeyConfig.getKeymap().values().stream().distinct().collect(Collectors.toList()) : keys;        log.info("The currently available keys are:{}", JSON.toJSONString(keys));        //用于标识今日请求,当达到日限额时 直接透传高德响应给调用者        Boolean day_limit = this.initDayLimit2Redis();        //个人认证的N个key,避免单key过热被限,自己实现了一个均衡的key挑选策略        String key = this.randomKeyAndRemoveUseless(keys,day_limit);        log.info("same day operation sign is:{},random key:{}",day_limit,key);
            //这里是关键:参数截取、目标URL截取 String params = request.getQueryString(); params = URLDecoder.decode(params); String servletPath = request.getServletPath(); String target = servletPath.indexOf(Constants.REDIRECT_KEY) > 0 ? servletPath.split(Constants.REDIRECT_KEY)[1] : servletPath.split(Constants.ROUTE_KEY)[1];        String query = "?"+params+"&"+Constants.URL_KEY+"="+key; try {            /** * 组装定义的uri后直接通过feign调用外部接口 * V50 :@Value("${restapi.amap.enable_v5:false}") * https://restapi.amap.com/v3/direction/driving?parameters * https://restapi.amap.com/v5/direction/driving?parameters * 高德有的服务有v3、v5多个版本提供不一样的数据返回,我们还可以更灵活一点,增加一个配置,判断当某些 * 接口进来时走v5,有的走v3 */                      URI uri = URI.create((V50 && target.equals(Constants.A) ? String.format(V3_ROOT,Constants.C) :  String.format(V3_ROOT,Constants.B))+ target + query);
    String resp = proxyServiceFeign.get(uri); JsonNode node = objectMapper.readTree(resp); Integer status = Integer.parseInt(node.get(Constants.STATUS).asText()); int infoCode = Integer.parseInt(node.get(Constants.INFO_CODE).asText()); log.info("redirectRequest2ThirdPart:{} response:{}",uri, node); if (Constants.SUCCESS.equals(status)) {                //成功时直接响应,还可以写缓存,节约key的消耗次数 return ResponseUtil.returnSuccessResp(node); }            // 一些异常的响应码说明这个key要从内存中移除,递归后再次发起请求 else if(restKeyConfig.getCodes().contains(infoCode)){                log.info("this key:{} request return info_code:{} hits the configuration list:{} and executes the retry logic……",key,infoCode,JSON.toJSONString(restKeyConfig.getCodes()));                return this.redirectRequest2ThirdPart(request,this.removeInvalidKeyAndRecord(keys,key,day_limit)); } return ResponseUtil.returnErrorResp(CommonStateCode.ILLEGAL_STATUS,String.format("Network Request Failed When Query AliMap, Code:[%s] infoCode:[%s] Reason:[%s]",status,infoCode, node.get("info").asText())); } catch (Exception exception){            log.error("redirectRequest2ThirdPart exception:{}", exception.getMessage()); throw new BusinessException(CommonStateCode.ILLEGAL_STATUS.getCode(),String.format("Network Request Failed When Query AliMap, Reason:[ %s ]", exception.getCause())); } }    //用于存储当日不能再使用的key,跨天后要移除视为正常key Set<String> invalids = new HashSet<>(); public Collection<String> removeInvalidKeyAndRecord(Collection<String> keys, String key, Boolean day_limit){ keys.remove(key); invalids.add(key); log.info("removeInvalidKeyAndRecord available keys:{},invalids:{}",JSON.toJSONString(keys),JSON.toJSONString(invalids)); if(CollectionUtils.isEmpty(keys)){            //是否为当日限制 if(day_limit){ log.warn("Can not find a valid key before executing the logic,Check Please!!!"); throw new BusinessException(CommonStateCode.EMPTY_PARAMS.getCode(),"Can not find a valid key before executing the logic,Check Please!!!"); } keys = invalids; invalids.clear(); } return keys; }


  4. 一帆风顺吗?

    写上述代码的过程,也并非一蹴而就!

    比如我调试过程发现高德有的时候返回的状态码只是临时限制,所以加一个递归了逻辑保证服务的高可以用

    key的消耗我原来是一个key没达到配置的限额就不停使用,但这在某些服务定时处理批量数据时会触发单位时间内调用频次限制,导致批量业务失败,所以我修改了顺序挑选key逻辑调整为随机。

    增加了缓存不同部门间的要处理业务不同,但调高德必要按高德的参数定义,所以可能有重复的请求,我希望这部分请求能命中缓存,直接就可以返回减小失败的概率。(点击这里可以查看我另一篇缓存文章)

    响应对象如何定义:各端对高德响应解析的维度不一样,刚开始我陷入如何定义一个大而全的对象,包含所有业务,很快我发现思路不对,无法穷举,最终选择(JsonNode)将原始高德响应原样输出不做任何处理

  5. 最后一击

    在经过本地多次调整提测后,所有问题貌似都解决了。剩下最后一个问题就是各端调整为调用我的服务,步骤也比较简单,各端挑一个良辰吉日将所有的高德前缀替换为本服务暴露出去的URL就OK。

    @RestController@RequestMapping("/restapi")public class RestApiController {    @Autowired private RestApiService restApiService;     /**     * 外部接口重定向 replace('https://restapi.amap.com/v3','http://domain/restap/redirect')     * @param request     * @return     */    @GetMapping("/redirect/**/")    public BaseResponseEntity<JsonNode> redirectRequest2ThirdPart(HttpServletRequest request){        if(request.getQueryString().indexOf(Constants.URL_KEY)>=0){            return ResponseUtil.returnErrorResp(CommonStateCode.ILLEGAL_PARAMETERS,"Request contains sensitive information,Check Please!!!");        }        return restApiService.redirectRequest2ThirdPart(request,null,Boolean.TRUE,null);    }}


  6. 说点别的

    @FeignClient(value = "aliMapClient",url = "${amap.v3_url}")public interface AliMapClient {}

    @FeignClient的URL是不支持热加载的!如果你的服务中有这种使用方式想通过配置来解决热加载,无论是nacos or apollo,发现更新配置后应用会收到事件通知,但再次执行你会发现请求还是原来的地址,即使你加上@RefreshScope还是不行这个坑就当我免费送的,希望你绕过。

    @FeignClient在微服务间调用,为了链路追踪、服务监控或第三方接口要求统一在Header加(Token或认证key)固定参数,如果在业务代码中每个方法都显示的写显得很冗余,这个时候我们可以实现RequestInterceptor接口统一处理:

    @Componentpublic class FeignClientInterceptor implements RequestInterceptor {    private final Logger log = LoggerFactory.getLogger(FeignClientInterceptor.class);    @Override    public void apply(RequestTemplate template) {        //requestId是一个key可以根据业务要求改变        template.header("requestId", UUID.randomUUID().toString());        log.info("feign header:{}", JSON.toJSONString(template.headers()));    }}//如果配置了多个RequestInterceptor的情况,想不同的FeignClient使用不同的配置,可以通过configuration实现//@FeignClient(value = "service-name", url = "https://api.xxx.com",configuration = FeignClientInterceptor.class)

“RequestInterceptor可以配置多个,对于拦截器的应用顺序,我们不做任何保证。


P.S:总体下来从0到1这个服务我大概用了2天完成,上线后一直稳定工作到现在。期间收到很多同学问我如何调用,我让他们把焦点还是放到高德API上,剩下的就是配置固定域名,整个过程沟通顺畅得到一致好评


每一个问题离不开前辈的耕耘,以下是参考链接:

feign热加载失效:

https://zhuanlan.zhihu.com/p/419553941

feign url动态配置:

https://blog.csdn.net/Be_insighted/article/details/127133893

spring cloud open-feign 官网资料:

https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/


有问题欢迎指出,听说看文章与关注更配哦!!!


晚霞程序员
一位需要不断学习的30+程序员……