简介:这份源码包围绕JSP动态网页技术与MySQL关系型数据库,构建了一款个人记事备忘系统,覆盖笔记新建、编辑、分类、保存与检索等常用功能,适合Java Web初学者、在校生做课程设计或毕业设计时参考。压缩包共374个文件,整体大小约27.51MB,主要包含24个Java类、9个JSP页面、26个HTML页面、28个CSS样式、86个JavaScript脚本,附带大量PNG和GIF图片素材、JAR依赖库、XML与properties配置等资源,目录结构清晰,便于阅读与部署。目前已有109人学习,整体体量较为轻量。通过源码可以梳理Servlet与JDBC操作数据库的增删改查流程、MVC分层思想、JSP页面渲染逻辑,以及部署到Servlet容器的具体方法,同时还能了解Bootstrap等前端组件在Java Web项目中的应用。从文件组成来看,页面、样式、脚本与后端类区分明显,适合逐步拆解并修改验证,以此巩固Java Web与MySQL开发基础。
1. 从 JSP 记事本源码看 Java Web 项目的真实组织方式
拿到notes-jsp这个目录名,基本就能猜到源码长什么样:没有 Maven 的标准目录结构,JSP 直接摆在 WebRoot 下,Java 类编译后丢进WEB-INF/classes,静态资源像bootstrap.css、ueditor.css、video-js.css全部平铺在根目录。这套基于 JSP 和 MySQL 的个人记事备忘系统,功能不复杂,但把 Java Web 里最经典的链路走全了:JSP 负责页面渲染,Servlet 接收请求,DAO 通过 JDBC 读写 MySQL,Bootstrap 管布局,UEditor 管富文本编辑。
这类源码对两类人有用。一类是还在学 Java Web 的学生,能从中看到 MVC 在真实项目里到底怎么落地,而不是教科书里的抽象图;另一类是工作中要接手老项目的开发,改这种 JSP 项目时反复遇到的乱码、class 缓存、SQL 拼接、上传路径问题,这套代码里全都有对应位置。下面的内容按数据库、请求链路、部署排错、加固的顺序拆开讲。
2. 看源码先看表:数据库设计与 JDBC 连接的常见写法
拿到 JSP 加 MySQL 的压缩包,我第一步永远是在解压目录里找.sql文件。这个项目源码没有把建表脚本单独放在 sql 目录下,但根据代码里NoteDAO、NoteServlet的类名,可以反推核心表的设计通常是下面这个样子。
CREATE TABLE `t_note` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '笔记ID', `user_id` int(11) NOT NULL COMMENT '所属用户ID,关联t_user.id', `title` varchar(100) NOT NULL COMMENT '笔记标题', `content` text COMMENT '笔记正文,对应UEditor生成的HTML片段', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='个人记事表';这段 DDL 里有几个选型是可以在课程设计答辩里直接讲的点。第一是ENGINE=InnoDB,它支持事务和行级锁,记事更新场景下即使中途断电也不会出现表损坏,MyISAM 在 Web 应用里没有优势。第二是CHARSET=utf8mb4,MySQL 5.7 之后存 emoji 和生僻字必须用它,只写utf8会让 UEditor 插入的特殊符号落库后变成问号。第三是idx_user_id这个普通索引,查询某个用户的笔记走的是WHERE user_id = ?,没有索引时数据量到几千条,SELECT的全表扫描就能明显感觉到卡顿。
2.1 连接参数:properties 文件与 DbUtil 的读取方式
JSP 老项目里最常见的数据库连接方式,是用一个DbUtil.java或DBHelper.java工具类读取 Classpath 下的db.properties,然后通过DriverManager.getConnection()建连。如果源码里是硬编码的账号密码,我一般会顺手改成 properties 形式,部署时只改配置文件,不用重新编译 class。
# 放在src目录下,编译后自动进入WEB-INF/classes jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/note_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=123456public class DbUtil { private static String url; private static String username; private static String password; static { try (InputStream in = DbUtil.class.getClassLoader() .getResourceAsStream("db.properties")) { Properties props = new Properties(); props.load(in); Class.forName(props.getProperty("jdbc.driver")); url = props.getProperty("jdbc.url"); username = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); } catch (Exception e) { throw new ExceptionInInitializerError("数据库配置加载失败: " + e.getMessage()); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } }这段代码的关键在 URL 参数上。characterEncoding=utf8保证写入 MySQL 的中文不乱码;MySQL 8.0 下必须加serverTimezone,否则会抛出The server time zone value的时区异常;useSSL=false是测试环境关闭证书校验,避免每次连接都做 SSL 握手。驱动类名也要注意,MySQL 8.0 对应的新驱动是com.mysql.cj.jdbc.Driver,老写法com.mysql.jdbc.Driver在新版本中已被移除。
2.2 连接池不该是最后才考虑的事
课程设计阶段,DriverManager.getConnection()每次调用都创建一次物理连接,请求量稍微上来就会耗尽 MySQL 的max_connections。我一般保留原逻辑不动,但会在注释里标明替换为 HikariCP 的位置。常见做法是往 lib 目录放一个HikariCP.jar,然后把DbUtil的getConnection()换成从HikariDataSource拿连接,核心改动只有几行,却能明显改善并发下的稳定性。
提示:如果源码里大量使用
Statement拼接 SQL,先别急着换连接池。连接池解决的是连接复用问题,真正的性能和安全大头在 SQL 预编译上,这个在第 5 章会展开。
3. 核心链路:NoteServlet 与 note.jsp 之间的动作协商
个人记事系统的请求 URL 通常设计成NoteServlet?action=list这种形式。一个 Servlet 通过action参数区分增删改查操作,在 JSP 项目里是非常普遍的写法,它的好处是路由集中、类数量少,坏处是方法膨胀后if-else会变得很长。
3.1 入口与分发:request.getParameter("action")
@WebServlet(urlPatterns = {"/NoteServlet"}) public class NoteServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getParameter("action"); if ("list".equals(action)) { listNotes(request, response); } else if ("add".equals(action)) { addNote(request, response); } else if ("delete".equals(action)) { deleteNote(request, response); } else { response.sendRedirect("login.jsp"); } } }这里有个容易被忽略的细节:action为空时直接重定向回登录页,避免用户通过构造 URL 跳过后台逻辑。更严谨的做法是在doPost开头用HttpSession里的用户对象做登录校验,后面 5.3 节会单独说。doGet里调用doPost是老项目统一 GET 和 POST 入口的简化手段,但注意重定向是二次请求,如果用request.setAttribute往 JSP 传数据,要改成forward才不会丢数据。
3.2 DAO 层:结果集映射与字段裁剪
NoteDAO.java里的方法通常和业务动作一一对应:listByUser、findById、insert、update、deleteById。它们的共同特征是通过 PreparedStatement 填充参数后执行 SQL,下面是一个典型的列表查询实现。
public List<Note> listByUser(int userId) throws SQLException { String sql = "SELECT id, title, create_time, update_time " + "FROM t_note WHERE user_id = ? ORDER BY update_time DESC"; List<Note> notes = new ArrayList<>(); try (Connection conn = DbUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, userId); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Note note = new Note(); note.setId(rs.getInt("id")); note.setTitle(rs.getString("title")); note.setCreateTime(rs.getTimestamp("create_time")); notes.add(note); } } } return notes; }这段查询没有把content字段查出来是有意的:content是 UEditor 生成的完整 HTML 片段,可能几十 KB,列表页只需要标题和时间,把正文查出来会浪费网络传输和内存。如果要在列表页显示内容摘要,SQL 层可以用LEFT(content, 120)截断,不要在 Java 里对完整字符串做substring,这样能减少不少 IO。
提示:
try-with-resources是 Java 7 的语法,如果项目编译级别还停留在 Java 6,需要改成在finally块里手动关闭 Connection、Statement、ResultSet,关闭顺序与打开顺序相反。
3.3 页面层:Bootstrap 布局与 UEditor 的初始化
列表页和编辑页的静态资源引用顺序直接影响页面表现:jquery.js必须在bootstrap.js之前加载,ueditor.config.js必须在ueditor.all.js之前加载。源码里的静态资源平铺在 WebRoot 下,JSP 用相对路径引用,这种目录组织方式虽然不如 Maven 规范,但部署时效率很高。
UEditor 的初始化一般写在页面底部:
var ue = UE.getEditor('editorContainer', { initialFrameWidth: '100%', initialFrameHeight: 320, serverUrl: '/notes-jsp/ueditor/jsp/controller.jsp' });三个配置项的含义:editorContainer是页面上一个<script id="editorContainer" type="text/plain" name="content"></script>占位符的 ID,UEditor 会把它替换成完整的编辑界面;initialFrameWidth设为 100% 是为了适配 Bootstrap 响应式栅格;serverUrl指向服务端上传接口,图片上传和文件上传都走这个地址。表单提交时,编辑器区域的 HTML 内容会作为content参数随请求发送到 Servlet。
这里必须配合统一的字符编码过滤器,否则中文标题很容易乱码。常见做法是在web.xml里声明一个 CharacterEncodingFilter,或者在 Servlet 开头手动执行request.setCharacterEncoding("UTF-8"),两者选其一,不要重复设置。
4. 从源码到可访问:Tomcat 部署流程与三个重复出现的坑
这类源码的高频检索词是"部署失败""启动报错"。我把排查结论放在前面:报错十次里有八次是版本不匹配,剩下两次是目录缓存或字符集。
4.1 版本兼容矩阵
| JDK | Tomcat | MySQL | 驱动类名 | 注意事项 |
|---|---|---|---|---|
| JDK 8 | Tomcat 8.5 | MySQL 5.7 | com.mysql.jdbc.Driver | 老课程设计标配,无需时区参数 |
| JDK 8 | Tomcat 8.5/9 | MySQL 8.0 | com.mysql.cj.jdbc.Driver | 需 serverTimezone 与 allowPublicKeyRetrieval |
| JDK 11+ | Tomcat 9 | MySQL 8.0 | com.mysql.cj.jdbc.Driver | 避免 Tomcat 10,jakarta 包名不兼容 |
如果本机装的是 JDK 11 以上,配 Tomcat 10 会遇到javax.servlet相关代码全部编译失败,因为 Tomcat 10 把包名改成了jakarta.servlet。最省事的方案是换用 Tomcat 9,而不是去改源码里的 import。
4.2 部署两条路:直接放 webapps 还是 IDE 配置
最直接的方式不需要 IDE:在 Tomcat 的webapps目录下新建notes-jsp文件夹,把源码包里的 WebRoot 内容整体复制进去,启动startup.bat即可。这种方式对排查文件缓存方便,webapps里就是真实在跑的内容。不管哪种方式,数据库初始化步骤都一样:
# 一、登录 MySQL 创建数据库,字符集与项目配置保持一致 CREATE DATABASE IF NOT EXISTS note_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_bin; # 二、执行源码里的建表脚本 mysql -uroot -p note_db < init.sql # 三、验证表是否创建成功 SHOW TABLES; SELECT COUNT(*) FROM t_note;utf8mb4_bin排序规则区分大小写,适合用户名字段的唯一约束;如果希望搜索时不区分大小写,改成utf8mb4_general_ci更合适,这个细节在写查询排序时会影响结果顺序。
4.3 坑一:jsp 改了不生效,改的可能是缓存副本
Tomcat 对 JSP 的处理策略是首次访问时把index.jsp翻译成 Java 源文件并编译成.class,保存在work/Catalina/localhost/notes-jsp/org/apache/jsp/目录下。很多人改 JSP 没反应,实际改的是源码目录副本,不是 Tomcat 正在服务的文件。排查方法是访问那个目录看index_jsp.class的时间戳:如果 class 文件时间比 JSP 旧,说明 JSP 根本没有被重新编译,手动删除 work 目录下对应项目文件夹再重启即可。
4.4 坑二:MySQL 8.0 连不上,Public Key Retrieval 报错
MySQL 8.0 默认使用caching_sha2_password插件,客户端在非 SSL 连接下需要获取服务端公钥。如果驱动版本跟不上,启动时就会报Public Key Retrieval is not allowed。解决方式是双管齐下:一是把db.properties的 URL 追加allowPublicKeyRetrieval=true,二是把驱动升级到mysql-connector-java-8.0.x以上版本。在 MySQL 8.0.46 这类较新的版本下,旧驱动根本无法建立连接,与其排查半天不如直接换驱动。
4.5 坑三:端口被占用,改一处就够
8080 端口被其他进程占用时,Tomcat 启动日志会显示Address already in use。改conf/server.xml里<Connector port="8080" ...>的端口号就行,改完后访问路径要跟着变成http://localhost:新端口/notes-jsp/index.jsp。注意改的是 server.xml 不是 web.xml,很多新手在这两个文件之间反复横跳。
5. 加固与进阶:把课程设计代码改成工程可用的样子
这个系统走到能跑,只算完成了 60%。剩下 40% 在于把源码里的"学生写法"换成"工程写法",下面是我改 JSP 老项目时优先级最高的三处。
5.1 用 PreparedStatement 替换字符串拼接
老课程设计最常见的 SQL 注入口子长这个样子:
// 不安全:直接拼接,用户可以传入 "1 or 1=1" String sql = "SELECT * FROM t_note WHERE id = " + id;改成参数化写法:
// 安全:先预编译,再绑定参数 String sql = "SELECT * FROM t_note WHERE id = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setInt(1, id);PreparedStatement不只是防注入,它还让 MySQL 对同一条 SQL 只做一次解析,后续重复执行时直接复用执行计划,性能上也优于Statement拼接。改动的原则是:凡是 SQL 里出现变量,一律用?占位,不要用单引号拼接字符串,也不要为了图省事把参数直接塞进 SQL。
5.2 UEditor 上传与 XSS 过滤的平衡
UEditor 的config.json里,imageUrlPrefix用于配置图片访问前缀,数据库里存的是相对路径,不配置会导致列表页图片打不开。上传目录要放在 WebRoot 可访问的范围内,比如/upload,并且要限制上传文件类型。默认配置允许的类型比较宽,生产环境至少把imageAllowFiles限制为常见图片格式。
富文本本身是 HTML,直接转义会让页面显示源码。合理做法是对编辑器内容做白名单过滤,只允许p、img、span、a、video等标签,删除所有onerror、onclick事件属性和javascript:协议链接。Java 里可以用 OWASP Java HTML Sanitizer,改动不大但能挡住大部分存储型 XSS。
5.3 会话校验与输入长度限制
最后是访问控制。每个需要登录的 Servlet 方法开头,都应该检查会话:
HttpSession session = request.getSession(false); User loginUser = (User) session.getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect("login.jsp"); return; }这里getSession(false)的使用很关键:传false表示当前没有会话时直接返回 null,而不是新造一个会话。配合在登录页设置的session.setAttribute("loginUser", user)和session.setMaxInactiveInterval(1800)超时配置,才算完整闭环。输入长度校验则放在 Servlet 层统一处理,标题超过数据库字段长度会导致Data too long异常,正文大小限制在 UEditor 和 Servlet 两侧同时控制,避免恶意构造超大请求耗尽 Tomcat 堆内存。
本文还有配套的精品资源,点击获取