Java框架 SpringCloud 快速入门: 实现 Feign 最佳实践(抽取方式)

概述

Feign 客户端如果每个消费者各写一份,接口一改就得挨个服务改,pojo 也会慢慢和提供方对不上。这篇把 UserClient、User、默认配置整体抽到独立的 feign-api 模块,order-service 从"自己写"改成"引依赖",跟着做一遍就能跑通。

纲要

  • 问题:Feign Client 与 pojo 散落在消费者工程里,重复、易漂移
  • 方案选择:一句话交代为什么选"抽取方式"而不是"继承方式"
  • 目标依赖方向 :order-service → feign-api,user-service 不反向依赖
  • 动手改造六步
    • 新建 feign-api 模块并配 pom
    • 把 Client / pojo / 默认配置搬进新模块(含包名规划)
    • order-service 引入 feign-api 依赖,并删除本地旧类
    • 启动类指定 Feign 扫描范围(basePackages 与 clients 两种写法)
    • 业务代码改用 cn.itcast.feign.* 的接口
    • 重启验证,访问订单接口看返回里有没有用户信息
  • 工程结构树 :feign-api 的真实目录
  • 五个坑 :扫描不到 Client、别打成可执行 jar、@Configuration 重复加载、两份同名 bean、pojo 字段不一致
  • 收益对比:改造前 vs 改造后
  • API 速览 :@FeignClient 与 @EnableFeignClients 常用属性

选抽取,不选继承

讲义 2.4 最佳实践 给了两条路。继承方式是定义一个 API 接口,让 Feign Client 和 Controller 都去实现它------代码共享了,但服务提供方被拉进来一起耦合,而且 SpringMVC 的参数注解不在继承范围内,Controller 里还得把方法、参数列表、注解再抄一遍。

抽取方式反过来:Client、pojo、默认配置都挪到一个独立模块,谁要用谁引依赖。提供方完全不知道有这个模块存在。

本篇只做抽取方式的动手实现,继承方式的优缺点在同系列另一篇已经展开过,这里不再重复。

改造后的模块依赖方向

#mermaid-svg-6vobxgfxBMH9iRI2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6vobxgfxBMH9iRI2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6vobxgfxBMH9iRI2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6vobxgfxBMH9iRI2 .error-icon{fill:#552222;}#mermaid-svg-6vobxgfxBMH9iRI2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6vobxgfxBMH9iRI2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6vobxgfxBMH9iRI2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6vobxgfxBMH9iRI2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6vobxgfxBMH9iRI2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6vobxgfxBMH9iRI2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6vobxgfxBMH9iRI2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6vobxgfxBMH9iRI2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6vobxgfxBMH9iRI2 .marker.cross{stroke:#333333;}#mermaid-svg-6vobxgfxBMH9iRI2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6vobxgfxBMH9iRI2 p{margin:0;}#mermaid-svg-6vobxgfxBMH9iRI2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6vobxgfxBMH9iRI2 .cluster-label text{fill:#333;}#mermaid-svg-6vobxgfxBMH9iRI2 .cluster-label span{color:#333;}#mermaid-svg-6vobxgfxBMH9iRI2 .cluster-label span p{background-color:transparent;}#mermaid-svg-6vobxgfxBMH9iRI2 .label text,#mermaid-svg-6vobxgfxBMH9iRI2 span{fill:#333;color:#333;}#mermaid-svg-6vobxgfxBMH9iRI2 .node rect,#mermaid-svg-6vobxgfxBMH9iRI2 .node circle,#mermaid-svg-6vobxgfxBMH9iRI2 .node ellipse,#mermaid-svg-6vobxgfxBMH9iRI2 .node polygon,#mermaid-svg-6vobxgfxBMH9iRI2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6vobxgfxBMH9iRI2 .rough-node .label text,#mermaid-svg-6vobxgfxBMH9iRI2 .node .label text,#mermaid-svg-6vobxgfxBMH9iRI2 .image-shape .label,#mermaid-svg-6vobxgfxBMH9iRI2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-6vobxgfxBMH9iRI2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6vobxgfxBMH9iRI2 .rough-node .label,#mermaid-svg-6vobxgfxBMH9iRI2 .node .label,#mermaid-svg-6vobxgfxBMH9iRI2 .image-shape .label,#mermaid-svg-6vobxgfxBMH9iRI2 .icon-shape .label{text-align:center;}#mermaid-svg-6vobxgfxBMH9iRI2 .node.clickable{cursor:pointer;}#mermaid-svg-6vobxgfxBMH9iRI2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6vobxgfxBMH9iRI2 .arrowheadPath{fill:#333333;}#mermaid-svg-6vobxgfxBMH9iRI2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6vobxgfxBMH9iRI2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6vobxgfxBMH9iRI2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6vobxgfxBMH9iRI2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6vobxgfxBMH9iRI2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6vobxgfxBMH9iRI2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6vobxgfxBMH9iRI2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6vobxgfxBMH9iRI2 .cluster text{fill:#333;}#mermaid-svg-6vobxgfxBMH9iRI2 .cluster span{color:#333;}#mermaid-svg-6vobxgfxBMH9iRI2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6vobxgfxBMH9iRI2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6vobxgfxBMH9iRI2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-6vobxgfxBMH9iRI2 .icon-shape,#mermaid-svg-6vobxgfxBMH9iRI2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6vobxgfxBMH9iRI2 .icon-shape p,#mermaid-svg-6vobxgfxBMH9iRI2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6vobxgfxBMH9iRI2 .icon-shape .label rect,#mermaid-svg-6vobxgfxBMH9iRI2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6vobxgfxBMH9iRI2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6vobxgfxBMH9iRI2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6vobxgfxBMH9iRI2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 提供方
共享契约
消费方
依赖:接口 + pojo
编译期依赖 openfeign
HTTP GET /user/1
order-service

cn.itcast.order
feign-api

cn.itcast.feign
user-service

cn.itcast.user
spring-cloud-starter-openfeign

注意 user-service 那一侧没有任何指向 feign-api 的连线。

这张图里最要紧的是箭头方向。feign-api 只管"怎么调",user-service 只管"怎么实现",两边互不知情,中间靠 HTTP 路径和字段名对齐。以后再有别的服务要调用户接口,加一条 X → feign-api 就行,不用重新写 Client,也不会碰上 user-service。

对比继承方式就更清楚了:继承方式必然产生 提供方 → API 接口 这条线,提供方换实现、换 Controller 都得跟着动;抽取方式下 user-service 一点都不知道自己被抽取了。

改造前长什么样

改造前,order-service 里自己养着一套 Client 和 pojo,包名跟着 order 走。大致是这样(包名以你工程的实际情况为准):

java 复制代码
package cn.itcast.order.clients;

import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "userservice")
public interface UserClient {

    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

User 也一样躺在 order-service 的 pojo 包里,DefaultFeignConfiguration 躺在配置包里。这三个东西的共同点是:它们描述的是用户服务的契约,跟订单业务没有半点关系,却占了订单工程的代码。

要搬的就是这三样:UserClient、User、DefaultFeignConfiguration。

步骤一:新建 feign-api 模块

在 cloud-demo 工程里新建一个 Maven module,名字 feign-api,然后先在父 pom 的 <modules> 里登记(不登记父工程就不会编译它):

xml 复制代码
<modules>
    <module>user-service</module>
    <module>order-service</module>
    <module>eureka-server</module>
    <module>feign-api</module>
    <module>gateway</module>
</modules>

feign-api 自己的 pom:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud-demo</artifactId>
        <groupId>cn.itcast.demo</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>feign-api</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

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

依赖只写一个 spring-cloud-starter-openfeign,因为 @FeignClient、@GetMapping 这些注解全在它里头。版本号不用写------父 pom 的 <dependencyManagement> 里 import 了 spring-cloud-dependencies,版本统一托管。

@Data 需要 lombok,也不用在这里声明:父 pom 的 <dependencies>(不是 dependencyManagement)里已经放了 lombok,所有子模块直接继承。Jackson 是 openfeign → spring-web → jackson-databind 传递进来的,反序列化 pojo 时本来就要用到,同样不用手写。

注意别在 feign-api 里加 spring-boot-maven-plugin。这个模块是一个普通 jar 库,不是可执行程序,理由在后面的坑里细说。

步骤二:把 Client、pojo、默认配置搬进 feign-api

先建包。包名别跟着 order 走,这个模块不属于任何单一业务,所以用 cn.itcast.feign 打头,再按职责分层:

  • cn.itcast.feign.clients ------ 放 @FeignClient 接口
  • cn.itcast.feign.pojo ------ 放契约实体
  • cn.itcast.feign.config ------ 放 Feign 默认配置

UserClient:

java 复制代码
package cn.itcast.feign.clients;

import cn.itcast.feign.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "userservice")
public interface UserClient {

    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

User:

java 复制代码
package cn.itcast.feign.pojo;

import lombok.Data;

@Data
public class User {
    private Long id;
    private String username;
    private String address;
}

DefaultFeignConfiguration:

java 复制代码
package cn.itcast.feign.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;

public class DefaultFeignConfiguration {
    @Bean
    public Logger.Level logLevel(){
        return Logger.Level.BASIC;
    }
}

这里有个细节,DefaultFeignConfiguration 故意不加 @Configuration。加了它就会被消费者启动类的组件扫描命中,变成全局默认配置------这个坑在后面单独讲。

契约怎么对齐的,看 user-service 那一侧就明白了:

java 复制代码
package cn.itcast.user.web;

// 省略 import

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

    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id,
                          @RequestHeader(value = "Truth", required = false) String truth) {
        System.out.println("truth: " + truth);
        return userService.queryById(id);
    }
}

@RequestMapping("/user") + @GetMapping("/{id}") 拼出 /user/{id},@FeignClient(value = "userservice") 里的 userservice 是提供方 spring.application.name,路径和方法签名就能对上。User 的字段 id / username / address 两边也一致,JSON 才反序列化得回来。

步骤三:order-service 引入 feign-api 依赖

先删东西:把 order-service 本地的 UserClient、User、DefaultFeignConfiguration 全部删掉。删完代码会一片红,正常,马上就好。

然后在 order-service/pom.xml 里加依赖:

xml 复制代码
<!--引入feign的统一api-->
<dependency>
    <groupId>cn.itcast.demo</groupId>
    <artifactId>feign-api</artifactId>
    <version>1.0</version>
</dependency>

groupId 和 version 跟父工程 cloud-demo 保持一致,artifactId 就是刚建的那个模块名。

删本地类这一步别省。留着的话,同一个 UserClient 接口在 cn.itcast.order 和 cn.itcast.feign.clients 下各有一份,两个 @FeignClient 注册出来的 bean 名都叫 userClient,Spring 启动时要么报 bean 名冲突,要么让你在 @Autowired 注入时挑一个------挑错了就是改了 A 处代码、跑的是 B 处行为。

步骤四:启动类指定 Feign 扫描范围

@EnableFeignClients 默认只扫它所在包及其子包 。order-service 的启动类在 cn.itcast.order 下,而 UserClient 现在在 cn.itcast.feign.clients 下,两条包路径没有父子关系,扫不到。

讲义给了两种写法。

方式一,指定扫描包:

java 复制代码
package cn.itcast.order;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients(basePackages = "cn.itcast.feign.clients")
public class OrderApplication {

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

    /**
     * 创建RestTemplate并注入Spring容器
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

方式二,指定具体的 Client 接口:

java 复制代码
package cn.itcast.order;

import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.config.DefaultFeignConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients(clients = UserClient.class, defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderApplication {

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

    /**
     * 创建RestTemplate并注入Spring容器
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

两个属性的差别:

属性 类型 范围 适用
basePackages String[] 该包及子包 下的所有 @FeignClient 接口 包里 Client 多、且都确定要用
clients Class<?>[] 只注册列出的这几个接口 精确控制,用哪个写哪个

工程里最终用的是第二种。clients 更精确:假设 cn.itcast.feign.clients 下以后堆了十个 Client(UserClient、OrderClient、PayClient......),basePackages 会把十个全注册成 bean,用不上的那些白白占用容器、还可能触发无关的配置;clients 只加你点名的,多一个不扫。

clients 的另一个好处是编译期就能发现拼错 ------写类名有 IDE 补全和编译检查,basePackages 写个字符串包名,拼错了要等启动时才炸。

顺带提醒:如果后面还要给别处指定 Feign 默认配置,用 defaultConfiguration 属性(如上面代码所示),别去给 DefaultFeignConfiguration 加 @Configuration。

步骤五:业务代码改成从 feign-api 导包

OrderService 里注入的是同一个 UserClient,只是 import 换了地方。

改造前:

java 复制代码
package cn.itcast.order.service;

import cn.itcast.order.clients.UserClient;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private UserClient userClient;

    public Order queryOrderById(Long orderId) {
        Order order = orderMapper.findById(orderId);
        User user = userClient.findById(order.getUserId());
        order.setUser(user);
        return order;
    }
}

改造后(只动了三行 import):

java 复制代码
package cn.itcast.order.service;

import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private UserClient userClient;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 2.用Feign远程调用
        User user = userClient.findById(order.getUserId());
        // 3.封装user到Order
        order.setUser(user);
        // 4.返回
        return order;
    }
}

业务逻辑一行没改。这也是抽取方式的性价比所在:收益在"下一批消费者"身上------它们连这三行 import 都不用写。

IDEA 里删完本地类之后,把光标放在红色的 UserClient 上按 Alt+Enter → Import class,选 cn.itcast.feign.clients.UserClient 就行;User 同理。启动类上如果之前 import 了本地的 DefaultFeignConfiguration,也要换成 cn.itcast.feign.config.DefaultFeignConfiguration。

改完自查一下 order-service 里还有没有 Feign 相关的代码。正常情况下只剩启动类那一行 @EnableFeignClients------那是开关,必须留着。

步骤六:重启验证

先启动 Nacos、user-service,再启动 order-service。浏览器访问订单接口:

bash 复制代码
curl http://localhost:8088/order/101

返回里带上 user 字段,就算通了:

text 复制代码
{"id":101,"price":699900,"userId":1,"name":"Apple 手机",
 "user":{"id":1,"username":"柳岩","address":"湖南省衡阳市"}}

user 为 null、或者整个请求 500,说明 Client 压根没注册上,回头看步骤四的扫描范围。

工程结构树

改造完成后 feign-api 的真实结构:

tree 复制代码
feign-api
├── pom.xml
└── src
    └── main
        ├── java
        │   └── cn
        │       └── itcast
        │           └── feign
        │               ├── clients
        │               │   └── UserClient.java
        │               ├── config
        │               │   └── DefaultFeignConfiguration.java
        │               └── pojo
        │                   └── User.java
        └── resources

注意它没有 src/main/resources/application.yml,也没有启动类。它是个库,不启动、不占端口、没有自己的配置,被打成 jar 塞进消费者的 BOOT-INF/lib 里。

五个坑

坑一:启动报找不到 Client 的 bean

不指定扫描范围就重启,最典型的报错长这样:

text 复制代码
***************************
APPLICATION FAILED TO START
***************************

Description:

Field userClient in cn.itcast.order.service.OrderService required a bean of type
'cn.itcast.feign.clients.UserClient' that could not be found.

The injection point has the following annotations:
	- @org.springframework.beans.factory.annotation.Autowired(required=true)

Action:

Consider defining a bean of type 'cn.itcast.feign.clients.UserClient' in your configuration.

关键在"编译不报错,注入失败"。类明明存在(依赖也引进来了),但容器里没有它的实例。归因就一句话:@EnableFeignClients 只扫自己所在的包及子包,cn.itcast.order 扫描不到 cn.itcast.feign.clients,.class 被扫到才会生成代理 bean,扫不到就没有。

反过来,别为了这件事把 @SpringBootApplication 的 scanBasePackages 改大。启动类放在哪就扫哪是惯例,扩大业务组件的扫描范围会顺带把一堆无关的 bean 拉进来,副作用比问题本身大。要解决的只是 Feign 客户端的注册范围,用 @EnableFeignClients(basePackages = ...) 或 clients = ... 就够了。

坑二:feign-api 的 pom 里别加 spring-boot-maven-plugin

给 feign-api 的 pom 加上这个插件,mvn package 出来的就不是普通 jar 了:

xml 复制代码
<!-- 不要这么做!feign-api 是库,不是可执行程序 -->
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

repackage 之后,jar 里的结构从 cn/itcast/feign/... 变成 BOOT-INF/classes/cn/itcast/feign/...,还多出一堆 org/springframework/boot/loader/ 的启动器类。消费者把它当依赖引进来时,JVM 按常规 jar 的方式找类,在 BOOT-INF/classes 下面找不到 cn.itcast.feign.clients.UserClient,直接 NoClassDefFoundError 或 ClassNotFoundException。

这个坑在 MyBatis 的实体模块、公共工具模块上都踩过无数次,判断标准很简单:这个模块自己有没有 main 方法和启动类。有→可以加插件;没有→一定不加。

坑三:feign-api 里的 @Configuration 会被重复加载

如果给 DefaultFeignConfiguration 加上 @Configuration 注解放进 feign-api,消费者启动时组件扫描会命中外面的 cn.itcast.feign.config 包(引进依赖后这类扫描场景很常见),这个配置就被当成消费者自己的配置注册进容器。里面那个 Logger.Level.BASIC 随即生效,对消费者所有的 Feign 客户端生效,而不只是你原来想指定的那一个。

所以工程里的 DefaultFeignConfiguration 是裸类,只能通过显式引用生效:

java 复制代码
@EnableFeignClients(clients = UserClient.class,
        defaultConfiguration = DefaultFeignConfiguration.class)

或者挂在单个客户端上:

java 复制代码
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration.class)

一个影响全局、一个只影响当前 Client,选哪个看需求。想深挖的话跟自定义 Feign 配置那篇的作用域问题是同一回事。

坑四:本地和 feign-api 各留一份同名接口

删本地类没删干净,或者从 Git 拉了个旧分支带回来一份,就会出现两个 @FeignClient(value = "userservice") 接口。默认 bean 名按类名首字母小写生成,两边都是 userClient,注册时冲突。即使靠 @Primary 之类的手段绕过去,也容易出现"改了 feign-api 里的路径、请求还是打到老接口上"这种更恶心的问题。

排查办法:往 OrderService 的 import 上看一眼,是 cn.itcast.feign.clients.UserClient 还是 cn.itcast.order.clients.UserClient;再在 IDEA 里 Ctrl+Alt+Shift+N 搜 UserClient.class,只应该命中一处。

坑五:pojo 字段不一致导致反序列化丢字段

User 在 feign-api 和 user-service 里各有一份拷贝(这是抽取方式的固有代价,契约靠两边同步)。如果 feign-api 里的 User 少写了字段,比如漏了 address:

java 复制代码
@Data
public class User {
    private Long id;
    private String username;
    // 少了一个 address
}

user-service 返回的 JSON 里 address 有值,Jackson 反序列化时找不到对应属性,默认策略是静默忽略 。结果不报错,但 user.getAddress() 一直是 null,排查起来相当费劲。

反过来的方向更糟,如果消费方多写了提供方没有的字段(FAIL_ON_UNKNOWN_PROPERTIES 开着的话),直接抛 UnrecognizedPropertyException。

值得记一句的是:user-service 那边给 User 加字段时,feign-api 要同步加。抽取方式用"两份 pojo"换来了模块解耦,代价就是这种人工同步------字段多的时候,或者团队里服务数量多的时候,就得考虑用 springdoc 生成的契约来兜底了。

改造收益对比

维度 改造前 改造后
重复代码量 每个消费者各写一份 Client + pojo 全工程只此一份,消费者零代码
契约一致性 靠各服务自觉,容易漂移 单点维护,改动集中在一处
改接口的影响面 改 N 个服务,逐个发版 改 feign-api 一处,消费者升个依赖版本
依赖方向 提供方与消费方无共享契约,各写各的 提供方完全不知道 feign-api;只有消费方依赖它
新增服务的成本 新服务要用用户接口,重写一遍 引依赖 + 一行 @EnableFeignClients(clients = ...)
模块/构建成本 无 多一个 Maven 模块,多一次发版
排查成本 Client 分散,路径对不对要在多个工程里找 统一在 feign-api 一处,改哪找哪

API 速览

注解 / 属性 作用 要点
@FeignClient(value = "userservice") 声明一个 Feign 客户端,value/name 是服务名 服务名必须等于提供方的 spring.application.name
@FeignClient(path = "/user") 给该客户端所有方法加统一路径前缀 与接口里的 @GetMapping 拼接
@FeignClient(configuration = XxxConfig.class) 只对当前客户端生效的配置类 该配置类不要加 @Configuration,避免全局生效
@EnableFeignClients(basePackages = "cn.itcast.feign.clients") 指定要扫描的包(含子包) 批量注册,字符串包名编译期不校验
@EnableFeignClients(clients = UserClient.class) 指定要注册的接口 推荐,精确、IDE 可补全、编译期可校验
@EnableFeignClients(defaultConfiguration = XxxConfig.class) 所有客户端共享的默认配置 与 configuration 的区别是作用域

官方文档

总结

  • 抽取方式把"怎么调"和"怎么实现"彻底分开:feign-api 只依赖 openfeign,user-service 完全不知道它的存在,依赖是单向的。
  • 动手顺序别乱:新建模块 → 搬 Client/pojo/配置 → 消费者引依赖 → 删本地旧类 → 指定扫描范围 → 改 import → 重启验证。
  • @EnableFeignClients(clients = UserClient.class) 比 basePackages 好用,包数量和拼写风险都小。
  • 最容易卡住的两个点:扫描包忘了指定(编译过了但注入失败),以及给库模块加了 spring-boot-maven-plugin(依赖进来找不到类)。
  • 抽取的代价是 pojo 有两份、需要人工同步,字段一变就要记得两边都改。
相关推荐
MandalaO_O2 小时前
IDEA 开发(快捷键 + 调试 + 序列化)
java·ide·intellij-idea
xiaoqiMikko2 小时前
JVM 线上排查实战(七):jps 看不见它、jstack 连不上它,可它明明活得好好的
java·jvm
狼爷3 小时前
Rust/Go/Java/Python/PHP 大比拼:负载下后端框架到底差多少?
java·后端·编程语言
念何架构之路3 小时前
zap扩展生态与总结
java·前端·数据库
第七页独白4 小时前
汽车零件厂如何通过 QMS 真正落地 IATF 16949——QMS软件系统:品质检验-内审稽核-8d客诉管理:全星质量管理软件系统
java·前端·数据库
QCoding4 小时前
Spring AI Alibaba Graph实战:从ReAct Agent到Workflow,企业AI复杂流程该如何编排?
java·人工智能
用户094248568034 小时前
第25章:Java虚拟线程(Project Loom)实战与调度协作
java·jvm
砚底藏山河4 小时前
量化实战:截面因子有效性检验(IC 分析与分层回测)
java·python·金融·maven
wuminyu4 小时前
ForkJoinPool内部WorkQueue的Lock-Free数组操作以及并发任务窃取原理剖析
java·linux·c语言·jvm·c++