简介基于Hadoop与SSH框架的HDFS网盘项目是一个面向分布式存储应用开发的完整工程适合正在学习Hadoop生态、Java Web编程或准备课程设计的初、中级开发者。资源展示了通过SSH安全通道操作Hadoop集群的方式实现了用户认证、文件上传下载、在线预览、目录管理等网盘常见功能体现了分布式系统与Web应用结合的实际落地路径。压缩包共629个文件约37.4MB涵盖Java后端源码、Class字节码、JSP页面、JS脚本、CSS样式、XML配置及依赖库等其余为界面图片和工程辅助文件整体结构完整、模块边界清楚。已有132人浏览学习。源码中可重点学习SSH免密登录配置、HDFS客户端接口封装、后台与页面数据交互、异常处理与日志监控等具体实现也可参考其类设计改造出适合自身业务的HDFS网盘服务对理解大规模数据存储系统的上层应用开发非常实用。1. 别把“ssh”当成加密协议这个基于Hadoop的网盘项目到底在做什么如果你是做Java Web的看到“基于Hadoop利用ssh框架实现hdfs网盘”这个标题第一反应多半是这跟Linux服务器上的SSH远程连接有什么关系真不是。在Java Web语境里SSH指的是Struts Spring Hibernate这套经典的三层框架组合。这个项目做的是把HDFS的文件目录包装成Web接口让浏览器用户像用百度网盘一样上传、下载、浏览文件而底层存储用的是Hadoop分布式文件系统。适合谁课程设计、“头歌”类综合实训、或者想给企业内网搭一个简单文件管理后台的开发者。这件事的难点不在页面而在理解HDFS的读写流程、权限模型和Java客户端的使用边界。2. 架构先行SSH三层怎么接上HDFS这层存储2.1 认准“SSH”里的三张脸Struts、Spring、Hibernate各自管什么先说框架分工。Struts2负责HTTP请求分发用户在页面上点击“上传”请求先进Struts的过滤器再落到Action方法Spring管Bean的创建、注入和事务边界HdfsService、FileListAction这些对象都交给Spring容器来管别自己newHibernate负责把网盘的业务记录——文件名、归属用户、上传时间、文件大小——持久化到MySQL。这里有个最容易想错的点Hibernate只管业务元数据不管文件内容本身。HDFS里存的是真正的文件块Hibernate表里存的是这些文件块的“索引卡片”。两者不能互相替代。很多人会纠结我能不能不用Struts直接用Spring MVC也能。但课程设计或实训环境里指定了SSH框架就按SSH来。核心是不变的Struts管接口入口Spring管对象和事务Hibernate管元数据HDFS管数据本体。理解了这个分工后面写代码就不会把HDFS路径塞进Hibernate的实体里去。2.2 用Spring把HDFS客户端的生命周期管起来Hadoop的FileSystem客户端是个重量级对象每次请求都重新创建Configuration、再调用FileSystem.get()去建立连接代价很高高并发时还会把客户端socket和RPC连接打满。常见的做法是把Configuration作为Spring单例Bean管理启动时初始化一次整个应用共享。先写一个HDFS配置工厂把连接参数集中在这里后续所有服务类都从Spring容器拿这个ConfigurationComponent(hdfsConfigFactory) public class HdfsConfigFactory { public static Configuration createConfiguration() { Configuration conf new Configuration(); // 指定NameNode的RPC地址通常是8020或9000按你集群的core-site.xml为准 conf.set(fs.defaultFS, hdfs://hadoop-master:8020); // 伪分布式环境必须设成1否则会因副本数不足写入卡住后文避坑章节细说 conf.set(dfs.replication, 1); // 开启后客户端会用DataNode主机名回连跨网段部署时很有用 conf.set(dfs.client.use.datanode.hostname, true); // 关闭HDFS权限校验课程设计阶段省去用户管理和Kerberos的麻烦 conf.set(dfs.permissions.enabled, false); return conf; } }我写的是配置工厂为什么不用Spring的XML直接配因为Hadoop的conf.set(key, value)不是标准的JavaBean setter在XML里写property会很别扭。在Spring的applicationContext.xml里把它注册成单例Beanbean idhdfsConfiguration classorg.apache.hadoop.conf.Configuration factory-beanhdfsConfigFactory factory-methodcreateConfiguration/之后在Service里通过构造器注入整个应用持有一个Configuration实例。FileSystem.get(conf)内部有缓存机制同样的Configuration第二次调用会返回同一个文件系统实例这正好符合Spring单例的思路。注意这里坑很多Configuration对象传进FileSystem.get之后不要随便修改参数改一个参数可能导致缓存key变化然后产生新的实例旧连接没人释放。2.3 HDFS权限模型不调好这个接口全会报Permission deniedHDFS的权限模型是从Linux文件权限抄来的owner、group、others三组每组r/w/x三种权限由NameNode强制校验。默认情况下一个没做过任何配置的HDFS集群超级用户是启动NameNode的那个Linux账号常见是hdfs或root。你的Java Web应用跑在tomcat用户下tomcat在HDFS眼里就是个普通用户对“/”目录根本没有写权限。这就是为什么我第一次调网盘上传接口时所有上传请求全部抛AccessControlException。解决办法分两种。认真一点的做法是给每个网盘注册用户创建独立的HDFS目录并在代码里用UserGroupInformation切换用户。课程设计阶段图省事直接在上面的配置工厂里把dfs.permissions.enabled设成false关掉权限检查。如果不想全局关权限又想保留用户名区分用UserGroupInformation的doAs来隔离用户操作public void mkdirAsUser(String username, String path) throws IOException, InterruptedException { UserGroupInformation ugi UserGroupInformation.createRemoteUser(username); ugi.doAs(new PrivilegedExceptionActionObject() { Override public Object run() throws Exception { FileSystem fs FileSystem.get(conf); fs.mkdirs(new Path(path)); return null; } }); }doAs方法要求异常处理run方法不能直接throws IOException以外的异常这两个细节都是容易卡住的地方。另外注意FileSystem.get(conf)会缓存实例不同用户共享同一个FileSystem实例时权限校验仍然按Linux用户来不会按你doAs里的username变所以真正要区分归属更好的办法是让每个用户访问自己独立的目录比如/user/zhangsan、/user/lisi而不是依赖HDFS做用户隔离。下表是我梳理的HDFS客户端核心参数按“调试时几乎一定会动到”排序参数名默认值作用踩坑提示fs.defaultFSfile:///指定默认文件系统不设就操作本地文件必填否则上传下载落在服务器本地磁盘dfs.replication3块副本数单节点伪分布式必须改成1dfs.permissions.enabledtrueHDFS权限开关课程设计可关生产环境不建关闭dfs.client.use.datanode.hostnamefalse客户端连接DataNode时用IP还是主机名Windows或跨网段开发时改成truedfs.blocksize128MB块大小小文件场景可调小但别低于1MB3. 跑通最小实现从Maven依赖到首个HDFS列表接口3.1 一个pom.xml把SSH和Hadoop都搂进来SSH三件套和Hadoop客户端的依赖坑集中在版本冲突上。最典型的是把hadoop-hdfs和hadoop-client同时引进来导致org.apache.hadoop.fs.FileSystem类出现两份运行时报NoSuchMethodError或者奇怪的空指针。我的建议是只引入一个hadoop-client它会传递引入hdfs、common、yarn-client等核心模块。Struts和Spring的版本也要匹配Struts2.5以下的版本配Spring4很顺畅上了较新的Spring5那套就得检查兼容性。下面这段pom.xml是我在类似项目里实际用过的骨架properties !-- 版本号以你的Hadoop集群版本为准这里示意2.10.x -- hadoop.version2.10.2/hadoop.version struts.version2.5.30/struts.version spring.version5.2.22.RELEASE/spring.version /properties dependencies dependency groupIdorg.apache.hadoop/groupId artifactIdhadoop-client/artifactId version${hadoop.version}/version /dependency dependency groupIdorg.apache.struts/groupId artifactIdstruts2-core/artifactId version${struts.version}/version /dependency dependency groupIdorg.apache.struts/groupId artifactIdstruts2-spring-plugin/artifactId version${struts.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency !-- Hibernate按你选的版本走注意与Spring的orm模块版本对应 -- dependency groupIdorg.hibernate/groupId artifactIdhibernate-core/artifactId version5.4.24.Final/version /dependency /dependencies版本号后面的注释是有意写的。你实际用的时候Hadoop版本要去问集群管理员或者在你自己的伪分布式环境里执行hadoop version确认。高版本Hadoop的客户端比如3.x连接低版本集群兼容性一般没问题反过来就难说。还有一点引入hadoop-client之后里面自带的javax.servlet相关类可能会和Tomcat的Servlet冲突运行时报“java.lang.LinkageError”这时用provided作用域排除掉servlet-api即可。3.2 封装HdfsService连接、建目录、列目录就这几招Service类是SSH架构里的业务层它不关心请求怎么来的只负责和HDFS打交道。最核心的三个操作给用户建专属目录、列目录内容、删除文件。我把它们封装到一个HdfsService里用构造器注入Spring里的Configuration单例。Service(hdfsService) public class HdfsService { private final Configuration conf; Autowired public HdfsService(Configuration conf) { this.conf conf; } private FileSystem getFileSystem() throws IOException { // 用缓存实例不在方法里close交给Spring容器统一管理 return FileSystem.get(conf); } /** * 返回目录下的文件列表每项包含文件名、大小、修改时间、是否目录。 */ public ListMapString, Object listFiles(String path) throws IOException { FileSystem fs getFileSystem(); Path dir new Path(path); FileStatus[] statuses fs.listStatus(dir); ListMapString, Object result new ArrayList(); for (FileStatus status : statuses) { MapString, Object item new HashMap(); item.put(name, status.getPath().getName()); item.put(isDir, status.isDirectory()); item.put(length, status.getLen()); item.put(modTime, status.getModificationTime()); result.add(item); } return result; } public boolean mkdir(String path) throws IOException { FileSystem fs getFileSystem(); return fs.mkdirs(new Path(path)); } public boolean delete(String path, boolean recursive) throws IOException { FileSystem fs getFileSystem(); return fs.delete(new Path(path), recursive); } }这段代码的核心是把HDFS客户端的使用收敛到一个类里后续Action层只需要面对ListMapString, Object不用关心Path、FileStatus这些Hadoop类型。注意getFileSystem()方法里没有调用close这是故意的。FileSystem.get返回的是带缓存实例一旦close下次get会尝试重建连接反而更容易出问题。正确的收尾方式是在Spring容器销毁时统一调用FileSystem.closeAll()来释放所有连接。listStatus方法的返回数组是不保证顺序的网盘页面如果希望按目录优先、文件名排序需要自己在内存里用Comparator排一下不然每次刷新顺序都可能变这是很多SSH结合HDFS项目容易忽略的体验细节。3.3 用Struts2 Action把HDFS目录树吐给页面Struts这边的事情就很机械了用户在页面点击目录请求参数是pathAction收到后调用HdfsService.listFiles获取列表把结果塞进request作用域转发到JSP进行渲染。public class FileListAction extends ActionSupport { // Struts2的action都是原型模式Spring通过struts2-spring-plugin注入service Autowired private HdfsService hdfsService; private String path /; public void setPath(String path) { this.path path; } public String getPath() { return path; } private ListMapString, Object files; public ListMapString, Object getFiles() { return files; } Override public String execute() throws Exception { // 防止路径穿越只允许相对路径拼接在业务根目录下 if (path null || path.trim().isEmpty()) { path /; } files hdfsService.listFiles(path); return SUCCESS; } }配合struts.xml里的映射配置struts package namenetdisk extendsstruts-default action namelist classcom.netdisk.action.FileListAction result namesuccess/WEB-INF/pages/list.jsp/result result nameerror/WEB-INF/pages/error.jsp/result /action /package /struts有几个坑值得提醒。第一Struts2的Action默认是多实例的每个请求new一个新对象所以HdfsService必须通过Spring注入如果Action直接newHdfsService里的Configuration不会是Spring管理的那一份。第二路径校验很重要。用户在浏览器把path参数改成../../就可能访问到HDFS里的其他目录课程设计虽然不要求安全加固但至少要把path统一拼到预设根目录下比如/user/netdisk禁止绝对路径直接透传给HdfsService。第三Struts的result里不要用/list.jsp这种路径放在WEB-INF下更安全用户没法直接访问JSP只能通过Action转发过去。4. 上传和下载HDFS读写流程、命令参数与边界4.1 上传文件用流式API避开HDFS的“小文件陷阱”HDFS设计上是为“一次写入、多次读取”的大文件准备的。写入流程是客户端把文件切分成数据块一个块一个块地通过流水线管道写入多个DataNode每个DataNode写完本地副本后逐级返回确认。这个过程由客户端侧的FSDataOutputStream驱动开发者能做的只有两件事准备好InputStream把数据源源不断write出去。上传Action的代码public class UploadAction extends ActionSupport { Autowired private HdfsService hdfsService; // Struts2从表单里接收文件文件名会被改写成uploadFileName private File upload; private String uploadFileName; private String targetDir; public void setUpload(File upload) { this.upload upload; } public void setUploadFileName(String uploadFileName) { this.uploadFileName uploadFileName; } public void setTargetDir(String targetDir) { this.targetDir targetDir; } Override public String execute() throws Exception { FileSystem fs FileSystem.get(hdfsService.getConfiguration()); // 中文文件名在HDFS里建议做转义否则前端URL取文件时会因编码问题404 String encodedName URLEncoder.encode(uploadFileName, UTF-8); Path target new Path(targetDir / encodedName); try (FSDataOutputStream out fs.create(target, true); InputStream in new BufferedInputStream(new FileInputStream(upload), 4 * 1024 * 1024)) { byte[] buffer new byte[4 * 1024 * 1024]; int read; while ((read in.read(buffer)) ! -1) { out.write(buffer, 0, read); } } catch (IOException e) { // 写入失败时最好把半成品文件清掉避免留下隐藏的残缺块 fs.delete(target, false); throw e; } return SUCCESS; } }这里有个细节缓冲区的字节数组设为4MB一开始我用64KB上传100MB的文件花了将近两分钟改成4MB之后明显快了。HDFS的create方法默认覆盖策略是lazy第二个参数true代表覆盖写如果网盘业务不允许覆盖同名文件自己先调用exists判断不要依赖HDFS去做事务级的覆盖保护。另外out.write(buffer, 0, read)里一定要写0和read不能图省事直接write(buffer)不然最后一次读取不满4MB时会带入旧数据这是流式写入最常见的数据损坏原因。4.2 下载与seek断点续传的底层是FSDataInputStream下载的反向链路同样是流式的。客户端拿到HDFS路径fs.open返回FSDataInputStream然后循环read到Servlet的输出流里。值得展开的是seek方法它能在文件内部按字节偏移跳转这是实现断点续传的基础。给一个带偏移参数下载的Action核心方法public void downloadFile(String hdfsPath, long offset, HttpServletResponse response) throws IOException { FileSystem fs FileSystem.get(hdfsService.getConfiguration()); Path src new Path(hdfsPath); try (FSDataInputStream in fs.open(src)) { FileStatus stat fs.getFileStatus(src); long fileLength stat.getLen(); // 校验偏移量不能超过文件末尾 if (offset fileLength - 1) { throw new IllegalArgumentException(offset out of range); } if (offset 0) { in.seek(offset); } // 只输出剩余部分用于断点续传 long remain fileLength - offset; response.setContentLengthLong(remain); response.setHeader(Content-Disposition, attachment; filename\ src.getName() \); byte[] buffer new byte[128 * 1024]; int read; long written 0; while ((read in.read(buffer)) ! -1) { response.getOutputStream().write(buffer, 0, read); written read; if (written remain) break; } } }这段代码里有两个经验点。第一HDFS读到文件末尾时会返回-1理论上有这个就可以退出循环但套上offset之后剩余字节数是不确定的可能出现多读的问题所以用written变量把输出截断到剩余长度。第二Content-Length头在断点续传时是门将设置不对前端a标签下载的文件大小就会飞。实际做网盘时还需要在HTTP头里加Accept-Ranges: bytes这样浏览器或下载管理器才知道这个服务器支持断点续传。HDFS里seek操作是廉价的NameNode只负责定位数据块位置信息在客户端会缓存所以不用怕频繁seek影响性能。4.3 用hadoop fs命令和distcp验证数据是否就位代码写完数据有没有真的落到HDFS别只信页面上的“上传成功”要进集群用命令行把数据翻出来看。这里列的是我每次做完网盘功能必跑的验证命令# 查看目录树递归列出所有文件和目录 hadoop fs -ls -R /user/netdisk # 查看某个目录下所有文件的总大小-h让数字更可读 hadoop fs -du -h /user/netdisk # 把HDFS上的文件拉到本地对照原始文件md5值检查是不是传的一样 hadoop fs -get /user/netdisk/测试文件.docx /tmp/download_test.docx md5sum /tmp/download_test.docx /path/to/original.docx # 检查HDFS上文件的块分配情况确认副本数和块大小 hadoop fsck /user/netdisk/测试文件.docx -files -blocks -locationsdistcp这个命令是跨集群或跨目录批量复制数据的工具网盘需要做数据备份时很管用。它的API是MapReduce作业天然支持并行。常用参数我整理成表参数作用典型取值-m最大并行map数决定复制带宽设为DataNode数的2~3倍-bandwidth限制每个map的带宽MB/s内网复制可设大跨机房设20~50-p保留权限、属主、时间戳备份场景必须加-update只复制源端比目标端新的文件同步增量数据-delete删除目标端多余文件慎用会清掉目标端独有数据例如把集群A的网盘目录完整同步到集群B可以执行hadoop distcp -m 4 -bandwidth 50 -p hdfs://cluster-a:8020/user/netdisk hdfs://cluster-b:8020/user/netdiskdistcp跑完退出码为0不一定是成功要去看YARN日志里是否有task失败因为某些块复制失败时作业也可能返回整体成功状态这个“半成功”状态是我踩过一次的暗坑后面避坑章节细说。5. HDFS网盘避坑指南5个让我翻过车的地方5.1 一调接口就是AccessControlException连目录都列不出来现象在页面上打开网盘首页列表接口直接报org.apache.hadoop.security.AccessControlException: Permission denied: usertomcat, accessREAD, inode/:root:supergroupHDFS的日志一行接一行地刷新“No user found for UID”。原因NameNode的超级用户是启动它的那个Linux账号比如root或hdfs。Tomcat进程跑在tomcat用户下天然不是超级用户对根目录/没有读权限所以第一眼看到的永远是权限拒绝。解决最省事的就是按2.3节说的在Configuration里把dfs.permissions.enabled设成false关闭权限检查。我第二次做这类项目时没有关全局权限而是给每个网盘用户show目录匹配让tomcat用户对/user/netdisk目录单独授权hadoop fs -chmod -R 755 /user/netdisk再让tomcat用户成为netdisk组的成员。这个办法能保住其他目录的权限隔离生产环境建议这么干。5.2 并发上传时连接数被打满页面卡成白屏现象内网5个人同时上传大文件开始还正常过了几十秒所有上传进度条都不动了NameNode日志里刷RPC超时客户端报Call timed out after 60000ms。原因FileSystem.get(conf)会根据Configuration里的连接数配置创建到NameNode的RPC连接默认的ipc.client.connection.maxidletime是10秒空闲连接会被回收但忙碌时不会无限创建。当时我没把Configuration交给Spring单例管理每个Action都new了一份高并发下连接没有被复用NameNode端达到IPC连接上限后直接把新请求拒掉。解决把Configuration收敛成Spring单例第3章的做法并调大客户端连接池参数在Configuration里加两行conf.set(ipc.client.connection.maxidletime, 60000); conf.set(dfs.client.block.write.replace-datanode-on-failure.policy, NEVER);前一个让空闲连接多存活60秒减少频繁建连的开销后一个避免流水线中某个DataNode失败时触发替换DataNode的额外握手过程缩短单次写入失败重试的时间。5.3 Windows下开发明明连的是HDFS却跑到了本地文件系统现象在Windows上跑Tomcat调试调用mkdir后Java进程的工作目录下多了一个名和HDFS路径一样的文件夹。原因Configuration里忘了设fs.defaultFS默认值是file:///。FileSystem.get(conf)没有拿到HDFS地址时默认创建LocalFileSystem所有路径解析都基于本地文件系统。这个问题隐蔽在控制台完全不出错只是文件落在奇怪的地方。解决把5.2和3.2里的那一行加回来——conf.set(fs.defaultFS, hdfs://namenode地址:8020)——并且在配置初始化之后打印一行日志确认取值log.info(fs.defaultFS {}, conf.get(fs.defaultFS));这行日志值得保留因为项目打包部署到不同环境时集群地址不一致有了它排查路径问题省掉一半时间。5.4 文件传上去了MySQL里也写了记录重启服务后记录对不上现象网盘页面上能看到某个文件HDFS里对应目录却没有这个文件或者反过来HDFS里有文件页面上消失不见了重启Tomcat后更严重。原因真凶在Hibernate事务和HDFS操作顺序。当时Action里先调HdfsService上传HDFS再调Hibernate保存记录两步没有放到同一个Spring事务里。上传成功后、数据库保存前Tomcat恰好重启HDFS上的孤儿文件就留下了反过来数据库提交成功但HDFS还没写完记录就成了幽灵数据。解决给网盘的业务入口方法加Transactional注解把两个操作包进事务并且坚持先写HDFS再写数据库。HDFS本身就是最终一致的数据源Hibernate只存元数据业务上允许“HDFS有了但记录还没写”的短暂窗口不允许反过来——记录存在但数据没了。SSH框架里Spring事务默认只对运行时异常回滚HDFS的IOException是受检异常不会触发回滚所以写数据库的时候要主动判断HDFS操作结果失败就抛一个运行时异常让事务回滚。5.5 伪分布式下副本数写成3写入永远卡在最后一个副本现象单节点Hadoop伪分布式环境上传文件时进度条走到98%就停住最后报org.apache.hadoop.ipc.RemoteException: java.io.IOException: File could only be written to 0 of the 1 minReplication nodes. There are 1 datanode(s) running and 1 node(s) are excluded in this operation.原因伪分布式只有一个DataNode而HDFS默认副本数配置是3。写入时客户端要求数据块必须写入至少3个DataNode实际可用只有1个所以第一次写失败后客户端会尝试调整副本数结果仍然不满足“最小副本数1”的最低条件最终卡死。解决在2.2节的配置工厂里加上dfs.replication1同时检查集群的hdfs-site.xml里是不是也有同样的配置。注意一个玄学点即使你在代码里反复设置NameNode端的dfs.replication参数没改客户端仍可能按2或3去请求所以两处都要设成1才算稳妥。6. 让网盘扛住真实使用用fsck和distcp做上线前验证这个网盘能跑起来只是开始上线前我习惯逼着自己做两件事验证数据完整性和验证容灾能力。验证完整性靠fsck它会扫描HDFS上的每个块确认每个文件的副本数是否达标、有没有坏块输出里如果出现CORRUPT字样说明有块损坏。完整命令是hadoop fsck /user/netdisk -files -blocks -locations看输出最末一行统计信息里Status: HEALTHY才是真健康。另一个验证是容灾演练把网盘目录用distcp复制到一个测试目录然后删除原目录再从测试目录恢复模拟一次误删事故。这一步做顺了网盘才有了“后悔药”功能否则用户手滑删了文件你只能去DataNode磁盘上考古。一定要记住HDFS不是普通文件系统它没有回收站之外的保护机制也没有事务补偿。把所有关键路径的操作记录到日志用数据库做一份变更流水比什么都重要。我的习惯是每次发布前跑一遍“上传→下载→校验大小→删除→恢复”的完整链路脚本只有这条链路全绿我才敢把网盘交出去。这个习惯帮我挡掉过两次版本升级带来的低级错误希望也能帮到你。本文还有配套的精品资源点击获取