携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第8天,点击查看活动详情
由于MapReduce中默认的OutputFormat是TextOutputFormat,按行写入输出文件。但是对于我们实际应用场景中,对于Reduce的输出结果可能想要放到各种各样的输出目的地,可能是想要放到某个指定文件名的文件中,也可能是想写入HBase/Elasticsearch等等数据存储系统中,而hadoop自身提供的几种OutputFormat无法满足我们的需求时,可能就需要我们自定义这样的一个输出 类。
在这个案例中我们想要实现的是,对于一个订单文件中的订单数据进行过滤,将订单中的不同类别商品输出到不同的文件中,这里与分区不同的是,分区无法指定写入的输出文件名,而这里我们可以指定水果放在fruit.txt中,冷冻食品放到frozen.txt中,蔬菜放到vegetable.txt中。
- 准备数据文件
[root@hadoop301 testdata]# pwd
/usr/local/wyh/software/hadoop-3.1.3/testdata
[root@hadoop301 testdata]# cat testorder.txt
apple-1
dumpling-2
orange-1
grape-1
potato-3
tofu-3
grape-1
tomato-3
#注意这里可能会有重复商品,比如:grape-1
商品后面的数字表示类别表示,"1"表示水果,"2"表示冷冻食品,"3"表示蔬菜。
上传至HDFS:
[root@hadoop301 testdata]# hdfs dfs -mkdir /test_order
[root@hadoop301 testdata]# hdfs dfs -put testorder.txt /test_order
新建project:
- 引入pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>test.wyh</groupId>
<artifactId>TestOrder</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>3.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>3.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>3.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-core</artifactId>
<version>3.1.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<minimizeJar>true</minimizeJar>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
- 创建自定义Mapper类
package test.wyh.order;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class OrderMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
/**
* Key:行偏移量
* Value:apple-1
* 输出Key:apple-1
* 输出Value:置空
*/
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
context.write(value, NullWritable.get());
}
}
- 自定义Reducer类
package test.wyh.order;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class OrderReducer extends Reducer<Text, NullWritable, Text, NullWritable> {
@Override
protected void reduce(Text key, Iterable<NullWritable> values, Reducer<Text, NullWritable, Text, NullWritable>.Context context) throws IOException, InterruptedException {
/**
* 这里使用for循环是为了避免如果原始数据中有相同的key,合并之后会变成一条记录,但是我们不希望原始数据中的数据合并,希望每条数据都能写入输出文件中。
* 所以这里对每个key对应的value进行遍历,因为相同的key对应的value是会写在一个集合中的。
* 比如apple-1,合并后是apple-1 <NullWritable.get(), NullWritable.get()>,只不过这里的value是置空的。
* 再举一个例子word count中的合并:hello <1,1,1>
* 那我们这里对每个value进行遍历,就可以保证相同的key原来有几条数据,那么每条数据都执行一下write()。
*/
for (NullWritable value : values) {
context.write(key, NullWritable.get());
}
}
}
- 自定义RecordWriter类
虽然说我们需要自定义OutputFormat去实现输出类,但真正写数据的实现是由RecordWriter完成的,素以我们需要自定义一个类去实现RecordWriter中的方法。
package test.wyh.order;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import java.io.IOException;
public class OrderRecordWriter extends RecordWriter<Text, NullWritable> {
private FSDataOutputStream fruitOutputStream;
private FSDataOutputStream frozenOutputStream;
private FSDataOutputStream vegetableOutputStream;
public OrderRecordWriter(TaskAttemptContext taskAttemptContext) {
try {
FileSystem fileSystem = FileSystem.get(taskAttemptContext.getConfiguration());
//对于三个输出文件,需要创建三个输出流。输出目录必须不存在
fruitOutputStream = fileSystem.create(new Path("hdfs://hadoop301:8020/orderoutput/fruit.txt"));
frozenOutputStream = fileSystem.create(new Path("hdfs://hadoop301:8020/orderoutput/frozen.txt"));
vegetableOutputStream = fileSystem.create(new Path("hdfs://hadoop301:8020/orderoutput/vegetable.txt"));
} catch (IOException e) {
e.printStackTrace();
}
}
//设置具体的写规则
@Override
public void write(Text text, NullWritable nullWritable) throws IOException, InterruptedException {
String key = text.toString();
if(key.contains("1")){
//水果
fruitOutputStream.writeBytes(key+"\n");
}else if(key.contains("2")){
//冷冻
frozenOutputStream.writeBytes(key+"\n");
}else{
//蔬菜
vegetableOutputStream.writeBytes(key+"\n");
}
}
@Override
public void close(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
//关闭流
IOUtils.closeStream(fruitOutputStream);
IOUtils.closeStream(frozenOutputStream);
IOUtils.closeStream(vegetableOutputStream);
}
}
- 自定义OutputFormat类
package test.wyh.order;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
//这里的泛型与reduce输出的类型保持一致
public class OrderOutputFormat extends FileOutputFormat<Text, NullWritable> {
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
OrderRecordWriter orw = new OrderRecordWriter(taskAttemptContext);
return orw;
}
}
- 自定义主类
package test.wyh.order;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.CombineTextInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
public class OrderJobMain extends Configured implements Tool {
@Override
public int run(String[] strings) throws Exception {
Job job = Job.getInstance(super.getConf(), "testCombineJob");
//!!!!!!!!!! 集群必须要设置 !!!!!!!!
job.setJarByClass(OrderJobMain.class);
//配置job具体要执行的任务步骤
//指定要读取的文件的路径,这里写了目录,就会将该目录下的所有文件都读取到
FileInputFormat.setInputPaths(job, new Path("hdfs://hadoop301:8020/test_order"));
//指定map处理逻辑类
job.setMapperClass(OrderMapper.class);
//指定map阶段输出的k2类型
job.setMapOutputKeyClass(Text.class);
//指定map阶段输出的v2类型
job.setMapOutputValueClass(NullWritable.class);
//指定reduce处理逻辑类
job.setReducerClass(OrderReducer.class);
//设置reduce之后输出的k3类型
job.setOutputKeyClass(Text.class);
//设置reduce之后输出的v3类型
job.setOutputValueClass(NullWritable.class);
//设置使用自定义输出类
job.setOutputFormatClass(OrderOutputFormat.class);
//虽然在自定义OutputFormat中已经指定了输出路径,但是在整个过程中还是会输出一个标记文件_SUCCESS,所以需要在这里设置一个路径存放mapreduce过程中产生的其他文件
FileOutputFormat.setOutputPath(job, new Path("hdfs://hadoop301:8020/testorderoutputmark"));
//返回执行状态
boolean status = job.waitForCompletion(true);
//使用三目运算,将布尔类型的返回值转换为整型返回值,其实这个地方的整型返回值就是返回给了下面main()中的runStatus
return status ? 0:1;
}
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
/**
* 参数一是一个Configuration对象,参数二是Tool的实现类对象,参数三是一个String类型的数组参数,可以直接使用main()中的参数args.
* 返回值是一个整型的值,这个值代表了当前这个任务执行的状态.
* 调用ToolRunner的run方法启动job任务.
*/
int runStatus = ToolRunner.run(configuration, new OrderJobMain(), args);
/**
* 任务执行完成后退出,根据上面状态值进行退出,如果任务执行是成功的,那么就是成功退出,如果任务是失败的,就是失败退出
*/
System.exit(runStatus);
}
}
- 打包并上传至服务器
- 运行jar
[root@hadoop301 testjar]# hadoop jar TestOrder-1.0-SNAPSHOT.jar test.wyh.order.OrderJobMain
- 查看执行结果
这样就简单地实现了自定义OutputFormat。