
本文介绍一种无需重构 WAR 包、支持多数据库并行访问的轻量级方案:通过 Servlet Filter 解析 URL 中的数据库标识(如 /database1/),动态绑定数据源,并在 Servlet 中透明获取对应连接,兼顾灵活性、可维护性与线程安全性。
本文介绍一种无需重构 war 包、支持多数据库并行访问的轻量级方案:通过 servlet filter 解析 url 中的数据库标识(如 `/database1/`),动态绑定数据源,并在 servlet 中透明获取对应连接,兼顾灵活性、可维护性与线程安全性。
在企业级 Java Web 应用中,常需同一套业务代码支撑多个客户或租户——每个租户拥有独立数据库实例。理想方案应满足:URL 可识别目标库、配置外置化、支持并发多库会话、不侵入现有 Servlet 逻辑。本文提供一个基于标准 Servlet 规范的可行实现,无需强依赖 Spring 或 Hibernate,亦可无缝集成。
✅ 核心思路:Filter 拦截 + 请求属性透传 + 外置数据源工厂
首先,使用 @WebFilter("/*") 拦截所有请求,识别形如 /database1/Scheduling 的路径:
@WebFilter("/*")
public class DatabasePathFilter extends HttpFilter {
private static final String DATABASE_PREFIX = "/database";
@Override
protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
String ctx = req.getContextPath();
String uri = req.getRequestURI();
// 匹配 /{context}/database{X}/...
if (uri.startsWith(ctx + DATABASE_PREFIX) && uri.length() > ctx.length() + DATABASE_PREFIX.length()) {
String rest = uri.substring(ctx.length());
String[] parts = rest.split("/", 3); // 分割为 ["", "database1", "Scheduling?..."]
if (parts.length >= 2 && parts[1].startsWith("database")) {
String dbKey = parts[1]; // e.g., "database1"
req.setAttribute("dbKey", dbKey);
// 重写请求路径:/database1/Scheduling → /Scheduling,使原 Servlet 映射生效
String targetUri = ctx + "/" + (parts.length > 2 ? parts[2] : "");
req.getRequestDispatcher(targetUri).forward(req, res);
return;
}
}
chain.doFilter(req, res);
}
}
⚠️ 注意:
forward()是服务端跳转,浏览器地址栏不变,确保用户看到/database1/Scheduling,而 Servlet 实际接收/Scheduling请求 —— 完美兼容原有web.xml或@WebServlet("/Scheduling")配置。
? 动态数据源管理(外置配置 + 线程安全)
在 Servlet 中,通过 req.getAttribute("dbKey") 获取当前请求所属数据库标识,再交由 DataSourceFactory 返回对应连接池:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
// 外置配置示例:/opt/myapp/datasources.yml(非 classpath,可热更新)
// database1:
// url: jdbc:mysql://db1.example.com:3306/app?useSSL=false
// username: user1
// password: pass1
// database2:
// url: jdbc:mysql://db2.example.com:3306/app?useSSL=false
// username: user2
// password: pass2
public class DataSourceFactory {
private static final Map<string datasource> CACHE = new ConcurrentHashMap();
private static final Path CONFIG_PATH = Paths.get("/opt/myapp/datasources.yml");
public static DataSource getDataSource(String dbKey) throws SQLException {
return CACHE.computeIfAbsent(dbKey, key -> {
try {
Yaml yaml = new Yaml();
Map<string object> config = yaml.load(Files.newInputStream(CONFIG_PATH));
Map<string object> dbConfig = (Map<string object>) config.get(key);
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setJdbcUrl((String) dbConfig.get("url"));
hikariConfig.setUsername((String) dbConfig.get("username"));
hikariConfig.setPassword((String) dbConfig.get("password"));
hikariConfig.setMaximumPoolSize(20);
return new HikariDataSource(hikariConfig);
} catch (IOException e) {
throw new RuntimeException("Failed to load datasource config for " + key, e);
}
});
}
}</string></string></string></string>
在 Servlet 中使用:
@WebServlet("/Scheduling")
public class SchedulingServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String dbKey = (String) req.getAttribute("dbKey");
if (dbKey == null) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing database path");
return;
}
try (Connection conn = DataSourceFactory.getDataSource(dbKey).getConnection();
PreparedStatement stmt = conn.prepareStatement("SELECT COUNT(*) FROM events")) {
ResultSet rs = stmt.executeQuery();
rs.next();
resp.getWriter().println("Events in " + dbKey + ": " + rs.getInt(1));
} catch (SQLException e) {
throw new IOException(e);
}
}
}
? 关键保障:会话隔离与并发安全
-
线程隔离:每个 HTTP 请求在独立线程中执行,
req.getAttribute()保证当前请求上下文专属,天然支持多库并行(如用户同时打开/database1/Scheduling和/database2/Scheduling)。 -
连接池隔离:每个
dbKey对应独立HikariDataSource实例,连接池互不干扰,避免跨库污染。 -
配置热加载(进阶):可通过
WatchService监听datasources.yml修改,触发CACHE中过期DataSource的优雅关闭与重建(需加锁同步)。
✅ 总结
该方案以最小侵入代价达成目标:
- ✅ URL 路径驱动数据库路由(
/database1/xxx→dbKey="database1") - ✅ 数据源配置完全外置,无需重新打包 WAR
- ✅ 原有 Servlet 逻辑零改造,仅需按需获取
dbKey - ✅ 天然支持多租户并发、会话隔离与连接池自治
- ✅ 兼容传统 Servlet 容器(Tomcat/Jetty)及现代框架(可作为 Spring
AbstractRoutingDataSource的轻量替代)
若后续规模扩大,可平滑迁移至 Spring Boot 多数据源或云原生多租户架构;但对多数场景,此 Filter + 工厂模式已足够稳健、清晰且高效。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










