Java项目使用内嵌Tomcat实践指南
在非传统Web应用中使用内嵌Tomcat的场景越来越广泛,主要得益于其:
- 轻量级部署
- 易集成性
- Servlet容器标准化
- 适合构建后台管理/监控界面等场景
1. 项目结构
tree
.
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── bingbaihanji
│ │ ├── Main.java # 主启动类
│ │ ├── database
│ │ │ ├── H2Database.java # 数据库配置
│ │ │ └── HikariMetricsServlet.java # 监控Servlet
│ │ └── servlet
│ │ └── UserServlet.java # 用户管理Servlet
│ ├── resources
│ │ ├── jdbc.properties # 数据库配置
│ │ ├── logback.xml # 日志配置
│ │ └── static # 静态资源
│ └── webapp
│ └── WEB-INF # Web配置
└── test
└── java # 测试代码
2. Maven依赖配置
关键依赖说明:
<!-- slf4j与logback集成配置 -->
<!-- slf4j日志门面 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<!-- logback 日志实现-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.4.14</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
</dependency>
<!-- 内嵌Tomcat核心 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>11.0.5</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>11.0.5</version>
</dependency>
<!-- 数据库相关 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.3.232</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.1.0</version>
</dependency>
<!-- jakarta.servlet-api -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.1.0</version>
<scope>provided</scope>
</dependency>
<!--lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
</dependency>
3. 数据库配置
jdbc.properties
文件配置:
# H2 database connection properties
jdbc.driverClassName=org.h2.Driver
jdbc.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MySQL;DATABASE_TO_UPPER=false;DEFAULT_NULL_ORDERING=HIGH;NON_KEYWORDS=USER
jdbc.username=bingbaihanji
jdbc.password=123456
# HikariCP specific properties
# 连接超时时间(毫秒)
hikari.connection-timeout=30000
# 连接空闲超时时间(毫秒)
hikari.idle-timeout=600000
# 连接最大存活时间(毫秒)
hikari.max-lifetime=1800000
# 连接池最大连接数
hikari.maximum-pool-size=10
# 连接池中最小空闲连接数
hikari.minimum-idle=5
# 默认自动提交行为
hikari.auto-commit=true
# 连接池名称
hikari.pool-name=HikariPool
# 默认只读模式
hikari.read-only=false
# 连接测试query
hikari.connection-test-query=SELECT 1
# 允许连接池暂停
hikari.allow-pool-suspension=true
4. 核心代码实现
4.1 数据库初始化类 - H2Database.java
package com.bingbaihanji.database;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
public class H2Database {
private static final HikariDataSource dataSource;
static {
Properties properties = new Properties();
try {
properties.load(H2Database.class.getResourceAsStream("/jdbc.properties"));
} catch (IOException e) {
throw new RuntimeException("无法加载 jdbc.properties", e);
}
HikariConfig config = new HikariConfig();
config.setJdbcUrl(properties.getProperty("jdbc.url"));
config.setUsername(properties.getProperty("jdbc.username"));
config.setPassword(properties.getProperty("jdbc.password"));
config.setDriverClassName(properties.getProperty("jdbc.driverClassName"));
// HikariCP优化配置
config.setPoolName(properties.getProperty("hikari.pool-name"));
config.setConnectionTimeout(Integer.parseInt(properties.getProperty("hikari.connection-timeout")));
config.setConnectionTimeout(Long.parseLong(properties.getProperty("hikari.idle-timeout")));
config.setMaxLifetime(Long.parseLong(properties.getProperty("hikari.max-lifetime")));
config.setMaximumPoolSize(Integer.parseInt(properties.getProperty("hikari.maximum-pool-size")));
config.setMinimumIdle(Integer.parseInt(properties.getProperty("hikari.minimum-idle")));
config.setAutoCommit(Boolean.parseBoolean(properties.getProperty("hikari.auto-commit")));
config.setReadOnly(Boolean.parseBoolean(properties.getProperty("hikari.read-only")));
config.setConnectionTestQuery(properties.getProperty("hikari.connection-test-query"));
config.setAllowPoolSuspension(Boolean.parseBoolean(properties.getProperty("hikari.allow-pool-suspension")));
dataSource = new HikariDataSource(config);
}
public static void initialize() {
try (Connection conn = getConnection();
Statement stmt = conn.createStatement()) {
// 创建表
stmt.execute("""
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE
)
""");
// 插入示例数据
stmt.execute("""
MERGE INTO users (id, name, email) KEY(id) VALUES
(1, 'John Doe', 'john@example.com'),
(2, 'Jane Smith', 'jane@example.com')
""");
} catch (SQLException e) {
throw new RuntimeException("Failed to initialize database", e);
}
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
public static HikariDataSource getDataSource() {
return dataSource;
}
}
4.2 用户管理Servlet - UserServlet.java
package com.bingbaihanji.servlet;
import com.bingbaihanji.database.H2Database;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@WebServlet("/users")
public class UserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
String action = req.getParameter("action");
String idParam = req.getParameter("id");
try {
if ("edit".equals(action) && idParam != null) {
showEditForm(out, Integer.parseInt(idParam));
return;
}
if ("delete".equals(action) && idParam != null) {
deleteUser(out, resp, Integer.parseInt(idParam));
return;
}
showUserList(out);
} catch (NumberFormatException e) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "无效的用户ID");
} catch (Exception e) {
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"数据库错误: " + e.getMessage());
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
String idParam = req.getParameter("id");
String name = req.getParameter("name");
String email = req.getParameter("email");
try {
if (idParam == null || idParam.isEmpty()) {
// 新增用户
addUser(resp, name, email);
} else {
// 修改用户
updateUser(resp, Integer.parseInt(idParam), name, email);
}
} catch (NumberFormatException e) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "无效的用户ID");
} catch (Exception e) {
if (e.getMessage().contains("UNIQUE")) {
resp.sendError(HttpServletResponse.SC_CONFLICT, "邮箱已存在");
} else {
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"数据库错误: " + e.getMessage());
}
}
}
private void showUserList(PrintWriter out) throws SQLException {
try (Connection conn = H2Database.getConnection();
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users");
ResultSet rs = stmt.executeQuery()) {
out.println("""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>用户管理</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
table { border-collapse: collapse; width: 80%; margin-bottom: 20px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
form { margin-bottom: 20px; padding: 15px; border: 1px solid #ddd; width: 300px; }
input { margin-bottom: 10px; width: 100%; padding: 8px; }
button { background-color: #4CAF50; color: white; padding: 10px; border: none; cursor: pointer; }
button:hover { background-color: #45a049; }
.actions { display: flex; gap: 5px; }
.edit-btn { background-color: #2196F3; }
.edit-btn:hover { background-color: #0b7dda; }
.delete-btn { background-color: #f44336; }
.delete-btn:hover { background-color: #da190b; }
</style>
</head>
<body>
<h1>用户管理</h1>
<form method="post" action="users">
<h2>添加新用户</h2>
<input type="text" name="name" placeholder="姓名" required>
<input type="email" name="email" placeholder="邮箱" required>
<button type="submit">添加用户</button>
</form>
<h2>用户列表</h2>
<table>
<tr><th>ID</th><th>姓名</th><th>邮箱</th><th>操作</th></tr>
""");
while (rs.next()) {
out.printf("""
<tr>
<td>%d</td>
<td>%s</td>
<td>%s</td>
<td class="actions">
<a href="users?action=edit&id=%d">
<button class="edit-btn">编辑</button>
</a>
<a href="users?action=delete&id=%d" onclick="return confirm('确定删除这个用户吗?')">
<button class="delete-btn">删除</button>
</a>
</td>
</tr>
""",
rs.getInt("id"),
rs.getString("name"),
rs.getString("email"),
rs.getInt("id"),
rs.getInt("id"));
}
out.println("""
</table>
</body>
</html>
""");
}
}
private void showEditForm(PrintWriter out, int id) throws SQLException {
try (Connection conn = H2Database.getConnection();
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
stmt.setInt(1, id);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
out.printf("""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>编辑用户</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
form { margin-bottom: 20px; padding: 15px; border: 1px solid #ddd; width: 300px; }
input { margin-bottom: 10px; width: 100%%; padding: 8px; }
button { background-color: #4CAF50; color: white; padding: 10px; border: none; cursor: pointer; }
button:hover { background-color: #45a049; }
.cancel-btn { background-color: #f44336; }
.cancel-btn:hover { background-color: #da190b; }
</style>
</head>
<body>
<h1>编辑用户</h1>
<form method="post" action="users">
<input type="hidden" name="id" value="%d">
<input type="text" name="name" value="%s" placeholder="姓名" required>
<input type="email" name="email" value="%s" placeholder="邮箱" required>
<div style="display: flex; gap: 5px;">
<button type="submit">保存</button>
<a href="users"><button type="button" class="cancel-btn">取消</button></a>
</div>
</form>
</body>
</html>
""",
rs.getInt("id"),
rs.getString("name"),
rs.getString("email"));
}
}
}
private void addUser(HttpServletResponse resp, String name, String email)
throws IOException, SQLException {
if (name == null || name.trim().isEmpty() || email == null || email.trim().isEmpty()) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "姓名和邮箱不能为空");
return;
}
try (Connection conn = H2Database.getConnection();
PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO users (name, email) VALUES (?, ?)")) {
stmt.setString(1, name.trim());
stmt.setString(2, email.trim());
stmt.executeUpdate();
resp.sendRedirect("users");
}
}
private void updateUser(HttpServletResponse resp, int id, String name, String email)
throws IOException, SQLException {
if (name == null || name.trim().isEmpty() || email == null || email.trim().isEmpty()) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "姓名和邮箱不能为空");
return;
}
try (Connection conn = H2Database.getConnection();
PreparedStatement stmt = conn.prepareStatement(
"UPDATE users SET name = ?, email = ? WHERE id = ?")) {
stmt.setString(1, name.trim());
stmt.setString(2, email.trim());
stmt.setInt(3, id);
int affectedRows = stmt.executeUpdate();
if (affectedRows > 0) {
resp.sendRedirect("users");
} else {
resp.sendError(HttpServletResponse.SC_NOT_FOUND, "用户不存在");
}
}
}
private void deleteUser(PrintWriter out, HttpServletResponse resp, int id)
throws IOException, SQLException {
try (Connection conn = H2Database.getConnection();
PreparedStatement stmt = conn.prepareStatement(
"DELETE FROM users WHERE id = ?")) {
stmt.setInt(1, id);
int affectedRows = stmt.executeUpdate();
if (affectedRows > 0) {
resp.sendRedirect("users");
} else {
resp.sendError(HttpServletResponse.SC_NOT_FOUND, "用户不存在");
}
}
}
}
4.3连接池配置查看类-HikariMetricsServlet.java
package com.bingbaihanji.database;
import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.HikariPoolMXBean;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class HikariMetricsServlet extends HttpServlet {
private final HikariDataSource dataSource;
public HikariMetricsServlet(HikariDataSource dataSource) {
this.dataSource = dataSource;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/plain");
PrintWriter writer = resp.getWriter();
// 获取 HikariCP 的 MXBean
HikariPoolMXBean pool = dataSource.getHikariPoolMXBean();
writer.println("# HikariCP Metrics");
writer.println("active_connections: " + pool.getActiveConnections());
writer.println("idle_connections: " + pool.getIdleConnections());
writer.println("total_connections: " + pool.getTotalConnections());
writer.println("threads_awaiting_connection: " + pool.getThreadsAwaitingConnection());
writer.println("max_pool_size: " + dataSource.getMaximumPoolSize());
writer.println("min_idle: " + dataSource.getMinimumIdle());
// 添加更多指标
writer.println("connection_timeout: " + dataSource.getConnectionTimeout());
writer.println("idle_timeout: " + dataSource.getIdleTimeout());
writer.println("max_lifetime: " + dataSource.getMaxLifetime());
writer.println("pool_name: " + dataSource.getPoolName());
}
}
4.4主启动类 - Main.java
package com.bingbaihanji;
import com.bingbaihanji.database.H2Database;
import com.bingbaihanji.database.HikariMetricsServlet;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.Context;
import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.webresources.DirResourceSet;
import org.apache.catalina.webresources.StandardRoot;
import org.apache.tomcat.util.scan.StandardJarScanner;
import org.slf4j.bridge.SLF4JBridgeHandler;
import java.io.File;
import java.nio.file.Path;
@Slf4j
public class Main {
// 日志桥接 (日志输出到 SLF4J)
static {
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
}
public static void main(String[] args) {
H2Database.initialize();
Tomcat tomcat = new Tomcat();
tomcat.setPort(8080);
Path tempDir = Path.of(System.getProperty("java.io.tmpdir"), "embedded-tomcat");
tempDir.toFile().mkdirs();
tomcat.setBaseDir(tempDir.toString());
try {
File webappDir = new File("src/main/webapp");
if (!webappDir.exists()) {
webappDir.mkdirs();
new File(webappDir, "WEB-INF").mkdirs();
}
/*手动加载Servlet*/
// Context ctx = tomcat.addWebapp("", webappDir.getAbsolutePath());
// // 添加用户Servlet
// Tomcat.addServlet(ctx, "UserServlet", new com.bingbaihanji.servlet.UserServlet());
// ctx.addServletMappingDecoded("/users", "UserServlet");
/*加载 @WebServlet("/users") 注解*/
Context ctx = tomcat.addWebapp("", webappDir.getAbsolutePath());
// 配置Jar扫描器(此项目不需要)
StandardJarScanner scanner = new StandardJarScanner();
scanner.setScanManifest(false);
scanner.setScanClassPath(true);
ctx.setJarScanner(scanner);
// 设置资源路径(保持这部分) 将 target/classes(编译后的类文件目录)映射到 Web 应用的 /WEB-INF/classes 路径
WebResourceRoot resources = new StandardRoot(ctx);
resources.addPreResources(new DirResourceSet(resources, "/WEB-INF/classes",
new File("target/classes").getAbsolutePath(), "/"));
ctx.setResources(resources);
// 启用注解处理
ctx.setAddWebinfClassesResources(true);
// 添加H2控制台
Context h2Console = tomcat.addContext("/h2-console", null);
Tomcat.addServlet(h2Console, "H2Console", new org.h2.server.web.JakartaWebServlet());
h2Console.addServletMappingDecoded("/*", "H2Console");
// 添加Hikari监控
HikariDataSource dataSource = H2Database.getDataSource();
// 替换原来的 PrometheusMetricsServlet 部分
Context hikariConsole = tomcat.addContext("/hikari", null);
Tomcat.addServlet(hikariConsole, "HikariMetrics", new HikariMetricsServlet(dataSource));
hikariConsole.addServletMappingDecoded("/metrics", "HikariMetrics");
tomcat.start();
log.info("服务器已启动 http://localhost:{}", tomcat.getConnector().getLocalPort());
log.info("访问用户列表: http://localhost:{}/users", tomcat.getConnector().getLocalPort());
log.info("H2控制台: http://localhost:{}/h2-console", tomcat.getConnector().getLocalPort());
log.info("Hikari监控: http://localhost:{}/hikari/metrics", tomcat.getConnector().getLocalPort());
log.info("服务器已启动 http://localhost:{}", tomcat.getConnector().getLocalPort());
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
tomcat.stop();
dataSource.close();
} catch (Exception e) {
log.error("停止服务时出错: {}", e.getMessage());
}
}));
tomcat.getServer().await();
} catch (Exception e) {
log.error("启动服务器时出错: {}", e.getMessage(), e);
System.exit(1);
}
}
}
5. 功能亮点
-
内嵌Tomcat集成
- 无需外部容器
- 可编程式配置
- 支持Servlet 6.0规范
-
数据库管理
- H2内存数据库
- HikariCP连接池
- 内置H2控制台
-
监控功能
- HikariCP连接池监控
- 通过
/hikari/metrics
端点暴露指标
-
用户管理功能
- 完整的CRUD操作
- 响应式HTML界面
- 表单验证
6. 启动与访问
-
启动应用:
mvn clean compile exec:java
-
访问地址:
- 用户管理界面:
http://localhost:8080/users
- H2控制台:
http://localhost:8080/h2-console
- Hikari监控:
http://localhost:8080/hikari/metrics
- 用户管理界面:
7. 最佳实践建议
- 生产环境考虑:
- 替换内存数据库为MySQL/PostgreSQL
- 添加认证机制
- 性能优化:
- 调整连接池参数
- 启用Tomcat的NIO模式
- 添加缓存层