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