ARTICLE DETAIL

资讯详情

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

基于JSP+Servlet+MySQL的相册管理系统开发教程

基于JSP+Servlet+MySQL的相册管理系统开发教程 简介基于JSP与MySQL的相册管理系统是一套面向Java学习者和课程设计人员的完整项目源码可帮助理解在线相册基本业务与实现思路。系统涵盖照片上传、相册分类、评论互动、后台管理等模块通过Servlet接收请求使用JDBC操作数据库结合JavaBean封装业务数据整体分层清晰便于阅读和二次开发。压缩包共115个文件大小约3.53兆字节包含JSP页面、Java源文件、编译后的字节码、图片素材、依赖库、SQL脚本及配置文件覆盖展示、业务、数据与部署等层面。目前已有433人学习下载适合作为毕业设计或课程设计的参考资料。通过阅读源码可以掌握JSP与数据库交互的完整流程并重点参考登录验证、SQL注入防护、图片压缩等安全优化设计这对于自行搭建相册类管理系统有切实帮助。1. 基于JSP的相册管理系统为什么这个老技术栈还能撑起你的课设和私有相册如果你正在准备JavaWeb课程设计或者公司里需要一个轻量的图片管理后台而时间只有一周那么“基于JSP的相册管理系统”可能是最不容易翻车的选择。它不像Spring Boot那样要折腾自动配置和一堆依赖管理也不像PHP那样要另起一套环境一个Tomcat、一个MySQL、几页JSP配上Servlet就能完成图片上传、分类浏览、删除和缩略图展示这些核心动作。这套方案特别适合两类人一类是第一次接触JavaWeb、需要尽早看到运行结果的学生另一类是只想在局域网内部建一个图片管理页面、不想引入重框架的后端工程师。下面我从数据库设计开始把一个能跑通的相册管理方案完整拆开。2. 相册管理系统的技术选型与数据库设计JSPServletJDBC而不是Spring Boot的取舍2.1 为什么还在用JSP框架也要服务于项目规模很多人一看到“管理系统”四个字就想上Spring Boot MyBatis Plus但相册管理系统本质是几张表的增删改查没有复杂的权限、事务和分布式需求。用JSP Servlet JDBC写项目结构透明任何一个报错都能直接在页面上看到堆栈排查成本很低。Spring Boot虽然开发快但打包、部署、环境变量、数据库连接池都要额外配置如果交付目标是“一个能跑的war包丢进Tomcat”原生态JSP更省心。从学习角度看手写Servlet能让你真正理解HTTP请求从哪里来、响应回哪里去之后再跳框架也会更轻松。2.2 photo表设计四到六个字段就够但分类和上传时间不能少数据库是相册系统的地基。图片本身不存BLOB字段而是把图片文件放到磁盘目录数据库只记录元数据这是Web应用最常见的做法。一张photo表设计如下CREATE TABLE photo ( id INT PRIMARY KEY AUTO_INCREMENT, original_name VARCHAR(255) NOT NULL COMMENT 原始文件名, saved_name VARCHAR(255) NOT NULL COMMENT 存储文件名UUID生成, category VARCHAR(50) NOT NULL DEFAULT 未分类 COMMENT 相册分类, description VARCHAR(500) DEFAULT NULL COMMENT 照片描述, size BIGINT NOT NULL COMMENT 文件大小字节, upload_time DATETIME NOT NULL COMMENT 上传时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个细节表字符集一定要用utf8mb4不要用utf8。utf8mb4兼容全量Unicode字符比如表情符号而utf8在MySQL里最多只存3字节遇到表情包类的文件名或描述直接报错或者变成乱码。saved_name用UUID加扩展名目的是避免前端上传两个同名文件时互相覆盖。original_name保留给人看saved_name给机器用。upload_time建议让数据库生成也可以从Java传new Timestamp(System.currentTimeMillis())。2.3 项目目录Maven结构还是传统WebContent结构如果你用Eclipse动态Web项目经典的目录是src/main/java、src/main/webapp、WEB-INF/lib。如果习惯传统结构用WebContent代替webapp本质一样。我更推荐Maven结构photo-album/ pom.xml src/main/java/ com/photo/ entity/Photo.java dao/PhotoDao.java util/DBUtil.java servlet/UploadServlet.java servlet/ListPhotoServlet.java servlet/DeletePhotoServlet.java src/main/java/resources/ db.properties src/main/webapp/ upload.jsp list.jsp index.jsp WEB-INF/web.xml这样打完war包后结构清晰也方便以后往Spring Boot迁移。如果用传统目录只要保证web.xml在WEB-INF下本质没区别。无论哪种结构上传目录我都建议放在webapp外面后面避坑章节会重点讲。2.4 数据库连接工具类DBUtil参数集中放还是散落各处JDBC最烦的就是每次写连接。抽一个DBUtil出来核心代码只有十几行package com.photo.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/photo_album?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }这里几个参数值得说明。characterEncodingutf8是必须的否则从Java写入的字符串会按数据库默认字符集编码导致中文问号。serverTimezoneAsia/Shanghai是MySQL 8.0后的要求不写会提示时区错误。驱动类名也分版本MySQL 5.x用com.mysql.jdbc.DriverMySQL 8.x用com.mysql.cj.jdbc.Driver写错会在启动时报ClassNotFoundException。对于课设级别的访问量直接DriverManager就够了不需要DBCP连接池如果以后并发上来把getConnection换成HikariDataSource的单例即可。2.5 实体类与DAO抽象越朴素的写法越容易调试实体类就是两个String加一个long加一个时间对象。Photo.java可以这样写package com.photo.entity; import java.util.Date; public class Photo { private int id; private String originalName; private String savedName; private String category; private String description; private long size; private Date uploadTime; // getter / setter 省略IDEA可以自动生成 }DAO层我习惯先定义接口再写实现类但在这个规模下没必要。直接用PhotoDao类方法固定为add(Photo)、findAll()、findById(int)、delete(int)、findByCategory(String)每个方法就是一个Statement或PreparedStatement的查询。第3章会贴出核心方法的实现。这里想强调的是不要为了“设计模式”强行引入Service接口、工厂类相册系统里这些抽象只会让新手不知道从哪查错。3. 从上传到展示用JSPServlet实现相册核心模块的完整代码3.1 上传ServletMultipartConfig和UUID文件名上传是相册系统的入口。Servlet 3.0之后原生的Part接口就能处理文件不用再引入commons-fileupload。写法如下package com.photo.servlet; import java.io.File; import java.io.IOException; import java.util.UUID; import javax.servlet.ServletException; import javax.servlet.annotation.MultipartConfig; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Part; import com.photo.dao.PhotoDao; import com.photo.entity.Photo; WebServlet(/upload) MultipartConfig(maxFileSize 10 * 1024 * 1024, maxRequestSize 50 * 1024 * 1024) public class UploadServlet extends HttpServlet { private static final long serialVersionUID 1L; private static final String UPLOAD_DIR uploads; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String category request.getParameter(category); String description request.getParameter(description); Part part request.getPart(file); String submittedFileName part.getSubmittedFileName(); String extension ; if (submittedFileName.lastIndexOf(.) 0) { extension submittedFileName.substring(submittedFileName.lastIndexOf(.)); } String savedName UUID.randomUUID().toString().replace(-, ) extension; String realPath getServletContext().getRealPath(UPLOAD_DIR); File uploadDir new File(realPath); if (!uploadDir.exists()) { uploadDir.mkdirs(); } part.write(realPath File.separator savedName); Photo photo new Photo(); photo.setOriginalName(submittedFileName); photo.setSavedName(savedName); photo.setCategory(category); photo.setDescription(description); photo.setSize(part.getSize()); PhotoDao dao new PhotoDao(); dao.add(photo); response.sendRedirect(request.getContextPath() /list); } }逻辑说明先拿到category和description这两个普通表单字段必须在getPart之前读取但要保证request.setCharacterEncoding(UTF-8)在最前面。part.getSubmittedFileName()返回原始文件名但我们不用它存盘只截取扩展名。UUID.randomUUID()生成36位字符串去掉横杠后作为新文件名这一步直接消灭了重名覆盖。part.write(realPath File.separator savedName)会把临时文件写到指定目录。最后通过PhotoDao插入数据库再重定向到列表页。MultipartConfig里的maxFileSize限制单文件10MBmaxRequestSize限制整个请求50MB参数按实际需求调。3.2 列表页面用JSTL还是Scriptlet列表页负责把所有照片以卡片方式展示。如果你不想往WEB-INF/lib塞JSTL jar包可以直接用Scriptlet但我个人更推荐JSTL因为模板里不该出现Java逻辑。先引入jstl和standard依赖Maven里加javax.servlet:jstl:1.2然后list.jsp% page contentTypetext/html;charsetUTF-8 languagejava % % taglib urihttp://java.sun.com/jsp/jstl/core prefixc % !DOCTYPE html html head title相册列表/title meta charsetUTF-8 style .grid { display: flex; flex-wrap: wrap; gap: 16px; } .card { border: 1px solid #ccc; padding: 10px; width: 220px; } .card img { width: 100%; height: 160px; object-fit: cover; } /style /head body h2相册管理系统/h2 a hrefupload.jsp上传新照片/a div classgrid c:forEach items${photoList} varphoto div classcard img src${pageContext.request.contextPath}/uploads/${photo.savedName} alt${photo.originalName} / pstrong${photo.originalName}/strong/p p分类${photo.category}/p p大小${photo.size / 1024} KB/p a hrefdelete?id${photo.id} onclickreturn confirm(确定删除吗)删除/a /div /c:forEach /div /body /html注意图片路径一定要用${pageContext.request.contextPath}拼前缀否则当页面在子目录打开时uploads/xxx.jpg会变成相对于当前URL的路径直接导致图片404。JSTL的${photo.size / 1024}会做整数除法如果想知道具体KB可以在实体类里加一个getSizeKB方法返回四舍五入值。用Scriptlet的写法就是把c:forEach换成% for (Photo p : (ListPhoto) request.getAttribute(photoList)) { %原理一致但对HTML的干扰更大。3.3 删除先删物理文件还是先删数据库记录删除功能最容易踩坑的是顺序。如果先删数据库再删物理文件万一文件删失败磁盘上就留下了无引用的孤儿文件如果先删物理文件再删数据库数据库删除失败比如外键约束会导致页面还显示一张已经不存在的图片。我的习惯是先查记录再删物理文件最后删数据库用try-catch兜底package com.photo.servlet; import java.io.File; import java.io.IOException; 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 com.photo.dao.PhotoDao; import com.photo.entity.Photo; WebServlet(/delete) public class DeleteServlet extends HttpServlet { private static final long serialVersionUID 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int id Integer.parseInt(request.getParameter(id)); PhotoDao dao new PhotoDao(); Photo photo dao.findById(id); if (photo ! null) { String realPath getServletContext().getRealPath(uploads); File file new File(realPath, photo.getSavedName()); if (file.exists()) { file.delete(); } dao.delete(id); } response.sendRedirect(request.getContextPath() /list); } }逻辑说明先把ID转成int通过DAO查询到完整记录。new File(realPath, photo.getSavedName())拼出物理文件路径file.delete()返回boolean但不强制判断因为就算删除失败数据库记录也要删否则用户会看到一张点击不存在的死图。如果想更严谨可以在file.delete()失败时记一条日志让运维定期清理。另外删除接口用的是doGet意味着用户可以直接在地址栏构造链接删除图片正规系统应该改成doPost并在JSP里用表单提交这里仅作为课设可用。3.4 分类筛选用query string参数而不是重新建表相册一般会有“家庭”“旅行”“工作”之类的分类。实现分类最简单的方式不是建category表而是在photo表里给category字段存字符串然后用?categoryxxx过滤。ListPhotoServlet处理这种逻辑package com.photo.servlet; import java.io.IOException; import java.util.List; 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 com.photo.dao.PhotoDao; import com.photo.entity.Photo; WebServlet(/list) public class ListPhotoServlet extends HttpServlet { private static final long serialVersionUID 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String category request.getParameter(category); PhotoDao dao new PhotoDao(); ListPhoto list; if (category null || category.isEmpty()) { list dao.findAll(); } else { list dao.findByCategory(category); } request.setAttribute(photoList, list); request.getRequestDispatcher(list.jsp).forward(request, response); } }这里需要注意字符编码category参数通过GET传递Tomcat 8.0及以上默认对GET请求使用UTF-8解析URL但为了兼容老版本Tomcat最好在server.xml的连接器上配置URIEncodingUTF-8。如果category显示成乱码多半就是这里出了问题。DAO里的findByCategory使用PreparedStatement带参查询避免SQL注入。4. 相册系统避坑手册乱码、路径、重名与图片加载的五个常见问题4.1 Tomcat重启后上传目录“消失”现象在Eclipse或IDEA里启动项目后上传几张图一切正常但一旦重新部署或者clean项目之前上传的图片全部404logs目录下文件也没了。原因getServletContext().getRealPath(uploads)返回的是webapps/项目名/uploads这个目录属于构建输出路径IDE在重新部署时会自动清空。解决把上传目录从webapp内移到外部比如在项目外建一个D:/photo_uploads然后在UploadServlet里读取一个全局配置参数。常见做法是用系统属性或者db.properties存储路径再写一个监听器把路径放到ServletContextpackage com.photo.listener; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; WebListener public class UploadPathListener implements ServletContextListener { public void contextInitialized(ServletContextEvent sce) { String uploadPath System.getProperty(photo.upload.path); if (uploadPath null) { uploadPath D:/photo_uploads; // Linux下写 /var/photo_uploads } sce.getServletContext().setAttribute(uploadDir, uploadPath); } }然后在上传、删除时用(String) getServletContext().getAttribute(uploadDir)替代getRealPath。这样即使Tomcat重启图片还在。4.2 上传后文件名出现%E4%B8%AD%E6%96%87这种百分号串现象数据库里存的original_name不是“照片.jpg”而是“%E7%85%A7%E7%89%87.jpg”。原因浏览器上传文件时如果表单没有声明enctypemultipart/form-data不会触发Part接口而Tomcat把文件名字符串按ISO-8859-1解码中文就被变成了URL编码形式。解决JSP里的form标签必须写enctypemultipart/form-data同时UploadServlet在读取Part之前必须执行request.setCharacterEncoding(UTF-8)。如果已经存进了乱码数据需要在Deliver后手动转码new String(request.getParameter(name).getBytes(ISO-8859-1), UTF-8)但这是治标不治本更稳妥的办法是让用户上传时用UTF-8编码的浏览器并且Tomcat 9默认就是UTF-8。4.3 数据库URL没加utf8导致的中文SQL报错现象向数据库插入记录时category字段在页面上显示“旅行”但数据库里变成“”更严重的是执行INSERT时报Incorrect string value: \xE5...。原因MySQL连接URL缺少characterEncodingutf8Java的Unicode字符串在传输时被转成Latin1或者表字符集不是utf8mb4。解决确认建表SQL里每个字符串字段都跟随表的DEFAULT CHARSETutf8mb4确认JDBC URL里加了useUnicodetruecharacterEncodingutf8并且serverTimezone也写上。这三项缺一不可尤其是MySQL 8.0。4.4 图片能列出但浏览器不显示控制台报404现象列表页在Tomcat里启动后可以看到图片文件的记录但img标签的src请求返回404直接访问http://localhost:8080/uploads/xxx.jpg也能打开但页面加载不出来。原因JSP页面里的图片路径用了相对路径uploads/xxx.jpg当当前URL是http://localhost:8080/album/list时相对路径会解析成http://localhost:8080/album/uploads/xxx.jpg这没问题但如果你把Servlet映射到/list.do页面经过forward后相对路径基准变成了/album/list.do/就会拼错。解决所有资源引用都用${pageContext.request.contextPath}开头。上面3.2的list.jsp里已经用了这个写法。另外如果设置了外部上传目录还要在server.xml里添加Context虚拟目录映射或者用一个独立的DownloadServlet去读取外部文件流否则外部路径无法被HTTP直接访问。4.5 大图片拖慢列表页既不做缩略图也不分页现象列表页一次查出200张照片每张原始图片5MB首页加载十几秒滚动卡顿。原因图片字节流没有经过任何处理浏览器下载原图即使CSS把img宽度压缩到100px传输量仍是5MB。解决上传时生成缩略图列表页只展示缩略图同时把列表查询改成按页码取数据。这两个方案第5章会给出具体代码是相册系统从“能跑”到“好用”的分水岭。5. 进阶玩法给相册加分页、缩略图和图片坐标定位5.1 分页查询LIMIT加OFFSET就够相册超过50张图之后一次加载所有数据既不现实也没必要。分页查询在PhotoDao中实现public ListPhoto findByPage(int page, int pageSize) throws SQLException { String sql SELECT * FROM photo ORDER BY upload_time DESC LIMIT ? OFFSET ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, pageSize); ps.setInt(2, (page - 1) * pageSize); try (ResultSet rs ps.executeQuery()) { ListPhoto list new ArrayList(); while (rs.next()) { Photo p new Photo(); p.setId(rs.getInt(id)); p.setOriginalName(rs.getString(original_name)); p.setSavedName(rs.getString(saved_name)); p.setCategory(rs.getString(category)); p.setDescription(rs.getString(description)); p.setSize(rs.getLong(size)); p.setUploadTime(rs.getTimestamp(upload_time)); list.add(p); } return list; } } }LIMIT ? OFFSET ?里的OFFSET是计算出来的第一页偏移0第二页偏移pageSize。ListServlet接收page参数默认1再结合count()方法得到总页数在JSP里渲染上一页/下一页链接。注意不能直接用字符串拼接LIMIT 0, 20虽然能跑但存在SQL注入风险。5.2 用ImageIO生成缩略图200像素宽足够上传之后立刻生成缩略图列表页只加载缩略图详情页才看原图。写一个ThumbnailUtilpackage com.photo.util; import java.awt.Graphics2D; import java.awt.Image; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import javax.imageio.ImageIO; public class ThumbnailUtil { public static void createThumbnail(File src, File dest, int width) throws IOException { BufferedImage original ImageIO.read(src); int originalWidth original.getWidth(); int originalHeight original.getHeight(); int height (int) ((double) originalHeight / originalWidth * width); BufferedImage thumb new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g2d thumb.createGraphics(); g2d.drawImage(original.getScaledInstance(width, height, Image.SCALE_SMOOTH), 0, 0, null); g2d.dispose(); ImageIO.write(thumb, jpg, dest); } }在UploadServlet里调用ThumbnailUtil.createThumbnail(new File(realPath, savedName), new File(realPath, thumb_ savedName), 200)然后列表页的img指向thumb_${savedName}。注意TYPE_INT_RGB会丢失PNG透明通道如果你处理GIF或PNG透明图改成TYPE_INT_ARGB输出格式也对应改为png。这个缺点对相册场景影响不大但遇到白底图标时可能背景变黑需要根据原图格式判断。5.3 在JSP里给图片做坐标定位onclick拿原图坐标热搜里的“jsp图片如何对坐标定位”是一个很实际的场景你在相册里放了一张校区地图想点击某个位置就记录下来。这个功能不需要任何后端接口纯粹在前端完成img src${pageContext.request.contextPath}/uploads/${photo.savedName} idmap-${photo.id} onclickshowPos(event, ${photo.id}) / span idpos-${photo.id}/span script function showPos(e, id) { var img document.getElementById(map- id); var rect img.getBoundingClientRect(); // 计算相对原图的坐标而不是相对显示尺寸 var x (e.clientX - rect.left) * (img.naturalWidth / rect.width); var y (e.clientY - rect.top) * (img.naturalHeight / rect.height); document.getElementById(pos- id).textContent 原图坐标: ( Math.round(x) , Math.round(y) ); } /script这里的关键是把显示尺寸映射回naturalWidth/naturalHeight否则你在CSS缩小的图片上点击得到的坐标全是缩略图尺寸后期存入数据库再去原图画点就错位了。这种方式适合“在地图上标出地点”“在合影里记住人物位置”这类相册增强功能坐标值通过Ajax或隐藏表单提交给后端即可。JSP做动态页面的优势就在这里服务端渲染出每个图片的ID和路径前端脚本统一处理。5.4 验证清单从部署到上线按下面这个顺序自测能避免交付前一晚翻车第一步本地启动MySQL执行建表SQL确认photo表存在。第二步把项目打成war包丢进Tomcat的webapps启动后访问/album/list能看到空列表没有报错。第三步上传一张中文命名的JPG图转到列表页确认缩略图显示、原始文件名不乱码、分类正确。第四步点删除确认第三方软件里的磁盘文件消失数据库记录也消失。第五步重启Tomcat再访问确认外部上传目录里的图片依然存在。如果每一步都通过这个系统基本就能交付了。这套方案做完你会对Servlet生命周期、Part上传、JDBC批处理和Tomcat目录映射都有直观认知之后再去学Spring Boot的MultipartFile封装会感觉毫无神秘感。我第一次用getRealPath存上传文件时也因为不懂tomcat临时目录机制丢过用户图片后来才改成外部路径。这个教训让我意识到技术没有越老越没用只有没被踩过坑的“好用”。希望帮到你。本文还有配套的精品资源点击获取
返回列表