由一个小bug引发的脱发文章,知道真相的我眼泪掉下来😶

1,448 阅读12分钟

前几天调试公司项目的时候遇见了一个very奇怪的问题,今天在这里研究一下

场景

由于公司这边新增加了一个PDA的接口用来查询历史的停车流水数据,我首先从数据库里查除符合要求的数据,并用Stream流循环出流水数据的list,对循环出来的对象进行处理后面put到JSONObject,实际上put流水时,我并没有设置mMap这个键,但是测试时候的返回结果的每一层都给我包裹了一个mMap键,下面是一部分的代码

dayTraceParkPage = dayTraceParkService.selectByHashMap(queryMap,initPage);
List<DayTracePark> dayTraceParkList = dayTraceParkPage.getData();
if (null != dayTraceParkList && dayTraceParkList.size() > 0) {
   dayTraceParkList.stream().peek(e -> {
		
		JSONObject res = new JSONObject();
		// ... 	 这里省略一部分代码 
		// 	
		res.put("traceParkTraceDay", e);
		jsonArray.add(res);
		}).collect(Collectors.toList());
		}
		
return AjaxUtils.printJson(new JSONResult<Object>(jsonArray,"未关联历史流水表,获取在场流水成功", true));	
		
// 返回结果 控制器我并没有设置mMap节点
{
    "data": [
        {
            "mMap": {
                "traceParkTraceDay": {
                    // ... 省略一部分数据
                },
                "traceTimeView": "1月9天"
            }
        },
    ],
    "message": "未关联历史流水表,获取在场流水成功",
    "statusCode": 0,
    "success": true
}

开始我以为是jdk8(代码中的dayTraceParkList.stream().peek)语法的问题,我就又改用普通的加强for循环,但是结果还是一样的,开始我百思不得其解,后来,我一看发现是引错了包,我用的JSONObject是com.alibaba.dubbo.common 下面的,实际上我应该用 fastjson下面的包(菜得卑微😂),知道真相的我眼泪掉下来,不过我还是决定看一下dubbo包下面的JSONObject为什么会自动添加一个mMap节点, 基于上面这一个,我写了一个简单的main方法模拟一下,代码如下:

// ps:我没有创建maven,直接导入的jar, dubbo-2.5.3 and gson-2.6.1
import com.alibaba.dubbo.common.json.JSONObject;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class Test {
	public static void main(String[] args) {
		System.out.println(test());
	}

	private static String test() {
		JSONObject jsonObj = new JSONObject();
		
		jsonObj.put("name", "谭婧杰");
		Gson gson = new GsonBuilder().serializeNulls().create();
		return gson.toJson(jsonObj);
	}
}

// 输出结果:
{"mMap":{"name":"谭婧杰"}}

开始

因为我们先new了一个JSONObject之后,调用了他的put方法,我们追入看一下他的源码(我这里看见了一个mMap,我有点怀疑我开始的返回值就是这个mMap)

// mMap 的本质就是一个HashMap, private Map<String, Object> mMap = new HashMap();
// 也就是说我向jsonObj中put一个值,相当于是put一个值到map中
 public void put(String name, Object value)
  {
    this.mMap.put(name, value);
  }

随后我调用了new GsonBuilder().serializeNulls().create(),new 一个GsonBuilder,这里我寻思应该调用了建造者模式创建对象,其实还可以直接用new Gson对象来创建一个Gson,使用GsonBuilder是因为他可以设置各种组件来创建特殊的Gson对象,其实也推荐用GsonBuilder创建,这种点点点的方式就叫链式调用,使用Lombok里的注解@Builder也可以生成相对略微复杂的构建器API

// new GsonBuilder().serializeNulls()只是设置了serializeNulls为true,并且直接返回了一个可以配置的Gson

// Gson在默认情况下序列化的时候是不导出值是null的属性的,配置了serializeNulls以后就会导出值为null的对象,主要应该是方便调试之类的
 public GsonBuilder serializeNulls()
  {
    this.serializeNulls = true;
    return this;
  }

继续调用create()

 public Gson create()
  {
    List factories = new ArrayList();
    // 此this.factories 不是刚刚new出来的factories,而是全局变量  private final List<TypeAdapterFactory> factories = new ArrayList();
   // 把GsonBuilder的factories传入到新定义的factories中,并交给新创建的Gson对象。
    factories.addAll(this.factories);
    // 使用Collections.reverse结合一定方法可以实现对list集合降序排序,但是不能直接使用Collections.reverse(list)这种方式来降序,下面我也写了一个demo
    //
    Collections.reverse(factories); 
    factories.addAll(this.hierarchyFactories);
    addTypeAdaptersForDate(this.datePattern, this.dateStyle, this.timeStyle, factories);

    return new Gson(this.excluder, this.fieldNamingPolicy, this.instanceCreators, this.serializeNulls, this.complexMapKeySerialization, this.generateNonExecutableJson, this.escapeHtmlChars, this.prettyPrinting, this.lenient, this.serializeSpecialFloatingPointValues, this.longSerializationPolicy, factories);
  }

其中获取了实例对象之后,我们看一下 gson.toJson(jsonObj);

public String toJson(Object src)
  {
    if (src == null) {
    // 如果src为空 返回一个JsonNull()对象
      return toJson(JsonNull.INSTANCE);
    }
    // 否则调用带两个入参的toJson()方法,传人src值和类型
    // 如果任何对象字段都是泛型类型,只是对象本身不应该是泛型类型,则可以用toJson(Object)。
如果对象是泛型类型,则使用toJson(Object,Type)来代替
    return toJson(src, src.getClass());
  }

  public String toJson(Object src, Type typeOfSrc)
  {
    StringWriter writer = new StringWriter();
    toJson(src, typeOfSrc, writer);
    return writer.toString();
  }
  // 继续
   public void toJson(Object src, Type typeOfSrc, Appendable writer)
    throws JsonIOException
  {
    try
    {
      JsonWriter jsonWriter = newJsonWriter(Streams.writerForAppendable(writer));
      toJson(src, typeOfSrc, jsonWriter);
    } catch (IOException e) {
      throw new JsonIOException(e);
    }
  }
  // 上面都是toJson重载方法,最后调用的是toJson(Object src, Type typeOfSrc, JsonWriter writer) JsonWriter是Gson序列化的主体
  
public void toJson(Object src, Type typeOfSrc, JsonWriter writer)
    throws JsonIOException
  {
  // 根据传入的Type得到对应的TypeAdapter,TypeToken主要是获取泛型信息的
    TypeAdapter adapter = getAdapter(TypeToken.get(typeOfSrc));
    boolean oldLenient = writer.isLenient();
    // 设置宽松的容错性(顶级值可以不是为object/array,数字可以为无穷)
    writer.setLenient(true);
    boolean oldHtmlSafe = writer.isHtmlSafe();
    // html 转义
    writer.setHtmlSafe(this.htmlSafe);
    boolean oldSerializeNulls = writer.getSerializeNulls();
    // 序列化 
    writer.setSerializeNulls(this.serializeNulls);
    try {
    //输出,完成整个序列化过程
      adapter.write(writer, src);
    } catch (IOException e) {
      throw new JsonIOException(e);
    } finally {
      writer.setLenient(oldLenient);
      writer.setHtmlSafe(oldHtmlSafe);
      writer.setSerializeNulls(oldSerializeNulls);
    }
  }
public <T> TypeAdapter<T> getAdapter(TypeToken<T> type)
  {
    TypeAdapter cached = (TypeAdapter)this.typeTokenCache.get(type);
    if (cached != null) {
      return cached;
    }

    Map threadCalls = (Map)this.calls.get();
    boolean requiresThreadLocalCleanup = false;
    if (threadCalls == null) {
      threadCalls = new HashMap();
      this.calls.set(threadCalls);
      requiresThreadLocalCleanup = true;
    }

    FutureTypeAdapter ongoingCall = (FutureTypeAdapter)threadCalls.get(type);
    if (ongoingCall != null) {
      return ongoingCall;
    }
    try
    {
      FutureTypeAdapter call = new FutureTypeAdapter();
      threadCalls.put(type, call);
 //遍历Gson的factories集合,其中Adapter封装后的TypeAdapterFacotry也在里面。
      for (TypeAdapterFactory factory : this.factories) {
      //重新从你封装过后的TypeAdapterFacotry获取到之前封装过后的Adapter
        TypeAdapter candidate = factory.create(this, type);
        if (candidate != null) {
        // 设置委托
          call.setDelegate(candidate);
          // 放入缓存
          this.typeTokenCache.put(type, candidate);
          return candidate;
        }
      }
      throw new IllegalArgumentException("GSON cannot handle " + type);
    } finally {
      threadCalls.remove(type);

      if (requiresThreadLocalCleanup)
        this.calls.remove();
    }
  }

// TypeAdapter抽象类有两个方法 toJson里面主要是调用write  adapter.write(writer, src);
public abstract class TypeAdapter<T> {
    // 类型对象转json
     public abstract void write(JsonWriter out, T value) throws IOException; 
     // 让读取的json转换成指定类型的对象T
      public abstract T read(JsonReader in) throws IOException; 

}

其实,上面这一段代码中最重要的应该是adapter.write(writer, src),我debugg看了一下他的入参如下图,看来我谭某人猜对了,那个莫名其妙的节点mMap就是这个

总而言之,序列化过程差不多就是

传入对象ObjectType →解析ObjectType ,生成TypeAdapter→ 调用adapter.write(writer, src)

就上面的Collections.reverse(factories)我也测试了一把

long[] data = {1506326821000l, 1506327060000l, 1506326880000l, 1506327000000l, 1506326940000l, 1506326760000l, 1506326700000l};
 List list = new ArrayList<>();
 for (long key : data) {
	    list.add(key);
	 }	   
 System.out.println("原list的值"+list);	   
  //再反转
Collections.reverse(list);
System.out.println("reverse之后的"+list);	   	   
	  
// -> 输出结果:
//原list的值[1506326821000, 1506327060000, 1506326880000, 1506327000000, 1506326940000, 1506326760000, 1506326700000]
//reverse之后的值;[1506326700000, 1506326760000, 1506326940000, 1506327000000, 1506326880000, 1506327060000, 1506326821000]

没有变化,要想成功反转需要先排序,必须改进

long[] data = {1506326821000l, 1506327060000l, 1506326880000l, 1506327000000l, 1506326940000l, 1506326760000l, 1506326700000l};
 List list = new ArrayList<>();
	    for (long key : data) {
	      list.add(key);
	    }
	    System.out.println("原list的值"+list);
	    //先升序
	    Collections.sort(list);
	    System.out.println("sort的值"+list);
	    //再反转
	    Collections.reverse(list);
	    System.out.println("reverse之后"+list);	   
 // -> 输出结果
 // 原list的值[1506326821000, 1506327060000, 1506326880000, 1506327000000, 1506326940000, 1506326760000, 1506326700000]
 //  sort的值[1506326700000, 1506326760000, 1506326821000, 1506326880000, 1506326940000, 1506327000000, 1506327060000]
 //  reverse之后[1506327060000, 1506327000000, 1506326940000, 1506326880000, 1506326821000, 1506326760000, 1506326700000]

TypeToken

泛型擦除

Java的泛型只在编译时有效,到了运行时这个泛型类型就会被擦除掉所以说List<String>List<Integer>在运行时其实都是List<Object>类型。

为什么选择这种实现机制?不擦除不行么?

在算法中,我们只能根据应用场景选择两个方式的设计,即牺牲时间换空间,或是牺牲空间换时间这两中折中的设计,“类型擦除”也是的,在Java诞生10年后,程序员想实现类似于C++模板的概念(泛型)。但是Java的类库是Java生态中非常宝贵的财富,必须保证向后兼容(即现有的代码和类文件依旧合法)和迁移兼容(泛化的代码和非泛化的代码可互相调用)基于上面这两个背景和考虑。我们才使用类型擦除这一个概念,所以说只要思想不滑坡,办法总比困难多😂

我上次看到网上一篇文章是这么描述规律的: 位于声明一侧的,源码里写了什么到运行时就能看到什么; 位于使用一侧的,源码里写什么到运行时都没了。

TypeToke主要是获取泛型的类型信息,他的主要思想就是如果List<String>这样中的泛型会被擦除掉,那么我用一个子类SubList extends List<String>这样的话,在JVM内部中会把父类泛型的类型保存

这里TypeToke使用场景一般有以下几种:

  • 泛型父类需要获取其子类定义的泛型类型
    final TypeToken<V> typeToken = new TypeToken<V>(getClass()) {};
    classType = (Class<V>) typeToken.getRawType();  //获得子类的泛型类型
  • 需要在方法或者局部变量中获取泛型类型时,需要获取类型的泛型类作为TypeToken的泛型参数构造一个匿名的子类,就可以通过getType()方法获取到我们使用的泛型类的泛型参数类型
// 三级警戒:注意注意注意!!! {}是用来定义匿名类,这个匿名类是继承了TypeToken类,它是TypeToken的子类
final TypeToken typeToken = new TypeToken<List<Integer>>() {}; 
final Type type = typeToken.getType();

在上面的源码分析中,我们只用的了TypeToken.get(typeOfSrc)

 final Class<? super T> rawType;
 // DK1.5引入了泛型之 后,Java中所有的Class都实现了Type接口Type接口作为Class和ParameterizedType, TypeVariable<D>, GenericArrayType, WildcardType这几种类型的总的父接口。这样可以用Type类型的参数来接受以上五种子类的实参或者返回值类型就是Type类型的参数。统一了与泛型有关的类型和原始类型Class
 final Type type;   
 final int hashCode;
    
 public static TypeToken<?> get(Type type) {
        return new TypeToken(type);
   }
    
  TypeToken(Type type) {
  // (Type)Preconditions.checkNotNull(type)判断空,如果不为空则返回type
        this.type = Types.canonicalize((Type)Preconditions.checkNotNull(type));
        // 该方法的作用是返回当前的ParameterizedType的类型。如一个List,返回的是List的Type,即返回当前参数化类型本身的Type。
        this.rawType = Types.getRawType(this.type);
        this.hashCode = this.type.hashCode();
    }
 static Type getSuperclassTypeParameter(Class<?> subclass) {
        Type superclass = subclass.getGenericSuperclass();
        if (superclass instanceof Class) {
            throw new RuntimeException("Missing type parameter.");
        } else {
           // ParameterizedType是表示带有泛型参数的类型的Java类型
            ParameterizedType parameterized = (ParameterizedType)superclass; 
            return Types.canonicalize(parameterized.getActualTypeArguments()[0]);
        }
 }    

JsonWriter

在Gson中,Java对象与JSON字符串之间的转换是通过字符流来进行操作的。JsonReader继承于Reader用来读取字符,JsonWriter继承于Writer用来写入字符


 /* <h3>例子</h3>
 * 假设我们想编码一个消息流,例如: <pre> {@code
 * [
 *   {
 *     "id": 912345678901,
 *     "text": "How do I stream JSON in Java?",
 *     "geo": null,
 *     "user": {
 *       "name": "json_newb",
 *       "followers_count": 41
 *      }
 *   },
 *   {
 *     "id": 912345678902,
 *     "text": "@json_newb just use JsonWriter!",
 *     "geo": [50.454722, -104.606667],
 *     "user": {
 *       "name": "jesse",
 *       "followers_count": 2
 *     }
 *   }
 * ]}</pre>
 * 此代码对上述结构进行编码: <pre>   {@code
 *   public void writeJsonStream(OutputStream out, List<Message> messages) throws IOException {
 *     JsonWriter writer = new JsonWriter(new OutputStreamWriter(out, "UTF-8"));
 *     writer.setIndent("    ");
 *     writeMessagesArray(writer, messages);
 *     writer.close();
 *   }
 *
 *   public void writeMessagesArray(JsonWriter writer, List<Message> messages) throws IOException {
 *     writer.beginArray();
 *     for (Message message : messages) {
 *       writeMessage(writer, message);
 *     }
 *     writer.endArray();
 *   }
 *
 *   public void writeMessage(JsonWriter writer, Message message) throws IOException {
 *     writer.beginObject();
 *     writer.name("id").value(message.getId());
 *     writer.name("text").value(message.getText());
 *     if (message.getGeo() != null) {
 *       writer.name("geo");
 *       writeDoublesArray(writer, message.getGeo());
 *     } else {
 *       writer.name("geo").nullValue();
 *     }
 *     writer.name("user");
 *     writeUser(writer, message.getUser());
 *     writer.endObject();
 *   }
 *
 *   public void writeUser(JsonWriter writer, User user) throws IOException {
 *     writer.beginObject();
 *     writer.name("name").value(user.getName());
 *     writer.name("followers_count").value(user.getFollowersCount());
 *     writer.endObject();
 *   }
 *
 *   public void writeDoublesArray(JsonWriter writer, List<Double> doubles) throws IOException {
 *     writer.beginArray();
 *     for (Double value : doubles) {
 *       writer.value(value);
 *     }
 *     writer.endArray();
 *   }}</pre>
 *
 * <p>每一个JonWriter可能编写一些简单的JSON stream.(JSON流)
 * 这个实例是线程不安全的. 调用失败时可能得到一个IllegalStateException异常
 */
 
// 实现了 AutoCloseable接口对JDK7新添加的带资源的try语句提供了支持,这种try语句可以自动执行资源关闭过程,Flushable强制将缓存的输出写入到与对象关联的流中
public class JsonWriter implements Closeable, Flushable {
// REPLACEMENT_CHARS还是HTML_SAFE_REPLACEMENT_CHARS的关键在于boolean htmlSafe 的取值
  private static final String[] REPLACEMENT_CHARS;
  private static final String[] HTML_SAFE_REPLACEMENT_CHARS;
   /**
   *名称和值的分隔符  ":" or ": ".
   */
  private String separator = ":";
 
  private boolean lenient;
/*
是否配置为发出可以安全地直接包含在HTML和XML文档中的Json。这将在将HTML字符<、>,&,=写入流之前对它们进行转义。如果没有此设置,XML/HTML编码器应该将这些字符替换为相应的转义序列。
*/
  private boolean htmlSafe;

  private String deferredName;
/* 是否序列化null
*/
  private boolean serializeNulls = true;
}

总结

感觉在Gson的源码中,链式调用这一个写法很常见,上次看《Effective Java》这一本书的时候,上面就说当构造方法参数过多时使用builder模式,当有很多参数时,很难编写客户端代码,而且很难读懂它。程序员不知道这些值是什么意思,并且必须仔细地计算参数才能找到答案。一长串相同类型的参数可能会导致一些细微的bug。如果客户端意外地反转了两个这样的参数,编译器并不会出现错误,但是运行时候会报错,现在在jdk1.8的流操作中,也都使用了链式操作

问题??

如果我没有记错的话在《深入理解计算机系统(原书第三版)》中说(PS:本来想求证一下的,可是忘记是哪一节了,找了半天没有找到,还是纸质书好一点)调用一个方法,入参超过6个的话,会产生寄存器的不足,其他多出来的参数会在栈分配,那么使用链式编程会不会在一定程度上提高了jvm效率了

最后希望长沙的冬天不要这么冷啊!!! 冷到无法呼吸/(ㄒoㄒ)/~~