8-1. Spring Cloud 的zuul是什么?
通过前面的学习,我们已经基本搭建出一套简略版的微服务架构了,我们有注册中心Eureka,可以将服务注册到注册中心去,我们有Ribbon或Feign可以实现对服务的负载均衡调用,我们有Hystrix可以实现服务的熔断,但是我们还缺少什么呢?
我们首先来看一个微服务架构图:
在上面的架构图中,我们的服务包括:内部服务ServiceA和内部服务ServiceB,这两个服务都是集群部署的,每个服务部署了三个实例,他们都会通过Eureka Server注册中心注册与订阅服务,而 Open Service是一个对外的服务,也是集群部署,外部调用方通过负载均衡设备调用Open Service服务,比如负载均衡使用Nginx,这样的实现是否合理,或者是否有更好的实现方式呢?接下来我们围绕这个问题展开讨论。
-
如果我们的微服务中有很多个独立服务要对外提供服务,那么我们要如何去管理这些接口?特别是当项目非常庞大的情况下要如何管理?
-
在微服务中,一个独立的系统被拆分成很多个独立的服务,为了确保安全,权限管理也是一个不可避免的问题,如果在每一个服务都添加上相同的权限验证代码来确保系统不被访问,那么工作量太大了,而且维护起来也非常不方便。
为了解决上述的这些问题,微服务架构中就提出了API网关的概念,它就像一个安检站一样,所有外部的请求都需要它的调度与过滤,然后API网关来实现请求路由、负载均衡、权限验证等功能。
那么Spring Cloud这个一站式的微服务开发框架基于Netflix Zuul实现了Spring Cloud Zuul ,采用Spring Cloud Zuul 即可实现一套API网关服务。
8-2.使用Zuul构建API网关
-
创建一个普通的Spring Boot工程 ,然后添加相关依赖。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!-- 指定Spring Cloud的版本 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <!--Spring Cloud 的仓库 --> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories>
-
在入口类上添加@EnableZuulProxy注解,开启Zuul的API网关服务功能:
@EnableZuulProxy // 开启zuul的API网关功能 @SpringBootApplication public class Application{ public static void main(String[] args){ SpringApplication.run(Application.class,args); } }
-
在application.properties文件中配置路由规则:
#配置服务内嵌的端口 server.port=8080 #配置服务器的名称 spring.application.name=springcloud-api-gateway #配置路由规则 zuul.routes.api.path=/api/** zuul.routes.api.serviceId=springcloud-service-feign #配置API网关注册中心,API网关也将作为一个服务注册到Eureka上 eureka.client.service-url.defaultZone=http://eureka1:8766/eureka/,http://eureka2:8767/eureka/
以上配置,我们的路由规则就是匹配/api/**的请求,只要路径带有/api/ 都将被转发到springcloud-service-feign服务上,至于springcloud-service-feign服务的地址到底是什么则由eureka-server注册中心去分析,我们只需要写上服务名即可
以我们目前搭建的项目为例,请求http://localhost:8080/api/web/hello接口则相当于请求http://localhost:8082/web/hello(springcloud-service-feign的服务地址为http://localhost:8082/web/hello),路由规则中配置的api是路由的名字,可以任意定义,但是一组path和serviceId映射关系的路由名要相同。
如果以上测试成功,则表示我们的API网关服务已经构建成功了,我们发送的符合路由规则的请求将自动被转发到相应的服务上去处理。
8-3. 使用Zuul进行请求过滤
我们知道Spring Cloud Zuul 就像一个安检站,所有的请求都会经过这个安检站,所以我们可以在该安检站内实现请求的过滤,下面我们以一个权限验证案例说明这一点:
-
我们定义一个过滤器并集成自ZuulFilter作为一个Bean:
@Component public class AuthFilter extends ZullFilter{ @Override public String filterType(){ return "pre"; } @Override public int filterOrder(){ return 0; } @Override public boolean shouldFilter(){ return true; } @Override public Object run() throws ZullException(){ RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); String token = request.getParameter("token"); if(token == null){ ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(401); ctx.setZullResponseHeader("content-type","text/html;charset=utf-8"); ctx.setResponseBody("非法访问"); } return null; } }
-
filterType方法的返回值为过滤器的类型,过滤器的类型决定了过滤器在哪个声明周期执行,pre表示在路由之前执行过滤器,其他值还有post、error、route和static,当然也可以自定义。
-
filterOrder方法表示过滤器的执行顺序,当过滤器较多时,我们可以通过该方法的返回值来指定过滤器的执行顺序。
-
shouldFilter方法用来判断过滤器是否执行,true代表执行,false表示不执行。
-
run方法表示过滤的具体逻辑,如果请求地址中携带了token参数的话,则认为是合法请求,否则为非法请求,如果是非法请求的话,首先设置 ctx.setSendZuulResponse(false);表示不对该请求进行路由,然后设置响应码和响应值。这个run方法的返回值暂时没有任何意义,可以任意返回值。
通过http://localhost:8080/api/web/hello 地址访问,就会被过滤器过滤。
-
8-4. Zuul的路由规则
-
在前面的例子中
#配置路由规则 zuul.routes.api.path=/api/** zuul.routes.api.serviceId=springcloud-service-feign
当访问地址符合/api/**规则的时候,会被自动定位到springcloud-service-feign服务上,不过两行代码有点麻烦可以简化为
zuul.routes.springcloud-service-feign=/api/**
zuul.routes后面跟着的是服务名,服务名后面跟着的是路由规则,这种配置方法更简单。
-
如果映射规则我们不写,系统也会给我们提供一套默认的配置规则,如下:
#默认的规则 zuul.routes.springcloud-service-feign.path=/springcloud-service-feign/** zuul.routes.springcloud-service-feign.serviceId=springcloud-service-feign
-
默认情况下,Eureka上所有注册的服务都会被Zuul创建映射关系来进行路由,但是对于我们下面的例子来说,我们希望springcloud-service-feign提供服务,而springcloud-service-provider作为服务提供者只对服务消费者提供服务,不对外提供服务。
如果使用默认的路由规则,则zuul也会自动为springcloud-service-provider创建映射规则,这个时候我们可以采用如下的方式来让zuul跳过springcloud-service-provider服务,不为其创建路由规则:
#忽略某些服务提供者的规则
zuul.ignored-services=springcloud-service-provider,springcloud-service-consumer
不给某个服务设置规则,这个配置我们可以进一步细化,比如说我们不想给/hello接口路由,那我们可以按如下配置:
#忽略掉某一些接口路径
zuul.ignored-patterns=/**/hello/**
此外,我们也可以统一的为路由规则加前缀,设置方法如下:
#配置服务网关的前缀
zuul.prefix=/myapi
此时我们的访问路径就编程了 http://localhost:8080/myapi/api/web/hello
-
路由规则通配符的定义
通配符 含义 举例 说明 ? 匹配任意单字符 /springcloud-service-feign/? 匹配
/springcloud-service-feign/a
,/springcloud-service-feign/b
,/springcloud-service-feign/c等* 匹配任意的字符 /springcloud-service-feign/* 匹配
/springcloud-service-feign/aaa,
/springcloud-service-feign/bbb,
/springcloud-service-feign/ccc等
无法匹配
/springcloud-service-feign/a/b/c** 匹配任意数量的字符 /springcloud-service-feign/** 匹配
/springcloud-service-feign/aaa,
/springcloud-service-feign/bbb,
/springcloud-service-feign/ccc
也可以匹配
/springcloud-service-feign/a/b/c -
一般情况下API网关只是作为各个微服务的统一入口,但是有时候我们可能也需要在API网关服务上做一些特殊的业务逻辑处理,那么我们可以让请求到达API网关后,再转发给自己本身,由API网关自己处理,那么我们可以进行如下的操作:
在springcloud-api-gateway项目中创建一个controller
@RestController public class GateWayController{ @RequestMapping("/api/local") public String hello(){ return "this is gateway"; } }
然后在application.properties配置文件中配置
zuul.routes.gateway.path=/gateway/** zuul.routes.gateway.url=forward:/api/local
8-5.Zuul的异常处理
Spring Cloud Zuul对异常的处理是非常方便的,但是由于Spring Cloud处于迅速发展中,各个版本之间存在差异,本案例以Finchley.RELEASE版本为例,来说明Spring Cloud Zuul中的异常处理问题。
首先我们来看一张官方给的Zuul请求的生命周期图:
- 正常情况下所有的请求都是按照pre、route、post的顺序来执行的,然后post返回response
- 在pre阶段,如果有自定义的过滤器则执行自定义的过滤器
- pro、routing、post的任意阶段如果抛出了异常了,则执行error过滤器
我们可以有两种方式统一处理异常:
-
禁用zuul默认的异常处理SendErrorFilter过滤器,然后自定义我们自己的ErrorFilter过滤器
zuul.SendErrorFilter.error.disable=true
@Component public class ErrorFilter extends ZullFilter{ private static final Logger logger=LoggerFactory.getLogger(ErrorFilter.class); @Override public String filterType(){ return "error"; } @Override public int filterOrder(){ return 1; } @Override public boolean shouldFilter(){ return true; } @Override public Object run() throws ZuulException(){ try{ RequestContext ctx = RequestContext.getCurrentContext(); ZuulException exception = (ZuulException)ctx.getThrowable(); logger.error("进入系统异常拦截",exception); HttpServletResponse response=ctx.getResponse(); response.setContentType("application/json;charset=utf8"); response.setStatus(exception.nStatusCode); PrintWriter writer=null; try{ writer = response.getWriter(); writer.print("{code:"+exception.nStatusCode+",message:\""+exception.getMessage()+"\"}"); }catch(IOException e){ e.printStackTrace(); }finally{ if(writer!=null){ writer.close(); } } }catch(Exception ee){ ReflectionUtils.rethrowRuntimeException(ee); } } return null; } }
-
自定义全局error错误页面
@RestController public class ErrorHandlerController implements ErrorController{ /** *出现异常后进入该方法,交由下面的方法处理 */ @Override public String getErrorPath(){ return "error"; } @RequestMapping("/error") public Object error(){ RequestContext ctx = RequestContext.getCurrentContext(); ZuulException exception = (ZuulException)ctx.getThrowable(); return exception.nStatusCode +"--"+exception. } }