配套资料,免费下载
链接:https://pan.baidu.com/s/1la_3-HW-UvliDRJzfBcP_w
提取码:lxfx
复制这段内容后打开百度网盘手机App,操作更方便哦
通过前面内容的学习,我们已经可以基本搭建出一套简略版的微服务架构了,我们有注册中心Eureka,可以将服务注册到该注册中心中,我们有Ribbon或Feign或OpenFegin可以实现对服务负载均衡地调用,我们有Hystrix可以实现服务的熔断、降级以及限流以及Dashboard和Turbine来进行服务调用监控。
Zuul是Spring Cloud全家桶中的微服务API网关,所有从移动设备或网站来的请求都会先经过Zuul的API网关然后才能到达后端的Netflix应用程序,作为一个边界性质的应用程序,Zuul提供了动态路由、监控、弹性负载和安全控制等功能。那么Spring Cloud这个一站式的微服务开发框架基于Netflix Zuul实现了Spring Cloud Zuul,采用Spring Cloud Zuul即可实现一套API网关服务。
如果我们的微服务中有很多个独立服务都要对外提供服务,那么我们要如何去管理这些接口?特别是当项目非常庞大的情况下要如何管理?
在微服务中,一个独立的系统被拆分成了很多个独立的服务,为了确保安全,权限管理也是一个不可回避的问题,如果在每一个服务上都添加上相同的权限验证代码来确保系统不被非法访问,那么工作量也就太大了,而且维护也非常不方便。
为了解决上述问题,微服务架构中提出了API网关的概念,它就像一个安检站一样,所有外部的请求都需要经过它的调度与过滤,然后API网关来实现请求路由、负载均衡、权限验证等功能。
Zuul包含了对请求的路由和过滤两个最主要的功能:其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础,过滤功能则负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。
Zuul底层利用各种filter实现如下功能:
我们接下来的所有操作均是在Hystrix
最后完成的工程上进行操作,相关代码请到配套资料中寻找。
我们打开项目以后,需要对项目进行启动,启动的顺序如下:
注意:有时候你确信自己的代码没有问题,可是效果就是出不来,很有可能是idea编译的缓存的问题,我们目前使用的热部署还存在一些问题,要想解决,我建议你可以先停止对应工程,删除对应工程中的
target
目录,然后手动启动,这样还不行的话,大概率是你某地方配置错了,在检查一下吧^__^
。
(1)在父工程spring-cloud-study
下创建子工程gateway-zuul5001
(2)在pom.xml
中导入以下依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-zuulartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
dependencies>
(3)新建application.yaml
,在application.yaml
中加入以下配置
server:
port: 5001
spring:
application:
name: gateway-zuul5001
eureka:
client:
#是否将自己注册到注册中心,默认为 true
register-with-eureka: false
#表示 Eureka Client 间隔多久去服务器拉取注册信息,默认为 30 秒
registry-fetch-interval-seconds: 10
#设置服务注册中心地址
service-url:
defaultZone: http://root:123456@eureka-server7001.com:7001/eureka/,http://root:123456@eureka-server7002.com:7002/eureka/
(4)新建启动类,在启动类中加入以下代码
@SpringBootApplication
@EnableZuulProxy
public class GatewayZuul5001Application {
public static void main(String[] args) {
SpringApplication.run(GatewayZuul5001Application.class);
}
}
(5)启动当前项目,这样简单的Zuul就搭建好了,我们可以通过默认的路由规则来访问指定的服务方法,比如:
格式:“http://”+Zuul网关的域名+":"+Zuul网关的端口+"/"+微服务的名称(一定小写)+微服务的服务路径(就是你Controller方法上标注的那个路径)
例如:http://localhost:5001/service-consumer9002/consumer/product/findAll
例如:http://localhost:5001/service-consumer9003/consumer/product/findAll
现在,我们已经基本实现了Zuul默认路由的功能,但是,一般我们也可以自定义路由配置,为什么要自定义,细心的你会发现,现在你访问指定的服务,必须要加注册服务的名称(例如:service-consumer9002
,service-consumer9003
),这个名称可能很长,也可能会暴露你这个服务的一些性质,我们想要给这个服务起个别名,来代替当前这个默认的规则,就必须使用自定义路由配置,自定义路由配置其实很简单,只需要在gateway-zuul5001
中的application.yaml
加上相对应的路由配置就行了,具体代码如下所示:
zuul:
routes:
#这个属性Key可以随便写(一般来说就是注册服务的名称,属性spring.application.name)
SERVICE-CONSUMER9002:
#你要映射的路径地址(/**代表后边可以有多级路径,/*只有一级路径)
path: /consumer9002/**
#准备转给哪一个服务(不知道的,可以去Eureka注册中心找)
serviceId: SERVICE-CONSUMER9002
#这个属性Key可以随便写(一般来说就是注册服务的名称,属性spring.application.name)
SERVICE-CONSUMER9003:
#你要映射的路径地址(/**代表后边可以有多级路径,/*只有一级路径)
path: /consumer9003/**
#准备转给哪一个服务(不知道的,可以去Eureka注册中心找)
serviceId: SERVICE-CONSUMER9003
如果你还有更多服务你还可以照着上边的规则继续往下写(服务提供者和服务消费者都算服务),写完后,请重新启动当前的项目,然后依次访问如下地址测试:
地址1:http://localhost:5001/consumer9002/consumer/product/findAll
地址2:http://localhost:5001/consumer9003/consumer/product/findAll
虽然实现了自定义路由设置,但是如果你使用之前默认的路由规则,他还是可以访问的,我们想要禁用掉,默认的那个路由规则,只需要增加一段配置,如下:
zuul:
#加入这个配置,代表忽略所有服务,也就是忽略默认的路由规则,你也可以单独指定某个服务不能使用服务名来访问
#这个虽然是代表禁用掉所有,但是,自定义的路由规则还是会生效的,不必担心
ignored-services: '*'
routes:
#这个属性Key可以随便写(一般来说就是注册服务的名称,属性spring.application.name)
SERVICE-CONSUMER9002:
#你要映射的路径地址(/**代表后边可以有多级路径,/*只有一级路径)
path: /consumer9002/**
#准备转给哪一个服务(不知道的,可以去Eureka注册中心找)
serviceId: SERVICE-CONSUMER9002
#这个属性Key可以随便写(一般来说就是注册服务的名称,属性spring.application.name)
SERVICE-CONSUMER9003:
#你要映射的路径地址(/**代表后边可以有多级路径,/*只有一级路径)
path: /consumer9003/**
#准备转给哪一个服务(不知道的,可以去Eureka注册中心找)
serviceId: SERVICE-CONSUMER9003
然后重新启动项目即可,启动后,输入以下地址进行测试:
地址1:http://localhost:5001/service-consumer9002/consumer/product/findAll
地址2:http://localhost:5001/service-consumer9003/consumer/product/findAll
虽然自定义路由功能很强大,可以映射指定路径,但是你不觉得写起来太麻烦了吗?有没有一种简化的写法,那自然是有的,你现在可以把之前的那一大段配置注释掉了,我们来看看全新的精炼的配置到底怎么配,如下:
zuul:
ignored-services: '*'
routes:
#配置路由规则,key代表服务名称,value代表映射规则
SERVICE-CONSUMER9002: /consumer9002/**
SERVICE-CONSUMER9003: /consumer9003/**
写完后,请重新启动当前的项目,然后依次访问如下地址测试:
地址1:http://localhost:5001/consumer9002/consumer/product/findAll
地址2:http://localhost:5001/consumer9003/consumer/product/findAll
如果你想要在所有请求前边加一个统一前缀,比如:/api
,Zuul也支持这种设置,配置如下:
zuul:
#统一添加路由前缀
prefix: /api
ignored-services: '*'
routes:
#配置路由规则,key代表服务名称,value代表映射规则
SERVICE-CONSUMER9002: /consumer9002/**
SERVICE-CONSUMER9003: /consumer9003/**
写完后,请重新启动当前的项目,然后依次访问如下地址测试:
地址1:http://localhost:5001/api/consumer9002/consumer/product/findAll
地址2:http://localhost:5001/api/consumer9003/consumer/product/findAll
通配符 | 含义 | 举例 | 说明 |
---|---|---|---|
? | 匹配任意单个字符 | /api/consumer9003/? | /api/consumer9003/a /api/consumer9003/b /api/consumer9003/c 以上三个路由规则都可以被匹配到 |
* | 匹配任意多个字符 只能匹配一级地址 |
/api/consumer9003/* | /api/consumer9003/aaa /api/consumer9003/bbb /api/consumer9003/ccc 以上三个路由规则都可以被匹配到 /api/consumer9003/a/b/c 以上一个路由规则不可以被匹配到 |
** | 匹配任意多个字符 可以匹配多级地址 |
/api/consumer9003/** | /api/consumer9003/aaa /api/consumer9003/bbb /api/consumer9003/ccc 以上三个路由规则都可以被匹配到 /api/consumer9003/a/b/c 以上一个路由规则也可以被匹配到 |
过滤器 (filter) 是Zuul的核心组件,Zuul大部分功能都是通过过滤器来实现的。
Zuul中定义了4种标准过滤器类型,这些过滤器类型对应于请求的典型生命周期。
新建过滤器:com.caochenlei.filter.MyLogFilter
/**
* 这个过滤器专门用来记录日志的
*
* @author CaoChenLei
*/
//标注当前过滤器是一个组件,需要被Spring管理,必须有
@Component
public class MyLogFilter extends ZuulFilter {
//用来指定当前这个过滤器的执行类型,可以写字符串也可以写枚举值
@Override
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}
//用来指定当前这个过滤器的执行顺序,可以写数字也可以写枚举值
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER;
}
//用来指定当前这个过滤器是否执行,可以直接写死true/fasle,也可以自行判断
@Override
public boolean shouldFilter() {
return true;
}
//用来指定当前这个过滤器的执行逻辑,这个return null没有什么意义,但是不能省略
@Override
public Object run() throws ZuulException {
//获取当前请求的上下文对象
RequestContext currentContext = RequestContext.getCurrentContext();
//获取当前请求的请求对象
HttpServletRequest request = currentContext.getRequest();
//输出自定义的语句信息,也可以直接保存到数据库,这里只是测试
System.out.println("===============日志记录开始===============");
System.out.println("访问地址:" + request.getRequestURI());
System.out.println("===============日志记录结束===============");
return null;
}
}
编写好自己的过滤器以后,重新启动当前这个项目,然后访问:http://localhost:5001/api/consumer9002/consumer/product/findAll
我们想要禁用某一个过滤器,只需要按照配置规则进行配置就好了,这里我们就禁用掉自己编写的过滤器,配置如下:
zuul:
#统一添加路由前缀
prefix: /api
ignored-services: '*'
routes:
#配置路由规则,key代表服务名称,value代表映射规则
SERVICE-CONSUMER9002: /consumer9002/**
SERVICE-CONSUMER9003: /consumer9003/**
#禁用指定的过滤器
MyLogFilter:
route: #代表过滤器类型
disable: true
编写好自己的过滤器以后,重新启动当前这个项目,然后访问:http://localhost:5001/api/consumer9002/consumer/product/findAll
我们发现控制台并没有打印我们自己定义的日志信息。
Spring Cloud Zuul 对异常的处理是非常方便的,我们已经学过了在pre、routing、post的任意一个阶段如果抛异常了,则执行error过滤器,如果你想要统一处理Zuul内部出现的异常,Zuul内部帮我们处理了,但是它返回的是一个白页,我们也可以自己统一处理异常,我们只需要定义一个类型为error的过滤器替换掉默认的SendErrorFilter
就能处理异常了,在处理的时候,可以使用json来统一返回错误信息,这样我们就看不到Spring Boot默认的错误白页了。
(1)禁用Zuul自带的异常过滤器,配置如下:
zuul:
#统一添加路由前缀
prefix: /api
ignored-services: '*'
routes:
#配置路由规则,key代表服务名称,value代表映射规则
SERVICE-CONSUMER9002: /consumer9002/**
SERVICE-CONSUMER9003: /consumer9003/**
#禁用指定的过滤器
MyLogFilter:
route: #代表过滤器类型
disable: true
#禁用Zuul自带的异常过滤器
SendErrorFilter:
error: #代表过滤器类型
disable: true
(2)新建过滤器:com.caochenlei.filter.MyErrorFilter,然后替换掉自带的过滤器
@Component
public class MyErrorFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.ERROR_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.SEND_ERROR_FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
try {
RequestContext context = RequestContext.getCurrentContext();
ZuulException exception = (ZuulException) context.getThrowable();
HttpServletResponse response = context.getResponse();
response.setContentType("application/json; charset=utf8");
response.setStatus(exception.nStatusCode);
PrintWriter writer = response.getWriter();
writer.print("{\"code\":" + exception.nStatusCode + ",\"message\":\"" + exception.getMessage() + "\"}");
writer.close();
} catch (Exception e) {
e.printStackTrace(); }
return null;
}
}
(3)再次新建一个过滤器(com.caochenlei.filter.MyThrowExceptionFilter),这个过滤器不做别的事情,就是在run中抛出一个异常,看看,咱们定义的全局过滤器能不能拦截到他:
@Component
public class MyThrowExceptionFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
//模拟出现异常
int i = 1 / 0;
return null;
}
}
(4)编写好自己的过滤器以后,重新启动当前这个项目,然后访问:http://localhost:5001/api/consumer9002/consumer/product/findAll
(5)既然可以处理自己定义过滤器的异常,那我们就没必要在留着MyThrowExceptionFilter
过滤器了,将网关恢复正常,也就是禁止掉MyThrowExceptionFilter
,具体的配置如下:
zuul:
#统一添加路由前缀
prefix: /api
ignored-services: '*'
routes:
#配置路由规则,key代表服务名称,value代表映射规则
SERVICE-CONSUMER9002: /consumer9002/**
SERVICE-CONSUMER9003: /consumer9003/**
#禁用指定的过滤器
MyLogFilter:
route: #代表过滤器类型
disable: true
#禁用Zuul自带的异常过滤器
SendErrorFilter:
error: #代表过滤器类型
disable: true
#禁用掉自定义的异常过滤器
MyThrowExceptionFilter:
pre:
disable: true
(6)编写好自己的过滤器以后,重新启动当前这个项目,然后访问:http://localhost:5001/api/consumer9002/consumer/product/findAll
好了,到这里,过滤器这部分相信你已经学会了,开始学习其他部分了。
Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。但是所有的超时策略都是走的默认值,比如熔断超时时间只有1S,很容易就触发了。
因此建议我们手动进行配置:
zuul:
#开启重试功能
retryable: true
#统一添加路由前缀
prefix: /api
ignored-services: '*'
routes:
#配置路由规则,key代表服务名称,value代表映射规则
SERVICE-CONSUMER9002: /consumer9002/**
SERVICE-CONSUMER9003: /consumer9003/**
#禁用指定的过滤器
MyLogFilter:
route: #代表过滤器类型
disable: true
#禁用Zuul自带的异常过滤器
SendErrorFilter:
error: #代表过滤器类型
disable: true
#禁用掉自定义的异常过滤器
MyThrowExceptionFilter:
pre:
disable: true
ribbon:
ConnectTimeout: 500 # 连接超时时间(ms)
ReadTimeout: 2000 # 通信超时时间(ms),超时时长不能超过hystrix的熔断时长
OkToRetryOnAllOperations: true # 是否对所有操作重试
MaxAutoRetriesNextServer: 2 # 同一服务不同实例的重试次数
MaxAutoRetries: 1 # 同一实例的重试次数
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMillisecond: 6000 # 熔断超时时长:6000ms
ConnectTimeout(连接超时时长) + ReadTimeout(读取超时时长) 是2500,但是ribbon会进行一次重试,那么默认就是(ConnectTimeout+ReadTimeout) * 2 是真正的总超时时长,所以timeoutInMillisecond熔断的时长必须要超过这个时间,否则会报错或者把ribbon的MaxAutoRetries设为0,那么就不进行重试了。
Zuul本身就是一个代理服务,但如果被代理的服务突然断了,这个时候Zuul上面会有出错信息,例如,停止了被调用的微服务。一般服务方自己会进行服务的熔断降级,但对于Zuul本身,也应该进行Zuul的降级处理,我们需要有一个Zuul的降级,实现如下:
com.caochenlei.fallback.ZullFallback
@Component
public class ZullFallback implements FallbackProvider {
@Override
public String getRoute() {
//对所有服务都降级,这里可以写指定路由
return "*";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
//如果不知道该导入哪一个包,请参考配套代码...
return new ClientHttpResponse() {
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "text/html; charset=UTF-8");
return headers;
}
@Override
public InputStream getBody() throws IOException {
// 响应体
return new ByteArrayInputStream("服务正在维护,请稍后再试.".getBytes());
}
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.BAD_REQUEST;
}
@Override
public int getRawStatusCode() throws IOException {
return HttpStatus.BAD_REQUEST.value();
}
@Override
public String getStatusText() throws IOException {
return HttpStatus.BAD_REQUEST.getReasonPhrase();
}
@Override
public void close() {
//此处可以不用处理...
}
};
}
}
重新启动当前这个项目,然后我们关闭service-consumer9002
服务,然后访问:http://localhost:5001/api/consumer9002/consumer/product/findAll
默认情况下,所有的请求经过zuul网关的代理,默认会通过SpringMVC预先对请求进行处理缓存,普通请求并不会有什么影响,但是对于文件上传,就会造成不必要的网络负担,在高并发时,可能导致网络阻塞,Zuul网关不可用,这样我们的整个系统就瘫痪了。所以,我们上传文件的请求需要绕过请求的缓存,直接通过路由到达目标微服务,简单的意思就是在url前面加上 “/zuul” ,那么就会跳过缓存。如果,你上传文件的地址还用到了ribbon的负载均衡器,那么,你应该调大超时时间,否则也会出问题。
ribbon:
ConnectTimeout: 300
ReadTimeout: 60000
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMillisecond: 60000
我们需要在当前访问路径api前面加上/zuul,例如:http://localhost:5001/zuul/api/upload-service/upload/image
如果你使用了nginx对zuul网关进行了负载均衡,你可以对路径进行重写处理,我们需要修改到以/zuul为前缀,可以通过nginx的rewrite指令实现这一需求。
location /api/upload-service/ {
rewrite "^/(.*)$" /zuul/$1 ;
}