如果你正在使用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";
}
}
结束语
其实只要理解到位从源码到实现也没多少东西,最近在研究网关层的技术,我写这篇博客也是为了巩固学习使用,希望能帮助到大家,有什么问题欢迎私信。