springcloud之动态路由
1. 背景
Zuul是Netflix提供的一个开源组件,Zuul致力于在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。小弟所在的公司,是使用它来作为网关的重要组成部分。今天就通过一个简单的实例,来具体说明一下是怎么实现的动态路由。
2. 架构演变
为了更好的帮助小伙伴们理解后面的demo,先来做个简单的架构演变,如下图所示:

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

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

我这边实现的简单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,如下所示:

从这个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. 总结
这里实现的动态路由,只是给小伙伴们提供一个思路。当然,解决问题的方法有很多。所以,欢迎小伙伴们大胆尝试。