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 {

        @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 locateRoutes() {
        LinkedHashMap routesMap = new LinkedHashMap<>();
        routesMap.putAll(super.locateRoutes());//加载父类路由
        routesMap.putAll(this.loadRoute());//加载自定义路由
        
        LinkedHashMap values = new LinkedHashMap<>();
//下面只是特殊情况处理
        for (Entry 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 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 loadRoute() {
        LinkedHashMap routesMap = new LinkedHashMap<>();
        String sql = "SELECT * FROM route";
        List> routes = jdbcTemplate.queryForList(sql);
        
        for (Map 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";
    }

}

结束语

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

你可能感兴趣的:(Spring Cloud Zuul 从源码理解如何实现动态路由(加载数据库数据))