Spring Cloud学习5
8 API网关Zuul
8.1 Spring Cloud的Zuul简介
通过前面内容的学习,可以基本搭建出一套简略版的微服务架构,有注册中心Eureka,可以将服务注册到该注册中心中,有Ribbon或Feign可以实现对服务负载均衡地调用,有Hystrix可以实现服务的熔断,但是缺少什么呢?首先来看一个微服务架构图:
在上面的架构图中,服务包括:内部服务Service A和内部服务Service B,这两个服务都是集群部署,每个服务部署了3个实例,他们都会通过Eureka Server注册中心注册与订阅服务,而Open Service是一个对外的服务,也是集群部署,外部调用方通过负载均衡设备调用Open Service服务,比如负载均衡使用Nginx,这样的实现是否合理,或者是否有更好的实现方式,围绕该问题展开讨论。
1、如果微服务中有很多个独立服务都要对外提供服务,那么要如何去管理这些接口?特别是当项目非常庞大的情况下要如何管理?
2、在微服务中,一个独立的系统被拆分成了很多个独立的服务,为了确保安全,权限管理也是一个不可回避的问题,如果在每一个服务上都添加上相同的权限验证代码来确保系统不被非法访问,那么工作量也就太大了,而且维护也非常不方便。
为了解决上述问题,微服务架构中提出了API网关的概念,它就像一个安检站一样,所有外部的请求都需要经过它的调度与过滤,然后API网关来实现请求路由、负载均衡、权限验证等功能;那么SpringCloud这个一站式的微服务开发框架基于NetflixZuul实现了SpringCloudZuul,采用SpringCloudZuul即可实现一套API网关服务。
8.2 使用Zuul构建API网关
1、创建一个普通的Spring Boot工程名为06-springcloud-api-gateway,然后添加相关依赖,这里主要添加两个依赖zuul和eureka依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.chengzi.springcloud</groupId>
<artifactId>06-springcloud-api-gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>06-springcloud-api-gateway</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--添加spring cloud的zuul的起步依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency><!--添加spring cloud的eureka的客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<!--添加spring的仓库因为maven中央仓库没有 springcloud依赖-->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>
2、在入口类上添加@EnableZuulProxy注解,开启Zuul的API网关服务功能:
@SpringBootApplication
@EnableZuulProxy //开启Zuul的API网关服务功能
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3、在application.properties文件中配置路由规则:
#配置服务内嵌的Tomcat端口
server.port=8080
#配置服务的名称
spring.application.name=06-springcloud-api-gateway
#配置路由规则
zuul.routes.api-wkcto.path=/api-wkcto/**
zuul.routes.api-wkcto.serviceId=05-springcloud-service-feign
#配置API网关到注册中心上,API网关也将作为一个服务注册到
eureka-server上eureka.client.service-url.defaultZone=http://eureka8761:8761/eureka/,http://eureka8762:8762/eureka
以上配置,路由规则就是匹配所有符合/api-wkcto/**的请求,只要路径中带有/api-wkcto/都将被转发到05-springcloud-service-feign服务上,至于05-springcloud-service-feign服务的地址到底是什么则由eureka-server注册中心去分析,只需要写上服务名即可。
以目前搭建的项目为例,请求http://localhost:8080/api-wkcto/web/hello接口则相当于请求http://localhost:8082/web/hello(05-springcloud-service-feign服务的地址为http://localhost:8082/web/hello),路由规则中配置的api-wkcto是路由的名字,可以任意定义,但是一组path和serviceId映射关系的路由名要相同。如果以上测试成功,则表示API网关服务已经构建成功了,发送的符合路由规则的请求将自动被转发到相应的服务上去处理。
8.3 使用Zuul进行请求过滤
SpringcloudZuul就像一个安检站,所有请求都会经过这个安检站,所以可以在该安检站内实现对请求的过滤,下面以一个权限验证案例说这一点:
1、定义一个过滤器类并继承自ZuulFilter,并将该Filter作为一个Bean
@Component
public class AuthFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String token = request.getParameter("token");
if (token == null) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.addZuulResponseHeader("content-type", "text/html;charset=utf-8");
ctx.setResponseBody("非法访问");
}
return null;
}
}
1)filterType方法的返回值为过滤器的类型,过滤器的类型决定了过滤器在哪个生命周期执行,pre表示在路由之前执行过滤器,其他值还有post、error、route和static,当然也可以自定义。
2)filterOrder方法表示过滤器的执行顺序,当过滤器很多时,我们可以通过该方法的返回值来指定过滤器的执行顺序。
3)shouldFilter方法用来判断过滤器是否执行,true表示执行,false表示不执行。
4)run方法则表示过滤的具体逻辑,如果请求地址中携带了token参数的话,则认为是合法请求,否则为非法请求,如果是非法请求的话,首先设置ctx.setSendZuulResponse(false);表示不对该请求进行路由,然后设置响应码和响应值。这个run方法的返回值目前暂时没有任何意义,可以返回任意值。
2、通过http://localhost:8080/api-wkcto/web/hello地址访问,就会被过滤器过滤。
8.4 Zuul的路由规则
(1)在前面的例子中:
#配置路由规则
zuul.routes.api-hello.path=/api-wkcto/**
zuul.routes.api-hello.serviceId=05-springcloud-service-feign
当访问地址符合/api-wkcto/**规则的时候,会被自动定位到05-springcloud-service-feign服务上,不过两行代码有点麻烦,还可以简化为:
zuul.routes.05-springcloud-service-feign=/api-hello/**zuul.routes
后面跟着的是服务名,服务名后面跟着的是路径规则,这种配置方式更简单。
2)如果映射规则我们什么都不写,系统也给我们提供了一套默认的配置规则默认的配置规则如下:
#默认的规则
zuul.routes.05-springcloud-service-feign.path=/05-springcloud-service-feign/**
zuul.routes.05-springcloud-service-feign.serviceId=05-springcloud-service-feign
(3) 默认情况下,Eureka上所有注册的服务都会被Zuul创建映射关系来进行路由。但是对于这里的例子来说,希望:05-springcloud-service-feign提供服务;而01-springcloud-service-provider作为服务提供者只对服务消费者提供服务,不对外提供服务。如果使用默认的路由规则,则Zuul也会自动为01-springcloud-service-provider创建映射规则,这个时候可以采用如下方式来让Zuul跳过01-springcloud-service-provider服务,不为其创建路由规则:
#忽略掉服务提供者的默认规则
zuul.ignored-services=01-springcloud-service-provider
不给某个服务设置映射规则,这个配置可以进一步细化,比如说我不想给/hello接口路由,那我们可以按如下方式配置:
#忽略掉某一些接口路径
zuul.ignored-patterns=/**/hello/**
此外,我们也可以统一的为路由规则增加前缀,设置方式如下:
#配置网关路由的前缀
zuul.prefix=/myapi
此时访问路径就变成了http://localhost:8080/myapi/web/hello
4) 路由规则通配符的含义:
通配符含义举例说明
含义符 | 含义 | 举例 | 说明 |
---|---|---|---|
? | 匹配任意单个字符 | /05-springcloud-service-feign/? | 匹配 //05-springcloud-service-feign/a/ /05-springcloud-service-feign/b /05-springcloud-service-feign/c等 |
* | 匹配任意数量的字符 | /05-springcloud-service-feign/* | 匹配 //05-springcloud-service-feign/aaa /05-springcloud-service-feign/bbb /05-springcloud-service-feign/ccc等 无法匹配 /05-springcloud-service-feign/a/b/c |
** | 匹配任意数量的字符 | /05-springcloud-service-feign/** | 匹配 //05-springcloud-service-feign/aaa /05-springcloud-service-feign/bbb /05-springcloud-service-feign/ccc等 也可以匹配 /05-springcloud-service-feign/a/b/c |
(5) 一般情况下API网关只是作为各个微服务的统一入口,但是有时候可能也需要在API网关服务上做一些特殊的业务逻辑处理,那么可以让请求到达API网关后,再转发给自己本身,由API网关自己来处理,可以进行如下的操作:
在06-springcloud-api-gateway项目中新建如下Controller:
@RestController
public class GateWayController {
@RequestMapping("/api/local")
public String hello() {
return "exec the api 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请求的生命周期图:
1.正常情况下所有的请求都是按照pre、route、post的顺序来执行,然后由post返回response2.在pre阶段,如果有自定义的过滤器则执行自定义的过滤器3.pre、routing、post的任意一个阶段如果抛异常了,则执行error过滤器我们可以有两种方式统一处理异常:
1、禁用zuul默认的异常处理SendErrorFilter过滤器,然后自定义Errorfilter过滤器
#Errorfilter过滤器
zuul.SendErrorFilter.error.disable=true
@Component
public class ErrorFilter extends ZuulFilter {
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 context = RequestContext.getCurrentContext();
ZuulException exception = (ZuulException) context.getThrowable();
logger.error("进入系统异常拦截", exception);
HttpServletResponse response = context.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 var5) {
ReflectionUtils.rethrowRuntimeException(var5);
}
return null;
}
}
在AuthFilter中构造异常进行测试
@Override
public Object run() throws ZuulException {
//人为构造运行时异常
int i = 10 / 0;
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String token = request.getParameter("token");
if (token == null) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.addZuulResponseHeader("content-type", "text/html;charset=utf-8");
ctx.setResponseBody("非法访问");
}
return null;
}
2、自定义全局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.getMessage();
}
}
将第一种方法的属性配置文件和 ErrorFilter注释调进行测试