版本说明: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。

JAVAAPI在hdfs创建目录 hdfs java api怎么创建_JAVAAPI在hdfs创建目录

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>

JAVAAPI在hdfs创建目录 hdfs java api怎么创建_JAVAAPI在hdfs创建目录_02

🌈 注意到,在依赖的 version 没有直接写版本号,这是应为如果添加很多依赖,就需要保证版本对应,所以统一在 properties 标签中定义了与 Hadoop 版本相同的版本号,避免后期用错版本。

1.4 Java类

到这里,不妨先瞄一眼后面会用哪些类。

类名

描述

org.apache.hadoop.con.Configuration

该类的对象封装了客户端或者服务器的配置

org.apache.hadoop.fs.FileSystem

该类的对象是一个文件系统对象,可以用该对象的一些方法来对文件进行操作

org.apache.hadoop.fs.FileStatus

该类用于向客户端展示系统中文件和目录的元数据,具体包括文件大小、块大小、副本信息、所有者、修改时间等

org.apache.hadoop.fs.FSDatalnputStream

该类是 HDFS 中的输入流,用于读取 Hadoop 文件

org.apache.hadoop.fs.FSDataOutputStream

该类是 HDFS 中的输出流,用于写 Hadoop 文件

org.apache.hadoop.fs.Path

该类用于表示 Hadoop 文件系统中的文件或者目录的路径

org.apache.hadoop.fs.permission.FsPermission

该类用于获取文件或目录的权限

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();

JAVAAPI在hdfs创建目录 hdfs java api怎么创建_JAVAAPI在hdfs创建目录_03

这是因为 FileSystem 类中包含抽象方法,所以它是一个抽象类。它长这样:

@Public
@Stable
public abstract class FileSystem extends Configured implements Closeable {...}

所以一个抽象类不能直接被实例化,但可以通过内部类的方法实现其抽象方法,就像线程那块用 Thread 实例化的时候需要实现 run 方法

我们进入到 FileSystem 类中,在 IDEA 左侧的 Structure 中查看该类的结构,并找到能够返回 FileSystem 实例的方法。使用较多的是下面选中的这个 get 方法。

JAVAAPI在hdfs创建目录 hdfs java api怎么创建_JavaAPI_04

2.2 FileSystem.get

FileSystem.get 方法通常需要传入三个参数,分别是URI、Concentration、UserName

参数

描述

URI

统一资源标识符

Configuration

HDFS的相关配置

UserName

用户名

JAVAAPI在hdfs创建目录 hdfs java api怎么创建_运维_05

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 创建目录指定权限

当需要指定权限时,我们需要提供第二个参数。

权限如下:

JAVAAPI在hdfs创建目录 hdfs java api怎么创建_服务器_06

案例演示:

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 :下载

JAVAAPI在hdfs创建目录 hdfs java api怎么创建_运维_07

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 的文件到本地,只不过默认 delSrctrue

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 ❤️