在实现动态路由之前,有必要先看看ZuulHandlerMapping如何处理请求的过程路径如何映射到对应的处理器
ZuulHandlerMapping会查找处理的时候,去生成url与处理器的映射,具体源码如下。其实在上面的链接中已经有了,如果不想看的话,从下面的代码链路中也可以看出来,路由数据是如何加载的。
public class ZuulHandlerMapping extends AbstractUrlHandlerMapping {
@Override
protected Object lookupHandler(String urlPath, HttpServletRequest request)
throws Exception {
if (this.errorController != null
&& urlPath.equals(this.errorController.getErrorPath())) {
return null;
}
if (isIgnoredPath(urlPath, this.routeLocator.getIgnoredPaths())) {
return null;
}
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.containsKey("forward.to")) {
return null;
}
if (this.dirty) {
synchronized (this) {
if (this.dirty) {
registerHandlers();
this.dirty = false;
}
}
}
return super.lookupHandler(urlPath, request);
}
private void registerHandlers() {
Collection<Route> routes = this.routeLocator.getRoutes();
if (routes.isEmpty()) {
this.logger.warn("No routes found from RouteLocator");
}
else {
for (Route route : routes) {
registerHandler(route.getFullPath(), this.zuul);
}
}
}
}
public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping implements MatchableHandlerMapping {
protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {
Assert.notNull(urlPath, "URL path must not be null");
Assert.notNull(handler, "Handler object must not be null");
Object resolvedHandler = handler;
// Eagerly resolve handler if referencing singleton via name.
if (!this.lazyInitHandlers && handler instanceof String) {
String handlerName = (String) handler;
ApplicationContext applicationContext = obtainApplicationContext();
if (applicationContext.isSingleton(handlerName)) {
resolvedHandler = applicationContext.getBean(handlerName);
}
}
Object mappedHandler = this.handlerMap.get(urlPath);
if (mappedHandler != null) {
if (mappedHandler != resolvedHandler) {
throw new IllegalStateException(
"Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath +
"]: There is already " + getHandlerDescription(mappedHandler) + " mapped.");
}
}
else {
if (urlPath.equals("/")) {
if (logger.isTraceEnabled()) {
logger.trace("Root mapping to " + getHandlerDescription(handler));
}
setRootHandler(resolvedHandler);
}
else if (urlPath.equals("/*")) {
if (logger.isTraceEnabled()) {
logger.trace("Default mapping to " + getHandlerDescription(handler));
}
setDefaultHandler(resolvedHandler);
}
else {
this.handlerMap.put(urlPath, resolvedHandler);
if (logger.isTraceEnabled()) {
logger.trace("Mapped [" + urlPath + "] onto " + getHandlerDescription(handler));
}
}
}
}
}
如何实现动态路由?
- 1.路由加载器是如何加载的。
其实从前面已经知道,ZuulHandlerMapping加载路由数据的时候,其实用的是复合的路由加载器。
@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass({ ZuulServlet.class, ZuulServletFilter.class })
@ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class)
public class ZuulServerAutoConfiguration {
上面这个用法是比较骚气的,其实就是将多个类型的路由加载器,整合到复合的路由加载器中。
@Bean
@Primary
public CompositeRouteLocator primaryRouteLocator(
Collection<RouteLocator> routeLocators) {
return new CompositeRouteLocator(routeLocators);
}
这个时候ZuulHandlerMapping中注入的RouteLocator 便是CompositeRouteLocator
@Bean
public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController());
mapping.setErrorController(this.errorController);
mapping.setCorsConfigurations(getCorsConfigurations());
return mapping;
}
}
- 2 那么如何构建一个自己的路由加载器呢?
其实只要实现RouteLocator,注入容器即可。 - 3 路由加载器要实现的功能
- 3.1 动态路由加载
既然是动态的,那么可以通过一个服务获取,也可以通过数据库。 - 3.1.1环境准备
这里由于是通过数据库来动态管理路由数据。
所以先按zuul保存路由数据的格式来定义一个表
CREATE TABLE "route" (
"id" bigserial,
"routeid" VARCHAR(32),--路由规则id
"path" VARCHAR(32),--路径
"serviceId" VARCHAR(32),--服务id
"url" VARCHAR(128),--url
"stripPrefix" BOOLEAN,--校验前缀
"retryable" BOOLEAN,--重试
"updatetime" int8,--更新时间
"createtime" int8,--创建时间
"gatewayGroup" VARCHAR(32)--网关组,自定义字段
);
- 3.1.2 持久层
主要是用于数据的获取
DAO
public interface RouteDAO {
List<Route> getRouteByGroup(String group);
}
DAO的实现
@Repository
public class RouteDAOImpl implements RouteDAO{
@Autowired
private RouteMapper routeMapper;
@Override
public List<Route> getRouteByGroup(String group) {
RouteExample re = new RouteExample();
re.createCriteria().andGatewaygroupEqualTo(group);
return routeMapper.selectByExample(re);
}
}
接口
public interface RouteService {
获取某个网关组的路由数据
public List<ZuulRoute> getRoutes(String group);
}
接口实现
@Service
public class RouteServieImpl implements RouteService{
@Autowired
private RouteDAO routeDAO;
@Override
public List<ZuulRoute> getRoutes(String group) {
if (StringUtils.isBlank(group)) {
return null;
}
List<Route> dbData = routeDAO.getRouteByGroup(group);
if (CollectionUtils.isEmpty(dbData)) {
return null;
}
return dbData.stream().map(item -> {
return genZuulRoute(item);
}).collect(Collectors.toList());
}
private ZuulRoute genZuulRoute(Route src) {
ZuulRoute destObj = new ZuulRoute();
destObj.setId(src.getRouteid());
destObj.setPath(src.getPath());
destObj.setRetryable(src.getRetryable());
destObj.setServiceId(src.getServiceid());
destObj.setStripPrefix(src.getStripprefix());
destObj.setUrl(src.getUrl());
return destObj;
}
}
- 3.1.3 路由加载器的构建
自定义的配置类,用于加载网关组配置,需要加配置文件中添加网关组。
@Configuration
@ConfigurationProperties(prefix = "custom.zuul")
@Data
public class CustomZuulConfigurationProperties {
private String group;
}
路由加载器的实现
@Slf4j
public class DynamicRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator{
使用LoadingCache作为缓存,具有定时清理缓存的作用
private LoadingCache<String, List<ZuulRoute>> routeCache;
用于从Db中读取路由数据
@Autowired
private RouteService routeService;
网关组
private String group;
private Callable<? extends List<ZuulRoute>> callable;
public DynamicRouteLocator(ZuulProperties properties,
CustomZuulConfigurationProperties customZuulConfigurationProperties) {
super(properties.getServletPath(), properties);
this.group = customZuulConfigurationProperties.getGroup();
缓存最大为128,30秒数据没有更新自动过期
this.routeCache = CacheBuilder.newBuilder().initialCapacity(128).expireAfterWrite(30, TimeUnit.SECONDS)
.build(new CacheLoader<String, List<ZuulRoute>>() {
@Override
public List<ZuulRoute> load(String key) throws Exception {
缓存不存在自动从DB中获取
return getZuulRoutes(key);
}
});
callable = new Callable<List<ZuulRoute>>() {
@Override
public List<ZuulRoute> call() throws Exception {
return new ArrayList<ZuulRoute>(0);
}
};
}
private List<ZuulRoute> getZuulRoutes(String group) {
return routeService.getRoutes(group);
}
private List<ZuulRoute> getZuulRoutesFromCache(String group) {
List<ZuulRoute> routesFromDB = null;
try {
routesFromDB = routeCache.get(this.group);
} catch (ExecutionException e) {
log.error("getZuulRoutesFromCache error");
}
return routesFromDB;
}
这个方法应该是最重要的,用于实现加载路由的过程
@Override
protected Map<String, ZuulRoute> locateRoutes() {
通过数据库模拟获取路由. 通过group获取对应路由
List<ZuulRoute> routesFromDB = getZuulRoutesFromCache(this.group);
LinkedHashMap<String, ZuulRoute> values = new LinkedHashMap<>();
if (CollectionUtils.isEmpty(routesFromDB)) {
return values;
}
Map<String, ZuulRoute> routesMap = routesFromDB.stream().collect(Collectors.groupingBy(ZuulRoute::getPath,
Collectors.collectingAndThen(Collectors.toList(), v -> v.get(0))));
for (Entry<String, ZuulRoute> entry : routesMap.entrySet()) {
String path = entry.getKey();
if (!path.startsWith("/")) {
path = "/" + path;
}
values.put(path, entry.getValue());
}
return values;
}
刷新方法,使用父类的实现即可,也是调用locateRoutes()。
至于刷新机制的触发,其实只有容器刷新的时候,会发送刷新事件,这个时候路由会重新加载。
但是由于使用了LoadingCache,可以让缓存定时失效,也就不用手动去刷新容器了。
@Override
public void refresh() {
doRefresh();
}
}
最后,通过配置类将路由加载器注入容器
@Configuration
public class CustomZuulServerAutoConfiguration{
@Bean
public DynamicRouteLocator dynamicRouteLocator(ZuulProperties properties,
CustomZuulConfigurationProperties customZuulConfigurationProperties) {
return new DynamicRouteLocator(properties, customZuulConfigurationProperties);
}
}
这样子其实就完成了动态路由的功能了。
利用数据库以及LoadingCache,可以做到数据自动更新,就可以达到动态路由的效果了。
如果追求时效性,想让数据即可更新,可以通过手动触发容器刷新的接口,一样可以做到立马刷新的效果。
网友评论