ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从JSP记事本源码看JavaWeb项目组织与部署加固

从JSP记事本源码看JavaWeb项目组织与部署加固 简介这份源码包围绕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 读写 MySQLBootstrap 管布局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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT个人记事表;这段 DDL 里有几个选型是可以在课程设计答辩里直接讲的点。第一是ENGINEInnoDB它支持事务和行级锁记事更新场景下即使中途断电也不会出现表损坏MyISAM 在 Web 应用里没有优势。第二是CHARSETutf8mb4MySQL 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.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/note_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password123456public 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 参数上。characterEncodingutf8保证写入 MySQL 的中文不乱码MySQL 8.0 下必须加serverTimezone否则会抛出The server time zone value的时区异常useSSLfalse是测试环境关闭证书校验避免每次连接都做 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?actionlist这种形式。一个 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 ListNote 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; ListNote 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 ideditorContainer typetext/plain namecontent/script占位符的 IDUEditor 会把它替换成完整的编辑界面initialFrameWidth设为 100% 是为了适配 Bootstrap 响应式栅格serverUrl指向服务端上传接口图片上传和文件上传都走这个地址。表单提交时编辑器区域的 HTML 内容会作为content参数随请求发送到 Servlet。这里必须配合统一的字符编码过滤器否则中文标题很容易乱码。常见做法是在web.xml里声明一个 CharacterEncodingFilter或者在 Servlet 开头手动执行request.setCharacterEncoding(UTF-8)两者选其一不要重复设置。4. 从源码到可访问Tomcat 部署流程与三个重复出现的坑这类源码的高频检索词是部署失败启动报错。我把排查结论放在前面报错十次里有八次是版本不匹配剩下两次是目录缓存或字符集。4.1 版本兼容矩阵JDKTomcatMySQL驱动类名注意事项JDK 8Tomcat 8.5MySQL 5.7com.mysql.jdbc.Driver老课程设计标配无需时区参数JDK 8Tomcat 8.5/9MySQL 8.0com.mysql.cj.jdbc.Driver需 serverTimezone 与 allowPublicKeyRetrievalJDK 11Tomcat 9MySQL 8.0com.mysql.cj.jdbc.Driver避免 Tomcat 10jakarta 包名不兼容如果本机装的是 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 追加allowPublicKeyRetrievaltrue二是把驱动升级到mysql-connector-java-8.0.x以上版本。在 MySQL 8.0.46 这类较新的版本下旧驱动根本无法建立连接与其排查半天不如直接换驱动。4.5 坑三端口被占用改一处就够8080 端口被其他进程占用时Tomcat 启动日志会显示Address already in use。改conf/server.xml里Connector port8080 ...的端口号就行改完后访问路径要跟着变成http://localhost:新端口/notes-jsp/index.jsp。注意改的是 server.xml 不是 web.xml很多新手在这两个文件之间反复横跳。5. 加固与进阶把课程设计代码改成工程可用的样子这个系统走到能跑只算完成了 60%。剩下 40% 在于把源码里的学生写法换成工程写法下面是我改 JSP 老项目时优先级最高的三处。5.1 用 PreparedStatement 替换字符串拼接老课程设计最常见的 SQL 注入口子长这个样子// 不安全直接拼接用户可以传入 1 or 11 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 堆内存。本文还有配套的精品资源点击获取
返回列表