Spring Cloud 学习(8) --- Feign(三) http client 替换、GET 方式传递 POJO等

在了解了 FeignClient 的配置、请求响应的压缩后,基本的调用已经没有问题。
接下来就需要了解 Feign 多参数传递、文件上传、header 传递 token、请求失败、图片流 等问题的解决,以及 HTTP Client 替换的问题。

Http Client 替换

源码:https://gitee.com/laiyy0728/spring-cloud/tree/master/spring-cloud-feign/spring-cloud-feign-httpclient

Feign 默认情况下使用的是 JDK 原生的 URLConnection 发送 HTTP 请求,没有连接池,但是对每个地址都会保持一个长连接。可以利用 Apache HTTP Client 替换原始的 URLConnection,通过设置连接池、超时时间等,对服务调用进行调优。

在类 feign/Client$Default.java 中,可以看到,默认执行 http 请求的是 URLConnection

public static class Default implements Client {

    @Override
    public Response execute(Request request, Options options) throws IOException {
      HttpURLConnection connection = convertAndSend(request, options);
      return convertResponse(connection).toBuilder().request(request).build();
    }
}

在类 org/springframework/cloud/openfeign/ribbon/FeignRibbonClientAutoConfiguration.java 中,可以看到引入了三个类:HttpClientFeignLoadBalancedConfigurationOkHttpFeignLoadBalancedConfigurationDefaultFeignLoadBalancedConfiguration

可以看到在 DefaultFeignLoadBalancedConfiguration 中,使用的是 Client.Default,即使用 URLConnection

使用 Apache Http Client 替换 URLConnection

pom 依赖

<!-- 引入 httpclient -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
</dependency>

<!-- 引入 feign 对 httpclient 的支持 -->
<dependency>
    <groupId>com.netflix.feign</groupId>
    <artifactId>feign-httpclient</artifactId>
    <version>8.18.0</version>
</dependency>

配置文件

feign:
  httpclient:
    enabled: true

查看验证配置

在类 HttpClientFeignLoadBalancedConfiguration 上,有注解:@ConditionalOnClass(ApacheHttpClient.class)@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true):在 ApacheHttpClient 类存在且 feign.httpclient.enabled 为 true 时启用配置。

HttpClientFeignLoadBalancedConfiguration 123 行打上断点,重新启动项目,可以看到确实进行了 ApacheHttpClient 的声明。在将 feign.httpclient.enabled 设置为 false 后,断点就进不来了。由此可以验证 ApacheHttpClient 替换成功。

使用 OkHttp 替换 URLConnection

pom 依赖

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
    <version>10.1.0</version>
</dependency>

配置文件

feign:
  httpclient:
    enabled: false
  okhttp:
    enabled: true

验证配置

OkHttpFeignLoadBalancedConfiguration 第 84 行打断点,重新启动项目,可以看到成功进入断点;当把 feign.okhttp.enabled 设置为 false 后,重新启动项目,没进入断点。证明 OkHttp 替换成功。


GET 方式传递 POJO等

源码:https://gitee.com/laiyy0728/spring-cloud/tree/master/spring-cloud-feign/spring-cloud-feign-multi-params

SpringMVC 是支持 GET 方法直接绑定 POJI 的,但是 Feign 的实现并未覆盖所有 SpringMVC 的功能,常用的解决方式:

  • 把 POJO 拆散成一个一个单独的属性放在方法参数里
  • 把方法参数变成 Map 传递
  • 使用 GET 传递 @RequestBody,这种方式有违 RESTFul。

实现 Feign 的 RequestInterceptor 中的 apply 方法,统一拦截转换处理 Feign 中 GET 方法传递 POJO 问题。而 Feign 进行 POST 多参数传递要比 Get 简单。

provider

provider 用于模拟用户查询、修改操作,作为服务生产者

pom 依赖:

 <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

配置文件:

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    instance-id: ${spring.application.name}:${server.port}
spring:
  application:
    name: spring-cloud-feign-multi-params-provider
server:
  port: 8888

实体、启动类、Controller


// 实体
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

    private int id;

    private String name;

}

// 启动类
@SpringBootApplication
@EnableDiscoveryClient
public class SpringCloudFeignMultiParamsProviderApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringCloudFeignMultiParamsProviderApplication.class, args);
    }

}


// Controller
@RestController
@RequestMapping(value = "/user")
public class UserController {

    @GetMapping(value = "/add")
    public String addUser(User user){
        return "hello!" + user.getName();
    }

    @PostMapping(value = "/update")
    public String updateUser(@RequestBody User user){
        return "hello! modifying " + user.getName();
    }

}

consumer

consumer 用于模拟服务调用,属于服务消费者,调用 provider 的具体实现

pom 依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

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

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

配置文件:

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    instance-id: ${spring.application.name}:${server.port}
spring:
  application:
    name: spring-cloud-feign-multi-params-consumer
server:
  port: 8889

feign:
  client:
    config:
      spring-cloud-feign-multi-params-provider:
        loggerLevel: full
logging:
  level:
    com.laiyy.gitee.feign.multi.params.springcloudfeignmultiparamscomsumer.MultiParamsProviderFeignClient: debug

实体、启动类、Controller、FeignClient


// 实体与 provider 一致,不再赘述

// 启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class SpringCloudFeignMultiParamsComsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringCloudFeignMultiParamsComsumerApplication.class, args);
    }

}


// Controller
@RestController
public class UserController {

    private final MultiParamsProviderFeignClient feignClient;

    @Autowired
    public UserController(MultiParamsProviderFeignClient feignClient) {
        this.feignClient = feignClient;
    }

    @GetMapping(value = "add-user")
    public String addUser(User user){
        return feignClient.addUser(user);
    }

    @PostMapping(value = "update-user")
    public String updateUser(@RequestBody User user){
        return feignClient.updateUser(user);
    }

}

// FeignClient
@FeignClient(name = "spring-cloud-feign-multi-params-provider")
public interface MultiParamsProviderFeignClient {

    /**
     * GET 方式
     * @param user user
     * @return 添加结果
     */
    @RequestMapping(value = "/user/add", method = RequestMethod.GET)
    String addUser(User user);

    /**
     * POST 方式
     * @param user user
     * @return 修改结果
     */
    @RequestMapping(value = "/user/update", method = RequestMethod.POST)
    String updateUser(@RequestBody User user);
}

验证调用

使用 POST MAN 测试工具,调用 consumer 接口,利用 Feign 进行远程调用

调用 update-user,验证调用成功

POST 方式调用 update

调用 add-user,验证调用失败

GET 方式调用 add

控制台报错:

{"timestamp":"2019-01-24T08:24:42.887+0000","status":405,"error":"Method Not Allowed","message":"Request method 'POST' not supported","path":"/user/add"}] with root cause

feign.FeignException: status 405 reading MultiParamsProviderFeignClient#addUser(User); content:
{"timestamp":"2019-01-24T08:24:42.887+0000","status":405,"error":"Method Not Allowed","message":"Request method 'POST' not supported","path":"/user/add"}
    at feign.FeignException.errorStatus(FeignException.java:62) ~[feign-core-9.5.1.jar:na]
    at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:91) ~[feign-core-9.5.1.jar:na]
    at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:138) ~[feign-core-9.5.1.jar:na]
    ...

命名是 GET 调用,为什么到底层就变成了 POST 调用?

GET 传递 POJO 解决方案

Feign 的远程调用中,GET 是不能传递 POJO 的,否则就是 POST,为了解决这个错误,可以实现 RequestInterceptor,解析 POJO,传递 Map 即可解决

在 consumer 中,增加一个实体类,用于解析 POJO

/**
 * @author laiyy
 * @date 2019/1/24 10:33
 * @description 实现 Feign Request 拦截器,实现 GET 传递 POJO
 */
@Component
public class FeignRequestInterceptor implements RequestInterceptor {
    private final ObjectMapper objectMapper;
    @Autowired
    public FeignRequestInterceptor(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }
    @Override
    public void apply(RequestTemplate template) {
        if ("GET".equals(template.method()) && template.body() != null) {
            try {
                JsonNode jsonNode = objectMapper.readTree(template.body());
                template.body(null);

                Map<String, Collection<String>> queries = new HashMap<>();

                // 构建 Map
                buildQuery(jsonNode, "", queries);

                // queries 就是 POJO 解析为 Map 后的数据
                template.queries(queries);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void buildQuery(JsonNode jsonNode, String path, Map<String, Collection<String>> queries) {
        if (!jsonNode.isContainerNode()) {
            // 如果是叶子节点
            if (jsonNode.isNull()) {
                return;
            }
            Collection<String> values = queries.get(path);
            if (CollectionUtils.isEmpty(values)) {
                values = new ArrayList<>();
                queries.put(path, values);
            }
            values.add(jsonNode.asText());
            return;
        }
        if (jsonNode.isArray()){
            // 如果是数组节点
            Iterator<JsonNode> elements = jsonNode.elements();
            while (elements.hasNext()) {
                buildQuery(elements.next(), path, queries);
            }
        } else {
            Iterator<Map.Entry<String, JsonNode>> fields = jsonNode.fields();
            while (fields.hasNext()) {
                Map.Entry<String, JsonNode> entry = fields.next();
                if (StringUtils.hasText(path)) {
                    buildQuery(entry.getValue(), path + "." + entry.getKey(), queries);
                } else {
                    // 根节点
                    buildQuery(entry.getValue(), entry.getKey(), queries);
                }
            }
        }
    }
}

重新启动 consumer,再次调用 add-user,验证结果:

GET 成功调用远程接口

由此验证,GET 方式传递 POJO 成功。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容