ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

JavaWeb日记系统实战:JSP+Servlet+MySQL从零搭建与避坑指南

2026/10/4 15:44:30 拓冰建站 浏览量
JavaWeb日记系统实战:JSP+Servlet+MySQL从零搭建与避坑指南 先交代一下背景我自己带过的JavaWeb课程设计里十个人有七八个会选“日记系统”“博客系统”这种经典CRUD技术栈基本逃不出 jsp servlet mysql bootstrap 这几样。你别看这个组合老它恰恰是理解JavaWeb请求响应模型最好的路径。Spring Boot再好它把底层流程全包了新手根本不知道一次请求是怎么从浏览器走到数据库再走回来的。所以今天这篇我就拿这个“基于javaweb和mysql的jspservlet日记系统”当样板从技术选型、数据库设计、核心代码到部署排错完整拆给你看。前半部分适合还在上课的学生党照着做课设后半部分全是实际跑项目时踩过的坑已经工作的朋友翻翻也能有点收获。1. 项目定位与技术栈选型1.1 为什么选JSPServlet这套“老古董”每次在知乎或者技术群看到有人问“现在还有必要学Servlet吗”我的答案都是有必要但别用它写生产项目。Servlet是JavaWeb的地基你理解了Servlet之后看Spring MVC的DispatcherServlet就毫无障碍因为你已经知道请求是怎么被接收、分发、处理的。这个日记系统选JSPServlet本质上就是为了把显示层、控制层、数据层手工拆开强迫自己理解MVC到底是怎么回事。这个项目适合三类人第一类是被课程设计逼着写JavaWeb的学生党第二类是自学Java找工作、需要补项目经历的转行者第三类是带新人的老员工想拿一个简单项目练内功。日记系统的好处是业务足够简单没有复杂的权限、支付、消息队列CRUD就是全部但麻雀虽小五脏俱全——登录注册、Session管理、数据库操作、分页查询、前端展示一个都没少。1.2 技术栈每层负责什么这套技术栈的分工非常清楚JSPJava Server Pages负责视图层就是在HTML里嵌Java代码用来动态渲染日记列表、用户信息等页面。Servlet负责控制层接收浏览器发来的HTTP请求调用业务方法再把数据转发给JSP页面。Bootstrap负责前端样式不需要自己写复杂的CSS用栅格系统、表单样式、模态框就能拼出一个好看的界面。MySQL负责数据持久化存用户账号和日记内容。JDBCJava里操作数据库的标准方式如果你的项目用了连接池比如Druid或者C3P0本质上还是JDBC。我见过不少同学直接用JSP里的Java代码把数据库查询、业务逻辑全干完了Servlet只是摆设。那样做虽然也能运行但页面里全是%和%维护起来想哭。这个项目我用的是标准的Servlet JavaBean JSP结构Servlet只做请求接收和跳转业务逻辑放在Service层数据库操作放在Dao层。一个忠告在这个项目阶段不要追求花哨的框架和中间件。把Servlet、HttpSession、request/response这几个对象彻底玩明白比会用SpringBootRestController有价值得多。2. 环境搭建与数据库设计2.1 IDEA里跑JavaWeb项目的完整配置这里我默认你用的是IDEA社区版或者Ultimate版。如果用Ultimate版本新建项目时可以直接选择Java EnterpriseIDEA会帮你生成Servlet骨架。社区版也没关系手动建一个普通的Java项目然后添加Web支持效果一样。首先建议用Maven管理依赖不要在WEB-INF/lib下手动加jar包麻烦且容易丢。打开pom.xml核心依赖如下dependencies !-- Servlet API -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSP API -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- JSTL 标签库JSP页面里做循环和判断用 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.32/version /dependency /dependencies这里有几个细节容易踩坑。javax.servlet-api的scope务必设置为provided因为Tomcat本身已经带了一份Servlet相关jar包如果Maven再打进去一份运行时会出现类冲突——具体表现是启动报ClassNotFoundException或者NoSuchFieldError很折磨人。MySQL驱动版本必须和你的MySQL服务端版本对应MySQL 8.x就用8.x的驱动MySQL 5.7就用5.1.48或8.x都行但要留意MySQL 8.0驱动默认带时区参数需求。接着配置Tomcat运行。IDEA菜单栏点击Run - Edit Configurations点号选择Tomcat Server - Local。然后Server标签页里Application server选择你本机装好的Tomcat目录。Deployment标签页点击选择Artifact然后选你的项目Artifact。Application context建议填/diary这样访问路径就是http://localhost:8080/diary比默认的/要好。注意一个最容易卡住新手的点如果你的Artifact用的是war exploded格式Deployment地址会自动带着_war_exploded后缀访问路径特别丑。解决办法是在Deployment页下方Application context里自己改成/diaryIDEA会自动帮你映射到exploded目录不用管物理路径在哪。2.2 数据库表结构与JDBC连接细节日记系统的表设计非常经典两张表足矣用户表users和日记表diaries。下面是我实际用的建表SQL你可以直接抄CREATE DATABASE IF NOT EXISTS diary_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE diary_system; -- 用户表 CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT 密码, nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 日记表 CREATE TABLE diaries ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL COMMENT 所属用户ID, title VARCHAR(200) NOT NULL COMMENT 标题, content TEXT COMMENT 日记内容, mood VARCHAR(20) DEFAULT NULL COMMENT 心情, weather VARCHAR(20) DEFAULT NULL COMMENT 天气, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_user_created (user_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT日记表;为什么指定utf8mb4而不是utf8因为utf8在MySQL里最多存3字节一个emoji表情是4字节插进去就报错Incorrect string value。日记内容本身就可能带emoji或者生僻字所以直接上utf8mb4一劳永逸。JDBC连接这块建议封装一个DBUtil工具类不要在Servlet里重复写Class.forName和DriverManager.getConnection。我习惯用配置文件方式管理连接参数db.properties内容如下jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/diary_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456这里有个必须解释清楚的坑MySQL 8.x驱动连接时如果不指定serverTimezone直接报The server time zone value йʱ你以为是编码问题其实是时区问题。加上serverTimezoneAsia/Shanghai即可。useSSLfalse是关闭SSL握手如果你用的是MySQL 5.7以上版本不关就会出现文章热搜词里那个ssl connection error报错信息大概率是Communications link failure。至于allowPublicKeyRetrievaltrue在MySQL 8.0之后用caching_sha2_password认证时某些连接工具不加这个参数会报Public Key Retrieval is not allowed。DBUtil工具类的核心代码长这样public class DBUtil { private static String driver; private static String url; private static String username; private static String password; static { try { InputStream is DBUtil.class.getClassLoader() .getResourceAsStream(db.properties); Properties props new Properties(); props.load(is); driver props.getProperty(jdbc.driver); url props.getProperty(jdbc.url); username props.getProperty(jdbc.username); password props.getProperty(jdbc.password); Class.forName(driver); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 关闭资源注意先关rs再关stmt最后关conn } }3. 核心编码从登录到日记CRUD3.1 登录注册与Session登录态管理登录功能是这套系统的第一个核心关卡。你要理解一个关键流程浏览器提交表单 - Servlet处理请求 - JDBC查询用户表 - 密码比对 - 登录成功写入Session - 跳转首页登录失败转发回登录页并携带错误信息。用户密码不建议明文存储。课程设计如果要求不高至少做个MD5加盐正规一点的用BCrypt。这里我用的是JDK自带的MessageDigest做MD5加盐处理简单够用// MD5加盐工具 public class Md5Util { 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(MD5加密失败, e); } } }用户注册时密码存储格式建议是md5(明文密码 固定盐值)盐值可以是一个写死的字符串比如diary_salt_2024。登录校验时把用户输入的密码做同样的拼接和加密再和数据库里的密文比较。虽然固定盐不如随机盐安全但在课设级别已经合格了。更进一步注册时判断用户名是否存在这部分用PreparedStatement的COUNT(*)查询即可。登录态的检查推荐用一个LoginFilter过滤器统一拦截而不是在每个Servlet里重复判断session.getAttribute(user) null。过滤器是Servlet规范里的标准组件配置方式如下WebFilter(/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String uri req.getRequestURI(); // 放行登录、注册页面和静态资源 if (uri.contains(/login) || uri.contains(/register) || uri.contains(/css/) || uri.contains(/js/) || uri.contains(/images/)) { chain.doFilter(request, response); return; } if (req.getSession().getAttribute(user) null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }这里有个细节要注意使用WebFilter(/*)这个注解后它对所有路径生效但JSP页面本身也会被拦截。如果你没有对login.jsp这类页面做放行处理就会出现“一直跳转到登录页但永远进不去”的死循环。所以上面的uri.contains(/login)就是用来放行登录页面相关路径的。3.2 日记列表、发布、编辑、删除的实现与分页日记是核心业务我把它写在DiaryServlet里通过一个action参数区分请求类型例如actionlist、actionadd、actionedit、actiondelete。这样比建五六个Servlet要省事得多运行时也只需要配置一个URL映射。日记列表的查询是整个系统里查询最频繁的操作也是最值得优化的一环。第一次做这个功能的时候我发现一个非常经典的性能问题用户登录后展示日记列表页面加载特别慢。原因是每查询一条日记都要单独执行一次用户名的子查询但查了资料之后发现更常见的坑是——个人日记页面展示时用user_id过滤数据时索引没走对。所以建表时我特意加了KEY idx_user_created (user_id, created_at)这个联合索引就是为查询指定用户的日记按时间倒序排序服务的。分页功能建议从第二页之后再考虑先实现一个最简单的手写分页不用引入PageHelper插件。逻辑是接收page和pageSize两个参数默认第一页每页5条然后计算offset (page - 1) * pageSizeSQL加一个LIMIT ?, ?SELECT * FROM diaries WHERE user_id ? ORDER BY created_at DESC LIMIT ?, ?;处理器里还要查总条数算出总页数int totalCount diaryDao.countByUserId(userId); int totalPage (totalCount pageSize - 1) / pageSize;这一行(totalCount pageSize - 1) / pageSize是分页的整数向上取整公式很多新手习惯用totalCount / pageSize (totalCount % pageSize 0 ? 0 : 1)也没问题但上面的写法更简洁。把每页数字传进JSP页面JSP里的分页条用Bootstrap的pagination组件渲染点击页码时跳转到diary?actionlistpage2pageSize5这样的地址。发布日记相对简单表单提交过来的title、content、mood、weather通过request.getParameter()获取然后调用Dao的insert方法。这里注意一点content如果是大文本JSP表单里对应的字段是textarea提交过来的文本可能包含换行符。如果直接把换行符存在数据库再原样渲染回HTML页面会发现换行全变成空格了。解决办法是渲染时把\n替换成br/或者用CSS的white-space: pre-wrap;属性后者更省事。编辑和删除逻辑大同小异。编辑时先根据id查出这条日记确认它的user_id是否等于当前登录用户的ID——这个权限校验很容易忽略。如果直接UPDATE diaries SET ... WHERE id ?一旦用户手动修改请求参数就能删掉别人的日记。所以实际操作时务必在UPDATE和DELETE语句中都带上user_id条件UPDATE diaries SET title ?, content ?, mood ?, weather ? WHERE id ? AND user_id ?;3.3 Bootstrap页面集成与前端交互细节Bootstrap在这个项目里主要干三件事排版、表单样式、模态框。但它的使用方式有个经常被忽略的细节——本地引入还是CDN引入。课程设计一般要求演示时能跑但我还是建议在webapp目录下手动放一份Bootstrap的CSS和JS文件用本地路径引入link relstylesheet href${pageContext.request.contextPath}/css/bootstrap.min.css script src${pageContext.request.contextPath}/js/bootstrap.min.js/script为什么不用CDN因为答辩现场的网络环境不一定有外网插上教室的网线可能连cdn.jsdelivr.net都访问不了页面直接变成裸奔状态。把静态资源放在本地项目里无论在哪台机器都能正常显示。JSP页面里要用好JSTL标签不要用% for (...) { %这种脚本片段。比如日记列表页面用c:forEach循环渲染比手写Java代码可读性强太多c:forEach items${diaryList} vardiary div classcard mb-3 div classcard-body h5 classcard-title${diary.title}/h5 p classcard-text${diary.content}/p div classtext-muted 心情${diary.mood} nbsp;|nbsp; 天气${diary.weather} /div div classmt-2 a hrefdiary?actioneditid${diary.id} classbtn btn-sm btn-warning编辑/a a hrefdiary?actiondeleteid${diary.id} classbtn btn-sm btn-danger onclickreturn confirm(确定删除这篇日记吗);删除/a /div /div /div /c:forEach注意上面的onclickreturn confirm(...)这个原生方法比Bootstrap的模态框更轻量用来做删除二次确认足够了。如果你非要用Bootstrap模态框那就要写额外的JavaScript来控制show和hide而且还要把每一行的日记ID传到模态框的按钮上——能折腾但没必要。另一个容易被忽略的点是当前登录用户信息怎么传给JSP页面。登录成功后在Session里存一个user对象页面里直接用${sessionScope.user.nickname}取昵称。如果昵称为空就显示用户名。这种三元判断在EL表达式里不支持可以用${empty user.nickname ? user.username : user.nickname}但EL的empty操作符很强大我自己倒是经常把它和fn:length配合用来判断列表为空比如c:if test${empty diaryList}就是“没有日记时显示暂无内容”提示。4. 部署运行与常见问题排查4.1 War包部署与Artifact打包细节IDEA里配置好了Tomcat点击运行可以直接在浏览器看到效果。但如果你想把项目打包成war放到独立Tomcat里部署需要先确认pom.xml里设置了打包方式为warpackagingwar/packaging用Maven的package命令打包之后target目录下会生成一个.war文件。把这个文件复制到Tomcat的webapps目录下启动Tomcat它就会自动解压部署。访问路径默认和war包文件名一致比如diary.war对应http://localhost:8080/diary/。但这里有一个大坑本地用IDEA内置Tomcat跑得好好的部署到独立Tomcat就白屏或者报404。这不一定是代码问题很可能是项目路径搞错了。检查顺序启动目录的控制台日志看有没有Deploying web application archive字样。确认访问的上下文路径是否和war包名一致。确认数据库连接是否指向了独立机器上的MySQL本地localhost在服务器上就是服务器本机。4.2 高频报错与排查指南我把做这个项目时遇到最多的报错整理成了一张表每一行都是真实踩过的坑报错信息根因解决办法Loading class com.mysql.jdbc.Driver. This is deprecated.用了旧驱动类名MySQL 8.x驱动类名改为com.mysql.cj.jdbc.DriverThe server time zone value Öйú±ê׼ʱ¼ä连接URL没指定时区URL加serverTimezoneAsia/ShanghaiPublic Key Retrieval is not allowedMySQL 8.0认证机制引起URL加allowPublicKeyRetrievaltruejava.lang.ClassNotFoundException: javax.servlet.jsp.jstl.core.Config缺少JSTL依赖添加jstl依赖注意是javax.servlet.jsp.jstlHTTP Status 500 - java.lang.NullPointerExceptionServlet里取参数或Session为空加断点或打印日志检查空指针发生位置Address already in use: JVM_Bind8080端口被占用换Tomcat端口或者杀掉占用进程Tomcat控制台输出中文乱码Tomcat日志编码和系统编码不一致修改conf/logging.properties里java.util.logging.ConsoleHandler.encoding UTF-8其中第4个ClassNotFoundException特别容易出现在你同时用了JSTL和JSP API的场景。JSTL 1.2版本相对省心如果用的是1.1版本还要额外引入standard.jar。所以我的pom.xml里直接写上version1.2/version就这一个jar搞定。还有一个小细节我在调试时经常需要往页面上打印SQL语句用一个System.out.println就够了但记得观察Tomcat的控制台不要用System.out往浏览器输出。有些同学因为用了不同版本的日志框架System.out输出到了别的地方排查半天找不到日志非常尴尬。4.3 页面刷新与加载状态的小优化热搜词里有个“jsp页面让加载完后刷新一次”实际项目里也真有人这么干。比如登录成功后跳转到首页但首页数据是Servlet在doGet里查询并setAttribute转发的浏览器按F5刷新后数据没了或者又重复提交了一次表单。解决办法有两个第一个是用重定向而不是转发登录成功后response.sendRedirect(diary?actionlist)让浏览器重新发一次GET请求。这样刷新页面时操作的是GET请求不会重复提交表单。把更新数据操作的Servlet改成“POST处理 重定向GET”的PRG模式Post-Redirect-Get刷新的时候不会重复提交。第二个是针对某些特殊场景确实需要JSP加载完成后自动刷一次可以在body标签里写script window.onload function() { location.reload(); } /script但我强烈不建议在正常业务里这么干会让用户体验极差。这个技巧只适合某些特定需求比如展示实时数据但服务端没有推送机制实在没办法才用一次。5. 对一些热搜词的深度解读和避坑写这篇博文之前我翻了翻后台热词发现很多人在搜“mysql安装教程8.0”“mysql 5.7.44安装过程详细”“docker安装mysql失败”这类问题。宿舍里装环境确实劝退了不少人这里统一回答几个高频情形。MySQL 8.0的zip压缩包安装方式解压后先新建一个my.ini内容注意配置[mysqld] basedirD:/mysql-8.0.32-winx64 datadirD:/mysql-8.0.32-winx64/data port3306 character-set-serverutf8mb4 default-authentication-pluginmysql_native_password其中default-authentication-pluginmysql_native_password值得单独说一句。MySQL 8.0默认的认证插件是caching_sha2_password如果你用了老版本的客户端工具比如很老的Navicat连接时会报认证失败。设置成mysql_native_password可以兼容旧客户端但如果你确定自己的驱动和客户端都够新这一行其实可以不要。以管理员身份打开命令行执行mysqld --initialize-insecure net start mysql注意--initialize-insecure会生成一个没有密码的root用户方便本地开发。有些教程用--initialize那是生成随机密码后续还得去data目录找mysql-error.log才能看到密码。你自己开发就别折腾了用--initialize-insecure省事得多。然后mysql -u root -p回车直接登录修改root密码ALTER USER rootlocalhost IDENTIFIED BY 123456; FLUSH PRIVILEGES;至于Docker安装MySQL失败最常见的原因是端口映射冲突或者容器名称重复。推荐直接用命令跑docker run --name diary-mysql \ -e MYSQL_ROOT_PASSWORD123456 \ -p 3306:3306 \ -d mysql:8.0 --default-authentication-pluginmysql_native_password跑起来之后再用docker exec -it diary-mysql mysql -u root -p进入容器操作数据库。注意容器里的MySQL默认数据不持久化加上-v mysql_data:/var/lib/mysql这个参数可以挂载宿主机目录但课设阶段无所谓了。6. 最后想分享的一点开发习惯把整个项目从零搭建起来之后回顾一下真正费时间的其实不是写业务代码而是排查环境问题MySQL装不上、时区报错、端口冲突、jar包缺依赖。这些问题的共同特征是报错信息模糊网上答案五花八门让你根本不知道信谁的。我个人的经验是遇到环境报错先分三层排查——第一层看网络能不能通第二层看端口和权限第三层看版本是否匹配。绝大多数JavaWeb环境问题最后都是版本不匹配比如JDK 17配Servlet 3.1Tomcat 11配javax.servlet包名都会出问题。你把这个排查思路记住以后做任何JavaWeb项目都能少走很多弯路。日记系统这套代码框架后续还能继续扩展成个人博客、笔记系统甚至加上标签分类和全文搜索。但无论扩展多少骨架是不变的Servlet处理请求、JDBC操作数据库、JSP渲染页面。把这套基本功打扎实了后面学Spring Boot你会觉得它别扭但你已经知道它背后在做些什么了。