微服务巡礼 Spring Cloud 体系使用(五)Spring Cloud Zuul

前面几篇文章已经可以构建一个小型微服务,对个人开发者真是绰绰有余了,但个人开发者一般用PHP、Node这种语言来得比较快捷,用Java未免过重。

现在所有服务都被拆分一个一个应用,应用之间的通信通过接口访问,在后端有个基本认识,接口通信一定要有数据校验、鉴权的操作,一开始应用少做这种操作简单,但当应用集群上去之后,一个一个手动更改将会是噩梦,所有对于接口之间的访问操作我们有下面介绍的API网关

Spring Cloud Zuul

同理,我们引入这个组件,不可能像Ribbon、Feign引入到个体,但每个服务都会经过Eureka注册中心,Zuul根据这个特点,将API网关服务结合到Eureka服务上,可以获得所有实例服务信息,不再需要人工介入,对于路由规则的维护,Zuul默认会将通过以服务名作为ContextPath的方式来创建路由映射,大部分情况下可以满足需求。将鉴权、限制操作这些非强业务性的服务独立出来,使微服务更为专注业务开发,而运维只要集中精力在Eureka和Zuul上即可。

下面说说Zuul的简单使用。

构建网关

创建一个Spring-Boot基础工程,命为gateway,pom.xml文件为

<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.10.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<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>
	</dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>Dalston.SR5</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

在应用主类,使用@EnableZuulProxy注解开启Zuul的API网关服务功能

@EnableZuulProxy
@SpringCloudApplication
public class Application {

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

application.yml文件中配置基础信息

spring:
  application:
    name: api-gateway

server:
  port: 5555

现在我们使用面向服务的路由,传统的指定host地址也可以实现,无疑是不合适,要人工维护host表,将会是个灾难。我们同样在application.yml文件中配置面向服务的路由

eureka:
  client:
    service-url:
      defaultZone: http://localhost:1111/eureka/

zuul:
  routes:
    api-client:
      path: /api-client/**
      serviceId: demo-client
    api-consumer:
      path: /api-consumer/**
      serviceId: feign-consume

针对我们之前已经做的两个微服务应用demo-client和feign-consume,在上面的配置中分别定义了两个名为api-client和api-consumer的路由映射它们,通过指定Eureka Server服务注册中心的位置,除了将自己注册成服务,同时也让Zuul能够获取demo-client和feign-consume服务的实例清单,以实现path映射服务,再从服务中挑选实例来进行请求转发的完整路由机制。

完成上面的配置后,我们启动所有服务

可以看到,三个服务已经跑起来,之后分别访问映射的接口http://localhost:5555/api-client/hellohttp://localhost:5555/api-consumer/feign-consumer,这两个url都会被映射到访问相关服务的实例服务中

可以看到,Zuul已经起作用,一个简单的API网关服务就这样完成。

请求过滤

既然服务提供者和服务消费者都会经过Eureka服务器,那么我们也可以根据这个特性将一些请求过滤鉴权放在这里,不用每个服务都做一套过滤系统,有效降低工作量。

现在我们编写一个简单的Zuul过滤器

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;


public class AccessFilter extends ZuulFilter{

    private static Logger log = LoggerFactory.getLogger(AccessFilter.class);

    @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();

        log.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString());

        Object accessToken = request.getParameter("accessToken");
        if(accessToken == null) {
            log.warn("access token is empty");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            return null;
        }
        log.info("access token ok");
        return null;
    }
}

这里要注意引入的库要正确。

filterType:过滤器的类型,它决定过滤器在请求的哪个生命周期中执行。这里定义为pre,代表会在请求被路由之前执行。
filterOrder:过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来依次执行。
shouldFilter:判断该过滤器是否需要被执行。这里我们直接返回了true,因此该过滤器对所有请求都会生效。实际运用中我们可以利用该函数来指定过滤器的有效范围。
run:过滤器的具体逻辑。这里我们通过ctx.setSendZuulResponse(false)令zuul过滤该请求,不对其进行路由,然后通过ctx.setResponseStatusCode(401)设置了其返回的错误码,当然我们也可以进一步优化我们的返回,比如,通过ctx.setResponseBody(body)对返回body内容进行编辑等。

之后自定义过滤器后,需要创建具体的Bean才能启动该过滤器。

public class Application {

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

	@Bean
	public AccessFilter accessFilter() {
		return new AccessFilter();
	}
}

完成上面改造后,再次重新编译启动gateway,请求地址查看是否生效

http://localhost:5555/api-client/hello:返回401错误
http://localhost:5555/api-client/hello&accessToken=token:正确路由到demo-client的/hello接口,并返回内容

得益强大的封装能力,Zuul为开发者屏蔽很多细节,可以做到开箱即用,大大解耦服务和非强业务的联系,使开发者更专注业务逻辑处理。源码解读和配置详解可以参考细说更多的书籍和资料,这里简单介绍。

Show Comments