想不到Feign的一个注解还挺多坑的

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 想不到Feign的一个注解还挺多坑的

引言

最近由于业务的需要,需要接入下阿里云的一个接口,打开文档看了看这个接口看下来还是比简单的目测个把小时就可以搞定,但是接入的过程还是比较坎坷的。首先我看了看他给的示例,首先把阿里云文档推荐的demo下载下来,把它的例子跑起来,替换下几个必要的参数比如秘钥啥的。这些秘钥一般公司都会有专职的人员与阿里云去对接,你只要负责管他要就行了。不过也不排除也有得公司需要自己去对接阿里云。说到这里就想吐槽下,对接阿里云的时候技术支持群居然是钉钉,所以需要他们的支持就必须要下载个钉钉,
电脑上莫名的有需要多装一个软件。扯远了我们还是回到正题,把它demo下载下来,然后把对应的秘钥等参数替换下,然后运行下demo看看是否能够正常返回结果,做这一步主要是为了保证产品给过来的秘钥等参数是否正确。如果能够掉通接口,那就说明参数没啥问题的接着我们就可以着手来写业务代码了。接入阿里云二要素认证https://market.aliyun.com/products/57000002/cmapi029454.html?spm=5176.10695662.1194487.1.60066c190NsSkZ#sku=yuncode2345400003
把官网的demo下载下来跑起来看看,官网给出的例子还是比较简单粗暴的,就是封装了一个Apachehttplcient工具类一大坨的代码,个人还是习惯性的使用feign来进行调用,因为feign的代码干净整洁,虽然底层也是通过HttpClient来实现,但是实现对我来说是无感的,毕竟业务代码看起来干净整洁。它的demo如下:

public static void main(String[] args) {
        String host = "https://safrvcert.market.alicloudapi.com";
        String path = "/safrv_2meta_id_name/";
        String method = "GET";
        String appcode = "你自己的AppCode";
        Map<String, String> headers = new HashMap<String, String>();
        //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
        headers.put("Authorization", "APPCODE " + appcode);
        Map<String, String> querys = new HashMap<String, String>();
        querys.put("__userId", "__userId");
        querys.put("customerID", "customerID");
        querys.put("identifyNum", "identifyNum");
            querys.put("identifyNumMd5", "identifyNumMd5");
        querys.put("userName", "userName");
        querys.put("verifyKey", "verifyKey");


        try {
            /**
            * 重要提示如下:
            * HttpUtils请从
            * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
            * 下载
            *
            * 相应的依赖请参照
            * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
            */
            HttpResponse response = HttpUtils.doGet(host, path, method, headers, querys);
            //错误信息见X-Ca-Error-Message字段
                System.out.println(response.toString());
            //获取response的body
            System.out.println(EntityUtils.toString(response.getEntity()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
HttpResponse response = HttpUtils.doGet(host, path, method, headers, querys);

根据它提供的代码我们可以看出来他是用一个httpUtils 类来实现http请求的,我们可以把这个httpClient类 替换成我们的FeignClient
替换后的代码如下:

@FeignClient(name = "verifyIdCardAndNameFeignClient", url = "https://safrvcert.market.alicloudapi.com")
public interface VerifyIdCardAndNameFeignClient {
    @RequestMapping(value = "/safrv_2meta_id_name/", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    Response verifyIdCardAndNameMap(@RequestParam Map<String,String> app, @RequestHeader("Authorization") String authorization);

相对比较下来下面这个HttpClientUtils代码是不是比较简洁
在这里插入图片描述
按照这个demo功能确实是实现了,说实话个人还是不是很喜欢用map来作为参数,map作为入参的话,参数全靠猜可读性以及可维护性有点差,个人还是习惯性的封装一个javaBean作为实体。阿里文档其实也有提到一嘴,虽然他只说到数据查询这一层。
在这里插入图片描述
下面我们就修改下请求参数把它改成一个javaBean,改变后的代码

@RequestMapping(value = "/safrv_2meta_id_name/", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
Response verifyIdCardAndNameDTO(@RequestBody AliyunVerifyIdCardAndNameReq app, @RequestHeader("Authorization") String authorization);

在这里插入图片描述
请求并没有成功,根据报错返回的信息看下来应该是没有接受到参数。我们是GET请求的方式然后参数传递的是实体导致没有接收到。feignClient不支持get方式传递实体类吗?后来经过查询资料发现了一个注解@SpringQueryMap 我们把上述代码@RequestBody替换成@SpringQueryMap完美解决这个问题

@SpringQueryMap

spring cloud 2.1.x 以上的版本,提供了一个新的注解@SpringQueryMap,为何这个注解可以帮我们实现。源码之下无秘密,我们可以翻翻
feign的源码相对来说应该是比较简单的,我们可以简单的来看下源码。看源码是不是也不知道从哪里看起,从头看到尾肯定也不现实,
不从头开始看,又不知道源码在哪里,有个很简单的方法我们直接拿着这个注解全局搜一下,看看有哪些地方使用到了,在每个地方都打上一个断点试试
在这里插入图片描述
我们全局搜下发现使用的地方主要在QueryMapParameterProcessor这个类里面。所以我们可以在这个类里面打上一个断点试试。


/**
 * {@link SpringQueryMap} parameter processor.
 *
 * @author Aram Peres
 * @see AnnotatedParameterProcessor
 */
public class QueryMapParameterProcessor implements AnnotatedParameterProcessor {

    private static final Class<SpringQueryMap> ANNOTATION = SpringQueryMap.class;

    @Override
    public Class<? extends Annotation> getAnnotationType() {
        return ANNOTATION;
    }

    @Override
    public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
        int paramIndex = context.getParameterIndex();
        MethodMetadata metadata = context.getMethodMetadata();
        if (metadata.queryMapIndex() == null) {
            metadata.queryMapIndex(paramIndex);
            metadata.queryMapEncoded(SpringQueryMap.class.cast(annotation).encoded());
        }
        return true;
    }
}

我们发现打这个类的话在容器启动的时候会进行加载,并且会执行processArgument方法,这个我们先不管这个方法,接下来我们来看看
Feign真正发起调用的地方找到SynchronousMethodHandler#invoke方法

public RequestTemplate create(Object[] argv) {
    ... 省略部分代码
    // metadata.queryMapIndex() 就是QueryMapParameterProcessor #processArgument方法赋值的
      if (metadata.queryMapIndex() != null) {
        // add query map parameters after initial resolve so that they take
        // precedence over any predefined values
        // 通过下标获取到需要特殊处理的对象,这里有个问题只会处理方法参数的第一个@SpringQueryMap注解,
        // 原因就是QueryMapParameterProcessor #processArgument这个方法只会把第一个下标赋值进去,然后这里也只会取第一个下标,所以只会处理第一个@SpringQueryMap注解
        Object value = argv[metadata.queryMapIndex()];
        //将对象转换为map  这里需要注意下默认使用解析参数的是FieldQueryMapEncoder类所以它并不会去解析父类的参数,如果需要解析父类的参数我们需要在feign的Config里面指定QueryMapEncoder为FieldQueryMapEncoder
        Map<String, Object> queryMap = toQueryMap(value);
        //拼接解析完成的对象为URL参数
        template = addQueryMapQueryParameters(queryMap, template);
      }
... 省略部分代码
}

上述代码逻辑还是挺好理解的

  • 首先去判断是否需要处理下querymap
  • 通过下标获取到需要特殊处理的对象
  • 将对象转换为map(这里有个坑默认不会去解析父类的字段)
  • 拼接追加mapurl

总结

  • 上面通过@SpringQueryMap注解实现了get传参,但是如果需要传递多个@SpringQueryMap注解我们可以怎么来实现呢?
  • 或者我们可以自己动手来实现一个我们自己的SpringQueryMap,我们该如何实现?
  • @SpringQueryMap注解默认是不会去解析父类的参数,如果需要解析父类的参数需要修改Feignconfig# QueryMapEncoderFieldQueryMapEncoder
  • 如果我们自己去实现了一个AnnotatedParameterProcessor所有默认的PathVariableParameterProcessor

RequestParamParameterProcessor、RequestHeaderParameterProcessor、QueryMapParameterProcessor都会失效,为啥会失效我们去看看SpringMvcContract这个类。所以自定义AnnotatedParameterProcessor需要慎重。

结束

  • 由于自己才疏学浅,难免会有纰漏,假如你发现了错误的地方,还望留言给我指出来,我会对其加以修正。
  • 如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。
  • 感谢您的阅读,十分欢迎并感谢您的关注。
目录
相关文章
|
7月前
|
存储 Java 程序员
浅析Spring的五大类注解和方法注解
浅析Spring的五大类注解和方法注解
|
5月前
|
XML Java 关系型数据库
面试一口气说出Spring的声明式事务@Transactional注解的6种失效场景
面试一口气说出Spring的声明式事务@Transactional注解的6种失效场景
132 0
|
JSON 前端开发 Java
烧脑巨作,Spring Boot请求处理(常用参数注解使用)
请求处理-SpringBoot常用参数注解使用 注解: @PathVariable 路径变量 @RequestHeader 获取请求头 @RequestParam 获取请求参数(指问号后的参数,url?a=1&b=2) @CookieValue 获取Cookie值 @RequestAttribute 获取request域属性 @RequestBody 获取请求体[POST] @MatrixVariable 矩阵变量 @ModelAttribute 1、@PathVariable注解 该注解主要是用于rest风格的搭配使用,在请求路径中不再以k : v的形式给出请求参数与值;而是直接给定一个值
276 1
|
开发框架 Java Linux
这类注解都不知道,还好意思说用过Spring Boot?
这类注解都不知道,还好意思说用过Spring Boot?
这类注解都不知道,还好意思说用过Spring Boot?
|
JSON 编解码 安全
Feign实战技巧篇
Feign实战技巧篇
139 0
|
消息中间件 JavaScript 小程序
Spring 6 正式“抛弃”feign
Spring 6 正式“抛弃”feign
|
XML 安全 Java
关于spring中bean配置的几件小事
关于spring中bean配置的几件小事
关于spring中bean配置的几件小事
|
存储 Java
Feign接口踩坑
由于业务需要,需要在接口中传递参数,调用消息中心的短信接口,进行短信的发送。如果使用Feign接口,没有携带token时,调用Feign接口,可以正常调用,但是如果携带token,就会出现appId拼接参数的情况。appId出现拼接时什么原因导致的呢?
274 0
Feign接口踩坑
|
Java 容器 Spring
SpringBoot自动配置底层原理(手撕@SpringBootApplication注解源码)
SpringBoot自动配置底层原理(手撕@SpringBootApplication注解源码)
168 0
SpringBoot自动配置底层原理(手撕@SpringBootApplication注解源码)
|
Java 应用服务中间件 nginx
纳尼?feign报400?
feign通过解析接口类、方法、参数上的注解,通过`RequestTemplate` @RequestParam以Url拼接的方式,构建了`Request`请求,通过Client访问http服务。对较长参数改为RequestBody方式调用服务
1307 0