一. 综述
Spring Cloud 集群提供了多个组件,用于进行集群内部的通信,例如服务管理组件Eureka,负载均衡组件Ribbon。如果集群提供了API或者Web服务,需要与外部进行通信,比较好的方式是添加一个网关,将集群的服务都隐藏到网关后面。这种做法对于外部客户端来说,无须关心集群的内部结构,只需关心网关的配置等信息。对于Spring Cloud集群来说,不必过多暴露服务,提升了集群的安全性。Zuul是Ntlix的一个子项目,Spring Cloud将Zuul进行了进一步的实现与封装,将其整合到spring-netflix项目中,为微服务集群提供代理、过滤、路由等功能。
二. 运行机制
当我们使用了@EnableZuulProxy
注解。开启该注解之后,在Spring容器初始化的时候,会将Zuul的相关配置初始化,其中包含一个Spring Boot的Bean:ServletRegistrationBean
,该类主要用于注册Servlet。Zuul提供了一个ZuulServlet
类,在Servlet的service 方法中,执行各种Zuul过滤器(ZuulFilter
)。如图所示是Http请求在ZuulServlet
中的生命周期。
ZuulServlet
的service方法接收到请求后,会执行pre
阶段的过滤器,再执行routing
阶段的过滤器,最后执行post
阶段的过滤器。其中routing
阶段的过滤器会将请求转发到“源服务”,源服务可以是第三方的Web服务,也可以是Spring Cloud
的集群服务。在执行pre和routing
阶段的过滤器时,如果出现异常,则会执行error
过滤器。整个过程的HTTP
请求、HTTP
响应、状态等数据,都会被封装到一个RequestionContext
对象中。如图是各阶段的一些具体过滤器。
旁边的数字表示他们的执行顺序,越小优先级越高。
三. 代码实例
SpringCloud版本:Greenwich.RELEASE
SpringBoot版本:2.1.4.RELEASE
-
新建zuul模块引入依赖
<!-- zuul --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <!-- Eureka客户端 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> <!-- 这里会自动引入版本,类似parent标签继承 --> </dependency> <dependencyManagement> <dependencies> <!-- <scope>import</scope>解决单继承问题,类似parent标签, --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Greenwich.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
-
在主启动类上标记
@EnableZuulProxy
注解,前面已经讲了该注解的作用 -
如果网关作为Eureka客户端注册到Eureka服务器时,可以通过ServiceId将请求转发到集群的服务中。那就需要在配置文件中配置Eureka的地址(这个
@EnableEurekaClient
可加可不加,@EnableZuulProxy
默认开启了)# 注册到Eureka中 eureka: instance: instance-id: ${info.application.name} client: service-url: defaultZone: http://localhost:7000/eureka/,http://feiyu:7001/eureka/
-
配置文件设置路由转发规则
# 网关转发配置 zuul: routes: # 提供映射规则 fei: # map的key值,任意即可 path: /fei/** # 映射路径 serviceId: springcloud-employee-client-feign # 服务名 yu: path: /yu/** # 映射路径 serviceId: springcloud-employee-provider-feign # 服务名 ignored-services: "*" # 忽略所有的服务,即不能使用服务id访问
也可以简写成:(serviceId省略,使用routeId代替)
# 网关转发配置 zuul: routes: springcloud-employee-client-feign: # serviceId省略,使用routeId代替 path: /fei/** # 映射路径 springcloud-employee-provider-feign: # serviceId省略,使用routeId代替 path: /yu/** # 映射路径 ignored-services: "*" # 忽略所有的服务,即不能使用服务id访问
-
配置完成,启动项目,使用
http://localhost:5000/fei/dept/list
地址可以访问到springcloud-employee-client-feign
这个服务的dept/list
接口
注意:默认集成了Ribbon,实现了服务访问的负载均衡。
四. Zuul进阶
4.1 Zuul与Hystrix
当我们对网关进行配置让其调用集群的服务时,将会执行Ribbon
路由过滤器(RibbonRoutingFilter
)。该过滤器在进行转发时会封装成一个Hystrix
命令予以执行。换言之,它具有容错的功能。如果 “源服务” 出现问题(例如超时),那么所执行的Hystrix
命令将会触发回退。
我们可以在配置文件中设置Ribbon调用的超时时间。
ribbon:
readTimeout: 2000
connectTimeout: 2000
还可以自定义一个回退处理类,只需实现FallbackProvider
接口即可(代码上都有详细注释)
package com.fei.springcloudzuul5000.hystrix;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* @Author: xiaoshijiu
* @Date: 2019/7/13
* @Description: Zuul中使用Hystrix处理回退
* @Component需要将其加到IOC容器中
*/
@Component
public class MyFallBackProvider implements FallbackProvider {
/**
* 返回路由的名称,表示对哪个路由处理,”*“表示对所有服务处理
*/
@Override
public String getRoute() {
return "*";
}
/**
* 具体回退返回逻辑,一个Http响应
*/
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse() {
/**
* 响应头
*/
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.TEXT_PLAIN);
return httpHeaders;
}
/**
* 响应体
*/
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("当前服务不可用,请稍后再试".getBytes());
}
/**
* 响应码,状态码
*/
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
/**
* 数字类型的状态码
*/
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
/**
* 响应状态文本
*/
@Override
public String getStatusText() throws IOException {
return "OK";
}
/**
* 用于释放资源
*/
@Override
public void close() {
}
};
}
}
当 ”源服务“ 出现问题,就会执行这里定义的回退逻辑,返回已经定义好的结果。
4.2 自定义Zuul过滤器
自定义Zuul过滤器也比较容易,只需继承ZuulFilter
类即可
package com.fei.springcloudzuul5000.filter;
import com.fei.common.log.Loggable;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import javax.servlet.RequestDispatcher;
/**
* @Author: xiaoshijiu
* @Date: 2019/7/13
* @Description: 自定义Zuul过滤器
* @Component: 需要让Spring知道他的存在,即加入IOC容器中
*/
@Component
public class MyTestFilter extends ZuulFilter implements Loggable {
/**
* 判断是否需要执行error过滤器的条件常量
*/
private static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran";
/**
* 过滤器类型
* @pre: 请求过来就执行,最先
* @routh:路由阶段,决定如何进行路由
* @post:路由之后执行的过滤器
* @error:发生异常时执行的过滤器
*/
@Override
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}
/**
* 执行顺序,数字越小表示执行顺序越靠前
* @这里设置为1,在routh中是最小的了
*/
@Override
public int filterOrder() {
return 1;
}
/**
* 是否执行
*/
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
String uri = ctx.getRequest().getRequestURI();
// 这里指定一下具体执行的路径
if (uri.indexOf("/static") != -1) {
return true;
}
return false;
}
/**
* 具体处理逻辑
*/
@Override
public Object run() throws ZuulException {
getLog().warn("用于测试的zuul过滤器来了!!");
RequestContext ctx = RequestContext.getCurrentContext();
RequestDispatcher dispatcher = ctx.getRequest().getRequestDispatcher("/static");
if (dispatcher != null) {
// 设置为true,表示不会再去执行error过滤器了
ctx.set(SEND_ERROR_FILTER_RAN, true);
// 设置为false,表示不会再去执行其他过滤器了
ctx.setSendZuulResponse(false);
try {
// 转发
dispatcher.forward(ctx.getRequest(), ctx.getResponse());
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}
每个方法代表什么意思,上面都有详细注解说明。
该过滤器的作用就是:拦截带有/static
的请求,将其转发到已经定义好的/static
的静态页面(写得可能比较粗糙!!)
多看看源码就都懂了。