(0). 整个项目git地址

git clone https://github.com/dashprateek/zuul2-sample.git

(1). 项目结构如下

├── README.md
├── pom.xml
├── src
│   ├── main
│   │   ├── groovy
│   │   │   └── com
│   │   │       └── netflix
│   │   │           └── zuul
│   │   │               └── sample
│   │   │                   └── filters
│   │   │                       ├── endpoint
│   │   │                       │   └── Healthcheck.groovy
│   │   │                       ├── inbound
│   │   │                       │   ├── Debug.groovy
│   │   │                       │   ├── DebugRequest.groovy
│   │   │                       │   └── Routes.groovy
│   │   │                       └── outbound
│   │   │                           └── ZuulResponseFilter.groovy
│   │   ├── java
│   │   │   └── com
│   │   │       └── gateway
│   │   │           └── api
│   │   │               ├── Bootstrap.java
│   │   │               ├── module
│   │   │               │   └── GatewayModule.java
│   │   │               ├── server
│   │   │               │   └── GatewayServerStartup.java
│   │   │               └── util
│   │   │                   └── RouteUtils.java
│   │   └── resources
│   │       ├── application-test.properties
│   │       ├── application.properties
│   │       └── log4j.properties

(2). application.properties

### Instance env settings

region=us-east-1
environment=test

### Eureka instance registration for this app

#Name of the application to be identified by other services
eureka.name=zuul

#The port where the service will be running and serving requests
eureka.port=7001

#Virtual host name by which the clients identifies this service
eureka.vipAddress=${eureka.name}:${eureka.port}

#For eureka clients running in eureka server, it needs to connect to servers in other zones
eureka.preferSameZone=false

# Don't register locally running instances.
eureka.registration.enabled=false


# Loading Filters
zuul.filters.root=src/main/groovy/com/netflix/zuul/sample/filters
zuul.filters.locations=${zuul.filters.root}/inbound,${zuul.filters.root}/outbound,${zuul.filters.root}/endpoint
zuul.filters.packages=com.netflix.zuul.filters.common


### Load balancing backends with Eureka

#eureka.shouldUseDns=true
#eureka.eurekaServer.context=discovery/v2
#eureka.eurekaServer.domainName=discovery${environment}.netflix.net
#eureka.eurekaServer.gzipContent=true
#
#eureka.serviceUrl.default=http://${region}.${eureka.eurekaServer.domainName}:7001/${eureka.eurekaServer.context}
#
#api.ribbon.NIWSServerListClassName=com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList
#api.ribbon.DeploymentContextBasedVipAddresses=api-test.netflix.net:7001


### Load balancing backends without Eureka

eureka.shouldFetchRegistry=false
eureka.validateInstanceId=false

demo.ribbon.listOfServers=127.0.0.1:8080
demo.ribbon.client.NIWSServerListClassName=com.netflix.loadbalancer.ConfigurationBasedServerList
demo.ribbon.readTimeout=10000
# http://localhost:7001/test-provider/demo  -->  http://localhost:8080/demo
routes.demo.path=/test-provider/**


# This has to be the last line
#@next=application-${@environment}.properties

(3). GatewayModule

package com.gateway.api.module;

import com.netflix.discovery.AbstractDiscoveryClientOptionalArgs;
import com.netflix.discovery.DiscoveryClient;
import com.netflix.netty.common.accesslog.AccessLogPublisher;
import com.netflix.netty.common.status.ServerStatusManager;
import com.netflix.spectator.api.DefaultRegistry;
import com.netflix.spectator.api.Registry;
import com.netflix.zuul.BasicRequestCompleteHandler;
import com.netflix.zuul.FilterFileManager;
import com.netflix.zuul.RequestCompleteHandler;
import com.netflix.zuul.context.SessionContextDecorator;
import com.netflix.zuul.context.ZuulSessionContextDecorator;
import com.netflix.zuul.init.ZuulFiltersModule;
import com.netflix.zuul.netty.server.BaseServerStartup;
import com.netflix.zuul.netty.server.ClientRequestReceiver;
import com.netflix.zuul.origins.BasicNettyOriginManager;
import com.netflix.zuul.origins.OriginManager;
import com.netflix.zuul.stats.BasicRequestMetricsPublisher;
import com.netflix.zuul.stats.RequestMetricsPublisher;
import com.gateway.api.server.GatewayServerStartup;
import com.google.inject.AbstractModule;

public class GatewayModule extends AbstractModule {

	    @Override
	    protected void configure() {
	        // sample specific bindings
	        bind(BaseServerStartup.class).to(GatewayServerStartup.class);

	        // use provided basic netty origin manager
	        bind(OriginManager.class).to(BasicNettyOriginManager.class);

	        // zuul filter loading
	        install(new ZuulFiltersModule());
	        bind(FilterFileManager.class).asEagerSingleton();

	        // general server bindings
	        bind(ServerStatusManager.class); // health/discovery status
	        bind(SessionContextDecorator.class).to(ZuulSessionContextDecorator.class); // decorate new sessions when requests come in
	        bind(Registry.class).to(DefaultRegistry.class); // atlas metrics registry
	        bind(RequestCompleteHandler.class).to(BasicRequestCompleteHandler.class); // metrics post-request completion
	        bind(AbstractDiscoveryClientOptionalArgs.class).to(DiscoveryClient.DiscoveryClientOptionalArgs.class); // discovery client
	        bind(RequestMetricsPublisher.class).to(BasicRequestMetricsPublisher.class); // timings publisher

	        // access logger, including request ID generator
	        bind(AccessLogPublisher.class).toInstance(new AccessLogPublisher("ACCESS",
	                (channel, httpRequest) -> ClientRequestReceiver.getRequestFromChannel(channel).getContext().getUUID()));
	    }
}

(4). GatewayServerStartup

package com.gateway.api.server;

import java.util.HashMap;
import java.util.Map;

import javax.inject.Inject;

import com.netflix.appinfo.ApplicationInfoManager;
import com.netflix.config.DynamicIntProperty;
import com.netflix.discovery.EurekaClient;
import com.netflix.netty.common.accesslog.AccessLogPublisher;
import com.netflix.netty.common.channel.config.ChannelConfig;
import com.netflix.netty.common.channel.config.CommonChannelConfigKeys;
import com.netflix.netty.common.metrics.EventLoopGroupMetrics;
import com.netflix.netty.common.proxyprotocol.StripUntrustedProxyHeadersHandler;
import com.netflix.netty.common.status.ServerStatusManager;
import com.netflix.spectator.api.Registry;
import com.netflix.zuul.FilterLoader;
import com.netflix.zuul.FilterUsageNotifier;
import com.netflix.zuul.RequestCompleteHandler;
import com.netflix.zuul.context.SessionContextDecorator;
import com.netflix.zuul.netty.server.BaseServerStartup;
import com.netflix.zuul.netty.server.DirectMemoryMonitor;
import com.netflix.zuul.netty.server.ZuulServerChannelInitializer;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.group.ChannelGroup;

public class GatewayServerStartup extends BaseServerStartup {

	@Inject
	public GatewayServerStartup(ServerStatusManager serverStatusManager, FilterLoader filterLoader,
			SessionContextDecorator sessionCtxDecorator, FilterUsageNotifier usageNotifier,
			RequestCompleteHandler reqCompleteHandler, Registry registry, DirectMemoryMonitor directMemoryMonitor,
			EventLoopGroupMetrics eventLoopGroupMetrics, EurekaClient discoveryClient,
			ApplicationInfoManager applicationInfoManager, AccessLogPublisher accessLogPublisher) {
		super(serverStatusManager, filterLoader, sessionCtxDecorator, usageNotifier, reqCompleteHandler, registry,
				directMemoryMonitor, eventLoopGroupMetrics, discoveryClient, applicationInfoManager,
				accessLogPublisher);
	}

	@Override
	protected Map<Integer, ChannelInitializer> choosePortsAndChannels(ChannelGroup clientChannels,
			ChannelConfig channelDependencies) {

		Map<Integer, ChannelInitializer> portsToChannels = new HashMap<>();

		int port = new DynamicIntProperty("zuul.server.port.main", 7001).get();

		ChannelConfig channelConfig = BaseServerStartup.defaultChannelConfig();
		/*
		 * These settings may need to be tweaked depending if you're running
		 * behind an ELB HTTP listener, TCP listener, or directly on the
		 * internet.
		 *
		 * The below settings can be used when running behind an ELB HTTP
		 * listener that terminates SSL for you and passes XFF headers.
		 */
		channelConfig.set(CommonChannelConfigKeys.allowProxyHeadersWhen,
				StripUntrustedProxyHeadersHandler.AllowWhen.ALWAYS);
		channelConfig.set(CommonChannelConfigKeys.preferProxyProtocolForClientIp, false);
		channelConfig.set(CommonChannelConfigKeys.isSSlFromIntermediary, false);
		channelConfig.set(CommonChannelConfigKeys.withProxyProtocol, false);

		portsToChannels.put(port,
				new ZuulServerChannelInitializer(port, channelConfig, channelDependencies, clientChannels));
		logPortConfigured(port, null);

		return portsToChannels;
	}

}

(5). RouteUtils

package com.gateway.api.util;

import java.util.Map;
import java.util.Map.Entry;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.util.AntPathMatcher;
import org.apache.shiro.util.PatternMatcher;

import com.netflix.config.ConfigurationManager;

public class RouteUtils {

	private static final Map<String, String> routes = RoutesHolder.ROUTES;

	private static final PatternMatcher matcher = new AntPathMatcher();

	public static Map<String, String> getRoutes() {
		return StreamSupport
				.stream(Spliterators.spliteratorUnknownSize(ConfigurationManager.getConfigInstance().getKeys("routes"),
						Spliterator.DISTINCT), false)
				.collect(
						Collectors.toMap(
								key -> StringUtils.removeEndIgnoreCase(
										StringUtils.removeStartIgnoreCase(key, "routes."), ".path"),
								key -> ConfigurationManager.getConfigInstance().getString(key)));
	}

	public static String getRouteVip(String path) {
		return routes.entrySet().stream().filter(entry -> matcher.matches(entry.getValue(), path)).findFirst()
				.map(Entry::getKey).orElse("");
	}

	static class RoutesHolder {
		private static final Map<String, String> ROUTES = getRoutes();
	}

}

(6). Bootstrap

package com.gateway.api;

import com.gateway.api.module.GatewayModule;
import com.google.inject.Injector;
import com.netflix.config.ConfigurationManager;
import com.netflix.governator.InjectorBuilder;
import com.netflix.zuul.netty.server.BaseServerStartup;
import com.netflix.zuul.netty.server.Server;

public class Bootstrap {

    public static void main(String[] args) {
        new Bootstrap().start();
    }

    public void start() {
        System.out.println("Zuul Sample: starting up.");
        long startTime = System.currentTimeMillis();
        int exitCode = 0;
        Server server = null;
        try {
            ConfigurationManager.loadCascadedPropertiesFromResources("application");
            Injector injector = InjectorBuilder.fromModule(new GatewayModule()).createInjector();
            BaseServerStartup serverStartup = injector.getInstance(BaseServerStartup.class);
            server = serverStartup.server();
            long startupDuration = System.currentTimeMillis() - startTime;
            System.out.println("Zuul Sample: finished startup. Duration = " + startupDuration + " ms");
            server.start(true);
        }
        catch (Throwable t) {
            t.printStackTrace();
            System.err.println("###############");
            System.err.println("Zuul Sample: initialization failed. Forcing shutdown now.");
            System.err.println("###############");
            exitCode = 1;
        }
        finally {
            // server shutdown
            if (server != null) server.stop();
            System.exit(exitCode);
        }
    }
}

(7). Routes

/*
 * Copyright 2018 Netflix, Inc.
 *
 *      Licensed under the Apache License, Version 2.0 (the "License");
 *      you may not use this file except in compliance with the License.
 *      You may obtain a copy of the License at
 *
 *          http://www.apache.org/licenses/LICENSE-2.0
 *
 *      Unless required by applicable law or agreed to in writing, software
 *      distributed under the License is distributed on an "AS IS" BASIS,
 *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *      See the License for the specific language governing permissions and
 *      limitations under the License.
 */

package com.netflix.zuul.sample.filters.inbound;

import java.util.Map;
import java.util.Map.Entry;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import org.apache.commons.lang3.StringUtils;

import com.gateway.api.util.RouteUtils;
import com.netflix.config.ConfigurationManager;
import com.netflix.zuul.context.SessionContext;
import com.netflix.zuul.filters.http.HttpInboundSyncFilter;
import com.netflix.zuul.message.http.HttpRequestMessage;
import com.netflix.zuul.netty.filter.ZuulEndPointRunner;

/**
 * Routes configuration
 *
 * Author: Arthur Gonigberg Date: November 21, 2017
 */
public class Routes extends HttpInboundSyncFilter {


    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter(HttpRequestMessage httpRequestMessage) {
        return true;
    }

    @Override
    public HttpRequestMessage apply(HttpRequestMessage request) {
        SessionContext context = request.getContext();
		// *********************************************************************
		// 这是重点:相当于配置URL与后端应用的关系.
		// *********************************************************************
        context.setEndpoint(ZuulEndPointRunner.PROXY_ENDPOINT_FILTER_NAME);
        context.setRouteVIP(RouteUtils.getRouteVip(request.getPath()));
		
		// *********************************************************************
		// 这一部份是实现对URL的重写.
		// *********************************************************************
        // (http://localhost:7001/test-provider/demo) 实际代理之后变成了: (http://localhost:8080/test-provider/demo)
        // 重写URL请求(去掉serviceId)
        // (http://localhost:7001/demo)  实际代理之后变成了: (http://localhost:8080/demo)
        request.setPath("/demo");
        return request;
    }
}

(8). pom.xml

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.gateway</groupId>
	<artifactId>zuul2-sample</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<properties>
		<maven.compiler.source>8</maven.compiler.source>
		<maven.compiler.target>8</maven.compiler.target>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>com.netflix.zuul</groupId>
			<artifactId>zuul-core</artifactId>
			<version>2.1.3</version>
		</dependency>
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-core</artifactId>
			<version>1.4.0</version>
		</dependency>
		<dependency>
			<groupId>commons-configuration</groupId>
			<artifactId>commons-configuration</artifactId>
			<version>1.8</version>
		</dependency>
		<dependency>
			<groupId>com.netflix.governator</groupId>
			<artifactId>governator-core</artifactId>
			<version>1.17.4</version>
		</dependency>
		<dependency>
			<groupId>com.google.inject</groupId>
			<artifactId>guice</artifactId>
			<version>4.1.0</version>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.8.0</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>
			<plugin>
				<!-- Build an executable JAR -->
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-jar-plugin</artifactId>
				<version>3.1.0</version>
				<configuration>
					<archive>
						<manifest>
							<addClasspath>true</addClasspath>
							<classpathPrefix>lib/</classpathPrefix>
							<mainClass>com.gateway.api.Bootstrap</mainClass>
						</manifest>
					</archive>
				</configuration>
			</plugin>
			<plugin>
				<artifactId>maven-assembly-plugin</artifactId>
				<configuration>
					<archive>
						<manifest>
							<mainClass>com.gateway.api.Bootstrap</mainClass>
						</manifest>
					</archive>
					<descriptorRefs>
						<descriptorRef>jar-with-dependencies</descriptorRef>
					</descriptorRefs>
				</configuration>
				<executions>
					<execution>
						<id>make-assembly</id> <!-- this is used for inheritance merges -->
						<phase>package</phase> <!-- bind to the packaging phase -->
						<goals>
							<goal>single</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>
</project>

(9). test-provider

package help.lixin.samples.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
	private Logger logger = LoggerFactory.getLogger(HelloController.class);

	@GetMapping("/demo")
	public String index() {
		return "index DEMO Hello World!!!";
	}
}

(10). 总结

从整体来看Zuul2从Zuul1的Servlet模型向Reactor模型靠扰了,但是,在代码可读性来说,真的不敢苟同(相比:Spring Cloud Gateway).
唯一的优势在于,用了Groovy脚本实现热加载.