springcloud之动态路由

麦田开拓者 2019-09-19

1. 背景

       Zuul是Netflix提供的一个开源组件,Zuul致力于在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。小弟所在的公司,是使用它来作为网关的重要组成部分。今天就通过一个简单的实例,来具体说明一下是怎么实现的动态路由。

2. 架构演变

      为了更好的帮助小伙伴们理解后面的demo,先来做个简单的架构演变,如下图所示:

springcloud之动态路由

     上图是没有网关参与的一个最典型的互联网架构。引入网关,为了拉取服务实例,引入springcloud中的eureka组件,作为注册中心,将架构演变后,如下图所示:

springcloud之动态路由

       因为Zuul网关是面向众多的外围系统,所以这种服务发现的方式,不适合用在网关产品。因此,将架构继续演变,如下图所示:

springcloud之动态路由

我这边实现的简单demo,就是根据上图实现的。

3. 动态路由

      既然路由有动态的,那么相对的,也有静态路由。在介绍动态路由之前,先搭建一个静态路由的demo。然后,根据这个示例,我们分析下使用动态路由的优势,再修改下这个demo,最后实现动态路由。

      这里demo的管理工具是maven,整个的项目结构如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<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.route</groupId>
    <artifactId>zuul-gateway-demo</artifactId>
    <packaging>pom</packaging>
    <version>1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.2.RELEASE</version>
    </parent>

    <modules>
        <module>gate-way</module>
        <module>demo-service</module>
    </modules>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Camden.SR6</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.2</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <executable>true</executable>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

       这里有个需要注意的地方,就是springboot和springcloud的对应版本,如果版本不匹配,会有版本兼容的问题,直接导致服务启动报错。

3.1 gateway项目

       服务启动类:

@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

       属性配置:

# 路由信息
zuul.routes.books.url=http://localhost:8090
zuul.routes.books.path=/book/**

# 不适用注册中心(否则会带来侵入性)
ribbon.eureka.enabled=false

# 网管端口
server.port=8888
 

3.2 demo-service项目

      服务启动类:

@RestController
@SpringBootApplication
@Slf4j
public class DemoServiceApplication {

    @RequestMapping(value = "/available")
    public String available() {
        log.info("Spring in Action");
        return "avaliable success";
    }

    @RequestMapping(value = "/checked-out")
    public String checkedOut() {
        return "checkout success";
    }

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

     属性配置:

# 服务端口号
server.port=8090

 一个简单的静态路由demo,已经搭建好了,测试下:http://localhost:8888/books/available

3.3 静态路由源码分析

       上面是一个简单的静态路由的demo,从源码分析下,实现转发及路由的关键是ZuulConfiguration,下面我们就直接看看这个配置文件的源码:

@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass(ZuulServlet.class)
@Import(ServerPropertiesAutoConfiguration.class)
public class ZuulConfiguration {
    // zuul的配置文件,对应了application.properties中的配置信息
	@Autowired
	protected ZuulProperties zuulProperties; 

	@Autowired
	protected ServerProperties server;

	@Autowired(required = false)
	private ErrorController errorController;

	@Bean
	public HasFeatures zuulFeature() {
		return HasFeatures.namedFeature("Zuul (Simple)", ZuulConfiguration.class);
	}

    // 核心类,路由定位器 
	@Bean
	@ConditionalOnMissingBean(RouteLocator.class)
	public RouteLocator routeLocator() {
		return new SimpleRouteLocator(this.server.getServletPrefix(),
				this.zuulProperties);
	}

    // zuul的控制器,负责处理链路调用
	@Bean
	public ZuulController zuulController() {
		return new ZuulController();
	}

    // MVC HandlerMapping that maps incoming request paths to remote services.
	@Bean
	public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
		ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController());
		mapping.setErrorController(this.errorController);
		return mapping;
	}

    // 注册了一个路由刷新监听器,默认实现是ZuulRefreshListener.class,这个是我们动态路由的关键
	@Bean
	public ApplicationListener<ApplicationEvent> zuulRefreshRoutesListener() {
		return new ZuulRefreshListener();
	}

	@Bean
	@ConditionalOnMissingBean(name = "zuulServlet")
	public ServletRegistrationBean zuulServlet() {
		ServletRegistrationBean servlet = new ServletRegistrationBean(new ZuulServlet(),
				this.zuulProperties.getServletPattern());
		// The whole point of exposing this servlet is to provide a route that doesn't
		// buffer requests.
		servlet.addInitParameter("buffer-requests", "false");
		return servlet;
	}

	// pre filters
    ........
	
	// post filters
    ........

    // 上面提到的路由刷新监听器
	private static class ZuulRefreshListener
			implements ApplicationListener<ApplicationEvent> {

		@Autowired
		private ZuulHandlerMapping zuulHandlerMapping;

		private HeartbeatMonitor heartbeatMonitor = new HeartbeatMonitor();

		@Override
		public void onApplicationEvent(ApplicationEvent event) {
			if (event instanceof ContextRefreshedEvent
					|| event instanceof RefreshScopeRefreshedEvent
					|| event instanceof RoutesRefreshedEvent) {
				this.zuulHandlerMapping.setDirty(true);
			}
			else if (event instanceof HeartbeatEvent) {
				if (this.heartbeatMonitor.update(((HeartbeatEvent) event).getValue())) {
					this.zuulHandlerMapping.setDirty(true);
				}
			}
		}

	}

}

源码中关键的实现,我这里都已经贴出来了,省略号的地方,有兴趣的可以自行查看源码。

3.4 动态路由

       动态路由需要达到可持久化配置,动态刷新的效果。如最后一个架构图所示,不仅要能满足从spring的配置文件properties加载路由信息,还需要从数据库加载我们的配置。另外一点是,路由信息在容器启动时就已经加载进入了内存,我们希望配置完成后,实施发布,动态刷新内存中的路由信息,达到不停机维护路由信息的效果。而从ZuulConfiguration的源码上分析,要实现动态路由,第一步需要理解路由定位器,我们画一个关于RouteLocator的UML,如下所示:

springcloud之动态路由

       从这个UML上,我们查看SimpleRouteLocator的源码,没有实现RefreshableRouteLocator接口。从接口关系来看,spring考虑到了路由刷新的需求,是没法用RouteLocator的默认实现类SimpleRouteLocator来是实现的。所以,我们只能参考DiscoveryClientRouteLocator来改造SimpleRouteLocator使其具备刷新能力。

从DiscoveryClientRouteLocator的源码分析,它是继承SimpleRouteLocator,但是比SimpleRouteLocator多了两个功能:第一是从DiscoveryClient(如Eureka)发现路由信息,代码片段如下所示:

public DiscoveryClientRouteLocator(String servletPath, DiscoveryClient discovery,
		ZuulProperties properties) {
	super(servletPath, properties);

	if (properties.isIgnoreLocalService()) {
		ServiceInstance instance = discovery.getLocalServiceInstance();
		if (instance != null) {
			String localServiceId = instance.getServiceId();
			if (!properties.getIgnoredServices().contains(localServiceId)) {
				properties.getIgnoredServices().add(localServiceId);
			}
		}
	}
	this.serviceRouteMapper = new SimpleServiceRouteMapper();
	this.discovery = discovery;
	this.properties = properties;
}

public DiscoveryClientRouteLocator(String servletPath, DiscoveryClient discovery,
			ZuulProperties properties, ServiceRouteMapper serviceRouteMapper) {
	this(servletPath, discovery, properties);
	this.serviceRouteMapper = serviceRouteMapper;
}

       从之前的架构图已经给大家解释清楚了,所以忽略它,第二是实现了RefreshableRouteLocator接口,能够实现动态刷新。

       在自定义实现动态路由之前,先分析下SimpleRouteLocator的源码:

@CommonsLog
public class SimpleRouteLocator implements RouteLocator {
    // 从配置文件中获取路由信息配置
	private ZuulProperties properties;

    // 路径正则配置器,即作用于path:/books/**
	private PathMatcher pathMatcher = new AntPathMatcher();

	private String dispatcherServletPath = "/";
	private String zuulServletPath;

	private AtomicReference<Map<String, ZuulRoute>> routes = new AtomicReference<>();

	public SimpleRouteLocator(String servletPath, ZuulProperties properties) {
		this.properties = properties;
		if (servletPath != null && StringUtils.hasText(servletPath)) {
			this.dispatcherServletPath = servletPath;
		}

		this.zuulServletPath = properties.getServletPath();
	}

    // 路由定位器和其他组件的交互,是最终把定位的Routes以list的方式提供出去,核心实现
	@Override
	public List<Route> getRoutes() {
		if (this.routes.get() == null) {
			this.routes.set(locateRoutes());
		}
		List<Route> values = new ArrayList<>();
		for (String url : this.routes.get().keySet()) {
			ZuulRoute route = this.routes.get().get(url);
			String path = route.getPath();
			values.add(getRoute(route, path));
		}
		return values;
	}
	
	// 省略部分实现
	.........

    // 这个方法在网关产品中也很重要,可以根据实际路径匹配到Route来进行业务逻辑的操作,进行一些加工
	@Override
	public Route getMatchingRoute(final String path) {

		if (log.isDebugEnabled()) {
			log.debug("Finding route for path: " + path);
		}

		if (this.routes.get() == null) {
			this.routes.set(locateRoutes());
		}

		if (log.isDebugEnabled()) {
			log.debug("servletPath=" + this.dispatcherServletPath);
			log.debug("zuulServletPath=" + this.zuulServletPath);
			log.debug("RequestUtils.isDispatcherServletRequest()="
					+ RequestUtils.isDispatcherServletRequest());
			log.debug("RequestUtils.isZuulServletRequest()="
					+ RequestUtils.isZuulServletRequest());
		}

		String adjustedPath = adjustPath(path);

		ZuulRoute route = null;
		if (!matchesIgnoredPatterns(adjustedPath)) {
			for (Entry<String, ZuulRoute> entry : this.routes.get().entrySet()) {
				String pattern = entry.getKey();
				log.debug("Matching pattern:" + pattern);
				if (this.pathMatcher.match(pattern, adjustedPath)) {
					route = entry.getValue();
					break;
				}
			}
		}
		if (log.isDebugEnabled()) {
			log.debug("route matched=" + route);
		}

		return getRoute(route, adjustedPath);

	}

	private Route getRoute(ZuulRoute route, String path) {
		if (route == null) {
			return null;
		}
		String targetPath = path;
		String prefix = this.properties.getPrefix();
		if (path.startsWith(prefix) && this.properties.isStripPrefix()) {
			targetPath = path.substring(prefix.length());
		}
		if (route.isStripPrefix()) {
			int index = route.getPath().indexOf("*") - 1;
			if (index > 0) {
				String routePrefix = route.getPath().substring(0, index);
				targetPath = targetPath.replaceFirst(routePrefix, "");
				prefix = prefix + routePrefix;
			}
		}
		Boolean retryable = this.properties.getRetryable();
		if (route.getRetryable() != null) {
			retryable = route.getRetryable();
		}
		return new Route(route.getId(), targetPath, route.getLocation(), prefix,
				retryable,
				route.isCustomSensitiveHeaders() ? route.getSensitiveHeaders() : null);
	}

	// 注意这个类并没有实现refresh接口,
	// 但是却提供了一个protected级别的方法
	// 旨在让子类不需要重复维护一个private AtomicReference<Map<String, ZuulRoute>> routes = new AtomicReference<>();
	// 也可以达到刷新的效果
	protected void doRefresh() {
		this.routes.set(locateRoutes());
	}

	// 具体就是在这儿定位路由信息的,我们之后从数据库加载路由信息,主要也是从这儿改写
	protected Map<String, ZuulRoute> locateRoutes() {
		LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();
		for (ZuulRoute route : this.properties.getRoutes().values()) {
			routesMap.put(route.getPath(), route);
		}
		return routesMap;
	}

        // 省略部分实现
	..........
}

省略的部分,有兴趣的小伙伴,可以直接翻查源码。

       分析源码之后,我们就是实现自己的RouteLocator,代码如下所示:

@Slf4j
public class CustomRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator{
    private JdbcTemplate jdbcTemplate;

    private ZuulProperties properties;

    public void setJdbcTemplate(JdbcTemplate jdbcTemplate){
        this.jdbcTemplate = jdbcTemplate;
    }

    public CustomRouteLocator(String servletPath, ZuulProperties properties) {
        super(servletPath, properties);
        this.properties = properties;
        log.info("servletPath:{}",servletPath);
    }

    //父类已经提供了这个方法,这里写出来只是为了说明这一个方法很重要!!!
    @Override
    public void refresh() {
        super.doRefresh();
    }

    @Override
    protected Map<String, ZuulRoute> locateRoutes() {
        LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<>();
        //从application.properties中加载路由信息
        routesMap.putAll(super.locateRoutes());
        //从db中加载路由信息
        routesMap.putAll(locateRoutesFromDB());
        //优化一下配置
        LinkedHashMap<String, ZuulRoute> values = new LinkedHashMap<>();
        for (Map.Entry<String, ZuulRoute> entry : routesMap.entrySet()) {
            String path = entry.getKey();
            // Prepend with slash if not already present.
            if (!path.startsWith("/")) {
                path = "/" + path;
            }
            if (StringUtils.hasText(this.properties.getPrefix())) {
                path = this.properties.getPrefix() + path;
                if (!path.startsWith("/")) {
                    path = "/" + path;
                }
            }
            values.put(path, entry.getValue());
        }
        log.info("locateRoutes:{}", values);
        return values;
    }

    private Map<String, ZuulRoute> locateRoutesFromDB(){
        Map<String, ZuulRoute> routes = new LinkedHashMap<>();
        List<ZuulRouteVO> results = jdbcTemplate.query("select * from gateway_api_define where enabled = 1 ",new BeanPropertyRowMapper<>(ZuulRouteVO.class));
        for (ZuulRouteVO result : results) {
            if(org.apache.commons.lang3.StringUtils.isAnyEmpty(result.getPath(), result.getUrl())){
                continue;
            }
            ZuulRoute zuulRoute = new ZuulRoute();
            try {
                org.springframework.beans.BeanUtils.copyProperties(result,zuulRoute);
            } catch (Exception e) {
                log.error("=============load zuul route info from db with error==============",e);
            }
            routes.put(zuulRoute.getPath(),zuulRoute);
        }
        return routes;
    }
}

在配置文件中添加下DB的配置:

spring.datasource.url=jdbc:mysql://xxxxxx/xxxxx
spring.datasource.username=xxxx
spring.datasource.password=xxxx
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

logging.level.jdbc.sqltiming=INFO
logging.level.jdbc.sqlonly=OFF
logging.level.jdbc.audit=OFF
logging.level.jdbc.resultset=OFF
logging.level.jdbc.connection=OFF

配置下CustomRouteLocator

@Configuration
public class CustomZuulConfig {

  @Autowired
  ZuulProperties zuulProperties;
  @Autowired
  ServerProperties server;
  @Autowired
  JdbcTemplate jdbcTemplate;

  @Bean
  public CustomRouteLocator routeLocator() {
    CustomRouteLocator routeLocator = new CustomRouteLocator(this.server.getServletPrefix(), this.zuulProperties);
    routeLocator.setJdbcTemplate(jdbcTemplate);
    return routeLocator;
  }
}

        现在容器启动时,就可以从数据库和配置文件中一起加载路由信息了,离动态路由还差最后一步,就是实时刷新,前面已经说过了,默认的ZuulConfigure已经配置了事件监听器,我们只需要发送一个事件就可以实现刷新了。

@Service
public class RefreshRouteService {

  @Autowired
  ApplicationEventPublisher publisher;

  @Autowired
  RouteLocator routeLocator;

  public void refreshRoute() {
    RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
    publisher.publishEvent(routesRefreshedEvent);
  }
}

4. 总结

       这里实现的动态路由,只是给小伙伴们提供一个思路。当然,解决问题的方法有很多。所以,欢迎小伙伴们大胆尝试。

相关推荐