服务之间咋说话?OpenFeign远程调用

16 阅读8分钟

一、先白话白话

昨天咱把user-serviceorder-service都登记到Nacos了,就像俩人都在社区服务中心登了记。但是光登记不中啊,得能互相找人互相帮忙

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

实际场景:

order-service(订单服务)想知道是哪个用户下单了,得问user-service(用户服务):

  • 以前:order-service得知道user-service住哪(IP:端口),自己写HTTP请求
  • 现在:order-service直接喊一声“user-service!”,Nacos负责找地址,OpenFeign负责传话

OpenFeign就是个翻译官+跑腿的

  1. 你写个接口,说想调user-service的某个方法
  2. OpenFeign帮你生成具体实现
  3. 帮你找到user-service在哪
  4. 帮你发请求、收响应
  5. 出问题了还帮你重试

二、为啥不用原生的RestTemplate?

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

RestTemplate也能调用,但是:

// RestTemplate方式(老式写法)
String url = "http://localhost:8081/user/" + userId;
User user = restTemplate.getForObject(url, User.class);

问题

  1. 得自己拼URL,麻烦!
  2. 得自己处理异常,麻烦!
  3. 负载均衡得自己写,麻烦!
  4. 改个地址得到处找,麻烦!

OpenFeign写法:

// Feign方式(新式写法)
User user = userClient.getUserById(userId);

优点

  1. 跟调本地方法一样简单
  2. 自动负载均衡
  3. 自动重试
  4. 配置统一管理

三、开整!让order-service调user-service

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

准备工作

1. 先完善user-service

user-service里加个正经接口:

// User.java(先简单点,明天连数据库)
public class User {
    private Integer id;
    private String name;
    private String address;
    private String favoriteFood;
    
    // 构造方法、getter、setter省略...
}

// UserController.java
@RestController
@RequestMapping("/user")
public class UserController {
    
    // 模拟数据库
    private Map<Integer, User> userMap = new HashMap<>();
    
    public UserController() {
        // 初始化几个用户
        userMap.put(1, new User(1, "张三", "郑州", "烩面"));
        userMap.put(2, new User(2, "李四", "洛阳", "水席"));
        userMap.put(3, new User(3, "王五", "开封", "灌汤包"));
    }
    
    @GetMapping("/{id}")
    public User getUserById(@PathVariable Integer id) {
        User user = userMap.get(id);
        if (user == null) {
            throw new RuntimeException("用户不存在!");
        }
        return user;
    }
    
    @GetMapping("/test")
    public String test() {
        return "user-service正常工作中...";
    }
}

启动user-service,访问http://localhost:8081/user/1看看:

{
    "id": 1,
    "name": "张三",
    "address": "郑州",
    "favoriteFood": "烩面"
}

中了!

步骤1:给order-service加OpenFeign依赖

order-servicepom.xml里加:

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

<!-- 负载均衡(必须加!) -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

步骤2:启动类加注解

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients  // 这个注解是重点!开启Feign
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

步骤3:创建Feign客户端接口

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

新建feign包,里面创建UserClient.java

@FeignClient(name = "user-service")  // 指定要调用的服务名
public interface UserClient {
    
    /**
     * 调用user-service的GET /user/{id}接口
     * 跟写本地Controller一样,只是注解变@GetMapping
     */
    @GetMapping("/user/{id}")
    User getUserById(@PathVariable("id") Integer id);
    
    /**
     * 调用user-service的GET /user/test接口
     */
    @GetMapping("/user/test")
    String test();
}

注意

  1. @FeignClient(name = "user-service")里的name必须跟Nacos里注册的服务名一致
  2. 方法上的注解(@GetMapping)要跟被调用方一致
  3. 方法签名(参数、返回值)也要一致

步骤4:写个Controller测试

order-service里:

@RestController
@RequestMapping("/order")
public class OrderController {
    
    @Autowired
    private UserClient userClient;  // 注入Feign客户端
    
    /**
     * 根据订单ID获取订单信息,顺便获取用户信息
     */
    @GetMapping("/{orderId}")
    public Map<String, Object> getOrderWithUser(@PathVariable Integer orderId) {
        Map<String, Object> result = new HashMap<>();
        
        // 1. 模拟订单信息
        result.put("orderId", orderId);
        result.put("orderName", "河南特产大礼包");
        result.put("status", "已发货");
        result.put("address", "河南省郑州市金水区");
        
        // 2. 调用user-service获取用户信息(关键!)
        // 假设订单1对应用户1,订单2对应用户2...
        Integer userId = orderId;
        try {
            User user = userClient.getUserById(userId);
            result.put("user", user);
        } catch (Exception e) {
            result.put("user", "获取用户信息失败:" + e.getMessage());
        }
        
        return result;
    }
    
    /**
     * 测试user-service是否正常
     */
    @GetMapping("/test/user")
    public String testUserService() {
        return userClient.test();
    }
}

步骤5:测试一下

  1. 启动所有服务:

    • Nacos(8848端口)
    • user-service(8081端口)
    • order-service(8082端口)
  2. 访问http://localhost:8082/order/test/user 应该返回:user-service正常工作中...

  3. 访问http://localhost:8082/order/1 应该返回:

{
    "orderId": 1,
    "orderName": "河南特产大礼包",
    "status": "已发货",
    "address": "河南省郑州市金水区",
    "user": {
        "id": 1,
        "name": "张三",
        "address": "郑州",
        "favoriteFood": "烩面"
    }
}

得劲! order-service成功调用了user-service!

四、Feign高级配置(稍微了解一下)

1. 超时配置(默认等1秒,太短)

order-serviceapplication.yml里:

feign:
  client:
    config:
      default:  # 全局配置
        connect-timeout: 5000  # 连接超时5秒
        read-timeout: 5000     # 读取超时5秒
      user-service:  # 针对user-service的配置
        connect-timeout: 3000
        read-timeout: 3000

# 或者用ribbon(Feign底层用ribbon)
ribbon:
  ConnectTimeout: 5000
  ReadTimeout: 5000

2. 开启日志(调试时用)

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

logging:
  level:
    com.example.order.feign.UserClient: DEBUG  # 你的Feign客户端包名

在配置类里:

@Configuration
public class FeignConfig {
    
    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;  // 详细日志
    }
}

日志会显示:

[UserClient#getUserById] ---> GET http://user-service/user/1 HTTP/1.1
[UserClient#getUserById] <--- HTTP/1.1 200 OK (15ms)

3. 失败重试

spring:
  cloud:
    loadbalancer:
      retry:
        enabled: true  # 开启重试

五、负载均衡(Load Balancing)

啥是负载均衡?

假设user-service有3个实例:

  • 实例1:8081端口
  • 实例2:8083端口
  • 实例3:8084端口

OpenFeign会自动把请求均匀分配给这三个实例,不会让一个实例累死。

测试负载均衡

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

  1. 启动多个user-service实例:

    • 改端口号启动就行
    • IDEA里复制配置,改server.port
    • 启动8081、8083、8084三个实例
  2. 在Nacos里能看到3个实例

  3. 多次访问http://localhost:8082/order/test/user

    • 看每个实例的控制台日志
    • 请求会被均匀分配

得劲! 不用改代码,自动负载均衡!

六、实际开发中的最佳实践

1. 统一返回结果

// 通用的返回类
public class Result<T> {
    private Integer code;
    private String message;
    private T data;
    // ...
}

// Feign客户端返回Result
@FeignClient(name = "user-service")
public interface UserClient {
    @GetMapping("/user/{id}")
    Result<User> getUserById(@PathVariable Integer id);
}

2. 统一异常处理

// 创建Fallback类(服务降级)
@Component
public class UserClientFallback implements UserClient {
    
    @Override
    public User getUserById(Integer id) {
        // user-service挂了,返回兜底数据
        return new User(id, "默认用户", "河南", "胡辣汤");
    }
    
    @Override
    public String test() {
        return "user-service暂时不可用";
    }
}

// Feign客户端指定Fallback
@FeignClient(name = "user-service", fallback = UserClientFallback.class)
public interface UserClient {
    // ...
}

3. 放在公共模块

把Feign客户端接口放到一个公共模块,谁想用谁引用。

七、今儿个总结

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

学会了啥?

  1. ✅ OpenFeign是干啥的(翻译官+跑腿的)
  2. ✅ 创建Feign客户端接口(@FeignClient
  3. ✅ 调用远程服务(跟调本地方法一样)
  4. ✅ 配置超时、日志
  5. ✅ 负载均衡(自动的!)

关键点

  1. 服务名要对@FeignClient(name = "user-service")里的name必须跟Nacos里一样
  2. 注解要一致:方法上的注解要跟被调用方一致
  3. 依赖要全:OpenFeign + LoadBalancer
  4. 启动类要加注解@EnableFeignClients

八、常见问题

1. 报错:UnknownHostException: user-service

  • 检查Nacos里user-service注册上了没
  • 检查@FeignClient的name写对没
  • 检查order-service连上Nacos没

2. 报错:Connection refused

  • 检查user-service启动了没
  • 检查端口号对不
  • 等一会儿再试(服务刚启动可能需要时间)

3. 调用返回null

  • 检查返回值类型对不
  • 检查被调用方接口路径对不
  • 开日志看看具体请求和响应

4. 超时问题

  • 默认1秒太短,调大点
  • 网络不好也会超时

九、明儿个学啥?

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

明天咱学服务熔断降级

  • user-service挂了咋办?
  • order-service一直等,等到天荒地老?
  • Sentinel做熔断降级
  • 跟电路保险丝一样,过载就跳闸!

明天咱让服务更坚强,一个挂了不影响别的!🚀