This is an automated email from the ASF dual-hosted git repository.
opwvhk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/avro.git
The following commit(s) were added to refs/heads/main by this push:
new 30c31a579e AVRO-4165: [java] ability to specify AvroEncode on a class
(#3425)
30c31a579e is described below
commit 30c31a579ef1ded9f57505fcc160805bf5a21d21
Author: Ashley Taylor <[email protected]>
AuthorDate: Sat Jul 19 22:03:48 2025 +1200
AVRO-4165: [java] ability to specify AvroEncode on a class (#3425)
---
.../java/org/apache/avro/reflect/AvroEncode.java | 4 +-
.../apache/avro/reflect/FieldAccessReflect.java | 4 +-
.../java/org/apache/avro/reflect/ReflectData.java | 37 ++++-
.../apache/avro/reflect/ReflectDatumReader.java | 14 ++
.../apache/avro/reflect/ReflectDatumWriter.java | 12 +-
.../org/apache/avro/reflect/ReflectionUtil.java | 16 ++
.../org/apache/avro/reflect/TestAvroEncode.java | 180 +++++++++++++++++++++
7 files changed, 262 insertions(+), 5 deletions(-)
diff --git
a/lang/java/avro/src/main/java/org/apache/avro/reflect/AvroEncode.java
b/lang/java/avro/src/main/java/org/apache/avro/reflect/AvroEncode.java
index 225f247a9e..b4a021dce7 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/reflect/AvroEncode.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/reflect/AvroEncode.java
@@ -18,6 +18,7 @@
package org.apache.avro.reflect;
import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@@ -30,7 +31,8 @@ import java.lang.annotation.Target;
* file. Use of {@link org.apache.avro.io.ValidatingEncoder} is recommended.
*/
@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.FIELD)
+@Inherited
+@Target({ ElementType.FIELD, ElementType.TYPE })
public @interface AvroEncode {
Class<? extends CustomEncoding<?>> using();
}
diff --git
a/lang/java/avro/src/main/java/org/apache/avro/reflect/FieldAccessReflect.java
b/lang/java/avro/src/main/java/org/apache/avro/reflect/FieldAccessReflect.java
index df258f9d50..72d0563290 100644
---
a/lang/java/avro/src/main/java/org/apache/avro/reflect/FieldAccessReflect.java
+++
b/lang/java/avro/src/main/java/org/apache/avro/reflect/FieldAccessReflect.java
@@ -28,7 +28,7 @@ class FieldAccessReflect extends FieldAccess {
@Override
protected FieldAccessor getAccessor(Field field) {
- AvroEncode enc = field.getAnnotation(AvroEncode.class);
+ AvroEncode enc = ReflectionUtil.getAvroEncode(field);
if (enc != null)
try {
return new ReflectionBasesAccessorCustomEncoded(field,
enc.using().getDeclaredConstructor().newInstance());
@@ -47,7 +47,7 @@ class FieldAccessReflect extends FieldAccess {
this.field = field;
this.field.setAccessible(true);
isStringable = field.isAnnotationPresent(Stringable.class);
- isCustomEncoded = field.isAnnotationPresent(AvroEncode.class);
+ isCustomEncoded = ReflectionUtil.getAvroEncode(field) != null;
}
@Override
diff --git
a/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectData.java
b/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectData.java
index aa15ee8f46..4b993c6fdd 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectData.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectData.java
@@ -69,6 +69,9 @@ public class ReflectData extends SpecificData {
private static final String STRING_OUTER_PARENT_REFERENCE = "this$0";
+ // holds a wrapper so null entries will have a cached value
+ private final ConcurrentMap<Schema, CustomEncodingWrapper> encoderCache =
new ConcurrentHashMap<>();
+
/**
* Always false since custom coders are not available for {@link
ReflectData}.
*/
@@ -864,7 +867,7 @@ public class ReflectData extends SpecificData {
/** Create a schema for a field. */
protected Schema createFieldSchema(Field field, Map<String, Schema> names) {
- AvroEncode enc = field.getAnnotation(AvroEncode.class);
+ AvroEncode enc = ReflectionUtil.getAvroEncode(field);
if (enc != null)
try {
return enc.using().getDeclaredConstructor().newInstance().getSchema();
@@ -1042,4 +1045,36 @@ public class ReflectData extends SpecificData {
}
return super.newRecord(old, schema);
}
+
+ public CustomEncoding getCustomEncoding(Schema schema) {
+
+ return this.encoderCache.computeIfAbsent(schema,
this::populateEncoderCache).get();
+ }
+
+ private CustomEncodingWrapper populateEncoderCache(Schema schema) {
+ var enc = ReflectionUtil.getAvroEncode(getClass(schema));
+ if (enc != null) {
+ try {
+ return new
CustomEncodingWrapper(enc.using().getDeclaredConstructor().newInstance());
+ } catch (Exception e) {
+ throw new AvroRuntimeException("Could not instantiate custom
Encoding");
+ }
+ }
+ return new CustomEncodingWrapper(null);
+ }
+
+ private static class CustomEncodingWrapper {
+
+ private final CustomEncoding customEncoding;
+
+ private CustomEncodingWrapper(CustomEncoding customEncoding) {
+ this.customEncoding = customEncoding;
+ }
+
+ public CustomEncoding get() {
+ return customEncoding;
+ }
+
+ }
+
}
diff --git
a/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectDatumReader.java
b/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectDatumReader.java
index 2a8fcee9f2..7ba8e4827c 100644
---
a/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectDatumReader.java
+++
b/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectDatumReader.java
@@ -73,6 +73,10 @@ public class ReflectDatumReader<T> extends
SpecificDatumReader<T> {
super(data);
}
+ private ReflectData getReflectData() {
+ return (ReflectData) getSpecificData();
+ }
+
@Override
protected Object newArray(Object old, int size, Schema schema) {
Class<?> collectionClass = ReflectData.getClassProp(schema,
SpecificData.CLASS_PROP);
@@ -251,6 +255,16 @@ public class ReflectDatumReader<T> extends
SpecificDatumReader<T> {
}
}
+ @Override
+ protected Object read(Object old, Schema expected, ResolvingDecoder in)
throws IOException {
+ CustomEncoding encoder = getReflectData().getCustomEncoding(expected);
+ if (encoder != null) {
+ return encoder.read(old, in);
+ } else {
+ return super.read(old, expected, in);
+ }
+ }
+
@Override
protected Object readInt(Object old, Schema expected, Decoder in) throws
IOException {
Object value = in.readInt();
diff --git
a/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectDatumWriter.java
b/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectDatumWriter.java
index 25555d99e4..b9b083fd6b 100644
---
a/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectDatumWriter.java
+++
b/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectDatumWriter.java
@@ -61,6 +61,10 @@ public class ReflectDatumWriter<T> extends
SpecificDatumWriter<T> {
super(reflectData);
}
+ private ReflectData getReflectData() {
+ return (ReflectData) getSpecificData();
+ }
+
/**
* Called to write a array. May be overridden for alternate array
* representations.
@@ -158,7 +162,13 @@ public class ReflectDatumWriter<T> extends
SpecificDatumWriter<T> {
datum = ((Optional) datum).orElse(null);
}
try {
- super.write(schema, datum, out);
+
+ CustomEncoding encoder = getReflectData().getCustomEncoding(schema);
+ if (encoder != null) {
+ encoder.write(datum, out);
+ } else {
+ super.write(schema, datum, out);
+ }
} catch (NullPointerException e) { // improve error message
throw npe(e, " in " + schema.getFullName());
}
diff --git
a/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectionUtil.java
b/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectionUtil.java
index 4fa52d0345..3221d91d1f 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectionUtil.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectionUtil.java
@@ -24,6 +24,7 @@ import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
+import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
@@ -188,4 +189,19 @@ public class ReflectionUtil {
}
}
+ protected static AvroEncode getAvroEncode(Field field) {
+ var enc = field.getAnnotation(AvroEncode.class);
+ if (enc != null) {
+ return enc;
+ } else {
+ return getAvroEncode(field.getType());
+ }
+ }
+
+ protected static AvroEncode getAvroEncode(Class<?> clazz) {
+ if (clazz == null) {
+ return null;
+ }
+ return clazz.getAnnotation(AvroEncode.class);
+ }
}
diff --git
a/lang/java/avro/src/test/java/org/apache/avro/reflect/TestAvroEncode.java
b/lang/java/avro/src/test/java/org/apache/avro/reflect/TestAvroEncode.java
new file mode 100644
index 0000000000..daee2a39a9
--- /dev/null
+++ b/lang/java/avro/src/test/java/org/apache/avro/reflect/TestAvroEncode.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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 org.apache.avro.reflect;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.apache.avro.AvroTypeException;
+import org.apache.avro.Schema;
+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.junit.jupiter.api.Test;
+
+public class TestAvroEncode {
+ EncoderFactory factory = new EncoderFactory();
+
+ @Test
+ void testWithinClass() throws IOException {
+
+ var wrapper = new Wrapper(new R1("test"));
+
+ var read = readWrite(wrapper);
+
+ assertEquals("test", wrapper.getR1().getValue());
+ assertEquals("test used this", read.getR1().getValue());
+ }
+
+ @Test
+ void testDirect() throws IOException {
+
+ var r1 = new R1("test");
+
+ var read = readWrite(r1);
+
+ assertEquals("test", r1.getValue());
+ assertEquals("test used this", read.getValue());
+ }
+
+ @Test
+ void testFieldAnnotationTakesPrecedence() throws IOException {
+
+ var wrapper = new OtherWrapper(new R1("test"));
+
+ var read = readWrite(wrapper);
+
+ assertEquals("test", wrapper.getR1().getValue());
+ assertEquals("test used other", read.getR1().getValue());
+ }
+
+ public static class Wrapper {
+
+ private R1 r1;
+
+ public Wrapper() {
+ }
+
+ public Wrapper(R1 r1) {
+ this.r1 = r1;
+ }
+
+ public R1 getR1() {
+ return r1;
+ }
+
+ public void setR1(R1 r1) {
+ this.r1 = r1;
+ }
+
+ }
+
+ public static class OtherWrapper {
+ @AvroEncode(using = R1EncodingOther.class)
+ private R1 r1;
+
+ public OtherWrapper() {
+ }
+
+ public OtherWrapper(R1 r1) {
+ this.r1 = r1;
+ }
+
+ public R1 getR1() {
+ return r1;
+ }
+
+ public void setR1(R1 r1) {
+ this.r1 = r1;
+ }
+
+ }
+
+ @AvroEncode(using = R1Encoding.class)
+ public static class R1 {
+
+ private final String value;
+
+ public R1(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ }
+
+ public static class R1Encoding extends CustomEncoding<R1> {
+
+ {
+ schema = Schema.createRecord("R1", null, null, false,
+ Arrays.asList(new Schema.Field("value",
Schema.create(Schema.Type.STRING), null, null)));
+ }
+
+ @Override
+ protected void write(Object datum, Encoder out) throws IOException {
+ if (datum instanceof R1) {
+ out.writeString(((R1) datum).getValue());
+ } else {
+ throw new AvroTypeException("Expected R1, got " + datum.getClass());
+ }
+
+ }
+
+ @Override
+ protected R1 read(Object reuse, Decoder in) throws IOException {
+ return new R1(in.readString() + " used this");
+ }
+ }
+
+ public static class R1EncodingOther extends CustomEncoding<R1> {
+
+ {
+ schema = Schema.createRecord("R1", null, null, false,
+ Arrays.asList(new Schema.Field("value",
Schema.create(Schema.Type.STRING), null, null)));
+ }
+
+ @Override
+ protected void write(Object datum, Encoder out) throws IOException {
+ if (datum instanceof R1) {
+ out.writeString(((R1) datum).getValue());
+ } else {
+ throw new AvroTypeException("Expected R1, got " + datum.getClass());
+ }
+ }
+
+ @Override
+ protected R1 read(Object reuse, Decoder in) throws IOException {
+ return new R1(in.readString() + " used other");
+ }
+ }
+
+ <T> T readWrite(T object) throws IOException {
+ var schema = new ReflectData().getSchema(object.getClass());
+ ReflectDatumWriter<T> writer = new ReflectDatumWriter<>(schema);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ writer.write(object, factory.directBinaryEncoder(out, null));
+ ReflectDatumReader<T> reader = new ReflectDatumReader<>(schema);
+ return reader.read(null,
DecoderFactory.get().binaryDecoder(out.toByteArray(), null));
+ }
+}