当已知格式时,从字符串解析JSON的最快方法

8
我希望能在Java中将一个字符串解析为内部JSON对象(或等效对象)。通常的库,如Gson和Jackson,对我的需求来说速度太慢了(根据我的基准测试,每个字符串到Json解析耗费大于100微秒)。我知道有一些稍快的库,但看网上的基准测试,它们提供的性能提升很小(不到一个数量级的改善)。
如果我事先知道JSON的格式,那么有没有一种方法可以更快地解析它?例如,我知道该字符串将是以下格式的JSON:
{
   "A" : 1.0 ,
   "B" : "X"
}

即,我知道两个键将是“A”和“B”,值分别为double和string。鉴于这种格式的高级知识,是否有一种库或某种方法可以比通常更快地解析JSON?


2
主要的性能问题可能出现在Jackson的ObjectMapper所需的反射上,以动态确定和映射数据。您可以尝试使用Jackson的流解析器,并在自己的代码中静态地将其映射到POJO类中。 - Andreas
1
做某事最快的方法……是根本不去做它 :-)。如果你真的需要高性能 I/O,使用一种紧凑的二进制表示法,它非常容易被摄取,而不需要解析文本的成本。 - Raedwald
1
@Raedwald 很不幸,我没有选择,必须从外部实时数据源中获取纯文本JSON格式的数据。 - ABC
2
我建议您务必确保重用ObjectMapper并测量稳态性能:由于负载非常小,Jackson和GSON应该能够比您看到的速度快10-100倍进行解码和绑定。无需执行二进制--这只会让您快50%。对于Jackson,还可以使用jackson-module-afterburner(https://github.com/FasterXML/jackson-modules-base/tree/master/afterburner),它可以进一步提高30-40%的性能。 - StaxMan
1
@ABC 只需构建一个静态单例实例,然后使用它。不要为每个操作创建新实例。原因是所有注释扫描和设置工作仅针对每种类型执行一次;重用映射器可避免在第一次之后再次执行此操作。 - StaxMan
显示剩余8条评论
2个回答

17

如果你知道JSON负载结构,你可以使用Streaming API读取数据。我创建了4种不同的方法来读取给定的JSON负载:

  1. 默认Gson - 使用Gson类。
  2. Gson适配器 - 使用Gson库中的JsonReader
  3. 默认Jackson - 使用Jackson的ObjectMapper
  4. Jackson Streaming API - 使用JsonParser类。

为了使它们可比较,所有这些方法都将JSON负载作为String接收,并返回代表AB属性的Pojo对象。下面的图表表示各种方法之间的差异:enter image description here

正如你所注意到的,JacksonStreaming API是从这4种方法中反序列化你的JSON负载最快的方式。

生成上述图表使用了以下数据:

1113 547 540 546 544 552 547 549 547 548 平均值 603.3
940 455 452 456 465 459 457 458 455 455 平均值 505.2
422 266 257 262 260 267 259 262 257 259 平均值 277.1
202 186 184 189 185 188 182 186 187 183 平均值 187.2

基准测试代码:

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;

public class JsonApp {

    private static final String json = "{\"A\" : 1.0 ,\"B\" : \"X\"}";

    private static final int MAX = 1_000_000;

    private static List<List<Duration>> values = new ArrayList<>();

    static {
        IntStream.range(0, 4).forEach(i -> values.add(new ArrayList<>()));
    }

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 10; i++) {
            int v = 0;
            values.get(v++).add(defaultGson());
            values.get(v++).add(gsonAdapter());
            values.get(v++).add(defaultJackson());
            values.get(v).add(jacksonJsonFactory());
        }
        values.forEach(list -> {
            list.forEach(d -> System.out.print(d.toMillis() + " "));
            System.out.println(" avg " + list.stream()
                    .mapToLong(Duration::toMillis)
                    .average().getAsDouble());
        });
    }

    static Duration defaultGson() {
        Gson gson = new Gson();

        long start = System.nanoTime();
        for (int i = MAX; i > 0; i--) {
            gson.fromJson(json, Pojo.class);
        }

        return Duration.ofNanos(System.nanoTime() - start);
    }

    static Duration gsonAdapter() throws IOException {
        PojoTypeAdapter adapter = new PojoTypeAdapter();

        long start = System.nanoTime();
        for (int i = MAX; i > 0; i--) {
            adapter.fromJson(json);
        }

        return Duration.ofNanos(System.nanoTime() - start);
    }

    static Duration defaultJackson() throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);

        long start = System.nanoTime();
        for (int i = MAX; i > 0; i--) {
            mapper.readValue(json, Pojo.class);
        }

        return Duration.ofNanos(System.nanoTime() - start);
    }

    static Duration jacksonJsonFactory() throws IOException {
        JsonFactory jfactory = new JsonFactory();

        long start = System.nanoTime();
        for (int i = MAX; i > 0; i--) {
            readPartially(jfactory);
        }
        return Duration.ofNanos(System.nanoTime() - start);
    }

    static Pojo readPartially(JsonFactory jfactory) throws IOException {
        try (JsonParser parser = jfactory.createParser(json)) {

            Pojo pojo = new Pojo();

            parser.nextToken(); // skip START_OBJECT - {
            parser.nextToken(); // skip A name
            parser.nextToken();
            pojo.A = parser.getDoubleValue();
            parser.nextToken(); // skip B name
            parser.nextToken();
            pojo.B = parser.getValueAsString();

            return pojo;
        }
    }
}

class PojoTypeAdapter extends TypeAdapter<Pojo> {

    @Override
    public void write(JsonWriter out, Pojo value) {
        throw new IllegalStateException("Implement me!");
    }

    @Override
    public Pojo read(JsonReader in) throws IOException {
        if (in.peek() == com.google.gson.stream.JsonToken.NULL) {
            in.nextNull();
            return null;
        }

        Pojo pojo = new Pojo();

        in.beginObject();
        in.nextName();
        pojo.A = in.nextDouble();
        in.nextName();
        pojo.B = in.nextString();

        return pojo;
    }
}

class Pojo {

    double A;
    String B;

    @Override
    public String toString() {
        return "Pojo{" +
                "A=" + A +
                ", B='" + B + '\'' +
                '}';
    }
}

注意:如果您需要非常精确的数据,请尝试使用优秀的JMH包创建基准测试。


3
非常好,感谢您在回答中付出的努力。 - ABC
1
我赞同使用JMH的建议,因为有很多因素可能会扭曲结果——例如,在这种情况下,重复次数似乎有点低,无法达到稳定状态,并且所有运行都在同一个JVM中。 另一方面,通过注释添加JVM并仅使用上面的代码应该非常容易。 - StaxMan
@StaxMan,感谢您的评论。我只是想展示4种方法之间的差异,并且Streaming API比其他方式的第一次迭代更加“稳定”。当然,这个测试并不完整,因为只测试了两个库和两种方式。但从另一方面来看,这个测试很容易运行,每个人都应该能够在自己的计算机上使用不同的JVM测试它的工作原理。我知道,它并不像它本应该那样完美和精确,但我想以某种方式帮助做出一个好决策,选择哪种方法。希望这不会对任何人产生误导。 - Michał Ziober
1
@MichałZiober 完全正确,我注意到(在开始写评论之后)你提到了jmh。图表看起来很可靠,所以我认为时间可能不会相差太远。我不认为这会误导人。稳定性也是有道理的,因为JVM需要优化的代码要少得多。 - StaxMan
我用最新的Moshi和Gson做了一个新的测试,结果并不相同,尤其是当数据类很大时。 - undefined

-1
你可以尝试使用BSON。BSON是一种二进制对象,比大多数JSON库运行速度更快。
 //import java.util.ArrayList;
 //import org.bson.Document;


 Document root = Document.parse("{ \"A\" : 1.0, \"B\" : \"X\" }");

 System.out.println((root.get("A")));
 System.out.println(((String)root.get("B")));

1
如果你声称这比大多数JSON库都要快,那么你真的应该添加一些证明的链接... - StaxMan
1
这是一个实现的链接,但没有提及性能。 - StaxMan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接