版本说明:JDK1.8、hadoop-2.7.7
HDFS 具有两种操作方式:命令行、Java API
命令行操作参考:Hadoop HDFS
下面我们就来聊聊如何在 IDEA 上操作 HDFS !
1. IDEA HDFS
在 IDEA 中操作 HDFS 需要一个项目管理和构建工具 Maven,没有配置 Maven 的话可以使用 IDEA 自带的,想动手配置的话可以参考这篇:Windows系统maven安装配置
还需要导入相关的依赖项,不知如何导依赖,可参考这篇的前半部分:MyBatis初识+创建项目
1.1 创建 Maven 项目
Maven 给的 archetype 都不用选,只需创建一个空的项目。
步骤: File --> New --> Project --> 侧边栏选择 Maven --> 修改项目名 --> Finish
1.2 创建子项目
🌈 此步骤也可以省略。
日常应用中我们会在一个项目中添加很多子项目,甚至在子项目中添加子项目。这时我们需要删除新建项目的 src
目录。右键项目名 New Module。
1.3 添加依赖
使得依赖:hadoop-client
,将依赖添加至项目的 pom.xml
,有子项目的话添加在其父项目的该文件中就OK。
配完依赖记得点击图上的更新图标,让修改生效。
hadoop-client: Hadoop 客户端
<properties>
...
...
<hadoop-version>2.7.7</hadoop-version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>${hadoop-version}</version>
</dependency>
</dependencies>
</dependencyManagement>
🌈 注意到,在依赖的 version
没有直接写版本号,这是应为如果添加很多依赖,就需要保证版本对应,所以统一在 properties
标签中定义了与 Hadoop 版本相同的版本号,避免后期用错版本。
1.4 Java类
到这里,不妨先瞄一眼后面会用哪些类。
类名 | 描述 |
| 该类的对象封装了客户端或者服务器的配置 |
| 该类的对象是一个文件系统对象,可以用该对象的一些方法来对文件进行操作 |
| 该类用于向客户端展示系统中文件和目录的元数据,具体包括文件大小、块大小、副本信息、所有者、修改时间等 |
| 该类是 HDFS 中的输入流,用于读取 Hadoop 文件 |
| 该类是 HDFS 中的输出流,用于写 Hadoop 文件 |
| 该类用于表示 Hadoop 文件系统中的文件或者目录的路径 |
| 该类用于获取文件或目录的权限 |
2. FileSystem
FileSystem 类是 HDFS Java操作的入口,该类就表示了 HDFS 文件系统。
下面通过一个简单实例先来获取到 FileSystem ,而后看看如何实现命令行模式下 hdfs dfs -ls /
的功能。
[root@node1 ~]# hdfs dfs -ls /
Found 4 items
-rw-r--r-- 3 root supergroup 18 2021-11-16 17:57 /demo1.txt
-rw-r--r-- 3 root supergroup 18 2021-11-16 17:57 /demo2.txt
drwxr-xr-x - root supergroup 0 2021-11-16 17:56 /filedir1
-rw-r--r-- 3 root supergroup 218720521 2021-11-16 17:49 /hadoop-2.7.7.tar.gz
2.1 FileSystem类
如果我们直接获取 FileSystem 实例,是有问题的:
new FileSystem();
这是因为 FileSystem 类中包含抽象方法,所以它是一个抽象类。它长这样:
@Public
@Stable
public abstract class FileSystem extends Configured implements Closeable {...}
所以一个抽象类不能直接被实例化,但可以通过内部类的方法实现其抽象方法,就像线程那块用 Thread 实例化的时候需要实现
run
方法
我们进入到 FileSystem 类中,在 IDEA 左侧的 Structure 中查看该类的结构,并找到能够返回 FileSystem 实例的方法。使用较多的是下面选中的这个 get
方法。
2.2 FileSystem.get
FileSystem.get
方法通常需要传入三个参数,分别是URI、Concentration、UserName
参数 | 描述 |
URI | 统一资源标识符 |
Configuration | HDFS的相关配置 |
UserName | 用户名 |
2.3 FileSystem实例
通过如下方式可获取到 FileSystem 实例,并且需要抛出异常。
public static void main(String[] args) throws IOException, InterruptedException {
URI uri = URI.create("hdfs://node1:8020");
Configuration conf = new Configuration();
String user = "root";
FileSystem fileSystem = FileSystem.get(uri, conf, user);
}
启动 HDFS (start-dfs.sh
),运行上面程序,我们可以输出看下 fileSystem 的内容。
DFS[DFSClient[clientName=DFSClient_NONMAPREDUCE_1546898099_1, ugi=root (auth:SIMPLE)]]
2.4 FileStatus实例
FileSystem 实例出来就可是实现文件上传、下载、新建、删除等等很多功能了,这个放后面说。
通过 FileSystem 实例化的对象可以获取到 FileStatus ,改类是文件的状态(属性)信息。
通过如下方式获取 FileStatus:
FileStatus[] fileStatuses = fileSystem.listStatus(new Path("/"));
🌈 listStatus
方法的缩写就像是命令行下的 ls
,后面是放一个根目录。
FileStatus 中包含如下属性:
@Public
@Stable
public class FileStatus implements Writable, Comparable {
private Path path;
private long length;
private boolean isdir;
private short block_replication;
private long blocksize;
private long modification_time;
private long access_time;
private FsPermission permission;
private String owner;
private String group;
private Path symlink;
}
2.5 实现 hdfs dfs -ls /
以上我们获取到了 FileSystem 实例,能够实现 HDFS 上的各种操纵了,又获取到了 FileStatus,那么通过遍历 FileStatus 中的对象,就能获取到具体的文件及相关信息了。
FileStatus[] fileStatuses = fileSystem.listStatus(new Path("/"));
for (FileStatus fileStatus : fileStatuses) {
System.out.println(fileStatus);
}
FileStatus{path=hdfs://node1:8020/demo1.txt; isDirectory=false...
FileStatus{path=hdfs://node1:8020/demo2.txt; isDirectory=false...
FileStatus{path=hdfs://node1:8020/filedir1; isDirectory=true...
FileStatus{path=hdfs://node1:8020/hadoop-2.7.7.tar.gz; isDirectory=false..
可以看到,将所有的属性都输出了,而且还不是我们想要的 ls
出的格式。
-rw-r--r-- 3 root supergroup 18 2021-11-16 17:57 /demo1.txt
-rw-r--r-- 3 root supergroup 18 2021-11-16 17:57 /demo2.txt
drwxr-xr-x - root supergroup 0 2021-11-16 17:56 /filedir1
-rw-r--r-- 3 root supergroup 218720521 2021-11-16 17:49 /hadoop-2.7.7.tar.gz
2.5.1 获取文件类型
第一个字符表示文件类型,-
表示普通文件、d
表示目录…
-rw-r--r--
drwxr-xr-x
运用三元运算符判断文件属性;
String fileType = fileStatus.isDirectory() ? "d" : "-";
🔵 输出:
-
-
d
-
2.5.2 获取文件权限
String permission = fileStatus.getPermission().toString();
🔵 输出:
rw-r--r--
rw-r--r--
rwxr-xr-x
rw-r--r--
2.5.3 获取副本数量
short replication = fileStatus.getReplication();
🔵 输出:
3
3
0
3
2.5.4 获取用户、用户组
String owner = fileStatus.getOwner();
String group = fileStatus.getGroup();
🔵 输出:
3
3
0
3
2.5.5 获取文件大小
long len = fileStatus.getLen();
🔵 输出:
18
18
0
218720521
2.5.5.1 格式优化
上面的输出是左对齐,而我们要的是右对齐,所以这个格式还需要优化。
实现方法:让其右对齐,就需要知道最大值的长度,然后在短的前面补充空格。
2.5.5.1.1 获取最大值
实现方法:想要获取最大的值,就需要先将 filestatuses 遍历一遍,然后比较大小,找到最大的。
long maxLen = 0;
for (FileStatus status : fileStatuses) {
long Len = status.getLen();
if (Len > maxLen) {
maxLen = Len;
}
}
🔵 输出:
218720521
上面的代码可简写为:
long maxLen = Arrays.stream(fileStatuses)
.map(FileStatus::getLen)
.max(Long::compareTo)
.get();
2.5.5.1.2 补充空格
实现方法:获取到最大值后,就需要根据最大值的长度,给短的补充空格。
long len = fileStatus.getLen();
long maxLen = Arrays.stream(fileStatuses)
.map(FileStatus::getLen)
.max(Long::compareTo)
.get();
String L = String.valueOf(len);
Integer longL = Integer.valueOf(String.valueOf(len).length());
String maxL = String.valueOf(maxLen);
Integer longMaxL = Integer.valueOf(String.valueOf(maxLen).length());
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < longMaxL - longL; i++) {
stringBuilder.append(" ");
}
System.out.println(stringBuilder.append(L).toString());
🔵 输出:
18
18
0
218720521
2.5.5.1.2 提取方法
String s = appendStr(len, maxLen, " ");
public static String appendStr(long len, long maxLen, String step) {
String L = String.valueOf(len);
Integer longL = Integer.valueOf(String.valueOf(len).length());
Integer longMaxL = Integer.valueOf(String.valueOf(maxLen).length());
StringBuilder builder = new StringBuilder();
for (int i = 0; i < longMaxL - longL; i++) {
builder.append(step);
}
return builder.append(L).toString();
}
2.5.5.1.3 优化方法
String s = appendStr(len, (maxLen + "").length(), " ");
public static String appendStr(Object src, int len, String step) {
String srcStr = src.toString();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < len - srcStr.length(); i++) {
builder.append(step);
}
return builder.append(srcStr).toString();
}
2.5.6 获取修改时间
SimpleDateFormat dataFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
String dataTime = dataFormat.format(new Date(fileStatus.getModificationTime()));
🔵 输出:
2021-11-16 17:57
2021-11-16 17:57
2021-11-16 17:56
2021-11-16 17:49
2.5.6.1 代码优化
SimpleDateFormat dataFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
String dataTime = dataFormat.format(new Date(fileStatus.getModificationTime()));
这样写在代码中显得臃肿,而且不利于代码复用,所以我们将其提成一个方法。
String dataTime = formatModificationTime(fileStatus);
private static String formatModificationTime(FileStatus fileStatus) {
long modificationTime = fileStatus.getModificationTime();
SimpleDateFormat dataFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
String dataTime = dataFormat.format(modificationTime);
return dataTime;
}
2.5.7 获取文件
Path path = fileStatus.getPath();
🔵 输出:
hdfs://node1:8020/demo1.txt
hdfs://node1:8020/demo2.txt
hdfs://node1:8020/filedir1
hdfs://node1:8020/hadoop-2.7.7.tar.gz
显示这不是我们想要的,我们可以截取字符串,也可以通过 path.getName()
直接获取到文件名,而后拼接一个字符串 "/"
。
demo1.txt
demo2.txt
filedir1
hadoop-2.7.7.tar.gz
2.5.7.1 截取实现
String pathStr = fileStatus.getPath().toString();
int pos = pathStr.indexOf("/", 7); // 从7个字符开始,找到 “/” 首次出现的下标位置
String absPath = pathStr.substring(pos); // 从该位置开始,向后截取字符串
🔵 输出:
/demo1.txt
/demo2.txt
/filedir1
/hadoop-2.7.7.tar.gz
2.5.8 整合输出
StringBuilder stringBuilder = new StringBuilder();
stringBuilder
.append(fileType)
.append(permission).append("\t")
.append(replication).append("\t")
.append(owner).append(" : ")
.append(group).append("\t")
.append(appendStr(len, maxLen, " ")).append("\t")
.append(dataTime).append("\t")
.append(absPath);
System.out.println(stringBuilder.toString());
🔵 输出:
-rw-r--r-- 3 root : supergroup 18 2021-11-16 17:57 /demo1.txt
-rw-r--r-- 3 root : supergroup 18 2021-11-16 17:57 /demo2.txt
drwxr-xr-x 0 root : supergroup 0 2021-11-16 17:56 /filedir1
-rw-r--r-- 3 root : supergroup 218720521 2021-11-16 17:49 /hadoop-2.7.7.tar.gz
3. JavaAPI 操作
3.1 创建目录
3.1.1 创建单极目录
方法:fileSystem.mkdirs(<Path>)
案例演示:
fileSystem.mkdirs(new Path("/newDir1"));
🔵 测试结果:
boolean exists = fileSystem.exists(new Path("/newDir1"));
System.out.println(exists);
/**
* 返回值:
* true
*/
3.1.2 创建目录指定权限
当需要指定权限时,我们需要提供第二个参数。
权限如下:
案例演示:
fileSystem.mkdirs(new Path("/newDir2"),
new FsPermission(FsAction.ALL, FsAction.EXECUTE, FsAction.NONE));
🔵 测试结果:
# 默认权限创建目录
drwxr-xr-x 0 root : supergroup 0 2021-11-18 19:45 /newDir1
drwx--x--- 0 root : supergroup 0 2021-11-18 19:45 /newDir2
3.1.3 源码窥探
public boolean mkdirs(Path f) throws IOException {
return this.mkdirs(f, FsPermission.getDirDefault());
}
public abstract boolean mkdirs(Path var1, FsPermission var2) throws IOException;
FileSystem 类中有两个创建目录的方法,并且还有一个抽象方法。
使用第一个方法,给一个路径即可完成创建,但源码中为自己提供了默认的权限(FsPermission.getDirDefault()
)。
public static FsPermission getDirDefault() {
return new FsPermission((short)511);
}
public static FsPermission getFileDefault() {
return new FsPermission((short)438);
}
⚠️ 以上可以看到默认的权限,但不要误以为是我们 chown
的那样,明显不一样!
3.1.4 多级目录
创建多级文件时,就需要用到上面说的那个抽象方法。
该方法在 DistributedFileSystem
类中的实现如下:
// 创建单极目录
public boolean mkdir(Path f, FsPermission permission) throws IOException {
return this.mkdirsInternal(f, permission, false);
}
// 创建多级目录
public boolean mkdirs(Path f, FsPermission permission) throws IOException {
return this.mkdirsInternal(f, permission, true);
}
我们又看到了第三个参数,单级目录默认为 false
,多级目录默认为 true
,这个参数表示是否创建父目录 createParent
。
3.2 删除文件/目录
3.2.1 删除单极目录
方法:fileSystem.delete(Path path)
案例演示:
fileSystem.delete(new Path("/newDir1"));
3.2.2 删除多级目录
方法:fileSystem.delete(Path path, boolean recursive)
说明:选自 DistributedFileSystem
中的实现方法,第二个参数指定是否递归删除
案例演示:
fileSystem.delete(new Path("/newDir2"), true);
3.2.3 窥见源码
public boolean delete(Path f) throws IOException {
return this.delete(f, true);
}
public abstract boolean delete(Path var1, boolean var2) throws IOException;
FileSystem 类中有两个删除目录的方法,并且还有一个抽象方法。
给定一个参数时或两个参数时,其实调用的同一个方法。
3.3 文件重命名
3.3.1 文件重命名
方法:fileSystem.rename(<oldPath>, <newPath>)
案例演示:
fileSystem.rename(new Path("/newDir2"),
new Path("/newDir3"));
🔵 测试结果:
drwxr-xr-x 0 root : supergroup 0 2021-11-18 19:45 /newDir1
drwx--x--- 0 root : supergroup 0 2021-11-18 19:45 /newDir3
3.3.2 窥见源码
FileSystem 的 rename
方法为抽象方法,DistributedFileSystem 中的实现如下:
// FileSystem
public abstract boolean rename(Path var1, Path var2) throws IOException;
// DistributedFileSystem
public boolean rename(Path src, Path dst) throws IOException {
this.statistics.incrementWriteOps(1);
Path absSrc = this.fixRelativePart(src); // adsSource 源路径
Path absDst = this.fixRelativePart(dst); // adsDestination 目标路径
3.4 文件上传
文件上传/下载都是使用 copy 方法,下面都使用参数最多的方法。
copyFromLocalFile
:上传
copyToLocalFile
:下载
3.4.1 文件上传API
方法:fileSystem.copyFromLocalFile(boolean delSrc, boolean overwrite, Path src, Path dst)
说明:选自 DistributedFileSystem
中的实现方法
演示案例:
fileSystem.copyFromLocalFile(true
, true
, new Path("C:\\Users\\Administrator\\Desktop\\file1.txt")
, new Path("/"));
🔵 测试结果:
# 文件上传后,源文件将被删除
-rw-r--r-- 3 root : supergroup 0 2021-11-18 21:53 /file1.txt
3.4.2 窥见源码
FileSystem 上传文件的四个方法如下,也是在调用参数最多的方法,只不过参数默认为 true
。
public void moveFromLocalFile(Path src, Path dst) throws IOException {
this.copyFromLocalFile(true, src, dst);
}
public void copyFromLocalFile(boolean delSrc, Path src, Path dst) throws IOException {
this.copyFromLocalFile(delSrc, true, src, dst);
}
public void copyFromLocalFile(boolean delSrc, boolean overwrite, Path[] srcs, Path dst) throws IOException {
Configuration conf = this.getConf();
FileUtil.copy(getLocal(conf), srcs, this, dst, delSrc, overwrite, conf);
}
public void copyFromLocalFile(boolean delSrc, boolean overwrite, Path src, Path dst) throws IOException {
Configuration conf = this.getConf();
FileUtil.copy(getLocal(conf), src, this, dst, delSrc, overwrite, conf);
}
3.5 文件下载
3.5.1 文件下载API
方法:fileSystem.copyToLocalFile(Path src, Path dst)
案例演示:
fileSystem.copyToLocalFile(new Path("/file1.txt")
, new Path("C:\\Users\\Administrator\\Desktop\\"));
3.5.2 窥见源码
FileSystem 上传文件的方法如下,其中包含一个移动 HDFS 的文件到本地,只不过默认 delSrc
为 true
public void copyToLocalFile(Path src, Path dst) throws IOException {
this.copyToLocalFile(false, src, dst);
}
public void moveToLocalFile(Path src, Path dst) throws IOException {
this.copyToLocalFile(true, src, dst);
}
public void copyToLocalFile(boolean delSrc, Path src, Path dst) throws IOException {
this.copyToLocalFile(delSrc, src, dst, false);
}
public void copyToLocalFile(boolean delSrc, Path src, Path dst, boolean useRawLocalFileSystem) throws IOException {
Configuration conf = this.getConf();
FileSystem local = null;
if (useRawLocalFileSystem) {
local = getLocal(conf).getRawFileSystem();
} else {
local = getLocal(conf);
}
FileUtil.copy(this, src, (FileSystem)local, dst, delSrc, conf);
}
3.6 IO 流
文件的上传下载,实际上也是通过输入流输出流实现的,下面我们自己通过 IO 来实现文件的上传和下载。
文件上传:本地文件 --输入流-> HDFS Java客户端 --输出流-> HDFS
文件下载:本地文件 <-输出流-- HDFS Java客户端 <-输入流- HDFS
3.6.1 文件上传
实现方法:
String src = "C:\\Users\\Administrator\\Desktop\\file1.txt";
String dst = "/file2.txt";
upload(fileSystem, src, dst);
private static void upload(FileSystem fileSystem, String src, String dst) throws IOException {
FileInputStream fileInputStream = new FileInputStream(src);
FSDataOutputStream fsDataOutputStream = fileSystem.create(new Path(dst));
IOUtils.copyBytes(fileInputStream, fsDataOutputStream, 4096);
fileInputStream.close();
fileInputStream.close();
fileSystem.close();
}
🌈 copyBytes 方法的第三个参数是缓冲数组的长度,4096就是4KB,刚好是一整页字节的数量。
FSDataOutputStream API:Class FSDataOutputStream
3.6.2 文件下载
实现方法:
String src = "/file2.txt";
String dst = "C:\\Users\\Administrator\\Desktop\\file3.txt";
download(fileSystem, src, dst);
private static void download(FileSystem fileSystem, String src, String dst) throws IOException {
FSDataInputStream fsDataInputStream = fileSystem.open(new Path(src));
FileOutputStream fileOutputStream = new FileOutputStream(dst);
IOUtils.copyBytes(fsDataInputStream, fileOutputStream, 4096);
fileOutputStream.close();
fsDataInputStream.close();
fileSystem.close();
}
FileSystem对象中的
open()
方法返回的是 FSDataInputStream 对象, 而不是标准的java.io类对象,这个类是继承了java.io.DataInputStream
接口的一个特殊类,并支持随机访问,可以从流中的任意位置读取数据。FSDataInputStream API:Class FSDataInputStream
4. 写在最后
使用 Java 操作 HDFS 需要先获取到 FileSystem 的实例,虽然其中有很多抽象方法,但通过使用子类实现的方法都能完成想要的操作。通过 FileSystem 抽象类中方法,可以实现同命令行中相同的功能了,Hadoop 是用 Java 实现的,所以学习使用 Java 实现文件/目录操作,有助于理解命令行中,不同命令的具体实现方法。
❤️ END ❤️