news 2026/9/9 4:12:09

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

作者头像

张小明

前端开发工程师

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

简介:面向SSM(Spring+SpringMvc+Mybatis)初学者的完整图片上传与回显项目源码包,解决Java Web开发中图片保存到数据库并重新显示的典型需求。资源共120个文件,压缩包约17.5MB,主体包括53个jar依赖、18个xml配置、13个java源码、11个jsp页面及sql脚本;jar负责搭建依赖环境,xml承担框架与映射配置,jsp提供上传和回显页面,结构清晰便于直接导入项目对照学习。已有5469人学习下载。项目完整覆盖图片从上传到回显的全链路实现,代码中包含框架配置、实体类、Mapper接口、控制器与页面模板;同时提供了文件类型校验、大小限制、安全存储等必要的防护示范,适合用作SSM课程设计、毕业设计或初学者进阶的实战参考。 老实说,这个需求在SSM项目里出现频率相当高,网上也到处能看到类似的问题讨论,但很多回答要么只给了片段,要么根本没讲清楚为什么这么做。我最初接手一个内部管理系统的时候,也在这上面折腾了好几天,后来把整个链路捋顺了才发现,其实核心就三件事:上传、存库、回显,外加处理几个坑。今天这篇就针对SSM(Spring+SpringMvc+Mybatis)图片上传保存到数据库与回显+sql这个完整场景,把从建表、写Mapper到Controller接口、再到前端展示的全部细节过一遍,顺便聊聊我实际踩过的那些坑,希望对正在做类似功能的人有点帮助。

提示:本文面向的是已经把SSM框架跑起来、但还没做过文件上传入库的同学。如果你连SpringMVC的基础路由都不太清楚,建议先补一下基础再回来看。

1. 图片存数据库,到底是不是个好方案?

先别急着写代码,这个问题想不明白,后面全是坑。

1.1 为什么有人选择把图片存进数据库而不是文件服务器

很多刚入行的同学会一脸疑惑:图片这种东西,最常规的保存方式不是放服务器某个目录,然后把路径存进数据库吗?为什么还要把图片本身写进数据库?

我刚开始也这么想。但实际接手过小中型项目之后,你就会发现“存路径”和“存文件本身”其实是两套完全不同的取舍。

存路径方案的优点很明显:数据库体积小,图片读写不占用数据库连接,性能高。但缺点同样突出:文件系统和数据库之间的一致性很难保证。比如用户上传了图片,数据库里记录了一条路径,但文件还没落盘,或者磁盘满了写不进去,就会出现“有记录没文件”的情况;反过来,文件被手动清理了,数据库里还留着一条死路径,前端拿到路径就是404。

而把图片转成二进制字节存进数据库BLOB字段,最大的好处就是事务一致性有保障,上传、保存、删除都在同一个事务里完成,要么都成功,要么都失败,不会出现数据对不上的问题。另外在备份迁移的时候,一张表导出就全带走了,不用额外打包一个图片文件夹,省心。

当年我做一个小型内部工单系统的时候,用户量不多,图片量也不大,就是一张工单截图、一个资质证明文件,同时老板把备份看得特别重。这种场景下,存数据库反而是最省心的方案,直接把库备份走,数据就齐全了。

1.2 存文件本体,有哪几种存储格式

既然决定了要往数据库里存,接下来就有个选型问题:图片在数据库里用什么类型存?

这里有两个主流方向:

  • BLOB二进制类型:把MultipartFile读取成byte[],直接通过Mybatis存进数据库。MySQL里常用MEDIUMBLOBLONGBLOB,具体用哪个后面会讲。
  • Base64文本类型:上传时把图片字节做Base64编码,变成一个很长的字符串,存到TEXTLONGTEXT字段里。回显时前端可以直接把这段字符串塞到img标签的src里。

两种方案我都实际用过。BLOB方案更节省空间,因为Base64编码本身会带来约33%的体积膨胀,一张2MB的图片,编码后就变成了大约2.67MB的字符串,白占空间。但Base64方案也有优势:回显极其简单,不用单独写一个输出图片流的接口,前端直接赋值给img的src就行,对前后端分离的小项目特别友好。

我的看法是:如果数据库是MySQL,存储空间不是特别紧张,而你更看重代码简洁,那Base64方案是很好用的;如果偏传统、讲究规范,那就选BLOB。本文主流程按BLOB方案展开,后面单独用一个小节讲Base64怎么改。

1.3 核心需求拆解:从上传到展示的四个环节

这个功能看着简单,拆开看其实包含了四个不可跳过的环节:

  1. 前端表单上传:必须设置enctype="multipart/form-data",否则文件数据根本不会出现在请求体里。
  2. 后端接收与处理:SpringMVC用MultipartFile接收,读取字节流,顺便校验大小和类型。
  3. 数据库写入:通过Mybatis把byte[]映射到BLOB字段,SQL里写清楚字段对应关系。
  4. 图片回显:后端提供一个查询接口,把数据库里的二进制数据读出来,以图片流形式写回浏览器响应,前端通过img标签访问。

这四个环节只要有一个地方理解错位,整个链路就调不通。我见过好多人在回显这一步卡住,明明上传成功、数据库里也有数据,但图片就是显示不出来,原因就是对HTTP响应头不理解。这篇文章的第三章会重点讲。

2. 环境准备与数据库表设计

动手写代码之前,环境版本必须搞清楚。很多诡异问题,其实都是版本兼容性引起的。

2.1 我用的基础环境与版本组合

我用的是以下这套组合,都是目前SSM项目里比较常见的稳定版,适合直接照搬:

组件版本
JDK1.8
Spring / SpringMVC5.0.8.RELEASE
Mybatis3.4.6
Mybatis-Spring 整合包2.0.1
MySQL5.7 / 8.0
Tomcat8.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后面加上allowPublicKeyRetrieval=true可以解决。如果用的是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`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='图片上传表';

逐列解释一下设计意图:

  • image_name:保存用户上传时的原始文件名,主要用于下载时设置文件名,回显用不到。
  • content_type:这张表里最重要的字段之一。回显时必须告诉浏览器“这是一张JPEG图片”,否则浏览器只会下载而不是直接显示。存了这个字段,回显接口就能动态设置Content-Type
  • image_data:核心字段。类型选LONGBLOB,原因下面细讲。
  • image_size:可选,用来显示“该图片多大”,也能校验是否传了空文件。

2.3 为什么字段类型一定要用LONGBLOB

MySQL的BLOB系列总共有四种,很多同学在建表时随手写了个BLOB,结果图片超过64KB就直接报错。

类型最大存储量
TINYBLOB256字节
BLOB64KB
MEDIUMBLOB16MB
LONGBLOB4GB

手机拍一张照片,动不动就3MB到10MB,BLOB(64KB)根本装不下,就算用MEDIUMBLOB,理论上够用,但遇到高分辨率截图或者PDF文件也会紧张。所以直接上LONGBLOB,写着省心,后面扩展也不用反复改表。

要特别提醒:Mybatis读取LONGBLOB字段时,会自动映射成Java里的byte[]类型,这个过程不需要特殊类型处理器,框架原生支持。但如果字段类型写成了MEDIUMBLOB,而代码里映射的是byte[],查询时也会正常工作,只不过存大文件会有被截断的风险,建议统一用LONGBLOB

3. 后端代码实现全流程

后端部分是整个功能的核心,也是坑最多的地方。我按分层结构逐步讲,顺便把关键配置一起列出来。

3.1 SpringMVC配置文件里的两个关键配置

SpringMVC要处理文件上传,第一步不是写Controller,而是在springmvc.xml里配好解析器:

<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="defaultEncoding" value="UTF-8"/> <property name="maxUploadSize" value="10485760"/><!-- 10MB --> <property name="maxUploadSizePerFile" value="5242880"/><!-- 单个文件5MB --> </bean>

同时启用注解驱动和静态资源放行,否则unmapped请求会报404:

<mvc: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 id="insertImage" parameterType="ImageEntity"> INSERT INTO t_image (image_name, content_type, image_data, image_size) VALUES (#{imageName}, #{contentType}, #{imageData}, #{imageSize}) </insert> <!-- 根据ID查询 --> <select id="selectImageById" resultType="ImageEntity"> SELECT id, image_name, content_type, image_data, image_size, create_time FROM t_image WHERE id = #{id} </select>

两个细节值得说一说:

  1. 插入时#{imageData}对应的就是实体类里的byte[] imageData,Mybatis会自动调用JDBC的setBytes()方法,不需要写typeHandler
  2. 查询时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-age=3600"); 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。我见过有人把表单里的name="picture"和后端的@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" method="post" enctype="multipart/form-data"> <input type="file" name="file" accept="image/*"/> <button type="submit">上传</button> </form>

这里必须注意两件事:

  • method必须是postget请求根本不会携带文件体。
  • enctype="multipart/form-data"必须加上,且表单里所有普通文本字段和文件字段,都会通过multipart格式传递。忘了这行,SpringMVC那边拿到的就是一堆字符串而不是文件。

accept="image/*"只是前端提示,用户依然可以改成“所有文件”。所以后端一定要有类型校验,不能完全依赖前端。

早年用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); } });

注意processDatacontentType必须设置成false,否则浏览器会把FormData转成字符串,导致文件数据丢失。

4.2 回显的两种主流方式对比

第一种就是上一章的/image/{id}接口方式,前端img标签这样用:

<img src="${pageContext.request.contextPath}/image/1" style="width:200px;"/>

这种方式的好处是后端只出一次请求,负载低,而且可以利用HTTP缓存。作为主力方案,我一直用这个。

第二种是Base64方式。把上传接口改成返回Base64字符串,或者再写一个接口专门查图片并转Base64:

// 上传时把 byte[] 转 base64 存到 LONGTEXT 字段 String base64 = Base64.getEncoder().encodeToString(file.getBytes()); // 回显时前端直接用 <img src="data:image/jpeg;base64,${base64Str}" />

Base64方案最致命的缺点是体积膨胀约33%。如果一个页面上要展示20张图片,每张2MB源码,那前端DOM里塞着近60MB的字符串,页面卡到怀疑人生。它更适合“系统里就存几张证件照、偶尔看一下”的场景。

所以我个人强烈建议实战项目中用BLOB加/image/{id}回显方案,数据库存储空间省,页面性能也更好。

4.3 多图片回显时的性能考虑

如果业务需要展示一个图片列表,比如相册、工单截图列表,千万别写循环里一个个查单条数据的SQL。数据库连接和查询开销会成倍增加。

正解是直接写一个按业务ID批量查图片的SQL,举个例子:

<select id="selectImagesByBizId" resultType="ImageEntity"> 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不一致检查表单enctype=multipart/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”,完全是乱码。

原因有两层:第一层是表单提交时浏览器怎么编码文件名,跟页面编码有关;第二层是后端接收和数据库存储的字符集。

解决方案:

  1. 页面编码统一UTF-8,<meta charset="utf-8">加上。
  2. springmvc.xml中的multipartResolver里设置defaultEncoding=UTF-8
  3. 数据库连接URL增加characterEncoding=utf8
    jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false
  4. 建表时统一用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倍。

实际处理中,我加了两道防线:

  1. 上传前做规格限制(比如微信小程序端的头像上传),在Controller层先校验file.getSize(),超过2MB直接拒绝。
  2. 超过阈值之后再用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输出成PNG,JPG输出成JPG。

5.3 SQL注入风险提醒

标题里带了sql,就不得不提SQL注入。我见过有初学者在Mapper XML里这么写:

<select id="queryByName" resultType="ImageEntity"> SELECT * FROM t_image WHERE image_name = '${imageName}' </select>

${}是做字符串拼接,用户传入一个' OR '1'='1,查询条件就被改写了,直接把整张表都查出来。正确写法是:

<select id="queryByName" resultType="ImageEntity"> SELECT * FROM t_image WHERE image_name = #{imageName} </select>

#{}在Mybatis底层会转成PreparedStatement的占位符?,参数值交由JDBC驱动处理,从根源上杜绝注入风险。这个习惯一定要养成,不只是这个功能里,任何一个项目都应该只用#{},除非你明确知道自己在做什么,且值来源于可控白名单。

5.4 Mybatis查询BLOB时可能出现的内存溢出与游标问题

如果查询结果的BLOB字段特别多、数据量特别大,一次性查出来全放内存里,很容易出现OutOfMemoryError。特别适合单条记录超过几十MB的时候。

解决方向:

  1. 层面一:绝不在列表查询里查BLOB字段,只查元数据,详情再走单查接口。
  2. 层面二:如果非要一次性拿大量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: 10MB
  • @Controller@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,但显式调一次,稳妥很多。

另外,花了几个小时调试之后,不妨回看一下自己写的代码,想想哪一步可能会被后来人搞混。给关键方法写两行清晰注释,比什么都重要。希望这篇文字能帮你少走一些弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 4:11:56

RTX 5070Ti游戏本跌破万元:选购、验机与避坑全指南

今天朋友圈里最先刷屏的不是新机发布&#xff0c;而是一张渠道报价单&#xff1a;RTX 5070Ti游戏本&#xff0c;i9处理器加32G内存加1T固态的配置&#xff0c;最低已经干到9999&#xff0c;个别二线型号甚至不到9500。放在半年前&#xff0c;这个价格连5070Ti的尾巴都摸不着&am…

作者头像 李华
网站建设 2026/9/9 4:11:01

YOLOv26行人车辆检测实战:从训练到部署

1. 项目动机&#xff1a;为什么我盯上了“行人车辆”双目标检测做目标检测的都知道&#xff0c;COCO数据集上跑个mAP图个乐很容易&#xff0c;但真正把模型丢到真实交通场景里&#xff0c;面对白天黑夜、晴天雨天、密集人流和川流不息的车流时&#xff0c;模型的“人设”瞬间就…

作者头像 李华
网站建设 2026/9/9 4:10:49

ESP32在线烧录全攻略:网页刷写、串口烧录与OTA升级实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 4:08:33

FAST-LIO2实战解析:直接法LiDAR SLAM与ikd-Tree点云管理核心原理

在真实环境中做 LiDAR SLAM&#xff0c;很多时候最大的问题不是算法不够精巧&#xff0c;而是它根本跑不起来。面对树叶乱晃的灌木丛、空旷的长走廊、或者灰尘弥漫的矿区&#xff0c;传统特征法前端先跪了一半&#xff0c;后端再强也救不回来。FAST-LIO2 之所以在这几年成为许多…

作者头像 李华
网站建设 2026/9/9 4:07:38

Java后端参数校验:Commons Validator与ValidX对比与选型

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 4:07:05

从引导扇区到中断处理:自制操作系统的实战构建与调试指南

简介&#xff1a;一套与《30天自制操作系统》同步的完整源文件包&#xff0c;面向操作系统学习者、计算机专业学生及对底层原理感兴趣的开发者&#xff0c;也可作为课程设计、自学项目或面试准备的重要参考资料。内容涵盖启动加载器、内核开发、进程管理、内存管理、文件系统与…

作者头像 李华