这篇主要讲一讲我看完Avro这部分的心得体会。Avro其实就是为了让不同语言可以无障碍的进行文件读写。也就是说,虽然读写这部分内容(不论在内存还是文件中)的语言各不相同,API也可不相同,但我们总可以通过Avro提供的方式实现这些语言之间的无障碍沟通。主要原理就是,不论什么语言写入的对象最后通过Avro都会生成Avro的统一格式,这样在最底层形成了统一的格式,我们只要给不同的语言以不同的API方式去翻译就可以了(就像英语是全世界的通用语言,各种语言的翻译机制不同,但大家都可以用英语来沟通)。
Avro有两种文件,一种文件是用来指定每条数据(类型)的构建模型的,另一种文件是数据文件,就是存储在指定的类型下具体的数据。前者我们叫做模式文件,后缀为.avsc,后者我们叫做数据文件,后缀为.avro。
下面来介绍一下具体的应用。
一、内存中的序列化和反序列化
下面就是一个模式文件的格式,像C语言中的结构体,只规定了数据的结构,没有具体值
{
"type": "record",
"name": "StringPair",
"doc": "A pair of strings.",
"fields": [
{"name": "left", "type": "string"},
{"name": "right", "type": "string"}
]
}
现附上原书的代码,我会删减掉一部分以便与分析:
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import org.apache.avro.AvroTypeException;
import org.apache.avro.Schema;
import org.apache.avro.file.DataFileReader;
import org.apache.avro.file.DataFileWriter;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericDatumReader;
import org.apache.avro.generic.GenericDatumWriter;
import org.apache.avro.generic.GenericRecord;
import org.apache.avro.io.DatumReader;
import org.apache.avro.io.DatumWriter;
import org.apache.avro.io.Decoder;
import org.apache.avro.io.DecoderFactory;
import org.apache.avro.io.Encoder;
import org.apache.avro.io.EncoderFactory;
import org.apache.avro.specific.SpecificDatumReader;
import org.apache.avro.specific.SpecificDatumWriter;
import org.apache.avro.util.Utf8;
import org.junit.Ignore;
import org.junit.Test;
public class AvroTest {
@Test
public void testPairGeneric() throws IOException {
// vv AvroParseSchema
Schema.Parser parser = new Schema.Parser();
Schema schema = parser.parse(getClass().getResourceAsStream("StringPair.avsc"));
//初始化一个schema,有了这个类,我们在传值的时候才能和模式文件中的具体位置对应起来
// ^^ AvroParseSchema
// vv AvroGenericRecordCreation
GenericRecord datum = new GenericData.Record(schema);
datum.put("left", "L");
datum.put("right", "R");
//GenericDatumWriter需要传递给schema,因为他会随着schema去决定从数据对象来的哪个值应该被输出,说白了这样相当于构成了键值对
// ^^ AvroGenericRecordCreation
// vv AvroGenericRecordSerialization
ByteArrayOutputStream out = new ByteArrayOutputStream();
DatumWriter<GenericRecord> writer = new GenericDatumWriter<GenericRecord>(schema);
Encoder encoder = EncoderFactory.get().binaryEncoder(out, null);
writer.write(datum, encoder);
encoder.flush();//encoder.flush()是刷新序列化工具
out.close();//out.close()关闭输出流
/**写入内存:
*(1)创建二进制输出流,
*(2)通过schema创建GenericRecord对象,并按照键值对应的关系写入数据
*(3)创建DantumWriter<GenericRecord>对象与schema对象绑定,这样才能使writer对象知道哪个键对应哪个值
*(4)创建Encoder对象与输出流绑定用于解析
*(5)利用writer对象把datum中的数据翻译成encoder可以理解的类型,然后由encoder写到输出流*/
// ^^ AvroGenericRecordSerialization
// vv AvroGenericRecordDeserialization
DatumReader<GenericRecord> reader = new GenericDatumReader<GenericRecord>(schema);
Decoder decoder = DecoderFactory.get().binaryDecoder(out.toByteArray(), null);
GenericRecord result = reader.read(null, decoder);
assertThat(result.get("left").toString(), is("L"));
assertThat(result.get("right").toString(), is("R"));
// ^^ AvroGenericRecordDeserialization
/**读步骤:
*(1)建立一个DatumReader<GenericRecord>对象和schema联系起来
*(2)创建Decoder对象联系输出流
*(3)通过Decoder对象把内容读取到DatumReader<GenericRecord>对象中,创建GenericRecord对象*/
@Test
public void testPairSpecific() throws IOException {
// vv AvroSpecificStringPair
StringPair datum = new StringPair();
datum.left = "L";
datum.right = "R";/*]*/
ByteArrayOutputStream out = new ByteArrayOutputStream();
/*[*/DatumWriter<StringPair> writer =
new SpecificDatumWriter<StringPair>(StringPair.class);/*]*/
Encoder encoder = EncoderFactory.get().binaryEncoder(out, null);
writer.write(datum, encoder);
encoder.flush();
out.close();
DatumReader<StringPair> reader =
new SpecificDatumReader<StringPair>(StringPair.class);/*]*/
Decoder decoder = DecoderFactory.get().binaryDecoder(out.toByteArray(), null);
StringPair result = reader.read(null, decoder);
assertThat(result./*[*/left/*]*/.toString(), is("L"));
assertThat(result./*[*/right/*]*/.toString(), is("R"));
// ^^ AvroSpecificStringPair
}
/**这个例子就是根据java类来创建一个实例用来代替GenericRecord对象,再使用SpecificDatumWriter类来把实例的内容写到数据流中,使用SpecificReader类来读取数据,放*在这里的作用就是表明不仅可以通过模式文件来构建模式,也可以直接以java类的形式构建,而且写到内存中的形式和先前的例子一样,同样可以被其他语言读取*/
@Test
public void testDataFile() throws IOException {
Schema schema = new Schema.Parser().parse(getClass().getResourceAsStream("StringPair.avsc"));
GenericRecord datum = new GenericData.Record(schema);
datum.put("left", "L");
datum.put("right", "R");
// vv AvroDataFileCreation
File file = new File("data.avro");
DatumWriter<GenericRecord> writer = new GenericDatumWriter<GenericRecord>(schema);
DataFileWriter<GenericRecord> dataFileWriter =
new DataFileWriter<GenericRecord>(writer);
dataFileWriter.create(schema, file);
dataFileWriter.append(datum);
dataFileWriter.close();
/**(1)创建数据文件路径 *(2)创建DatumWriter实例 *(3)通过DatumWriter实例创建DataFileWriter实例 *(4)利用DataFileWriter.create实例来创建数据文件 *(5)利用DataFileWriter.append把Avro对象写入数据文件*/
// vv AvroDataFileGetSchema
DatumReader<GenericRecord> reader = new GenericDatumReader<GenericRecord>();
DataFileReader<GenericRecord> dataFileReader =
new DataFileReader<GenericRecord>(file, reader);
assertThat("Schema is the same", schema, is(dataFileReader.getSchema()));
// ^^ AvroDataFileGetSchema
// vv AvroDataFileRead/**读取数据对象文件的时候与写相似,只是不用指定模式, (1)创建DatumTeader实例 (2)创建一个DataFileReader为这个文件添加一个reader*/
assertThat(dataFileReader.hasNext(), is(true));
GenericRecord result = dataFileReader.next();
assertThat(result.get("left").toString(), is("L"));
assertThat(result.get("right").toString(), is("R"));
assertThat(dataFileReader.hasNext(), is(false));
// ^^ AvroDataFileRead
file.delete();
}
}
二、模式的解析
{
"type": "record",
"name": "StringPair",
"doc": "A pair of strings with an added field.",
"fields": [
{"name": "left", "type": "string"},
{"name": "right", "type": "string"},
{"name": "description", "type": "string", "default": "}
]
}
description只是新增的一个字段,这一部分的内容主要是在讲,当读模式和写模式不通的时候该如何处理(毕竟随着新旧版本的更替这是经常发生的)。
public void testSchemaResolution() throws IOException {
Schema schema = new Schema.Parser().parse(getClass().getResourceAsStream("StringPair.avsc"));
Schema newSchema = new Schema.Parser().parse(getClass().getResourceAsStream("NewStringPair.avsc"));
ByteArrayOutputStream out = new ByteArrayOutputStream();
DatumWriter<GenericRecord> writer = new GenericDatumWriter<GenericRecord>(schema);
Encoder encoder = EncoderFactory.get().binaryEncoder(out, null /* reuse */);
GenericRecord datum = new GenericData.Record(schema); // no description
datum.put("left", "L");
datum.put("right", "R");
writer.write(datum, encoder);
encoder.flush();
// vv AvroSchemaResolution DatumReader<GenericRecord> reader = /*[*/new GenericDatumReader<GenericRecord>(schema, newSchema);/*]*/ Decoder decoder = DecoderFactory.get().binaryDecoder(out.toByteArray(), null); GenericRecord result = reader.read(null, decoder);//由于写入模式在数据文件头就有规定,所以在此可以传入空值 assertThat(result.get("left").toString(), is("L")); assertThat(result.get("right").toString(), is("R")); /*[*/assertThat(result.get("description").toString(), is(""));
}//这个实例旨在说明当用新的模式去读取旧的模式(写入时的模式)时的做法,新的模式只是比旧的模式多了一个field(即description)
当新模式变为
{
"type": "record",
"name": "StringPair",
"doc": "The right field of a pair of strings.",
"fields": [
{"name": "right", "type": "string"}
]
}
相当于减少了一个字段,减少了一个字段后的读取方式和先前还是一样的,故可以判断读取方式都是一样的
三、排序、别名
其实还可以在字段的属性里添加排序好别名
排序
{
"type": "record",
"name": "StringPair",
"doc": "A pair of strings, sorted by right field descending.",
"fields": [
{"name": "left", "type": "string", "order": "ignore"},
{"name": "right", "type": "string", "order": "descending"}
]
}
作用:排序的作用一贯是map处理完数据后要进行排序别名
{
"type":"record",
"name":"com.sweetop.styhadoop.StringPair",
"doc":"A pair of strings",
"fields":[
{"name":"first","type":"string","aliases":["left"]},
{"name":"second","type":"string","aliases":["right"]}
]
}
作用:,将写的schema的field名字转换成读的schema的field,使用别名schema去读数据,这里不能再用left,right,而要用first,second,参考这篇博客。原书最后还提供了一个Avro在mapreduce应用的实例,以后再添加吧。