在 Storm 中一个拓扑由一个Spout和多个Bolt组成,Spout主要做接收数据、数据分发的工作,Bolt主要做数据处理。
1. 案例处理流程(共需要5个类,一个Spout类、3个Bolt类,一个拓扑类)
在词频统计案例中,数据源就是各行英文短句,以下是整个数据处理的流程:
- Spout 就负责将一行英文短句作为一条消息输出到数据流中,提供给后续的Bolt进行处理
- 第一个 Bolt 获取到数据后,将英文短句进行分词操作(单独取出各个英文单词),将单词输出到数据流中
- 第二个 Bolt 获取到数据后,从原先准备的map集合中根据单词作为key获取对应的value值(map存储的是键值对key-value数据,这里的key就是单词,value就是单词目前出现的次数);如果单词已经出现过就可以获取到该单词出现的次数,只需要在这个次数上+1即可,如果单词是第一次出现就新建一个键值对,key是单词,value是1,最后将元组输出到数据流
- 第三个 Bolt 获取到已经合并后的结果(即处理的最终结果做数据输出)
- 创建一个类(拓扑类),在该类中声明spout与bolt的拓扑关系及传输方式,提交运行方式等(最终的执行类)
2. 业务处理
(1)项目创建(创建maven项目)
将以下依赖包内容添加到pom文件中(在标签中就行)
<dependency>标签记录的是依赖jar包信息(storm中的封装好的方法或类都在storm-core依赖包中)
<plugin>标签存放的是插件信息(maven-compiler-plugin插件用于编译打包)
<dependencies>
<dependency>
<groupId>org.apache.storm</groupId>
<artifactId>storm-core</artifactId>
<version>1.0.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
(2)在项目下的src/main/java目录下创建package用于存放类
默认项目结构如图所示
(3)Spout类、Bolt类、拓扑类的说明
①. 实现 Spout 类需要继承 BaseRichSpout 类,并需要继承实现它的三个方法分别是:
- open():初始化方法,主要用于声明实例化的输出类(只会执行一次,在程序刚启动时执行)
- nextTuple():针对数据创建元组,并将元组发送到数据流中
- declareOutputFields():输出字段说明
②. 实现 Bolt 类需要继承 BaseRichBolt 类,并需要继承实现它的三个方法分别是:
- prepare():预处理(初始化方法),主要用于声明实例化的输出类
- execute():执行方法(数据的处理与输出都是在该方法中实现的)
- declareOutputFields():输出字段说明(作用于Spout中相同,只要需要将数据输出到数据流都需要诗实现该方法)
③. 实现拓扑类(即topology)需要一个程序执行入口(即java的main方法),在拓扑类中主要需要完成的任务如下:
- 声明拓扑并指定消息分发策略
- 配置拓扑任务所需的执行器数量、消息传输语义等信息
- 任务提交方式及路径(本地/集群)
(4)词频统计案例完整代码
- ①. 数据分发的 Spout 类的具体实现如下(本案例使用自定义数据源,从数组datas中获取数据,每次获取一个元素即一个英文短句发送到数据流中,获取到最后一条又返回第一条循环获取)
public class DataSourceSpout extends BaseRichSpout {
private SpoutOutputCollector collector;
// 设置句子集(数据源)
private String[] datas = { "This is example of chapter 4", "This is word count example", "Very basic example of Apache Storm",
"Apache Storm is open source real time processing engine"};
// 定义索引下标
private int index = 0;
public void open(@SuppressWarnings("rawtypes") Map config, TopologyContext context, SpoutOutputCollector collector) {
// 实例化输出类
this.collector = collector;
}
public void nextTuple() {
// 获取数据
String datasource= datas[index];
System.out.println(datasource);
// 将数据输出到数据流中
this.collector.emit(new Values(datasource));
// 当获取到最后一条数据时返回第一条数据重新获取
index++;
if (index >= datas.length) {
index = 0;
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void declareOutputFields(OutputFieldsDeclarer declarer) {
// 说明输出字段类别为 datasource
declarer.declare(new Fields("datasource"));
}
}
- ②. 做字符串分割处理的 Bolt 类的具体实现如下(即将英文短句进行分词,获取到每个单词)
public class SplitWordBolt extends BaseRichBolt {
private OutputCollector collector;
public void prepare(@SuppressWarnings("rawtypes") Map config, TopologyContext context, OutputCollector collector) {
this.collector = collector;
}
public void execute(Tuple tuple) {
// 从数据流中获取类别为 datasource 的数据
String datasource= tuple.getStringByField("datasource");
// 英文短句通过空格分割获取到单词
String[] words = datasource.split(" ");
for (String word : words) {
// 将单词传输到数据流中
this.collector.emit(new Values(word));
}
}
public void declareOutputFields(OutputFieldsDeclarer declarer) {
// 指定输出的单词字段类别为 word
declarer.declare(new Fields("words"));
}
}
- ③. 获取到单词后将计算单词出现次数的 Bolt 类的具体实现如下(使用一个map集合存储单词及出现次数对应的键值对信息,即(word,count),如果当前单词出现过就在原有的count上+1,如果没出现过就新创建一个键值对且word为当前单词,count为1)
public class WordCountBolt extends BaseRichBolt {
private OutputCollector collector;
private HashMap<String, Long> counts = null;
public void prepare(@SuppressWarnings("rawtypes") Map config, TopologyContext context, OutputCollector collector) {
this.collector = collector;
// 实例化一个空的map集合用于存储(word,count)的键值对信息
this.counts = new HashMap<String, Long>();
}
public void execute(Tuple tuple) {
// 从数据流中取出单词
String word = tuple.getStringByField("words");
// 从map集合中获取该单词对应的键值对,如果单词不存在map中会返回null
Long count = this.counts.get(word);
// 如果word在map集合(counts)中是不存在,给该word一个初始count值为0
if (count == null) {
count = 0L;
}
// 在原本的count值上+1
count++;
this.counts.put(word, count);
// 将键值对结果输出到数据流中
this.collector.emit(new Values(word, count));
}
public void declareOutputFields(OutputFieldsDeclarer declarer) {
// 由于本次输出到数据流的字段是两个字段,所以分别为第一个字段定义类型为word,第二个字段为count
declarer.declare(new Fields("word", "count"));
}
}
- ④. 获取最终处理结果并对输出到控制台的 Bolt 类的具体实现如下:
cleanup方法:清理方法,与prepare方法类似,一个是在刚启动时执行一次,一个是在bolt最终要关闭前执行(也只会执行一次)
public class DisplayBolt extends BaseRichBolt {
private HashMap<String, Long> counts = null;
public void prepare(@SuppressWarnings("rawtypes") Map config, TopologyContext context, OutputCollector collector) {
this.counts = new HashMap<String, Long>();
}
public void execute(Tuple tuple) {
String word = tuple.getStringByField("word");
Long count = tuple.getLongByField("count");
// 获取数据流中的键值对,并将键值对存放到一个map集合中
this.counts.put(word, count);
}
public void declareOutputFields(OutputFieldsDeclarer declarer) {
// 当前bolt是用于获取最终结果并最终处理的,不需要再将该结果输出到数据流,所以不需要实现该方法
}
public void cleanup() {
System.out.println("--- FINAL COUNTS ---");
List<String> keys = new ArrayList<String>();
// 获取map集合中的所有key值(即获取所有单词)
keys.addAll(this.counts.keySet());
// 将单词进行排序
Collections.sort(keys);
// 根据排序的单词逐个获取每个单词出现的次数,将结果输出到控制台
for (String key : keys) {
System.out.println(key + " : " + this.counts.get(key));
}
System.out.println("--------------");
}
}
- ⑤. 至此,关于词频统计的整个处理逻辑已完成,最后一个类是Storm要执行一个任务需要以拓扑的方式提交,所以最后一个类是拓扑类,具体实现如下:
public class TopologyDemo {
// java程序执行入口
public static void main(String[] args) throws Exception {
// 实例化一个拓扑
TopologyBuilder builder = new TopologyBuilder();
// 声明spout
builder.setSpout("datasource-spout", new DataSourceSpout());
// 声明spout到splitbolt的消息分发方式:随机分发
builder.setBolt("split-bolt", new SplitWordBolt()).shuffleGrouping("datasource-spout");
// 声明splitbolt到countbolt的消息分发方式:字段分发
builder.setBolt("count-bolt", new WordCountBolt()).fieldsGrouping("split-bolt", new Fields("words"));
// 声明countbolt到displaybolt的消息分发方式:全局分发
builder.setBolt("display-bolt", new DisplayBolt()).globalGrouping("count-bolt");
// 打包成jar的配置
Config config = new Config();
// 当前拓扑运行时 需要占用两个槽位(2个worker进程)
config.setNumWorkers(2);
config.setNumAckers(0); // storm可靠性处理相关
StormSubmitter.submitTopology("word-count-topology",config,builder.createTopology());
// 在本地直接运行的配置(本地有storm环境)
/*
if (args != null && args.length > 0) {
// 提交到集群模式
System.out.println("submitting on cluster mode");
StormSubmitter.submitTopology("word-count-Storm-topology", config, builder.createTopology());
} else {
// 提交到本地模式(单机模式)
System.out.println("submitting on local mode");
LocalCluster cluster = new LocalCluster();
// 提交拓扑
cluster.submitTopology("word-count-Storm-topology", config, builder.createTopology());
Thread.sleep(20000);
// 关闭拓扑
cluster.killTopology("word-count-Storm-topology");
cluster.shutdown();
}
*/
}
}
项目打包参考:
打包时需要注意将storm-core.jar从项目包中移除(由于是打包成jar提交到storm中,本地使用的storm-core.jar与storm程序会冲突,所以需要将storm-core.jar从jar信息中移除)
配置完成点击【Apply】、【OK】即可