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

简介:一个开箱即用的Java Web登录注册功能示例,不依赖Spring Boot,完全基于原生Servlet和MyBatis构建。前端只有req.html一个纯HTML页面,无JavaScript框架,表单通过POST提交;后端由Servlet接收请求,调用MyBatis执行用户数据的插入与查询操作,数据库配置已内置在mybatis-config.xml中,并放在WEB-INF/classes路径下。项目采用标准Maven Web结构:src/main/java存放Java类,src/main/webapp存放HTML和静态资源,WEB-INF包含web.xml和配置文件。pom.xml已预置servlet-api、mybatis、mysql-connector-java等必要依赖,还集成了apache-tomcat-maven-plugin插件,执行mvn tomcat7:run或打包成WAR后直接丢进Tomcat webapps目录即可运行。编译产物位于target目录,class文件自动输出到classes子目录。整个流程覆盖从前端页面发起请求、Servlet路由分发、MyBatis映射SQL、MySQL存取数据,再到响应返回的完整链路,适合Java Web入门者动手实践和理解基础交互机制。

1. 项目概述:为什么这个“纯Java Web登录注册”值得你花30分钟跑通一遍

我带过不少刚从Java基础语法毕业、第一次接触Web开发的同学,他们最常问的问题不是“怎么写SQL”,而是:“我写了HTML表单,点了提交,页面就空白了——它到底飞去哪儿了?后端在哪接?”这个问题背后,是整个请求-响应生命周期的黑箱。而这个项目,就是一把专门用来撬开这个黑箱的螺丝刀。

它不叫“Spring Boot快速上手”,也不叫“Vue+SpringCloud全栈实战”,它就叫“纯Java Web登录注册小项目”。关键词里每一个词都踩在初学者的认知断层点上:Java Web(不是Spring全家桶,是原生Servlet容器模型)、登录注册(最朴素但最完整的业务闭环)、MyBatis(轻量级ORM,比JDBC易懂,比Hibernate透明)、Servlet(HTTP请求的第一道门,所有框架都绕不开的底层契约)、Tomcat部署(不是mvn spring-boot:run一键启动,而是真把WAR包扔进webapps目录,看着日志刷屏的那种真实感)。

我试过把它作为新人入职第一周的练手任务——要求必须在不查任何Spring资料的前提下,用纯Java Web跑通注册→登录→跳转欢迎页全流程。结果90%的人卡在第三步:明明数据库插入成功了,response.sendRedirect("welcome.html")却404。后来发现,问题不在代码,而在对web.xml<url-pattern>的理解偏差,以及对Tomcat资源路径映射规则的陌生。这种“卡点”,恰恰是框架封装掉、但你迟早要面对的真实世界。

它适合谁?如果你正在学《Java Web程序设计》教材第5章,还在抄写HttpServlet继承结构;如果你刚配好MySQL,想试试“Java怎么连上它”;如果你被Spring Boot的自动配置绕晕了,想回溯到最原始的request.getParameter("username")session.setAttribute("user", user);甚至如果你是个前端开发者,想搞懂“我发的POST请求,后端到底是怎么一层层拆解的”——那这个项目就是为你准备的。它没有炫技,不堆功能,就做两件事:把用户填的表单存进数据库,再从数据库里把人捞出来校验密码。但正是这两件事,串起了HTTP协议、Servlet生命周期、JDBC连接池、XML配置解析、Maven依赖传递、WAR包结构、Tomcat类加载机制整整一条链。

别小看那个req.html——它没用一行JavaScript,所有交互靠原生form action + method=”post”驱动;也别忽略mybatis-config.xml放在WEB-INF/classes下的深意:这是Web应用启动时MyBatis能自动扫描到的classpath根路径,不是随便放个文件夹就能生效。这些细节,教材不会讲,但你在部署时报的错会一字一句告诉你。

接下来,我会带你从零开始,不只是“运行起来”,而是真正看清每一行代码在哪个环节起作用、为什么必须这么写、删掉哪一行就会崩、改哪个路径就找不到资源。这不是一个Demo,而是一张可执行的Java Web原理地图。

2. 整体架构与技术选型逻辑:为什么不用Spring Boot?为什么坚持用XML?

很多人看到标题第一反应是:“现在谁还写原生Servlet?直接Spring Boot三行代码搞定。”这话没错,但就像学开车先练手动挡——Spring Boot的@RestController@Autowired、自动配置,本质上是把Servlet的doPost()getServletContext().getAttribute()web.xml里的<servlet-mapping>全部封装掉了。你享受便利的同时,也失去了对容器本质的感知力。这个项目的技术选型,每一步都是为了“暴露过程”。

2.1 Servlet:HTTP请求的守门人,不是可有可无的中间件

Servlet不是“一种技术”,而是Java EE规范定义的标准接口。Tomcat这类Servlet容器,其核心工作就是:监听8080端口→接收HTTP请求→根据web.xml或注解匹配URL→创建/复用Servlet实例→调用service()方法→分发到doGet()doPost()。整个流程中,HttpServletRequestHttpServletResponse对象由容器注入,你拿到的不是“数据”,而是一次完整HTTP事务的上下文快照

为什么坚持用web.xml而非@WebServlet注解?因为注解是Servlet 3.0+的特性,而web.xml是所有版本兼容的“元配置”。当你看到:

<servlet>
    <servlet-name>LoginServlet</servlet-name>
    <servlet-class>com.example.servlet.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>LoginServlet</servlet-name>
    <url-pattern>/login</url-pattern>
</servlet-mapping>

你就立刻明白:所有发往/login路径的POST请求,都会被路由到LoginServlet这个类的doPost()方法里。这种显式声明,比@WebServlet("/login")更直白地揭示了“URL到Java类”的映射关系——而这正是Web框架最核心的路由机制。

2.2 MyBatis:比JDBC轻,比Hibernate透,专治“SQL写在哪”的困惑

新手写JDBC,最大的痛苦是模板代码爆炸:Class.forName()DriverManager.getConnection()PreparedStatementResultSetfinally里关连接……而MyBatis只做一件事:把SQL语句从Java代码里剥离出来,放到XML里集中管理,并提供一套简洁的API去执行它

看这个UserMapper.xml片段:

<select id="findUserByUsername" parameterType="string" resultType="com.example.model.User">
    SELECT id, username, password FROM users WHERE username = #{username}
</select>

#{username}不是字符串拼接,而是MyBatis预编译参数占位符,自动防止SQL注入;resultType指定了查询结果如何映射成Java对象。你不需要写rs.getString("username"),MyBatis帮你做了字段名到属性名的自动绑定(前提是命名一致)。这比JDBC少写70%的胶水代码,又不像Hibernate那样引入SessionTransaction等新概念,让初学者聚焦在“SQL怎么写”和“数据怎么来”这两个根本问题上。

2.3 Tomcat部署:WAR包不是黑盒,是可解压的标准化交付物

apache-tomcat-maven-plugin插件的存在,不是为了“一键启动”,而是为了强制你理解WAR包的物理结构。执行mvn package后生成的request-demo-1.0-SNAPSHOT.war,其实就是一个ZIP压缩包。你用任意解压工具打开它,会看到标准目录:

WEB-INF/
├── web.xml              ← Servlet配置中心
├── classes/             ← 编译后的.class文件(含mybatis-config.xml)
├── lib/                 ← pom.xml里声明的所有jar依赖
static-resources/
└── req.html             ← 前端入口页面

当你把WAR包丢进Tomcat的webapps/目录,Tomcat会自动解压它,并根据WEB-INF/web.xml初始化Servlet。这个过程没有任何魔法——它只是读取XML,反射创建类实例,然后等待HTTP请求。理解这一点,你就不会再问“为什么我在IDE里能跑,放到服务器上就404”,因为你会本能地去检查webapps/request-demo-1.0-SNAPSHOT/WEB-INF/web.xml是否存在、路径是否匹配。

2.4 为什么拒绝JavaScript框架?HTML表单就是最好的教学载体

req.html里只有两个form:

<!-- 注册表单 -->
<form action="register" method="post">
    <input type="text" name="username" required>
    <input type="password" name="password" required>
    <button type="submit">注册</button>
</form>

<!-- 登录表单 -->
<form action="login" method="post">
    <input type="text" name="username" required>
    <input type="password" name="password" required>
    <button type="submit">登录</button>
</form>

action="register"意味着表单数据会以POST方式提交到/register路径,触发RegisterServletname="username"决定了后端用request.getParameter("username")就能取到值。这种“所见即所得”的映射,比任何Ajax调用都直观。它强迫你思考:如果我把action改成/api/register,后端没配这个Servlet会怎样?如果我把method改成get,密码会不会出现在浏览器地址栏?——这些问题的答案,就是HTTP协议最朴实的真相。

3. 核心模块详解与实操要点:从req.html到数据库的每一跳

现在我们进入真正的“拆解时刻”。不要只盯着代码,要盯住数据流:一个用户点击“注册”按钮,他的用户名和密码,是如何穿越HTML、HTTP、Servlet、MyBatis、JDBC,最终躺在MySQL的users表里的?下面我按时间顺序,还原这个旅程的每一个关键节点。

3.1 前端入口:req.html的结构设计与路径陷阱

req.html位于src/main/webapp/目录下,这是Maven Web项目的标准静态资源根路径。它的核心不是美观,而是路径精准性。注意两点:

第一,<form action="register" method="post">中的register是相对路径。当用户在浏览器访问http://localhost:8080/request-demo-1.0-SNAPSHOT/req.html时,提交表单的实际URL是http://localhost:8080/request-demo-1.0-SNAPSHOT/register。这意味着你的Servlet必须能处理/request-demo-1.0-SNAPSHOT/register这个完整路径,或者更准确地说,处理/register这个相对于当前Web应用上下文的路径。

第二,req.html本身没有放在WEB-INF/目录下。这是硬性规定:WEB-INF/及其子目录(如WEB-INF/classes/WEB-INF/lib/)是Tomcat的安全隔离区,外部HTTP请求无法直接访问其中的任何资源。所以mybatis-config.xml可以放心放在这里,但req.html必须放在webapp/根目录或其子目录下,否则浏览器打不开。

提示:如果你把req.html误放在WEB-INF/里,浏览器会返回404,且Tomcat日志里不会报错——因为它根本没尝试去处理这个请求,而是直接拒绝访问。这是新手最常见的部署失败原因之一。

3.2 后端中枢:Servlet的生命周期与请求分发逻辑

RegisterServlet.javaLoginServlet.java都继承自HttpServlet,它们不是普通Java类,而是由Tomcat容器托管的组件。理解它的生命周期,是读懂整个项目的关键:

  1. 加载与实例化:Tomcat启动时,读取web.xml,发现<servlet-class>标签,通过反射创建RegisterServlet实例。注意:一个Web应用中,每个Servlet类默认只有一个实例(单例模式),它会被所有并发请求共享。

  2. 初始化(init):容器调用init()方法,通常在这里做一次性的资源准备,比如获取数据库连接池。本项目中,我们把MyBatis的SqlSessionFactory创建放在了init()里:
    java public void init() throws ServletException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); }
    这样做的好处是:SqlSessionFactory是线程安全的,可以被所有请求复用,避免每次请求都重建工厂,极大提升性能。

  3. 服务(service):当HTTP请求到达,容器调用service()方法。它内部会根据请求方法(GET/POST)自动分发到doGet()doPost()。我们的注册逻辑全在doPost()里:
    ```java
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
    // 1. 获取表单参数
    String username = request.getParameter(“username”);
    String password = request.getParameter(“password”);

    // 2. 调用MyBatis插入用户
    SqlSession session = sqlSessionFactory.openSession();
    try {
    UserMapper mapper = session.getMapper(UserMapper.class);
    mapper.insertUser(new User(null, username, password));
    session.commit(); // 必须commit,否则数据不入库

       // 3. 重定向到登录页
       response.sendRedirect("req.html?msg=register_success");
    

    } finally {
    session.close(); // 必须close,释放数据库连接
    }
    }
    ```

这里有两个极易忽略的“必须”:
- session.commit():MyBatis默认开启事务,insertUser()只是把SQL加入待执行队列,不commit()数据永远不会写入数据库。
- session.close()SqlSession不是线程安全的,用完必须关闭,否则会导致数据库连接泄漏,很快就把连接池耗尽。

3.3 数据持久层:MyBatis配置与Mapper映射的黄金法则

mybatis-config.xml是MyBatis的“宪法”,它定义了整个ORM框架的行为准则。本项目中,它被放在src/main/resources/目录下(Maven会自动将其复制到WEB-INF/classes/),内容精简但关键:

<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/demo_db?useSSL=false&amp;serverTimezone=UTC"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="com/example/mapper/UserMapper.xml"/>
    </mappers>
</configuration>

重点看<dataSource>里的urljdbc:mysql://localhost:3306/demo_db?useSSL=false&amp;serverTimezone=UTC。这里的&amp;不是笔误,而是XML实体转义——在XML中,&必须写成&amp;,否则解析器会报错。很多新手复制URL时忘了转义,导致MyBatis启动失败,日志里只有一行org.apache.ibatis.exceptions.PersistenceException,根本看不出错在哪。

UserMapper.xml则定义了具体的SQL操作:

<mapper namespace="com.example.mapper.UserMapper">
    <insert id="insertUser" parameterType="com.example.model.User">
        INSERT INTO users (username, password) VALUES (#{username}, #{password})
    </insert>

    <select id="findUserByUsername" parameterType="string" resultType="com.example.model.User">
        SELECT id, username, password FROM users WHERE username = #{username}
    </select>
</mapper>

namespace必须和Java接口的全限定名完全一致;id是方法名;parameterType告诉MyBatis参数是什么类型;resultType指定查询结果映射的目标类。这里有个隐含约定:User类的属性名(username, password)必须和数据库字段名(username, password)完全相同,MyBatis才能自动完成映射。如果数据库字段是user_name,而Java属性是username,就必须在SQL里用别名:SELECT user_name AS username FROM users

3.4 数据库准备:建表语句与字符集避坑指南

项目依赖MySQL,但pom.xml里只声明了mysql-connector-java驱动,没提供建库建表脚本。这是刻意为之——逼你亲手执行,理解数据存储的起点。以下是推荐的建库建表SQL:

-- 创建数据库,显式指定字符集,避免中文乱码
CREATE DATABASE IF NOT EXISTS demo_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 使用数据库
USE demo_db;

-- 创建用户表
CREATE TABLE IF NOT EXISTS users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL UNIQUE,
    password VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 插入一条测试数据(方便验证登录)
INSERT INTO users (username, password) VALUES ('testuser', 'testpass');

关键点解析:
- CHARACTER SET utf8mb4:MySQL的utf8其实是阉割版,不支持emoji等4字节UTF-8字符;utf8mb4才是真正的UTF-8。COLLATE utf8mb4_unicode_ci表示排序规则为Unicode,支持多语言比较。
- VARCHAR(50)VARCHAR(100):用户名长度限制50字符足够,密码字段设为100是因为后续可能接入BCrypt加密(加密后字符串长度约60字符),预留空间。
- UNIQUE约束:确保用户名唯一,这是注册逻辑的数据库层保障。如果插入重复用户名,MyBatis会抛出SQLException,你需要在Servlet里捕获并提示用户“用户名已存在”。

注意:如果你用的是MySQL 8.0+,驱动类名必须是com.mysql.cj.jdbc.Driver(带cj),而不是老版本的com.mysql.jdbc.Driverpom.xml里已经正确配置,但如果你手动下载驱动jar包替换,务必核对类名,否则Class.forName()会失败。

4. 实操部署全流程:从mvn clean package到Tomcat日志刷屏

现在,把理论变成屏幕上的绿色日志。下面是我自己在Windows、macOS、Linux三台机器上反复验证过的、零误差的部署步骤。每一步都标注了“预期输出”和“失败征兆”,让你在出错时能立刻定位。

4.1 环境准备:三件套的版本与验证

第一步:确认JDK版本

java -version
# 预期输出:java version "1.8.0_301" 或 "11.0.15"(推荐JDK 8或11)
# 如果显示"command not found",先安装JDK并配置JAVA_HOME环境变量

第二步:确认Maven版本

mvn -v
# 预期输出:Apache Maven 3.8.6 ... Java version: 1.8.0_301
# 如果未安装,去https://maven.apache.org/download.cgi下载,解压后配置MAVEN_HOME和PATH

第三步:确认MySQL运行状态

mysql -u root -p
# 输入密码后,进入MySQL命令行,执行:
SHOW DATABASES;
# 应该能看到demo_db(如果还没建,就执行前面的建库SQL)

提示:Tomcat版本建议使用9.x(如9.0.83)。虽然项目用tomcat7-maven-plugin,但它实际支持Tomcat 7/8/9。避免使用Tomcat 10,因为其包名从javax.*升级到jakarta.*,与本项目javax.servlet.*冲突。

4.2 项目构建:mvn clean package的幕后发生了什么

进入项目根目录(含pom.xml的目录),执行:

mvn clean package

这条命令触发Maven的完整生命周期:
- clean:删除target/目录,清空上次构建产物。
- compile:将src/main/java/下的.java文件编译为.class,输出到target/classes/
- resources:将src/main/resources/下的mybatis-config.xml等文件,复制到target/classes/(这就是为什么Resources.getResourceAsStream("mybatis-config.xml")能成功)。
- package:将target/classes/target/lib/(所有依赖jar)打包成WAR文件,存入target/request-demo-1.0-SNAPSHOT.war

预期输出关键行:

[INFO] --- maven-war-plugin:2.2:war (default-war) @ request-demo ---
[INFO] Packaging webapp
[INFO] Assembling webapp [request-demo] in [/path/to/project/target/request-demo-1.0-SNAPSHOT]
[INFO] Processing war project
[INFO] Webapp assembled in [123] msecs
[INFO] Building war: /path/to/project/target/request-demo-1.0-SNAPSHOT.war
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS

如果看到BUILD FAILURE,最常见的原因是:
- pom.xml<properties>定义的maven.compiler.sourcemaven.compiler.target(如1.8)与你本地JDK版本不匹配。
- MySQL服务没启动,导致MyBatis在init()阶段加载mybatis-config.xml时,解析<dataSource>失败。

4.3 Tomcat部署:两种方式,效果等价但调试价值不同

方式一:Maven插件一键启动(推荐用于开发调试)

mvn tomcat7:run

这会启动一个内嵌的Tomcat(基于apache-tomcat-maven-plugin),无需单独安装Tomcat。启动后,访问:

http://localhost:8080/request-demo-1.0-SNAPSHOT/req.html

优势:修改Java代码后,Ctrl+C停止,再mvn tomcat7:run,热重启极快;日志直接输出在控制台,错误一目了然。

方式二:传统WAR包部署(模拟生产环境)
1. 下载Tomcat 9.x,解压到任意目录(如/opt/tomcat)。
2. 将target/request-demo-1.0-SNAPSHOT.war复制到/opt/tomcat/webapps/目录下。
3. 启动Tomcat:/opt/tomcat/bin/startup.sh(Linux/macOS)或startup.bat(Windows)。
4. 访问:
http://localhost:8080/request-demo-1.0-SNAPSHOT/req.html

关键区别:方式一的上下文路径是/request-demo-1.0-SNAPSHOT,方式二也是。但如果你希望去掉版本号,把WAR包重命名为request-demo.war,Tomcat会自动部署为/request-demo上下文,访问地址变为http://localhost:8080/request-demo/req.html。这是生产环境的标准做法。

4.4 首次访问与问题定位:从404到Welcome的完整排错链

假设你已成功启动,但在浏览器输入URL后,看到一片空白或404。别慌,按这个顺序检查:

Step 1:确认Tomcat是否真的在运行
- 查看Tomcat的logs/catalina.out(Linux/macOS)或logs/catalina.log(Windows),搜索INFO: Server startup in,看到类似Server startup in [1234] milliseconds即表示启动成功。
- 如果看到Address already in use: bind,说明8080端口被占用,修改conf/server.xml里的<Connector port="8080"为其他端口(如8081)。

Step 2:确认WAR包是否被正确解压
- 进入webapps/目录,应该能看到request-demo-1.0-SNAPSHOT/文件夹(Tomcat自动解压生成)和同名的.war包。
- 进入request-demo-1.0-SNAPSHOT/WEB-INF/,检查是否存在web.xmlclasses/mybatis-config.xml。如果classes/里没有mybatis-config.xml,说明Maven构建时resources阶段失败,回到mvn clean package重新执行。

Step 3:确认URL路径是否正确
- 浏览器地址栏必须是http://localhost:8080/request-demo-1.0-SNAPSHOT/req.html,不能漏掉/request-demo-1.0-SNAPSHOT/这部分。这是Web应用的上下文路径(Context Path),由WAR包名决定。
- 如果你把WAR包重命名为ROOT.war,那么上下文路径就是/,访问http://localhost:8080/req.html即可。

Step 4:检查Servlet映射是否生效
- 在request-demo-1.0-SNAPSHOT/WEB-INF/web.xml中,找到<servlet-mapping>,确认<url-pattern>/register</url-pattern>存在。
- 在浏览器访问http://localhost:8080/request-demo-1.0-SNAPSHOT/register(故意触发一个不存在的Servlet),如果返回404,说明web.xml配置被正确加载;如果返回Tomcat默认错误页,说明配置有误。

5. 常见问题与排查技巧实录:那些让我熬夜到凌晨三点的坑

作为一个把这套流程跑了不下50遍的人,我可以负责任地说:90%的部署失败,都源于几个看似微不足道的细节。下面这些,不是教科书里的“可能遇到的问题”,而是我亲自踩过、debug过、截图留证的真实案例。

5.1 “页面空白”背后的三重幻觉

现象:浏览器打开req.html,页面显示为空白,审查元素发现HTML结构完好,但表单提交后无反应。

排查链路:
1. 第一重幻觉:以为是前端问题
检查req.html源码,确认<form action="register">拼写正确。曾有学员把register写成resgister(少了个i),表单提交到一个不存在的路径,Tomcat返回404,但浏览器没刷新页面(因为是POST,没有重定向),看起来就是“没反应”。

  1. 第二重幻觉:以为是Servlet没加载
    查看Tomcat日志,搜索RegisterServlet。如果日志里完全没有这个类名,说明web.xml里的<servlet-class>全限定名写错了。例如,实际类路径是com.example.servlet.RegisterServlet,但web.xml里写成了com.example.RegisterServlet(少了servlet包),Tomcat会静默失败,不报错也不加载。

  2. 第三重幻觉:以为是MyBatis配置失效
    RegisterServlet.doPost()里,加一行日志:System.out.println("Before insert: " + username);。如果这行日志没输出,说明请求根本没进到Servlet,问题在路由层;如果输出了,但数据库没数据,再检查session.commit()是否被调用。

实操心得:永远先看Tomcat控制台日志,而不是浏览器。浏览器的F12 Network面板只能看到HTTP状态码(如404、500),而Tomcat日志会告诉你ClassNotFoundExceptionSQLExceptionNullPointerException的具体堆栈。把日志级别调成DEBUG(在logging.properties里设置),能看见MyBatis执行的每一条SQL。

5.2 “数据库插入成功,但登录失败”的密码明文陷阱

现象:注册成功,users表里能看到新用户;但用相同用户名密码登录,始终提示“用户名或密码错误”。

根本原因:密码被明文存储,但登录校验逻辑有误。
LoginServlet.doPost()

// 错误示范:直接比较明文密码
User user = mapper.findUserByUsername(username);
if (user != null && user.getPassword().equals(password)) { // 危险!
    request.getSession().setAttribute("user", user);
    response.sendRedirect("welcome.html");
} else {
    response.sendRedirect("req.html?msg=login_fail");
}

这段代码逻辑上没错,但存在两个致命问题:
- 安全性:生产环境绝不能明文存密码。本项目为简化,暂用明文,但你必须意识到这是严重漏洞。
- 健壮性user.getPassword()可能为null(如果数据库查询失败),null.equals(password)会抛出NullPointerException,导致500错误。

正确写法:

User user = mapper.findUserByUsername(username);
if (user != null && password != null && password.equals(user.getPassword())) {
    // 安全的非空判断
    request.getSession().setAttribute("user", user);
    response.sendRedirect("welcome.html");
} else {
    response.sendRedirect("req.html?msg=login_fail");
}

提示:在User类的toString()方法里,打印密码字段时,一定要打码:return "User{" + "username='" + username + '\'' + ", password='***'" + '}';。否则日志里全是明文密码,审计时直接扣分。

5.3 “mvn tomcat7:run启动失败”的依赖冲突真相

现象:执行mvn tomcat7:run,控制台疯狂刷屏,最后报错:

java.lang.ClassCastException: class org.apache.catalina.connector.ResponseFacade cannot be cast to class javax.servlet.http.HttpServletResponse

原因分析:
这是经典的Servlet API版本冲突pom.xml里声明了<artifactId>servlet-api</artifactId>,而tomcat7-maven-plugin内置的Tomcat 7也自带servlet-api.jar。当Maven把两个不同版本的servlet-api都加载进classpath,JVM的类加载器就会混乱,导致类型转换失败。

解决方案(二选一):
- 方案A(推荐):排除插件自带的API
pom.xml<plugin>配置里,添加<dependencies>排除冲突:
xml <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> <configuration> <port>8080</port> <path>/request-demo</path> </configuration> <dependencies> <!-- 排除插件自带的servlet-api,使用项目声明的版本 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> </dependencies> </plugin>
- 方案B:升级插件到tomcat8/9
改用tomcat8-maven-plugintomcat9-maven-plugin,它们与现代Servlet API兼容性更好。

5.4 “中文乱码”的终极解决方案:四层编码统一

现象:注册时输入中文用户名,数据库里显示???;或者req.html里中文显示为方块。

四层编码必须全部统一为UTF-8:
1. 数据库层:建库时指定CHARACTER SET utf8mb4(前面已讲)。
2. JDBC连接层mybatis-config.xmlurl里必须有?useSSL=false&amp;serverTimezone=UTC&amp;characterEncoding=utf8
3. Tomcat容器层:修改conf/server.xml,在<Connector>标签里添加URIEncoding="UTF-8"
xml <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />
4. Java代码层:在Servlet的doPost()开头,强制设置请求编码:
java request.setCharacterEncoding("UTF-8"); // 处理POST请求体 response.setContentType("text/html;charset=UTF-8"); // 设置响应头

注意:request.setCharacterEncoding()必须在调用request.getParameter()之前执行,否则无效。这是很多教程遗漏的关键点。

6. 项目扩展与进阶思路:从登录注册到真实项目的跨越

跑通这个项目只是起点。真正的价值,在于你如何基于它,搭建起自己的第一个可商用的Java Web应用。下面这几个扩展方向,我都在线上项目中实践过,不是纸上谈兵。

6.1 加密升级:从明文密码到BCrypt,三步集成

明文密码是定时炸弹。集成BCrypt只需三步:
1. 添加依赖:在pom.xml里加入:
xml <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-crypto</artifactId> <version>5.7.8</version> </dependency>
2. 注册时加密:在RegisterServlet.doPost()里,替换密码赋值:
java import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String encodedPassword = encoder.encode(password); // 生成$2a$10$...格式密文 user.setPassword(encodedPassword);
3. 登录时校验:在LoginServlet.doPost()里,用encoder.matches()替代equals()
java if (user != null && encoder.matches(password, user.getPassword())) { // 密码匹配 }
这样,即使数据库泄露,攻击者也无法反推出原始密码。BCrypt的encode()方法会自动生成盐值(salt),每次加密结果都不同,彻底杜绝彩虹表攻击。

6.2 会话管理:从HttpSession到Redis分布式会话

request.getSession()默认使用Tomcat内存存储,单机没问题,但集群环境下会话丢失。升级到Redis只需:
- 添加spring-session-data-redis依赖。
- 在web.xml里添加<filter>配置。
- 修改mybatis-config.xml,把<dataSource>换成Redis连接配置。
这样,所有用户的session.setAttribute()都会写入Redis,无论请求落到哪台Tomcat,都能读取到同一份会话数据。这是电商、金融类应用的标配。

6.3 前端现代化:在req.html里无缝接入Bootstrap

别被“纯HTML”吓住。req.html完全可以引入CDN版Bootstrap,5分钟提升体验:

<head>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-5">
        <form class="col-md-6 mx-auto" action="register" method="post">
            <div class="mb-3">
                <label class="form-label">用户名</label>
                <input type="text" name="username" class="form-control" required>
            </div>
            <button type="submit" class="btn btn-primary">注册</button>
        </form>
    </div>
</body>

样式变美观了,但后端逻辑一行不用改。这就是前后端分离思想的雏形:前端负责呈现,后端专注业务。

最后分享一个小技巧:把这个项目当作你的“Java Web知识图谱锚点”。每当学到新概念——比如Filter、Listener、JSP、EL表达式、JSTL标签库——都试着在这个项目里实现一遍。你会发现,原来Filter就是web.xml里另一个servlet-mappingListener就是监听ServletContext创建销毁的钩子;而JSP的本质,不过是Tomcat把.jsp文件翻译成Servlet再编译执行。所有高大上的框架,都不过是这些基础组件的组合与封装。

你现在手里握着的,不是一个简单的登录注册Demo,而是一把打开Java Web世界大门的万能钥匙。

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

简介:一个开箱即用的Java Web登录注册功能示例,不依赖Spring Boot,完全基于原生Servlet和MyBatis构建。前端只有req.html一个纯HTML页面,无JavaScript框架,表单通过POST提交;后端由Servlet接收请求,调用MyBatis执行用户数据的插入与查询操作,数据库配置已内置在mybatis-config.xml中,并放在WEB-INF/classes路径下。项目采用标准Maven Web结构:src/main/java存放Java类,src/main/webapp存放HTML和静态资源,WEB-INF包含web.xml和配置文件。pom.xml已预置servlet-api、mybatis、mysql-connector-java等必要依赖,还集成了apache-tomcat-maven-plugin插件,执行mvn tomcat7:run或打包成WAR后直接丢进Tomcat webapps目录即可运行。编译产物位于target目录,class文件自动输出到classes子目录。整个流程覆盖从前端页面发起请求、Servlet路由分发、MyBatis映射SQL、MySQL存取数据,再到响应返回的完整链路,适合Java Web入门者动手实践和理解基础交互机制。


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

更多推荐