まったり技術ブログ

Technology is power.

GsonとJacksonのJSONパース処理速度を比べてみる

f:id:motikan2010:20180120011546j:plain

 JSONのパース処理はいろんな場面で利用していますが、処理速度を意識して利用していなかった。
 そこでJSONパーサーライブラリの処理速度を比べてみて、いざという時に備えておく。

比べるライブラリは下記の2種類

Gson

github.com

Jackson

github.com

下記のパターン(特徴)で比較

  • ノーマル
  • Date型
  • 長い文字列
  • Date型 ×2
  • 大量のメンバ変数

結果から言うと、Jacksonが優勢であった。

処理速度の計測方法

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class SampleApplication {

    public static void main(String[] args) throws Exception{
        // 1. 30万含んだユーザリストを作成
        List<User> userList = new ArrayList<>();
        for (int i=0; i < 300_000; i++) {
            userList.add(new User1("firstName", "lastName"));
        }

        // 2. ユーザリストをJSONテキストに変換
        String jsonString = null;
        for (int i=0; i < 5;i++) {
            Instant createStart = Instant.now();
            jsonString = createJson(userList);
            System.out.println("Create JSON : " + Duration.between(createStart, Instant.now()).toNanos() / 1000000.0 + " [ms]");
        }

        // 3. JSONテキストからユーザリストを復元
        for (int i=0; i < 5;i++) {
            Instant parseStart = Instant.now();
            User user = parseJson(jsonString);
            System.out.println("Parse  JSON : " + Duration.between(parseStart, Instant.now()).toNanos() / 1000000.0 + " [ms]");
        }
    }
}

それぞれのパース処理の内容は以下のようになっています。

"Gson"を使ったJSON文字列の作成・解析

private static String createJson (List<User> userList) {
    Gson gson = new Gson();
    String json = gson.toJson(userList);
    return json;
}

private static User parseJson (String json) {
    Gson gson = new Gson();
    List<User> userList = gson.fromJson(json, new TypeToken<List<User1>>(){}.getType());
    return userList.get(0);
}

"Jackson"を使ったJSON文字列の作成・解析

private static String createJson (List<User> userList) throws Exception{
    ObjectMapper objectMapper = new ObjectMapper();
    String json = objectMapper.writeValueAsString(userList);
    return json;
}

private static User parseJson (String json) throws Exception {
    ObjectMapper objectMapper = new ObjectMapper();
    List<User> userList = objectMapper.readValue(json, new TypeReference<List<User1>>(){});
    return userList.get(0);
}

Userクラスの中身は以下のようになっており、メンバ変数の個数データ型を変えていき、処理速度に影響しているかを確認していきます。

@Data
@AllArgsConstructor
public class User1 implements User {

    private String firstName;

    private String lastName;
}

ノーマル

データ型
第1引数 String firstName
第2引数 String lastName

 JSON文字列・オブジェクトの変換処理はそれぞれ5回行い、処理毎に早くなっているが分かる。
 だが2つの処理速度に大差は感じられない。

インスタンス → JSON
Gson Jackson
1回目 1082 ms 968 ms
2回目 697 ms 512 ms
3回目 665 ms 144 ms
4回目 349 ms 150 ms
5回目 324 ms 205 ms
JSON → インスタンス
Gson Jackson
1回目 293 ms 386 ms
2回目 164 ms 160 ms
3回目 159 ms 104 ms
4回目 175 ms 132 ms
5回目 140 ms 101 ms

特徴 : Date型

メンバ変数にDate型を加えてパース処理を実施。 ||データ型|値| |-|-|-| |第1引数|String|firstName| |第2引数|String|lastName| |第3引数|Date|new Date()| ここで処理速度に差が生じてきた。 GsonはDate型の変換が苦手なのか、著しく処理が遅くなった事が確認できる。

オブジェクト → JSON文字列
Gson Jackson
1回目 2356 ms 1141 ms
2回目 1101 ms 618 ms
3回目 793 ms 277 ms
4回目 606 ms 180 ms
5回目 431 ms 253 ms
JSON文字列 → オブジェクト
Gson Jackson
1回目 3845 ms 388 ms
2回目 3007 ms 193 ms
3回目 2928 ms 169 ms
4回目 2847 ms 163 ms
5回目 2823 ms 176 ms

特徴 : 長い文字列

データ型
第1引数 String firstName
第2引数 String lastName
第3引数 String (500文字)

 Jacksonが相変わらず早いが、先の例ほどは差がなく、文字長はそこまで処理時間に関係なさそう。

オブジェクト → JSON文字列
Gson Jackson
1回目 2578 ms 2247 ms
2回目 1833 ms 964 ms
3回目 1379 ms 860 ms
4回目 1315 ms 836 ms
5回目 1251 ms 768 ms
JSON文字列 → オブジェクト
Gson Jackson
1回目 1354 ms 742 ms
2回目 666 ms 445 ms
3回目 990 ms 687 ms
4回目 873 ms 521 ms
5回目 963 ms 609 ms

ちなみに、第3引数に600文字を指定して実行すると、"OutOfMemoryError"になった。

特徴 : Date型 ×2

データ型
第1引数 String firstName
第2引数 String lastName
第3引数 Date new Date()
第4引数 Date new Date()
オブジェクト → JSON文字列
Gson Jackson
1回目 2925 ms 989 ms
2回目 1477 ms 767 ms
3回目 840 ms 384 ms
4回目 842 ms 324 ms
5回目 877 ms 150 ms
JSON文字列 → オブジェクト
Gson Jackson
1回目 6182 ms 538 ms
2回目 5400 ms 359 ms
3回目 5429 ms 201 ms
4回目 5378 ms 212 ms
5回目 5447 ms 202 ms

特徴 : 大量のメンバ変数

データ型
第1引数 String SampleText
第2引数 String SampleText
第3引数 String SampleText
第4引数 String SampleText
(以下略)
第30引数 String SampleText

ここにきてGsonの強みが確認された。
gsonは処理を終えることができたが、 Jacksonの場合「オブジェクト → JSON文字列」の処理で"OutOfMemoryError"となった。

オブジェクト → JSON文字列
Gson Jackson
1回目 5338 ms 3503 ms
2回目 5237 ms - ms
3回目 2868 ms - ms
4回目 2744 ms - ms
5回目 2555 ms - ms
JSON文字列 → オブジェクト
Gson Jackson
1回目 4633 ms 4854 ms
2回目 4089 ms 5163 ms
3回目 2629 ms 3910 ms
4回目 5575 ms 2939 ms
5回目 3798 ms 2981 ms

【JavaFX】レイアウトを使ってみる

f:id:motikan2010:20180107061741j:plain
以下のレイアウトを紹介します。

  • VBox クラス:垂直にUIコントロールを配置
  • HBox クラス:平行にUIコントロールを配置
  • FlowPane クラス:平行にUIコントロールを配置(折り返し有り)
  • BorderPane クラス:上下・左右・中心の位置にUIコントロールを配置
  • GridPane クラス:行と列を指定してUIコントロールを配置
  • TilePane クラス:クリッド状にUIコントロールを配置
  • StackPane クラス:重ねてUIコントロールを配置

VBox クラス

垂直にUIコントロールを配置します。
上部から順に配置されていきます。

f:id:motikan2010:20180107044324p:plain:w100
VBox (JavaFX 8)

import com.google.common.collect.Lists;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.util.List;

public class VBoxExample extends Application {

    @Override
    public void start(Stage stage) throws Exception{
        stage.setTitle("HBox Example");

        List<Button> buttonList = Lists.newArrayList();
        for (int i=0; i<5; i++) {
            buttonList.add(new Button(Integer.toString(i)));
            buttonList.get(i).setPrefWidth(80);
        }

        VBox vBox = new VBox();
        vBox.setAlignment(Pos.CENTER);
        vBox.setPadding(new Insets(10, 10, 10, 10));
        vBox.getChildren().addAll(buttonList);

        stage.setScene(new Scene(vBox));
        stage.show();
    }
}

UIコントロール間に間隔を空けることも可能です。

VBox vBox = new VBox();
vBox.setAlignment(Pos.CENTER);
vBox.setPadding(new Insets(10, 10, 10, 10));
vBox.setSpacing(5.0); // 追加
vBox.getChildren().addAll(buttonList);

f:id:motikan2010:20180107044413p:plain:w100

HBox クラス

平行にUIコントロールを配置します。
f:id:motikan2010:20180107044758p:plain:w450

HBox (JavaFX 8)

import com.google.common.collect.Lists;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

import java.util.List;

public class HBoxExample extends Application {

    @Override
    public void start(Stage stage) throws Exception{
        stage.setTitle("HBox Example");

        List<Button> buttonList = Lists.newArrayList();
        for (int i=0; i<5; i++) {
            buttonList.add(new Button(Integer.toString(i)));
            buttonList.get(i).setPrefWidth(80);
        }

        HBox hBox = new HBox();
        hBox.setAlignment(Pos.CENTER);
        hBox.setPadding(new Insets(10, 10, 10, 10));
        hBox.getChildren().addAll(buttonList);

        stage.setScene(new Scene(hBox));
        stage.show();
    }
}

HBox同様に、UIコントロール間に間隔を空けることも可能です。

HBox hBox = new HBox();
hBox.setAlignment(Pos.CENTER);
hBox.setPadding(new Insets(10, 10, 10, 10));
hBox.setSpacing(5.0); // 追加
hBox.getChildren().addAll(buttonList);

f:id:motikan2010:20180107044840p:plain:w450

FlowPane クラス

平行にUIコントロールを配置します。
入りきらないUIコントロールは折り返しが行われます。

f:id:motikan2010:20180107045018p:plain:w450
FlowPane (JavaFX 8)

import com.google.common.collect.Lists;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;

import java.util.List;

public class FlowPaneExample extends Application {

    @Override
    public void start(Stage stage) throws Exception{
        stage.setTitle("Flow Pane Example");

        List<Button> buttonList = Lists.newArrayList();
        for (int i=0; i<5; i++) {
            buttonList.add(new Button(Integer.toString(i)));
            buttonList.get(i).setPrefWidth(80);
        }

        FlowPane flowPane = new FlowPane();
        flowPane.setPadding(new Insets(10, 10, 10, 10));
        flowPane.getChildren().addAll(buttonList);

        stage.setScene(new Scene(flowPane));
        stage.show();
    }

}

HBoxクラスとの違い

ウィンドウの幅を狭めてみると、2つの違いが分かります。

  • HBox   :ウィンドウの幅に合わせてボタンのサイズが縮小
  • FlowPane:ウィンドウの幅に合わせてボタンが次の行へ折り返し
HBoxクラス

f:id:motikan2010:20180107045119p:plain:w300

FlowPane クラス

f:id:motikan2010:20180107045121p:plain:w300

BorderPaneクラス

上下・左右・中心の位置にUIコントロールを配置します。

f:id:motikan2010:20180107051907p:plain:w250
GridPane (JavaFX 8)

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class BorderPaneExample extends Application {

    @Override
    public void start(Stage stage) throws Exception{
        stage.setTitle("Border");
        stage.setWidth(200);
        stage.setHeight(110);

        Button topButton = new Button("Top");
        topButton.setPrefWidth(210);
        topButton.setPrefHeight(30);

        Button leftButton = new Button("Left");
        leftButton.setPrefHeight(30);

        Button centerButton = new Button("Center");
        centerButton.setPrefWidth(160);
        centerButton.setPrefHeight(30);

        Button rightButton = new Button("Right");
        rightButton.setPrefHeight(30);

        Button bottomButton = new Button("Bottom");
        bottomButton.setPrefWidth(210);
        bottomButton.setPrefHeight(30);

        BorderPane borderPane = new BorderPane();
        borderPane.setTop(topButton);
        borderPane.setLeft(leftButton);
        borderPane.setCenter(centerButton);
        borderPane.setRight(rightButton);
        borderPane.setBottom(bottomButton);

        VBox vBox = new VBox();
        vBox.getChildren().addAll(borderPane);

        stage.setScene(new Scene(vBox));
        stage.show();
    }

}

GridPane クラス

行と列を指定してUIコントロールを配置します。

f:id:motikan2010:20180107051647p:plain:w250
GridPane (JavaFX 8)

import com.google.common.collect.Lists;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.util.List;

public class VBoxExample extends Application {

    @Override
    public void start(Stage stage) throws Exception{
        stage.setTitle("HBox Example");

        List<Button> buttonList = Lists.newArrayList();
        for (int i=0; i<5; i++) {
            buttonList.add(new Button(Integer.toString(i)));
            buttonList.get(i).setPrefWidth(80);
        }

        VBox vBox = new VBox();
        vBox.setAlignment(Pos.CENTER);
        vBox.setPadding(new Insets(10, 10, 10, 10));
        vBox.setSpacing(5.0);
        vBox.getChildren().addAll(buttonList);

        stage.setScene(new Scene(vBox));
        stage.show();
    }
}

TilePane クラス

クリッド状にUIコントロールを配置します。

f:id:motikan2010:20180107051732p:plain:w350
TilePane (JavaFX 8)

import com.google.common.collect.Lists;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.TilePane;
import javafx.stage.Stage;

import java.util.List;

public class TilePaneExample extends Application {

    @Override
    public void start(Stage stage) throws Exception{
        stage.setTitle("Tile Pane Example");
        stage.setWidth(320);
        stage.setHeight(130);

        List<Button> buttonList = Lists.newArrayList();
        for (int i=0; i<16; i++) {
            buttonList.add(new Button("Button:" + Integer.toString(i)));
            buttonList.get(i).setPrefWidth(80);
            buttonList.get(i).setPrefHeight(20);
        }

        TilePane tilePane = new TilePane();
        tilePane.getChildren().addAll(buttonList);

        stage.setScene(new Scene(tilePane));
        stage.show();
    }
}

ウィンドウの縮尺に合わせて配置が変わります。
f:id:motikan2010:20180107062347p:plain

StackPane クラス

重ねてUIコントロールを配置ができます。

f:id:motikan2010:20180107051814p:plain:w300
StackPane (JavaFX 8)

import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;


public class StackPaneExample extends Application {

    @Override
    public void start(Stage stage) throws Exception{
        stage.setTitle("Stack Pane Example");
        stage.setWidth(260);
        stage.setHeight(130);

        Button smButton = new Button("Small Button");
        smButton.setPrefWidth(80);
        smButton.setPrefHeight(30);
        smButton.setAlignment(Pos.TOP_CENTER);

        Button mdButton = new Button("Medium Button");
        mdButton.setPrefWidth(160);
        mdButton.setPrefHeight(60);
        mdButton.setAlignment(Pos.TOP_CENTER);

        Button lgButton = new Button("Large Button");
        lgButton.setPrefWidth(240);
        lgButton.setPrefHeight(90);
        lgButton.setAlignment(Pos.TOP_CENTER);

        StackPane stackPane = new StackPane();
        stackPane.setAlignment(Pos.BOTTOM_RIGHT);
        stackPane.getChildren().addAll(lgButton, mdButton, smButton);

        stage.setScene(new Scene(stackPane));
        stage.show();
    }
}

Specificationインタフェースを利用した副問合せ

Specificationインタフェースを利用した副問合せに関して、あまり情報が見当たらなかったので、メモ程度にφ(・ω・ )。

サンプルテーブル

よく見かける 1対多 の関係で説明していきます。
f:id:motikan2010:20171013000832p:plain:w400

ユーザ テーブル

サンプルコード

下記のサンプルコードでは、「同じ内容のツイートを3回以上しているユーザ」を選択という少し面倒くさいSQL文を発行してみます。
f:id:motikan2010:20171014134003j:plain

ファイル構成

 ── dbapp
    ├── DbappApplication.java
    ├── model
    │   ├── Tweet.java
    │   ├── Tweet_.java
    │   ├── User.java
    │   └── User_.java
    ├── repository
    │   ├── TweetRepository.java
    │   └── UserRepository.java
    └── spec
        └── BadUserSpec.java

Specification

今回の肝となる部分です。

BadUserSpec.java
package com.motikan2010.dbapp.spec;

import com.motikan2010.dbapp.model.Tweet;
import com.motikan2010.dbapp.model.Tweet_;
import com.motikan2010.dbapp.model.User;
import com.motikan2010.dbapp.model.User_;
import org.springframework.data.jpa.domain.Specification;

import javax.persistence.criteria.*;
import java.util.ArrayList;
import java.util.List;

public class BadUserSpec implements Specification<User> {

    @Override
    public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
        final List<Predicate> predicates = new ArrayList<>();

        // サブクエリ
        Subquery<Tweet> subquery = query.subquery(Tweet.class);
        Root<Tweet> subRoot = subquery.from(Tweet.class);
        subquery.select(subRoot.get(Tweet_.user.getName()));
        subquery.where(cb.equal(root.get(User_.id.getName()), subRoot.get(Tweet_.user.getName())));
        // ツイート内容でグループ化
        subquery.groupBy(subRoot.get(Tweet_.body.getName()));
        // 条件 
        subquery.having(cb.and(
                cb.greaterThanOrEqualTo(cb.count(subRoot), 3L)
        ));

        predicates.add(cb.exists(subquery));

        return cb.and(predicates.toArray(new Predicate[predicates.size()]));
    }

}

エンティティ(モデル)

User.java
package com.motikan2010.dbapp.model;

import lombok.Data;

import javax.persistence.*;
import java.util.List;

@Entity
@Data
@Table(name = "user")
public class User {

    @Id
    @GeneratedValue
    @Column(name = "id")
    private int id;
    
    @Column(name = "nickname")
    private String nickname;

    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Tweet> tweetList;

}
Tweet.java
package com.motikan2010.dbapp.model;

import com.sun.istack.internal.NotNull;
import lombok.Data;

import javax.persistence.*;

@Entity
@Data
@Table(name = "tweet")
public class Tweet {

    @Id
    @GeneratedValue
    @Column(name = "id")
    private int id;
    
    @Column(name = "body")
    private String body;

    @NotNull
    @Column(name = "user_id")
    private int userId;
    
    @ManyToOne(targetEntity=User.class)
    @JoinColumn(name = "user_id", referencedColumnName = "id", insertable=false, updatable=false)
    private User user;

}

メタモデル

恥ずかしながらメタモデルの存在を今まで知らなかった。。
JPAを深掘りする〜Criteria APIで型安全な検索を追求しよう!【基本編】 - 技術ブログ | 株式会社クラウディア
ちなみにエンティティと同じパケッケージに所属させないといけないらしい。
別のパッケージに配置し、ヌルポと格闘したのはいい思い出・・・。

stackoverflow.com

User_.java
package com.motikan2010.dbapp.model;

import javax.persistence.metamodel.ListAttribute;
import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.StaticMetamodel;

@StaticMetamodel(User.class)
public class User_ {
    public static volatile SingularAttribute<User, Integer> id;
    public static volatile SingularAttribute<User, String> nickname;
    public static volatile ListAttribute<User, Tweet> tweetList;
}
Tweet_.java
package com.motikan2010.dbapp.model;

import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.StaticMetamodel;

@StaticMetamodel(Tweet.class)
public class Tweet_ {
    public static volatile SingularAttribute<Tweet, Integer> id;
    public static volatile SingularAttribute<Tweet, String> body;
    public static volatile SingularAttribute<Tweet, User> user;
}

リポジトリ

今回は空っぽ。

package com.motikan2010.dbapp.repository;

import com.motikan2010.dbapp.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecificationExecutor<User>{
}

呼び出し側

DbappApplication.java
package com.motikan2010.dbapp;

import com.motikan2010.dbapp.model.User;
import com.motikan2010.dbapp.repository.UserRepository;
import com.motikan2010.dbapp.spec.BadUserSpec;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.util.List;

@SpringBootApplication
public class DbappApplication implements CommandLineRunner {

    @Autowired
    UserRepository userRepo;

    public static void main(String[] args) {
        SpringApplication.run(DbappApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {

        BadUserSpec spec = new BadUserSpec();
        List<User> userList = userRepo.findAll(spec);

        for(User user : userList){
            System.out.println(user.getId() + " : " + user.getNickname());
        }

    }

}

発行SQL

select user0_.id as id1_1_, user0_.nickname as nickname2_1_ from user user0_ where exists (select tweet1_.user_id from tweet tweet1_, user user2_ where tweet1_.user_id=user2_.id and user0_.id=tweet1_.user_id group by tweet1_.body having count(tweet1_.id)>=3)

分かりやすくするとこんな感じ。

select id , nickname 
from user user1 
where exists (
    select user_id 
    from tweet, user user2 
    where tweet.user_id = user2.id and user1.id = tweet.user_id 
    group by body 
    having count(tweet.id)>=3
)

いい感じに副問い合わせが行えている。
countの部分をsumやavgに変えるなどして様々なパターンに応用できそう。