From e5f2050f3316989ae70ace39a3e6363219d5a02d Mon Sep 17 00:00:00 2001 From: hengyunabc Date: Tue, 10 Oct 2023 17:32:17 +0800 Subject: [PATCH] add arthas-grpc-services, support ObjectService --- arthas-grpc-services/pom.xml | 145 +++++++++ .../arthas/services/JavaObjectConverter.java | 198 ++++++++++++ .../io/arthas/services/ObjectServiceImpl.java | 139 ++++++++ .../src/main/proto/ArthasServices.proto | 106 ++++++ .../services/JavaObjectConverterTest.java | 301 ++++++++++++++++++ pom.xml | 1 + 6 files changed, 890 insertions(+) create mode 100644 arthas-grpc-services/pom.xml create mode 100644 arthas-grpc-services/src/main/java/io/arthas/services/JavaObjectConverter.java create mode 100644 arthas-grpc-services/src/main/java/io/arthas/services/ObjectServiceImpl.java create mode 100644 arthas-grpc-services/src/main/proto/ArthasServices.proto create mode 100644 arthas-grpc-services/src/test/java/io/arthas/services/JavaObjectConverterTest.java diff --git a/arthas-grpc-services/pom.xml b/arthas-grpc-services/pom.xml new file mode 100644 index 000000000..971ae270d --- /dev/null +++ b/arthas-grpc-services/pom.xml @@ -0,0 +1,145 @@ + + + + arthas-all + com.taobao.arthas + ${revision} + ../pom.xml + + 4.0.0 + arthas-grpc-services + arthas-grpc-services + https://github.com/alibaba/arthas + + 1.8 + 1.46.0 + + + + + io.grpc + grpc-bom + ${grpc.version} + pom + import + + + + + + + + com.taobao.arthas + arthas-common + ${project.version} + + + com.taobao.arthas + arthas-vmtool + ${project.version} + + + io.netty + netty-codec-http + + + com.alibaba.arthas + arthas-repackage-logger + + + io.grpc + grpc-netty + provided + + + io.grpc + grpc-services + provided + + + javax.annotation + javax.annotation-api + 1.3.2 + provided + true + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + test + + + ch.qos.logback + logback-core + test + + + org.junit.vintage + junit-vintage-engine + test + + + org.junit.jupiter + junit-jupiter + test + + + org.apache.httpcomponents + httpmime + 4.5.2 + test + + + + + + mac + + + mac + + + + osx-x86_64 + + + + + ${project.artifactId} + + + kr.motd.maven + os-maven-plugin + 1.6.2 + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.6.1 + + ${basedir}/src/main/proto + com.google.protobuf:protoc:3.11.0:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:1.28.0:exe:${os.detected.classifier} + + + + + compile + compile-custom + + + + + + + + + \ No newline at end of file diff --git a/arthas-grpc-services/src/main/java/io/arthas/services/JavaObjectConverter.java b/arthas-grpc-services/src/main/java/io/arthas/services/JavaObjectConverter.java new file mode 100644 index 000000000..933062304 --- /dev/null +++ b/arthas-grpc-services/src/main/java/io/arthas/services/JavaObjectConverter.java @@ -0,0 +1,198 @@ +package io.arthas.services; + + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import io.arthas.api.ArthasServices.ArrayElement; +import io.arthas.api.ArthasServices.ArrayValue; +import io.arthas.api.ArthasServices.BasicValue; +import io.arthas.api.ArthasServices.CollectionValue; +import io.arthas.api.ArthasServices.CollectionValue.Builder; +import io.arthas.api.ArthasServices.JavaField; +import io.arthas.api.ArthasServices.JavaFields; +import io.arthas.api.ArthasServices.JavaObject; +import io.arthas.api.ArthasServices.MapEntry; +import io.arthas.api.ArthasServices.MapValue; +import io.arthas.api.ArthasServices.NullValue; +import io.arthas.api.ArthasServices.UnexpandedObject; + + + +public class JavaObjectConverter { + private static final int MAX_DEPTH = 5; + + public static JavaObject toJavaObject(Object obj) { + return toJavaObject(obj, 0); + } + + public static JavaObject toJavaObject(Object obj, int depth) { + if (depth >= MAX_DEPTH) { + return null; + } + + if (obj == null) { + return JavaObject.newBuilder().setNullValue(NullValue.getDefaultInstance()).build(); + } + + JavaObject.Builder objectBuilder = JavaObject.newBuilder(); + Class objClazz = obj.getClass(); + objectBuilder.setClassName(objClazz.getName()); + + // 基础类型 + if (isBasicType(objClazz)) { + return objectBuilder.setBasicValue(createBasicValue(obj)).build(); + } else if (obj instanceof Collection) { // 集合 + return objectBuilder.setCollection(createCollectionValue((Collection) obj, depth)).build(); + } else if (obj instanceof Map) { // map + return objectBuilder.setMap(createMapValue((Map) obj, depth)).build(); + } else if (objClazz.isArray()) { + return objectBuilder.setArrayValue(toArrayValue(obj, depth)).build(); + } + + Field[] fields = objClazz.getDeclaredFields(); + List javaFields = new ArrayList<>(); + + for (Field field : fields) { + field.setAccessible(true); + JavaField.Builder fieldBuilder = JavaField.newBuilder(); + fieldBuilder.setName(field.getName()); + + try { + Object fieldValue = field.get(obj); + Class fieldType = field.getType(); + + if (fieldValue == null) { + fieldBuilder.setNullValue(NullValue.newBuilder().setClassName(fieldType.getName()).build()); + } else if (fieldType.isArray()) { + ArrayValue arrayValue = toArrayValue(fieldValue, depth + 1); + if (arrayValue != null) { + fieldBuilder.setArrayValue(arrayValue); + } else { + fieldBuilder.setUnexpandedObject( + UnexpandedObject.newBuilder().setClassName(fieldType.getName()).build()); + } + } else if (fieldType.isPrimitive() || isBasicType(fieldType)) { + BasicValue basicValue = createBasicValue(fieldValue); + fieldBuilder.setBasicValue(basicValue); + } else if (fieldValue instanceof Collection) { // 集合 + fieldBuilder.setCollection(createCollectionValue((Collection) fieldValue, depth)); + } else if (fieldValue instanceof Map) { // map + fieldBuilder.setMap(createMapValue((Map) fieldValue, depth)); + } else { + JavaObject nestedObject = toJavaObject(fieldValue, depth + 1); + if (nestedObject != null) { + fieldBuilder.setObjectValue(nestedObject); + } else { + fieldBuilder.setUnexpandedObject( + UnexpandedObject.newBuilder().setClassName(fieldType.getName()).build()); + } + } + } catch (IllegalAccessException e) { + // TODO ignore ? + } + javaFields.add(fieldBuilder.build()); + } + + objectBuilder.setFields(JavaFields.newBuilder().addAllFields(javaFields).build()); + return objectBuilder.build(); + } + + private static ArrayValue toArrayValue(Object array, int depth) { + if (array == null || depth >= MAX_DEPTH) { + return null; + } + + ArrayValue.Builder arrayBuilder = ArrayValue.newBuilder(); + Class componentType = array.getClass().getComponentType(); + + arrayBuilder.setClassName(componentType.getName()); + + int length = Array.getLength(array); + for (int i = 0; i < length; i++) { + Object element = Array.get(array, i); + + if (element != null) { + if (componentType.isArray()) { + ArrayValue nestedArrayValue = toArrayValue(element, depth + 1); + if (nestedArrayValue == null) { + arrayBuilder.addElements(ArrayElement.newBuilder().setArrayValue(nestedArrayValue)); + } else { + arrayBuilder.addElements(ArrayElement.newBuilder().setUnexpandedObject( + UnexpandedObject.newBuilder().setClassName(element.getClass().getName()).build())); + } + + } else if (componentType.isPrimitive() || isBasicType(componentType)) { + BasicValue basicValue = createBasicValue(element); + arrayBuilder.addElements(ArrayElement.newBuilder().setBasicValue(basicValue)); + } else { + JavaObject nestedObject = toJavaObject(element, depth + 1); + if (nestedObject != null) { + arrayBuilder.addElements(ArrayElement.newBuilder().setObjectValue(nestedObject)); + } else { + arrayBuilder.addElements(ArrayElement.newBuilder().setUnexpandedObject( + UnexpandedObject.newBuilder().setClassName(element.getClass().getName()).build())); + } + + } + } else { + arrayBuilder.addElements(ArrayElement.newBuilder() + .setNullValue(NullValue.newBuilder().setClassName(componentType.getName()).build())); + } + } + + return arrayBuilder.build(); + } + + private static MapValue createMapValue(Map map, int depth) { + MapValue.Builder builder = MapValue.newBuilder(); + + for (Entry entry : map.entrySet()) { + MapEntry mapEntry = MapEntry.newBuilder().setKey(toJavaObject(entry.getKey(), depth)) + .setValue(toJavaObject(entry.getValue(), depth)).build(); + builder.addEntries(mapEntry); + } + return builder.build(); + } + + private static CollectionValue createCollectionValue(Collection collection, int depth) { + Builder builder = CollectionValue.newBuilder(); + for (Object o : collection) { + builder.addElements(toJavaObject(o, depth)); + } + return builder.build(); + } + + private static BasicValue createBasicValue(Object value) { + BasicValue.Builder builder = BasicValue.newBuilder(); + + if (value instanceof Integer) { + builder.setInt((int) value); + } else if (value instanceof Long) { + builder.setLong((long) value); + } else if (value instanceof Float) { + builder.setFloat((float) value); + } else if (value instanceof Double) { + builder.setDouble((double) value); + } else if (value instanceof Boolean) { + builder.setBoolean((boolean) value); + } else if (value instanceof String) { + builder.setString((String) value); + } + + return builder.build(); + } + + private static boolean isBasicType(Class clazz) { + if (String.class.equals(clazz) || Integer.class.equals(clazz) || Long.class.equals(clazz) + || Float.class.equals(clazz) || Double.class.equals(clazz) || Boolean.class.equals(clazz)) { + return true; + } + return false; + } +} \ No newline at end of file diff --git a/arthas-grpc-services/src/main/java/io/arthas/services/ObjectServiceImpl.java b/arthas-grpc-services/src/main/java/io/arthas/services/ObjectServiceImpl.java new file mode 100644 index 000000000..aaba1c8b4 --- /dev/null +++ b/arthas-grpc-services/src/main/java/io/arthas/services/ObjectServiceImpl.java @@ -0,0 +1,139 @@ +package io.arthas.services; + + +import java.lang.instrument.Instrumentation; +import java.lang.invoke.MethodHandles; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.taobao.arthas.common.VmToolUtils; + +import arthas.VmTool; +import io.grpc.Status; +import io.grpc.stub.StreamObserver; +import io.arthas.api.ObjectServiceGrpc.ObjectServiceImplBase; +import io.arthas.api.ArthasServices.JavaObject; +import io.arthas.api.ArthasServices.ObjectQuery; +import io.arthas.api.ArthasServices.ObjectQueryResult; +import io.arthas.api.ArthasServices.ObjectQueryResult.Builder; + +public class ObjectServiceImpl extends ObjectServiceImplBase { + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass().getName()); + + private VmTool vmTool; + private Instrumentation inst; + + public ObjectServiceImpl(Instrumentation inst, String libDir) { + this.inst = inst; + + try { + String detectLibName = VmToolUtils.detectLibName(); + String vmToolLibPath = Paths.get(libDir, detectLibName).toString(); + + logger.info("vmtool lib path: {}", vmToolLibPath); + + vmTool = VmTool.getInstance(vmToolLibPath); + } catch (Throwable e) { + logger.error("init vmtool error", e); + } + } + + @Override + public void query(ObjectQuery query, StreamObserver responseObserver) { + if (vmTool == null) { + throw Status.UNAVAILABLE.withDescription("vmtool can not work").asRuntimeException(); + } + + String className = query.getClassName(); + String classLoaderHash = query.getClassLoaderHash(); + String classLoaderClass = query.getClassLoaderClass(); + int limit = query.getLimit(); + int depth = query.getDepth(); + + // 如果只传递了 class name 参数,则jvm 里可能有多个同名的 class,需要全部查找 + if (isEmpty(classLoaderHash) && isEmpty(classLoaderClass)) { + List> foundClassList = new ArrayList<>(); + for (Class clazz : inst.getAllLoadedClasses()) { + if (clazz.getName().equals(className)) { + foundClassList.add(clazz); + } + } + + // 没找到 + if (foundClassList.size() == 0) { + responseObserver.onNext(ObjectQueryResult.newBuilder().setSuccess(false) + .setMessage("can not find class: " + className).build()); + responseObserver.onCompleted(); + return; + } else if (foundClassList.size() > 1) { + String message = "found more than one class: " + className; + responseObserver.onNext(ObjectQueryResult.newBuilder().setSuccess(false).setMessage(message).build()); + responseObserver.onCompleted(); + return; + } else { // 找到了指定的 类 + Object[] instances = vmTool.getInstances(foundClassList.get(0), limit); + Builder builder = ObjectQueryResult.newBuilder().setSuccess(true); + for (Object obj : instances) { + JavaObject javaObject = JavaObjectConverter.toJavaObject(obj, depth); + builder.addObjects(javaObject); + } + responseObserver.onNext(builder.build()); + responseObserver.onCompleted(); + return; + } + } + + // 有指定 classloader hash 或者 classloader className + + Class foundClass = null; + + for (Class clazz : inst.getAllLoadedClasses()) { + if (!clazz.getName().equals(className)) { + continue; + } + + ClassLoader classLoader = clazz.getClassLoader(); + + if (classLoader == null) { + continue; + } + + if (!isEmpty(classLoaderHash)) { + String hex = Integer.toHexString(classLoader.hashCode()); + if (classLoaderHash.equals(hex)) { + foundClass = clazz; + break; + } + } + + if (!isEmpty(classLoaderClass) && classLoaderClass.equals(classLoader.getClass().getName())) { + foundClass = clazz; + break; + } + } + // 没找到类 + if (foundClass == null) { + responseObserver.onNext(ObjectQueryResult.newBuilder().setSuccess(false) + .setMessage("can not find class: " + className).build()); + responseObserver.onCompleted(); + return; + } + + Object[] instances = vmTool.getInstances(foundClass, limit); + Builder builder = ObjectQueryResult.newBuilder().setSuccess(true); + for (Object obj : instances) { + JavaObject javaObject = JavaObjectConverter.toJavaObject(obj, depth); + builder.addObjects(javaObject); + } + responseObserver.onNext(builder.build()); + responseObserver.onCompleted(); + } + + public static boolean isEmpty(Object str) { + return str == null || "".equals(str); + } +} diff --git a/arthas-grpc-services/src/main/proto/ArthasServices.proto b/arthas-grpc-services/src/main/proto/ArthasServices.proto new file mode 100644 index 000000000..c3dd7ba0d --- /dev/null +++ b/arthas-grpc-services/src/main/proto/ArthasServices.proto @@ -0,0 +1,106 @@ +syntax = "proto3"; + +package io.arthas.api; + +message BasicValue { + oneof value { + int32 int = 1; + int64 long = 2; + float float = 3; + double double = 4; + bool boolean = 5; + string string = 6; + } +} + +message ArrayElement { + oneof element { + BasicValue basicValue = 1; + JavaObject objectValue = 2; + ArrayValue arrayValue = 3; + NullValue nullValue = 4; + UnexpandedObject unexpandedObject = 5; + } +} + +message ArrayValue { + string className = 1; + repeated ArrayElement elements = 2; +} + +message NullValue { + string className = 1; +} + +message UnexpandedObject { + string className = 1; +} + +message CollectionValue { + string className = 1; + repeated JavaObject elements = 2; +} + +message MapEntry { + JavaObject key = 1; + JavaObject value = 2; +} + +message MapValue { + string className = 1; + repeated MapEntry entries = 2; +} + +message JavaField { + string name = 1; + + oneof value { + JavaObject objectValue = 2; + BasicValue basicValue = 3; + ArrayValue arrayValue = 4; + NullValue nullValue = 5; + CollectionValue collection = 6; + MapValue map = 7; + UnexpandedObject unexpandedObject = 8; + } +} + + +message JavaFields { + repeated JavaField fields = 1; +} + +message JavaObject { + string className = 1; + + oneof value { + JavaObject objectValue = 2; + BasicValue basicValue = 3; + ArrayValue arrayValue = 4; + NullValue nullValue = 5; + CollectionValue collection = 6; + MapValue map = 7; + UnexpandedObject unexpandedObject = 8; + JavaFields fields = 9; + } +} + + +message ObjectQuery { + string className = 1; + string express = 2; + string ClassLoaderHash = 3; + string classLoaderClass = 4; + int32 limit = 5; + int32 depth = 6; +} + +message ObjectQueryResult { + bool success = 1; + string message = 2; + repeated JavaObject objects = 3; +} + +service ObjectService { + rpc query(ObjectQuery) returns (ObjectQueryResult); +} \ No newline at end of file diff --git a/arthas-grpc-services/src/test/java/io/arthas/services/JavaObjectConverterTest.java b/arthas-grpc-services/src/test/java/io/arthas/services/JavaObjectConverterTest.java new file mode 100644 index 000000000..5988b15ce --- /dev/null +++ b/arthas-grpc-services/src/test/java/io/arthas/services/JavaObjectConverterTest.java @@ -0,0 +1,301 @@ +package io.arthas.services; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.Assert; +import org.junit.Test; + +import io.arthas.api.ArthasServices.ArrayElement; +import io.arthas.api.ArthasServices.ArrayValue; +import io.arthas.api.ArthasServices.BasicValue; +import io.arthas.api.ArthasServices.CollectionValue; +import io.arthas.api.ArthasServices.JavaField; +import io.arthas.api.ArthasServices.JavaFields; +import io.arthas.api.ArthasServices.JavaObject; +import io.arthas.api.ArthasServices.MapEntry; +import io.arthas.api.ArthasServices.MapValue; +import io.arthas.api.ArthasServices.NullValue; + +public class JavaObjectConverterTest { + + @Test + public void testString() { + JavaObject javaObject = JavaObjectConverter.toJavaObject("sss"); + System.err.println(javaObject); + assertNotNull(javaObject); + assertEquals("java.lang.String", javaObject.getClassName()); + assertTrue(javaObject.hasBasicValue()); + assertEquals("sss", javaObject.getBasicValue().getString()); + } + + @Test + public void testToJavaObjectWithBasicType() { + int intValue = 123; + JavaObject javaObject = JavaObjectConverter.toJavaObject(intValue); + assertNotNull(javaObject); + assertEquals("java.lang.Integer", javaObject.getClassName()); + assertTrue(javaObject.hasBasicValue()); + assertEquals(intValue, javaObject.getBasicValue().getInt()); + } + + @Test + public void testToJavaObjectWithArray() { + int[] intArray = { 1, 2, 3 }; + JavaObject javaObject = JavaObjectConverter.toJavaObject(intArray); + assertNotNull(javaObject); + assertEquals("[I", javaObject.getClassName()); + assertTrue(javaObject.hasArrayValue()); + ArrayValue arrayValue = javaObject.getArrayValue(); + assertNotNull(arrayValue); + assertEquals("int", arrayValue.getClassName()); + assertEquals(3, arrayValue.getElementsCount()); + + ArrayElement element = arrayValue.getElements(1); + assertEquals(2, element.getBasicValue().getInt()); + } + + @Test + public void testToJavaObjectWithCollection() { + List stringList = new ArrayList<>(); + stringList.add("foo"); + stringList.add("bar"); + stringList.add("baz"); + JavaObject javaObject = JavaObjectConverter.toJavaObject(stringList); + assertNotNull(javaObject); + assertEquals("java.util.ArrayList", javaObject.getClassName()); + assertTrue(javaObject.hasCollection()); + CollectionValue collectionValue = javaObject.getCollection(); + assertNotNull(collectionValue); + assertEquals(3, collectionValue.getElementsCount()); + + JavaObject object3 = collectionValue.getElements(2); + assertEquals("baz", object3.getBasicValue().getString()); + } + + @Test + public void testToJavaObjectWithMap() { + Map stringIntegerMap = new HashMap<>(); + stringIntegerMap.put("one", 1); + stringIntegerMap.put("two", 2); + JavaObject javaObject = JavaObjectConverter.toJavaObject(stringIntegerMap); + assertNotNull(javaObject); + assertEquals("java.util.HashMap", javaObject.getClassName()); + assertTrue(javaObject.hasMap()); + MapValue mapValue = javaObject.getMap(); + assertNotNull(mapValue); + assertEquals(2, mapValue.getEntriesCount()); + + MapEntry mapEntry = mapValue.getEntries(0); + + JavaObject key = mapEntry.getKey(); + assertEquals("one", key.getBasicValue().getString()); + JavaObject value = mapEntry.getValue(); + assertEquals(1, value.getBasicValue().getInt()); + } + + @Test + public void testToJavaObject() { + // 创建一个复杂的 Object + ComplexObject complexObject = createComplexObject(); + + // 转换为 JavaObject + JavaObject javaObject = JavaObjectConverter.toJavaObject(complexObject); + + // 对转换后的 JavaObject 进行断言,验证各个 field 的值是否一致 + Assert.assertNotNull(javaObject); + Assert.assertEquals(ComplexObject.class.getName(), javaObject.getClassName()); + + JavaFields fields = javaObject.getFields(); + + Map fieldMap = fields.getFieldsList().stream() + .collect(Collectors.toMap(JavaField::getName, field -> field)); + + // 验证基础类型字段 + BasicValue basicValue = fieldMap.get("basicValue").getBasicValue(); + Assert.assertEquals(5, basicValue.getInt()); + + // 验证集合字段 + JavaField collection = fieldMap.get("collection"); + CollectionValue collectionValue = collection.getCollection(); + + Assert.assertEquals(2, collectionValue.getElementsCount()); + + // 验证数组字段 + JavaField array = fieldMap.get("arrayValue"); + ArrayValue arrayValue = array.getArrayValue(); + Assert.assertEquals(2, arrayValue.getElementsCount()); + + // 验证嵌套对象字段 + JavaField nestedObject = fieldMap.get("nestedObject"); + JavaObject nestedJavaObject = nestedObject.getObjectValue(); + JavaFields nestedObjectFields = nestedJavaObject.getFields(); + Assert.assertEquals(1, nestedObjectFields.getFieldsCount()); + JavaField nestedObjectField = nestedObjectFields.getFields(0); + Assert.assertEquals("stringValue", nestedObjectField.getName()); + Assert.assertEquals("nestedValue", nestedObjectField.getBasicValue().getString()); + } + + private ComplexObject createComplexObject() { + ComplexObject complexObject = new ComplexObject(); + complexObject.setBasicValue(5); + complexObject.setCollection(Arrays.asList("element1", "element2")); + complexObject.setArrayValue(new int[] { 1, 2 }); + complexObject.setNestedObject(new NestedObject("nestedValue")); + return complexObject; + } + + private static class ComplexObject { + private int basicValue; + private Collection collection; + private int[] arrayValue; + private NestedObject nestedObject; + + public int getBasicValue() { + return basicValue; + } + + public void setBasicValue(int basicValue) { + this.basicValue = basicValue; + } + + public Collection getCollection() { + return collection; + } + + public void setCollection(Collection collection) { + this.collection = collection; + } + + public int[] getArrayValue() { + return arrayValue; + } + + public void setArrayValue(int[] arrayValue) { + this.arrayValue = arrayValue; + } + + public NestedObject getNestedObject() { + return nestedObject; + } + + public void setNestedObject(NestedObject nestedObject) { + this.nestedObject = nestedObject; + } + } + + private static class NestedObject { + private String stringValue; + + public NestedObject(String stringValue) { + this.stringValue = stringValue; + } + + public String getStringValue() { + return stringValue; + } + + public void setStringValue(String stringValue) { + this.stringValue = stringValue; + } + } + + private static class TestObject { + private Double[] doubleArray; + + public Double[] getDoubleArray() { + return doubleArray; + } + + public void setDoubleArray(Double[] doubleArray) { + this.doubleArray = doubleArray; + } + } + + @Test + public void testObjectWithDubboArrayField() { + // 创建测试对象 + TestObject testObject = new TestObject(); + testObject.setDoubleArray(new Double[] { 1.0, 2.0, 3.0 }); + + // 转换为JavaObject + JavaObject javaObject = JavaObjectConverter.toJavaObject(testObject); + + // 检查各个field的值是否一致 + for (int i = 0; i < testObject.getDoubleArray().length; i++) { + Double expectedValue = testObject.getDoubleArray()[i]; + ArrayValue arrayValue = javaObject.getFields().getFields(0).getArrayValue(); + ArrayElement arrayElement = arrayValue.getElements(i); + Double actualValue = arrayElement.getBasicValue().getDouble(); + Assert.assertEquals(expectedValue, actualValue); + } + } + + @Test + public void testToJavaObjectWithNullValue() { + JavaObject result = JavaObjectConverter.toJavaObject(null); + assertNotNull(result); + assertTrue(result.hasNullValue()); + assertEquals(NullValue.getDefaultInstance(), result.getNullValue()); + } + + @Test + public void testToJavaObjectWithNullKeyInMap() { + Map map = new HashMap<>(); + map.put(null, "value"); + + JavaObject result = JavaObjectConverter.toJavaObject(map); + assertNotNull(result); + assertTrue(result.hasMap()); + MapValue mapValue = result.getMap(); + assertEquals(1, mapValue.getEntriesCount()); + + MapEntry entry = mapValue.getEntries(0); + assertNotNull(entry.getKey()); + assertTrue(entry.getKey().hasNullValue()); + assertEquals(NullValue.getDefaultInstance(), entry.getKey().getNullValue()); + + assertNotNull(entry.getValue()); + assertTrue(entry.getValue().hasBasicValue()); + + assertEquals("value", entry.getValue().getBasicValue().getString()); + } + + @Test + public void testToJavaObjectWithNullValueInArray() { + Object[] array = new Object[3]; + array[0] = "value"; + array[1] = null; + array[2] = 123; + + JavaObject result = JavaObjectConverter.toJavaObject(array); + assertNotNull(result); + assertTrue(result.hasArrayValue()); + ArrayValue arrayValue = result.getArrayValue(); + assertEquals(3, arrayValue.getElementsCount()); + + ArrayElement element1 = arrayValue.getElements(0); + assertTrue(element1.hasObjectValue()); + JavaObject objectValue1 = element1.getObjectValue(); + assertTrue(objectValue1.hasBasicValue()); + assertEquals("value", objectValue1.getBasicValue().getString()); + + ArrayElement element2 = arrayValue.getElements(1); + assertNotNull(element2.getNullValue()); + + ArrayElement element3 = arrayValue.getElements(2); + assertTrue(element3.hasObjectValue()); + JavaObject objectValue3 = element3.getObjectValue(); + assertTrue(objectValue3.hasBasicValue()); + assertEquals(123, objectValue3.getBasicValue().getInt()); + } +} diff --git a/pom.xml b/pom.xml index 2ab112686..9cffbd9ff 100644 --- a/pom.xml +++ b/pom.xml @@ -76,6 +76,7 @@ site packaging arthas-grpc-web-proxy + arthas-grpc-services