一. 什么是Feign

Feign是一个声明式的http客户端,使用Feign可以实现声明式REST调用,它的目的就是让Web Service调用更加简单。Feign整合了RibbonSpringMvc注解,这让Feign的客户端接口看起来就像一个Controller。Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。而Feign则会完全代理HTTP请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。同时Feign整合了Hystrix,可以很容易的实现服务熔断和降级。

image-20221013184846981

二.实例

  1. 创建一个springboot模块,并导入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
       
    <dependencies>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>com.lqs</groupId>
    <artifactId>springcloud-entity</artifactId>
    <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!--openfeign-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    </dependencies>
  2. 配置类如下

    主配置类增加@EnableFeignClients标签 , 其value属性可以指定Feign的客户端接口的包,当然也可以省略value属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /`
    * 支付的启动类
    * @EnableFeignClients :开启Feign支持
    */
    @SpringBootApplication
    @EnableEurekaClient
    @EnableFeignClients(value="com.lqs.feignclient")
    public class PayServerApp
    {
    public static void main( String[] args )
    {
    SpringApplication.run(PayServerApp.class);
    }
    }
  3. yml配置如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    server:
    port: 8089
    spring:
    application:
    name: pay-server
    eureka:
    client:
    service-url:
    defaultZone: http://localhost:10086/eureka
    instance:
    prefer-ip-address: true
    instance-id: pay-server:${server.port}
    user-server:
    ribbon:
    # NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
    ConnectTimeout: 3000
    ReadTimeout: 6000

    logging:
    level:
    com.lqs: debug

    feign:
    hystrix:
    enabled: true #开启hystrix熔断
    ribbon:
    ReadTimeout: 1000
    SocketTimeout: 1000
    ConnectTimeout: 1000
    hystrix:
    command:
    default:
    execution:
    isolation:
    thread:
    timeoutInMilliseconds: 4000

  4. 编写Feign的客户端接口

    Feign的客户端接口是用来调用微服务的

    1
    2
    3
    4
    5
    @FeignClient(value = "user-server")
    public interface UserServerClient {
    @GetMapping("/user/{id}")
    User getByUserId(@PathVariable("id") Long id);
    }

解释
@FeignClient(“user-server”) : user-server是用户服务的服务名字,Feign根据服务名能够在注册中心找到目标服务的通信地址

其实Feign就是通过客户端接口里面的方法,来决定目标服务的资源路径url,参数以及返回值,这里我们可以直接把要调用的目标服务的controller方法拷贝过来,然后去掉方法体即可。

Feign可以根据@FeignClient(“user-server”)找到用户服务,根据方法上的 @GetMapping(“/user/{id}”)找到目标服务的controller的方法 ,我们在使用Feign接口时传入的参数就会作为目标服务controller方法的参数,而返回值就是目标服务controller方法的返回值。

即:服务名要一致 , url路径要一致 , 参数要一致 , 返回值类型要一致。

  1. 编写Controller使用Feign接口

    通过注入UserFeignClient ,直接发起调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @RestController
    @RequestMapping("/pay")
    public class PayBillController {
    @Autowired
    private UserServerClient userServerClient;

    @GetMapping("{id}")
    public PayBiIl getBillById(@PathVariable("id") Long id) {
    final User user = userServerClient.getByUserId(id);
    return new PayBiIl(1L, "202210131137", user);
    }

三.理解Feign的工作原理

要使用Feign,我们除了导入依赖之外,需要主配置类通过@EnableFeignClients(value="")注解开启Feign,也可以通过value属性指定了Feign的扫描包。同时我们需要为Feign编写客户端接口,接口上需要注解@FeignClient标签。 当程序启动,注解了@FeignClient的接口将会被扫描到然后交给Spring管理

当请求发起,会使用jdk的动态代理方式代理接口,生成相应的RequestTemplate,Feign会为每个方法生成一个RequestTemplate同时封装好http信息,如:url,请求参数等等

最终RequestTemplate生成request请求,交给Http客户端(UrlConnection ,HttpClient,OkHttp)。然后Http客户端会交给LoadBalancerClient,使用Ribbon的负载均衡发起调用。

四.Feign的参数配置

  1. 负载均衡配置

    Feign已经集成了Ribbon,所以它的负载均衡配置基于Ribbon配置即可

    1
    2
    3
    user-server:
    ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
  2. Feign的超时配置

    如果在服务调用时出现了 “feign.RetryableException : Read timed out…”错误日志,说明Ribbon处理超时 ,我们可以配置Ribbon的超时时间:

    1
    2
    3
    ribbon:
    ConnectTimeout: 3000
    ReadTimeout: 6000

    如果服务调用出现“com.netflix.hystrix.exception.HystrixRuntimeException:.. timed - out and no fallback available” 错误日志,是因为Hystrix超时,默认Feign集成了Hystrix,但是高版本是关闭了Hystrix,我们可以配置Hystrix超时时间:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    feign:
    hystrix:
    enabled: true #开启熔断支持
    hystrix:
    command:
    default:
    execution:
    isolation:
    thread:
    timeoutInMilliseconds: 6000 #hystrix超时时间
  1. 配置Feign日志打印内容

    有的时候我们需要看到Feign的调用过程中的参数及相应,我们可以对Feign的日志进行配置,Feign支持如下几种日志模式来决定日志记录内容多少:

    • NONE,不记录(DEFAULT)。
    • BASIC,仅记录请求方法和URL以及响应状态代码和执行时间。
    • HEADERS,记录基本信息以及请求和响应标头。
    • FULL,记录请求和响应的标题,正文和元数据。

    创建Feign配置类

1
2
3
4
5
6
7
@Configuration
public class FeignConfiguration {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL; //打印Feign的所有日志
}
}
1
2
3
logging:
level:
com.lqs: debug

五.Hystrix熔断器

  1. Hystrix介绍

    Hystrix是国外知名的视频网站Netflix所开源的非常流行的高可用架构框架。Hystrix能够完美的解决分布式系统架构中打造高可用服务面临的一系列技术难题,如雪崩。

    Hystrix是处理依赖隔离的框架,将出现故障的服务通过熔断、降级等手段隔离开来,这样不影响整个系统的主业务(比如你得了传染病是不是要把你关起来隔离呢),同时也是可以帮我们做服务的治理和监控

    Hystrix的英文是豪猪,中文翻译为 熔断器,其思想来源于我们家里的保险开关,当家里出现短路,保险开关及时切掉电路,保证家里人员的安全,其目的就是起保护作用。

    Hystrix其设计原则如下:

    1. 防止单个服务异常导致整个微服务故障。
    2. 快速失败,如果服务出现故障,服务的请求快速失败,线程不会等待。
    3. 服务降级,请求故障可以返回设定好的二手方案数据(兜底数据)。
    4. 熔断机制,防止故障的扩散,导致整个服务瘫痪。
    5. 服务监控,提供了Hystrix Bashboard仪表盘,实时监控熔断器状态

微服务系统中,Hystrix 能够帮助我们实现以下目标:

  • 保护线程资源:防止单个服务的故障耗尽系统中的所有线程资源。
  • 快速失败机制:当某个服务发生了故障,不让服务调用方一直等待,而是直接返回请求失败。
  • 提供降级(FallBack)方案:在请求失败后,提供一个设计好的降级方案,通常是一个兜底方法,当请求失败后即调用该方法。
  • 防止故障扩散:使用熔断机制,防止故障扩散到其他服务。
  • 监控功能:提供熔断器故障监控组件 Hystrix Dashboard,随时监控熔断器的状态。
  1. Hystrix工作机制

    正常情况下,断路器处于关闭状态(Closed),如果调用持续出错或者超时达到设定阈值,电路被打开进入熔断状态(Open),这时请求这个服务会触发快速失败(立马返回兜底数据不要让线程死等),后续一段时间内的所有调用都会被拒绝(Fail Fast),一段时间以后(withCircuitBreakerSleepWindowInMilliseconds=5s),保护器会尝试进入半熔断状态(Half-Open),允许少量请求进来尝试,如果调用仍然失败,则回到熔断状态,如果调用成功,则回到电路闭合状态;

image-20221013192148794

六.Hystrix的使用

  1. 导入依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
  2. 配置类开启Hystrix

    主配置类通过 @EnableCircuitBreaker 标签开启熔断功能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @SpringBootApplication
    @EnableEurekaClient
    @EnableCircuitBreaker // 开启Hystrix熔断
    public class OrderServerApp {
    public static void main(String[] args) {
    SpringApplication.run(OrderServerApp.class);
    }

    @LoadBalanced // 实现负载均衡
    @Bean
    public RestTemplate getRestTemplate() {// 实现服务之间的通信
    return new RestTemplate();
    }

    /**
    * 配置随机的负载均衡算法
    * @return RandomRule
    */
    @Bean
    public RandomRule randomRule() {
    return new RandomRule();
    }
    }
  3. 方法熔断

    通过 @HystrixCommand 标签标记方法熔断,标签的fallbackMethod属性指定兜底方法。那么当该方法在像远程服务发起调用出现异常,或是方法本身出现异常都会触发托底方法的执行,最终结果是托底方法的返回结果。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Autowired
    private RestTemplate restTemplate;

    @HystrixCommand(fallbackMethod = "fallbackGetOrderById")// 兜底方法
    @GetMapping("{id}")
    public Order getOrderById(@PathVariable("id") Long id) {
    // 发送的http请求
    String url = "http://user-server/user/" + id;
    final User user = restTemplate.getForObject(url, User.class);
    return new Order(1L, 88L, "芋泥啵啵奶茶", 2, 1L, user);
    }

    public Order fallbackGetOrderById(Long id) {
    User user = new User(-1L, "返回数据错误!!!", "返回数据错误!!!", "请稍后再试!!!");
    return new Order(1L, 88L, "芋泥啵啵奶茶", 2, 1L, user);
    }

    我们可以在每个方法上打@HystrixCommand(fallbackMethod = “fallbackMethod”) 标签进行方法单独熔断,也可以在Controller使用 @DefaultProperties做统一配置,如

    1
    2
    3
    4
    5
    6
    7
    8
    @RestController
    @DefaultProperties(defaultFallback ="fallbackMethod") //统一降级配置
    public class OrderController {

    @HystrixCommand //方法熔断
    @RequestMapping(value = "/order/{id}",method = RequestMethod.GET)
    public User getById(@PathVariable("id")Long id)
    //...省略...
  4. OpenFeign使用Hystrix

    通过feign.hystrix.enabled=true开启Hystrix

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    feign:
    hystrix:
    enabled: true #开启熔断支持
    ribbon:
    ReadTimeout: 1000
    SocketTimeout: 1000
    ConnectTimeout: 1000
    hystrix:
    command:
    default:
    execution:
    isolation:
    thread:
    timeoutInMilliseconds: 4000

    Fiegn接口熔断-fallback方式

    服务通过Feign接口调用异常或超时需要触发降级,返回托底数据。这里有两种方式,分别是通过@FeignClient(fallback=..) ,以及@FeignClient(fallbackFactory=..) 来指定托底类,区别在于通过fallback的方式编写的托底是没办法打印出异常日志的 ,而fallbackFactory方式是可以打印出异常日志

    1
    2
    3
    4
    5
    6
    7
    8
    @FeignClient(value = "user-server",fallback = UserFeignClientFallback.class)
    public interface UserFeignClient {

    //订单服务来调用这个方法 http://localhost:1020/user/10
    // @GetMapping(value = "/user/{id}" )
    @RequestMapping(value = "/user/{id}",method = RequestMethod.GET)
    User getById(@PathVariable("id")Long id);
    }

    托底实现类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //让Spring扫描到该托底类
    @Component
    public class UserFeignClientFallback implements UserFeignClient {

    //日志打印器
    private Logger log = LoggerFactory.getLogger(UserFeignClientFallback.class);

    @Override
    public User getById(Long id) {
    log.info("用户服务不可用");
    //托底数据
    return new User(-1l,"无此用户","用户服务不可用");
    }
    }

提示:注意,这里托底类需要交给Spirng管理,类上需要打 @Component 注解 , 拖地类需要实现 Feign接口,复写接口中的方法作为托底方法返回拖地数据。当Fiegn调用失败就会以拖地方法返回的结果返回给用户

七.Fiegn接口熔断-fallbackFactory方式(常用)

使用fallbackFactory属性,使用工厂方式指定托底

1
2
3
4
5
@FeignClient(value = "user-server",fallbackFactory = UserFeignClientFallback.class)
public interface UserServerClient {
@GetMapping("/user/{id}")
User getByUserId(@PathVariable("id") Long id);
}

编写托底类
工程方式的托底类需要去实现 FallbackFactory接口 ,并指定泛型为“”Feign客户端接口(UserFeignClient )。FallbackFactory的create方法返回了Feign客户端接口的实例,该方法的throwable是参数是Feign调用失败的异常信息,如下:

1
2
3
4
5
6
7
8
9
10
@Component
public class UserFeignClientFallback implements FallbackFactory<UserServerClient> {
@Override
public UserServerClient create(Throwable throwable) {
return id -> {
System.err.println(throwable.getMessage());
return new User(-1L, "返回数据错误!!!", "返回数据错误!!!", "请稍后再试-_-!!!");
};
}
}

八.服务网关-spring cloud zuul

  1. 为什么要zuul

    试想一下如果我们有很多的微服务,他们都需要登录之后才能访问,那么我需要在每个微服务都去做一套登录检查逻辑,这样是不是会存在大量重复的代码和工作量,我们希望的是把登录检查这种公共的逻辑进行统一的抽取,只需要做一套检查逻辑即可,而zuul就可以用来干这类事情,我们可以把zuul看做是微服务的大门,所有的请求都需要通过zuul将请求分发到其他微服务,根据这一特性我们就可以在zuul做统一的登录检查,下游的微服务不再处理登录检查逻辑。

  2. 什么是zuul

    Zuul 是netflix开源的一个API Gateway 服务器, 本质上是一个web servlet(filter)应用。Zuul 在云平台上提供动态路由(请求分发),监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门,也要注册入Eureka,用一张图来理解zuul在架构中的的角色:

    image-20221013193448394

需要注意的是,zuul本身是一个独立的服务,默认集成了Ribbon,zuul通过Ribbon将客户端的请求分发到下游的微服务,所以zuul需要通过Eureka做服务发行,同时zuul也集成了Hystrix。

根据上图理解 ,我们需要建立独立的工程去搭建Zuul服务,同时需要把Zuul注册到EurekaServer,因为当请求过来时,zuul需要通过EurekaServer获取下游的微服务通信地址,使用Ribbon发起调用。

  1. 新建服务模块导入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--服务网关-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    </dependency>
    </dependencies>
  2. 配置类开启Zuul

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 用户的启动类
*
* @EnableEurekaClient: 标记该应用是 Eureka客户端
* @EnableZuulProxy : 开启zuul 可以看做是 @EnableZuulServer 的增强版 ,一般用这个
* @EnableZuulServer : 这个标签也可以开启zuul,但是这个标签开启的Filter更少
*/
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZuulServerApp {// 服务网关

public static void main(String[] args) {
SpringApplication.run(ZuulServerApp.class);
}
}
  1. 配置文件配置zuul

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server:
port: 8848
spring:
application:
name: zuul-server
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka
instance:
prefer-ip-address: true
instance-id: zuul-server:${server.port}

zuul:
prefix: "/servers" #统一访问前缀
ignoredServices: "*" #禁用掉使用浏览器通过服务名的方式访问服务
routes:
pay-server: "/pay/**" #指定pay-server这个服务使用 /pay路径来访问 - 别名
order-server: "/order/**" #指定order-server这个服务使用 /order路径来访问
user-server: "/user/**"
  • zuul.prefix : 作为统一的前缀,在浏览器访问的时候需要加上该前缀
  • zuul.ignoredServices : 忽略使用服务名方式访问服务,而是通过routes指定的路径进行访问
  • zuul.routes : 配置服务的访问路径

注意:在么有使用zuul之前我们是通过 http://localhost:8089/pay/1 来直接访问支付服务,现在需要通过zuul来访问,格式如下:http:// zuul的ip : zuul的port /zuul前缀 / 服务路径 /服务的controller路径 ,即:

http://localhost:8848/servers/pay/pay/1

  1. 自定义zuul的Filter

    Zuul提供了一个抽象的Filter:ZuulFilter我们可以通过该抽象类来自定义Filter,该Filter有四个核心方法,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public abstract class ZuulFilter implements IZuulFilter{

    abstract public String filterType();

    abstract public int filterOrder();


    //下面两个方法是 IZuulFilter 提供的

    boolean shouldFilter();

    Object run() throws ZuulException;

    }

    提示:

    • filterType :是用来指定filter的类型的(类型见常量类:FilterConstants)
    • filterOrder :是filter的执行顺序,越小越先执行
    • shouldFilter :是其父接口IZuulFilter的方法,用来决定run方法是否要被执行
    • run :是其父接口IZuulFilter的方法,该方法是Filter的核心业务方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package com.lqs.filter;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.http.HttpStatus;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
* @author lqs
* @date 2022/10/15 9:58
*/
@Component
public class AuthTokenFilter extends ZuulFilter {

/**
* @return 返回过滤类型
*/
@Override
public String filterType() {
return "pre";
}

/**
* @return 返回过滤器执行顺序 值越小越优先
*/
@Override
public int filterOrder() {
return 0;
}

/**
* @return 是否执行run方法 开启过滤
*/
@Override
public boolean shouldFilter() {
// /static/** ,/login , /register 不需要做登录检查,返回false
//1.获取request对象 , 获取请求中的uri
final HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
final String uri = request.getRequestURI();
if (uri.endsWith("/login") || uri.endsWith("/register")) {
return false;
}
//要做登录检查的返回true
return true;
}

/**
* @return 过滤逻辑
*/
@Override
public Object run() throws ZuulException {
//1.获取请求对象
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
//响应对象
HttpServletResponse response = RequestContext.getCurrentContext().getResponse();

//2.获取请求头中的 token
String token = request.getHeader("token");
if (StrUtil.isBlank(token)) {
Map<String, Object> map = new HashMap<>();
map.put("success", false);
map.put("message", "登录检查失败,请重新登录");
//中文编码
response.setContentType("application/json;charset=utf-8");
//把map转成json字符串,写到浏览器
final String jsonStr = JSONUtil.toJsonStr(map);
try {
response.getWriter().println(jsonStr);
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
} catch (IOException e) {
e.printStackTrace();
}
// 阻止filter继续往后执行
RequestContext.getCurrentContext().setSendZuulResponse(false);
}
return null;
}
}