这篇文章不止是普通的动态路由,他可以帮你将zuul网关由静态路由升级为动态路由,还可以在路由之后,再将利用自定义规则实现同一个URL请求根据用户(或者别的属性)访问到不同的服务中。
我在的公司是个半外包的公司,想做成一个产品但是又要满足不同客户的不同需求。采用微服务拆分所有业务模块,每一个业务模块根据不同的客户可能会有不同的场景。对于前端请求和后端服务间相互调用来说,不可能因为一个模块新增了一种业务场景就重写或者修改一次代码。于是就想,同一个模块的所有场景能否用同一个URL来请求,根据客户不同来请求不同的服务的接口。这样就有了这两个动态路由和场景选择。
源码在这里:动态路由源码 ,这个项目是我的微服务脚手架项目,基于eureka-zuul构建的。
动态配置与网上其他文章的思路是一样的,继承org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator
重写locateRoutes
方法并实现org.springframework.cloud.netflix.zuul.filters.RefreshableRouteLocator
接口的refresh
方法。
SimpleRouteLocator
是zuul基础路由加载类,初始化的时候自动将配置文件中配置的路由规则加载到内存中,重写locateRoutes
方法将路由规则配置改为自定义的源。RefreshableRouteLocator
接口仅提供刷新路由表的方法,实现很简单。
代码如下:
/**
* 动态路由实现
* @author 無痕剑
* @date 2018/11/15 22:05
*/
public class DynamicRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {
/**
* 是否启用动态路由。
* 在配置文件中:saas.routes.dynamic.enabled,默认为false
*/
private boolean enabled;
public DynamicRouteLocator(boolean enabled, String servletPath, ZuulProperties properties) {
super(servletPath, properties);
this.enabled = enabled;
}
/**
* 重载路由规则
*/
@Override
protected Map locateRoutes() {
if (!enabled) {
return super.locateRoutes();
}
Map routeMap = new HashMap<>();
// 从数据源获取路由配置
// 先模拟几个配置
String path = "/log/**";
String serviceId = "service-log";
// 任意一个为空,则不进行动态路由
if (StringUtil.isBlank(path) || StringUtil.isBlank(serviceId)) {
return super.locateRoutes();
}
// 生成ZuulRoute对象
ZuulProperties.ZuulRoute zuulRoute = createZuulRoute(path, serviceId);
routeMap.put(path, zuulRoute);
String path1 = "/basedata/**";
String serviceId2 = "service-basedata";
ZuulProperties.ZuulRoute zuulRoute1 = createZuulRoute(path1, serviceId2);
routeMap.put(path1, zuulRoute1);
return routeMap;
}
/**
* 刷新路由
*/
@Override
public void refresh() {
super.doRefresh();
}
public boolean getEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* 生成ZuulRoute对象
* @param path 映射路径
* @param serviceId 服务Id
*/
private ZuulProperties.ZuulRoute createZuulRoute(String path, String serviceId) {
ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
zuulRoute.setId(path);
zuulRoute.setPath(path);
zuulRoute.setServiceId(serviceId);
return zuulRoute;
}
}
完成自定义路由规则加载以后,创建Bean,让Spring帮我们管理它:
/**
* 动态路由配置
* @author: Overload
* @review:
* @date: 2018/11/22 9:29
*/
@Configuration
public class DynamicRouteConfig {
@Value("${saas.routes.dynamic.enabled}")
private boolean enabledDynamicRoute;
/**
* 使用自定义的路由策略代替默认路由策略
*/
@Bean
public SimpleRouteLocator routeLocator(ZuulProperties zuulProperties) {
return new DynamicRouteLocator(enabledDynamicRoute, zuulProperties.getPrefix(), zuulProperties);
}
}
ZuulProperties在zuul框架中已经创建了对应的Bean了,这里我们只需要注入它即可,不需要再创建了。
这个思路是zuul路由是在filterType为route的过滤器之后实现的,实际上在进入第一个filter之前,zuul就已经将请求上下文RequestContext
中的FilterConstants.SERVICE_ID_KEY
已经设置好了(具体是根据1.中的配置进行设置的)。那么在这里,我们只需要替换这个SERVICE_ID_KEY为我们自定义规则匹配后的值即可。
这里有个小提示,放置这个自定义路由过滤器的位置很关键,如果你有前置处理请求参数或者用户信息鉴权等filter的话,记得把这个filter放置在他们后面配置filterOrder的值即可。
/**
* 场景选择过滤器。
* 查询一个静态常量,匹配到请求用户的对应场景的 serviceId
,并设置到请求上下文中的属性{@link FilterConstants#SERVICE_ID_KEY}中。
* 该过滤器放置在 {@link PreDecorationFilter} 之后,
* 相当于替换 PreDecorationFilter
已经设置好的 serviceId
。
* 如果放置在 PreDecorationFilter
之前, PreDecorationFilter
会不进行过滤。就需要重写 PreDecorationFilter
的功能。
* @author 無痕剑
* @date 2018/11/23 23:40
*/
@Slf4j
@Component
public class SceneSelectorFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
/**
* 将该过滤器放置在PreDecorationFilter之后
*/
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER + 1;
}
/**
* 所有请求都要适配场景
*/
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
// 获取当前请求上下文
RequestContext currentContext = RequestContext.getCurrentContext();
// 获取URI
String uri = currentContext.getRequest().getRequestURI();
// 获取服务名称
String serviceName = RequestUtil.matchServiceName(uri);
// 获取用户信息
// 根据serviceName和user.companyId获取场景
// 将请求头中的serviceId设置为对应场景
currentContext.set(FilterConstants.SERVICE_ID_KEY, "service-basedata");
return null;
}
}
需要将一个FeignClient的请求根据客户信息,发送到不同的服务上去。
例如,客户C1-C3,订单服务O1-O3,商品服务P。客户均访问同一个商品服务,商品服务中一个接口会直接创建订单,请求接口为/service-order/create。现在要求C1只能访问O1,C2只能访问O2,C3只能访问O3,在不改变服务P的情况下,需要进行此业务处理。
思路大体上与zuul层修改请求路由路径类似,都是修改serviceId来达到对请求的修改。FeignClient默认采用的是org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient
进行FeignClient请求处理,execute
方法有两个参数,我们需要的是feign.Request
这个参数,将里面对应的URL中的serviceId替换为我们自定义规则匹配的serviceId即可。
继承LoadBalancerFeignClient,重写execute方法即可,最后再调用super.execute()来进行请求。
/**
* 动态路由FeignClient
* @author 無痕剑
* @date 2018/11/27 22:49
*/
public class DynamicRouteFeignClient extends LoadBalancerFeignClient {
public DynamicRouteFeignClient(Client delegate, CachingSpringLoadBalancerFactory lbClientFactory, SpringClientFactory clientFactory) {
super(delegate, lbClientFactory, clientFactory);
}
/**
* 重写这个方法,在前面加上动态路由。
* 其实就是修改URL
*/
@Override
public Response execute(Request request, Request.Options options) throws IOException {
/*
获取远程调用的URL
格式:http://service-log/operation/remote/listByTypeAndId?mappingType=asd&mappingId=12&companyId=11
*/
String url = request.url();
// 获取请求接口的serviceId
String serviceId = StringUtil.urlServiceId(url);
// 看获取场景是通过本地服务连缓存系统还是连配置中心服务获取
// 连接配置中心的请求,需要跳过下面几步,直接return
//TODO 获取用户购买的场景对应的serivceId
String nServiceId = "";
// 修改URL中serviceId为对应场景serviceId
String newUrl = StringUtil.changeServiceId(url, nServiceId);
//生成新请求
Request newRequest = Request.create(request.method(), newUrl, request.headers(), request.body(), request.charset());
// 执行请求
return super.execute(newRequest, options);
}
}
这里由于构造器为含参构造器,创建Bean的时候,需要注入这三个参数。CachingSpringLoadBalancerFactory cachingFactory
与SpringClientFactory clientFactory
在feign框架中已经声明为Bean了,只是使用了@ConditionalOnMissingBean
注解,可能IDEA之类的编辑器会提示无法找到对应的Bean,但是没有关系,依然可以自动注入。
创建Bean配置文件如下:
/**
* @author 無痕剑
* @date 2018/11/27 23:38
*/
@Configuration
public class DynamicRouteFeignConfig {
@Bean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory, SpringClientFactory clientFactory) {
return new DynamicRouteFeignClient(new Client.Default(null, null), cachingFactory, clientFactory);
}
}
这样就整体实现了动态路由与请求。阅读源码可以很方便的找到你想要的东西。