由于公司环境较多,为避免频繁更新网关路由表配置,同时方便测试时可以略过网关解密功能,需要根据固定规则的请求url自动生成路由表,并转发请求到后端服务。
要求:
- 支持注册中心服务名与固定ip转发请求。
- 兼容配置文件中已经配置的路由转发规则
- 只有配置文件中允许的模块才允许动态路由
- 不允许重复创建并刷新路由,防止请求过慢
实际测试来看,如果请求走动态创建路由规则后再转发请求到后端接口,则请求要慢15倍左右。
1. 创建过滤器拦截所有请求url并判断是否需要创建动态路由
@Component
public class DynamicRouteFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(DynamicRouteFilter.class);
@Autowired
private DynamicRouteLocator dynamicRouteLocator;
//允许动态路由的模块名
@Value("${dynamic-route-module}")
private String moduleName;
// 允许动态路由的模块名集合
private static LinkedHashSet<String> moduleNames = new LinkedHashSet<>();
// 健康检查路径
private final static String HEALTH_URL = "/health";
private final static String BACKSLASH = "/";
/**
* 加载配置文件中允许动态路由的模块名
*
* @param filterConfig
* @throws ServletException
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Collections.addAll(moduleNames, moduleName.split(","));
}
/**
* 获取请求路径,判断是否需要创建路由
*
* @param servletRequest
* @param servletResponse
* @param filterChain
* @throws IOException
* @throws ServletException
*/
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String requestURI = request.getRequestURI(); // /apaas/api/user-service/getalluser
if (BACKSLASH.equals(requestURI)){
logger.error("request url is blank");
filterChain.doFilter(servletRequest, servletResponse);
return;
}
String[] urls = requestURI.split(BACKSLASH);
if (requestURI.endsWith(HEALTH_URL) ||
(StringUtils.isNotBlank(urls[1]) && !moduleNames.contains(urls[1]))) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
StringBuffer result = new StringBuffer(64);
boolean notBlank = true;
for (int i = 1; i < 4; i++) {
notBlank = notBlank && StringUtils.isNotBlank(urls[i]);
}
if (notBlank) {
result.append("/").append(urls[1]).append("/").append(urls[2]).append("/")
.append(urls[3]).append("/**");
}
if (this.create(result.toString())) {
filterChain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void destroy() {
}
/**
* 如果路由表中没有,则创建动态路由
*
* @param result
* @return
*/
private synchronized boolean create(String result) {
Map<String, ZuulProperties.ZuulRoute> routeMap = dynamicRouteLocator.getRouteMap();
if (!routeMap.containsKey(result)) {
dynamicRouteLocator.getUrl(result);
}
return true;
}
}
2. spring-cloud-zuul要实现自定义路由需要继承SimpleRouteLocator类并实现路由刷新接口RefreshableRouteLocator
public class DynamicRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {
private static final Logger logger = LoggerFactory.getLogger(DynamicRouteLocator.class);
@Autowired
private ZuulHandlerMapping zuulHandlerMapping;
// 初始化路由规则map长度
private final static int mapSize = 1024;
// 初始化路由规则id长度
private final static int routeIdSize = 64;
// 路由规则map
private static Map<String, ZuulProperties.ZuulRoute> routeMap;
//路由转发路径
private String url = null;
/**
* 加载配置文件中的路由配置并且创建新路由表
*
* @param servletPath
* @param properties
*/
public DynamicRouteLocator(String servletPath, ZuulProperties properties) {
super(servletPath, properties);
routeMap = new HashMap<>(mapSize);
routeMap.putAll(super.locateRoutes());
logger.info("load " + super.locateRoutes().size() + " Routes rule from yml");
}
/**
* 获取请求url,刷新路由
*
* @param url
*/
public void getUrl(String url) {
this.url = url;
this.refresh();
}
/**
* 获取请求url,刷新路由
*
* @return
*/
public Map<String, ZuulProperties.ZuulRoute> getRouteMap() {
return locateRoutes();
}
/**
* 拼接信息并创建路由刷新规则
*
* @return 路由规则表
*/
@Override
protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() {
try {
String path = this.url;// /apaas/api/mp-b-configure-service/**
if (StringUtils.isBlank(path)) {
return routeMap;
}
String[] urls = path.split("/");
if (urls.length < 5) {
logger.error("urls length not match");
return routeMap;
}
String target = "";
ZuulProperties.ZuulRoute zuulRoute = null;
StringBuilder id = new StringBuilder(routeIdSize);
id.append(urls[1]).append("-").append(urls[2]).append("-").append(urls[3]);
if (urls[3].startsWith("ip_")) {
String[] ipAndPort = urls[3].split("_");
target = "http://" + ipAndPort[1] + ":" + ipAndPort[2];
// 生成转发到固定ip port的ZuulRoute对象
zuulRoute = createZuulRouteWithUrl(id.toString(), path, target);
} else {
target = urls[3];
// 生成转发到服务名的ZuulRoute对象
zuulRoute = createZuulRouteWithServiceID(id.toString(), path, target);
}
routeMap.put(path, zuulRoute);
this.url = null;
// 刷新路由,添加到SpringMvc的HandlerMapping链中,只有选择了ZuulHandlerMapping的请求才能出发到Zuul的后续流程
zuulHandlerMapping.setDirty(true);
} catch (Exception e) {
e.printStackTrace();
}
return routeMap;
}
@Override
public void refresh() {
locateRoutes();
}
/**
* 创建路由规则
*
* @param id 路由表唯一id
* @param path 映射路径
* @param serviceId 服务名
*/
private ZuulProperties.ZuulRoute createZuulRouteWithServiceID(String id, String path, String serviceId) {
ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
zuulRoute.setId(id);
zuulRoute.setPath(path);
zuulRoute.setServiceId(serviceId);
zuulRoute.setStripPrefix(true);
logger.info("创建了路由 " + zuulRoute.toString());
return zuulRoute;
}
/**
* 创建路由规则
*
* @param id 路由表唯一id
* @param path 映射路径
* @param url 固定ip:port
*/
private ZuulProperties.ZuulRoute createZuulRouteWithUrl(String id, String path, String url) {
ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
zuulRoute.setId(id);
zuulRoute.setPath(path);
zuulRoute.setStripPrefix(true);
zuulRoute.setUrl(url);
logger.info("创建了路由 " + zuulRoute.toString());
return zuulRoute;
}
}
3. 启动类加载自定义路由bean
ZuulProperties springboot已经提前加载,直接使用即可
@Bean
public DynamicRouteLocator getRouteLocator(ZuulProperties zuulProperties){
return new DynamicRouteLocator(zuulProperties.getPrefix(), zuulProperties);
}
4. 网关路由规则
- Yml配置文件配置静态路由
- 例如:
sso:
path: /api/sso/**
service-id: mp-b-sso-service
- 动态路由
服务yml中配置允许动态路由的模块名称,多个模块使用,分割,只有配置了允许动态路由的模块才能走动态路由,不配置不允许走动态路由。
动态路由请求url规则:
- 例如:
http://localhost:9099/apaas/openapi/mp-b-user-service/getall
/apaas:模块名,类似apaas,pay,等等
/api:需要解密的请求
/openapi:不需要解密的请求
/mp-b-user-service:请求的后端服务在注册中心的名称
/getall:后端服务接口 - 例如:
http://localhost:9099/apaas/openapi/ip_127.0.0.1_9090/getall
按照规则的url可选择是否使用加密请求后端固定ip端口服务,无需再配置文件中添加路由
url规则:http://ip:port/模块名/是否要解密/ip_服务ip_服务端口/后端服务接口
/apaas:模块名,类似apaas,pay,等等
/api:需要解密的请求
/openapi:不需要解密的请求
ip_127.0.0.1_9090:请求的后端服务的ip端口,注意是ip_为开始标志
/getall:后端服务接口