
简介面向Java Web开发者的图片上传下载示例项目基于Spring Boot框架整合ckeditor4富文本编辑器演示从后端接收图片、保存至服务器目录、再由前端访问的完整实现链路。资源适合初步接触文件上传的开发者可从中掌握MultipartFile参数处理、文件重命名与路径安全、CORS跨域配置以及REST接口的JSON返回格式。压缩包共71个文件包含35个Java源码、18个class文件、7个xml配置、6个yml配置、3个properties配置及1个jar包整体大小仅133KB目录结构包含标准Maven工程、资源目录和构建产物便于对照学习。目前已有235人学习。内容还涉及静态资源映射、防盗链设置、云存储与缩略图处理等扩展方向能够帮助读者搭建一个带ckeditor4后台上传能力的轻量级文件管理演示适合课程设计或企业内部工具快速落地。1. Java 图片上传与下载一个 Spring Boot 工程把链路走通做 Java Web 开发图片上传与下载算是绕不开的一关。做内容管理系统、博客后台、课程设计的时候需要跟富文本编辑器打交道而 ckeditor4 依然是老项目和新课设里的常客。这次拆的这份 springboot_file 工程就是用 Spring Boot 把一个「图片上传 下载 静态资源访问」的后端接口完整跑起来并且专门对齐了 ckeditor4 的图片上传协议它要什么字段、你回什么 JSON都写明白了。适合两类人看一是刚学完 Java 基础准备把课程设计落到代码上的同学二是接手了带富文本功能的旧项目正被「编辑器里选完图就裂」折磨的开发者。这东西不复杂但把链路走通、把坑填平需要一点血泪经验。2. 上传接口设计从 MultipartFile 到文件落盘的完整链路2.1 为什么先选本地磁盘存储上传接口的第一步不是写代码而是决定文件存哪。最常见的做法是存服务器本地磁盘比如项目根目录下的uploads/或者操作系统里的/var/www/uploads/。本地磁盘的优势很直接零依赖、性能好、单机部署时读写都够快不需要引入额外的 SDK 或中间件调试起来也直观——文件到底有没有写进去看一眼目录就明白。但本地存储也有明确边界。应用多实例部署时用户上传的图片落在 A 机器请求却被负载均衡转发到 B 机器B 机器上找不到这张图页面就裂了。另外重启或重新部署时如果文件写在 classpath 里比如src/main/resources/static/uploads打出来的 jar 包根本写不进新文件旧文件也随构建过程被覆盖。所以这个工程里我建议把上传目录配在外部绝对路径Spring Boot 只负责读写不把上传目录塞进打包产物。这份springboot_file工程默认也是这个思路application.properties里配一个file.upload-dir自定义属性代码里通过Value注入。这样换机器、换环境只改配置不改代码。2.2 pom.xml 与 application.properties先把地基打对打开工程里的pom.xml核心依赖其实就一个spring-boot-starter-web。文件上传解析、REST 接口、内嵌 Tomcat 都靠它。其他像spring-boot-starter-test是测试用的跑不跑无所谓。如果你是从零起项目用 Maven 构建父工程指向spring-boot-starter-parent然后加 web starterMaven 仓库会自动把依赖拉齐。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies这段依赖的要点是版本继承。spring-boot-starter-parent会把依赖版本统一管理你不需要写 Spring MVC 或 Tomcat 的具体版本号省去一堆版本冲突。从零建工程时最先确认的就是这点——很多人花半天排依赖最后发现是自己手动写的版本和 Spring Boot 内置版本打架。然后是application.properties里的上传配置spring.servlet.multipart.max-file-size10MB spring.servlet.multipart.max-request-size20MB file.upload-dir./uploadsmax-file-size限制单文件大小max-request-size限制一次请求的总大小后者在批量上传时必须大于前者。file.upload-dir是自定义属性代码里用Value(${file.upload-dir})取。需要注意这个目录如果不存在代码里要负责创建Spring 不会帮你建。2.3 写一个能扛住校验的上传接口上传接口的核心是MultipartFile。Spring MVC 在收到multipart/form-data请求时会自动把文件对象封装成MultipartFile注入 Controller 方法参数。这里最容易踩的坑是参数名不匹配ckeditor4 默认的文件字段名是upload而很多通用前端传的是file。先用RequestParam(upload)对齐 ckeditor4如果你的前端传别的名字改成对应字段即可。RestController public class FileUploadController { Value(${file.upload-dir}) private String uploadDir; PostMapping(/api/upload/image) public MapString, Object uploadImage(RequestParam(upload) MultipartFile file) { if (file.isEmpty()) { throw new RuntimeException(上传文件不能为空); } String originalFilename file.getOriginalFilename(); String ext ; if (originalFilename ! null originalFilename.contains(.)) { ext originalFilename.substring(originalFilename.lastIndexOf(.)); } ListString allowedExt Arrays.asList(.jpg, .jpeg, .png, .gif, .webp); if (!allowedExt.contains(ext.toLowerCase())) { throw new RuntimeException(不支持的文件类型: ext); } String newName UUID.randomUUID().toString().replace(-, ) ext; File dir new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } File dest new File(uploadDir File.separator newName); try { file.transferTo(dest); } catch (IOException e) { throw new RuntimeException(文件保存失败, e); } MapString, Object result new HashMap(); result.put(uploaded, 1); result.put(fileName, newName); result.put(url, /uploads/ newName); return result; } }逻辑上分五步判空、取扩展名、白名单校验、UUID 重命名、落盘。transferTo是 Spring 封装的原子操作内部处理了临时文件迁移比自己用FileOutputStream复制稳妥。扩展名白名单是必须的否则任意文件都能上传服务器上被丢一个 JSP 或者可执行脚本就麻烦了。UUID 重命名避免文件名碰撞也顺手把中文名和特殊字符问题解决了。3. 对接 ckeditor4 的后端接口请求字段、JSON 响应与 CORS3.1 ckeditor4 的上传请求到底长什么样ckeditor4 本身不处理文件上传它通过配置项filebrowserUploadUrl把上传动作转交给后端。页面里的编辑器初始化代码通常长这样CKEDITOR.replace(editor, { filebrowserUploadUrl: /api/upload/image });配置之后用户在编辑器里点击「图片」按钮、选中本地文件ckeditor4 会向/api/upload/image发一个multipart/form-data的 POST 请求文件字段名固定是upload。这就是上一章代码里RequestParam(upload)的来源。很多第一次对接的人在这里翻车后端接口写的是RequestParam(file)前端选完图接口报 400因为字段名对不上。你可以在浏览器开发者工具里看到实际请求的 Form Data 里是upload: xxx.png后端接收时就必须写upload。3.2 返回给编辑器的 JSON字段对不上就是黑匣子ckeditor4 的图片上传响应格式是固定的少了url或者把uploaded写成success编辑器都会静默失败——看起来选完图了但编辑区没有反应也不报错就是一片空白。这个黑匣子曾经坑过不少人。字段类型说明uploadedint固定返回 1表示上传成功fileNameString重命名后的文件名urlString图片的可访问地址编辑区会用它渲染 img 标签errorObject上传失败时返回包含message字段第 2 章代码里返回的正是这个结构。注意url可以是相对路径/uploads/xxx.jpg也可以拼成完整的http://host:port/uploads/xxx.jpg。如果前端项目和后端项目部署在不同域名建议直接返回绝对 URL省得前端还要猜协议和端口。失败时按 ckeditor4 的约定uploaded必须置 0同时给出error.message{ uploaded: 0, error: { message: 文件类型不允许 } }3.3 CORS 与跨域本地联调最常见的翻车现场前后端分离开发时前端页面跑在http://localhost:8081后端接口在http://localhost:8080浏览器会拦截跨域请求。ckeditor4 的图片上传本质上是从前端页面发起的 AJAX 请求同样受同源策略限制。不配 CORS接口在 Postman 里能通编辑器里就是不行。Spring Boot 里配 CORS 最干净的方式是让WebMvcConfigurer统一处理Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:8081) .allowedMethods(POST, GET, OPTIONS) .allowedHeaders(*) .maxAge(3600); } }allowedOrigins建议写成具体地址不要直接用*。OPTIONS方法必须放开因为浏览器跨域请求会先发一次预检。配置完记得重启CORS 是请求层面的拦截改完不重启不生效。如果你用 Spring Security还要注意 Security 的过滤器链会先于 CORS 配置执行两套配置都要放行。4. 图片下载与静态资源映射把 uploads 变成可访问的 URL4.1 静态资源映射让 /uploads/** 指向磁盘目录文件落盘之后下一个问题是让用户能通过 URL 访问到它。Spring Boot 默认静态资源路径是classpath:/static/、classpath:/public/这些但上传到服务器磁盘的文件并不在这些目录里直接请求/uploads/xxx.jpg会返回 404。需要手动把 URL 路径映射到磁盘路径。Configuration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/uploads/**) .addResourceLocations(file: uploadDir File.separator); } }addResourceHandler(/uploads/**)声明 URL 前缀addResourceLocations(file: uploadDir)声明磁盘根目录。注意file:前缀不能丢它告诉 Spring 这是文件系统路径而不是 classpath 路径路径末尾的File.separator也要补上否则目录拼接可能出错。配好之后http://localhost:8080/uploads/abc.jpg就能直接访问./uploads/abc.jpg。4.2 URL 拼接把相对路径变成完整地址上一章返回的url字段用的是相对路径/uploads/xxx.jpg。如果图片只在页面里展示相对路径够用。但如果你要把这个 URL 存数据库、推给第三方系统或者给小程序前端用就必须拼成完整地址。常见做法是在 Controller 里通过HttpServletRequest动态拼String fullUrl request.getScheme() :// request.getServerName() : request.getServerPort() /uploads/ newName;getScheme()返回http或httpsgetServerName()是请求的域名getServerPort()是端口。如果后端前面挂了 Nginx 做了 HTTPS 终止这里拿到的可能是http和内网端口需要额外处理X-Forwarded-Proto头。最简单的替代方案是把请求域名配在application.properties里部署时人工确认避免自动拼接在代理环境下出错。4.3 防直接访问与防盗链轻量方案和它的边界默认情况下/uploads/**里的图片是公开的任何人拿到 URL 都能访问。如果图片涉及用户隐私公开访问就不合适。轻量做法是写一个下载接口加权限校验后再读文件返回流GetMapping(/files/{fileName}) public ResponseEntityResource download(PathVariable String fileName, RequestHeader(value Authorization, required false) String token) { if (!valid-token.equals(token)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } Path path Paths.get(uploadDir).resolve(fileName).normalize(); Resource resource new FileSystemResource(path); return ResponseEntity.ok() .contentType(MediaType.IMAGE_JPEG) .body(resource); }resolve(...).normalize()是防路径穿越的关键确保fileName里的../不能跳出上传目录。但这个方案要求前端拿图片时带上鉴权头img标签的src做不到你只能用 base64 或者 XMLHttpRequest 拉取后转 blob。防盗链同理可以通过拦截器检查Referer头来实现但Referer可以被伪造它只能防君子不防小人适合给图片加个门槛不适合做核心安全边界。5. 避坑指南上传下载与 ckeditor4 联调的五个常见坑5.1 上传成功但图片 404资源映射没配或者写进了 classpath现象接口返回uploaded: 1数据也显示保存成功但浏览器访问返回的url是 404。原因最常见的是没有配置WebMvcConfigurer.addResourceHandlersSpring Boot 根本不认识/uploads/这个路径。另一种情况是文件被写进了src/main/resources/static/uploads开发运行时能访问打包成 jar 部署后既写不进新文件旧文件也读不到。解决上传目录独立到外部路径比如./uploads用addResourceHandlers把/uploads/**映射到磁盘目录。打包前确认application.properties里的file.upload-dir指向的是外部路径而不是 classpath 里的相对路径。5.2 中文文件名乱码和路径穿越现象用原始文件名保存中文名变成一串乱码或者构造特殊 URL发现能读到上传目录之外的文件。原因文件系统编码不一致Windows GBK 和 Linux UTF-8 对中文的处理不同。路径穿越则是直接拿客户端提交的文件名去拼接磁盘路径没有做过滤。解决统一用 UUID 重命名从源头规避文件名编码问题。虽然你不需要展示原始文件名但作为这行的习惯落盘文件名里永远不要出现用户可控制的字符串。Path.normalize()是对路径类操作的最后一道防线这两步一起做才稳妥。5.3 大文件上传直接报错Spring 默认限制只有 1MB现象上传 2MB 的图片后端报MaxUploadSizeExceededException或者前端直接收到 500。原因Spring Boot 的spring.servlet.multipart.max-file-size默认是 1MBmax-request-size默认 10MB。超过限制不进 Controller在过滤器层就被拦了。解决在application.properties里调大限制单文件 10MB、请求总量 20MB 是常见配置。注意改了配置要重启Value注入的 multipart 配置在启动时就确定了。如果用了 Nginx还要同步调大client_max_body_size否则请求到不了 Tomcat。5.4 ckeditor4 选完图没反应JSON 字段对不上现象接口返回 200编辑器里图片没有插入控制台也不报错。原因ckeditor4 对响应格式有严格要求。它只认uploaded和url字段你返回{code: 0, data: {url: ...}}之类的结构它解析不出来就静默放弃。解决严格按官方格式返回{uploaded: 1, fileName: xxx.jpg, url: /uploads/xxx.jpg}。调试时用浏览器开发者工具看 Network 面板的响应体确认 JSON 结构没问题再检查前端配置。如果接口返回 HTML 错误页通常是 404检查filebrowserUploadUrl路径是否和后端 Controller 路径一致。5.5 多实例部署后图片时好时坏本地存储的天然短板现象上报到负载均衡环境用户上传的头像有时能显示有时 404两台机器上查文件只在其中一台找到了。原因本地磁盘是单机存储多实例部署时请求被分发到不同机器文件不在同一份目录里。解决这种场景要换共享存储。最省事的是对象存储把文件丢到 OSS 或 S3返回 URL 即可。如果暂时不换云至少要做 NFS 把多台机器的磁盘挂载成同一个目录。这点在设计阶段就要想清楚否则上线后迁移文件会是一段痛苦的经历。6. 进阶从本地磁盘到对象存储接口层怎么改才不伤筋动骨本地磁盘方案撑到一定规模迟早要面对一个问题文件存储需要迁移到对象存储。这时候最怕的是上传逻辑散落在各个 Controller 里改一处漏一处。所以从第一版开始就应该把存储动作抽成接口。public interface StorageService { String store(MultipartFile file); void delete(String fileName); }本地实现放在一个类里把第 2 章的落盘逻辑原封不动搬进来将来要做 OSS就再写一个OssStorageServiceImpl内部用云厂商 SDK 上传返回 URL。Controller 里只依赖StorageService不关心底层是磁盘还是对象存储切换时把标注Service的实现类换掉即可。后续要加缩略图生成在接口层加一个FileProcessService按需处理原图和质量压缩也不会污染上传主流程。从那以后我每次新开一个带文件上传功能的后端项目都会强制自己先花十来分钟把存储接口抽出来哪怕第一版只有本地实现。这个习惯救过我很多次——数据量一旦上来从一台服务器往对象存储迁移的代价远超你当时写接口省下的那点时间。文件上传下载这条路功能做完只是开始存储边界、安全校验、部署形态每一项都要在动手前想清楚。希望帮到你。本文还有配套的精品资源点击获取