I'm looking for a way to convert a POJO to an avro object in a generic way. The implementation should be robust to any changes of the POJO-class. I have achieved it but filling the avro record explicitly (see example below).
Is there a way to get rid of the hard-coded field names and just fill the avro record from the object? Is reflection the only way, or does avro provide this functionality out of the box?
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.apache.avro.Schema;
import org.apache.avro.generic.GenericData.Record;
import org.apache.avro.reflect.ReflectData;
public class PojoToAvroExample {
static class PojoParent {
public final Map<String, String> aMap = new HashMap<String, String>();
public final Map<String, Integer> anotherMap = new HashMap<String, Integer>();
}
static class Pojo extends PojoParent {
public String uid;
public Date eventTime;
}
static Pojo createPojo() {
Pojo foo = new Pojo();
foo.uid = "123";
foo.eventTime = new Date();
foo.aMap.put("key", "val");
foo.anotherMap.put("key", 42);
return foo;
}
public static void main(String[] args) {
// extract the avro schema corresponding to Pojo class
Schema schema = ReflectData.get().getSchema(Pojo.class);
System.out.println("extracted avro schema: " + schema);
// create avro record corresponding to schema
Record avroRecord = new Record(schema);
System.out.println("corresponding empty avro record: " + avroRecord);
Pojo foo = createPojo();
// TODO: to be replaced by generic variant:
// something like avroRecord.importValuesFrom(foo);
avroRecord.put("uid", foo.uid);
avroRecord.put("eventTime", foo.eventTime);
avroRecord.put("aMap", foo.aMap);
avroRecord.put("anotherMap", foo.anotherMap);
System.out.println("expected avro record: " + avroRecord);
}
}
Are you using Spring?
I build a mapper for that using a Spring feature. But it is also possible to build such a mapper via raw reflection utils too:
import org.apache.avro.Schema;
import org.apache.avro.generic.GenericData;
import org.apache.avro.reflect.ReflectData;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.util.Assert;
public class GenericRecordMapper {
public static GenericData.Record mapObjectToRecord(Object object) {
Assert.notNull(object, "object must not be null");
final Schema schema = ReflectData.get().getSchema(object.getClass());
final GenericData.Record record = new GenericData.Record(schema);
schema.getFields().forEach(r -> record.put(r.name(), PropertyAccessorFactory.forDirectFieldAccess(object).getPropertyValue(r.name())));
return record;
}
public static <T> T mapRecordToObject(GenericData.Record record, T object) {
Assert.notNull(record, "record must not be null");
Assert.notNull(object, "object must not be null");
final Schema schema = ReflectData.get().getSchema(object.getClass());
Assert.isTrue(schema.getFields().equals(record.getSchema().getFields()), "Schema fields didn't match");
record.getSchema().getFields().forEach(d -> PropertyAccessorFactory.forDirectFieldAccess(object).setPropertyValue(d.name(), record.get(d.name()) == null ? record.get(d.name()) : record.get(d.name()).toString()));
return object;
}
}
With this mapper you can generate a GenericData.Record which can be easily serialized to avro. When you deserialize an Avro ByteArray you can use it to rebuild a POJO from deserialized record:
Serialize
byte[] serialized = avroSerializer.serialize("topic", GenericRecordMapper.mapObjectToRecord(yourPojo));
Deserialize
GenericData.Record deserialized = (GenericData.Record) avroDeserializer.deserialize("topic", serialized);
YourPojo yourPojo = GenericRecordMapper.mapRecordToObject(deserialized, new YourPojo());
Here is generic way to convert
public static <V> byte[] toBytesGeneric(final V v, final Class<V> cls) {
final ByteArrayOutputStream bout = new ByteArrayOutputStream();
final Schema schema = ReflectData.get().getSchema(cls);
final DatumWriter<V> writer = new ReflectDatumWriter<V>(schema);
final BinaryEncoder binEncoder = EncoderFactory.get().binaryEncoder(bout, null);
try {
writer.write(v, binEncoder);
binEncoder.flush();
} catch (final Exception e) {
throw new RuntimeException(e);
}
return bout.toByteArray();
}
public static void main(String[] args) {
PojoClass pojoObject = new PojoClass();
toBytesGeneric(pojoObject, PojoClass.class);
}
With jackson/avro, it's very easy to convert pojo to byte[], similar to jackson/json:
byte[] avroData = avroMapper.writer(schema).writeValueAsBytes(pojo);
p.s.
jackson handles not only JSON, but also XML/Avro/Protobuf/YAML etc, with very similar classes and APIs.
Two steps to convert any pojo class to avro genric record
Using jackson/avro, to convert the pojo into bytes with Avro Mapper.
Using Avro GenericDatumReader to read it as Generic Record.
public class AvroConverter{
public static GenericRecord convertToGenericRecord(String schemaPath, SomeClass someObject){
Schema schema = new Schema.Parser().setValidate(true).parse(new ClassPathResource(schemaPath).getFile());
AvroSchema avSchema = new AvroSchema(schema);
ObjectWritter writter = new AvroMapper().writer(avSchema);
final byte[] bytes = writter.writeValueAsBytes(someObject);
GenericDatumReader<Object> genericRecordReader = new GenericDatumReader<>(avSchema);
return (GenericRecord) genericRecordReader.read(null, DecoderFactory.get().binaryDecoder(bytes, null));
}
}
Gradle Depedency
// https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-avro
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-avro'
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With