
简介这是一套面向Java后端与大数据方向学习者的企业云盘项目源码基于SpringBoot与Hadoop技术栈构建适合希望实践微服务架构与分布式存储结合的中高级开发者参考。项目围绕用户管理、文件上传下载、共享权限、版本控制与多租户隔离等核心功能展开并涉及HDFS分布式存储、MapReduce并行计算与YARN资源调度等大数据处理环节。压缩包共2210个文件以2022个svg图标资源、50个java源码、28个css与21个js前端脚本为主另含少量html、xml、yml配置及sql脚本整体约7.03MB目录结构清晰便于按模块检索。目前已有268人学习下载。读者可从中获取完整的云盘服务端与前端实现思路、RESTful接口设计、文件元数据存储方案以及容器化部署与监控的参考配置对理解SpringBoot与Hadoop的整合应用具有较高借鉴价值。1. 企业云盘为什么总在“能跑”和“能用”之间翻车很多团队第一次做企业云盘Demo 阶段用 SpringBoot 加本地磁盘就能跑通上传下载一上生产就原形毕露文件一多单机磁盘先满并发一高Tomcat 线程池被大文件 IO 拖死想扩容发现文件路径全写死在本地迁移一次要停机半天。这套「基于 SpringBoot 与 Hadoop 实现的企业云盘」要解决的正是这个断层——用 SpringBoot 扛住 Web 层和业务编排用 Hadoop 的 HDFS 做底层海量存储把「能跑」推到「能用」。它适合三类人正在做 Java 课程设计、需要一套完整可讲清架构的参考实现的学生手里有 SpringBoot 项目、想补上分布式存储这一环的后端以及要评估「自建云盘 vs 买对象存储」的架构决策者。核心链路其实就四条用户认证、文件上传落 HDFS、元数据落关系库、下载时从 HDFS 流式回吐。把这四条打通剩下的都是工程细节。下面按「先立架构、再动手、最后避坑」的顺序拆开讲。2. 架构选型SpringBoot 与 Hadoop 各自该扛哪一段2.1 为什么不是 SpringBoot 直接写本地磁盘本地磁盘方案在单机、小文件、低并发下没问题但企业云盘的三个特征会直接击穿它。第一是容量天花板单块盘再大也有上限而 HDFS 可以横向加 DataNode第二是可靠性本地盘坏了文件就没了HDFS 默认三副本能扛住单节点故障第三是扩展性本地路径写死后迁移成本极高HDFS 的命名空间是逻辑的扩容对上层透明。但也不能矫枉过正把所有东西都塞进 HDFS。HDFS 的强项是大文件顺序读写弱项是海量小文件和随机修改。企业云盘里用户信息、文件目录树、权限、分享记录这些结构化、需要频繁更新和条件查询的数据放 MySQL 这类关系库才合理。所以正确的分工是文件内容进 HDFS文件元数据进 MySQL两者用文件 ID 关联。这个边界一旦划错后面全是坑。2.2 SpringBoot 侧的分层与关键依赖SpringBoot 在这里的角色是「业务编排 Web 接入」不是存储。典型分层是 Controller 接请求、Service 编排上传下载逻辑、DAO 管元数据、一个独立的 HDFS 客户端封装层负责和 NameNode 通信。依赖上除了常规的 spring-boot-starter-web、mybatis-plus核心是 Hadoop 客户端!-- pom.xml 关键依赖版本按你集群实际对齐 -- dependency groupIdorg.apache.hadoop/groupId artifactIdhadoop-client/artifactId version3.3.4/version !-- 必须和集群 Hadoop 版本一致否则 RPC 协议可能不兼容 -- /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency这里最容易翻车的是版本。hadoop-client 的版本必须和集群 NameNode、DataNode 的版本严格对齐大版本不一致时常见ProtocolException或Server IPC version mismatch。我一般先在集群上跑hadoop version确认再回填到 pom。另外 hadoop-client 会带进一堆 log4j、slf4j 的传递依赖和 SpringBoot 自带的日志实现冲突是高频问题通常要 exclude 掉slf4j-log4j12和log4j统一走 logback。2.3 HDFS 侧的存储规划HDFS 不是拿来就用企业云盘场景要提前定三件事。一是块大小默认 128MB如果业务以小文件几 KB 到几 MB 的文档、图片为主块大小调小意义不大反而要面对小文件问题正确做法是引入归档或合并策略而不是改块大小。二是副本数默认 3测试集群可以降到 1 省资源生产别低于 2。三是目录规划建议按业务或租户分目录比如/cloud/{tenantId}/{yyyyMM}/避免所有文件堆在一个目录下导致 NameNode 元数据压力集中。# 在集群上创建云盘根目录并授权注意用 hdfs 用户执行 hdfs dfs -mkdir -p /cloud hdfs dfs -chmod -R 755 /cloud hdfs dfs -chown -R hadoop:hadoop /cloud # 换成你实际的运行用户 hdfs dfs -ls / # 确认目录已建好参数说明-p递归建目录-chmod 755保证应用侧用户有读写执行权限-chown把属主改成应用运行用户否则 SpringBoot 进程连不上会报Permission denied。这一步看着简单但权限没配对是新手最常见的第一个卡点。3. 从零跑通上传下载的最小可复现链路3.1 HDFS 客户端封装与连接参数不要在业务代码里到处 new Configuration封装一个单例的 FileSystem 客户端集中管理连接和关闭。核心配置项就几个Configuration public class HdfsConfig { Value(${hdfs.uri}) // 例如 hdfs://namenode-host:8020 private String hdfsUri; Value(${hdfs.user}) // 有权限的 HDFS 用户 private String hdfsUser; Bean public FileSystem fileSystem() throws IOException { Configuration conf new Configuration(); // 副本数在客户端设置只对新建文件生效已有文件不受影响 conf.set(dfs.replication, 2); // 关闭 HDFS 客户端的短路读避免本地读路径带来的权限困惑 conf.set(dfs.client.read.shortcircuit, false); // 连接超时网络抖动时避免线程长时间挂起 conf.set(ipc.client.connect.timeout, 10000); return FileSystem.get(URI.create(hdfsUri), conf, hdfsUser); } }逻辑说明FileSystem.get带 user 参数是为了明确以哪个身份访问避免依赖环境变量HADOOP_USER_NAME这种隐式行为。dfs.replication在客户端设置只影响本客户端新建的文件是个容易误解的点。超时参数很关键默认值偏长NameNode 短暂不可用时会把 Tomcat 线程拖住设成 10 秒能让失败快速暴露。参数怎么改副本数按集群规模定测试 1、生产 2 到 3超时按网络质量调内网 5 到 10 秒够用。3.2 文件上传流式写入与元数据落库上传的核心是「边收边写」不要先把整个文件读进内存再写 HDFS大文件会直接 OOM。用 MultipartFile 的输入流直接对接 HDFS 输出流Service public class FileService { Autowired private FileSystem fileSystem; Autowired private FileMetaMapper metaMapper; public String upload(MultipartFile file, Long userId) throws IOException { // 生成全局唯一文件 ID用 UUID 避免文件名冲突和路径穿越 String fileId UUID.randomUUID().toString().replace(-, ); String hdfsPath /cloud/ userId / fileId; // try-with-resources 保证流一定关闭否则会泄漏 HDFS 连接 try (InputStream in file.getInputStream(); FSDataOutputStream out fileSystem.create(new Path(hdfsPath))) { // 8KB 缓冲区边读边写内存占用恒定 byte[] buffer new byte[8192]; int len; while ((len in.read(buffer)) ! -1) { out.write(buffer, 0, len); } } // 元数据落库和文件内容分开存 FileMeta meta new FileMeta(); meta.setFileId(fileId); meta.setUserId(userId); meta.setFileName(file.getOriginalFilename()); meta.setSize(file.getSize()); meta.setHdfsPath(hdfsPath); meta.setCreateTime(new Date()); metaMapper.insert(meta); return fileId; } }逻辑说明fileSystem.create返回的是 FSDataOutputStream写入过程 HDFS 会自动分块和复制。缓冲区 8KB 是经验值太小系统调用频繁太大内存浪费64KB 到 128KB 在高吞吐场景也可以。参数说明hdfsPath用 fileId 而不是原始文件名是为了规避中文名、特殊字符和路径穿越../问题原始文件名只存元数据。注意这里没有做事务如果元数据插入失败HDFS 上会留下孤儿文件生产环境要么加补偿清理要么先插元数据再写文件。3.3 文件下载流式回吐与断点续传的取舍下载同样要流式把 HDFS 输入流直接接到 HTTP 响应输出流public void download(String fileId, HttpServletResponse response) throws IOException { FileMeta meta metaMapper.selectById(fileId); if (meta null) { response.setStatus(404); return; } // 设置响应头文件名做 URL 编码避免中文乱码 String encodedName URLEncoder.encode(meta.getFileName(), UTF-8); response.setHeader(Content-Disposition, attachment; filename encodedName); response.setContentType(application/octet-stream); response.setContentLengthLong(meta.getSize()); try (FSDataInputStream in fileSystem.open(new Path(meta.getHdfsPath())); OutputStream out response.getOutputStream()) { byte[] buffer new byte[8192]; int len; while ((len in.read(buffer)) ! -1) { out.write(buffer, 0, len); } } }逻辑说明fileSystem.open拿到的是分布式文件输入流HDFS 客户端会自动从最近的 DataNode 拉数据。Content-Length用元数据里的 size让浏览器能显示进度。断点续传这里没做如果要支持需要解析请求头Range用FSDataInputStream.seek()定位再返回 206 状态码这是进阶内容最小链路先不做。参数说明缓冲区同上Content-Disposition的 filename 一定要编码否则中文文件名在部分浏览器会乱码或截断。3.4 用 curl 验证整条链路写完别急着上页面先用命令行把链路验一遍排除前端干扰# 上传-F 指定表单文件字段名要和 Controller 的 RequestParam 对齐 curl -X POST http://localhost:8080/file/upload \ -H Authorization: Bearer token \ -F file./test.pdf # 下载-o 保存到本地观察文件是否完整 curl -X GET http://localhost:8080/file/download?fileId返回的fileId \ -o downloaded.pdf # 对比大小一致说明链路没丢数据 ls -l test.pdf downloaded.pdf逻辑说明先验上传返回的 fileId再用它下载最后比对文件大小。如果大小不一致多半是流没读完就关了或者 Content-Length 设错。这一步能快速定位是应用层问题还是 HDFS 层问题——如果 HDFS 上hdfs dfs -ls /cloud/{userId}/能看到文件且大小对那问题就在下载侧。4. 避坑与排查那些让云盘“看起来正常”的隐性故障4.1 上传大文件报 OOM 或连接超时现象上传几十 MB 以上文件时应用日志出现OutOfMemoryError或SocketTimeoutException。原因通常是用了file.getBytes()把整个文件读进内存或者 HDFS 写入超时太短。解决坚持用输入流边读边写绝不调getBytes()同时调大dfs.client.socket-timeout和dfs.datanode.socket.write.timeout大文件写入慢是正常的别让超时误杀。4.2 小文件把 NameNode 压垮现象云盘跑一段时间后上传变慢NameNode 内存持续上涨。原因每个小文件在 HDFS 上都对应一条元数据海量小文件会耗尽 NameNode 内存。解决业务层做合并比如同一用户的多个小文件打包成归档或者引入 HDFS 的 HAR 归档更彻底的是在元数据层记录逻辑文件物理上合并存储。这是 HDFS 的固有特性不是配置能绕过的。4.3 权限报 Permission denied现象本地 IDE 跑得好好的部署到服务器就报权限错误。原因HDFS 客户端默认用当前系统用户身份访问IDE 里可能是你的账号服务器上是另一个用户。解决在FileSystem.get里显式传 user 参数或者设置环境变量HADOOP_USER_NAME并确保该用户在 HDFS 上有对应目录的权限。别用 root 跑应用权限模型会乱。4.4 中文文件名乱码现象上传中文名文件下载后名字变成乱码或问号。原因HTTP 响应头Content-Disposition没做编码或者 HDFS 路径里直接用了中文名。解决文件名只存元数据HDFS 路径用 UUID响应头用URLEncoder.encode(name, UTF-8)编码。两处都做基本不会再乱。4.5 元数据和 HDFS 不一致现象数据库里有记录HDFS 上文件却没了或者反过来。原因上传时先写 HDFS 再插库中间失败没回滚或者有人手动删了 HDFS 文件。解决加对账任务定期比对元数据和 HDFS 实际文件写入顺序上可以先插元数据状态为“上传中”写完 HDFS 再更新为“完成”失败时靠状态字段清理。这是分布式系统绕不开的一致性问题别指望一次写对。5. 进阶把云盘从“能存”做到“好用”的两个技巧第一个技巧是给上传加秒传和去重。企业云盘里重复文件很常见同一份文档被多人上传。做法是在上传前先算文件的 MD5 或 SHA-256拿哈希去元数据表查命中就直接返回已有 fileId不重复写 HDFS。这样既省存储又提速。实现上注意大文件算哈希要流式别读进内存public String calcSha256(InputStream in) throws Exception { MessageDigest digest MessageDigest.getInstance(SHA-256); byte[] buffer new byte[8192]; int len; while ((len in.read(buffer)) ! -1) { digest.update(buffer, 0, len); } // 转成十六进制字符串作为去重键 StringBuilder sb new StringBuilder(); for (byte b : digest.digest()) { sb.append(String.format(%02x, b)); } return sb.toString(); }逻辑说明流式算哈希内存恒定适合大文件。参数说明SHA-256 比 MD5 安全但计算稍慢如果只做去重不涉及安全MD5 也够用看你对碰撞的容忍度。注意算哈希要读一遍流上传还要再读一遍等于读两次可以在上传时边写 HDFS 边算一次读完。第二个技巧是验证副本是否真的生效。很多人以为设了dfs.replication3就高枕无忧其实要看实际块状态# 查看某个文件的块和副本分布 hdfs fsck /cloud/1/abc123 -files -blocks -locations # 输出里每个块会列出所在的 DataNode副本数不足会有 under-replicated 提示逻辑说明fsck是 HDFS 自带的健康检查工具-blocks看块信息-locations看副本落在哪些节点。如果显示Under-replicated说明副本没达到预期可能是 DataNode 掉线或磁盘满。定期跑这个命令比等用户报故障再查要主动得多。我自己踩得最深的一次是测试环境图省事把副本数设成 1上线前忘了改回 3结果一台 DataNode 磁盘故障直接丢了一批文件元数据还在文件却读不出来对账时才发现。从那以后我养成的习惯是任何存储相关的配置测试和生产用两套 profile 明确区分上线前用fsck把关键目录扫一遍。这套 SpringBoot 加 Hadoop 的云盘方案本身不复杂难的是把每个边界都想清楚。希望帮到你。本文还有配套的精品资源点击获取