SpringCloudZuul之API网关服务




写在前面

该文参考来自 程序猿DD 的Spring Cloud 微服务实战一书,该文是作为阅读了 spring cloud Zuul 一章的读书笔记。书中版本比较老,我选择了最新稳定版的 spring cloud Greenwich.SR2 版本,该版本较书中版本有些变动。非常感谢作者提供了这么好的学习思路,谢谢!文章也参考了 Spring-cloud-netflix 的官方文档。


路由是微服务体系结构的一个组成部分。例如 / 可能映射到你的 web应用程序,/api/users 映射到用户服务,/api/shop 映射到商店服务。 Zuul 是一个来自 Netflix 的基于JVM 的路由器和服务端负载均衡器。DD 书里这样一段话描述的非常好。“API 网关是一个更为智能的应用服务器,它的定义类似于面向对象设计模式中的 Facade 模式,它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤。”

1. 入门

新建 api-gateway 工程

1.1 导入依赖:
	<dependencies>
        <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>
    </dependencies>
1.2 添加启动类
@EnableZuulProxy
@SpringCloudApplication
public class Application {

    public static void main(String[] args) {
        new SpringApplicationBuilder().sources(Application.class).web(WebApplicationType.SERVLET).run(args);
    }
}

@EnableZuulProxy 注解开启 Zuul 的 API 网关服务功能。

1.3 添加配置文件
spring:
  application:
    name: api-gateway
server:
  port: 60010
eureka:
  client:
    service-url:
      defaultZone: http://localhost:1112/eureka/
zuul:
  routes:
    api-a:
    # 配置对路径 /api-a/ 的所有访问将会路由到 hello-service 服务上
      path: /api-a/**
      serviceId: hello-service
    api-b:
    # 配置对路径 /api-b/ 的所有访问将会路由到 feign-consumer 服务上
      path: /api-b/**
      serviceId: feign-consumer

运行工程,访问地址 /api-b/feign-consumer

以上是我们基于服务的路由方式,那么对于传统的路由,我们应该怎样配置呢?

zuul:
  routes:
    api-a-url:
    # 配置对路径 /api-a-url/ 的所有访问将会路由到  http://localhost:8080/provider/ 服务上
      path: /api-a-url/**
      url: http://localhost:8080/provider/
    api-b-url:
    # 配置对路径 /api-b-url/ 的所有访问将会路由到 http://localhost:8082/ 服务上
      path: /api-b-url/**
      serviceId: http://localhost:8082/

该配置和上面基于服务的路由配置是一样的,访问地址/api-b-url/feign-consumer


2. 嵌入式 Zuul 反向代理

Spring Cloud创建了一个嵌入式 Zuul代理,以简化UI应用程序希望对一个或多个后端服务进行代理调用的常见用例的开发。该特性对于用户界面代理到所需的后端服务非常有用,从而避免了对所有后端独立管理 CORS 和身份验证问题的需要。

要启用它,请使用@EnableZuulProxy注释Spring引导主类。这样做会导致本地调用被转发到适当的服务。按照惯例,具有用户ID的服务接收位于/users的代理的请求(去掉前缀)。代理使用Ribbon来定位要通过服务发现转发到的服务实例。所有请求都在一个hystrix命令中执行,因此失败将出现在hystrix指标中。一旦电路打开,代理服务器就不会试图联系服务。

Zuul Starter 并不包含服务发现客户端,所以基于服务路由的方式,我们需要引入服务发现客户端的依赖包。

简单的路由方式并不会作为 HystrixCommand 执行,也不会用 Ribbon 来负载均衡多个 URL 。为了实现这一目标,我们可以使用静态的服务器列表来指定 serviceId,如下:

zuul:
  routes:
    echo:
      path: /myusers/**
      serviceId: myusers-service
      stripPrefix: true

hystrix:
  command:
    myusers-service:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: ...

myusers-service:
  ribbon:
    NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
    listOfServers: https://example1.com,http://example2.com
    ConnectTimeout: 1000
    ReadTimeout: 3000
    MaxTotalHttpConnections: 500
    MaxConnectionsPerHost: 100

3. 管理端点

默认情况下,使用 @EnableZuulProxy 注解后,将启用两个额外的端点(需要通过management.endpoints.web.exposure.include=routes,filters对外暴露):

  • routes

    使用 GET 请求访问 /actuator/routes ,将返回路由映射列表:

    {
    	"/api-a/**": "hello-service",
    	"/api-b/**": "feign-consumer",
    	"/api-a-url/**": "http://localhost:8080/provider/",
    	"/api-b-url/**": "http://localhost:8082/",
    	"/feign-consumer/**": "feign-consumer",
    	"/eureka-server-node1/**": "eureka-server-node1",
    	"/hello-service/**": "hello-service",
    	"/eureka-server-node2/**": "eureka-server-node2"
    }
    

    你会发现,这里包含了我们未在配置文件中配置的路径映射。原因是引入服务发现客户端后,会有服务的自动映射。我们可以通过 zuul.ignored-services='*' 来忽略它,对于我们额外的路由配置,并不会被忽略。

    为我们的配置文件添加完该配置后,再次访问路由端点:

    {
        "/api-a/**":"hello-service",
     	"/api-b/**":"feign-consumer",
        "/api-a-url/**":"http://localhost:8080/provider/",
        "/api-b-url/**":"http://localhost:8082/"
    }
    

    额外的,我们可以通过 ?format=details 或者 /routes/details 来获取路由详细信息:

    GET /routes/details.

    {
    	"/api-a/**": {
    		"id": "api-a",
    		"fullPath": "/api-a/**",
    		"location": "hello-service",
    		"path": "/**",
    		"prefix": "/api-a",
    		"retryable": false,
    		"customSensitiveHeaders": false,
    		"prefixStripped": true
    	},
    	"/api-b/**": {
    		"id": "api-b",
    		"fullPath": "/api-b/**",
    		"location": "feign-consumer",
    		"path": "/**",
    		"prefix": "/api-b",
    		"retryable": false,
    		"customSensitiveHeaders": false,
    		"prefixStripped": true
    	},
    	"/api-a-url/**": {
    		"id": "api-a-url",
    		"fullPath": "/api-a-url/**",
    		"location": "http://localhost:8080/provider/",
    		"path": "/**",
    		"prefix": "/api-a-url",
    		"retryable": false,
    		"customSensitiveHeaders": false,
    		"prefixStripped": true
    	},
    	"/api-b-url/**": {
    		"id": "api-b-url",
    		"fullPath": "/api-b-url/**",
    		"location": "http://localhost:8082/",
    		"path": "/**",
    		"prefix": "/api-b-url",
    		"retryable": false,
    		"customSensitiveHeaders": false,
    		"prefixStripped": true
    	}
    }
    

    通过 POST 请求访问 /routes 端点将强制刷新已经存在的路由,我们可以通过 endpoints.routes.enabled = false 来禁用该端点。

  • filters

    GET 请求访问 /filters 端点将返回过滤器类型映射。

    {
    	"error": [{
    		"class": "org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter",
    		"order": 0,
    		"disabled": false,
    		"static": true
    	}],
    	"post": [{
    		"class": "org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter",
    		"order": 1000,
    		"disabled": false,
    		"static": true
    	}],
    	"pre": [{
    		"class": "org.springframework.cloud.netflix.zuul.filters.pre.DebugFilter",
    		"order": 1,
    		"disabled": false,
    		"static": true
    	}, {
    		"class": "org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter",
    		"order": -1,
    		"disabled": false,
    		"static": true
    	}, {
    		"class": "com.duofei.filter.AccessFilter",
    		"order": 0,
    		"disabled": false,
    		"static": true
    	}, {
    		"class": "org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter",
    		"order": -2,
    		"disabled": false,
    		"static": true
    	}, {
    		"class": "org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter",
    		"order": -3,
    		"disabled": false,
    		"static": true
    	}, {
    		"class": "org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter",
    		"order": 5,
    		"disabled": false,
    		"static": true
    	}],
    	"route": [{
    		"class": "org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter",
    		"order": 100,
    		"disabled": false,
    		"static": true
    	}, {
    		"class": "org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter",
    		"order": 10,
    		"disabled": false,
    		"static": true
    	}, {
    		"class": "org.springframework.cloud.netflix.zuul.filters.route.SendForwardFilter",
    		"order": 500,
    		"disabled": false,
    		"static": true
    	}]
    }
    

    其中,com.duofei.filter.AccessFilter 是我自定义的过滤器,代码如下:

    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() throws ZuulException {
            RequestContext currentContext = RequestContext.getCurrentContext();
            HttpServletRequest request = currentContext.getRequest();
            log.info("send {} request to {}",request.getMethod(),request.getRequestURL().toString());
            String accessToken = request.getParameter("accessToken");
            if (accessToken == null) {
                log.warn("access token is empty");
                currentContext.setSendZuulResponse(false);
                currentContext.setResponseStatusCode(401);
                return null;
            }
            log.info("access token ok");
            return null;
        }
    }
    

    还需要在启动类中将该类添加到 Spring环境中:

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

4. 扼杀模式和本地转发

迁移现有应用程序或 API 时的一种常见模式是“扼杀”旧端点,用不同的实现缓慢地替换它们。Zuul 代理在这方面是一个有用的工具,因为您可以使用它来处理来自旧端点的所有流量,但将一些请求重定向到新端点。

application.yml

 zuul:
  routes:
    first:
      path: /first/**
      url: https://first.example.com
    second:
      path: /second/**
      url: forward:/second
    third:
      path: /third/**
      url: forward:/3rd
    legacy:
      path: /**
      url: https://legacy.example.com

在这个示例中,我们扼杀了 legacy 遗留程序;/first/** 中的路径将映射到外部 URL 的新服务中,/second/** 的路径将被转发,因此我们可以在本地处理它,(像普通的 @RequestMapping), /third/** 中的路径也被转发,但前缀不同(例如/third/foo 将被转发到/3rd/foo)。

以上来自于 Spring 官方文档,我对文档中提到的功能做了实践,在此做下记录。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值