zuul
Zuul 是 Netflix OSS 中的一员,是一个基于 JVM 路由和服务端的负载均衡器. 提供路由、监控、弹性、安全等方面的服务框架。Zuul 能够与 Eureka、Ribbon、Hystrix 等组件配合使用。 Zuul 的核心是过滤器,通过这些过滤器我们可以扩展出很多功能,比如:- 动态路由:
动态地将客户端的请求路由到后端不同的服务,做一些逻辑处理,比如聚合多个服务的数据返回 - 请求监控:
可以对整个系统的请求进行监控,记录详细的请求响应日志,可以实时统计出当前系统的访问量以及监控状态。 - 认证鉴权
对每一个访问的请求做认证,拒绝非法请求,保护好后端的服务 - 压力测试
通过 Zuul 可以动态地将请求转发到后端服务的集群中,还可以识别测试流量和真实流量,从而做一些特殊处理 - 灰度发布
灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度
使用(路由配置)
坐标
-------------------
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
yml
-------------------
zuul:
routes:
microservicecloud-dept:
path: /api/** #将服务名称换了一个名称,一个*代表一个层级,两个代表多个层级
prefix: /rest #配置路径前缀
ignored-services: microservicecloud-dept #上面配置了服务名称成了路径访问,但是服务名称依然可以访问,忽略服务名称访问,可通过配置 * 来表示所有的服务名称,防止服务过多,太臃肿
microservicecloud-dept是一个服务对外暴露的名称
上面的配置表示了通过微服务名称是不可以访问的,需要通过配置的/api/**代替
注意,此时网关这个服务想访问服务提供者者就不能使用服务名称去访问,而是 /api/** 接上其他的路径
通过zuul去访问就是ip:端口/api/**8 去访问才可以 换成服务名称是不行得,
注意,要是加了前缀,应该要在/api前面加上/rest
还有的一种配置的方式:
zuul:
routes:
mydept.serviceId: microservicecloud-dept #微服务名称
mydept.path: /myway/** 路径
这种方式配置也是可以的,mydept是自定义自己随便写的
还有
spring:
application:
name: springcloud_zuul_getway
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka
启动类
-------------------
@EnableZuulProxy
过滤器
Zuul 中的过滤器跟我们之前使用的 javax.servlet.Filter 不一样,javax.servlet.Filter 只有一种类型,可以通过配置 urlPatterns 来拦截对应的请求.而 Zuul 中的过滤器总共有 4 种类型,且每种类型都有对应的使用场景。- pre
可以在请求被路由之前调用。适用于身份认证的场景,认证通过后再继续执行下面的流程 - route
在路由请求时被调用。适用于灰度发布场景,在将要路由的时候可以做一些自定义的逻辑。 - post
在 route 和 error 过滤器之后被调用。这种过滤器将请求路由到达具体的服务之后执行。适用于需要添加响应头,记录响应日志等应用场景。 - error
处理请求时发生错误时被调用。在执行过程中发送错误时会进入 error 过滤器,可以用来统一记录错误信息
使用过滤器
创建一个 pre 过滤器,我们需要继承一个抽象类ZuulFilter ,需要覆写他的很多个方法filterType(): 过滤器的类型,就是上面所说的四种
filterOrder(): 过滤器的执行顺序,越小的越先执行
shouldFilter() :为false的话不会被转发到后端服务器
run(): 自己的过滤器逻辑处理
代码示例:
public class IpFilter extends ZuulFilter {
public IpFilter() {
super();
}
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
String remoteHost = request.getRemoteHost();
String remoteAddr = request.getRemoteAddr();
if ("127.0.0.1".equals(remoteHost)){
currentContext.setSendZuulResponse(false);
Result data = new Result();
data.setCode(401);
data.setMsg("非法请求");
currentContext.setResponseBody(JSON.toJSONString(data));
currentContext.getResponse().setContentType("application/json; charset=utf-8");
return null;
}
return null;
}
}
主要起作用的是这个currentContext.setSendZuulResponse(false);
他表示不需要将当前请求转发到后端的服务,所以直接就返回了
创建了这么一个类,我们还需要一个配置类
@Configuration
public class IpConfig {
@Bean
public IpFilter getIpConfig(){
return new IpFilter();
}
}
此时该过滤器就生效了,
,配置多个过滤器的方式也是一样的,但是现在问题来了,有多个过滤器,也有先后顺序,那么如何设置如果请求不满足前面的过滤器要求,就不会进入到后面的其他的过滤器逻辑中呢
首先我们要知道 shouldFilter()方法返回的布尔值决定了是否会进入自己的run方法逻辑中
而currentContext.setSendZuulResponse(false)则决定了是否会被转发到后端服务器进行
因此有多个过滤器时,要注意,最高优先级的过滤器的shouldFilter()方法我们直接返回true,保证可以进自己的run方法,去执行他的逻辑,
如果符合我们的逻辑我们放行,currentContext.setSendZuulResponse(true),如果不符合我们就拦截改为false,其实默认就是true,
那么后面的过滤器呢,他的shouldFilter()方法就是有点不一样了,我们需要通过
RequestContext currentContext = RequestContext.getCurrentContext(); 获取当前的上下文
return !currentContext.sendZuulResponse();
//注意,如果上个选择器设置了它fasle,也就是上个过滤器就不符合逻辑,被拦截了, 没有被拦截那就是true,会执行当前的run方法,以此类推。
注意:第一个拦截器要直接放回==返回true
禁用过滤器
当我们因为一些原因需要关闭某个过滤器功能时,我们可以这样;
在配置文件中
zuul.IpFilter.pre.disable=true #注意IpFilter是我们的过滤器的类名
过滤器之间的值传递
设置
currentContext.set(键,值);
取值
currentContext.get(键);
它的原理就是ThreadLocal
容错回退
Zuul 默认整合了 Hystrix,当后端服务异常时可以为 Zuul 添加回退功能,返回默认的数据。
实现回退机制需要实现 ZuulFallbackProvider 接口去覆写他的两个方法:
如:
@Component
public class MyZuulFallbackProvider implements ZuulFallbackProvider {
@Override
public String getRoute() {
return "*"; //表示对所有服务进行回退操作,单个的回退需要放回服务的名称,必须是已经注册到eureka的服务
}
@Override
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
//返回响应的状态码
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return this.getStatusCode().value();
}
@Override
public String getStatusText() throws IOException {
//返回响应状态码对应的文本
return this.getStatusCode().getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
//返回回退的内容
RequestContext currentContext = RequestContext.getCurrentContext();
Result result = new Result();
result.setMsg("服务异常");
result.setCode(500);
return new ByteArrayInputStream(JSON.toJSONString(result).getBytes());
}
@Override
public HttpHeaders getHeaders() {
//返回响应的请求头信息
HttpHeaders headers = new HttpHeaders();
MediaType mt = new MediaType("application", "json", Charset.forName("UTF-8"));
headers.setContentType(mt);
return headers;
}
};
}
}
当配置好后,我们先将注册中心和网关启动,服务提供者启动,先访问一次,通过网关去访问服务提供者的接口,是没有问题的,那么现在把服务提供方关了,然后在访问就会出现我们配好的回退信息
{"code":500,"msg":"服务异常"}
zuul自带了ribbon
现在我们启动两个服务提供者,然后的他们的接口是一样的路径也是一样的,
然后我们通过zuul去访问服务,这时就会发现它会轮询着去访问这两个服务的接口,默认是轮询
注意:如果两个服务提供者我先关了一个,然后去访问,依然是轮询,只不过访问到服务关闭了的会进行一个回退,返回我们上面写好的返回数据,如果zuul的配置文件里加了重试的配置的话,那么就有可能不是轮询的一种表现状态了,
此时有可能访问到宕机的服务,然后他会重试,再次访问,还可能在重试,
重试
zuul.retryable=true
高可用
routes 路由端点
当 @EnableZuulProxy 与 Spring Boot Actuator 配合使用时,Zuul 会暴露一个路由管理端点 /routes,我们可以借助这个端点可以查看和管理路由添加配置
management:
endpoints:
web:
exposure:
include: "*"
security:
enabled: false
访问端口:ip:端口/routes就可以了
filters 过滤器信息
/fliters 端点会返回 Zuul 中所有过滤器的信息ip:端口/filters
补充:
服务状态监听public class EurekaStateChangeListener {
@EventListener
public void listen(EurekaInstanceCanceledEvent event) {
System.err.println(event.getServerId() + "\t" + event.getAppName() + " 服务下线 ");
}
@EventListener
public void listen(EurekaInstanceRegisteredEvent event) {
InstanceInfo instanceInfo = event.getInstanceInfo();
System.err.println(instanceInfo.getAppName() + " 进行注册 ");
}
@EventListener
public void listen(EurekaInstanceRenewedEvent event) {
System.err.println(event.getServerId() + "\t" + event.getAppName() + " 服务进行续约 ");
}
@EventListener
public void listen(EurekaRegistryAvailableEvent event) {
System.err.println(" 注册中心启动 ");
}
@EventListener
public void listen(EurekaServerStartedEvent event) {
System.err.println("Eureka Server启动 ");
}
}