目录
写在前面
服务间调用常见的方案
方案一: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,指的是一类产品或方案,比如还有OkHttp系列的。这种方案,是最暴力的实现方式,直接通过Java代码模拟浏览器去调用接口,也是在最早的分布式系统中所使用的,因为那时候没有服务治理框架,只能这样去调用。但是这种方式,暴力不代表不合理。在当下,依赖第三方接口的开发只能用这种方式去调用,比如微信公众号开发中的相关API接口。
这种方式呢,调用一个接口比较繁琐,大体的流程一致:先获取一个client对象,然后要构建request对象、接收response对象,如果有请求头还需要Header对象,如果是https的还要处理SSL。传参的话也不是很灵活,get请求还好说,post请求简直就是噩梦,特别是写到请求体里的body传参方式……
在微服务的架构下,我们有服务治理中心,当然不会通过上面的方式去调用啦……
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。它是对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/
这里我们将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;
/**
*
* 统一返回数据
*
*
* @author 秋枫艳梦
* @date 2020-01-11
* */
@Builder
@ToString
@Accessors(chain = true)
@AllArgsConstructor
public class Back 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 error(String msg) {
this.state = BACK_ERROR;
this.msg = msg;
return this;
}
public Back msg(String msg) {
this.msg = msg;
return this;
}
}
然后我们新建entity包,并且在此包中新建Student.java实体类:
package com.cloud.feign.servicea.entity;
import lombok.Data;
/**
*
* 学生实体
*
*
* @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;
/**
*
* 学生模块,控制器
*
*
* @author 秋枫艳梦
* @date 2020-02-25
* */
@RestController
@RequestMapping(value = "/student")
public class StudentController {
/**
* 简单的查询,不带任何参数
* @return 响应数据
* */
@GetMapping(value = "/get")
public Back get () {
Student student = new Student();
student.setName("张三");
student.setAge(18);
return new Back<>(student).msg("success");
}
}
最终的项目结构:
现在我们搭建另一个服务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项目中建一个包,叫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.*;
/**
*
* 调用其他服务的声明式接口、Feign客户端
* 我们使用@FeignClient注解将此接口声明为一个Feign客户端,并且通过
* value属性指定此接口要请求的服务名称,这里即是service-a
*
*
* @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.*;
/**
*
* 测试入口
*
*
* @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实例,并且让这些实例返回不同的数据,你就会看到效果。
上面我们只是实现了最简单的get请求,而且什么参数都没有传,真正的项目环境中可要复杂的多,我们的service-b服务在调用service-a服务的时候可能要传递各种参数,下面我们就实现各种传参的调用示例。
先总结一下我们常用的传参方式:
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;
/**
*
* 学生模块,控制器
*
*
* @author 秋枫艳梦
* @date 2020-02-25
* */
@RestController
@RequestMapping(value = "/student")
public class StudentController {
/**
* 简单的查询,不带任何参数
* @return 响应数据
* */
@GetMapping(value = "/get")
public Back 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 getById (@RequestParam(value = "id") Integer id) {
//为了方便测试观察,直接将id返回去
return new Back<>(id).msg("根据ID查询成功");
}
/**
* RequestBody请求体参数
* @param student 学生实体
* @return 响应数据
* */
@PostMapping(value = "/save")
public Back save (@RequestBody Student student) {
//为了方便测试观察,直接将实体返回去,你也可以在控制台进行打印输出
return new Back<>(student).msg("保存成功");
}
/**
* PathVariable参数
* @param id 接收到的id
* @return 响应数据
* */
@DeleteMapping(value = "/delete/{id}")
public Back delete (@PathVariable Integer id) {
return new Back<>(id).msg("删除成功");
}
/**
* 请求头参数,从请求头中获取相应的参数
* @param request 请求对象
* @return 响应数据
* */
@GetMapping(value = "/token")
public Back 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.*;
/**
*
* 调用其他服务的声明式接口、Feign客户端
* 我们使用@FeignClient注解将此接口声明为一个Feign客户端,并且通过
* value属性指定此接口要请求的服务名称,这里即是service-a
*
*
* @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;
/**
*
* 测试入口
*
*
* @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进行测试各个接口:
通过上面的结果,我们已经大功告成啦!
我们已经通过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;
/**
*
* 调用service-a失败时的熔断处理方法
* 使用@Component注解声明这个类为Bean
*
*
* @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