前言
Zuul 是Netflix 提供的一个开源组件,致力于在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。也有很多公司使用它来作为网关的重要组成部分,碰巧今年公司的架构组决定自研一个网关产品,集动态路由,动态权限,限流配额等功能为一体,为其他部门的项目提供统一的外网调用管理,最终形成产品(这方面阿里其实已经有成熟的网关产品了,但是不太适用于个性化的配置,也没有集成权限和限流降级)。
不过这里并不想介绍整个网关的架构,而是想着重于讨论其中的一个关键点,并且也是经常在交流群中听人说起的:动态路由怎么做?
再阐释什么是动态路由之前,需要介绍一下架构的设计。
传统互联网架构图
上图是没有网关参与的一个最典型的互联网架构(本文中统一使用book代表应用实例,即真正提供服务的一个业务系统)
加入eureka的架构图
book注册到eureka注册中心中,zuul本身也连接着同一个eureka,可以拉取book众多实例的列表。服务中心的注册发现一直是值得推崇的一种方式,但是不适用与网关产品。因为我们的网关是面向众多的其他部门的已有或是异构架构的系统,不应该强求其他系统都使用eureka,这样是有侵入性的设计。
最终架构图
要强调的一点是,gateway最终也会部署多个实例,达到分布式的效果,在架构图中没有画出,请大家自行脑补。
本博客的示例使用最后一章架构图为例,带来动态路由的实现方式,会有具体的代码。
动态路由
动态路由需要达到可持久化配置,动态刷新的效果。如架构图所示,不仅要能满足从spring的配置文件properties加载路由信息,还需要从数据库加载我们的配置。另外一点是,路由信息在容器启动时就已经加载进入了内存,我们希望配置完成后,实施发布,动态刷新内存中的路由信息,达到不停机维护路由信息的效果。
zuul–HelloWorldDemo
项目结构
- <groupId>com.sinosoft</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>gateway</module>
- <module>book</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>
- </dependencies>
- </dependencyManagement>
tip:springboot-1.5.2对应的springcloud的版本需要使用Camden.SR6,一开始想专门写这个demo时,只替换了springboot的版本1.4.0->1.5.2,结果启动就报错了,最后发现是版本不兼容的锅。
gateway项目:
启动类:GatewayApplication.java
- @EnableZuulProxy
- @SpringBootApplication
- public class GatewayApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(GatewayApplication.class, args);
- }
-
- }
配置:application.properties
- #配置在配置文件中的路由信息
- zuul.routes.books.url=http://localhost:8090
- zuul.routes.books.path=/books/**
- #不使用注册中心,会带来侵入性
- ribbon.eureka.enabled=false
- #网关端口
- server.port=8080
book项目:
启动类:BookApplication.java
- @RestController
- @SpringBootApplication
- public class BookApplication {
-
- @RequestMapping(value = "/available")
- public String available() {
- System.out.println("Spring in Action");
- return "Spring in Action";
- }
-
- @RequestMapping(value = "/checked-out")
- public String checkedOut() {
- return "Spring Boot in Action";
- }
-
- public static void main(String[] args) {
- SpringApplication.run(BookApplication.class, args);
- }
- }
配置类:application.properties
测试访问:http://localhost:8080/books/available
上述demo是一个简单的静态路由,简单看下源码,zuul是怎么做到转发,路由的。
我们要解决动态路由的难题,第一步就得理解路由定位器的作用。
很失望,因为从接口关系来看,spring考虑到了路由刷新的需求,但是默认实现的SimpleRouteLocator没有实现RefreshableRouteLocator接口,看来我们只能借鉴DiscoveryClientRouteLocator去改造SimpleRouteLocator使其具备刷新能力。
- public interface RefreshableRouteLocator extends RouteLocator {
- void refresh();
- }
DiscoveryClientRouteLocator比SimpleRouteLocator多了两个功能,第一是从DiscoveryClient(如Eureka)发现路由信息,之前的架构图已经给大家解释清楚了,我们不想使用eureka这种侵入式的网关模块,所以忽略它,第二是实现了RefreshableRouteLocator接口,能够实现动态刷新。
对SimpleRouteLocator.class的源码加一些注释,方便大家阅读:
- 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;
- }
-
- @Override
- public Collection<String> getIgnoredPaths() {
- return this.properties.getIgnoredPatterns();
- }
-
- //这个方法在网关产品中也很重要,可以根据实际路径匹配到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());
- }
-
-
- //具体就是在这儿定位路由信息的,我们之后从数据库加载路由信息,主要也是从这儿改写
- /**
- * Compute a map of path pattern to route. The default is just a static map from the
- * {@link ZuulProperties}, but subclasses can add dynamic calculations.
- */
- 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;
- }
-
- protected boolean matchesIgnoredPatterns(String path) {
- for (String pattern : this.properties.getIgnoredPatterns()) {
- log.debug("Matching ignored pattern:" + pattern);
- if (this.pathMatcher.match(pattern, path)) {
- log.debug("Path " + path + " matches ignored pattern " + pattern);
- return true;
- }
- }
- return false;
- }
-
- private String adjustPath(final String path) {
- String adjustedPath = path;
-
- if (RequestUtils.isDispatcherServletRequest()
- && StringUtils.hasText(this.dispatcherServletPath)) {
- if (!this.dispatcherServletPath.equals("/")) {
- adjustedPath = path.substring(this.dispatcherServletPath.length());
- log.debug("Stripped dispatcherServletPath");
- }
- }
- else if (RequestUtils.isZuulServletRequest()) {
- if (StringUtils.hasText(this.zuulServletPath)
- && !this.zuulServletPath.equals("/")) {
- adjustedPath = path.substring(this.zuulServletPath.length());
- log.debug("Stripped zuulServletPath");
- }
- }
- else {
- // do nothing
- }
-
- log.debug("adjustedPath=" + path);
- return adjustedPath;
- }
-
- }
重写过后的自定义路由定位器如下:
- public class CustomRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator{
-
- public final static Logger logger = LoggerFactory.getLogger(CustomRouteLocator.class);
-
- 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;
- logger.info("servletPath:{}",servletPath);
- }
-
- //父类已经提供了这个方法,这里写出来只是为了说明这一个方法很重要!!!
- // @Override
- // protected void doRefresh() {
- // super.doRefresh();
- // }
-
-
- @Override
- public void refresh() {
- doRefresh();
- }
-
- @Override
- protected Map<String, ZuulRoute> locateRoutes() {
- LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();
- //从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());
- }
- 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 = true ",new BeanPropertyRowMapper<>(ZuulRouteVO.class));
- for (ZuulRouteVO result : results) {
- if(org.apache.commons.lang3.StringUtils.isBlank(result.getPath()) || org.apache.commons.lang3.StringUtils.isBlank(result.getUrl()) ){
- continue;
- }
- ZuulRoute zuulRoute = new ZuulRoute();
- try {
- org.springframework.beans.BeanUtils.copyProperties(result,zuulRoute);
- } catch (Exception e) {
- logger.error("=============load zuul route info from db with error==============",e);
- }
- routes.put(zuulRoute.getPath(),zuulRoute);
- }
- return routes;
- }
-
- public static class ZuulRouteVO {
-
- /**
- * The ID of the route (the same as its map key by default).
- */
- private String id;
-
- /**
- * The path (pattern) for the route, e.g. /foo/**.
- */
- private String path;
-
- /**
- * The service ID (if any) to map to this route. You can specify a physical URL or
- * a service, but not both.
- */
- private String serviceId;
-
- /**
- * A full physical URL to map to the route. An alternative is to use a service ID
- * and service discovery to find the physical address.
- */
- private String url;
-
- /**
- * Flag to determine whether the prefix for this route (the path, minus pattern
- * patcher) should be stripped before forwarding.
- */
- private boolean stripPrefix = true;
-
- /**
- * Flag to indicate that this route should be retryable (if supported). Generally
- * retry requires a service ID and ribbon.
- */
- private Boolean retryable;
-
- private Boolean enabled;
-
- public String getId() {
- return id;
- }
-
- public void setId(String id) {
- this.id = id;
- }
-
- public String getPath() {
- return path;
- }
-
- public void setPath(String path) {
- this.path = path;
- }
-
- public String getServiceId() {
- return serviceId;
- }
-
- public void setServiceId(String serviceId) {
- this.serviceId = serviceId;
- }
-
- public String getUrl() {
- return url;
- }
-
- public void setUrl(String url) {
- this.url = url;
- }
-
- public boolean isStripPrefix() {
- return stripPrefix;
- }
-
- public void setStripPrefix(boolean stripPrefix) {
- this.stripPrefix = stripPrefix;
- }
-
- public Boolean getRetryable() {
- return retryable;
- }
-
- public void setRetryable(Boolean retryable) {
- this.retryable = retryable;
- }
-
- public Boolean getEnabled() {
- return enabled;
- }
-
- public void setEnabled(Boolean enabled) {
- this.enabled = enabled;
- }
- }
- }
配置这个自定义的路由定位器:
- @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已经配置了事件监听器,我们只需要发送一个事件就可以实现刷新了。
- public class RefreshRouteService {
-
- @Autowired
- ApplicationEventPublisher publisher;
-
- @Autowired
- RouteLocator routeLocator;
-
- public void refreshRoute() {
- RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
- publisher.publishEvent(routesRefreshedEvent);
- }
-
- }
具体的刷新流程其实就是从数据库重新加载了一遍,有人可能会问,为什么不自己是手动重新加载Locator.dorefresh?非要用事件去刷新。这牵扯到内部的zuul内部组件的工作流程,不仅仅是Locator本身的一个变量,具体想要了解的还得去看源码。
到这儿我们就实现了动态路由了,所以的实例代码和建表语句我会放到github上,下载的时候记得给我star QAQ !!!
链接:https://github.com/lexburner/zuul-gateway-demo
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持w3xue。