Spring Cloud Zuul 从源码理解如何实现动态路由(加载数据库数据)

如果你正在使用Spring Cloud 那么网关层必不可少,那么必定会遇到这样的一个情况,如果新增一个服务如何去更新网关和新服务之间的映射关系呢?修改配置文件然后重启服务,这是最简单也是最常见的方法,难道每次新增一个服务就要重启一次吗?那服务不允许重启呢?像电商项目这种是绝对不允许在生产环境频繁重启服务的,那就引出下面我们要说的。

Spring Cloud Zuul 可以动态加载路由配置,新增一个服务,只需要更改数据库表,zuul会去动态加载,如果您正在使用zuul那么可以花点时间来学学原理和实现

以下分为原理讲解和如何实现,不想看原理可以直接看如何实现的

实现原理

不管学习什么源码, 我们需要找到入口,Zuul的入口就是@EnableZuulProxy
以下截取重点代码:

@Configuration
public class ZuulProxyMarkerConfiguration {
    @Bean
    public Marker zuulProxyMarkerBean() {
        return new Marker();//这里new 了个对象 里面也是new 个对象
    }
    class Marker {

    }
}

看到这里,可能有同学就有点一头雾水的感觉,啥意思呢,熟悉springboot的同学知道这里其实用到springboot的自动配置,再截取一段代码就明白了

@Configuration
@Import({ RibbonCommandFactoryConfiguration.RestClientRibbonConfiguration.class,
        RibbonCommandFactoryConfiguration.OkHttpRibbonConfiguration.class,
        RibbonCommandFactoryConfiguration.HttpClientRibbonConfiguration.class,
        HttpClientConfiguration.class })
@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class)//其实这里才是真正的入口
// springboot 条件注解使用
public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration {
...
}

既然知道入口,接下来这里面的东西我就挑重点来说了,ZuulProxyAutoConfiguration这个类主要是发现服务用的,我们重点看下它的父类ZuulServerAutoConfiguration
还是来波关键代码

@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass({ ZuulServlet.class, ZuulServletFilter.class })
@ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class)
// Make sure to get the ServerProperties from the same place as a normal web app would
// FIXME @Import(ServerPropertiesAutoConfiguration.class)
public class ZuulServerAutoConfiguration {
...
        /**
          * 路由定位器,这是Zuul 执行转发的关键,如何转发请求就是靠它
        */
        @Bean
    @ConditionalOnMissingBean(SimpleRouteLocator.class)
    public SimpleRouteLocator simpleRouteLocator() {
        return new SimpleRouteLocator(this.server.getServlet().getContextPath(),
                this.zuulProperties);
    }

...

/**
   * 看名字都猜到一大半了,对,这就是Zuul刷新路由的监听器
 */
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
                    || event instanceof InstanceRegisteredEvent) {
                reset();
            }
            else if (event instanceof ParentHeartbeatEvent) {
                ParentHeartbeatEvent e = (ParentHeartbeatEvent) event;
                resetIfNeeded(e.getValue());
            }
            else if (event instanceof HeartbeatEvent) {
                HeartbeatEvent e = (HeartbeatEvent) event;
                resetIfNeeded(e.getValue());
            }
        }

        private void resetIfNeeded(Object value) {
            if (this.heartbeatMonitor.update(value)) {
                reset();
            }
        }
 // 下面这个方法就是Zuul 实现动态路由刷新的关键方法
 // 每次请求zuul都会检查dirt这个属性,如果为true 则会重新注册一次路由规则
//其实Spring Cloud 有一个守护线程会轮询检测服务健康状态,也就是HeartbeatEvent事件
        private void reset() {
            this.zuulHandlerMapping.setDirty(true);
        }

    }
...
}

其实看到这里,大概也明白该怎么做了,我们再进setDirty 方法看看

public void setDirty(boolean dirty) {
        this.dirty = dirty;
        if (this.routeLocator instanceof RefreshableRouteLocator) {
            ((RefreshableRouteLocator) this.routeLocator).refresh();
        }
    }

划重点,必须要是RefreshableRouteLocator这个类的子类才会刷新路由,至此, 我们已经理清刷新逻辑,接下来我们来看看如何实现

如何实现

我们就先来实现RefreshableRouteLocator 这个类

public abstract class AbstractRefreshRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {
    
    private ZuulProperties zuulProperties;
    
    public AbstractRefreshRouteLocator(String servletPath, ZuulProperties properties) {
        super(servletPath, properties);
        this.zuulProperties = properties;
    }

    @Override
    public void refresh() {
                //这里调用的是父类方法,其实调用的是下面locateRoutes 方法
        doRefresh();
    }
    
       // 核心方法
    @Override
    protected Map<String, ZuulRoute> locateRoutes() {
        LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<>();
        routesMap.putAll(super.locateRoutes());//加载父类路由
        routesMap.putAll(this.loadRoute());//加载自定义路由
        
        LinkedHashMap<String, ZuulRoute> values = new LinkedHashMap<>();
//下面只是特殊情况处理
        for (Entry<String, ZuulRoute> entry : routesMap.entrySet()) {
            String path = entry.getKey();
            if (!path.startsWith("/")) {
                path = "/" + path;
            }
            if (StringUtils.hasText(this.zuulProperties.getPrefix())) {
                path = this.zuulProperties.getPrefix() + path;
                if (!path.startsWith("/")) {
                    path = "/" + path;
                }
            }
            values.put(path, entry.getValue());
        }
        
        return values;
    }
    
//这里使用了模板方法模式
// 因为加载路由的方式有很多,我们就留给子类去实现(我们用数据库实现)
    public abstract Map<String, ZuulRoute> loadRoute();

}

然后我们看看子类如何写

public class RefreshRouteFromDBLocator extends AbstractRefreshRouteLocator {
    
    private JdbcTemplate jdbcTemplate;
    
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public RefreshRouteFromDBLocator(String servletPath, ZuulProperties properties) {
        super(servletPath, properties);
    }
        
//一目了然
    @Override
    public Map<String, ZuulRoute> loadRoute() {
        LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<>();
        String sql = "SELECT * FROM route";
        List<Map<String, Object>> routes = jdbcTemplate.queryForList(sql);
        
        for (Map<String, Object> map : routes) {
            ZuulRoute zuulRoute = new ZuulRoute();
            zuulRoute.setId(map.get("route_id").toString());
            zuulRoute.setPath(map.get("path").toString());
            zuulRoute.setServiceId(map.get("service_id").toString());
            
            routesMap.put(map.get("path").toString(), zuulRoute);
        }
        return routesMap;
    }

}

写到这里,核心就完了,就这么点代码就可以实现动态路由,上面说了Spring Cloud 有个守护线程一直轮询,但我们为了保险起见,还是发布一个刷新事件

我这里使用http请求方式发布刷新事件,可以根据自己业务需求更改

@RequestMapping("/route")
@RestController
public class RouteController {
    
    @Autowired
    private ApplicationEventPublisher publisher;
    
    @Autowired
    private RefreshRouteFromDBLocator locator;
    
    @GetMapping("/refresh")
    public String refresh() {
        publisher.publishEvent(new RoutesRefreshedEvent(locator));
        return "success";
    }

}

结束语

其实只要理解到位从源码到实现也没多少东西,最近在研究网关层的技术,我写这篇博客也是为了巩固学习使用,希望能帮助到大家,有什么问题欢迎私信。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容