资讯动态

SSM图片上传保存数据库与回显实战:从建表SQL到前端展示全流程

发布时间:2026/9/9 4:12:16 来源:尧图企业网站定制
简介面向SSMSpringSpringMvcMybatis初学者的完整图片上传与回显项目源码包解决Java Web开发中图片保存到数据库并重新显示的典型需求。资源共120个文件压缩包约17.5MB主体包括53个jar依赖、18个xml配置、13个java源码、11个jsp页面及sql脚本jar负责搭建依赖环境xml承担框架与映射配置jsp提供上传和回显页面结构清晰便于直接导入项目对照学习。已有5469人学习下载。项目完整覆盖图片从上传到回显的全链路实现代码中包含框架配置、实体类、Mapper接口、控制器与页面模板同时提供了文件类型校验、大小限制、安全存储等必要的防护示范适合用作SSM课程设计、毕业设计或初学者进阶的实战参考。 老实说这个需求在SSM项目里出现频率相当高网上也到处能看到类似的问题讨论但很多回答要么只给了片段要么根本没讲清楚为什么这么做。我最初接手一个内部管理系统的时候也在这上面折腾了好几天后来把整个链路捋顺了才发现其实核心就三件事上传、存库、回显外加处理几个坑。今天这篇就针对SSM(SpringSpringMvcMybatis)图片上传保存到数据库与回显sql这个完整场景把从建表、写Mapper到Controller接口、再到前端展示的全部细节过一遍顺便聊聊我实际踩过的那些坑希望对正在做类似功能的人有点帮助。提示本文面向的是已经把SSM框架跑起来、但还没做过文件上传入库的同学。如果你连SpringMVC的基础路由都不太清楚建议先补一下基础再回来看。1. 图片存数据库到底是不是个好方案先别急着写代码这个问题想不明白后面全是坑。1.1 为什么有人选择把图片存进数据库而不是文件服务器很多刚入行的同学会一脸疑惑图片这种东西最常规的保存方式不是放服务器某个目录然后把路径存进数据库吗为什么还要把图片本身写进数据库我刚开始也这么想。但实际接手过小中型项目之后你就会发现“存路径”和“存文件本身”其实是两套完全不同的取舍。存路径方案的优点很明显数据库体积小图片读写不占用数据库连接性能高。但缺点同样突出文件系统和数据库之间的一致性很难保证。比如用户上传了图片数据库里记录了一条路径但文件还没落盘或者磁盘满了写不进去就会出现“有记录没文件”的情况反过来文件被手动清理了数据库里还留着一条死路径前端拿到路径就是404。而把图片转成二进制字节存进数据库BLOB字段最大的好处就是事务一致性有保障上传、保存、删除都在同一个事务里完成要么都成功要么都失败不会出现数据对不上的问题。另外在备份迁移的时候一张表导出就全带走了不用额外打包一个图片文件夹省心。当年我做一个小型内部工单系统的时候用户量不多图片量也不大就是一张工单截图、一个资质证明文件同时老板把备份看得特别重。这种场景下存数据库反而是最省心的方案直接把库备份走数据就齐全了。1.2 存文件本体有哪几种存储格式既然决定了要往数据库里存接下来就有个选型问题图片在数据库里用什么类型存这里有两个主流方向BLOB二进制类型把MultipartFile读取成byte[]直接通过Mybatis存进数据库。MySQL里常用MEDIUMBLOB或LONGBLOB具体用哪个后面会讲。Base64文本类型上传时把图片字节做Base64编码变成一个很长的字符串存到TEXT或LONGTEXT字段里。回显时前端可以直接把这段字符串塞到img标签的src里。两种方案我都实际用过。BLOB方案更节省空间因为Base64编码本身会带来约33%的体积膨胀一张2MB的图片编码后就变成了大约2.67MB的字符串白占空间。但Base64方案也有优势回显极其简单不用单独写一个输出图片流的接口前端直接赋值给img的src就行对前后端分离的小项目特别友好。我的看法是如果数据库是MySQL存储空间不是特别紧张而你更看重代码简洁那Base64方案是很好用的如果偏传统、讲究规范那就选BLOB。本文主流程按BLOB方案展开后面单独用一个小节讲Base64怎么改。1.3 核心需求拆解从上传到展示的四个环节这个功能看着简单拆开看其实包含了四个不可跳过的环节前端表单上传必须设置enctypemultipart/form-data否则文件数据根本不会出现在请求体里。后端接收与处理SpringMVC用MultipartFile接收读取字节流顺便校验大小和类型。数据库写入通过Mybatis把byte[]映射到BLOB字段SQL里写清楚字段对应关系。图片回显后端提供一个查询接口把数据库里的二进制数据读出来以图片流形式写回浏览器响应前端通过img标签访问。这四个环节只要有一个地方理解错位整个链路就调不通。我见过好多人在回显这一步卡住明明上传成功、数据库里也有数据但图片就是显示不出来原因就是对HTTP响应头不理解。这篇文章的第三章会重点讲。2. 环境准备与数据库表设计动手写代码之前环境版本必须搞清楚。很多诡异问题其实都是版本兼容性引起的。2.1 我用的基础环境与版本组合我用的是以下这套组合都是目前SSM项目里比较常见的稳定版适合直接照搬组件版本JDK1.8Spring / SpringMVC5.0.8.RELEASEMybatis3.4.6Mybatis-Spring 整合包2.0.1MySQL5.7 / 8.0Tomcat8.5为什么推荐Spring 5.0以上因为Spring 5.0全面支持Java 8多部分请求解析器也做了优化。Mybatis就选3.4.x到3.5.x之间太老的版本对byte[]类型自动映射支持不够好太新的又可能跟旧版整合包冲突。MySQL 8.0能用但连接驱动注意别用5.x的老驱动否则会报Public Key Retrieval is not allowed错误在连接URL后面加上allowPublicKeyRetrievaltrue可以解决。如果用的是MySQL 5.7那驱动版本无所谓经典配置就行。2.2 建表SQL与字段设计说明既然标题里带了个sql这张表的设计必须到位。我习惯把图片存储和业务信息分开两张表但初学阶段先放一张表讲清楚原理。CREATE TABLE t_image ( id INT NOT NULL AUTO_INCREMENT COMMENT 主键ID, image_name VARCHAR(128) NOT NULL COMMENT 原始文件名, content_type VARCHAR(64) NOT NULL COMMENT 图片MIME类型如image/jpeg, image_data LONGBLOB NOT NULL COMMENT 图片二进制数据, image_size BIGINT DEFAULT NULL COMMENT 图片大小单位字节, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 上传时间, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT图片上传表;逐列解释一下设计意图image_name保存用户上传时的原始文件名主要用于下载时设置文件名回显用不到。content_type这张表里最重要的字段之一。回显时必须告诉浏览器“这是一张JPEG图片”否则浏览器只会下载而不是直接显示。存了这个字段回显接口就能动态设置Content-Type。image_data核心字段。类型选LONGBLOB原因下面细讲。image_size可选用来显示“该图片多大”也能校验是否传了空文件。2.3 为什么字段类型一定要用LONGBLOBMySQL的BLOB系列总共有四种很多同学在建表时随手写了个BLOB结果图片超过64KB就直接报错。类型最大存储量TINYBLOB256字节BLOB64KBMEDIUMBLOB16MBLONGBLOB4GB手机拍一张照片动不动就3MB到10MBBLOB64KB根本装不下就算用MEDIUMBLOB理论上够用但遇到高分辨率截图或者PDF文件也会紧张。所以直接上LONGBLOB写着省心后面扩展也不用反复改表。要特别提醒Mybatis读取LONGBLOB字段时会自动映射成Java里的byte[]类型这个过程不需要特殊类型处理器框架原生支持。但如果字段类型写成了MEDIUMBLOB而代码里映射的是byte[]查询时也会正常工作只不过存大文件会有被截断的风险建议统一用LONGBLOB。3. 后端代码实现全流程后端部分是整个功能的核心也是坑最多的地方。我按分层结构逐步讲顺便把关键配置一起列出来。3.1 SpringMVC配置文件里的两个关键配置SpringMVC要处理文件上传第一步不是写Controller而是在springmvc.xml里配好解析器bean idmultipartResolver classorg.springframework.web.multipart.commons.CommonsMultipartResolver property namedefaultEncoding valueUTF-8/ property namemaxUploadSize value10485760/!-- 10MB -- property namemaxUploadSizePerFile value5242880/!-- 单个文件5MB -- /bean同时启用注解驱动和静态资源放行否则unmapped请求会报404mvc:annotation-driven/这段配置里最容易被忽略的是defaultEncoding。如果你没设置UTF-8上传的文件名是中文时出来就是乱码图片本身倒是没事但image_name字段没法看。3.2 Mybatis实体类与Mapper接口实体类对应的字段直接对齐数据库表public class ImageEntity { private Integer id; private String imageName; private String contentType; private byte[] imageData; private Long imageSize; private Date createTime; // 省略 getter/setter }Mapper接口public interface ImageMapper { int insertImage(ImageEntity image); ImageEntity selectImageById(Integer id); }3.3 Mybatis映射文件里的核心SQL写法这里是很多人真正想看的“sql”部分。当时我踩过不少坑但核心SQL其实很简单!-- 插入图片 -- insert idinsertImage parameterTypeImageEntity INSERT INTO t_image (image_name, content_type, image_data, image_size) VALUES (#{imageName}, #{contentType}, #{imageData}, #{imageSize}) /insert !-- 根据ID查询 -- select idselectImageById resultTypeImageEntity SELECT id, image_name, content_type, image_data, image_size, create_time FROM t_image WHERE id #{id} /select两个细节值得说一说插入时#{imageData}对应的就是实体类里的byte[] imageDataMybatis会自动调用JDBC的setBytes()方法不需要写typeHandler。查询时image_data字段会自动映射回byte[]同样不需要手动转换。如果你用了一些自动生成代码的工具生成的实体类里可能会把image_data映射成Object那就需要手动改为byte[]。实际调试的时候如果发现插入的时候没报错但数据是空优先检查parameterType是不是写对了以及到底有没有调用setImageData。3.4 Service层与Controller层上传和回显两个接口Service层逻辑很简单就直接放Controller核心代码了Controller public class ImageController { Autowired private ImageMapper imageMapper; RequestMapping(value /upload, method RequestMethod.POST) ResponseBody public String upload(RequestParam(file) MultipartFile file) { if (file null || file.isEmpty()) { return 上传失败文件为空; } String originalFilename file.getOriginalFilename(); // 简单校验扩展名防止恶意上传 String ext originalFilename.substring(originalFilename.lastIndexOf(.) 1).toLowerCase(); if (!(jpg.equals(ext) || jpeg.equals(ext) || png.equals(ext) || gif.equals(ext))) { return 上传失败仅支持jpg、jpeg、png、gif格式; } try { ImageEntity image new ImageEntity(); image.setImageName(originalFilename); image.setContentType(file.getContentType()); image.setImageData(file.getBytes()); image.setImageSize(file.getSize()); imageMapper.insertImage(image); return 上传成功图片ID image.getId(); } catch (Exception e) { e.printStackTrace(); return 上传失败 e.getMessage(); } } RequestMapping(value /image/{id}, method RequestMethod.GET) public void showImage(PathVariable(id) Integer id, HttpServletResponse response) { ImageEntity image imageMapper.selectImageById(id); if (image null || image.getImageData() null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } // 设置内容类型和缓存策略 response.setContentType(image.getContentType()); response.setContentLength(image.getImageData().length); response.setHeader(Cache-Control, max-age3600); try { response.getOutputStream().write(image.getImageData()); response.getOutputStream().flush(); } catch (IOException e) { e.printStackTrace(); } } }先说上传接口。RequestParam(file)对应表单里input标签的name属性必须一致否则SpringMVC会报Required request part file is not present。我见过有人把表单里的namepicture和后端的RequestParam(file)写错位调了半天才发现是对不上。再说回显接口。这个接口不返回JSON而是直接把图片二进制字节写到HttpServletResponse的输出流里。关键是这两行response.setContentType(image.getContentType())告诉浏览器返回的是图片而不是文本。response.getOutputStream().write(image.getImageData())把数据库里的字节流吐给客户端。如果漏掉setContentType或者每次都写死image/jpeg那么同一套接口显示PNG图片时就可能出问题。技巧回显接口支持浏览器缓存。对静态图片非常适合加Cache-Control响应头。否则一个页面上10张图片每次刷新都查10次数据库数据库压力会很大还很慢。4. 前端页面实现与回显方式后端接口好了前端要做的两件事就是上传表单、显示图片。这块同样也有不少细节。4.1 上传表单的几个容易踩的坑上传表单的标准写法如下form action${pageContext.request.contextPath}/upload methodpost enctypemultipart/form-data input typefile namefile acceptimage/*/ button typesubmit上传/button /form这里必须注意两件事method必须是post。get请求根本不会携带文件体。enctypemultipart/form-data必须加上且表单里所有普通文本字段和文件字段都会通过multipart格式传递。忘了这行SpringMVC那边拿到的就是一堆字符串而不是文件。acceptimage/*只是前端提示用户依然可以改成“所有文件”。所以后端一定要有类型校验不能完全依赖前端。早年用form形式提交有个体验问题页面会刷新。如果你在做一个管理系统不想刷新页面的可以用jQuery的FormData异步提交代码大概这样$.ajax({ url: contextPath /upload, type: POST, data: new FormData($(#uploadForm)[0]), processData: false, contentType: false, success: function(result) { // result 是后端返回的图片ID字符串 $(#imgPreview).attr(src, contextPath /image/ result); } });注意processData和contentType必须设置成false否则浏览器会把FormData转成字符串导致文件数据丢失。4.2 回显的两种主流方式对比第一种就是上一章的/image/{id}接口方式前端img标签这样用img src${pageContext.request.contextPath}/image/1 stylewidth:200px;/这种方式的好处是后端只出一次请求负载低而且可以利用HTTP缓存。作为主力方案我一直用这个。第二种是Base64方式。把上传接口改成返回Base64字符串或者再写一个接口专门查图片并转Base64// 上传时把 byte[] 转 base64 存到 LONGTEXT 字段 String base64 Base64.getEncoder().encodeToString(file.getBytes()); // 回显时前端直接用 img srcdata:image/jpeg;base64,${base64Str} /Base64方案最致命的缺点是体积膨胀约33%。如果一个页面上要展示20张图片每张2MB源码那前端DOM里塞着近60MB的字符串页面卡到怀疑人生。它更适合“系统里就存几张证件照、偶尔看一下”的场景。所以我个人强烈建议实战项目中用BLOB加/image/{id}回显方案数据库存储空间省页面性能也更好。4.3 多图片回显时的性能考虑如果业务需要展示一个图片列表比如相册、工单截图列表千万别写循环里一个个查单条数据的SQL。数据库连接和查询开销会成倍增加。正解是直接写一个按业务ID批量查图片的SQL举个例子select idselectImagesByBizId resultTypeImageEntity SELECT id, content_type, image_data, image_size FROM t_image WHERE biz_id #{bizId} /select虽然这样依然要查BLOB数据但至少数据库访问次数从N次降为1次。要更进一步可以拆成两步列表查询只查非BLOB字段前端img标签请求具体图片时再单独走/image/{id}。这也是很多商业化系统的标准做法。5. 实际开发中踩过的坑与排查技巧这功能看起来简单真正联调时问题一个接一个。我把常见的问题整理成了一张表并补充对应的排查思路。现象原因解决方案上传报错Required request part file is not present表单enctype没设置或RequestParam名字和input的name不一致检查表单enctypemultipart/form-data检查参数名对齐数据库里image_data为NULL没调用file.getBytes()或者实体类的setter没执行debug看Controller里实体对象是否有值回显时浏览器直接下载而不是显示图片响应头Content-Type没设置或设置错误用image.getContentType()填充或根据扩展名判断类型图片上传后无法显示但数据库有数据回显接口路径和img标签路径不一致或静态资源被拦截器拦截核对路径检查DispatcherServlet的url-pattern是否拆了路由Base64回显在浏览器里报错缺少data:image/png;base64,前缀或base64字符串本身包含换行符拼接前缀删除字符串中的\r\n换行符上传大文件时Tomcat默认拒绝Tomcat默认maxPostSize为2MB或multipartResolver限制了大小调大Tomcat连接器的maxPostSize和multipartResolver参数下面挑几个典型场景详细展开。5.1 中文文件名乱码问题这个坑在Windows下尤其常见。上传了一个“测试图片.png”存到数据库后变成“æµè¯图片.png”完全是乱码。原因有两层第一层是表单提交时浏览器怎么编码文件名跟页面编码有关第二层是后端接收和数据库存储的字符集。解决方案页面编码统一UTF-8meta charsetutf-8加上。springmvc.xml中的multipartResolver里设置defaultEncodingUTF-8。数据库连接URL增加characterEncodingutf8jdbc:mysql://localhost:3306/test?useUnicodetruecharacterEncodingutf8useSSLfalse建表时统一用utf8mb4。如果数据库已经建好且改不了表结构还可以在代码里对文件名重新编码String name file.getOriginalFilename(); // 处理较老版本Tomcat下出现的中文乱码问题 if (name ! null name.getBytes().length ! name.length()) { // 不要直接使用 ISO-8859-1 转 UTF-8容易二次乱码 // 推荐检查表单页面、连接、容器编码 }老实说编码问题排查起来很烦最好在项目初始化阶段就统一UTF-8后面能省好多事。5.2 图片过大导致数据库和内存双压前面提到上传接口用file.getBytes()意味着整张图片先全部加载进JVM内存里。如果同时有100个用户每人上传一张5MB的图内存压力立刻翻5倍。实际处理中我加了两道防线上传前做规格限制比如微信小程序端的头像上传在Controller层先校验file.getSize()超过2MB直接拒绝。超过阈值之后再用ImageIO.read()做压缩把长宽缩到合理范围再入库。用Java ImageIO压缩图片的参考代码BufferedImage bufferedImage ImageIO.read(file.getInputStream()); if (bufferedImage.getWidth() 800) { int targetWidth 800; int targetHeight (int) (bufferedImage.getHeight() * (targetWidth * 1.0 / bufferedImage.getWidth())); BufferedImage newImage new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB); Graphics2D g newImage.createGraphics(); g.drawImage(bufferedImage, 0, 0, targetWidth, targetHeight, null); g.dispose(); ByteArrayOutputStream baos new ByteArrayOutputStream(); ImageIO.write(newImage, jpg, baos); fileBytes baos.toByteArray(); }这种压缩方案适合一般的证件照、截图场景。但要注意如果图片有透明背景且是PNG直接输出成JPG会把透明部分变黑正确做法是根据contentType判断PNG输出成PNGJPG输出成JPG。5.3 SQL注入风险提醒标题里带了sql就不得不提SQL注入。我见过有初学者在Mapper XML里这么写select idqueryByName resultTypeImageEntity SELECT * FROM t_image WHERE image_name ${imageName} /select${}是做字符串拼接用户传入一个 OR 11查询条件就被改写了直接把整张表都查出来。正确写法是select idqueryByName resultTypeImageEntity SELECT * FROM t_image WHERE image_name #{imageName} /select#{}在Mybatis底层会转成PreparedStatement的占位符?参数值交由JDBC驱动处理从根源上杜绝注入风险。这个习惯一定要养成不只是这个功能里任何一个项目都应该只用#{}除非你明确知道自己在做什么且值来源于可控白名单。5.4 Mybatis查询BLOB时可能出现的内存溢出与游标问题如果查询结果的BLOB字段特别多、数据量特别大一次性查出来全放内存里很容易出现OutOfMemoryError。特别适合单条记录超过几十MB的时候。解决方向层面一绝不在列表查询里查BLOB字段只查元数据详情再走单查接口。层面二如果非要一次性拿大量BLOB数据可以考虑在Mybatis的映射文件中做分页比如一次只取10条。对一个图片上传功能来说保持数据量可控再加上缩略图策略基本不会遇到这个问题。怕的就是不设防什么都往库里塞还不压缩。6. 后续可以怎么扩展功能完成了不妨想一想它在未来项目里怎么演进。我没打算写那种特别宏大的架构方案就说几个实实在在的方向。6.1 引入文件存储数据库和文件系统分工当系统慢慢做大图片量达到几十G甚至几百G时数据库BLOB方案就会变成负担。备份和恢复很慢数据库缓冲池也被大字段拖累。此时更合理的思路是图片文件存入本机磁盘目录、FastDFS、MinIO数据库里只存文件的路径或唯一标识。这套方案的优点是数据库变轻文件IO更快代价是文件系统和数据库的一致性问题又回来了需要通过事务或补偿机制保证。如果项目要快速落地我更推荐一个简单路线先把BLOB方案上线跑着同时预留一个file_url字段等量上来后做数据迁移把BLOB导出为物理文件再更新URL字段。迁移完成后再逐步切到文件存储接口。6.2 从SSM迁移到Spring Boot时要注意的差异现在已经有很多人开始用Spring Boot了也会问SSM的这套方案能不能直接搬过去。可以搬但有几个点要改Spring Boot里不再需要MultipartResolver的XML配置它会默认加载MultipartAutoConfiguration你只需要在application.yml下配置spring: servlet: multipart: max-file-size: 5MB max-request-size: 10MBController、Service、Autowired这些注解不变Mapper还是用MapperScan扫描。Mybatis的mybatis-config.xml和Mapper XML照样能用只要在application.yml里指定mybatis.mapper-locations: classpath:mapper/*.xml。也就是说核心的SQL、实体类、Service、Controller代码在SSM和Spring Boot之间是高度可复用的主要改动集中在配置方式上。前段时间我把一个老SSM项目往Spring Boot迁两个晚上就把核心功能全部跑通这个功能本身几乎没改动非常省事。6.3 统一采购一个文件上传组件是不是更好这里必须老实说一句如果你是在一个全新项目里做文件上传主流做法永远是先考虑成熟的组件或对象存储服务而不是自己把文件写进数据库。成熟文件上传组件比如一些云厂商的对象存储SDK自带多线程分片上传、断点续传、CDN加速、内容审核直接省掉底层各种琐碎问题。而“SSM图片上传保存到数据库”这套方案适合的场景是内网系统、数据量可控且对一致性要求高的项目或纯学习练手。认清适用边界活在现实里比什么都重要。我在实际项目中通常这样做小型内部系统——存数据库省事面向公网、可能有大流量轰炸的系统——上对象存储或专业文件服务。这两个方向不冲突关键是按需求选型不盲目跟风。写在最后再分享一个一直留在我习惯清单里的小技巧做这类图片上传功能时用response.getOutputStream()向外写数据的时候写完一定要flush()。我第一次写的时候漏掉了导致前端的img图片偶尔显示一半或不全排查了很久才发现是输出流没有及时刷出完整数据。虽然理论上连接关闭前会自动flush但显式调一次稳妥很多。另外花了几个小时调试之后不妨回看一下自己写的代码想想哪一步可能会被后来人搞混。给关键方法写两行清晰注释比什么都重要。希望这篇文字能帮你少走一些弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价