在前面几篇文章中,主要和大家介绍了服务的注册与消费。在介绍过程中,我们从最最原始的手动利用 DiscoveryClient 发现服务开始,手动实现负载均衡,再到最后的自动化配置。相信经过前面几篇文章的学习,大家对微服务之间的调用应该有了一个基本的认知。但是我们前面的所有服务调用都是手动写 RestTemplate 来实现的,大家可能已经发现这样写有点麻烦,每次都要写请求 Url 、配置响应数据类型,最后还要组装参数,更重要的是这些都是一些重复的工作,代码高度相似,每个请求只有 Url 不同,请求方法不同、参数不同,其它东西基本都是一样的,既然如此,那有没有办法简化请求呢?有!这就是本文我们要聊的声明式微服务调用 Feign。不对,严格来说,应该叫 OpenFeign,为什么这么说呢?早期我们用的是叫 Netflix Feign,不过这个东西的最近一次更新还停留在 2016年7月,OpenFeign 则是 Spring Cloud 团队在 Netflix Feign 基础上开发出来的声明式服务调用组件,OpenFeign也一直在维护,具体的迁移工作,大家参考

准备工作

和前面几篇文章的步骤一样,首先我们还是要先搭建一个父工程,然后创建一个服务注册中心。OK,那就开始吧! 首先创建一个名为 Feign 的 Maven 父工程,然后在父工程中创建一个 eureka 工程充当我们的服务注册中心,具体步骤我这里就不再赘述了,大家如果忘了如何搭建服务注册中心,可以参考前面的文章。

服务注册中心搭建成功后,接下来我们还要再搭建一个 provider 用来提供服务。这个 provider 和前面 provider 的搭建也是基本一致的。 provider 搭建成功后,依然提供一个 HelloController 接口,里边配上一个 /hello的接口:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(String name) {
        return "hello " + name + " !";
    }
}

然后分别启动服务注册中心 eureka 以及服务提供者 provider ,然后在浏览器中输入http://localhost:1111 可以看到我们的实例情况
在这里插入图片描述

如何使用Feign

准备工作完成后,我们创建一个feign-consumer的SpringBoot工程,项目创建好后依赖如下 :

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <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>cn.com.scitc</groupId>
            <artifactId>commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

创建好了后,我们在application.yml中将我们的feign -consumer 注册到服务中心 (eureka)中

spring:
  application:
    name: feigin-consumer
eureka:
  client:
    service-url:
      defaultZone: http://localhost:1111/eureka
server:
  port: 5002

最后在我们的feign-consumer中的主启动类中添加@EnableFeignClients
注解,开启Feign的支持

@SpringBootApplication
@EnableFeignClients
public class FeiginConsumerApplication {

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

下面我们创建一个HelloService接口,用来消费provider提供的接口

@FeignClient("provider")
public interface HelloService {
    @GetMapping("/hello")
    String hello(@RequestParam("name") String name);
}

这个接口做了两件事:

  1. 使用 @FeignClient(“provider”) 注解将当前接口和 provider 服务绑定, provider 是服务名,可以忽略大小写;
  2. 然后使用 SpringMVC 的 @GetMapping("/hello") 注解将 hello 方法和 provider 中的 hello 接口绑定在一起。需要注意的是,在 SpringMVC 中,在需要给参数设置默认值或者要求参数必填的情况下才需要用到 @RequestParam 注解,而在这里,这个注解一定要加。

经过这样的步骤之后,我们就可以在一个 Controller 中注入 HelloService 接口并使用它了,而 HelloService 接口也会去调用相关的服务。我的 Controller 如下:

@RestController
public class HelloController {
    @Autowired
    HelloService helloService;

    @GetMapping("/hello")
    public String hello(String name) {
        return helloService.hello(name);
    }
}

配置好了后,我们在浏览器上访问http://localhost:5002/hello?name=技术无止境,显示的效果如下:
在这里插入图片描述
可以看到我们这样写代码 比之前用restTemplate 清爽了许多。

参数传递和JSON

上面和大家展示了 Feign 的基本使用。接下来和大家说说 Feign 中的参数传递问题,相信对于大多数人而言,在开发中,参数传递无非就是 key/value 形式的参数、放在 body 中的参数、放在 Url 路径上的参数以及放在请求头上的参数,这四种是较为常见的四种传参方式,因此,这里就重点和大家分享下这四种不同的传参方式在 Feign 中要如何使用。

我们再创建一个叫commons普通的maven工程 作为Feign的子项目,然后在commons项目中添加一个UserDTO 如下:

public class UserDTO {
    private Integer id;
    private String nickname;
    private String address;
 }

这里省略get/set方法,然后我们在provider和feign-consumer中加入commons这个依赖,就可以使用了。

然后我们在provider 中添加UserController


@RestController
public class UserController {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    @PostMapping("/user")
    public ResponseEntity<UserDTO> addUser(@RequestBody UserDTO userDTO) {
        logger.info("provider提供了addUser服务");
        return new ResponseEntity<UserDTO>(userDTO, HttpStatus.OK);
    }

    @PutMapping("/user")
    public ResponseEntity<Object> updateUser(@RequestBody UserDTO userDTO) {
        HashMap<String,Object> map = new HashMap<>();
        map.put("name",userDTO.getNickname());
        map.put("id", userDTO.getId());
        return new ResponseEntity<Object>(map,HttpStatus.OK);
    }

    @GetMapping("/user")
    public ResponseEntity<UserDTO> getUserDTOByName(@RequestParam String name) {
        UserDTO userDTO = new UserDTO();
        userDTO.setNickname(name);
        logger.info("provider提供了getUserDTOByName服务");
        return new ResponseEntity<UserDTO>(userDTO, HttpStatus.OK);
    }

    @DeleteMapping("/user/{id}")
    public ResponseEntity<Integer> deleteUserDTOById(@PathVariable Integer id) {
        logger.info("provider提供了deleteUserDTOById服务");

        return new ResponseEntity<Integer>(id, HttpStatus.OK);
    }
}

上面的几个方法我做几个解释
@PostMapping("/user") 是一个创建的方法
@PutMapping("/user") 是一个更新方法
@GetMapping("/user") 是一个查询的方法
@DeleteMapping("/user/{id}") 根据id来删除的方法

在provider中添加完成后,我们在feign-consumer中添加代码如下:

@FeignClient("provider")
public interface HelloService {
    @GetMapping("/hello")
    String hello(@RequestParam("name") String name);

    @PostMapping("/user")
    String addUser(@RequestBody UserDTO userDTO);

    @PutMapping("/user")
    String updateUser(@RequestBody UserDTO userDTO);

    @DeleteMapping("/user/{id}")
    String deleteUserDTOById(@PathVariable Integer id);

    @GetMapping("/user")
    String getUserDTOByName(@RequestParam String name);
}

这里的调用,基本也和前面一样,无需赘述,但是有一个地方需要强调,那就是不同于 provider ,这里的参数如果是 key/value 形式的,一定要在 @RequestParam 注解中指明 name 属性,如果是在 header 中传递的,则一定要在 @RequestHeader 注解中添加 name 属性,如果参数放在 Url 路径中,那么一定需要在 @PathVariable 注解中添加 name 属性指明参数名称。

配置完成后 我们在feign-consumer 中的HelloController中添加 一个接口测试如下:

 @PostMapping("/user")
    public ResponseEntity<String> addUser(@RequestBody UserDTO userDTO ) {
        String s = helloService.addUser(userDTO);
        log.info("消费了provider的addUser");
        return new ResponseEntity<String>(s, HttpStatus.OK);
    }

我们用postman 或者 restlet 工具来测试 http://localhost:5002/user接口 返回结果如下图所示 :
在这里插入图片描述
然后看我们provider 日志输出:

在这里插入图片描述
再看我们的feign-consumer的日志输出
在这里插入图片描述
其它的方法就不演示了,详情看代码,我这里非常的建议大家自己去写一些服务然后去测试,这样的话就非常的好理解。

总结

本节主要通过两个案例介绍了声明式服务调用的一个基本用法。相信有一部分读者在读完本节后,会很容易想到 MyBatis,很多人在刚开始学习 MyBatis 的时候,并不是一上来就使用 Mapper ,而是先从简单的 SqlSessionFactory 开始,在使用的过程中,发现了大量的模板化代码,于是才有了 Mapper ,使用了动态代理启动生成调用逻辑,开发者只需要生命最最关键的部分。MyBatis 中的这一演变过程和我们这里的演变如出一辙, 直接使用 RestTemplate 也带来了大量的模板化代码,通过 Feign ,我们只需要定义一下方法中最最关键的部分,就能实现调用。那么有人要问了,用了 Feign ,我们在 RestTemplate 中使用的负载均衡还能继续用吗?答案当然是可以继续使用,关于这个问题以及 Feign 的一些高级用法,我将在下篇文章中和大伙儿分享。

源码地址

github

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐