1.背景
在企业开发中,hadoop框架自带的InputFormat不能满足所有的应用场景,需要自定义InputFormat来解决实际问题
2.需求
将多个小文件合并成一个SequenceFile文件(SequenceFile文件是Hadoop用来存储二进制形式的key-value对的文件格式),SequenceFile里面存储着多个文件,存储的形式为文件路径+名称为key,文件内容为value。
3.代码实现
1.FileInputFormat的实现类
package com.zj.practice.mapreduce04.user_defined_InputFormat;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.JobContext;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import java.io.IOException;
// 自定义InputFormat,继承FileInputFormat
// 一次读取一个文件,转化为<key,value>的形式输出
public class WholeInputFormat extends FileInputFormat {
/**
* 设置文件不支持切片,目的是一次读取一个文件(当读取大文件时,这一点是必须的)
*
* @param context
* @param filename
* @return false --> 不支持切片
*/
@Override
protected boolean isSplitable(JobContext context, Path filename) {
return false;
}
/**
* 重写 createRecordReader,实现一次读取一个文件封装为<key,value>
*
* @param inputSplit
* @param taskAttemptContext
* @return
*/
public RecordReader createRecordReader(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
// 创建重写的RecordReader对象
WhileRecordReader whileRecordReader = new WhileRecordReader();
// 初始化,获取切片信息
whileRecordReader.initialize(inputSplit, taskAttemptContext);
// 返回RecordReader对象
return whileRecordReader;
}
}
2.InputFormat的核心方法类
package com.zj.practice.mapreduce04.user_defined_InputFormat;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/* 重写 createRecordReader,实现一次读取一整个文件封装为<key,value>
Text --> key:文件路径+名称
Text --> value:文件内容
*/
public class WhileRecordReader extends RecordReader<Text, Text> {
// 默认没有读取文件
private Boolean isRead = true;
// 文件的路径作为key值
private Text key = new Text();
// 文件的内容作为value值
private Text value = new Text();
// 文件的路径
private Path path;
/**
* 初始化方法,获取切片信息
*
* @param inputSplit 文件对象
* @param taskAttemptContext
*/
public void initialize(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
// 创建文件对象(多态)
FileSplit split = (FileSplit) inputSplit;
// 获取文件路径
path = split.getPath();
}
/**
* 返回是否存在下一组的<key,value>
*
* @return
* @throws IOException
* @throws InterruptedException
*/
public boolean nextKeyValue() throws IOException, InterruptedException {
// 如果没有读取文件,则将key设置为:文件路径+名称
// 将value设置为:文件内容
if (isRead) {
/* ------------------------将key设置为:文件路径+名称----------------- */
this.key.set(path.toString());
// 封装文件对象
// 默认会在被路径前面添加 --> file:/ --> 所以要去掉这个字符串
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(
new FileInputStream(
path.toString().replace("file:/", "")
))
);
/* ------------------------将value设置为:文件内容----------------- */
// 一行一行的读取文件内容,并将value设置为:文件内容
// 用于读取文件内容
String line;
// 用于拼接字符串
StringBuffer stringBuffer = new StringBuffer();
// 遍历文件内容
while (StringUtils.isNotEmpty(line = bufferedReader.readLine())) {
// 读取文件的一行,拼接字符串
stringBuffer.append(line + ";;;");
}
// 将value设置为:文件内容
this.value.set(stringBuffer.toString());
// 读取完毕
this.isRead = false;
return true;
}
return false;
}
/**
* @return 返回当前的key
*/
public Text getCurrentKey() throws IOException, InterruptedException {
return this.key;
}
/**
* @return 返回当前的value
*/
public Text getCurrentValue() throws IOException, InterruptedException {
return this.value;
}
/**
* 获取文件读取进度
*
* @return
*/
public float getProgress() throws IOException, InterruptedException {
// 读取完毕,返回0,否则返回1
return this.isRead ? 0 : 1;
}
/**
* 释放资源
*/
public void close() throws IOException {
}
}
3.driver
package com.zj.practice.mapreduce04.user_defined_InputFormat;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat;
import java.io.IOException;
public class WhileDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
// 获取配置文件
Configuration configuration = new Configuration();
// 封装job任务对象
Job job = Job.getInstance(configuration);
// Jar包本地路径
job.setJarByClass(WhileDriver.class);
// 设置自定义的InputFormat类
job.setInputFormatClass(WholeInputFormat.class);
// 设置输出的文件类型
job.setOutputFormatClass(SequenceFileOutputFormat.class);
// 不加载mapper、reduce类的话,默认继承父类,输入什么就输出什么
// 设置最终输出的数据类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
// 设置数据源、数据路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 返回结果
boolean res = job.waitForCompletion(true);
System.exit(res ? 0 : 1);
}
}
4.核心要点
- 在InputFormat端,如果不想切片,可以重写isSplitable方法,返回false则不支持切片
- 将多个小文件合并成一个SequenceFile文件,在driver类中可以指定