
本文介绍如何通过 Servlet Filter 解析请求 URL 中的数据库标识(如 /database1/),动态路由至同一组 Servlet,并在运行时按需加载外部配置的数据库连接,支持多库并发访问与零代码重建的配置管理。
本文介绍如何通过 servlet filter 解析请求 url 中的数据库标识(如 `/database1/`),动态路由至同一组 servlet,并在运行时按需加载外部配置的数据库连接,支持多库并发访问与零代码重建的配置管理。
在企业级 Java Web 应用中,常需以一套业务代码服务多个客户或租户,而各客户数据物理隔离于独立数据库。此时,要求“同一套 WAR 包、同一组 Servlet,仅通过 URL 路径区分目标数据库”,且数据库连接配置必须外部化(如放在 $CATALINA_HOME/conf/databases/ 或 /etc/myapp/db-configs/),避免每次增删库都重新构建部署。这一需求完全可行——核心在于解耦请求路由与数据源绑定,而非依赖传统 web.xml 静态映射。
✅ 核心方案:使用 @WebFilter 动态解析并重写请求路径
Servlet Filter 是最轻量、标准且可控的切入点。它在请求进入 Servlet 前拦截,可安全提取路径参数、设置请求属性,并转发至标准化路径,从而让后续 Servlet 无感知地处理不同数据库上下文。
以下为生产就绪的过滤器实现(兼容 Servlet 3.0+):
@WebFilter("/*")
public class DatabaseRoutingFilter extends HttpFilter {
private static final String DATABASE_PREFIX = "/database";
private static final String CONFIG_BASE_PATH = "/opt/myapp/db-configs/"; // 外部配置根目录
@Override
protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
String uri = req.getRequestURI();
String ctx = req.getContextPath();
// 匹配 /database{dbId}/... 格式(如 /database1/Scheduling)
if (uri.startsWith(ctx + DATABASE_PREFIX) && uri.length() > ctx.length() + DATABASE_PREFIX.length() + 1) {
// 提取 database1、database2 等标识
String[] segments = uri.substring(ctx.length()).split("/", 3);
if (segments.length >= 2 && segments[1].startsWith("database")) {
String dbId = segments[1];
// ✅ 关键:将数据库标识存入 request 属性,供后续 Servlet 使用
req.setAttribute("currentDatabaseId", dbId);
// ✅ 构造标准化 servlet 路径(去除 databaseX 前缀)
String targetPath = ctx + "/" + (segments.length > 2 ? segments[2] : "");
req.getRequestDispatcher(targetPath).forward(req, res);
return;
}
}
// 不匹配则放行原始请求(如静态资源、登录页等)
chain.doFilter(req, res);
}
}
? 说明:该过滤器将
GET /database1/Scheduling重写为对/Scheduling的内部转发,同时将"database1"注入request.getAttribute("currentDatabaseId")—— 后续任何 Servlet 均可通过此属性获知本次请求应操作哪个库。
Alibabacloud Sdk Client Initialization For Java下载在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
?️ 在 Servlet 中动态获取数据源
不再硬编码 DataSource,而是根据 currentDatabaseId 查找对应连接池。推荐使用线程安全的工厂模式 + 外部 YAML/Properties 配置:
@WebServlet("/Scheduling")
public class SchedulingServlet extends HttpServlet {
private final DataSourceFactory dataSourceFactory = new DataSourceFactory(CONFIG_BASE_PATH);
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String dbId = (String) req.getAttribute("currentDatabaseId");
if (dbId == null) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing database identifier in URL");
return;
}
try (Connection conn = dataSourceFactory.getDataSource(dbId).getConnection()) {
// 执行业务 SQL(自动连接到 database1 或 database2)
PreparedStatement ps = conn.prepareStatement("SELECT * FROM schedule WHERE active = ?");
ps.setBoolean(1, true);
ResultSet rs = ps.executeQuery();
// ... 渲染响应
} catch (SQLException e) {
throw new RuntimeException("DB operation failed for " + dbId, e);
}
}
}
DataSourceFactory 示例(简化版,支持热加载):
public class DataSourceFactory {
private final String configDir;
private final Map<string datasource> cache = new ConcurrentHashMap();
public DataSourceFactory(String configDir) {
this.configDir = configDir;
loadAllConfigs(); // 启动时加载所有 *.properties 文件
}
public DataSource getDataSource(String dbId) {
return cache.computeIfAbsent(dbId, this::buildDataSourceFromConfig);
}
private DataSource buildDataSourceFromConfig(String dbId) {
Properties props = new Properties();
try (InputStream is = Files.newInputStream(Paths.get(configDir, dbId + ".properties"))) {
props.load(is);
} catch (IOException e) {
throw new IllegalArgumentException("Failed to load DB config: " + dbId, e);
}
HikariConfig config = new HikariConfig(props); // 使用 HikariCP 连接池
return new HikariDataSource(config);
}
// 可扩展:监听文件变更,触发 reload(如使用 Apache Commons IO Monitor)
}</string>
⚠️ 关键注意事项与最佳实践
-
会话隔离性:每个 HTTP 请求天然独立,
req.getAttribute()仅作用于当前请求线程,因此database1和database2标签页可安全并行打开,互不干扰;无需额外 Session 管理。 -
配置外部化:
.properties文件示例(/opt/myapp/db-configs/database1.properties):jdbcUrl=jdbc:mysql://db1-host:3306/app_db?useSSL=false username=app_user password=secret123 maximumPoolSize=20
-
安全性:务必校验
dbId是否为白名单值(如正则^database\d+$),防止路径遍历或任意文件读取。 -
性能优化:
DataSource是重量级对象,必须复用(缓存于ConcurrentHashMap),禁止每次请求新建。 -
替代方案对比:
- Spring Boot Multi-Tenancy(
AbstractRoutingDataSource)功能更强大,但引入框架依赖; - Hibernate 多租户需深度集成 ORM 层;
- 纯 Servlet Filter 方案零框架侵入、启动快、运维透明,是轻量级多库路由的首选。
- Spring Boot Multi-Tenancy(
通过以上设计,你获得了一个可扩展、可运维、符合 Java EE 规范的多数据库路由架构:URL 即租户标识,配置即服务,无需重启应用即可新增数据库实例。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











