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. 功能亮点

  1. 内嵌Tomcat集成

    • 无需外部容器
    • 可编程式配置
    • 支持Servlet 6.0规范
  2. 数据库管理

    • H2内存数据库
    • HikariCP连接池
    • 内置H2控制台
  3. 监控功能

    • HikariCP连接池监控
    • 通过/hikari/metrics端点暴露指标
  4. 用户管理功能

    • 完整的CRUD操作
    • 响应式HTML界面
    • 表单验证

6. 启动与访问

  1. 启动应用:

    mvn clean compile exec:java
    
  2. 访问地址:

    • 用户管理界面: http://localhost:8080/users
    • H2控制台: http://localhost:8080/h2-console
    • Hikari监控: http://localhost:8080/hikari/metrics

7. 最佳实践建议

  1. 生产环境考虑
    • 替换内存数据库为MySQL/PostgreSQL
    • 添加认证机制
  2. 性能优化
    • 调整连接池参数
    • 启用Tomcat的NIO模式
    • 添加缓存层

人生不作安期生,醉入东海骑长鲸