Spring Cloud系列(三十一) Zuul动态加载—Finchley版本


本文摘自:《Spring Cloud 微服务实战》——翟永超


在微服务架构中,由于API网关服务担负着外部访问统一入口的重任,它同其他应用不同,任何关闭和重启应用的操作都会是系统对外停止服务,对于很多7*24小时服务的系统来说,这种请求是不允许的。所以作为最外部的网关,它必须具备动态更新内部逻辑的能力,比如动态修改路由规则、动态添加、删除过滤器等。

通过Zuul实现的API网关服务具备了动态路由和动态过滤的器能力,可以在不重启API网关服务的前提下为其动态修改路由规则和添加或删除过滤器。

动态路由

通过之前对请求路由的介绍,发现对于路由规则的控制都可以在配置文件 application.properties 或 application.yml 文件中完成。之前还介绍了 Spring Cloud Config 的动态刷新机制,所以只需要将API网关服务的配置文件通过 Spring Cloud Config 连接的Git仓库存储和管理,我们就能实现动态刷新路由规则的功能。

第一步,创建一个API网关服务,命名为api-gateway-dynamic-route,并引入Zuul、Config、Eureka的依赖。

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.0.2.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>
	<spring-cloud.version>Finchley.RC1</spring-cloud.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>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-actuator</artifactId>
	</dependency>
	<!-- Config客户端依赖 -->
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-config</artifactId>
	</dependency>
	<!-- Eureka客户端依赖 -->
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
	</dependency>
	<!-- Zuul依赖 -->
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
	</dependency>
</dependencies>

第二步,创建bootstrap.yml,指定 Config Sever 和 Eureka Server 的具体地址,以获取应用的配置文件和实现服务注册与发现。 

spring:
  application:
    name: api-gateway-dynamic-route #应用名
  cloud:
    config:
      name: api-gateway #对应application
      uri: http://localhost:5666/ # 配置服务中心的地址
      discovery:
        enabled: true
        service-id: config-server
      fail-fast: true  #没有读取成功则执行快速失败
server:
  port: 5222

#指定服务注册中心位置
eureka:
  client:
    service-url:
      defaultZone: http://localhost:1111/eureka/ 
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}

#actuator配置    
management:
  endpoints: 
    web:
      exposure:
        include: routes,refresh 

第三步,创建应用主类,添加 @EnableDiscoveryClient@EnableZuulProxy 注解,并使用 @RefreshScope 注解来将 Zuul 的配置内容动态化。

@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class Application {
	
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
	
	@Bean(name="zuul.CONFIGURATION_PROPERTIES")
    @RefreshScope
    @ConfigurationProperties("zuul")
    @Primary
	public ZuulProperties zuulProperties() {
		return new ZuulProperties();
	}

}

第四步,在Git仓库添加Zuul的配置文件api-gateway.properties。

配置文件中配置了如下路由规则

zuul.routes.service-a.path=/service-a/**
zuul.routes.service-a.serviceId=hello-service

测试

启动服务注册中心,即eureka-server-vFinchley.Rc2工程

启动服务提供者hello-service,即eureka-client-vFinchley.Rc2 工程

启动Config Server,即config-server-vFinchley.RC2工程

启动API网关服务,即api-gateway-dynamic-route工程

启动过程可以看到控制台打印如下信息

2018-10-09 14:00:49.503  INFO 9440 --- [           main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at: http://192.168.1.228:5666/
2018-10-09 14:00:50.973  INFO 9440 --- [           main] c.c.c.ConfigServicePropertySourceLocator : Located environment: name=api-gateway, profiles=[default], label=null, version=26b07dc73ae5119922a91b4c079e57ca0ea31d24, state=null
2018-10-09 14:00:50.974  INFO 9440 --- [           main] b.c.PropertySourceBootstrapConfiguration : Located property source: CompositePropertySource {name='configService', propertySources=[MapPropertySource {name='configClient'}, MapPropertySource {name='https://github.com/WYA1993/spring_cloud_config_demo/config/config_repo/api-gateway.properties'}]}
2018-10-09 14:00:50.979  INFO 9440 --- [           main] com.wya.springcloud.Application          : No active profile set, falling back to default profiles: default

请求的/actuator/routes接口,返回如下路由规则

{
"/service-a/**": "hello-service",
"/hello-service/**": "hello-service",
"/config-server/**": "config-server"
}

其中/service-a/**路由是在api-gateway.properties读取的,而/hello-service/**和/config-server/**是Zuul默认生成的路由规则,为什么会生成默认路由我在讲解Zuul的时候有介绍,这里就不解释了,这说明已经成功通过Config Server读取到了配置文件。

请求http://192.168.1.228:5222/service-a/hello接口也可以成功返回信息--“hello”。

接下来修改api-gateway.properties,修改后内容如下:

zuul.routes.service-a.path=/service-aaa/**
zuul.routes.service-a.serviceId=hello-service

通过Post请求http://localhost:5222/actuator/refresh刷新路由规则。

再次请求的/actuator/routes接口,返回如下路由规则

{
"/service-aaa/**": "hello-service",
"/hello-service/**": "hello-service",
"/config-server/**": "config-server"
}

请求http://192.168.1.228:5222/service-aaa/hello接口也可以成功返回信息--“hello”。

这就实现了动态路由。

动态过滤器

实现请求过滤器的动态加载,需要借助基于JVM实现的动态语言的帮助,比如Groovy。

第一步,创建 Spring Boot 工程,命名api-gateway-dynamic-filter,并引入Zuul、Eureka、Groovy的依赖。

<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.2.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>
		<spring-cloud.version>Finchley.RC2</spring-cloud.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.codehaus.groovy/groovy-all -->
		<dependency>
			<groupId>org.codehaus.groovy</groupId>
			<artifactId>groovy-all</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</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>

	<repositories>
		<repository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</repository>
	</repositories>

第二步,创建application.yml文件,指定Eureka服务注册中心地址,并指定路由规则

spring:
  application:
    name: api-gateway-dynamic-filter #为服务命名
server:
  port: 5111
eureka:
  client:
    service-url: 
      defaultZone: http://localhost:1111/eureka/ #指定服务注册中心位置
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
#服务路由配置
zuul:
  routes:
    api-a:
      path: /api-a/**
      serviceId: hello-service

接下来为这个基础服务增加动态过滤器的功能。

第三步,为了方便使用,先自定义一些用来配置动态加载过滤器的参数,并将它们的配置值加入到application.yml中

zuul:
  routes:
    api-a:
      path: /api-a/**
      serviceId: hello-service
  # 动态过滤器配置参数 自定义
  filter:
    root: filter
    interval: 5

第四步,创建用来加载自定义属性的配置类,命名为FilterConfiguration。

@ConfigurationProperties("zuul.filter")
public class FilterConfiguration {

    private String root;
    private Integer interval;

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public Integer getInterval() {
        return interval;
    }

    public void setInterval(Integer interval) {
        this.interval = interval;
    }
}

第五步,创建应用主类,引入FilterConfiguration配置,并创建动态加载过滤器的实例。

@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
@EnableConfigurationProperties({FilterConfiguration.class})
public class Application {
	
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
	
	@Bean
	public FilterLoader filterLoader(FilterConfiguration filterConfiguration) {
		FilterLoader filterLoader = FilterLoader.getInstance();
		filterLoader.setCompiler(new GroovyCompiler());
		try {
			FilterFileManager.setFilenameFilter(new GroovyFileFilter());
			FilterFileManager.init(
					filterConfiguration.getInterval(),
					filterConfiguration.getRoot() + "/pre",
					filterConfiguration.getRoot() + "/post");
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		return filterLoader;
	}
}

第六步,创建存储过滤器的文件夹,否则启动会报错。

根据上面的配置,API网关会每隔5秒,从API网关服务所在的位置的 filter/pre 和 filter/post目录下获取Groovy定义的过滤器,并对其进行编译和动态加载使用。对于动态加载的时间间隔可以通过zuul.filter.interval参数修改。而加载过滤器实现类的根目录可通过zuul.filter.root调整。

测试

启动服务注册中心,即eureka-server-vFinchley.Rc2工程

启动服务提供者hello-service,即eureka-client-vFinchley.Rc2 工程

启动API网关服务,即api-gateway-dynamic-filter工程

正常启动后,请求http://192.168.1.228:5111/api-a/hello可以返回hello。

接下来再pre目录下添加一个pre类型的过滤器。

package com.wya.springcloud.filter.pre

import org.slf4j.Logger
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;

class PreFilter extends ZuulFilter {

  Logger log = LoggerFactory.getLogger(PreFilter.class)

  @Override
  String filterType() {
    return "pre"
  }

  @Override
  int filterOrder() {
    return 1000
  }

  @Override
  boolean shouldFilter() {
    return true
  }

  @Override
  Object run() {
    HttpServletRequest request = RequestContext.getCurrentContext().getRequest()
    log.info("this is a pre filter: Send {} request to {}", request.getMethod(), request.getRequestURL().toString())
    return null
  }

}

在post目录下添加一个post类型的过滤器。

package com.wya.springcloud.filter.post

import com.netflix.zuul.ZuulFilter
import com.netflix.zuul.context.RequestContext
import com.netflix.zuul.http.HttpServletResponseWrapper
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.cloud.netflix.zuul.util.RequestUtils

import javax.servlet.http.HttpServletResponse

class PostFilter extends ZuulFilter{

  Logger log = LoggerFactory.getLogger(PostFilter.class)

  @Override
  String filterType() {
    return "post"
  }

  @Override
  int filterOrder() {
    return 2000
  }

  @Override
  boolean shouldFilter() {
    return true
  }

  @Override
  Object run() {
    log.info("debug request : {}", RequestContext.getCurrentContext().getBoolean("debugRequest"))
    log.info("this is a post filter: Receive response")
    HttpServletResponse response = RequestContext.getCurrentContext().getResponse()
    response.getOutputStream().print(", dynamic filter")
    response.flushBuffer()
  }
}

不需要重新启动,5秒后会自动加载,再次请求http://192.168.1.228:5111/api-a/hello可以返回hello, dynamic filter。

控制台并打印如下内容:

2018-10-09 15:05:03.519  INFO 16128 --- [nio-5111-exec-5] com.didispace.filter.pre.PreFilter       : this is a pre filter: Send GET request to http://192.168.1.228:5111/api-a/hello
2018-10-09 15:05:03.529  INFO 16128 --- [nio-5111-exec-5] com.didispace.filter.post.PostFilter     : debug request: false
2018-10-09 15:05:03.529  INFO 16128 --- [nio-5111-exec-5] com.didispace.filter.post.PostFilter     : this is a post filter: Receive response
2018-10-09 15:06:13.157  INFO 16128 --- [trap-executor-0] c.n.d.s.r.aws.ConfigClusterResolver      : Resolving eureka endpoints via configuration
2018-10-09 15:08:02.608  INFO 16128 --- [nio-5111-exec-8] com.didispace.filter.pre.PreFilter       : this is a pre filter: Send GET request to http://192.168.1.228:5111/api-a/hello
2018-10-09 15:08:02.619  INFO 16128 --- [nio-5111-exec-8] com.didispace.filter.post.PostFilter     : debug request : false
2018-10-09 15:08:02.619  INFO 16128 --- [nio-5111-exec-8] com.didispace.filter.post.PostFilter     : this is a post filter: Receive response

这虽然实现了动态过滤器的功能,但是有几个缺陷:

  • 在filter目录下删除Groovy文件并不能从当前运行的API网关服务中移除这个过滤器,如果需要移除可以修改shouldFilter返回 false。
  • 这样的动态过滤器无法注入Spring 容器中加载的实例来使用,比如在动态过滤器中注入RestTemplate对各个微服务发起请求。

猜你喜欢

转载自blog.csdn.net/WYA1993/article/details/82800265