用一个案例来讲述怎样自定义实现InputFormat和OutputFormat
需求:
将多个小文件合并成一个SequenceFile文件
(SequenceFile文件是Hadoop用来存储二进制形式的key-value对的文件格式,SequenceFile里面存储着多个文件,存储的形式为文件路径+名称为key,文件内容为value)
package com.jee.inputformat;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
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;
// 直接继承FileInputFormat而不是InputFormat
// 原因是如果继承InputFormat 我们需要重写两个方法 一个是切片方法 另一个是将读取的内容转化成key-value
// 而我们之间继承InputFormat的子类FileInputFormat 它已经实现了切片方法(就是默认的FIF切片方法) 在我们这个案例也适用 我们只需要实现将读取的内容转化成key-value即可
// FileInputFormat的参数类型 是map任务接受到的类型 根据案例要求是 文件名和文件内容
// 所以参数适用 Text 和 BytesWritable (因为有些文件不一定是字符串 所以最好不使用Text 而是用BytesWritable 字节)
public class WholeFileInputFormat extends FileInputFormat<Text, BytesWritable> {
//根据需求 我们的文件 不论大小 都不能被切片 直接返回false
@Override
protected boolean isSplitable(JobContext context, Path filename) {
return false;
}
public RecordReader createRecordReader(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
return new WholeFileRecordReader();
}
}
package com.jee.inputformat;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.IOUtils;
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.IOException;
//自定义的RecordReader 主要功能是将切片完之后的数据内容变成我们想要的key-value形式(key:文件的路径和名称,value:文件的内容)
public class WholeFileRecordReader extends RecordReader<Text, BytesWritable> {
//标记文件是否已经读完
private boolean readOver = false;
//返回的Key值-->Text
private Text key = new Text();
//返回的value值-->ByteWritable
private BytesWritable value = new BytesWritable();
//读取文件的输入流
private FSDataInputStream inputStream;
//文件切片
private FileSplit split = new FileSplit();
//初始化 在框架开始的时候调用一次
public void initialize(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
//我们要读取一整个文件 我们需要开流
//init的时候开流 close的时候关流
//参数中的 inputSplit就是我们拿到的切片 taskAttemptContext就是我们任务的所有信息
//强制转化切片类型至文件切片
split = (FileSplit) inputSplit;
//通过切片获取路径
Path path = split.getPath();
//通过路径获取文件系统(configuration不能随意new一个 要使用当前context中的配置)
FileSystem fileSystem = path.getFileSystem(taskAttemptContext.getConfiguration());
//开流
inputStream = fileSystem.open(path);
}
//判断是否还能读取下一组kv值
public boolean nextKeyValue() throws IOException, InterruptedException {
if(!readOver){
readOver = !readOver;
//具体读文件的流程
//读key
key.set(split.getPath().toString());
//读value
//一次性读取完整个文件内容 所以byte数组的大小就是文件的大小
byte[] bytes = new byte[(int)split.getLength()];
inputStream.read(bytes);
value.set(bytes,0,bytes.length);
return true;
}else {
return false;
}
}
//返回当前的Key
public Text getCurrentKey() throws IOException, InterruptedException {
return key;
}
//返回当前的Value
public BytesWritable getCurrentValue() throws IOException, InterruptedException {
return value;
}
//显示进度 (0~1) 1代表100%
public float getProgress() throws IOException, InterruptedException {
return readOver ? 1 : 0;
}
//关闭
public void close() throws IOException {
//关流
IOUtils.closeStream(inputStream);
}
}
package com.jee.inputformat;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
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;
//如果Mapper和Reducer什么事都没有干 只是将拿到的文件原封不动的输出 那么可以不用写 有默认的
public class WholeFileDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//获取Job
Job job = Job.getInstance(new Configuration());
//设置Job的类路径
job.setJarByClass(WholeFileDriver.class);
//设置Map和Reduce的输入和输出类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(BytesWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(BytesWritable.class);
//将自定义的InputFormat和OutputFormat注册到job中
job.setInputFormatClass(WholeFileInputFormat.class);
job.setOutputFormatClass(SequenceFileOutputFormat.class);
//设置输入输出路径
FileInputFormat.setInputPaths(job,new Path("d:/Hadoop/input"));
FileOutputFormat.setOutputPath(job,new Path("d:/Hadoop/output"));
//提交
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
自定义分区
分区是在map任务执行完之后 在shuffle
过程内执行的 默认就是一个 但是我们可以设置多个分区 分区的数量就是我们输出文件的数量
package com.jee.partition;
import com.jee.flow.FlowBean;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
//自定义的分区实现类继承Partitioner类 (参数就是Map任务输入的key-value类型)
public class MyPartitioner extends Partitioner<Text, FlowBean> {
//设置分区
public int getPartition(Text text, FlowBean flowBean, int i) {
String key = text.toString();
switch (key.substring(0,3)){
case "137" :{
return 0;
} case "138" :{
return 1;
} case "139" :{
return 2;
} case "182" :{
return 3;
} default :{
return 4;
}
}
}
}
package com.jee.partition;
import com.jee.flow.FlowBean;
import com.jee.flow.FlowDriver;
import com.jee.flow.FlowMapper;
import com.jee.flow.FlowReducer;
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 java.io.IOException;
//这个的MapReduce和序列化类的例子中的MapReduce是一样的 所以我们可以复用MapReduce 只需要修改一下driver的分区
public class PartitionerDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//1.获取job实例
Job job = Job.getInstance(new Configuration());
//2.设置类路劲
job.setJarByClass(FlowDriver.class);
//3.设置Mapper和Reducer
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReducer.class);
//4.设置输入输出类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//5.设置输入输出路径
FileInputFormat.setInputPaths(job,new Path("D:/Hadoop/input"));
FileOutputFormat.setOutputPath(job,new Path("D:/Hadoop/output"));
//设置ReduceTasks的数量
job.setNumReduceTasks(5);
//注册我们自定义的分区
job.setPartitionerClass(MyPartitioner.class);
//6.提交
boolean b = job.waitForCompletion(true);
System.exit(b ? 0: 1);
}
}