文章目录
摘要 针对在接口动态调用场景,复杂业务接口存在复杂业务对象,人为构造测试数据十分繁琐的问题。本文通过分析复杂对象的内部特点,针对不同数据类型,使用反射实例化及递归序列化等方式,自动实现复杂入参的动态生成。可极大提升构造复杂请求参数开发效率。
关键词: java;动态生成;接口调用;springboot
1. 背景
在低代码开发设计工作中,通过会使用后端服务编排,来实现较为复杂的业务能力。而springboot项目后端服务编排,如果要实现不实际编码,动态实时生效。需要提供后端动态bean调用能力,从而代替手写Service组合业务的目的。这个功能并不难,熟悉java反射,结合spring的ApplicationContext即可实现。
后端调用伪代码:
Object bean = applicationContext.getBean(beanName);
Method method = ClassInvokeUtils.findMethod(bean, methodName);
// 调用方法
ReflectionUtils.invokeMethod(method, target, args); // 【1】
前端请求示例:
{
"beanName": "bizLogicBean",
"methodName": "batchCreate",
"parameters": {
// 业务参数 【2】
}
}
问题在于【1】args参数,需要通过前端提交【2】parameters。与此同时,流程编排人员,不知道数据结构(后端代码不可见),就无法准确调用后端的业务能力。
如何设计一个工具,实现能够快速准确构造请求参数示例,保证使用者只要更改数据内容而不用思考数据结构,就是本文的研究内容。
2. 分析
通过类比分析法,这个需求实际上和java后端的接口文档框架生成请求参数的需求类似。如下
但是动态调用的接口非web接口,无法利用现成的框架辅助生成。
2.1 逻辑分析
3. 技术实现
3.1 java类型判断工具实现
/**
class的类型的判断工具类
getWrapperTypes() 方法:初始化包含所有基础类型包装类的集合。
getCommonJavaTypes() 方法:初始化包含常用 Java 类的集合,例如 String, BigDecimal, BigInteger, java.util.Date, java.sql.Date, 和 java.sql.Timestamp。
isPrimitiveOrWrapper(Class<?> clazz) 方法:检查类是否是基础类型或其包装类。
isJavaStandardLibrary(Class<?> clazz) 方法:检查类是否属于 Java 标准库。
isCommonJavaType(Class<?> clazz) 方法:检查类是否是常用 Java 类。
isCustomClass(Class<?> clazz) 方法:综合以上三个方法,来判断是否是自定义类。
*/
public class ClassTypeUtils {
// 基础类型和其包装类的集合
private static final Set<Class<?>> WRAPPER_TYPES = getWrapperTypes();
// 常用 Java 类的集合
private static final Set<Class<?>> COMMON_JAVA_TYPES = getCommonJavaTypes();
private static Set<Class<?>> getWrapperTypes() {
Set<Class<?>> ret = new HashSet<>();
ret.add(Boolean.class);
ret.add(Character.class);
ret.add(Byte.class);
ret.add(Short.class);
ret.add(Integer.class);
ret.add(Long.class);
ret.add(Float.class);
ret.add(Double.class);
ret.add(Void.class);
return ret;
}
private static Set<Class<?>> getCommonJavaTypes() {
Set<Class<?>> ret = new HashSet<>();
ret.add(String.class);
ret.add(BigDecimal.class);
ret.add(BigInteger.class);
ret.add(java.util.Date.class);
ret.add(java.sql.Date.class);
ret.add(java.sql.Timestamp.class);
return ret;
}
// 检查是否是基础类型或其包装类
public static boolean isPrimitiveOrWrapper(Class<?> clazz) {
return clazz.isPrimitive() || WRAPPER_TYPES.contains(clazz);
}
// 检查是否是 Java 标准库类
public static boolean isJavaStandardLibrary(Class<?> clazz) {
if (clazz.getPackage() == null) {
return false; // 无包名的类不可能是标准库类
}
String packageName = clazz.getPackage().getName();
return packageName.startsWith("java.") || packageName.startsWith("javax.");
}
// 检查是否是常用 Java 类
public static boolean isCommonJavaType(Class<?> clazz) {
return COMMON_JAVA_TYPES.contains(clazz);
}
// 检查是否是自定义类
public static boolean isCustomClass(Class<?> clazz) {
return !isPrimitiveOrWrapper(clazz) && !isJavaStandardLibrary(clazz) && !isCommonJavaType(clazz);
}
public static void main(String[] args) {
Class<?>[] testClasses = {
int.class, Integer.class, String.class, boolean.class, Boolean.class, Object.class,
BigDecimal.class, java.util.Date.class, ClassTypeUtils.class, CustomClass.class
};
for (Class<?> clazz : testClasses) {
System.out.println(clazz.getName() + " is custom class: " + isCustomClass(clazz));
}
}
// 示例自定义类
public static class CustomClass {
// some fields and methods
}
}
3.2 类默认值生成
/**
* 类默认值工具类(利用json工具类可以生成默认json值)
*/
public class ClassDefaultValueUtils {
public static Map<String, Object> generateSampleParameters(Method method) throws Exception {
Map<String, Object> sampleParams = new LinkedHashMap<>();
for (int i = 0; i < method.getParameterCount(); i++) {
MethodParameter parameter = new MethodParameter(method, i);
parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
String paramName = parameter.getParameterName();
Type paramType = parameter.getGenericParameterType();
sampleParams.put(paramName, getDefaultValue(paramType, new HashSet<>()));
}
return sampleParams;
}
public static Object getDefaultValue(Type type) throws Exception {
return getDefaultValue(type, new HashSet<>());
}
private static Object getDefaultValue(Type type, Set<Type> visitedTypes) throws Exception {
if (visitedTypes.contains(type)) {
return null; // 防止循环引用
}
// 一般自定义类里又持有对象本身
if (ClassTypeUtils.isCustomClass(type.getClass())) {
visitedTypes.add(type);
}
if (type instanceof Class<?>) {
return getDefaultValue((Class<?>) type, visitedTypes, new Type[]{});
} else if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Type rawType = parameterizedType.getRawType();
if (rawType instanceof Class<?>) {
Class<?> rawClass = (Class<?>) rawType;
if (List.class.isAssignableFrom(rawClass) && parameterizedType.getActualTypeArguments().length == 1) {
List<Object> sampleList = new ArrayList<>();
sampleList.add(getDefaultValue(parameterizedType.getActualTypeArguments()[0], visitedTypes));
return sampleList;
} else if (Map.class.isAssignableFrom(rawClass) && parameterizedType.getActualTypeArguments().length == 2) {
Map<Object, Object> sampleMap = new HashMap<>();
sampleMap.put(getDefaultValue(parameterizedType.getActualTypeArguments()[0], visitedTypes), getDefaultValue(parameterizedType.getActualTypeArguments()[1], visitedTypes));
return sampleMap;
}
}
}
return null;
}
private static Object getDefaultValue(Class<?> clazz, Set<Type> visitedTypes, Type... genericTypes) throws Exception {
if (clazz.isPrimitive()) {
return getPrimitiveDefaultValue(clazz);
} else if (clazz == String.class) {
return "sampleString";
} else if (clazz == Integer.class) {
return 0;
} else if (clazz == Long.class) {
return 0L;
} else if (clazz == Boolean.class) {
return false;
} else if (clazz == Double.class) {
return 0.0;
} else if (clazz == Date.class) {
return new Date();
} else if (clazz == List.class) {
if (genericTypes.length == 1) {
return Collections.singletonList(getDefaultValue(genericTypes[0], visitedTypes));
}
return Collections.emptyList();
} else if (clazz == Map.class) {
if (genericTypes.length == 2) {
return Collections.singletonMap(
getDefaultValue(genericTypes[0], visitedTypes),
getDefaultValue(genericTypes[1], visitedTypes)
);
}
return Collections.emptyMap();
} else {
return instantiateClass(clazz, visitedTypes);
}
}
private static Object getPrimitiveDefaultValue(Class<?> clazz) {
if (clazz == boolean.class) {
return false;
} else if (clazz == byte.class) {
return (byte) 0;
} else if (clazz == short.class) {
return (short) 0;
} else if (clazz == int.class) {
return 0;
} else if (clazz == long.class) {
return 0L;
} else if (clazz == float.class) {
return 0.0f;
} else if (clazz == double.class) {
return 0.0;
} else if (clazz == char.class) {
return '\0';
}
return null;
}
private static Object instantiateClass(Class<?> clazz, Set<Type> visitedTypes) throws Exception {
Object instance = clazz.getDeclaredConstructor().newInstance();
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
field.set(instance, getDefaultValue(field.getGenericType(), visitedTypes));
}
return instance;
}
}
4 应用测试
4.1 任意java类生成json测试数据
@RestController
@RequestMapping("/v3/")
public class DynamicCallControllerV3 {
@Autowired
private ApplicationContext applicationContext;
@GetMapping("/generateDefaultValue")
@ResponseBody
public Object generateDefaultValue(@RequestParam String className) throws Exception {
Class<?> clazz = Class.forName(className);
return ClassDefaultValueUtils.getDefaultValue(clazz);
}
}
- 效果
4.2 生成方法的测试数据
@RestController
@RequestMapping("/v3/")
public class DynamicCallControllerV3 {
@Autowired
private ApplicationContext applicationContext;
@GetMapping("/generateSampleJson")
@ResponseBody
public Map<String, Object> generateSampleJson(@RequestParam String beanName, @RequestParam String methodName) throws Exception {
Object bean = applicationContext.getBean(beanName);
Method method = ClassInvokeUtils.findMethod(bean, methodName);
Map<String, Object> sampleJson = new LinkedHashMap<>();
sampleJson.put("beanName", beanName);
sampleJson.put("methodName", methodName);
sampleJson.put("parameters", ClassDefaultValueUtils.generateSampleParameters(method));
return sampleJson;
}
}
- bean方法及模拟复杂对象
@Service
public class BizLogicBean {
public List<UserDO> batchCreate(List<UserDO> users, List<String> blackList) {
return users;
}
}
public class UserDO {
private String nick;
private Long userId;
private Date gmtCreate;
private String position;
private Double salary;
private MyClass myClass;
private Map<String, List<MyClass>> map;
private Map<String, MyClass> map2;
public Map<String, List<MyClass>> getMap() {
return map;
}
}
- 效果
5 总结
本文从接口动态调用场景的模拟造数据问题,出发分析任意方法接口的模拟测试数据功能设计。封装实现后,可以快速造请求数据结构,提升效率,可以作为自动化测试的基础功能。当然目前还存在一些不足,数据内容业务性还是太弱了,显得"不聪明"。后续可以再结合javafaker,模拟生成更有意义的数据。