srpingboot方法入参测试数据自动生成方法

摘要 针对在接口动态调用场景,复杂业务接口存在复杂业务对象,人为构造测试数据十分繁琐的问题。本文通过分析复杂对象的内部特点,针对不同数据类型,使用反射实例化及递归序列化等方式,自动实现复杂入参的动态生成。可极大提升构造复杂请求参数开发效率。
关键词: 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 逻辑分析

对象内部分类初始化
yes
yes
yes
no
no
no
类型判断开始
结束
基本数据初始化
是否基本数据类型
包装类型初始化
包装数据类型
集合带泛型类型
自定义数据初始化
反射遍历每个字段
接口入参初始化
解决复杂java对象的默认初始化

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,模拟生成更有意义的数据。

  • 27
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值