需求:SpringCloud 整合 zuul路由,实现在数据库中修改serviceId 字段的值即可切换调用服务。譬如:假设在本地环境,zuul的运行端口是 8081。现在有ServiceA, ServiceB在正常运行,在zuul里配置的路由地址分别是 /service-a/** 和 /service-b/** 。现在访问A,访问地址可以写成 http://localhost:8081/service-a/xxx。现在我要实现只在数据库的路由表中改一下 serviceId,使得原来访问服务A的地址去访问到B服务上。
实现步骤如下:
1. 搭建zuul 项目
1.1 导入依赖
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- zuul -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- ribbon -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
1.2 核心代码
自定义路由处理器是实现动态路由的核心,需要继承
org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator 类
和实现 org.springframework.cloud.netflix.zuul.filters.RefreshableRouteLocator 接口
package com.atguigu.springcloud.filter;
import com.atguigu.springcloud.entity.ZuulRouteEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.RefreshableRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.StringUtils;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Created by caoshi at 21:21 2022-05-04
*/
public class CustomRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {
private static Logger logger = LoggerFactory.getLogger(CustomRouteLocator.class);
private JdbcTemplate jdbcTemplate;
private ZuulProperties properties;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public CustomRouteLocator(String servletPath, ZuulProperties properties) {
super(servletPath, properties);
this.properties = properties;
logger.info("servletPath:{}", servletPath);
}
@Override
public void refresh() {
doRefresh();
}
@Override
protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<>();
routesMap.putAll(super.locateRoutes());
routesMap.putAll(locateRoutesFromDB());
Map<String, ZuulProperties.ZuulRoute> values = new LinkedHashMap<>();
for (Map.Entry<String, ZuulProperties.ZuulRoute> entry : routesMap.entrySet()) {
String path = entry.getKey();
if (!path.startsWith("/")) {
path = "/" + path;
}
if (StringUtils.hasText(this.properties.getPrefix())) {
path = this.properties.getPrefix() + path;
if (!path.startsWith("/")) {
path = "/" + path;
}
}
values.put(path, entry.getValue());
}
return values;
}
private Map<String, ZuulProperties.ZuulRoute> locateRoutesFromDB() {
logger.info("============= 开始加载db路由表配置 ==============");
Map<String, ZuulProperties.ZuulRoute> routes = new LinkedHashMap<>();
try {
List<ZuulRouteEntity> results =
this.jdbcTemplate.query("select * from x_gw_route where enabled = 1",
new BeanPropertyRowMapper<>(ZuulRouteEntity.class));
for (ZuulRouteEntity result : results) {
if (StringUtils.isEmpty(result.getPath())) {
continue;
}
ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
copy2ZuulRoute(result, zuulRoute);
routes.put(zuulRoute.getPath(), zuulRoute);
}
} catch (DataAccessException e) {
logger.error("=============加载db中路由表配置出错==============", e);
}
return routes;
}
/**
*
* @param zuulRouteEntity
* @param zuulRoute
*/
private void copy2ZuulRoute(ZuulRouteEntity zuulRouteEntity, ZuulProperties.ZuulRoute zuulRoute) {
if (zuulRouteEntity == null || zuulRoute == null) {
return;
}
zuulRoute.setId(zuulRouteEntity.getId());
zuulRoute.setServiceId(zuulRouteEntity.getService_id());
zuulRoute.setPath(zuulRouteEntity.getPath());
zuulRoute.setStripPrefix(zuulRouteEntity.getStrip_prefix() == 1);
zuulRoute.setUrl(zuulRouteEntity.getUrl());
zuulRoute.setRetryable(zuulRouteEntity.getRetryable() == 1);
}
}
路由表映射实体类:
package com.atguigu.springcloud.entity;
/**
* Created by caoshi at 20:53 2022-06-16
*/
public class ZuulRouteEntity {
private String id; // 主键id
private String path; // 访问路径 /xxx/**
private String service_id; // 服务id 一般配成 spring.application.name
private String url; //
private int strip_prefix; // 是否去除前缀,zuul自带前缀 /zuul,所以设置成1就是去掉前缀
private int retryable; // 是否失败后重新请求
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getService_id() {
return service_id;
}
public void setService_id(String service_id) {
this.service_id = service_id;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public int getStrip_prefix() {
return strip_prefix;
}
public void setStrip_prefix(int strip_prefix) {
this.strip_prefix = strip_prefix;
}
public int getRetryable() {
return retryable;
}
public void setRetryable(int retryable) {
this.retryable = retryable;
}
}
配置类,配置 CustomRouteLocator 交由Spring容器管理。
package com.atguigu.springcloud.config;
import com.atguigu.springcloud.filter.CustomRouteLocator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
/**
* Created by caoshi at 20:51 2022-06-16
*/
@Configuration
public class CustomZuulConfig {
private static Logger logger = LoggerFactory.getLogger(CustomZuulConfig.class);
private ZuulProperties zuulProperties = new ZuulProperties();
@Autowired
private ServerProperties server;
@Autowired
private JdbcTemplate jdbcTemplate;
@Bean
public CustomRouteLocator routeLocator() {
String contextPath = server.getServlet().getContextPath();
logger.info("=========== servlet contextPath {}", contextPath);
CustomRouteLocator routeLocator = new CustomRouteLocator(contextPath, zuulProperties);
routeLocator.setJdbcTemplate(jdbcTemplate);
return routeLocator;
}
}
数据库路由表结构
CREATE TABLE `x_gw_route` (
`id` varchar(50) NOT NULL COMMENT '主键',
`path` varchar(255) NOT NULL COMMENT '访问路径',
`service_id` varchar(50) DEFAULT NULL COMMENT '服务id',
`url` varchar(255) DEFAULT NULL,
`retryable` tinyint(1) DEFAULT NULL COMMENT '是否失败后重新请求1是0否',
`enabled` tinyint(1) NOT NULL COMMENT '是否使用1正常0不使用',
`strip_prefix` int(11) DEFAULT NULL COMMENT '是否去除前缀1是0否',
`api_name` varchar(255) DEFAULT NULL COMMENT '服务名称',
`c_desc` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '描述',
`c_createuserid` varchar(60) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '创建用户id',
`c_createtime` datetime DEFAULT NULL,
`c_updateuserid` varchar(60) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '最后修改者id',
`c_updatetime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
至此动态路由的功能,zuul的配置完成。
如果还要实现请求过滤,比如校验token信息等。可以自定义过滤器
package com.atguigu.springcloud.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
/**
* Created by caoshi at 22:43 2022-06-04
*/
@Component
public class AuthHeaderFilter extends ZuulFilter {
private static Logger logger = LoggerFactory.getLogger(AuthHeaderFilter.class);
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
Object serviceIdKey = requestContext.get(FilterConstants.SERVICE_ID_KEY);
Object request_uri_key = requestContext.get(FilterConstants.REQUEST_URI_KEY);
System.out.println(serviceIdKey + " " + request_uri_key);
String servletPath = requestContext.getRequest().getServletPath();
System.out.println("servletPath = " + servletPath);
logger.info("网关接收到路由请求 {} ", requestContext.getRequest().getRequestURI());
return null;
}
}
可以重写 run() 方法,可以取到 RequestContext 对象对请求进行过滤处理。
踩坑记录:
默认在路由表里 strip_prefix 字段的值是0,导致zull不会去除前缀,而zuul本身默认就包含一个前缀 /zuul
导致在真实的请求前面加上这个前缀 /zuul。而我在服务提供者方的控制器方法上设置的 匹配路径中是不包含 前缀/zuul的,所以导致请求失败,报 404 错误。
最后的解决办法是 将路由表里 strip_prefix 设置为 1,表示去除前缀。