【Hadoop/Java】基于HDFS的Java Web网络云盘
本人BNUZ大学生萌新,水平不足,还请各位多多指教!
实验目的
- 熟悉HDFS Java API的使用;
- 能使用HDFS Java API编写应用程序
实验要求
云盘系统通过互联网为企业和个人提供信息的储存、读取、下载等服务,具有安全稳定、海量存储的特点。根据用户群定位,云盘系统可以划分为公有云盘、社区云盘、私有云盘等。请利用HDFS Java API,编写一个云盘系统,要求实现功能如下:
环境
Ubuntu 20.04LTS + Java (OpenJDK 8) + IDEA Ultimate 2021.3.3 + Hadoop 3.3.2
项目下载
Github仓库:https://github.com/gennwolf/yunpan-hadoop
使用框架以及Web服务器
Maven + SpringBoot 2.6.4 + SpringMVC + Apache Tomcat 9.0.58
前端模板来源:https://colorlib.com (使用了Bootstrap + jQuery)
分布式集群配置
有3个节点,每个节点的主机名、IP以及担任的角色如下表所示:
节点主机名IP角色1Master192.168.170.111NameNode, SecondaryNameNode, DataNode, ResourceManager, NodeManager2Slave1192.168.170.112DataNode, NodeManager3Slave2192.168.170.113DataNode, NodeManager网关子网掩码192.168.170.2255.255.255.0
实验步骤
- 启动Hadoop集群,分别到各个节点使用jps命令查看进程是否与上表匹配: 下图为Master节点的Java进程,和上表对应节点的角色相匹配: 可以在Master上用ssh连接到其他节点查看进程
- 在HDFS的根目录下创建一个文件userinfo.dat用于保存用户信息,用户信息包含用户名和密码,用逗号分隔,该文件用于云盘的登录/注册等操作,我们往里面添加两个用户spring和summer,密码都为123456,如下图:
- 每个用户只能访问自己的资源,用以前上数据库系统原理课程的知识来说,就是每一名用户都有自己的文件目录视图,在本次实验中,我的想法是,每一名用户对应一个目录,比如说用户spring对应HDFS中的目录/spring,用户summer对应/summer,用户spring是不能操作用户summer的文件目录的,这样就实现了用户之间的隔离性,确保每位用户访问到的是自己的资源,我们在/spring下创建一个文本文件test.txt,再创建一个目录dd,在dd下也创建一个文本文件test.txt,创建文件可以用touch命令,创建目录可以用mkdir命令,我们递归打印/spring目录下的所有文件查看是否创建成功,到这里就完成了文件的准备工作:
- 打开IDEA,创建SpringBoot项目,配置好Web相关依赖后,加入Hadoop依赖,依赖的version对应当前系统中的Hadoop版本:
<dependency><groupId>org.apache.hadoop</groupId><artifactId>hadoop-client</artifactId><version>3.3.2</version></dependency>
- 项目的结构如图: 关键的文件及目录说明如下表所示:
名称类型目录Controller目录用于存放Controller类FileSystemControllerJava类用于接收前端的请求,从而进行请求处理以及HDFS操作的Controller类PageControllerJava类用于接收前端请求,实现网页跳转的Controller类domain目录用于存放数据类HDFSFileJava类用于描述HDFS文件信息的数据类,含有三个字段,name表示名称,date表示修改日期,type表示类型(文件/目录)YunpanHadoopApplicationJava类用于启动Springboot项目的类resources.static目录用于存放静态资源的目录,本项目的前端的所有静态资源,包括CSS、JS、图片等,都放在此目录下application.properties配置文件SpringBoot配置文件webapp.WEB-INF目录存放JSP页面pom.xml配置文件Maven配置文件………
下面围绕页面来进行功能讲解,本项目页面数量很少,只有3个页面:
- 由于操作HDFS,需要频繁创建FileSystem实例,所以我把创建FileSystem实例写成了一个函数,方便以后调用:
//实例化Configuration和FileSystempublicFileSystemgetFileSystem()throwsIOException{Configuration conf =newConfiguration();
conf.set("fs.defaultFS","hdfs://192.168.170.111:9000");returnFileSystem.get(conf);}
- 另外本web项目全程session绑定,用户登陆后就设置名为LOGIN_STATUS的session,用户退出登录则删除该session。同时也设置一个名为path的session,用于记录当前用户访问的HDFS目录地址
//当前登录状态检查publicbooleanloginStatusCheck(HttpSession session){return session.getAttribute("LOGIN_STATUS")!=null;}
- index.jsp页面以及register.jsp页面 index.jsp页面全貌: 点击“没有账号?点击此处注册 →”按钮,跳转到注册页面register.jsp,跳转功能用PageController类实现,对应的跳转语句: 前端(前端语句只示例一次):
<divclass="text-center p-t-136"><aclass="txt2"href="<%=request.getContextPath()%>/jumpToRegisterPage">
没有账号?点击此处注册
<iclass="fa fa-long-arrow-right m-l-5"aria-hidden="true"></i></a></div>
后台:
@RequestMapping("/jumpToRegisterPage")publicStringjumpToRegisterPage(){return"register";}
跳转到register.jsp页面,页面全貌:
输入用户名,密码以及确认密码,每一项都不能为空,前端做了约束:
两次密码必须输入一致,后台做了约束,否则注册不成功:
执行注册操作,用户名为test,密码为test,后台获取前台的数据,注册操作相关代码:
//注册@RequestMapping("/register")publicStringregister(HttpServletRequest request,Model model)throwsIOException{String username = request.getParameter("username");String userpasswd = request.getParameter("userpasswd");String userpasswd_confirm = request.getParameter("userpasswd_confirm");if(!userpasswd.equals(userpasswd_confirm)){
model.addAttribute("status","两次密码输入不一致!");return"register";}if(userExistCheck(username)){
model.addAttribute("status","用户已存在!");return"register";}insertUserInfoToFile(username, userpasswd);mkdir("/"+ username);
model.addAttribute("status","注册成功,请登录!");return"index";}
注册的时候要检查该用户是否已经存在,调用userExistCheck方法来检查:
//判断用户是否已经注册publicbooleanuserExistCheck(String username)throwsIOException{FileSystem fs =getFileSystem();Path srcPath =newPath("/userinfo.dat");FSDataInputStream in = fs.open(srcPath);BufferedReader reader =newBufferedReader(newInputStreamReader(in));String line ="";while((line = reader.readLine())!=null){String[] userinfo = line.split(",");if(userinfo[0].equals(username)){
fs.close();returntrue;}}
fs.close();returnfalse;}
若用户之前没有注册过,则允许该用户注册,注册时把用户信息写入到userinfo.dat中,调用insertUserInfoToFile方法:
//把新注册的用户信息插入到userinfo.dat中publicvoidinsertUserInfoToFile(String username,String userpasswd)throwsIOException{FileSystem fs =getFileSystem();Path srcPath =newPath("/userinfo.dat");FSDataOutputStream out = fs.append(srcPath);String userinfo = username +","+ userpasswd +"\n";
out.write(userinfo.getBytes(StandardCharsets.UTF_8));
out.close();
fs.close();}
同时在HDFS中创建该用户的个人目录/test,调用mkdir方法:
//创建目录核心操作publicvoidmkdir(String path)throwsIOException{FileSystem fs =getFileSystem();Path srcPath =newPath(path);
fs.mkdirs(srcPath);
fs.close();}
注册成功后,跳转到登录页面,并且给出提示要求登录:
观察userinfo.dat文件,发现添加了新注册的用户信息:
观察HDFS根目录,发现添加了test用户的个人目录:
下面我们用spring用户登录,输入用户名spring,密码123456,点击登录按钮,把相关表单数据提交到Controller的login方法处理:
//登录@RequestMapping("/login")publicStringlogin(HttpServletRequest request,HttpSession session,Model model)throwsIOException{String username = request.getParameter("username");String userpasswd = request.getParameter("userpasswd");if(!loginCheck(username, userpasswd)){
model.addAttribute("status","用户名或密码错误!");return"index";}
session.setAttribute("LOGIN_STATUS", username);
session.setAttribute("path","/"+ username);
model.addAttribute("currentpath","/"+ username);
model.addAttribute("filelist",getFileList("/"+ username));return"myfiles";}
登录时后台会检查用户名和密码是否正确,调用loginCheck方法:
//检查用户名和密码是否正确publicbooleanloginCheck(String username,String userpasswd)throwsIOException{FileSystem fs =getFileSystem();Path srcPath =newPath("/userinfo.dat");FSDataInputStream in = fs.open(srcPath);BufferedReader reader =newBufferedReader(newInputStreamReader(in));String line ="";while((line = reader.readLine())!=null){String[] userinfo = line.split(",");if(userinfo[0].equals(username)&& userinfo[1].equals(userpasswd)){
fs.close();returntrue;}}
fs.close();returnfalse;}
若登录不成功,则刷新登陆页面,并给出文字提示:
若登陆成功,则设置session,LOGIN_STATUS的值为当前用户的用户名,path设置为用户的个人目录,然后跳转到个人网盘页面myfiles.jsp
9. myfiles.jsp页面:
个人网盘页面,页面全貌:
该页面有上传文件按钮,表单左上角显示当前所在路径,表单第一行提供返回上级目录功能,然后就是文件列表,若为目录,则在类型列显示目录,若为文件,则在类型列显示文件,另外显示文件或目录的创建时间,最右边一列提供删除文件或目录的功能,左下角提供创建目录/退出登录以及注销账户的功能
当登录成功后,跳转到个人网盘页面,在页面打印用户个人目录下的文件,我们用的是spring用户,所以我们打印HDFS中/spring路径下的所有文件和目录,以及相关的信息,由于文件创建时间在HDFS中用的是时间戳表示,所以我们要先转换为我们方便看的时间,格式为年-月-日 时:分:秒,每一个文件或者目录相关信息我都放在HDFSFile数据类里面:
importlombok.Data;@DatapublicclassHDFSFile{privateString name;privateString date;privateString type;}
通过遍历得到文件和目录列表,用List类型封装HDFSFile数据,然后我们要保证目录始终在文件列表的前面,所以我排了个序,得到最终的文件列表,该方法名为getFileList方法:
//获取特定路径的所有文件publicList<HDFSFile>getFileList(String path)throwsIOException{FileSystem fs =getFileSystem();SimpleDateFormat format =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");List<HDFSFile> fileList =newArrayList<>();FileStatus[] fileStatuses = fs.listStatus(newPath(path));for(FileStatus fileStatus: fileStatuses){HDFSFile file =newHDFSFile();
file.setName(fileStatus.getPath().getName());
file.setDate(format.format(fileStatus.getModificationTime()));if(fileStatus.isDirectory())
file.setType("目录");else
file.setType("文件");
fileList.add(file);}//排序,目录放前面,文件放后面Collator collator =Collator.getInstance(Locale.CHINA);
fileList.sort((f1, f2)->(collator.compare(f1.getType(), f2.getType())));return fileList;}
当点击文件列表中的目录的时候,改变session,把path的值变成当前目录,然后刷新页面,显示你选的目录中的文件,若点击文件列表中的文件,则下载该文件,处理文件和目录请求用Controller的fileHandle方法:
//处理文件和目录请求,如果是目录,则跳转到对应目录中去,如果是文件,则下载文件@RequestMapping("/fileHandle")publicStringfileHandle(HttpSession session,HttpServletRequest request,Model model,HttpServletResponse response)throwsIOException{if(!loginStatusCheck(session)){
model.addAttribute("status","此操作需要你登录!");return"index";}String filename = request.getParameter("filename");String filetype = request.getParameter("type");if(filetype.equals("目录")){
session.setAttribute("path", session.getAttribute("path").toString()+"/"+ filename);
model.addAttribute("currentpath", session.getAttribute("path").toString());
model.addAttribute("filelist",getFileList(session.getAttribute("path").toString()));return"myfiles";}FileSystem fs =getFileSystem();FSDataInputStream in = fs.open(newPath(session.getAttribute("path").toString()+"/"+ filename));
response.setHeader("Content-disposition","attachment; filename="+URLEncoder.encode(filename,"UTF-8"));BufferedInputStream bufferedInputStream =newBufferedInputStream(in);BufferedOutputStream bufferedOutputStream =newBufferedOutputStream(response.getOutputStream());byte[] buff =newbyte[2048];int bytesRead;while((bytesRead = bufferedInputStream.read(buff,0, buff.length))!=-1)
bufferedOutputStream.write(buff,0, bytesRead);
bufferedInputStream.close();
bufferedOutputStream.close();
fs.close();returnnull;}
若为下载,需要触发浏览器事件,所以要改变header,让浏览器执行下载操作:
若为目录跳转,则用新路径重新运行getFileList方法,刷新页面:
因为涉及HDFS核心操作,所以若检测到LOGIN_STATUS session不存在,则跳转到登陆页面要求用户登录,后面的核心操作也同样设定了安全约束:
点击返回上层目录,则更新session,改变path,去掉path的最后一个/右边内容以及最后一个/,然后重新调用getFileList方法,刷新页面,请求交给Controller的back方法处理:
//返回上一级目录@RequestMapping("/back")publicStringback(HttpSession session,HttpServletRequest request,Model model)throwsIOException{if(!loginStatusCheck(session)){
model.addAttribute("status","此操作需要你登录!");return"index";}String currentpath = session.getAttribute("path").toString();String[] pathsplit = currentpath.split("/");if(pathsplit.length ==2){
model.addAttribute("warning","alert(\"当前已经是根目录!\");");
model.addAttribute("currentpath", session.getAttribute("path").toString());
model.addAttribute("filelist",getFileList(session.getAttribute("path").toString()));return"myfiles";}StringBuilder sb =newStringBuilder();for(int i =1; i < pathsplit.length -1; i++)
sb.append("/").append(pathsplit[i]);
session.setAttribute("path", sb.toString());
model.addAttribute("currentpath", session.getAttribute("path").toString());
model.addAttribute("filelist",getFileList(session.getAttribute("path").toString()));return"myfiles";}
若当前已经是用户个人目录的最上层,若还点返回上层目录,则弹出警告,并刷新页面:
点击test.txt最右边的删除按钮,则删除文件,若点击的是目录的删除,则会顺带删除该目录下面的所有文件,删除文件或目录的请求交给Controller的deleteFile方法处理,然后核心操作交给delete方法处理:
//删除文件或目录@RequestMapping("/deleteFile")publicStringdeleteFile(HttpSession session,HttpServletRequest request,Model model)throwsIOException{if(!loginStatusCheck(session)){
model.addAttribute("status","此操作需要你登录!");return"index";}String filename = request.getParameter("filename");delete(session.getAttribute("path").toString()+"/"+ filename);
model.addAttribute("warning","alert(\"删除成功!\");");
model.addAttribute("currentpath", session.getAttribute("path").toString());
model.addAttribute("filelist",getFileList(session.getAttribute("path").toString()));return"myfiles";}
//删除文件或目录核心操作publicvoiddelete(String path)throwsIOException{FileSystem fs =getFileSystem();Path srcPath =newPath(path);
fs.delete(srcPath,true);
fs.close();}
删除成功后弹出提示框,然后刷新页面:
点击创建目录按钮,则弹出提示框:
输入新目录的名称,输入不能为空,否则会弹出警示:
若输入目录正确,则创建目录,刷新页面,创建目录请求交给Controller的makeDirectory方法处理:
//创建目录@RequestMapping("/makeDirectory")publicStringmakeDirectory(HttpSession session,HttpServletRequest request,Model model)throwsIOException{String dirname = request.getParameter("dirname");if(fileExist(session.getAttribute("path").toString()+"/"+ dirname))
model.addAttribute("warning","alert(\"该目录已存在,请重新输入目录名!\");");else{mkdir(session.getAttribute("path").toString()+"/"+ dirname);
model.addAttribute("warning","alert(\"创建成功!\");");}
model.addAttribute("currentpath", session.getAttribute("path").toString());
model.addAttribute("filelist",getFileList(session.getAttribute("path").toString()));return"myfiles";}
创建成功后弹出提示:
页面刷新后可以看到刚刚创建的目录:
若要创建的目录已经存在,则弹出警告,刷新页面:
检测目录或文件是否存在,用fileExist方法处理:
//判断文件或目录是否存在publicbooleanfileExist(String path)throwsIOException{FileSystem fs =getFileSystem();boolean isExist = fs.exists(newPath(path));
fs.close();return isExist;}
选择文件,然后点击上传,若没有选择文件直接点击上传,前台做了相关约束,不允许提交请求:
点击选择文件按钮,弹出文件选择框,选中文件,点击打开,则网页显示文件的名称:
点击上传,由于上传需要时间,在上传过程中,不可进行浏览器的刷新等其他操作,很可能会造成文件上传失败,所以在等待上传的时间里,页面会显示警示文字:
文件上传成功后,弹出提示框提示用户:
然后页面刷新,文件已经在文件列表中:
用命令行查看/spring目录,文件已经上传到HDFS中:
若上传的文件已经在HDFS中了,如果重复上传,则不允许执行上传操作,并弹出警告,刷新页面:
上传文件操作请求交给Controller的upload方法处理,这里用了commons-io以及commons-fileupload插件,需要在Maven POM文件中导入相关依赖:
<dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.4</version></dependency><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.11.0</version></dependency>
//文件上传@RequestMapping("/upload")publicStringupload(HttpSession session,HttpServletRequest request,Model model)throwsFileUploadException,IOException{FileSystem fs =getFileSystem();boolean isExist =false;DiskFileItemFactory factory =newDiskFileItemFactory();ServletFileUpload upload =newServletFileUpload(factory);
upload.setHeaderEncoding("UTF-8");List<FileItem> list = upload.parseRequest(request);for(FileItem item: list){if(!item.isFormField()){String filename = item.getName();if(filename ==null|| filename.trim().equals(""))continue;
isExist =fileExist(session.getAttribute("path").toString()+"/"+ filename);if(isExist)continue;
fs =getFileSystem();InputStream in = item.getInputStream();FSDataOutputStream out = fs.create(newPath(session.getAttribute("path").toString()+"/"+ filename));byte[] buff =newbyte[2048];int bytesRead =0;while((bytesRead = in.read(buff))>0)
out.write(buff,0, bytesRead);
in.close();
out.close();
item.delete();}}
model.addAttribute("currentpath", session.getAttribute("path").toString());
model.addAttribute("filelist",getFileList(session.getAttribute("path").toString()));if(isExist)
model.addAttribute("warning","alert(\"网盘有同名文件,建议更改文件名!\");");else
model.addAttribute("warning","alert(\"文件上传成功!\");");
fs.close();return"myfiles";}
判断文件是否存在使用上面提到的fileExist方法处理
点击退出登录按钮,则清空LOGIN_STATUS session,然后返回登陆页面,要求客户登录:
对应的退出登录请求交给Controller的logout方法处理:
//退出登录@RequestMapping("/logout")publicStringlogout(HttpSession session){
session.removeAttribute("path");
session.removeAttribute("LOGIN_STATUS");return"index";}
用户点击注销账户按钮,则弹出选择框让用户再次确认是否注销账户:
当用户点击确认后,则开始执行注销用户任务,注销成功后,弹出提示框提示用户:
注销用户请求交给Controller的deleteUser方法处理:
//注销账户@RequestMapping("/deleteUser")publicStringdeleteUser(HttpSession session,Model model)throwsIOException{String username = session.getAttribute("path").toString().split("/")[1];delete("/"+ username);removeUserFromFile(username);
model.addAttribute("warning","alert(\"注销成功!\");");
session.removeAttribute("path");
session.removeAttribute("LOGIN_STATUS");return"index";}
首先调用上面提到的delete方法,删除该用户的个人目录/spring,然后修改userinfo.dat,删除该用户的账号密码信息,修改userinfo.dat使用removeUserFromFile方法:
//从userinfo.dat中移除用户publicvoidremoveUserFromFile(String username)throwsIOException{FileSystem fs =getFileSystem();Path srcPath =newPath("/userinfo.dat");FSDataInputStream in = fs.open(srcPath);BufferedReader reader =newBufferedReader(newInputStreamReader(in));StringBuilder sb =newStringBuilder();String line ="";while((line = reader.readLine())!=null){String[] userinfo = line.split(",");if(userinfo[0].equals(username))continue;
sb.append(line).append("\n");}
in.close();FSDataOutputStream out = fs.create(srcPath,true);
out.write(sb.toString().getBytes(StandardCharsets.UTF_8));
out.close();
fs.close();}
查看HDFS根目录,发现spring用户的个人目录已经被移除:
查看userinfo.dat文件,发现spring用户的个人信息已经被删除:
然后清空session,返回登陆页面,完成注销账户的所有操作
至此本云盘的所有功能以及代码都说明完毕!
存在的一些问题
后端的话,我觉得优化空间还是很大的,代码的逻辑上应该是可以优化的
关于前端的话,我属于前端小白来着,非常不会写前端
我觉得文件上传可以写一个进度条,然后一些交互通讯其实可以用AJAX来处理
版权归原作者 Wolf.Genn 所有, 如有侵权,请联系我们删除。