Spring Cloud微服务之Feign——声明式服务间调用

目录

写在前面

服务间调用常见的方案

方案一:HttpClient

方案二:RestTemplate

方案三:Feign

框架版本问题及可能遇到的坑

搭建服务注册中心

搭建服务1——service-a

搭建服务2——service-b

在service-b中通过Feign调用service-a

进阶:Feign服务间调用—传递各种参数

再次进阶:引入Hystrix熔断器

总结


写在前面

在之前的 Spring Cloud微服务入门级教程(零基础,最详细,可运行) 文章中,我们搭建了一套基于Spring Cloud的微服务项目。在那个项目中,我们有两个相同的服务提供者和一个网关(Gateway),然后客户端直接访问我们的网关,由网关转发到两个相同的服务节点。这种情况其实只适用于所有API接口都混合在同一个服务下的项目,我们只是提供了多个服务实例来达到负载均衡,并没有真正的按照功能进行拆分。

在真正的微服务项目开发中,我们绝对不只是提供多个服务实例去负载均衡,要不然直接用Nginx不就行了吗?大部分情况是根据业务维度进行拆分,所以在特定的业务场景下就会出现服务间相互调用的需求。比如服务A需要调用服务B的接口,那么我们应该如何实现呢?

服务间调用常见的方案

方案一:HttpClient

这里说的HttpClient,指的是一类产品或方案,比如还有OkHttp系列的。这种方案,是最暴力的实现方式,直接通过Java代码模拟浏览器去调用接口,也是在最早的分布式系统中所使用的,因为那时候没有服务治理框架,只能这样去调用。但是这种方式,暴力不代表不合理。在当下,依赖第三方接口的开发只能用这种方式去调用,比如微信公众号开发中的相关API接口。

这种方式呢,调用一个接口比较繁琐,大体的流程一致:先获取一个client对象,然后要构建request对象、接收response对象,如果有请求头还需要Header对象,如果是https的还要处理SSL。传参的话也不是很灵活,get请求还好说,post请求简直就是噩梦,特别是写到请求体里的body传参方式……

方案二:RestTemplate

在微服务的架构下,我们有服务治理中心,当然不会通过上面的方式去调用啦……

RestTemplate是Spring Cloud提供的一种服务间调用解决方案,它虽然利用了服务治理中心来调取服务,但是我感觉它就是穿着华丽外衣的HttpClient,看一下它的调用方式:

它在调用接口的时候,依然会让你传入一个url,只不过通过HttpClient调用接口时传入的url是一个可以外网访问的唯一http地址,而RestTemplate传入的url,是把域名换成了服务名,然后根据服务名去注册中心找对应的服务。

什么意思呢?下面看一个例子。

假如我们有一个服务,在注册中心的服务名叫做service-a,这个service-a服务下面有一个接口,地址是/student/list,假设域名www.test.com可以直接定位到service-a服务。同时注册中心有service-b,需要调用service-a的这个接口,那么如果我们通过HttpClient去调用这个服务接口,那传入的url就是http://www.test.com/student/list,这样显然没有通过我们的注册中心来获取服务;如果我们通过RestTemplate去调用,那就是http://service-a/student/list。

上面的例子,RestTemplate就会根据服务名称去注册中心寻找对应的服务,将请求转发过去。如果运用了Ribbon的话,还会实现负载均衡的效果:注册中心将所有此名称的服务实例清单返回,然后从这些服务清单中任选一个服务进行请求转发。

RestTemplate的传参、请求体的设置、请求头的设置,和HttpClient的流程如出一辙,比较繁琐,不够优雅,区别就是可以利用服务治理中心来达到了负载均衡。

方案三:Feign

接下来说说我们今天的主角,Feign。它是对RestTemplate和Ribbon的封装,让我们的服务间调用更加方便、更加简洁,实现声明式的服务调用,具体什么意思呢?

我们在使用Feign进行服务间调用时,只需要在调用方建一个interface接口,并且加上@FeignClient注解将其声明为一个服务调用客户端,并且进行相关的注解赋值,就可以调用其他服务了!

实践出真知,下面我们就来代码实战。

框架版本问题及可能遇到的坑

笔者使用的Spring Boot版本是2.2.2.RELEASE,Spring Cloud版本是Hoxton.RELEASE,JDK版本是10,不同版本的框架所使用的maven依赖可能是不同的,如果你的版本不对,可能会导致依赖下载不下来。

另外笔者用的是IDEA自带的maven,不是自己安装配置的,所以如果你也要使用的话,请在如下地方进行设置:

如果大家在搭建过程中遇到其他问题,请在下方进行评论留言或私信,我会及时查看。

搭建服务注册中心

开发工具是IDEA,怎样创建项目我就不说了,在Spring Cloud微服务入门级教程(零基础,最详细,可运行)里写的很详细。需要注意的是,在创建项目的时候要勾选上eureka依赖:

项目创建好之后,目录结构如下:

然后我们在启动类上面添加注解@EnableEurekaServer,表明这是一个服务注册中心:

package com.demo.feign.register;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;


@SpringBootApplication
@EnableEurekaServer
public class DemoRegisterApplication {

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

}

然后在配置文件填写如下配置(注意我的后缀是yml,如果你的是properties请修改格式):

# 端口8080
server:
  port: 8080

# 禁止注册自己
eureka:
  client:
    fetch-registry: false
    register-with-eureka: false
    service-url:
      defaultZone: http://localhost:8080/eureka/

搭建服务1——service-a

这里我们将service-a作为一个服务,待会建一个service-b来调用service-a。

这里我们选择的依赖有Web、eureka-client、lombok(需单独在IDEA进行安装),对应的依赖如下:

然后我们需要在启动类添加注解,表明这是一个服务客户端:

package com.cloud.feign.servicea;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@EnableEurekaClient
@SpringBootApplication
public class ServiceAApplication {

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

}

然后在配置文件进行如下配置:

# 端口 8081
server:
  port: 8081

# 服务名称 service-a
spring:
  application:
    name: service-a

# 注册到服务中心
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8080/eureka/

为了良好的工程习惯,我们新建一个back包,并且在包下新建Back.java类,用于统一返回数据格式:

package com.cloud.feign.servicea.back;
 
 
import lombok.*;
import lombok.experimental.Accessors;
 
import java.io.Serializable;
 
/**
 *  <p>
 *      统一返回数据
 *  </p>
 *
 * @author 秋枫艳梦
 * @date 2020-01-11
 * */
@Builder
@ToString
@Accessors(chain = true)
@AllArgsConstructor
public class Back<T> implements Serializable {
    private static final long serialVersionUID = 1L;
 
    /**
     * 运行成功
     */
    final String RUN_SUCCESS = "0";
 
    /**
     * 运行失败
     */
    final String RUN_ERROR = "1";
 
    /**
     * 业务失败
     */
    final Boolean BACK_ERROR = false;
 
    /**
     * 业务成功
     */
    final Boolean BACK_SUCCESS = true;
 
 
    @Getter
    @Setter
    private String code = RUN_SUCCESS;
 
    @Getter
    @Setter
    private String msg = "success";
 
 
    @Getter
    @Setter
    private Boolean state = BACK_SUCCESS;
 
    @Getter
    @Setter
    private Long count = null;
 
 
    @Getter
    @Setter
    private T data;
 
    public Back() {
        super();
    }
 
    public Back(T data) {
        super();
        this.data = data;
    }
 
 
    public Back<T> error(String msg) {
        this.state = BACK_ERROR;
        this.msg = msg;
        return this;
    }
 
    public Back<T> msg(String msg) {
        this.msg = msg;
        return this;
    }
}

然后我们新建entity包,并且在此包中新建Student.java实体类:

package com.cloud.feign.servicea.entity;

import lombok.Data;

/**
 *  <p>
 *      学生实体
 *  </p>
 *
 * @author 秋枫艳梦
 * @date 2020-02-25
 * */
@Data
public class Student {
    //姓名
    private String name;
    //年龄
    private Integer age;
}

然后我们新建controller包,并在包下面新建StudentController.java类:

package com.cloud.feign.servicea.controller;

import com.cloud.feign.servicea.back.Back;
import com.cloud.feign.servicea.entity.Student;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

/**
 *  <p>
 *      学生模块,控制器
 *  </p>
 *
 * @author 秋枫艳梦
 * @date 2020-02-25
 * */
@RestController
@RequestMapping(value = "/student")
public class StudentController {

    /**
     *  简单的查询,不带任何参数
     * @return 响应数据
     * */
    @GetMapping(value = "/get")
    public Back<Student> get () {
        Student student = new Student();
        student.setName("张三");
        student.setAge(18);

        return new Back<>(student).msg("success");
    }
}

最终的项目结构:

搭建服务2——service-b

现在我们搭建另一个服务service-b,在此服务中将通过Feign服务间调用的方式去调用我们在service-a当中编写的接口。

新建项目时除了service-a的那些依赖,因为我们要用到Feign,所以要勾选OpenFeign依赖,所有的依赖如下:

接下来在启动类添加@EnableEurekaClient和@EnableFeignClients,前者 表明这是一个服务客户端,后者表明此客户端需要通过Feign的方式调用其他的服务,通过此注解开启对Feign功能的支持。

package com.cloud.feign.serviceb;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableEurekaClient
@EnableFeignClients
@SpringBootApplication
public class ServiceBApplication {

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

}

然后我们在配置文件进行如下配置:

server:
  port: 8082

spring:
  application:
    name: service-b

eureka:
  client:
    service-url:
      defaultZone : http://localhost:8080/eureka/

至此,基本的环境就搭建好了,我们马上开始进行调用。

在service-b中通过Feign调用service-a

首先我们先在service-b项目中建一个包,叫service,并且在里面新建一个StudentService接口,用于调用service-a服务:

package com.cloud.feign.serviceb.service;

import com.cloud.feign.serviceb.fallback.StudentBack;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

/**
 *  <p>
 *      调用其他服务的声明式接口、Feign客户端
 *      我们使用@FeignClient注解将此接口声明为一个Feign客户端,并且通过
 *      value属性指定此接口要请求的服务名称,这里即是service-a
 *  </p>
 * 
 * @author 秋枫艳梦
 * @date 2020-02-27
 * */
@FeignClient(value = "service-a")
public interface StudentService {

    /**
     *  使用@GetMapping注解,表明此方法要请求service-a服务下的/student/get接口,
     *  相当于RestTemplate中的 http://service-a/student/get
     *  
     *  由于此处我们并没有处理service-a服务返回的结果,只是做了一下类似于请求转发,
     *  返回结果是service-a决定的,所以在这里指定为Object
     * */
    @GetMapping(value = "/student/get")
    Object get ();
}

上面的代码注释,笔者已经解释得很清楚了,当我们在service-b中的其他类(比如控制器)中调用StudentService的get()方法,那么Feign就会请求service-a服务下的/student/get接口,并将结果返回给调用者。

接下来,我们在service-b中新建一个包controller,并且在里面新建一个控制器,作为我们测试的入口:

package com.cloud.feign.serviceb.controller;

import com.cloud.feign.serviceb.service.StudentService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;

/**
 *  <p>
 *      测试入口
 *  </p>
 * 
 * @author 秋枫艳梦
 * @date 2020-02-27
 * */
@RestController
@RequestMapping(value = "/test")
@AllArgsConstructor
public class TestController {
    //装配Feign服务间调用组件
    private StudentService service;

    /**
     *  获取数据,调用StudentService的get()方法
     * */
    @GetMapping(value = "/get")
    public Object get () {
        return service.get();
    }
}

 

现在让我们将服务注册中心、service-a、service-b全部启动,可以看到注册中心已经将服务注册进来:

我们访问在service-b中的接口,即localhost:8082/test/get ,如果没问题的话应该会把service-a中的接口数据返回给我们,看一下效果:

可以看到我们已经通过Feign实现了简单的服务间调用,而且是不是比RestTemplate简单好多? 而且,Feign也集成来Ribbon,默认是支持负载均衡的,如果你有多个service-a实例,并且让这些实例返回不同的数据,你就会看到效果。

进阶:Feign服务间调用—传递各种参数

上面我们只是实现了最简单的get请求,而且什么参数都没有传,真正的项目环境中可要复杂的多,我们的service-b服务在调用service-a服务的时候可能要传递各种参数,下面我们就实现各种传参的调用示例。

先总结一下我们常用的传参方式:

  • RequestParam,也是最基本的传参,直接跟在路径后面,如http://www.xxx.com/list?id=1&name=张三
  • RequestBody,经常用于POST或PUT请求,新增或编辑数据时使用,是一种JSON请求体的参数
  • PathVariable,在RESTful风格的路径中比较常见,比如删除id为1的学生:http://www.xxx.com/student/del/1
  • RequestHeader,即将参数写到请求头中,多用于token、身份验证数据或敏感数据

Feign中各个服务之间传递参数的方式,跟平常的controller层入参基本一样,所以特别浅显易懂。接下来,笔者将带领大家实现这四种传参方式,基本可以满足开发中95%以上的使用场景。

首先,我们在service-a中的controller层扩展几个方法,分别涵盖上面的四种参数传递方式:

注意这里的代码是service-a中的StudentController.java!!!

package com.cloud.feign.servicea.controller;

import com.cloud.feign.servicea.back.Back;
import com.cloud.feign.servicea.entity.Student;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

/**
 *  <p>
 *      学生模块,控制器
 *  </p>
 *
 * @author 秋枫艳梦
 * @date 2020-02-25
 * */
@RestController
@RequestMapping(value = "/student")
public class StudentController {

    /**
     *  简单的查询,不带任何参数
     * @return 响应数据
     * */
    @GetMapping(value = "/get")
    public Back<Student> get () {
        Student student = new Student();
        student.setName("张三");
        student.setAge(18);

        return new Back<>(student).msg("success");
    }

    /**
     *  RequestParam参数
     * @param id 接收到的id
     * @return 响应数据
     * */
    @GetMapping(value = "/get/by/id")
    public Back<Integer> getById (@RequestParam(value = "id") Integer id) {
        //为了方便测试观察,直接将id返回去
        return new Back<>(id).msg("根据ID查询成功");
    }

    /**
     *  RequestBody请求体参数
     * @param student 学生实体
     * @return 响应数据
     * */
    @PostMapping(value = "/save")
    public Back<Student> save (@RequestBody Student student) {
        //为了方便测试观察,直接将实体返回去,你也可以在控制台进行打印输出
        return new Back<>(student).msg("保存成功");
    }

    /**
     *  PathVariable参数
     * @param id 接收到的id
     * @return 响应数据
     * */
    @DeleteMapping(value = "/delete/{id}")
    public Back<Integer> delete (@PathVariable Integer id) {
        return new Back<>(id).msg("删除成功");
    }

    /**
     *  请求头参数,从请求头中获取相应的参数
     * @param request 请求对象
     * @return 响应数据
     * */
    @GetMapping(value = "/token")
    public Back<String> header (HttpServletRequest request) {
        return new Back<>(request.getHeader("token")).msg("success");
    }
}

接下来我们在service-b中的StudentService.java,也扩展一下,和上面的对应上,注意看注释:

这里需要将service-a中的实体Student,拿到service-b中一份

package com.cloud.feign.serviceb.service;

import com.cloud.feign.serviceb.entity.Student;
import com.cloud.feign.serviceb.fallback.StudentBack;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

/**
 *  <p>
 *      调用其他服务的声明式接口、Feign客户端
 *      我们使用@FeignClient注解将此接口声明为一个Feign客户端,并且通过
 *      value属性指定此接口要请求的服务名称,这里即是service-a
 *  </p>
 *
 * @author 秋枫艳梦
 * @date 2020-02-27
 * */
@FeignClient(value = "service-a")
public interface StudentService {

    /**
     *  使用@GetMapping注解,表明此方法要请求service-a服务下的/student/get接口,
     *  相当于RestTemplate中的 http://service-a/student/get
     *
     *  由于此处我们并没有处理service-a服务返回的结果,只是做了一下类似于请求转发,
     *  返回结果是service-a决定的,所以在这里指定为Object
     * */
    @GetMapping(value = "/student/get")
    Object get ();

    /**
     *  Feign调用其他服务时,@RequestParam必须显式声明参数名,否则会报错。
     *  我们在参数前面加上注解,就像Controller层接收参数一样,这样Feign在
     *  调用服务时就会以对应的注解方式进行传参,假设id为1,这里的效果等价于:
     *  http://service-a/student/get/by/id?id=1
     *  
     * @param id ID
     * */
    @GetMapping(value = "/student/get/by/id")
    Object getById (@RequestParam(value = "id") Integer id);

    /**
     *  由于我们此处用的注解是@PostMapping,那么这个方法被调用时,其实就是以
     *  POST的形式调用了 http://service-a/student/save 接口,并且将接收到的
     *  实体以JSON请求体的方式传递过去
     *  
     * @param student 数据实体
     * */
    @PostMapping(value = "/student/save")
    Object save (@RequestBody Student student);

    /**
     *  同上,Feign会先识别出此处的方法注解是@DeleteMapping,就会以delete方式
     *  请求 http://service-a/student/delete/接口,又因为我们使用了@PathVariable
     *  注解,所以假设当参数为1时,等价于:
     *  http://service-a/student/delete/1
     *  
     * @param id ID
     * */
    @DeleteMapping(value = "/student/delete/{id}")
    Object delete (@PathVariable Integer id);

    /**
     *  由于此处使用了@RequestHeader注解,Feign在调用/student/token接口
     *  时会把token写到请求头中,并且被调用者可以通过HttpServletRequest对
     *  象获取到这个参数
     * 
     * @param token token
     * */
    @GetMapping(value = "/student/token")
    Object headers (@RequestHeader(value = "token") String token);
}

然后我们继续修改service-b中的TestController.java:

package com.cloud.feign.serviceb.controller;

import com.cloud.feign.serviceb.entity.Student;
import com.cloud.feign.serviceb.service.StudentService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

/**
 *  <p>
 *      测试入口
 *  </p>
 *
 * @author 秋枫艳梦
 * @date 2020-02-27
 * */
@RestController
@RequestMapping(value = "/test")
@AllArgsConstructor
public class TestController {
    //装配Feign服务间调用组件
    private StudentService service;

    /**
     *  获取数据,调用StudentService的get()方法
     * */
    @GetMapping(value = "/get")
    public Object get () {
        return service.get();
    }

    @GetMapping(value = "/get/by/id")
    public Object getById (@RequestParam(value = "id") Integer id) {
        return service.getById(id);
    }

    @PostMapping(value = "/save")
    public Object save (@RequestBody Student student) {
        return service.save(student);
    }

    @DeleteMapping(value = "/delete/{id}")
    public Object delete (@PathVariable Integer id) {
        return service.delete(id);
    }

    @GetMapping(value = "/token")
    public Object header (HttpServletRequest request) {
        return service.headers(request.getHeader("token"));
    }
}

现在让我们开始使用Postman进行测试各个接口:

 

 

 

通过上面的结果,我们已经大功告成啦!

再次进阶:引入Hystrix熔断器

我们已经通过Feign实现了服务间的调用,但是服务和服务之间的这种依赖关系,可能一个服务挂了,另一个依赖它的服务也会受影响。我们要有一种机制,如果在调用某个服务的时候请求出错或者超时,要及时熔断,避免大量请求被阻塞,同时我们可能希望记录一下日志,或者将数据回滚,就像try-catch那样。

在之前的文章中笔者介绍过Gateway网关配合Hystrix的使用,但是我们这里没有走网关,走的是服务间内部调用,那该怎么办呢?强大的Feign对Hystrix也有很好的支持,就拿本文的例子来说:

我们可以声明一个类,比如叫StudentBack,用来执行service-a中的student相关接口异常时的回滚操作,我们只需要用这个类实现我们的Feign客户端StudentService接口,并且在注解上通过fallback指定StudentBack.class,那么出现异常或者超时,将会执行StudentBack中的相关方法。看一个例子!

我们在service-b中引入Back类,用于返回信息,同时建一个fallback包,然后建一个处理类StudentBack.java:

package com.cloud.feign.serviceb.fallback;

import com.cloud.feign.serviceb.back.Back;
import com.cloud.feign.serviceb.entity.Student;
import com.cloud.feign.serviceb.service.StudentService;
import org.springframework.stereotype.Component;

/**
 *  <p>
 *      调用service-a失败时的熔断处理方法
 *      使用@Component注解声明这个类为Bean
 *  </p>
 *
 * @author 秋枫艳梦
 * @date 2020-02-27
 * */
@Component
public class StudentBack  implements StudentService {

    /**
     *  调用http://service-a/student/get失败时的处理方法
     *
     * */
    @Override
    public Object get() {
        return new Back<>().error("内部系统异常");
    }


    @Override
    public Object getById(Integer id) {
        return null;
    }

    @Override
    public Object save(Student student) {
        return null;
    }

    @Override
    public Object delete(Integer id) {
        return null;
    }

    @Override
    public Object headers(String token) {
        return null;
    }
}

上面只以一个get()方法为例。因为StudentService是一个interface接口,而这里的StudentBack是一个实现了StudentService接口的类,所以每当一个StudentService中的方法执行失败时(也就是调用其他服务的接口失败时),都会执行StudentBack中相应的实现方法,这样我们就可以为每一个请求的失败进行定制化处理,记录日志、重试、数据回滚……

然后不要忘了在我们的StudentService中的注解上指定fallback:

最后还需要在配置文件中开启Feign对Hystrix的支持:


然后我们在service-a中的接口,制造一个异常:

 重启服务,进行测试,访问service-b中的接口:

 以上,我们就实现了异常情况下的处理。

如果要配置超时熔断,则将service-b的配置文件修改如下:

server:
  port: 8082

spring:
  application:
    name: service-b

eureka:
  client:
    service-url:
      defaultZone : http://localhost:8080/eureka/

feign:
  hystrix:
    enabled: true

# ribbon级超时配置
ribbon:
  ReadTimeout: 3000
  ConnectTimeout: 3000

# hystrix级超时配置
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 6000

以上就是关于超时的配置,如果service-a的controller中睡眠8秒,就可以看到效果,关于这里的超时配置,参考的是下面的这篇文章,这个博主说的很好:

https://blog.csdn.net/east123321/article/details/82385816

总结

今天的文章到这里就结束了,希望可以帮助到大家。如果有什么问题,可以给我评论留言或者私信,我会及时查看,欢迎大家多来交流。

另外,给大家贴出来Spring Cloud Feign的官方文档,里面有一些新的玩法大家可以研究一下:

https://cloud.spring.io/spring-cloud-netflix/multi/multi_spring-cloud-feign.html

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值