1.理解zuul
1.1.为什么要zuul
- 试想一下如果我们有很多的微服务,他们都需要登录之后才能访问,那么我需要在每个微服务都去做一套登录检查逻辑,这样是不是会存在大量重复的代码和工作量,我们希望的是把登录检查这种公共的逻辑进行统一的抽取,只需要做一套检查逻辑即可,而zuul就可以用来干这类事情,我们可以把zuul看做是微服务的大门,所有的请求都需要通过zuul将请求分发到其他微服务,根据这一特性我们就可以在zuul做统一的登录检查,下游的微服务不再处理登录检查逻辑。
1.2.什么是zuul
- Zuul 是netflix开源的一个API Gateway 服务器, 本质上是一个web servlet(filter)应用。
- Zuul 在云平台上提供动态路由(请求分发),监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门,也要注册入Eureka,用一张图来理解zuul在架构中的的工作流程:
- 需要注意的是,zuul本身是一个独立的服务,zuul是通过Ribbon实现请求的分发,默认集成了Ribbon,同时也集成了Hystrix,我们需要建立独立的工程区搭建Zuul服务,同时需要把Zuul注册到EurekaServer,因为当请求过来时,zuul需要通过EurekaServer获取下游的微服务通信地址,使用Ribbon发起调用。
2.zuul的搭建
2.1.导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud-parent</artifactId>
<groupId>cn.itsource.springboot</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>springcloud-zuul-server-1050</artifactId>
<name>springcloud-zuul-server-1050</name>
<dependencies>
<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-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
2.2.配置开开启Zuul
- 配置类通过 @EnableZuulProxy 注解开启zuul服务功能,同时该注解让zuul拥有了代理下游微服务的能力。
/**
* 用户的启动类
* @EnableEurekaClient: 标记该应用是 Eureka客户端
* @EnableZuulProxy : 开启zuul , 改标签让zuul成为后端微服务的代理服务。
* @EnableZuulServer :也是开启zuul但是他没有代理的功能 ,但是我们都用 @EnableZuulProxy
*/
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZuulServerApplicationConfig {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplicationConfig.class,args);
}
}
2.3.配置文件配置zuul
- 这里配置两个东西,一个是EurekaClien的配置,让zuul注册到EurekaServer,二个就是zuul的配置了.
#注册到EurekaServer
eureka:
client:
serviceUrl:
defaultZone: http://peer1:1010/eureka/,http://peer2:1011/eureka/,http://peer3:1012/eureka/
#使用ip地址进行注册
instance:
prefer-ip-address: true
#要指定服务的实例ID
instance-id: zuul-server:1050
server:
port: 1050
spring:
application:
name: zuul-server #服务名
zuul:
prefix: "/servers" #统一访问前缀
ignoredServices: "*" #禁用掉使用浏览器通过服务名的方式访问服务
routes:
pay-server: "/pay/**" #指定pay-server这个服务使用 /pay路径来访问 - 别名
order-server: "/order/**" #指定order-server这个服务使用 /order路径来访问
user-server: "/user/**" #指定user-server这个服务使用 /user路径来访问
提示: 我们对zuul主要做了三个配置
-
zuul.prefix : 作为统一的前缀,在浏览器访问的时候需要加上该前缀
-
zuul.ignoredServices : 忽略使用服务名方式访问服务,而是通过routes指定的路径进行访问
-
zuul.routes : 配置服务的访问路径
-
注意:在么有使用zuul之前我们是通过 http://localhost:1040/pay/1 来直接访问支付服务,现在需要通过zuul来访问,格式如下:http:// zuul的ip : zuul的port /zuul前缀 / 服务路径 /服务的controller路径 ,即:
-
http://localhost:1050/servers/pay/pay/1
特别说明:其实这里我们直接浏览器也能访问到目标服务,即可以通过:http://localhost:1040/pay/1
绕过zuul,但是这种情况不用担心,因为在产品上线的时候我们都是内网部署,只有zuul我们部署成外网,也就是说直接访问目标微服务的方式是访问不到的,所以我们只需要通过zuul访问即可。
2.4.zuul的超时配置
- Zuul集成了hystrix,如果服务的调用链过长,或者ribbon调用事件过长,可能会触发Hystrix的熔断机制,导致请求拿不到正常的结果,我们通常会对Ribbon和Hystrix的超时时间配置。如下配置对所有消费者微服务都有用:
- zuul配置文件加上如下配置:
ribbon:
MaxAutoRetries: 1 #最大重试次数,当Eureka中可以找到服务,但是服务连不上时将会重试
MaxAutoRetriesNextServer: 1 #切换实例的重试次数
OkToRetryOnAllOperations: false # 对所有的操作请求都进行重试,如果是get则可以,如果是post,put等操作没有实现幂等的情况下是很危险的,所以设置为false
ConnectTimeout: 3000 #请求连接的超时时间
ReadTimeout: 5000 #请求处理的超时时间
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 10000
#如果配置ribbon的重试,hystrix的超时时间要大于ribbon的超时时间,ribbon才会重试
#hystrix的超时时间=(1 + MaxAutoRetries + MaxAutoRetriesNextServer) * ReadTimeout 比较好,具体看需求
3.自定义zuul的Filter
3.1.zuul的执行流程
-
zuul的底层是通过各种Filter来实现的,zuul中的filter按照执行顺序分为了“pre”前置,”custom”自定义,“routing”路由,“post”后置,以及“error”异常Filter组成,当各种Filter出现了异常,请求会跳转到“error
filter”,然后再经过“post filter” 最后返回结果,下面是Filter的执行流程图:
-
正常流程:请求到达首先会经过pre类型过滤器,而后到达routing类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。
-
异常流程:
- 整个过程中,pre或者routing过滤器出现异常,都会直接进入error过滤器,再error处理完毕后,会将请求交给POST过滤器,最后返回给用户。
- 如果是error过滤器自己出现异常,最终也会进入POST过滤器,而后返回。
- 如果是POST过滤器出现异常,会跳转到error过滤器,但是与pre和routing不同的时,请求不会再到达POST过滤器了。
3.2.ZuulFilter
- 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;
}
提示:
- filterType :是用来指定filter的类型的(类型见常量类:FilterConstants)
- filterOrder :是filter的执行顺序,越小越先执行
- shouldFilter :是其父接口IZuulFilter的方法,用来决定run方法是否要被执行
- run :是其父接口IZuulFilter的方法,该方法是Filter的核心业务方法
3.2.自定义Filter
- 我们来演示一个案例,在Zuul层实现统一的登录检查:如果请求头中有“token”属性,我们就认为已经登录成功,可以继续往下游的服务执行,否则就视为请求未登录,直接返回错误信息,这一需求需要自定义Filter继承ZuulFilter类来实现,具体代码如下
@Component
public class LoginCheckFilter extends ZuulFilter {
private static final Logger log = LoggerFactory.getLogger(LoginCheckFilter.class);
//执行顺序
private static final int ORDER = 1;
//filter类型 : "pre"前置
@Override
public String filterType() {
return FilterConstants.PRE_TYPE; //pre
}
//执行顺序
@Override
public int filterOrder() {
return ORDER;
}
//返回结果决定 是否要执行run方法
@Override
public boolean shouldFilter() {
// /static/** ,/login , /register 不需要做登录检查,返回false
//1.获取request对象 , 获取请求中的url
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
String url = request.getRequestURI();
log.info("请求地址:"+url);
//2.判断是否包含在: static/** ,/login , /register
if(url.endsWith("/login ") || url.endsWith("/register ") || url.contains("/static/") ){
return false;
}
//要做登录检查的返回true
return true;
}
//核心业务方法 : 登录检查 , 如果请求头中有token,就是登录成功
@Override
public Object run() {
//1.获取请求对象
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
//响应对象
HttpServletResponse response = RequestContext.getCurrentContext().getResponse();
//2.获取请求头中的 token
String token = request.getHeader("token");
//3.如果没有token,登录检查失败 ,
if(!StringUtils.hasLength(token)){
//3.1.返回登录检查失败的错误信息 :{ success:false, message:"登录检查失败,请重新登录"}
Map<String,Object> resultMap = new HashMap<>();
resultMap.put("success" , false);
resultMap.put("message" , "登录检查失败,请重新登录");
//中文编码
response.setContentType("application/json;charset=utf-8");
//把map转成json字符串,写到浏览器
String resultJsonString = JSON.toJSONString(resultMap);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
try {
response.getWriter().print(resultJsonString);
} catch (IOException e) {
e.printStackTrace();
}
//3.2.阻止filter继续往后执行
RequestContext.getCurrentContext().setSendZuulResponse(false);
}
//这里的返回结果没有任何意义,不用管
return null;
}
}
提示:
- 在 filterType方法中我们返回“pre”前置filter的常量,让他成为前置filter(登录检查需要在请求的最前面来做)
- 在filterOrder方法中返回的顺序值是 1 ,执行顺序越小越先执行
- 在shouldFilter方法中通过判断请求的url来决定是否需要做登录检查,返回true就是要做然后才会执行run方法
- 在run方法中我们通过获取请求头中的token判断是否登录,如果没登录就返回错误信息,阻止继续执行。
- RequestContext.getCurrentContext() 是一个Zuul提供的请求上下文对象
注意:在返回JSON格式的错误信息时我用到了fastjson,需要在zuul工程中导入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.50</version>
</dependency>