KafkaStreams学习笔记-03

第三章 开发Kafka Streams

书里很多代码都用了lambda表达式,所以先补一下这方面的知识。

lambda表达式

Java是面向对象语言,不能直接传递代码块,或者说调用方法。如果需要重复使用一个代码块,需要构造一个类或接口的实例,封装这个方法,比较麻烦,因此引入lambda表达式。lambda表达式看起来可以直接调用方法,有点偏向函数式编程的感觉。
格式:
(参数)-> 表达式
表达式可以展开用代码块{}大括号来写
如果无参,不可以省略括号,除非编译器可以推导出参数类型
表达式的返回值无需指定类型

Java中有些接口只封装了一个方法(functional interface),而使用这个方法时常常需要实现接口的实例调用方法【之前常用匿名内部类实现】,这种情况下也可以用lambda表达式,看起来更简洁。

Thread thread1 = new Thread(new Runnable{
	@override
	public void run(){
		System.out.println("this is a java thread running");
		}
	}
);

可以改写为lambda表达式形式

Thread thread2 = new Thread(()->System.out.println("this is a java thread-lambda running"));

lambda 表达式形式中没有声明Runnable接口,这是因为编译器可以从上下文推断出这里调的是Runnable接口实例。因为Thread类在构造时需要的参数是Runnable接口的实例。

给我的感觉lambda表达式的类型是函数接口的实例,它根据上下文省略了封装该方法的接口的实例化语句。所以其他时候Java可以调用lambda语句,看起来很像直接调用了方法,也可以把lambda表达式赋值给接口。

方法引用又将lambda表达式进一步简写
双冒号运算符::
类名::方法名
常结合lambda表达式,参数是类名下的一个对象,调用了方法,且返回了方法的返回值。

总的来说就是JDK的升级编译器越来越聪明了,能推断的东西越来越多了,所以需要程序源写清楚的就少了,可以简写了,lambda表达式就是一个人例子。

开发步骤理解

Kafka Stream API的核心是KStream 对象。许多方法都使用连贯接口的方式。而许多接口都是functional interface,可以用lambda表达式。
fluent interface的核心就是调用方法的对象和方法返回的对象是同一个。非常方便用链式编程处理对象,给我的感觉就是用不同方法对对象进行修饰和更改,比较直观。
但Kafka Stream API的区别是,每此KStream调用一个方法,返回的是一个KStream副本,而不是原来的对象。【为什么?这样设计,优点在哪里?原来的对象去了哪里?,在使用上我如何知道这个对象是copy而不是原来的对象】

一般开发步骤

  1. 定义Kafka Streams配置
  2. 创建Serde实例,可自定义或用default
  3. 创建处理器拓扑
  4. 创界和启动KStream

书里以Yelling App作为一个例子,但解释顺序是3-1-2-4。我读了两遍懂了意思,准备笔记按顺序写,这样以后自己复习的时候思路比较清楚。

Kafka Streams配置

Kafka Streams程序高度可配置,有两个配置项是必须的,用方法props.put配置如下

props.put(StreamsConfig.APPLICATION_ID_CONFIG,"yelling_app_id");
props.put(StreamsConfig,BOOTSTRAP_SERVERS_CONFIG,"localhost:9092");

这两个语句中发现props.put方法直接为StreamsConfig中的final变量赋值,其中StreamsConfig应该是个final类,变量的值在程序中不被修改。application_id是程序的唯一标识符,这ID要保证在集群中唯一。后面的主机名:端口名配置的是当前应用程序在集群中的位置

Serde实例创建

Kafka 主题的存储格式是字节数组,数据在kafka streams 上传输格式是json,处理时用到的是对象。这就需要把消息(字节数组)转换为json,再转换为对象,给处理器处理。这个过程需要serde对象进行序列化或反序列化。基本类型的序列化或反序列化由Serds工厂方法实现,比如:

Serde<String> stringSerde = Serdes.String();

serde接口是带泛型的,应该是需要序列化或反序列化对象的类型,然后创建一个相应的实例,可以用Serdes类中的方法返回。这里发现Serdes是一个类,应该是含有常用类型的序列化或反序列化方法,其他自定义的类需要创建自定义的Serde类。Serde是个接口,用来接收返回的这样一个Serde实例,它是对象的序列化器和反序列化器的容器。

Yelling App的拓扑

将Topic信息全部转换为大写字母消息:
两个Topic作为数据存储的file
三个处理器:源处理器,转换为大写字母,接收处理器这三个是需要用API构建的
整个拓扑非常简单,单链的

source processor

KStream<String, String > simpleFirstStream = builder.stream("src-topic", Comsumed.with(stringSerde, stringSerde));

这个语句中可以发现,KStream对象是带泛型的【是键值对吗?】,由builder.stream()创建,需要两个参数,第一个参数是Topic,第二个参数是Serde对象。而Serde对象是由Comsumed.with方法确定的。后者需要的参数也是Serde对象,也符合流畅接口的原则。注意,Serde对象是用来对消息序列化或反序列化的,Serde类提供了一些基本类型序列化或反序列化的方法,特殊类型需要自定义(重写方法)。
然后Comsumed和Produced这两个类可以理解为IO流中的输入输出,或者说读写。当消息被读入(输入流)对应的是消费消息。当消息被写入(输出流)对应的是生产消息。

大写字符处理器

KStream<String, String> upperCasedStream = simpleFirstStream.mapValues(String::toUpperCase);

这个语句中可以看出,由source processor的simpleFirstStream对象调用了mapValues方法,得到了一个新的KStream对象作为大写字符处理器,这个处理器是source processor的副本。这里mapValues方法做的就是将消息转换为大写字符。
mapValues方法中的参数使用了lambda表达式,调用了String类中的toUpperCase方法。这是一种比较懒惰的表达方法,对于写程序的来说很明白,对于读程序的来说会有些模糊。其实这个mapValues方法的参数是接收一个ValueMaper<V,V1>接口的实例,这个接口中有一个apply方法,也就是说这个接口是一个functional interface,它的作用是把传入的一个值,处理传出为另一个值(字面意思,映射map)。在没有lambda表达式的时候,需要做的是用匿名类实例化这个接口,并对apply方法进行重写。这里直接用lambda表达式了,其实apply方法的重写就是调用了String类中的toUpperCase方法。完整的lambda表达式可以是(s)-> s.toUpperCase(),这里用了双冒号方法引用。
由于mapValues这个方法接收的一定是上述实例,所以lambda表达式允许不写那么多麻烦的东西,代码中没有体现接口和重写方法,但底层我觉得还是一步步由接口和方法重写实现的。
可能因为我是小白,我觉得lambda表达式这种代码里不带接口的写法就是写代码一时爽,读代码火葬场……【收!】

sink processor将处理后的消息写入指定主题

upperCasedStream.to("out-topic",Produed.with(stringSerde,stringSerde));

这句发现其实没有创建新的KStream对象,因为没有新的拓扑了,可以调用to方法写入消息。to方法需要两个参数,有点像源处理器中的builder.stream,需要的其中一个参数是输出主题名,第二个参数是Serde实例,这次由Produced.with处理给定的Serde实例。

到这里一个yelling app的简单拓扑就创建完成了。构建过程是:从主题创建源处理器,调用mapValues返回一个大写字符处理器,调用to方法写入主题。由于每一步处理都返回一个KStream副本,可以用链式编程把上述的拓扑创键代码调整为:

builder.stream("src-topic, Consumed.with(stringSerde,stringSerde))
.mapValues(String::toUpperCase)
.to("out-topic",Produced.with(stringSerde, stringSerde));

程序源码

/*
 * Copyright 2016 Bill Bejeck
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package bbejeck.chapter_3;

import bbejeck.clients.producer.MockDataProducer;
import org.apache.kafka.common.serialization.Serde;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.Consumed;
import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.kstream.KStream;
import org.apache.kafka.streams.kstream.Printed;
import org.apache.kafka.streams.kstream.Produced;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Properties;

public class KafkaStreamsYellingApp {
	//log info
    private static final Logger LOG = LoggerFactory.getLogger(KafkaStreamsYellingApp.class);

    public static void main(String[] args) throws Exception {


        //Used only to produce data for this application, not typical usage
        MockDataProducer.produceRandomTextData();
		
		 //use properties class to configure application
        Properties props = new Properties();
        props.put(StreamsConfig.APPLICATION_ID_CONFIG, "yelling_app_id");
        props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");

        StreamsConfig streamsConfig = new StreamsConfig(props);
		 
		 //Serde instance
        Serde<String> stringSerde = Serdes.String();

		 //use builder to build topology
        StreamsBuilder builder = new StreamsBuilder();

        //source processor,read from topic
        KStream<String, String> simpleFirstStream = builder.stream("src-topic", Consumed.with(stringSerde, stringSerde));

		 //upperCase processor
        KStream<String, String> upperCasedStream = simpleFirstStream.mapValues(String::toUpperCase);

		 //sink processor, wrtie to topic
        upperCasedStream.to( "out-topic", Produced.with(stringSerde, stringSerde));
        
		 //console print?
        upperCasedStream.print(Printed.<String, String>toSysOut().withLabel("Yelling App"));

		 //build kafkaStream app
        KafkaStreams kafkaStreams = new KafkaStreams(builder.build(),streamsConfig);
        LOG.info("Hello World Yelling App Started");
		 
		 //start app
        kafkaStreams.start();
        Thread.sleep(35000);
        LOG.info("Shutting down the Yelling APP now");
  		 
  		 //close resources
        kafkaStreams.close();
        MockDataProducer.shutdown();

    }
}

总结

Kafka Streams应用程序的一般步骤:

  1. 创建StreamsConfig实例-配置程序
  2. 创建Serde对象-序列化反序列化器
  3. 构造处理器拓扑-KStreams节点
  4. 启动Kafka Streams 应用程序

还没有弄清楚的事情:

  • serde类的作用,序列化和反序列化怎样具体实现的
  • KStream<>这个泛型需要的两个类型没理解是什么关系,键值对吗?创建对象的时候这两个类型对应的是什么?【是不是后面builder()方法的两个参数类型,第一个是Topic名,应该是String,第二个是处理的消息类型,在yelling app例子中是String类的,在ZMart例子中是purchase类?】
  • 这个简单的例子没有给我感觉处分布式,它与物理集群,broker是怎么建立分布式处理关系的,没有体现和其他broker的messaging,或者故障处理
  • 每个节点,也就是KStream对象消费和生产消息具体是怎么进行的,比如说当消息有很多的时候,是一条条读取的吗,每次读取的size是多少,是每次读取都创建一次KStream对象和相应的拓扑吗?【从理解上显然不是这样的,但从代码上暂时找不出答案,应该是一个拓扑建立之后,它就是稳定存在的。因为我对比普通的Java IO流处理数据,是在程序和文件间建立通道,然后制定处理速率,传输数据。但这里给我的感觉,只是指定了Topic,似乎没有读写数据的语句,这里不太理解】
发布了9 篇原创文章 · 获赞 0 · 访问量 858

猜你喜欢

转载自blog.csdn.net/weixin_43138930/article/details/105468448