在使用 SpringBoot 的默认序列化方式 Jackson 时,如果在执行 RPC,并且参数中含有 Map 类型的字段时,如果不做特别的处理,会抛出异常,提示参数不能是 LinkedHashMap,这遍文章将探寻报错的原因与解决方法。

当调用一个参数为Map的方法时(在 RPC 调用中常常出现),例如如下的方法:

public void setRequestParam(Map<String, Object> requestParam) {
  this.requestParam = requestParam;
}

为什么使用反射调用会报类型转化异常?

反射调用分为两部,第一步时找到反射方法,第二步时执行反射调用,在获取反射方法时,执行的具体方法是:

Method method = clazz.getMethod(setter, value.getClass());

value.getClass() 返回的是 LinkedHashMap 实例,但是在类中并没有 参数类型为 LinkedHashMap 的方法,比如:

public void setRequestParam(LinkedHashMap<String, Object> requestParam) {
  this.requestParam = requestParam;
}

所以会报错。

所以在获取方法的方式不能是指定具体的参数类型,而是应该遍历类的具体方法,例如:

// 查找匹配的方法
Method targetMethod = null;
for (Method method : clazz.getMethods()) {
  if (method.getName().equals(methodName) && method.getParameterCount() == 1) {
    Class<?> parameterType = method.getParameterTypes()[0];
    if (parameterType.isAssignableFrom(LinkedHashMap.class)) {
      targetMethod = method;
      break;
    }
  }
}

这样才能无视 Map 的接口或者是 Map 的子类,执行反射调用,但是这也会有一个问题,执行某个同名方法的重载时可能会找到到方法导致报错。

完整的工具类示例下:

public class BeanUtils {

  /**
     * 将字符串的首字母大写。
     *
     * @param str 需要处理的字符串。
     * @return 首字母大写的字符串。
     */
  private static String capitalize(String str) {
    if (str == null || str.isEmpty()) {
      return str;
    }
    return str.substring(0, 1).toUpperCase() + str.substring(1);
  }

  /**
     * 将Java对象的字段和属性转换为Map。
     *
     * @param object Java对象
     * @return Map类型的键值对
     */
  public static Map<String, Object> objectToMap(Object object) {
    Map<String, Object> map = new HashMap<>();
    Class<?> clazz = object.getClass();
    for (Field field : clazz.getDeclaredFields()) {
      field.setAccessible(true);
      try {
        map.put(field.getName(), field.get(object));
      } catch (IllegalAccessException e) {
        e.printStackTrace();
      }
    }
    return map;
  }

  /**
     * 将JSON字符串转换为Map。
     *
     * @param jsonString JSON字符串
     * @return Map类型的键值对
     */
  public static Map jsonToMap(String jsonString) {
    ObjectMapper objectMapper = new ObjectMapper();
    try {
      return objectMapper.readValue(jsonString, Map.class);
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }

  /**
     * 将Map转换为特定类型的对象。
     *
     * @param map   包含属性值的Map。
     * @param clazz 目标对象的Class。
     * @param <T>   目标对象的类型。
     * @return 转换后的对象。
     * @throws Exception 可能抛出的异常。
     */
  public static <T> T mapToObject(Map<String, Object> map, Class<T> clazz) throws Exception {
    T instance = clazz.newInstance();
    if (null == map && map.isEmpty()) {
      return null;
    }
    for (Map.Entry<String, Object> entry : map.entrySet()) {
      String key = entry.getKey();
      Object value = entry.getValue();
      // 尝试获取属性名对应的setter方法
      String methodName = "set" + capitalize(key);
      // 查找匹配的方法
      Method targetMethod = null;
      for (Method method : clazz.getMethods()) {
        if (method.getName().equals(methodName) && method.getParameterCount() == 1) {
          Class<?> parameterType = method.getParameterTypes()[0];
          if (parameterType.isAssignableFrom(LinkedHashMap.class)) {
            targetMethod = method;
            break;
          }
        }
      }
      if (targetMethod != null) {
        targetMethod.invoke(instance, value);
      }
    }
    return instance;
  }
}