Feign是一个声明式的http客户端,使用Feign可以实现声明式REST调用,它的目的就是让Web Service调用更加简单。Feign整合了Ribbon和SpringMvc注解,这让Feign的客户端接口看起来就像一个Controller。Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息
。而Feign则会完全代理HTTP请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。同时Feign整合了Hystrix,可以很容易的实现服务熔断和降级。
创建一个springboot模块,并导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.lqsgroupId>
<artifactId>springcloud-entityartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
dependencies>
配置类如下
主配置类增加@EnableFeignClients
标签 , 其value属性可以指定Feign的客户端接口的包,当然也可以省略value属性
/`
* 支付的启动类
* @EnableFeignClients :开启Feign支持
*/
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(value="com.lqs.feignclient")
public class PayServerApp
{
public static void main( String[] args )
{
SpringApplication.run(PayServerApp.class);
}
}
yml配置如下
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
编写Feign的客户端接口
Feign的客户端接口是用来调用微服务的
@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路径要一致 , 参数要一致 , 返回值类型要一致。
编写Controller使用Feign接口
通过注入UserFeignClient ,直接发起调用
@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,我们除了导入依赖之外,需要主配置类通过@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已经集成了Ribbon,所以它的负载均衡配置基于Ribbon配置即可
user-server:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
如果在服务调用时出现了 “feign.RetryableException : Read timed out…”错误日志,说明Ribbon处理超时 ,我们可以配置Ribbon的超时时间:
ribbon:
ConnectTimeout: 3000
ReadTimeout: 6000
如果服务调用出现“com.netflix.hystrix.exception.HystrixRuntimeException:… timed - out and no fallback available” 错误日志,是因为Hystrix超时,默认Feign集成了Hystrix,但是高版本是关闭了Hystrix,我们可以配置Hystrix超时时间:
feign:
hystrix:
enabled: true #开启熔断支持
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 6000 #hystrix超时时间
有的时候我们需要看到Feign的调用过程中的参数及相应,我们可以对Feign的日志进行配置,Feign支持如下几种日志模式来决定日志记录内容多少:
创建Feign配置类
@Configuration
public class FeignConfiguration {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL; //打印Feign的所有日志
}
}
logging:
level:
com.lqs: debug
Hystrix是国外知名的视频网站Netflix所开源的非常流行的高可用架构框架。Hystrix能够完美的解决分布式系统架构中打造高可用服务面临的一系列技术难题,如雪崩。
Hystrix是处理依赖隔离的框架,将出现故障的服务通过熔断、降级等手段隔离开来
,这样不影响整个系统的主业务(比如你得了传染病是不是要把你关起来隔离呢),同时也是可以帮我们做服务的治理和监控
。
Hystrix的英文是豪猪,中文翻译为 熔断器,其思想来源于我们家里的保险开关,当家里出现短路,保险开关及时切掉电路,保证家里人员的安全,其目的就是起保护作用。
Hystrix其设计原则如下:
微服务系统中,Hystrix 能够帮助我们实现以下目标:
正常情况下,断路器处于关闭状态(Closed),如果调用持续出错或者超时达到设定阈值,电路被打开进入熔断状态(Open),这时请求这个服务会触发快速失败(立马返回兜底数据不要让线程死等),后续一段时间内的所有调用都会被拒绝(Fail Fast),一段时间以后(withCircuitBreakerSleepWindowInMilliseconds=5s),保护器会尝试进入半熔断状态(Half-Open),允许少量请求进来尝试,如果调用仍然失败,则回到熔断状态,如果调用成功,则回到电路闭合状态;
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
主配置类通过 @EnableCircuitBreaker
标签开启熔断功能
@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();
}
}
通过 @HystrixCommand
标签标记方法熔断,标签的fallbackMethod
属性指定兜底方法。那么当该方法在像远程服务发起调用出现异常,或是方法本身出现异常都会触发托底方法的执行,最终结果是托底方法的返回结果。
@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做统一配置,如
@RestController
@DefaultProperties(defaultFallback ="fallbackMethod") //统一降级配置
public class OrderController {
@HystrixCommand //方法熔断
@RequestMapping(value = "/order/{id}",method = RequestMethod.GET)
public User getById(@PathVariable("id")Long id)
//...省略...
通过feign.hystrix.enabled=true
开启Hystrix
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方式是可以打印出异常日志
@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);
}
托底实现类:
//让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调用失败就会以拖地方法返回的结果返回给用户
使用fallbackFactory属性,使用工厂方式指定托底
@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调用失败的异常信息,如下:
@Component
public class UserFeignClientFallback implements FallbackFactory<UserServerClient> {
@Override
public UserServerClient create(Throwable throwable) {
return id -> {
System.err.println(throwable.getMessage());
return new User(-1L, "返回数据错误!!!", "返回数据错误!!!", "请稍后再试-_-!!!");
};
}
}
试想一下如果我们有很多的微服务,他们都需要登录之后才能访问,那么我需要在每个微服务都去做一套登录检查逻辑,这样是不是会存在大量重复的代码和工作量,我们希望的是把登录检查这种公共的逻辑进行统一的抽取,只需要做一套检查逻辑即可,而zuul就可以用来干这类事情,我们可以把zuul看做是微服务的大门,所有的请求都需要通过zuul将请求分发到其他微服务,根据这一特性我们就可以在zuul做统一的登录检查,下游的微服务不再处理登录检查逻辑。
Zuul 是netflix开源的一个API Gateway 服务器, 本质上是一个web servlet(filter)应用。Zuul 在云平台上提供动态路由(请求分发),监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门,也要注册入Eureka,用一张图来理解zuul在架构中的的角色:
需要注意的是,zuul本身是一个独立的服务,默认集成了Ribbon,zuul通过Ribbon将客户端的请求分发到下游的微服务,所以zuul需要通过Eureka做服务发行,同时zuul也集成了Hystrix。
根据上图理解 ,我们需要建立独立的工程去搭建Zuul服务,同时需要把Zuul注册到EurekaServer,因为当请求过来时,zuul需要通过EurekaServer获取下游的微服务通信地址,使用Ribbon发起调用。
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-zuulartifactId>
dependency>
dependencies>
/**
* 用户的启动类
*
* @EnableEurekaClient: 标记该应用是 Eureka客户端
* @EnableZuulProxy : 开启zuul 可以看做是 @EnableZuulServer 的增强版 ,一般用这个
* @EnableZuulServer : 这个标签也可以开启zuul,但是这个标签开启的Filter更少
*/
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZuulServerApp {// 服务网关
public static void main(String[] args) {
SpringApplication.run(ZuulServerApp.class);
}
}
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之前我们是通过 http://localhost:8089/pay/1 来直接访问支付服务,现在需要通过zuul来访问,格式如下:http:// zuul的ip : zuul的port /zuul前缀 / 服务路径 /服务的controller路径 ,即:
http://localhost:8848/servers/pay/pay/1
Zuul提供了一个抽象的Filter:ZuulFilter我们可以通过该抽象类来自定义Filter,该Filter有四个核心方法,如下:
public abstract class ZuulFilter implements IZuulFilter{
abstract public String filterType();
abstract public int filterOrder();
//下面两个方法是 IZuulFilter 提供的
boolean shouldFilter();
Object run() throws ZuulException;
}
提示:
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;
}
}
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;
}
}