1. 从零搭建一个用户注册功能为什么我选这条最笨的路很多人第一次接触Java Web脑子里想的都是我要做个网站然后打开搜索引擎看到Spring Boot、MyBatis、Maven、Tomcat一堆名词砸过来直接懵了。我当年也是这样光是配环境就折腾了整整两天最后连个Hello World都没跑起来。后来我换了个思路——不追求最新最全只追求能跑通用一个最原始、最笨、但每一步都能看得见摸得着的方式把用户注册这个功能从头到尾做出来。这篇文章要聊的就是这件事用最基础的Java Web技术栈Servlet JSP JDBC MySQL从安装JDK开始一步步做到一个能用的用户注册页面。没有Spring没有框架没有花里胡哨的东西。你可能会问2025年了还学Servlet我的回答是你连Servlet都没搞明白学Spring Boot也只是在抄配置。注册功能虽然简单但它把前端表单提交→后端接收参数→数据库写入→结果反馈这条完整链路全串起来了这是所有Web开发的骨架。适合谁看适合刚学完Java基础语法、想动手做个东西但不知道从哪下手的人适合被各种框架教程绕晕、想回到原点重新理解Web开发的人也适合那些想快速给某个小项目加个注册功能、不想引入重型框架的人。我保证每一步都有明确的命令和代码你跟着敲就能跑通。在开始之前先把要用的东西列清楚免得做到一半发现缺东西。组件版本建议作用JDK8或11Java运行环境编译和运行代码Tomcat8.5或9Web服务器负责处理HTTP请求MySQL5.7或8.0数据库存用户信息IDEIntelliJ IDEA社区版写代码的工具JDBC驱动mysql-connector-javaJava连接MySQL的桥梁这些东西全部免费不需要任何付费工具。版本也不用追求最新JDK 8配Tomcat 8.5是最稳的组合网上遇到问题也最容易搜到答案。2. 环境安装每一步都告诉你为什么这么做2.1 JDK安装与验证JDK是整个Java世界的基石没有它什么都干不了。去Oracle官网或者用OpenJDK都行下载对应你操作系统的安装包。Windows用户双击安装一路下一步就行但记住安装路径后面配环境变量要用。安装完之后最关键的一步是配环境变量。很多人这一步出错导致命令行里敲java提示不是内部或外部命令。具体操作右键此电脑→属性→高级系统设置→环境变量在系统变量里新建JAVA_HOME值填你的JDK安装目录比如C:\Program Files\Java\jdk1.8.0_301。然后找到Path变量编辑它新增一条%JAVA_HOME%\bin。为什么要配这个因为操作系统需要知道去哪里找java.exe和javac.exe这两个可执行文件。JAVA_HOME是给其他工具比如Tomcat、Maven引用的Path是给命令行直接调用的。两个都要配缺一不可。验证方式很简单打开命令行输入java -version javac -version如果都能输出版本号说明JDK装好了。如果只成功了一个检查Path里是不是只加了bin目录但JAVA_HOME没配对。2.2 Tomcat的下载与目录结构解读Tomcat是一个Servlet容器说白了就是一个能跑Java Web程序的服务器。你去Apache Tomcat官网下载zip压缩包解压到任意目录就行不需要安装。解压后你会看到一堆文件夹我挑几个重要的说bin存放启动和关闭脚本startup.batWindows或startup.shMac/Linux用来启动conf配置文件目录最重要的是server.xml里面配了端口号默认8080webapps你的Web项目就放在这里Tomcat启动时会自动加载libTomcat运行需要的jar包logs日志文件出问题第一个来看这里双击bin/startup.bat然后浏览器访问http://localhost:8080如果看到Tomcat的欢迎页面说明服务器跑起来了。这一步看着简单但我见过太多人卡在这里——要么端口被占用改成8081就行要么JDK没配好导致闪退命令行里运行startup.bat能看到报错信息。注意Tomcat启动窗口不要关掉关了服务就停了。如果双击闪退打开命令行cd到bin目录再执行这样能看到错误信息。2.3 MySQL安装与建库建表MySQL的安装比前两个稍微麻烦一点Windows用户下载安装包后选择Developer Default或者Custom都行关键是记住你设置的root密码。安装过程中会让你配置端口默认3306不用改。装完之后用命令行或者Navicat之类的工具连接数据库创建一个专门给这个项目用的库CREATE DATABASE user_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;为什么用utf8mb4而不是utf8因为MySQL里的utf8其实是个假UTF-8只支持3字节存不了emoji和一些特殊字符。utf8mb4才是真正的4字节UTF-8现在建库一律用它别省这点空间。然后建用户表USE user_db; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, email VARCHAR(100), create_time DATETIME DEFAULT CURRENT_TIMESTAMP );几个字段的设计理由id自增主键不用多说username加了UNIQUE约束从数据库层面保证用户名不重复这比在Java代码里查一遍再插入要可靠得多password长度给100因为后面要存加密后的密码明文密码存数据库是大忌create_time自动记录注册时间方便后续排查问题。2.4 IDEA项目创建与JDBC驱动导入打开IDEA新建项目选择Java Enterprise勾选Web Application确保SDK选的是你装的JDK。项目建好后目录结构大概是这样的项目名/ ├── src/ Java源代码 ├── web/ Web资源 │ ├── WEB-INF/ │ │ └── web.xml │ └── index.jsp └── ...接下来导入JDBC驱动。去MySQL官网下载mysql-connector-java的jar包然后在IDEA里File→Project Structure→Libraries→点→Java→选中你下载的jar包。这样项目就能用JDBC连接MySQL了。为什么要手动导jar包而不是用Maven因为Maven本身又是一套需要学习的东西对于第一次做Web项目的人来说多一个变量就多一个出错的可能。手动导包虽然原始但你能清楚地知道依赖在哪里、怎么加的。3. 注册功能的核心链路从表单到数据库3.1 前端注册页面怎么写才不容易出错在web目录下新建一个register.jsp这就是用户看到的注册页面。代码不复杂但有几个细节值得说% page contentTypetext/html;charsetUTF-8 languagejava % !DOCTYPE html html head title用户注册/title /head body h2注册新账号/h2 form actionregister methodpost p用户名input typetext nameusername required/p p密码input typepassword namepassword required/p p邮箱input typeemail nameemail/p pbutton typesubmit注册/button/p /form /body /htmlform的action写的是register这是Servlet的访问路径后面会配。method必须是post因为注册涉及密码传输用get的话密码会直接暴露在URL里这是安全大忌。required属性是HTML5自带的校验能在前端就拦住空提交减少后端压力。contentTypetext/html;charsetUTF-8这行不能省否则中文会乱码。我见过太多人页面显示?????排查半天以为是数据库问题其实就是JSP没声明编码。3.2 Servlet接收参数的完整流程在src下新建一个包比如com.example.servlet然后创建RegisterServlet类继承HttpServlet重写doPost方法package com.example.servlet; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; WebServlet(/register) public class RegisterServlet extends HttpServlet { private static final String URL jdbc:mysql://localhost:3306/user_db?useSSLfalseserverTimezoneUTCcharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 你的数据库密码; Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); String username req.getParameter(username); String password req.getParameter(password); String email req.getParameter(email); if (username null || username.trim().isEmpty() || password null || password.trim().isEmpty()) { resp.getWriter().write(用户名和密码不能为空); return; } Connection conn null; PreparedStatement ps null; try { Class.forName(com.mysql.cj.jdbc.Driver); conn DriverManager.getConnection(URL, USER, PASSWORD); String sql INSERT INTO t_user(username, password, email) VALUES(?, ?, ?); ps conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password); ps.setString(3, email); int rows ps.executeUpdate(); if (rows 0) { resp.getWriter().write(注册成功); } else { resp.getWriter().write(注册失败请重试); } } catch (ClassNotFoundException e) { e.printStackTrace(); resp.getWriter().write(驱动加载失败 e.getMessage()); } catch (SQLException e) { e.printStackTrace(); if (e.getMessage().contains(Duplicate entry)) { resp.getWriter().write(用户名已存在请换一个); } else { resp.getWriter().write(数据库错误 e.getMessage()); } } finally { try { if (ps ! null) ps.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } } }这段代码有几个关键点需要展开说。WebServlet(/register)这个注解把Servlet映射到了/register路径和JSP表单的action对应。如果你用的Servlet版本低于3.0注解不生效就得去web.xml里手动配但Tomcat 8.5以上都支持注解放心用。req.setCharacterEncoding(UTF-8)必须在获取任何参数之前调用否则中文参数会乱码。这个顺序不能反反了就没效果。Class.forName(com.mysql.cj.jdbc.Driver)是加载驱动类。MySQL 8.0的驱动类名是com.mysql.cj.jdbc.Driver5.x版本是com.mysql.jdbc.Driver写错了会报ClassNotFoundException。连接URL里的参数也值得解释useSSLfalse关掉SSL连接本地开发不需要serverTimezoneUTC设置时区不设的话MySQL 8.0可能报时区错误characterEncodingutf8保证中文正常存取。用PreparedStatement而不是Statement是因为前者能防SQL注入。假设有人用户名输入 OR 11用Statement拼接SQL的话整个查询逻辑就被篡改了。PreparedStatement用占位符?参数值会被当作纯数据处理从根本上杜绝注入。3.3 数据库连接为什么要放在finally里关闭上面代码里finally块负责关闭PreparedStatement和Connection。为什么这么写因为数据库连接是稀缺资源不关的话连接池很快耗尽程序跑一会儿就卡死。放在finally里能保证无论是否发生异常连接都会被释放。每个close()都单独包了try-catch是因为关闭一个连接时抛异常不应该影响关闭另一个。这种写法看着啰嗦但在实际项目里能避免很多莫名其妙的问题。4. 跑通之后这些坑你大概率也会遇到4.1 中文乱码的三层排查法中文乱码是Java Web新手遇到频率最高的问题没有之一。它可能出现在三个环节排查要一层层来。第一层是JSP页面编码。% page contentTypetext/html;charsetUTF-8 %这行必须写而且charset要大写U。如果页面显示的中文是乱码先检查这里。第二层是Servlet请求编码。req.setCharacterEncoding(UTF-8)要在getParameter之前调用。如果提交的中文存到数据库变成乱码检查这里。第三层是数据库编码。建库时用utf8mb4连接URL里加characterEncodingutf8。如果前两层都没问题但数据库里还是乱码检查这里。我建议的做法是三层全部显式声明UTF-8不要依赖任何默认值。默认值在不同环境下可能不一样显式声明才是可控的。4.2 驱动加载失败的几种常见原因ClassNotFoundException: com.mysql.cj.jdbc.Driver这个错误原因通常有三个一是jar包没导入项目。去Project Structure里确认Libraries里有没有mysql-connector的jar。二是驱动类名写错了。MySQL 8.0用com.mysql.cj.jdbc.Driver5.x用com.mysql.jdbc.Driver版本和类名要对应。三是jar包版本和MySQL版本不匹配。用MySQL 8.0的数据库配5.x的驱动或者反过来都可能出问题。最稳的做法是驱动版本和数据库版本保持一致。4.3 端口占用与Tomcat启动失败Tomcat启动时如果报Address already in use: JVM_Bind说明8080端口被别的程序占了。解决办法有两个一是找到占用端口的程序关掉它二是改Tomcat的端口。改端口去conf/server.xml找到Connector port8080这行把8080改成8081或别的。还有一种情况是Tomcat闪退窗口一闪就没了。这种一般是JDK环境变量没配好Tomcat找不到Java。解决办法是打开命令行cd到Tomcat的bin目录手动执行startup.bat这样错误信息会留在窗口里能看到具体报什么错。4.4 用户名重复插入的异常处理上面代码里我特意捕获了Duplicate entry这个异常信息。因为数据库表里username加了UNIQUE约束重复插入时MySQL会抛异常。与其在插入前先查一遍多一次数据库交互不如直接插入然后捕获异常这样效率更高。但要注意e.getMessage()的内容格式在不同MySQL版本里可能不一样所以判断的时候用contains(Duplicate entry)而不是精确匹配。这种写法不够优雅但对于入门项目来说足够用了。5. 从能跑到像个样子的几个改进方向5.1 密码不能明文存试试MD5上面代码里密码是直接存明文的这在真实项目里绝对不行。哪怕是个练习项目也建议养成加密存储的习惯。最简单的加密方式是MD5import java.security.MessageDigest; public static String md5(String input) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(input.getBytes(UTF-8)); StringBuilder sb new StringBuilder(); for (byte b : digest) { sb.append(String.format(%02x, b)); } return sb.toString(); } catch (Exception e) { throw new RuntimeException(e); } }存的时候存md5(password)的结果登录验证时也把用户输入的密码做MD5再比对。MD5虽然现在被认为不够安全有碰撞风险但对于练习项目来说比明文存储强太多了。真要上生产环境应该用BCrypt或Argon2这类专门为密码设计的哈希算法。5.2 注册成功后的页面跳转怎么做现在注册成功后只是输出一行文字体验很粗糙。更好的做法是跳转到登录页或者直接跳到首页。Servlet里做跳转有两种方式// 重定向浏览器地址栏会变发两次请求 resp.sendRedirect(login.jsp); // 转发地址栏不变服务器内部跳转能传递request属性 req.getRequestDispatcher(login.jsp).forward(req, resp);注册场景一般用重定向因为注册是一次性操作刷新页面不应该重复提交。重定向的发两次请求特性正好避免了表单重复提交的问题。5.3 用连接池替代每次新建连接现在每次请求都DriverManager.getConnection()新建一个连接用完关掉。这种模式在并发量稍大的时候性能很差因为建立TCP连接和认证的开销不小。改进方式是引入连接池比如Druid或HikariCP启动时预先建好一批连接用的时候从池里拿用完还回去。连接池的配置不复杂以Druid为例加个配置文件初始化一个DataSource然后getConnection()从池里拿就行。但对于入门项目来说先把基础链路跑通连接池可以后面再优化。5.4 前端加一点校验减少无效请求现在前端只有required属性做非空校验可以再加一些input typetext nameusername required minlength3 maxlength20 pattern[a-zA-Z0-9_] title用户名只能包含字母、数字和下划线minlength和maxlength限制长度pattern用正则限制字符范围。这些校验在浏览器端就能拦住不合规的输入减少后端处理无效请求的压力。当然后端该做的校验一个都不能少前端校验只是提升体验不能替代后端安全校验。6. 这套笨办法教会我的事做完这个注册功能我最大的感受是Web开发没那么神秘。浏览器发一个HTTP请求服务器收到后处理查数据库返回结果就这么回事。框架帮你封装了很多细节但如果你不理解底层在干什么出了问题就只能靠猜。用Servlet JSP JDBC这套原始技术栈做一遍你会清楚地知道请求参数是怎么从表单到Servlet的SQL是怎么执行的连接是怎么管理的异常是怎么处理的。这些知识在你后面用Spring Boot的时候依然有用——Spring Boot只是帮你把这些步骤自动化了但底层逻辑没变。还有一个体会是关于最小可运行系统的。很多人学东西喜欢追求大而全一上来就想做个完整的系统结果卡在环境配置上就放弃了。正确的做法是先做一个最小的、能跑通的功能哪怕它很简陋。跑通之后再逐步加东西加密码加密、加连接池、加前端校验、加错误页面。每一步都是在前一步能跑的基础上改进这样你永远有一个能用的版本不会陷入什么都做了一半的困境。最后说一个实际的小技巧把数据库连接信息写在一个单独的配置文件里而不是硬编码在Java代码中。这样换环境的时候只改配置文件就行不用重新编译代码。可以用Properties类读取db.properties文件几行代码的事但能省很多麻烦。# db.properties jdbc.urljdbc:mysql://localhost:3306/user_db?useSSLfalseserverTimezoneUTCcharacterEncodingutf8 jdbc.usernameroot jdbc.password你的密码 jdbc.drivercom.mysql.cj.jdbc.Driver读取的代码Properties props new Properties(); props.load(RegisterServlet.class.getClassLoader().getResourceAsStream(db.properties)); String url props.getProperty(jdbc.url);这个习惯从练习项目就开始养成后面做真实项目的时候会感谢自己。