
简介本资源是一套基于ServletJspMySQL开发的艺术作品展览管理系统完整工程面向Java Web初学者与课程设计实践者解决艺术类机构在线化管理展览、作品及用户的核心需求。压缩包共50个文件含18个Java后端逻辑类如Servlet控制器与DAO层、15个JSP前端页面涵盖注册、登录、作品展示、展览详情等交互界面、12个XML配置文件包括web.xml、Maven pom.xml及数据库连接配置以及SQL建表脚本与README说明文档整体仅52KB轻量易部署。已有53人学习下载适合快速理解MVC分层结构、JDBC数据库操作及前后端协同流程。源码注释清晰目录结构规范含src/main/java、JSP页面独立模块、SQL初始化脚本并提供可直接导入运行的MySQL建表语句与基础数据便于读者复现系统、调试功能或拓展新增模块。1. 这不是又一个“Hello World”JSP项目它真能跑通艺术馆前台后台用户评论闭环且数据库字段设计直击展览管理痛点你试过在 Tomcat 里部署一个 JSP 项目结果首页 404、登录跳转 500、图片路径死活不显示查日志发现是ClassNotFoundException: com.mysql.cj.jdbc.Driver而你明明把mysql-connector-java-8.0.33.jar放进了WEB-INF/lib别急——这套艺术作品展览管理系统就是专为「踩过坑的人」写的实战型源码包。它不是教学 Demo而是完整覆盖「策展人录入作品→管理员审核上架→观众注册登录→浏览带缩略图的展览列表→点击进入详情页看高清图作者生平风格流派→发表评论并实时刷新」全链路的可运行系统。核心价值不在“用了 ServletJSP”而在它用3 张表artwork / exhibition / user 7 个关键外键约束 2 级图片路径分离缩略图 vs 原图把真实展览业务逻辑压进 MySQL更关键的是它的init.sql不是空库建表脚本而是预置了 12 条测试数据含梵高《星月夜》、莫奈《睡莲》等真实作品元信息连Jsp_register.jsp的表单验证都做了非空邮箱正则密码二次确认——这意味着你解压后改完数据库连接参数5 分钟内就能看到带真实作品图的首页。适合两类人Java Web 新手想拿一个「有血有肉」的项目练手而不是对着index.jsp?namexxx干敲代码也适合需要快速交付小型美术馆线上展示模块的外包工程师直接改artwork表字段就能对接本地藏品库。2. 从零启动Tomcat 部署 数据库初始化 JDBC 连接三步闭环2.1 Tomcat 环境准备为什么必须用 Tomcat 9 而不是 SmartTomcat 插件项目根目录下自带.smarttomcat文件夹这是 IntelliJ IDEA 的 SmartTomcat 插件配置目录但切勿直接点击“Run”启动。原因有三第一SmartTomcat 默认使用内置 JRE而本项目pom.xml中java.version1.8/java.version明确要求 JDK 8若 IDE 使用 JDK 17 启动JspServlet会因javax.servlet.*包版本冲突报NoClassDefFoundError第二.smarttomcat未配置context.xml导致 JNDI 数据源无法注入第三插件自动打包的 WAR 包会忽略src/main/webapp/WEB-INF/lib下的 MySQL 驱动造成运行时ClassNotFoundException。提示务必手动下载 Apache Tomcat 9.0.83官方最新稳定版解压后将项目src/main/webapp目录整体复制到TOMCAT_HOME/webapps/artwork注意路径名必须与web.xml中display-name一致再将mysql-connector-java-8.0.33.jar手动放入TOMCAT_HOME/lib/不是WEB-INF/libTomcat 9 要求 JDBC 驱动放全局 lib 目录才能被JndiDataSourceFactory识别。2.2 数据库初始化init.sql的 4 个隐藏动作你必须执行init.sql文件看似只是建表语句实则包含四个关键操作缺一不可-- 1. 创建数据库字符集强制 utf8mb4避免中文乱码和 emoji 存储失败 CREATE DATABASE IF NOT EXISTS artwork_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 2. 创建用户并授权注意不是 root而是专用账号符合最小权限原则 CREATE USER artwork_userlocalhost IDENTIFIED BY Art2024; GRANT SELECT,INSERT,UPDATE,DELETE ON artwork_db.* TO artwork_userlocalhost; -- 3. 建表时显式指定存储引擎为 InnoDB保障事务一致性尤其评论表需支持行锁 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(100) NOT NULL, password VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 4. 插入测试数据时对图片路径做两级处理缩略图存相对路径原图存绝对路径 INSERT INTO artwork (title, author, year, style, thumbnail_path, full_image_path) VALUES (星月夜, 文森特·梵高, 1889, 后印象派, /images/thumbnails/starry-night.jpg, /var/www/artwork/images/full/starry-night.jpg);执行顺序必须是先在 MySQL 命令行中source init.sql再检查SELECT COUNT(*) FROM artwork;返回 12 —— 若为 0说明USE artwork_db;未执行或字符集不匹配。2.3 JDBC 连接配置context.xml和web.xml的双保险写法本项目采用 JNDI 数据源而非硬编码 URL这是企业级部署的标配。需同时修改两处第一步TOMCAT_HOME/conf/context.xml添加资源声明!-- 在 Context 标签内插入 -- Resource namejdbc/artworkDB authContainer typejavax.sql.DataSource factoryorg.apache.tomcat.jdbc.pool.DataSourceFactory driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/artwork_db?useUnicodetrueamp;characterEncodingutf8mb4amp;serverTimezoneAsia/Shanghaiamp;allowPublicKeyRetrievaltrueamp;useSSLfalse usernameartwork_user passwordArt2024 maxActive20 minIdle5 maxWait10000/注意amp;是 XML 实体转义不可写成serverTimezoneAsia/Shanghai必须显式指定否则 MySQL 8.0 会因时区不匹配抛SQLExceptionuseSSLfalse是开发环境必需生产环境应启用 SSL。第二步src/main/webapp/WEB-INF/web.xml声明资源引用!-- 在 web-app 标签下插入 -- resource-ref descriptionArtwork Database Connection/description res-ref-namejdbc/artworkDB/res-ref-name res-typejavax.sql.DataSource/res-type res-authContainer/res-auth /resource-ref验证是否生效启动 Tomcat 后访问http://localhost:8080/artwork/若首页正常加载 12 件作品缩略图且浏览器开发者工具 Network 标签中list.jsp返回状态码 200则 JNDI 连接成功。3. 源码结构深挖src/main/java下 5 个 Servlet 如何串联起 CRUD 全流程3.1ArtworkServlet.javaGET 请求如何动态拼接 SQL 并防注入该 Servlet 处理/artwork/list和/artwork/detail两个路径核心逻辑在doGet()中protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action request.getParameter(action); if (list.equals(action)) { // 关键使用 PreparedStatement 预编译而非 Statement 拼接字符串 String sql SELECT * FROM artwork WHERE style ? AND year ? ORDER BY created_at DESC; try (Connection conn dataSource.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setString(1, request.getParameter(style)); // 用户传参安全 pstmt.setInt(2, Integer.parseInt(request.getParameter(minYear))); // 类型强转防字符串注入 ResultSet rs pstmt.executeQuery(); // 将结果集转为 ListArtwork 并 setAttribute 到 request } } else if (detail.equals(action)) { // 单条查询同样用 ? 占位符 String sql SELECT * FROM artwork WHERE id ?; // ... 同上 } }参数说明request.getParameter(style)接收前端 GET 参数但pstmt.setString(1, ...)确保值被当作字符串字面量处理即使传入 OR 11也不会触发 SQL 注入Integer.parseInt()强制类型转换若用户传非数字则抛NumberFormatException由try-catch捕获并跳转错误页比WHERE year 2024 OR 11安全得多。3.2CommentServlet.javaPOST 提交评论时的事务控制与并发安全评论功能涉及user表查用户 IDartwork表校验作品存在comment表插入新记录三张表联动必须用事务保证原子性protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Connection conn null; try { conn dataSource.getConnection(); conn.setAutoCommit(false); // 关键关闭自动提交 // 步骤1查用户是否存在根据 session 中的 username String username (String) request.getSession().getAttribute(username); String userIdSql SELECT id FROM user WHERE username ?; PreparedStatement userPs conn.prepareStatement(userIdSql); userPs.setString(1, username); ResultSet userRs userPs.executeQuery(); if (!userRs.next()) { throw new RuntimeException(User not found); } int userId userRs.getInt(id); // 步骤2查作品是否存在 int artworkId Integer.parseInt(request.getParameter(artworkId)); String artworkSql SELECT id FROM artwork WHERE id ?; PreparedStatement artPs conn.prepareStatement(artworkSql); artPs.setInt(1, artworkId); if (!artPs.executeQuery().next()) { throw new RuntimeException(Artwork not found); } // 步骤3插入评论关键使用 NOW() 函数确保时间戳精确到毫秒 String commentSql INSERT INTO comment (user_id, artwork_id, content, created_at) VALUES (?, ?, ?, NOW()); PreparedStatement commentPs conn.prepareStatement(commentSql); commentPs.setInt(1, userId); commentPs.setInt(2, artworkId); commentPs.setString(3, request.getParameter(content).trim()); commentPs.executeUpdate(); conn.commit(); // 全部成功才提交 response.sendRedirect(request.getContextPath() /artwork/detail?actiondetailid artworkId); } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } request.setAttribute(error, 评论提交失败请重试); request.getRequestDispatcher(/error.jsp).forward(request, response); } }事务边界conn.setAutoCommit(false)开启事务conn.commit()显式提交conn.rollback()回滚。若步骤 1 或 2 失败直接throw中断流程catch块自动回滚若步骤 3 插入失败如内容超长同样触发回滚。NOW()函数比 Javanew Date()更可靠避免服务器时钟误差。3.3LoginServlet.java密码如何安全存储与验证项目未使用 BCrypt 或 SHA-256而是采用MySQL 内置的 SHA2() 函数 盐值拼接虽非顶级方案但比明文安全// 注册时RegisterServlet.java String salt UUID.randomUUID().toString().substring(0, 8); // 8位随机盐 String passwordHash new StringBuilder() .append(salt) .append(request.getParameter(password)) .toString(); // 执行 SQLINSERT INTO user (username, email, password, salt) VALUES (?, ?, SHA2(?, 256), ?) String insertSql INSERT INTO user (username, email, password, salt) VALUES (?, ?, SHA2(?, 256), ?); PreparedStatement ps conn.prepareStatement(insertSql); ps.setString(1, username); ps.setString(2, email); ps.setString(3, passwordHash); // 传入拼接后的字符串 ps.setString(4, salt); ps.executeUpdate(); // 登录时LoginServlet.java String loginSql SELECT id, password, salt FROM user WHERE username ?; PreparedStatement ps conn.prepareStatement(loginSql); ps.setString(1, username); ResultSet rs ps.executeQuery(); if (rs.next()) { String storedHash rs.getString(password); String salt rs.getString(salt); String inputHash new StringBuilder().append(salt).append(password).toString(); // 在 MySQL 中执行SELECT SHA2(?, 256) ?避免 Java 层计算 SHA2 String verifySql SELECT SHA2(?, 256) ?; PreparedStatement verifyPs conn.prepareStatement(verifySql); verifyPs.setString(1, inputHash); verifyPs.setString(2, storedHash); ResultSet verifyRs verifyPs.executeQuery(); if (verifyRs.next() verifyRs.getBoolean(1)) { // 登录成功 } }安全逻辑盐值salt与密码拼接后送入 MySQL 的SHA2()函数计算哈希数据库只存哈希值验证时同样拼接盐值再哈希比对全程不暴露原始密码。虽不如 BCrypt 自带加盐和迭代但已远超MD5(password)的弱防护。4. 避坑指南部署与调试中 5 个高频翻车点及血泪解决方案4.1 现象首页能打开但点击“注册”按钮后跳转到http://localhost:8080/Jsp_register.jsp404原因web.xml中servlet-mapping的url-pattern与实际 JSP 路径不匹配。项目中Jsp_register.jsp位于src/main/webapp/根目录但web.xml里配置了url-pattern/register/url-pattern而Jsp_register.jsp未被映射为 ServletTomcat 默认不处理.jsp后缀的直接请求除非开启default servlet。解决两种方案任选其一① 将Jsp_register.jsp重命名为register.jsp并在web.xml中添加servlet-mapping映射/register到该文件② 更推荐删除Jsp_register.jsp改用RegisterServlet控制器在doGet()中request.getRequestDispatcher(/register.jsp).forward(request, response)转发——这样符合 MVC 规范且register.jsp可放在WEB-INF/下防止直接访问。4.2 现象登录成功后session.setAttribute(username, username)设置了但header.jsp中% session.getAttribute(username) %始终为空原因header.jsp被include到多个页面但某些页面如login.jsp未调用request.getSession(true)显式创建 Session导致session对象为null。JSP 中% session.getAttribute(...) %在 session 为 null 时抛NullPointerException而 Tomcat 默认静默吞掉异常页面空白。解决在所有 JSP 页面顶部强制声明 session% page sessiontrue %。更彻底的做法是在web.xml中全局配置session-config session-timeout30/session-timeout cookie-http-onlytrue/cookie-http-only /session-config4.3 现象上传作品图片时input typefile选择文件后request.getParameter(title)能取到但request.getPart(image)报java.lang.IllegalStateException: Not supported原因Servlet 3.0 的文件上传需MultipartConfig注解但本项目UploadServlet.java缺失该注解且web.xml中未声明multipart-config。Tomcat 9 默认禁用 multipart 解析。解决在UploadServlet类上添加注解WebServlet(/upload) MultipartConfig( fileSizeThreshold 1024 * 1024, // 1MB maxFileSize 1024 * 1024 * 10, // 10MB maxRequestSize 1024 * 1024 * 15 // 15MB ) public class UploadServlet extends HttpServlet { ... }同时确保web.xml的web-app根标签声明version3.1。4.4 现象MySQL 8.0 连接时报错Public Key Retrieval is not allowed原因MySQL 8.0 默认禁用公钥检索allowPublicKeyRetrievalfalse而mysql-connector-java-8.0.33驱动在握手阶段需此参数获取 RSA 公钥以加密密码传输。解决在context.xml的 JDBC URL 中显式添加allowPublicKeyRetrievaltrue见 2.3 节示例且必须配合useSSLfalse开发环境否则会因 SSL 握手失败而卡住。4.5 现象exhibition.jsp中遍历ListExhibition时c:forEach items${exhibitions} vare报javax.servlet.jsp.JspTagException: Dont know how to iterate over supplied items原因JSTL 标签库未正确引入。pom.xml中虽有jstl依赖但web.xml未声明taglib且WEB-INF/lib下缺少standard.jarJSTL 1.1 必需。解决① 在web.xml中添加jsp-config taglib taglib-urihttp://java.sun.com/jsp/jstl/core/taglib-uri taglib-location/WEB-INF/tld/c.tld/taglib-location /taglib /jsp-config② 下载jstl-1.2.jar和standard-1.1.2.jar放入WEB-INF/lib③ JSP 页面顶部添加% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %。5. 图片路径与前端交互thumbnail_path与full_image_path的两级分离策略5.1 为什么必须分离缩略图与原图路径艺术作品管理系统的核心体验在于「快速加载列表 高清查看详情」。若所有图片都存原图路径首页 12 张图每张 5MB总加载量超 60MB用户等待超 30 秒若全用缩略图则详情页无法展示细节。本项目采用物理路径分离 逻辑路径映射thumbnail_path存相对路径如/images/thumbnails/starry-night.jpg供list.jsp循环生成img src${e.thumbnail_path}Nginx/Apache 可直接静态服务full_image_path存绝对路径如/var/www/artwork/images/full/starry-night.jpg仅在detail.jsp中由 Servlet 读取文件流输出避免暴露服务器真实路径。5.2DetailServlet.java如何安全输出原图而不暴露路径protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int artworkId Integer.parseInt(request.getParameter(id)); // 1. 从数据库查出 full_image_path String imagePath getFullImagePath(artworkId); // 查询 SQLSELECT full_image_path FROM artwork WHERE id ? // 2. 验证路径合法性防目录穿越攻击 File imageFile new File(imagePath); String canonicalPath imageFile.getCanonicalPath(); String baseDir /var/www/artwork/images/full/; // 白名单根目录 if (!canonicalPath.startsWith(baseDir)) { response.sendError(HttpServletResponse.SC_FORBIDDEN, Invalid image path); return; } // 3. 设置响应头强制浏览器下载或内联显示 response.setContentType(image/jpeg); // 根据文件扩展名动态设置 response.setHeader(Content-Disposition, inline; filename imageFile.getName()); // 4. 流式输出避免内存溢出 try (FileInputStream fis new FileInputStream(imageFile); OutputStream os response.getOutputStream()) { byte[] buffer new byte[8192]; int length; while ((length fis.read(buffer)) ! -1) { os.write(buffer, 0, length); } } }关键防护getCanonicalPath()将../../../etc/passwd这类路径解析为真实路径再与baseDir字符串比对杜绝目录穿越response.setContentType()根据文件扩展名设置 MIME 类型避免浏览器误解析为 HTML 执行 XSSinline头让图片在浏览器中直接显示而非下载。5.3list.jsp中的图片懒加载与错误兜底为提升首屏速度list.jsp对缩略图启用原生懒加载并添加onerror事件兜底c:forEach items${artworks} vara div classartwork-card img src${a.thumbnail_path} loadinglazy alt${a.title} onerrorthis.src/images/default-thumbnail.png; this.alt图片加载失败 h3${a.title}/h3 p${a.author} · ${a.year}/p /div /c:forEachloadinglazy是 Chrome 76 原生支持的懒加载属性无需 JS 库onerror在图片 404 时自动替换为默认图避免页面出现丑陋的破损图标。default-thumbnail.png需提前放入src/main/webapp/images/目录。6. 进阶技巧用log4j2.xml替换System.out.println()实现全链路日志追踪6.1 为什么System.out.println()在生产环境是灾难当你在LoginServlet.java里写System.out.println(User username logged in);这些日志会混在 Tomcat 的catalina.out中无法按级别过滤INFO/WARN/ERROR、无法按类名归类、无法滚动切割、更无法对接 ELK。本项目虽未自带 log4j2但补上后能让调试效率提升 3 倍。6.2 三步集成 log4j2从依赖到配置到使用第一步pom.xml添加依赖dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-core/artifactId version2.20.0/version /dependency dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-api/artifactId version2.20.0/version /dependency dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-web/artifactId version2.20.0/version /dependency第二步src/main/resources/log4j2.xml配置文件?xml version1.0 encodingUTF-8? Configuration statusWARN Appenders RollingFile nameFileAppender fileNamelogs/artwork.log filePatternlogs/artwork-%d{yyyy-MM-dd}-%i.log.gz PatternLayout Pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/Pattern /PatternLayout Policies TimeBasedTriggeringPolicy / SizeBasedTriggeringPolicy size10 MB/ /Policies DefaultRolloverStrategy max30/ /RollingFile Console nameConsoleAppender targetSYSTEM_OUT PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ /Console /Appenders Loggers Logger namecom.artwork.servlet leveldebug additivityfalse AppenderRef refFileAppender/ AppenderRef refConsoleAppender/ /Logger Root levelinfo AppenderRef refConsoleAppender/ /Root /Loggers /Configuration关键参数filePatternlogs/artwork-%d{yyyy-MM-dd}-%i.log.gz实现按天滚动压缩SizeBasedTriggeringPolicy size10 MB防止单个日志过大Logger namecom.artwork.servlet精准捕获所有 Servlet 日志避免污染全局日志。第三步在 Servlet 中替换println// LoginServlet.java 顶部 private static final Logger logger LogManager.getLogger(LoginServlet.class); // doLogin() 方法中 logger.info(User {} attempting login from IP {}, username, request.getRemoteAddr()); if (isValidUser(username, password)) { logger.debug(User {} authenticated successfully, username); request.getSession().setAttribute(username, username); } else { logger.warn(Failed login attempt for user {}, IP {}, username, request.getRemoteAddr()); request.setAttribute(error, 用户名或密码错误); }日志价值request.getRemoteAddr()记录用户 IP便于溯源debug级别记录成功登录warn级别记录失败info级别记录行为起点。当线上出现“用户说登不上”你只需查artwork-2024-06-15.log搜索Failed login attempt立刻定位是密码输错还是数据库连接超时。从那以后我每次部署新 JSP 项目都会在pom.xml里先加 log4j2 依赖再写log4j2.xml配置最后把所有System.out.println()替换掉——这已成我的上线前 checklist 第一条。因为真正的调试不是靠猜而是靠日志里每一行精准的时间戳、线程名、类名和变量值。希望帮到你。本文还有配套的精品资源点击获取