Zuul 二
Zuul 一 主要是说了一些为什么要用Zuul的理由和使用Zuul的好处。现在说一下如何去实现这些好处。
pom.xml
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> <version>1.3.5.RELEASE</version> </dependency> <!--包含了Hystrix的依赖和Ribbon的依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId> <version>1.3.5.RELEASE</version> </dependency>
启动类:
package com.yjp.zuul; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; @EnableZuulProxy @SpringBootApplication public class ZuulApplication { public static void main(String[] args) { SpringApplication.run(ZuulApplication.class, args); } }
配置: 路由规则
server.port=8085 spring.application.name=zuul-service eureka.client.service-url.defaultZone=http://localhost:8761/eureka/ #配置路由规则 api-a-url可以随便定义 但是path和url要一样 zuul.routes.api-a-url.path=/api-a-url/** zuul.routes.api-a-url.url=http://localhost:8080/ #关闭Eureka完成负载均衡 zuul.routes.user-service.path=/user - service/** zuul.routes.user-service.serviceId=user - service #不使用Eureka注册中心 ribbon.eureka.enabled=false user-service.ribbon.listOfServers=http://localhost:8080/,http://localhost:8081/ #整合Eureka的路由规则 规则:zuul.routes.<route>.path与zuul.routes.<route> .serviceId zuul.routes.hello-service.path=/hello-service/** zuul.routes.hello-service.serviceId=hello-service # 整合Eureka的路由规则 简化配置 规则:zuul.routes.<serviceid>=<path> zuul.routes.hello-service=/hello-service/**
当我们访问api-a-url这个路径的时候就会对应到localhost:8080这个地址,然后对应**这个RequestMapper这个路径。
当使用path与url的映射关系来配置路由规则的时候, 对于路由转发的请求不会采用HystrixCommand来包装
当我们访问hello-service这个路径的时候就会自动去Eureka注册中心上面拉取地址信息,同样可以完成映射
当我们使用了Eureka注册中心的时候其实配置变得很简单,zuul会默认的帮我们创建路径
新的配置:
server.port=8085 spring.application.name=zuul-service eureka.client.service-url.defaultZone=http://localhost:8761/eureka/ eureka.instance.prefer-ip-address=true
这样就很方便的完成了配置。
当然也可以设置忽略路由规则。
比如, 设置为zuul.ignored-services=*的时候,Zuul将对所有的服务都不自动创建路由规则。那么就需要采用第一种配置。
zuul.ignored-services=microservice-provider-user
也可以这样只忽略单一的路径。
也可以自定义路由规则
package com.yjp.zuul; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; import org.springframework.cloud.netflix.zuul.filters.discovery.PatternServiceRouteMapper; import org.springframework.context.annotation.Bean; @EnableZuulProxy @SpringBootApplication public class ZuulApplication { public static void main(String[] args) { SpringApplication.run(ZuulApplication.class, args); } /** * 自定义路由规则 * 将userservice-vl 、 userservice-v2 的服务名映射为/vl/userservice/**的路由匹配规则 * * @return */ @Bean public PatternServiceRouteMapper serviceRouteMapper() { return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+$)" , "${version}/${name}"); } }
匹配符的含义
路由规则的匹配是有顺序的
举个栗子:
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.serviceid=user-service
zuul.routes.user-service-ext.path=/user-service/ext/**
zuul.routes.user-service-ext.serviceid= user-service-ext
调 用 user-service-ext的服务,本来是想调用第二个,但是会被第一个匹配上,那么就和我们的本意冲突了。匹配是按照顺序来的,匹配上了第一个就不会去匹配第二个了。如果我们是使用的yml的配置文件,那么只需要将两个路由规则换位置就可以。因为yml配置文件是有序的,但是如果是properties文件将会有问题,所以使用properties文件需要尽量避免这个问题。properties文件是无序的。
本地跳转:
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.url=http://localhost:8001/
zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.url=forward:/local
访问/api-b/**这个路径的时候就会默认去本地寻找,如果找不就会抛异常。在 API 网关上还需要增加一个/local/**的接口实现才能让 api-b 路由规则生效。
Cookie与头信息
SpringCloud Zuul在请求路由时, 会过滤掉HTTP请求头信息中的一些敏感信息, 防止它们被传递到下游的外部服务器。默认的敏感头信息通过zuul.sensitiveHeaders参数定义,包括Cookie、Set-Cookie、Authorization三个属性。所以, 我们在开发Web项目时常用的Cookie在 SpringCloud Zuul网关中默认是不会传递 的,这就会引发 一个常见的问题: 如果我们要将使用了Spring Security、 Shiro等安全框架构建的Web应用通过SpringCloud Zuul构建的 网关来进行路由时,由于Cookie信息无法传递, 我们的Web应用将无法实现登录和鉴权。
通过配置可以解决这个问题:
通过设置全局参数为空来覆盖默认值, 具体如下:zuul.sensitiveHeaders=
这种方法并不推荐, 虽然可以实现Cookie的传递, 但是破坏了默认设置的用意。在微服务架构的API网关之内, 对于无状态的RESTfulAPI请求肯定是要远多于这些Web类应用请求的, 甚至还有一些架构 设计会 将Web类应用和App客户端一样都归为API 网关之外的客户端应用。所有的配置都应该细化。
#方法一:对指定路由开启自定义敏感头zuul.routes..customSensitiveHeaders=true
#方法二:将指定路由的敏感头设置为空zuul.routes..sensitiveHeaders=
重定向问题:
虽然可以通过网关访问登录页面并发起登录请求, 但是登录成功之后, 我们跳转到的页面URL却是 具体Web应用实例的地址, 而不是通过网关的路由地址。
通过浏览器开发工具查看登录以及登录之后的请求详情, 可以发现, 引起问题的大致原因是由于SpringSecurity或Shiro在登录完成之后,通过重定向的方式跳转到登录后的页面,此时登录后的请求结果状态码为302, 请求响应头信息中的 Location指向了具体的服务实例地址, 而请求头信息中的Host也指向 了具体的服务实例 IP地址和端口。 所以, 该问题的根本原因在于Spring Cloud Zuul在路由请求时,并没有将最初的Host信息设置正确。
设置网关在进行路由转发前为请求设置Host头信息,以标识最初的服务端请求地址。具体配置方式如下:zuul.addHostHeader=true
Hystrix 和 Ribbon 支持 在Hystrix里面说的请求的合并,微服务聚合,以及异步都是可以实现的。
这一部分没什么说的,就一个重试机制,就是设置熔断的时间和超时重试的时间。
zuul.retryable=false
zuul.routes.<routes>.retryable=false
其中,zuul.retryable 用来全局关闭重试机制,而 zuul.routes.<routes>.retryable=false 则是指定路由关闭重试机制。
还有比较重要的一点就是zuul的请求回退的问题。
代码: 首先实现ZuulFallbackProvider接口 然后交给注入spring
package com.yjp.microservicegatewayzuul.back; import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider; 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; import java.nio.charset.Charset; @Component public class UserFallbackProvider implements ZuulFallbackProvider { @Override public String getRoute() { //需要回退的微服务 //调用microservice-provider-user路径出错的时候就会回滚 return "microservice-provider-user"; } @Override public ClientHttpResponse fallbackResponse() { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { //fallback时的状态码 return HttpStatus.OK; } @Override public int getRawStatusCode() throws IOException { //数字类型的状态码,这里返回200 正常 return 200; } @Override public String getStatusText() throws IOException { //状态文本,这里返回的是Ok return this.getStatusCode().getReasonPhrase(); } @Override public void close() { } @Override public InputStream getBody() throws IOException { //相应体 return new ByteArrayInputStream("用户微服务不可用".getBytes()); } @Override public HttpHeaders getHeaders() { //响应头 HttpHeaders httpHeaders = new HttpHeaders(); MediaType mt = new MediaType("application", "json", Charset.forName("utf-8")); httpHeaders.setContentType(mt); return httpHeaders; } }; } }
请求过滤:其实也就是拦截器 继承ZuulFilter 然后注入bean就可以了
package com.yjp.microservicegatewayzuul.zuul.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import javax.servlet.http.HttpServletRequest; public class PreRequestLogFilter extends ZuulFilter { @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 1; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); System.err.println("------> s"); return null; } }
启动类: 将拦截器注入就可以
@Bean public PreRequestLogFilter preRequestLogFilter() { return new PreRequestLogFilter(); }
拦截器好处:
它作为系统的统一入口, 屏蔽了系统内部各个微服务的细节。
它可以与服务治理框架结合,实现自动化的服务实例维护以及负载均衡的路由转发。
它可以实现接口权限校验与微服务业务逻辑的解耦。
通过服务网关中的过炖器, 在各生命周期中去校验请求的内容, 将原本在对外服务层做的校验前移, 保证了微服务的无状态性, 同时降低了微服务的测试难度, 让服务本身更集中关注业务逻辑的处理。
仔细说一下拦截器的用法:
filterType: 过滤器的类型, 它决定过滤器在请求的哪个生命周期中执行。 这里定义为pre, 代表会在请求被路由之前执行。
filterOrder: 过滤器的执行顺序。 当请求在一个阶段中存在多个过滤器时, 需要根据该方法返回的值来依次执行。
shouldFilter: 判断该过滤器是否需要被执行。 这里我们直接返回了true, 因此该过滤器对所有请求都会生效。 实际运用中我们可以利用该函数来指定过滤器的有效范围。
run: 过滤器的具体逻辑。 这里我们通过ctx.setSendZuulResponse(false)令zuul过滤该请求, 不对其进行路由, 然后通过 ctx.setResponseStatusCode(401)设置了其返回的错误码,当然也可以进 一步优化我们的返回 比如通过ctx.setResponseBody(body)对返回的body内容进行编辑等。
filterType()有4中类型,也就是有4种字符串。
pre: 可以在请求被路由之前调用。
routing: 在路由请求时被调用。
post: 在 routing 和 error 过滤器之后被调用。post最后发生
error: 处理请求时发生错误时被调用。
filterOrder: 通过 int 值来定义过滤器的执行顺序, 数值越小优先级越高。
run: 过滤器的具体逻辑。 在该函数中, 我们可以实现自定义的过滤逻辑, 来确定是否要拦截当前的请求, 不对其进行后续的路由, 或是在请求路由返回结果之后,对处理结果做一些加工等。
当请求无法通过的时候:下面的代码定义了 一 个简单 的 Zuul 过滤器, 它实现了在请求被路由之前检查HttpServletRequet中是否有 accessToken 参数, 若有就进行路由, 若没有就拒绝访问, 返回 401 Unauthorized 错误。
package com.yjp.zuul.demo; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import javax.servlet.http.HttpServletRequest; public class AccessFilter extends ZuulFilter { @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); Object accessToken = request.getParameter("accessToken"); if (null == accessToken) { //拒绝请求 ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(401); } return null; } }
异常处理:
当使用拦截器出现异常的时候,如何将异常反馈到页面上,如果仅仅通过throw new RuntimeException("Exist some errors ... ");
页面是没有任何显示的。
RequestContext ctx = RequestContext.getCurrentContext();
catch (Exception e) {
ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
ctx.set("error.exception", e);
}
必须在catch中为RequestContext中的error.status_code和error.exception才能将异常抛出到页面上面
我们没有捕获的异常,意外抛出的异常又会导致没有控制台输出也没有任何响应信息的情况出现,这个时候就可以去写一个error的拦截器
package com.yjp.zuul.demo; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import javax.servlet.http.HttpServletResponse; public class ErrorFilter extends ZuulFilter { @Override public String filterType() { return "error"; } @Override public int filterOrder() { return 10; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); Throwable throwable = ctx.getThrowable(); ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); ctx.set("error.exception", throwable.getCause()); return null; } }
这样就可以将这些异常也抛到页面上面。
这些很多都是书上的,还有一些优化的,大家有兴趣可以去看看。
努力吧,皮卡丘