Java Web学生信息查询系统:Servlet处理+MySQL 8.0存储+Tomcat 9部署

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一个开箱即用的Java Web学生信息查询系统,基于标准Servlet技术实现前后端通信,后端使用MySQL 8.0数据库管理学生ID、姓名、班级等基础信息。项目运行在Apache Tomcat 9.0.89服务器上,结构遵循Java EE分层规范:src目录包含学生实体类(stu)用于数据封装,StudentServlet负责接收POST请求并按ID查询学生详情,另附简易测试Servlet(Student)响应/hello路径便于快速验证。lib目录已集成必需依赖库,.gitignore和README.md提供版本控制支持与使用指引,同时内置IntelliJ IDEA项目配置文件(.idea、*.iml),无需额外配置即可导入编译运行。适合Java Web初学者练习Servlet开发流程、高校课程设计参考,或作为轻量级学生信息查询功能模块进行二次扩展。

1. 项目概述:为什么这个“老派”组合依然值得你亲手敲一遍

你可能在想:都2024年了,Spring Boot动动手指三分钟就能跑起来,谁还愿意花半天时间配Tomcat、写web.xml、手动加载MySQL驱动?但恰恰是这套看起来“过时”的Java Web经典栈——Servlet + MySQL 8.0 + Tomcat 9——才是理解Web开发底层逻辑的真正入口。它不藏掖、不抽象,每一个请求怎么进、数据怎么查、响应怎么回,全摊开在你眼皮底下。我带过十几届Java方向的毕业设计,发现一个规律:凡是跳过Servlet阶段直接上Spring Boot的同学,后期遇到Filter链失效、Session跨域丢失、字符编码乱码这类问题,第一反应永远是百度搜“解决方案”,而不是打开浏览器开发者工具看Network面板里Request Headers到底少了哪一行;而那些老老实实手写过5个以上Servlet、自己配过3次Tomcat连接池的同学,往往能盯着日志里的ClassNotFoundException反向定位到是lib目录下mysql-connector-java版本和MySQL 8.0协议不兼容——这种直觉,不是框架给的,是肌肉记忆练出来的。

这个学生信息查询系统,就是为你量身定制的“底层认知训练器”。它不追求炫酷界面,首页就一个输入框加查询按钮;它不堆砌功能,只做一件事:根据ID查学生姓名和班级。但正是这份极简,让它成了绝佳的解剖样本。关键词里提到的“Servlet开发”不是指调用@WebServlet注解,而是从javax.servlet.http.HttpServlet继承、重写doPost()、手动解析request.getParameter("stuId");“学生信息查询”背后是JDBC的PreparedStatement预编译防SQL注入、ResultSet逐行遍历的原始手感;“MySQL 8.0”意味着你必须直面caching_sha2_password认证插件带来的驱动兼容性雷区;“Tomcat 9”则要求你理解context.xml<Resource>标签如何把数据库连接池交给Servlet容器管理——这些细节,在Spring Boot的自动配置背后全被封装掉了,但它们真实存在,且在生产环境出问题时,往往就是决定排查速度的关键。

我把它部署在Tomcat 9.0.89上,不是因为情怀,而是这个版本对Java 11+支持最稳,同时保留了传统web.xml部署描述符的完整支持,让你既能体验注解式开发(比如那个/hello测试Servlet),也能回归XML配置时代去理解<servlet-mapping>的映射原理。整个项目结构像一本摊开的教科书:src/stu包里那个只有三个字段的Student类,是你第一次亲手写的POJO;StudentServlet里那几十行代码,是你和HTTP协议的第一次深度对话;而lib目录下那个mysql-connector-java-8.0.33.jar,则是你和数据库握手时递出的第一张身份证。它适合谁?如果你正在学《Java Web程序设计》这门课,它就是你的实验报告模板;如果你要交课程设计,删掉Student测试类、把StudentServlet改成增删改查四合一,三天就能交差;如果你是个刚转行的开发者,想补上企业级Java Web的底层拼图,那就别急着建Spring Boot项目,先把这127行Servlet代码逐行debug十遍——你会发现,所谓“框架”,不过是把这127行代码,用更优雅的方式,重复写了上千遍。

2. 整体架构与技术选型深挖:为什么拒绝“一键生成”,坚持手写每一层

2.1 分层设计不是摆设:从src目录看Java EE的骨架逻辑

看到项目结构里src目录下只有stu包和两个Servlet类,你可能会觉得“这也叫分层?”但恰恰是这种极简,暴露了Java EE分层的本质——它不是靠文件夹数量堆出来的,而是靠职责隔离刻出来的。我们来拆解这个看似单薄的结构:

  • stu.Student类:这不是一个简单的JavaBean。它的每个字段都带着明确的契约:private String stuId;——ID必须是字符串,因为MySQL里VARCHAR(10)主键不能自动转成intprivate String name;——名字字段声明为String而非Object,是因为前端表单提交的永远是字符串,强转成其他类型是业务逻辑层的事,实体层只负责原样承载;private String className;——班级名用className而非class,避开Java关键字,这是从第一天写代码就该养成的肌肉记忆。这个类里没有@Data注解,没有Lombok,只有get/set/toString,因为你要亲手敲一遍,才能记住setStuId(String stuId)里那个this.stuId = stuId;this到底指向谁。

  • StudentServlet:这是整个系统的神经中枢。它继承HttpServlet,意味着它天然具备处理HTTP生命周期的能力。doPost()方法里那句String stuId = request.getParameter("stuId");,是你第一次直面“请求参数从哪来”——不是框架注入,是Servlet容器从HTTP请求体里解析application/x-www-form-urlencoded格式后塞进request对象的。而response.setContentType("text/html;charset=UTF-8");这一行,比任何Spring Boot的@RestController都更赤裸地告诉你:浏览器渲染页面,靠的是响应头里的Content-Type,缺了charset=UTF-8,中文班级名显示成乱码不是前端问题,是后端没告诉浏览器“请用UTF-8解码”。

  • Student测试Servlet:别小看这个只返回”Hello World”的类。它的存在价值在于验证整个链路是否通畅。当你访问http://localhost:8080/javaweb/hello能立刻看到响应,说明Tomcat启动成功、应用上下文javaweb部署正确、Servlet映射无误。这比一上来就查数据库快十倍定位环境问题。很多初学者卡在第一步,不是代码写错了,而是web.xml<url-pattern>/hello</url-pattern>写成了/Hello(大小写敏感),或者IDEA里没勾选“Deploy application context”——这个测试Servlet,就是你的健康检查探针。

提示:Java EE分层的核心思想是“变化隔离”。实体类Student只随数据库表结构变;Servlet只随业务流程变(比如查询逻辑从“按ID查”变成“按姓名模糊查”);而web.xml配置只随部署环境变(开发机用8080端口,测试机用9090)。当你发现改一个需求要动三个地方,那大概率是分层出了问题。

2.2 MySQL 8.0的“甜蜜陷阱”:驱动、认证、时区三座大山

用MySQL 8.0不是为了赶时髦,而是因为它强制暴露了过去被低版本掩盖的底层细节。这个项目里lib/mysql-connector-java-8.0.33.jar的选择,背后有三重考量:

第一重:驱动版本与协议匹配
MySQL 8.0默认启用caching_sha2_password认证插件,而老版本驱动(如5.1.x)根本不认识这个协议。如果你错误地用了mysql-connector-java-5.1.49.jar,启动时会报java.sql.SQLException: Unknown initial character set index '255' received from server。8.0.33驱动则原生支持该协议,但需要在JDBC URL里显式指定serverTimezone=UTC(稍后详解)。这个报错不是你的代码问题,是客户端和服务器在“握手”时语言不通——就像你跟英国人说美式英语,对方听懂了但一脸困惑。

第二重:JDBC URL的魔鬼细节
项目中StudentServlet里连接数据库的URL长这样:

String url = "jdbc:mysql://localhost:3306/studentdb?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true";

拆解每个参数:
- useSSL=false:本地开发关闭SSL,避免证书配置麻烦。生产环境必须开启,但那是另一个故事;
- serverTimezone=UTC:这是MySQL 8.0的强制要求。如果不加,驱动会尝试用JVM默认时区(比如Asia/Shanghai)去解析MySQL返回的时间戳,结果相差8小时——你存2024-01-01 12:00:00,查出来变成2024-01-01 04:00:00,学生上午的课被显示成凌晨;
- allowPublicKeyRetrieval=true:配合caching_sha2_password认证,允许驱动向服务器请求公钥来加密密码。这是8.0.33驱动新增的安全机制,老驱动没有这个参数,加了反而报错。

第三重:字符集统一战线
从数据库创建开始就要布防:

CREATE DATABASE studentdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

注意是utf8mb4,不是utf8。MySQL的utf8实际是utf8mb3,最多存3字节字符,而emoji和部分生僻汉字需要4字节。utf8mb4才是真正的UTF-8。接着在my.cnf里配置:

[client]
default-character-set = utf8mb4

[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

最后在JDBC URL里加characterEncoding=utf8mb4。三者缺一不可,否则张三存进去变成张三。我见过太多人只改了数据库字符集,忘了改JDBC URL,结果调试两小时才发现是连接层编码错了。

2.3 Tomcat 9.0.89:不只是容器,更是你的“运行时教练”

选Tomcat 9.0.89,是因为它在兼容性和现代性之间找到了黄金分割点。它完全支持Servlet 4.0规范(比如HTTP/2),但又没激进到放弃web.xml——这意味着你可以同时体验两种开发模式:用@WebServlet("/query")注解快速上手,也能通过web.xml手动配置来理解传统部署逻辑。更重要的是,它的日志系统极其友好。当你在StudentServlet里写System.out.println("Querying student: " + stuId);,这条日志不会消失,而是精准出现在logs/catalina.out里,时间戳、线程名、类名全带齐。对比Spring Boot的INFO级别日志淹没在千行启动日志中,Tomcat的日志就像手术刀一样精准。

另一个常被忽略的价值是context.xml的连接池管理。项目虽未提供,但你应该知道:把数据库连接配置从Servlet代码里抽出来,放到META-INF/context.xml里:

<Resource name="jdbc/studentdb" 
          auth="Container"
          type="javax.sql.DataSource"
          maxTotal="20"
          maxIdle="10"
          minIdle="5"
          username="root"
          password="123456"
          driverClassName="com.mysql.cj.jdbc.Driver"
          url="jdbc:mysql://localhost:3306/studentdb?useSSL=false&amp;serverTimezone=UTC"/>

然后在Servlet里用JNDI查找:

Context initCtx = new InitialContext();
Context envCtx = (Context) initCtx.lookup("java:comp/env");
DataSource ds = (DataSource) envCtx.lookup("jdbc/studentdb");
Connection conn = ds.getConnection();

这样做不是为了炫技,而是让数据库连接成为容器管理的资源,像内存、线程一样被统一调度。当并发查询激增时,连接池的maxTotal=20会硬性限流,避免数据库被打垮——这种资源治理思维,是所有高并发系统的基石。

3. 核心模块实现详解:从数据库建表到Servlet响应的完整链路

3.1 数据库准备:手写DDL比GUI操作更能建立空间感

别急着打开Navicat点点点,先打开记事本,把建表语句一行行敲出来。这种“原始操作”能强迫你思考每个字段的设计意图:

-- 创建数据库(注意字符集!)
CREATE DATABASE IF NOT EXISTS studentdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 切换数据库
USE studentdb;

-- 创建学生表
CREATE TABLE student (
  stu_id VARCHAR(10) PRIMARY KEY COMMENT '学生ID,如2023001',
  name VARCHAR(20) NOT NULL COMMENT '学生姓名',
  class_name VARCHAR(30) NOT NULL COMMENT '班级名称,如计算机科学与技术2023级1班',
  created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生基本信息表';

-- 插入测试数据(中文班级名必须用utf8mb4才能存)
INSERT INTO student (stu_id, name, class_name) VALUES 
('2023001', '张三', '计算机科学与技术2023级1班'),
('2023002', '李四', '软件工程2023级2班'),
('2023003', '王五', '网络工程2023级3班');

敲完这段SQL,你至少要问自己三个问题:
1. 为什么stu_idVARCHAR(10)而不是INT?——因为学号可能是2023CS001这样的混合字符串,INT会自动截断前导零;
2. 为什么class_name长度设为30?——打开学校官网查几个真实班级名,最长的那个加5个字冗余,这就是经验估算;
3. 为什么created_timeTIMESTAMP而不是DATETIME?——TIMESTAMP会自动转换时区,插入时存UTC,查询时转成本地时区,配合前面JDBC URL的serverTimezone=UTC,能保证全球部署时时间一致。

执行完后,用命令行验证:

mysql -u root -p -D studentdb -e "SELECT * FROM student;"

看到中文正常显示,才算过关。如果出现??,立刻回头检查my.cnf和JDBC URL的字符集参数——这是数据库层面的“Hello World”。

3.2 Student实体类:POJO的“最小完备性”原则

src/stu/Student.java只有38行,但它体现了POJO设计的黄金法则——最小完备性:只包含业务必需的字段和方法,不多不少。

package stu;

public class Student {
    private String stuId;
    private String name;
    private String className;

    // 无参构造器:反射实例化必需
    public Student() {}

    // 全参构造器:方便DAO层直接new Student(rs.getString(...))
    public Student(String stuId, String name, String className) {
        this.stuId = stuId;
        this.name = name;
        this.className = className;
    }

    // getter/setter(此处省略,但必须手写)
    public String getStuId() { return stuId; }
    public void setStuId(String stuId) { this.stuId = stuId; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getClassName() { return className; }
    public void setClassName(String className) { this.className = className; }

    @Override
    public String toString() {
        return "Student{" +
                "stuId='" + stuId + '\'' +
                ", name='" + name + '\'' +
                ", className='" + className + '\'' +
                '}';
    }
}

关键细节解析:
- 无参构造器的存在意义:JDBC的ResultSet.getObject()或后续ORM框架(如MyBatis)通过反射创建对象时,必须调用无参构造器。没有它,rs.getObject("stu_id", Student.class)会直接抛InstantiationException
- 全参构造器的实用价值:在DAO层查询时,可以一行代码完成对象封装:new Student(rs.getString("stu_id"), rs.getString("name"), rs.getString("class_name")),比先new Student()setXXX()少三行代码,且不可变性更强;
- toString()的调试价值:当System.out.println(student)输出Student{stuId='2023001', name='张三', className='计算机...'}时,你一眼就能确认数据封装正确。没有它,你只能看到stu.Student@1a2b3c4d这样的哈希码,调试效率归零。

注意:这里故意没加@Override注解的equals()hashCode(),因为学生ID是主键,业务上不存在两个不同对象内容相同的情况。过度添加这些方法反而增加维护成本——POJO设计的第一法则是“够用就好”。

3.3 StudentServlet:HTTP请求的“庖丁解牛”全过程

StudentServlet.java是整个项目的灵魂,我们逐行拆解其doPost()方法(核心逻辑约45行):

protected void doPost(HttpServletRequest request, HttpServletResponse response) 
        throws ServletException, IOException {

    // 1. 设置响应编码,解决中文乱码(重中之重!)
    response.setContentType("text/html;charset=UTF-8");
    PrintWriter out = response.getWriter();

    // 2. 获取前端传来的学生ID
    String stuId = request.getParameter("stuId");
    if (stuId == null || stuId.trim().isEmpty()) {
        out.println("<h3 style='color:red'>错误:学生ID不能为空!</h3>");
        return;
    }

    // 3. JDBC数据库查询(简化版,实际应抽到DAO层)
    Connection conn = null;
    PreparedStatement pstmt = null;
    ResultSet rs = null;
    try {
        // 加载驱动(Tomcat 9+可省略,但显式写出更清晰)
        Class.forName("com.mysql.cj.jdbc.Driver");

        // 建立连接(URL含serverTimezone=UTC!)
        String url = "jdbc:mysql://localhost:3306/studentdb?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true";
        conn = DriverManager.getConnection(url, "root", "123456");

        // 预编译SQL,防止SQL注入
        String sql = "SELECT stu_id, name, class_name FROM student WHERE stu_id = ?";
        pstmt = conn.prepareStatement(sql);
        pstmt.setString(1, stuId); // 参数索引从1开始!

        // 执行查询
        rs = pstmt.executeQuery();

        // 4. 处理查询结果
        if (rs.next()) {
            // 封装为Student对象
            Student student = new Student(
                rs.getString("stu_id"),
                rs.getString("name"),
                rs.getString("class_name")
            );

            // 5. 生成HTML响应(MVC中的View层极简实现)
            out.println("<h2>查询结果</h2>");
            out.println("<table border='1' width='500'>");
            out.println("<tr><th>ID</th><th>姓名</th><th>班级</th></tr>");
            out.println("<tr>");
            out.println("<td>" + student.getStuId() + "</td>");
            out.println("<td>" + student.getName() + "</td>");
            out.println("<td>" + student.getClassName() + "</td>");
            out.println("</tr>");
            out.println("</table>");
        } else {
            out.println("<h3 style='color:orange'>提示:未找到ID为 [" + stuId + "] 的学生</h3>");
        }

    } catch (SQLException e) {
        e.printStackTrace(); // 开发期打印堆栈,生产环境应记录日志
        out.println("<h3 style='color:red'>数据库查询失败:" + e.getMessage() + "</h3>");
    } catch (ClassNotFoundException e) {
        out.println("<h3 style='color:red'>数据库驱动未找到,请检查lib目录</h3>");
    } finally {
        // 6. 资源释放(顺序不能错!)
        try { if (rs != null) rs.close(); } catch (SQLException e) {}
        try { if (pstmt != null) pstmt.close(); } catch (SQLException e) {}
        try { if (conn != null) conn.close(); } catch (SQLException e) {}
    }
}

关键步骤深度解析:
- 第1步 response.setContentType():这是最容易被忽略的致命点。很多初学者写完Servlet发现中文显示为????,第一反应是数据库编码错了,其实只是这行代码漏了。charset=UTF-8必须紧跟在setContentType后面,且不能写成charset=utf-8(大小写敏感);
- 第2步 参数校验request.getParameter()返回null或空字符串是常态。直接rs.setString(1, stuId)会抛NullPointerException,所以必须前置校验。这里用trim().isEmpty()而非== "",因为用户可能输入空格;
- 第3步 预编译SQL"SELECT ... WHERE stu_id = ?"里的?是占位符,pstmt.setString(1, stuId)将参数安全注入。对比拼接SQL "SELECT ... WHERE stu_id = '" + stuId + "'",后者遇到stuId="1'; DROP TABLE student; --"就会执行恶意语句——这就是SQL注入,预编译是唯一防线;
- 第4步 rs.next()的布尔逻辑ResultSet初始游标在第一行之前,next()返回true表示成功移到下一行,并将当前行数据加载到内存。如果查不到,next()返回false,直接进入else分支;
- 第6步 资源释放顺序:必须按ResultSet → PreparedStatement → Connection逆序关闭。因为ResultSet依赖PreparedStatementPreparedStatement依赖Connection,反向关闭会导致SQLException: Operation not allowed after ResultSet closed

3.4 前端交互:HTML表单与Servlet的“契约式通信”

项目虽未提供完整前端,但index.html(你需自行创建)是整个流程的起点。它的设计必须与StudentServlet严格契约:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>学生信息查询系统</title>
</head>
<body>
    <h1>学生信息查询</h1>
    <!-- 表单action指向StudentServlet的映射路径 -->
    <form action="query" method="post">
        <label for="stuId">学生ID:</label>
        <input type="text" id="stuId" name="stuId" placeholder="请输入学生ID,如2023001" required>
        <button type="submit">查询</button>
    </form>

    <!-- 查询结果显示区域(由Servlet动态生成) -->
    <div id="result"></div>
</body>
</html>

关键契约点解析:
- method="post"StudentServlet重写了doPost(),所以表单必须用POST方法。如果写成method="get",请求会进入doGet()(未实现),返回405错误;
- action="query":这是web.xml<servlet-mapping>配置的<url-pattern>值。Servlet容器收到/query请求,才会转发给StudentServlet实例;
- name="stuId"request.getParameter("stuId")里的字符串必须和这里的name属性完全一致(大小写敏感)。写成name="studentId"就会取不到值;
- required属性:HTML5原生校验,阻止空提交,减轻后端压力。但后端仍需二次校验,因为用户可以禁用JS或用curl绕过。

当用户点击查询,浏览器发送的HTTP请求体长这样(简化):

POST /javaweb/query HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded

stuId=2023001

StudentServletdoPost()方法,就是从这个原始字节流里解析出stuId=2023001——这就是前后端通信的物理本质。

4. 部署与运行全流程:从IDEA导入到Tomcat启动的避坑指南

4.1 IDEA项目导入:识别“开箱即用”的隐藏配置

项目自带.idea.iml文件,这是IntelliJ IDEA的专属配置,意味着它已经帮你预设了最佳实践。导入时务必按以下步骤操作,否则可能触发“配置地狱”:

  1. 启动IDEA → Open → 选择项目根目录(不要选prrSvS7qm5SWyagbO5OM-master-4d052c0a5503f7e7b0cb113e0a918d8ab5c37388子目录,那是Git下载的压缩包名,要选外层javawebmyLab目录);
  2. 弹出“Import Project”窗口时,选择“Import project from external model” → “Maven”(即使项目没pom.xml,IDEA也用Maven方式解析Java Web结构);
  3. 关键一步:勾选“Create module groups for multi-module projects”——这会让IDEA自动识别src/main/javasrc/main/webapp为标准目录,否则web.xml可能被当成普通文件;
  4. 在“Project SDK”下拉框中,选择JDK 11或JDK 17(Tomcat 9.0.89官方推荐JDK 11+,JDK 8已停止支持);
  5. 点击“Finish”后,等待IDEA索引完成(右下角进度条消失),此时观察Project面板:src应为蓝色(源代码根目录),webapp应为绿色(Web资源根目录),lib应为黄色(库目录)。

提示:如果lib目录没自动识别为库,右键lib → “Add as Library”,确保mysql-connector-java-8.0.33.jar出现在External Libraries里。缺少这个jar,编译时Class.forName("com.mysql.cj.jdbc.Driver")会报红。

4.2 Tomcat 9.0.89配置:三处必改配置项

IDEA内置Tomcat插件,但默认配置有坑,必须手动修正:

  1. 配置Tomcat Server
    - Run → Edit Configurations → + → Tomcat Server → Local
    - 在Application server点击Configure...,选择你解压的apache-tomcat-9.0.89目录;
    - 关键修改:在Deployment选项卡中,点击+Artifact → 选择javaweb:war exploded(不是war,是war exploded,即解压部署,便于热更新);
    - 在Application context中,必须填写/javaweb(与项目名一致),否则访问http://localhost:8080/会404,必须加/javaweb前缀。

  2. 配置JVM参数(解决中文乱码终极方案)
    - 在Server选项卡中,找到VM options追加以下参数
    -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8
    这确保Tomcat启动时JVM使用UTF-8编码读取所有文件,包括web.xml和JSP。没有它,web.xml里的中文注释可能解析失败。

  3. 配置Tomcat日志级别(加速调试)
    - 打开apache-tomcat-9.0.89/conf/logging.properties
    - 找到java.util.logging.ConsoleHandler.level = INFO,改为:
    java.util.logging.ConsoleHandler.level = FINE
    这样System.out.println()e.printStackTrace()会完整输出到IDEA的Run窗口,而不是被过滤掉。

4.3 启动与验证:四步黄金验证法

启动Tomcat后,不要急于查学生,按顺序执行四步验证,每步失败立即停住排查:

第一步:验证Tomcat基础服务
访问http://localhost:8080/,看到Tomcat欢迎页(猫图标),证明容器启动成功。如果打不开,检查端口是否被占用(netstat -ano | findstr :8080),或IDEA里Tomcat配置的端口是否被改过。

第二步:验证应用部署
访问http://localhost:8080/javaweb/,看到404错误页是正常的,但关键看错误页顶部:type Status Report下方是否有message /javaweb/。如果有,说明应用已部署到/javaweb上下文;如果显示The requested resource [/] is not available,说明Application context没填对。

第三步:验证测试Servlet
访问http://localhost:8080/javaweb/hello,看到”Hello World”,证明Student类被正确加载,web.xmlservlet-mapping生效。如果404,检查web.xml<servlet-class>是否写成stu.Student(包名不能漏)。

第四步:验证数据库连通性
StudentServletdoPost()开头,临时加入:

out.println("<h3>数据库连接测试中...</h3>");
try {
    Class.forName("com.mysql.cj.jdbc.Driver");
    Connection testConn = DriverManager.getConnection(
        "jdbc:mysql://localhost:3306/studentdb?useSSL=false&serverTimezone=UTC", 
        "root", "123456");
    out.println("<h3 style='color:green'>✓ 数据库连接成功!</h3>");
    testConn.close();
} catch (Exception e) {
    out.println("<h3 style='color:red'>✗ 数据库连接失败:" + e.getMessage() + "</h3>");
}

访问/javaweb/query,看到绿色成功提示,才进行最终的学生查询测试。

4.4 常见问题速查表:那些让你抓狂半小时的“低级错误”

问题现象可能原因排查命令/操作解决方案
中文班级名显示为???1. response.setContentType()缺失
2. JDBC URL缺characterEncoding=utf8mb4
3. MySQL数据库/表字符集非utf8mb4
mysql -u root -p -e "SHOW CREATE DATABASE studentdb;"补全response.setContentType("text/html;charset=UTF-8");在JDBC URL加&characterEncoding=utf8mb4;重建数据库
java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Drivermysql-connector-java-8.0.33.jar未被Tomcat加载查看IDEA Run窗口,搜索Driver右键libAdd as Library;或复制jar到apache-tomcat-9.0.89/lib/目录
查询总是返回“未找到学生”1. web.xml<url-pattern>写错
2. 表单name="stuId"request.getParameter("stuId")不一致
3. MySQL里stu_id字段值有空格
SELECT CONCAT('[', stu_id, ']') FROM student;检查web.xml映射路径;确认HTML表单name属性;用TRIM()函数清理数据库数据
Tomcat启动后立即退出JVM内存不足或端口冲突netstat -ano \| findstr :8005(shutdown端口)在IDEA Tomcat配置的Server选项卡中,修改Shutdown port8006;或增大VM options: -Xms512m -Xmx1024m
SQLException: Unknown system variable 'tx_isolation'MySQL 8.0移除了tx_isolation变量,旧驱动不兼容mysql -u root -p -e "SELECT VERSION();"必须使用mysql-connector-java-8.0.33.jar,删除旧版驱动

实操心得:我踩过的最大坑是MySQL 8.0的caching_sha2_password。某次重装系统后,MySQL root密码重置,我习惯性用ALTER USER 'root'@'localhost' IDENTIFIED BY '123456';,结果新密码用了caching_sha2_password插件。而项目里JDBC URL没加allowPublicKeyRetrieval=true,导致连接时卡死在认证环节,IDEA控制台只显示Connecting to database...,毫无报错。最终解决方案是:ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; 切换回传统认证插件。这个教训告诉我:数据库升级不是只改驱动,更要同步检查认证机制。

5. 二次开发与能力延伸:从查询系统到完整CRUD的平滑演进

5.1 从“查”到“增”:三步扩展学生录入功能

现在系统只能查,但课程设计通常要求增删改查。扩展“新增学生”只需三步,且完全复用现有结构:

第一步:扩展现有HTML表单
index.html的查询表单下方,添加录入表单:

<h2>新增学生</h2>
<form action="add" method="post">
    <input type="text" name="stuId" placeholder="学生ID" required><br>
    <input type="text" name="name" placeholder="姓名" required><br>
    <input type="text" name="className" placeholder="班级" required><br>
    <button type="submit">添加</button>
</form>

注意action="add",这将触发新的Servlet。

第二步:创建AddStudentServlet
新建类src/stu/AddStudentServlet.java,继承HttpServlet,重写doPost()

protected void doPost(HttpServletRequest request, HttpServletResponse response) 
        throws ServletException, IOException {
    response.setContentType("text/html;charset=UTF-8");
    PrintWriter out = response.getWriter();

    String stuId = request.getParameter("stuId");
    String name = request.getParameter("name");
    String className = request.getParameter("className");

    if (stuId == null || name == null || className == null) {
        out.println("<h3 style='color:red'>错误:所有字段必填!</h3>");
        return;
    }

    Connection conn = null;
    PreparedStatement pstmt = null;
    try {
        Class.forName("com.mysql.cj.jdbc.Driver");
        conn = DriverManager.getConnection(
            "jdbc:mysql://localhost:3306/studentdb?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true", 
            "root", "123456");

        String sql = "INSERT INTO student (stu_id, name, class_name) VALUES (?, ?, ?)";
        pstmt = conn.prepareStatement(sql);
        pstmt.setString(1, stuId);
        pstmt.setString(2, name);
        pstmt.setString(3, className);
        int rows = pstmt.executeUpdate(); // 注意是executeUpdate(),不是executeQuery()

        if (rows > 0) {
            out.println("<h3 style='color:green'>✓ 学生添加成功!</h3>");
        } else {
            out.println("<h3 style='color:red'>✗ 添加失败,请检查ID是否重复</h3>");
        }
    } catch (SQLException e) {
        if (e.getSQLState().equals("23000")) { // MySQL唯一约束错误码
            out.println("<h3 style='color:red'>✗ 错误:学生ID [" + stuId + "] 已存在!</h3>");
        } else {
            out.println("<h3 style='color:red'>数据库错误:" + e.getMessage() + "</h3>");
        }
    } catch (Exception e) {
        out.println("<h3 style='color:red'>系统错误:" + e.getMessage() + "</h3>");
    } finally {
        // 同样的资源释放逻辑...
    }
}

第三步:配置web.xml映射
web.xml</servlet>后添加:

<servlet>
    <servlet-name>AddStudentServlet</servlet-name>
    <servlet-class>stu.AddStudentServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>AddStudentServlet</servlet-name>
    <url-pattern>/add</url-pattern>
</servlet-mapping>

至此,“增”功能完成。你会发现,新增逻辑和查询逻辑高度复用:相同的驱动加载、相同的连接获取、相同的异常处理模式。这就是分层设计的价值——业务变化只影响特定层次,其他层岿然不动。

5.2 架构升级路线图:从Servlet原生到现代Java Web的演进路径

这个项目是你的“技术起跑线”,而非终点。基于它,你可以规划一条平滑的升级路径:

  • 短期(1周内):引入DAO模式
    StudentServlet里所有JDBC代码抽离到stu.dao.StudentDAO类中,StudentServlet只负责接收请求、调用dao.findByStuId(stuId)、返回结果。这会让你第一次体会到“关注点分离”的威力——Servlet专注HTTP,DAO专注数据库。

  • 中期(2周):集成JSP+JSTL
    StudentServlet里拼接HTML的代码去掉,改为request.setAttribute("student", student); request.getRequestDispatcher("result.jsp").forward(request, response);,然后在result.jsp里用<c:if test="${not empty student}"><c:out value="${student.name}"/>渲染。这带你进入MVC的View层,理解请求转发与重定向的区别。

  • 长期(1个月):迁移到Spring Boot
    用Spring Initializr创建新项目,添加spring-boot-starter-webspring-boot-starter-jdbc依赖。你会发现:

  • @RestController替代了HttpServlet
  • JdbcTemplate替代了PreparedStatement
  • application.properties里的spring.datasource.url替代了硬编码的JDBC URL;
  • @PathVariable替代了request.getParameter()
    这时回看这个Servlet项目,你会恍然大悟:所谓框架,不过是把重复劳动封装成约定俗成的接口。

最后分享一个小技巧:在StudentServletdoPost()方法里,把System.out.println()换成ServletContext日志:
java ServletContext context = getServletContext(); context.log("查询学生ID: " + stuId); // 日志会写入catalina.out,且带时间戳
这比System.out更专业,且符合Servlet规范。当你在生产环境排查问题时,这种规范日志能救你一命——因为System.out在某些容器配置下会被重定向到黑洞,而ServletContext.log()永远可靠。

这个学生信息查询系统,表面看只是几行代码和一张表,但它的每一处设计都在无声地告诉你:Web开发的本质,是理解请求如何穿越网络、数据如何跨越进程、字符如何在不同编码间舞蹈。当你亲手填平MySQL 8.0的认证陷阱、读懂Tomcat日志里的每一行堆栈、在web.xml里准确配置一个<servlet-mapping>时,你就不再是一个调用API的程序员,而是一个掌控全局的系统构建者。这,才是这个“古老”项目最珍贵的礼物。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一个开箱即用的Java Web学生信息查询系统,基于标准Servlet技术实现前后端通信,后端使用MySQL 8.0数据库管理学生ID、姓名、班级等基础信息。项目运行在Apache Tomcat 9.0.89服务器上,结构遵循Java EE分层规范:src目录包含学生实体类(stu)用于数据封装,StudentServlet负责接收POST请求并按ID查询学生详情,另附简易测试Servlet(Student)响应/hello路径便于快速验证。lib目录已集成必需依赖库,.gitignore和README.md提供版本控制支持与使用指引,同时内置IntelliJ IDEA项目配置文件(.idea、*.iml),无需额外配置即可导入编译运行。适合Java Web初学者练习Servlet开发流程、高校课程设计参考,或作为轻量级学生信息查询功能模块进行二次扩展。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值