请求路由

上面只是搭建完成了API网关服务,但是里面什么也没有,接下来我们将通过一个简单的例子来往这个API网关服务中增加请求路由的功能。

首先需要将之前的服务注册中心(eureka-server)、服务提供者(eureka-client,服务名为hello-service)、服务消费者(feign-consumer)都启动起来,每个项目只需启动一个服务实例即可,然后访问Eureka信息面板的地址http://localhost:1111即可看到如下信息:

传统路由方式

为了加深大家对于Zuul的理解,这里先学习使用传统的路由方式来实现请求路由的功能。使用Spring Cloud Zuul实现路由功能非常简单,只需要对api-gateway服务增加一些关于路由规则的配置,就可以实现传统的路由转发功能,在application.yml配置文件中添加如下配置:

1
2
3
4
5
zuul:
routes:
api-test-url:
path: /api-test-url/**
url: http://localhost:8083

以上配置定义了发往API网关服务的请求中,所有符合/api-test-url/**规则的访问请求都将被路由到http://localhost:8083地址上。举个例子,假设我们访问http://localhost:3005/api-test-url/hello链接的时候,API网关服务会将该请求路由到http://localhost:8083/hello提供的微服务接口上。需要说明的是zuul.routes.api-test-url中的api-test-url是路由的名字,开发者可以随意定义,但是需要注意的是一组path和url映射关系的路由名要相同,后面即将介绍的面向服务的映射方式也是如此。

面向服务的路由

聪明的你可能已经发现了,传统方式的路由配置有一个致命的缺陷就是路由规则都是固定的,无法实现动态修改,且运维人员需要花费大量的时间来维护各个路由path与url的关系,最合理的方式就是使用服务治理,采用服务治理体系中的处理方式。是的,在前面我们已经在pom.xml配置文件中增加了对Eureka的依赖,然后使用服务注册,我们可以让路由的path不再是映射的具体url,而是映射到某个具体的服务,而具体的URL则交给Eureka的服务发现机制去自动维护,我们称这类的路由为面向服务的路由。

那么在Spring Cloud Zuul中如何使用面向服务的路由呢?其实非常简单,正常都会有一下以下几个步骤:
(1)如果之前没有在api-gateway项目的pom.xml配置文件中引入Eureka的依赖,那么此时需要引入,如下所示:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

(2)在api-gateway项目的application.yml配置文件中配置Eureka的位置和服务路由,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
## 面向服务的路由方式
zuul:
routes:
api-a:
path: /api-a/**
serviceId: hello-service
api-b:
path: /api-b/**
serviceId: feign-consumer

### 服务注册中心
eureka:
client:
service-url:
defaultZone: http://localhost:1111/eureka/

由于前面我们启动了两个微服务应用hello-service和feign-consumer,因此在application.yml配置文件中通过定义两个名为api-a和api-b的路由来映射它们。此外,我们还需要配置服务注册中心的地址,这样将Zuul就被注册到服务中心内,然后就能获取到hello-service和fiegn-consumer服务的实例清单,以实现path的映射服务,最后再从服务中挑选实例来进行请求转发的完整路由机制。

接下来可以将之前的服务注册中心(eureka-server)、服务提供者(eureka-client,服务名为hello-service)、服务消费者(feign-consumer)和这里的api-gateway服务都启动起来,每个项目只需启动一个服务实例即可,然后访问Eureka信息面板的地址http://localhost:1111即可看到如下信息:

完成上面的搭建工作后,接下来就可以通过服务网关来访问hello-service和feign-consumer这两个服务了。由于前面我们已经在application.yml配置文件中增加了一些映射关系,因此就可以分别向这些网关发起请求:

  • 当用户访问http://localhost:3005/api-a/hello链接时,由于该url符合/api-a/**的规则,由api-a路由负责转发,该路由映射的serviceId为hello-service,所以最终的/hello请求会被转发到hello-service服务的某个实例上,即访问的链接为http://localhost:8081/hello

  • 当用户访问http://localhost:3005/api-b/feign-consumer2链接时,由于该url符合/api-b/**的规则,由api-b路由负责转发,该路由映射的serviceId为feign-consumer,所以最终的/feign-consumer2请求会被转发到feign-consumer服务的某个实例上,即访问的链接为http://localhost:3001/feign-consumer2

通过面向服务的路由配置方式,开发者不再需要为各个路由维护微服务应用的具体实例位置,而是通过简单的path和serviceId的映射组合,极大的简化了维护工作。这完全归功于Spring Cloud Eureka的服务发现机制,它使得API网关服务可以自动完成服务实例清单的维护,不再需要人来对路由实例进行维护。

请求过滤

在实现了请求路由后,那么微服务应用提供的接口就可以通过统一的API网关入口来被客户端访问了。但是你知道的,正常来说我们会要求不同的客户端(用户)请求同一个微服务提供的接口时,它们具有不同的访问权限,也就是说系统不会将所有的微服务接口都向所有用户开放,只针对特定的用户。但是目前的服务路由似乎没有提供权限限制这一功能,因此所有的请求都会全部转发到具体的应用并返回结果。那么如何实现我们的需求呢?

最简单和粗暴的方法就是为每一个微服务应用都实现一套用于校验签名和鉴别权限的过滤器或者拦截器,但是很明显这种做法不可取,它会增加日后系统的维护难度,且由于校验逻辑都是相似的,很容易出现代码冗余,所以这种做法比较适合初级开发者。

对于中级开发者来说,不仅是要实现功能,更要考虑今后的扩展性等问题。所以他们可能会将这些校验逻辑剥离出去,构建出一个独立的鉴权服务,之后开发者可能会直接在微服务应用中通过调用这个鉴权服务来实现校验功能,但是这种做法仅仅是解决了鉴权逻辑的分离,在本质上并没有将这部分不属于冗余的逻辑从原有的微服务应用中拆分出去,冗余的拦截器或过滤器依然是存在的。

那么有没有非常好的做法呢?有的,我们可以通过前置的网关服务来完成这些非业务性质的校验。为什么说这是非业务性质的校验呢,因为它可以根据既定的规则使得请求可以访问到指定服务,然后才执行指定服务的指定方法逻辑。

由于网关服务的加入,而外部客户端访问系统时已经有了统一的入口,且这些校验与具体业务无关,因此完全可以在请求到达的时候就完成校验和过滤,而不是采取先转发后过滤的策略,进而导致请求的延长。同时,在网关中完成请求的校验和过滤,微服务应用端就可以去除各种复杂的过滤器和拦截器,这无疑也降低了微服务应用接口的开发和测试的难度。

请求过滤的实现

想在API网关中实现对客户端请求的校验,需要学习和使用Spring Cloud Zuul的另一个核心功能:请求过滤。Zuul允许开发者在API网关上通过定义过滤器来实现对请求的拦截与过滤,实现的方式非常简单,只需定义一个类并继承ZuulFilter抽象类,然后实现该抽象方法中的4个抽象方法:filterType()、filterOrder()、shouldFilter()和run(),这样就可以完成对请求的拦截和过滤。

第一步,自定义过滤器。回到api-gateway项目中,在其项目包内新建一个filter包,并在里面新建一个MyFilter类,让该类继承ZuulFilter抽象类并实现上述介绍的4个抽象方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class MyFilter extends ZuulFilter {
public MyFilter() {
super();
}

@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 rqc = RequestContext.getCurrentContext();
HttpServletRequest request = rqc.getRequest();
String login = request.getParameter("login");
if(login==null){
rqc.setSendZuulResponse(false);
rqc.setResponseStatusCode(404);
rqc.addZuulResponseHeader("content-type", "text/html;charset=utf-8");
rqc.setResponseBody("未登录,无法访问");
}
return null;
}
}

简单解释一下上述代码的含义:
(1)上面定义的过滤器中,我们通过继承ZuulFilter这个抽象类并重写其中的4个抽象方法来实现自定义的过滤器;
(2)filterType方法的返回值为过滤器的类型。过滤器的类型决定了过滤器在哪个生命周期执行。这里设置了pre,表示在路由之前执行过滤器,其他可选值还有post、error、route和static,当然也可以自定义;
(3)filterOrder方法表示过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来依次执行;
(4)shouldFilter方法用来判断过滤器是否需要被执行。设置true表示执行,false表示不执行,在实际开发中,开发者可以根据当前请求地址来决定要不要对该地址进行过滤,即可以根据该函数来指定过滤器的有效返回,这里笔者直接设置true,表示该过滤器对所有请求都生效。
(5)run方法则是过滤的具体逻辑。这里笔者模拟了一个场景,判断请求地址中是否携带了login参数:如果地址中携带了login参数,则认为该请求合法,否则为非法请求。当然,如果是非法请求,还需要设置rqc.setSendZuulResponse(false);,它表示对该请求不进行路由,然后通过rqc.setResponseStatusCode(404);设置其返回的错误码,以及通过rqc.setResponseBody("未登录,无法访问");来对返回的body内容进行编辑设置。

第二步,创建生成Bean的方法。前面仅仅是自定义了过滤器,但是它并不会直接生效,还需要为它创建具体的Bean才能启动该过滤器。回到api-gateway项目的入口类中,在里面添加一个用于生成MyFilter过滤器对象的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class ApiGatewayApplication {
@Bean
MyFilter myFilter(){
return new MyFilter();
}

public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}

注意其实@SpringBootApplication@EnableDiscoveryClient注解可以使用另一个注解@SpringCloudApplication来代替,查看一下@SpringCloudApplication注解的源码,如下所示:

1
2
3
4
5
6
7
8
9
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
}

可以看到它其实是一个组合注解,里面有@SpringBootApplication@EnableDiscoveryClient@EnableCircuitBreaker注解,因此上面入口类的代码可以修改为如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
@SpringCloudApplication
@EnableZuulProxy
public class ApiGatewayApplication {
@Bean
MyFilter myFilter(){
return new MyFilter();
}

public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}

第三步,启动项目并测试。接下来可以将之前的服务注册中心(eureka-server)、服务提供者(eureka-client,服务名为hello-service)和这里的api-gateway服务都启动起来,每个项目只需启动一个服务实例即可,然后访问http://localhost:3005/api-a/hello即可看到如下信息:

但是当开发者试访问http://localhost:8081/hello链接时,则结果如下所示:

然后访问另一个链接http://localhost:3005/api-a/hello?login=envy,可以看到结果如下所示:

这样我们验证了请求过滤是在请求路由之前完成的,且请求过滤确实非常方便的完成了我们的需求。

小结

通过上面的例子,我们学到了Spring Cloud Zuul组件中两个核心的功能:请求路由和请求过滤,且也切身体会到API网关服务对于微服务架构的重要性,那么接下来将简单的对Zuul进行小结:
(1)API网关服务作为系统的统一入口,它屏蔽了系统内部各个微服务的细节;
(2)API网关服务可与服务治理框架,实现自动化的服务实例维护以及负载均衡的路由转发;
(3)API网关服务可以实现接口权限校验和微服务业务逻辑的解耦;
(4)通过使用服务网关中的过滤器,开发者可以在各生命周期中去校验请求的内容,将原本在对外服务层做的校验进行了前移,保证了微服务的无状态性,同时降低了微服务测试难度,使得服务本身更集中关注业务逻辑的处理。

OK,那么关于API网关服务的基础使用就学习到这里,后续会进一步学习更深的内容。