CLOVER🍀

That was when it all began.

Jacksonで、JSONをコンテナ型(ListやMapなど)のような型引数を持ったクラスにデシリアライズする

これは、なにをしたくて書いたもの?

Jacksonを使ってJSONをデシリアライズする時に、ObjectMapper#readValueをよく使うわけですが。そういえば、自分で
書いている時にListMapといったジェネリックな型にデシリアライズしたことがないな、と思い。

工夫が要りそうだなと思い、ちょっと調べてみることに。

シリアライズ時に型情報を与える

まずは、ObjectMapper#readValue(第2引数がClassクラスの方)のJavadocを見てみます。

readValue(JsonParser p, Class valueType)

よくよく見ると、こんなことが書いてあります。

Note: this method should NOT be used if the result type is a container (Collection or Map. The reason is that due to type erasure, key and value types cannot be introspected when using this method.

結果型をCollectionMapといったコンテナ型にする場合、このメソッドは使ってはいけません、と。

型情報がなくなるので、キーや値の型がわからなくなるからですね。

このような時は、以下のメソッド(第1引数がStringなどの他のバリエーションのものも含めて)を使うのが良さそうです。

readValue(JsonParser p, TypeReference valueTypeRef)

readValue(JsonParser p, JavaType valueType)

TypeReference

メソッドの説明を見ると、今回の用途にはこちらを使うのがまずは良いのでしょうか?

Method to deserialize JSON content into a Java type, reference to which is passed as argument. Type is passed using so-called "super type token" (see ) and specifically needs to be used if the root type is a parameterized (generic) container type.

readValue(JsonParser p, TypeReference valueTypeRef)

パラメーター化されたコンテナ型をルート型に要求される場合、こちらを使うように、だそうです。

ここで使うものがTypeReferenceクラスで、サブクラスを作成する時に型情報を埋め込みます。

TypeReference

Javadocの例からですが、こんな感じに使います。

TypeReference ref = new TypeReference<List<Integer>>() { };

こちらを、ObjectMapper#readValueの第2引数に渡せばOKです。

もしくは、TypeFactoryを使ってJavaTypeに変換して使います。

which can be passed to methods that accept TypeReference, or resolved using TypeFactory to obtain ResolvedType.

JavaType

もうひとつが、JavaTypeを使う方法ですね。

readValue(JsonParser p, JavaType valueType)

TypeFactoryを使って直接JavaTypeを組み立ててもよいですし、TypeReferenceから変換する方法もあるようです。
TypeReferenceから変換する場合も、TypeFactoryを使用します。

TypeFactory

こんな感じに使うようです。

ObjectMapper mapper = new ObjectMaper();
JavaType stringCollection = mapper.getTypeFactory().constructCollectionType(List.class, String.class);

では、それぞれ使っていってみましょう。

環境

今回の環境は、こちら。

$ java --version
openjdk 11.0.11 2021-04-20
OpenJDK Runtime Environment (build 11.0.11+9-Ubuntu-0ubuntu2.20.04)
OpenJDK 64-Bit Server VM (build 11.0.11+9-Ubuntu-0ubuntu2.20.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.1 (05c21c65bdfed0f71a2f2ada8b84da59348c4c5d)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.11, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-74-generic", arch: "amd64", family: "unix"

Mavenでの依存関係などは、このように定義。

    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.12.3</version>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.7.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.7.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.19.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
        </plugins>
    </build>

動作確認は、テストコードで行います。

テストコードの雛形とお題

テストコードの雛形は、こちら。

src/test/java/org/littlewings/jackson/DeserializeJsonWithTypeTest.java

package org.littlewings.jackson;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.databind.type.MapType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class DeserializeJsonWithTypeTest {
    // ここに、テストを書く!
}

お題としては、以下のクラスを題材に、ListMapに格納したインスタンスJSONシリアライズ、デシリアライズする
パターンをいくつか試してみようと思います。

src/test/java/org/littlewings/jackson/Person.java

package org.littlewings.jackson;

public class Person {
    String lastName;
    String firstName;
    int age;

    public static Person create(String lastName, String firstName, int age) {
        Person person = new Person();
        person.setLastName(lastName);
        person.setFirstName(firstName);
        person.setAge(age);

        return person;
    }

    // getter/setterは省略
}

List

まずはListで試してみましょう。

Classを指定する

最初は、ObjectMapper#readValueClassを指定してみます。

    @Test
    public void nonProvideTypeAsList() throws JsonProcessingException {
        List<Person> persons =
                List.of(
                        Person.create("磯野", "カツオ", 11),
                        Person.create("フグ田", "タラオ", 3)
                );

        ObjectMapper mapper = new ObjectMapper();

        String json = mapper.writeValueAsString(persons);

        assertThat(json).isEqualTo("[{\"lastName\":\"磯野\",\"firstName\":\"カツオ\",\"age\":11},{\"lastName\":\"フグ田\",\"firstName\":\"タラオ\",\"age\":3}]");

        List<Object> deserializedPersons = mapper.readValue(json, List.class);

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get(0)).isNotInstanceOf(Person.class);
        assertThat(deserializedPersons.get(0)).isInstanceOf(LinkedHashMap.class);
        assertThat(deserializedPersons.get(0)).extracting("lastName").isEqualTo("磯野");
        assertThat(deserializedPersons.get(0)).extracting("firstName").isEqualTo("カツオ");
        assertThat(deserializedPersons.get(0)).extracting("age").isEqualTo(11);

        assertThat(deserializedPersons.get(1)).isNotInstanceOf(Person.class);
        assertThat(deserializedPersons.get(1)).isInstanceOf(LinkedHashMap.class);
        assertThat(deserializedPersons.get(1)).extracting("lastName").isEqualTo("フグ田");
        assertThat(deserializedPersons.get(1)).extracting("firstName").isEqualTo("タラオ");
        assertThat(deserializedPersons.get(1)).extracting("age").isEqualTo(3);
    }

このようにObjectMapper#readValueの第2引数にList.classとか渡してしまうと、その中に入るのはこのケースだと
LinkedHashMapインスタンスになります。

        List<Object> deserializedPersons = mapper.readValue(json, List.class);

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get(0)).isNotInstanceOf(Person.class);
        assertThat(deserializedPersons.get(0)).isInstanceOf(LinkedHashMap.class);
        assertThat(deserializedPersons.get(0)).extracting("lastName").isEqualTo("磯野");
        assertThat(deserializedPersons.get(0)).extracting("firstName").isEqualTo("カツオ");
        assertThat(deserializedPersons.get(0)).extracting("age").isEqualTo(11);

        assertThat(deserializedPersons.get(1)).isNotInstanceOf(Person.class);
        assertThat(deserializedPersons.get(1)).isInstanceOf(LinkedHashMap.class);
        assertThat(deserializedPersons.get(1)).extracting("lastName").isEqualTo("フグ田");
        assertThat(deserializedPersons.get(1)).extracting("firstName").isEqualTo("タラオ");
        assertThat(deserializedPersons.get(1)).extracting("age").isEqualTo(3);

こんな感じに書いてもコンパイル自体は通りますが、Listに格納されたデータを扱う時にキャストに失敗します。

        List<Person> deserializedPersons = mapper.readValue(json, List.class);

こちらが、その時の例外メッセージ。

java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class org.littlewings.jackson.Person (java.util.LinkedHashMap is in module java.base of loader 'bootstrap'; org.littlewings.jackson.Person is in unnamed module of loader 'app')

もうちょっと具体的に型を指定したとしても、これくらいでしょうか。

        List<LinkedHashMap<String, Object>> deserializedPersons = mapper.readValue(json, List.class);
TypeReferenceを使う

次は、TypeReferenceを使ってみましょう。

    @Test
    public void provideTypeReferenceAsList() throws JsonProcessingException {
        List<Person> persons =
                List.of(
                        Person.create("磯野", "カツオ", 11),
                        Person.create("フグ田", "タラオ", 3)
                );

        ObjectMapper mapper = new ObjectMapper();

        String json = mapper.writeValueAsString(persons);

        assertThat(json).isEqualTo("[{\"lastName\":\"磯野\",\"firstName\":\"カツオ\",\"age\":11},{\"lastName\":\"フグ田\",\"firstName\":\"タラオ\",\"age\":3}]");

        List<Person> deserializedPersons = mapper.readValue(json, new TypeReference<>() {
        });

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get(0)).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get(0).getLastName()).isEqualTo("磯野");
        assertThat(deserializedPersons.get(0).getFirstName()).isEqualTo("カツオ");
        assertThat(deserializedPersons.get(0).getAge()).isEqualTo(11);

        assertThat(deserializedPersons.get(1)).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get(1).getLastName()).isEqualTo("フグ田");
        assertThat(deserializedPersons.get(1).getFirstName()).isEqualTo("タラオ");
        assertThat(deserializedPersons.get(1).getAge()).isEqualTo(3);
    }

今回は、ObjectMapper#readValueの第2引数にいきなりTypeReferenceのサブクラスを作成して渡しています。

        List<Person> deserializedPersons = mapper.readValue(json, new TypeReference<>() {
        });

すると、Listの中に格納されるのがPersonインスタンスになります(LinkedHashMapではなく)。

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get(0)).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get(0).getLastName()).isEqualTo("磯野");
        assertThat(deserializedPersons.get(0).getFirstName()).isEqualTo("カツオ");
        assertThat(deserializedPersons.get(0).getAge()).isEqualTo(11);

        assertThat(deserializedPersons.get(1)).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get(1).getLastName()).isEqualTo("フグ田");
        assertThat(deserializedPersons.get(1).getFirstName()).isEqualTo("タラオ");
        assertThat(deserializedPersons.get(1).getAge()).isEqualTo(3);
JavaTypeを使う

続いて、JavaType

    @Test
    public void provideJavaTypeAsList() throws JsonProcessingException {
        List<Person> persons =
                List.of(
                        Person.create("磯野", "カツオ", 11),
                        Person.create("フグ田", "タラオ", 3)
                );

        ObjectMapper mapper = new ObjectMapper();

        String json = mapper.writeValueAsString(persons);

        assertThat(json).isEqualTo("[{\"lastName\":\"磯野\",\"firstName\":\"カツオ\",\"age\":11},{\"lastName\":\"フグ田\",\"firstName\":\"タラオ\",\"age\":3}]");

        TypeFactory typeFactory = mapper.getTypeFactory();
        CollectionType collectionType = typeFactory.constructCollectionType(List.class, Person.class);

        List<Person> deserializedPersons = mapper.readValue(json, collectionType);

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get(0)).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get(0).getLastName()).isEqualTo("磯野");
        assertThat(deserializedPersons.get(0).getFirstName()).isEqualTo("カツオ");
        assertThat(deserializedPersons.get(0).getAge()).isEqualTo(11);

        assertThat(deserializedPersons.get(1)).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get(1).getLastName()).isEqualTo("フグ田");
        assertThat(deserializedPersons.get(1).getFirstName()).isEqualTo("タラオ");
        assertThat(deserializedPersons.get(1).getAge()).isEqualTo(3);
    }

こんな感じで、ObjectMapperからTypeFactoryを取得して、TypeFactory#construct〜Typeを使ってJavaType
構築します。

        TypeFactory typeFactory = mapper.getTypeFactory();
        CollectionType collectionType = typeFactory.constructCollectionType(List.class, Person.class);

        List<Person> deserializedPersons = mapper.readValue(json, collectionType);

construct〜Typeなメソッドは、配列、コレクション、MapParameticTypeなどいろいろあります。

TypeFactory

TypeReferenceをJavaTypeに変換して使う

最後に、TypeReferenceJavaTypeに変換してみましょう。

    @Test
    public void provideTypeReferenceToJavaTypeAsList() throws JsonProcessingException {
        List<Person> persons =
                List.of(
                        Person.create("磯野", "カツオ", 11),
                        Person.create("フグ田", "タラオ", 3)
                );

        ObjectMapper mapper = new ObjectMapper();

        String json = mapper.writeValueAsString(persons);

        assertThat(json).isEqualTo("[{\"lastName\":\"磯野\",\"firstName\":\"カツオ\",\"age\":11},{\"lastName\":\"フグ田\",\"firstName\":\"タラオ\",\"age\":3}]");

        TypeReference<List<Person>> typeReference = new TypeReference<>() {
        };
        JavaType javaType = mapper.getTypeFactory().constructType(typeReference);

        List<Person> deserializedPersons = mapper.readValue(json, javaType);

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get(0)).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get(0).getLastName()).isEqualTo("磯野");
        assertThat(deserializedPersons.get(0).getFirstName()).isEqualTo("カツオ");
        assertThat(deserializedPersons.get(0).getAge()).isEqualTo(11);

        assertThat(deserializedPersons.get(1)).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get(1).getLastName()).isEqualTo("フグ田");
        assertThat(deserializedPersons.get(1).getFirstName()).isEqualTo("タラオ");
        assertThat(deserializedPersons.get(1).getAge()).isEqualTo(3);
    }

こんな感じで、TypeReferenceのサブクラスのインスタンスを作成した後に、TypeFactory#constructTypeを使って
JavaTypeを構築することができます。

        TypeReference<List<Person>> typeReference = new TypeReference<>() {
        };
        JavaType javaType = mapper.getTypeFactory().constructType(typeReference);

        List<Person> deserializedPersons = mapper.readValue(json, javaType);

だいたい、使い方はわかった気がしますね。

Mapで使う

もうひとつ、Mapでバリエーションを試してみましょう。

Classを指定する

まずは、ClassObjectMapper#readValueに指定するパターン。

    @Test
    public void nonProvideTypeAsMap() throws JsonProcessingException {
        Map<String, Person> persons = new LinkedHashMap<>();
        persons.put("katsuo", Person.create("磯野", "カツオ", 11));
        persons.put("tarao", Person.create("フグ田", "タラオ", 3));

        ObjectMapper mapper = new ObjectMapper();

        String json = mapper.writeValueAsString(persons);

        assertThat(json).isEqualTo("{\"katsuo\":{\"lastName\":\"磯野\",\"firstName\":\"カツオ\",\"age\":11},\"tarao\":{\"lastName\":\"フグ田\",\"firstName\":\"タラオ\",\"age\":3}}");

        Map<String, Object> deserializedPersons = mapper.readValue(json, Map.class);

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get("katsuo")).isNotInstanceOf(Person.class);
        assertThat(deserializedPersons.get("katsuo")).isInstanceOf(LinkedHashMap.class);
        assertThat(deserializedPersons.get("katsuo")).extracting("lastName").isEqualTo("磯野");
        assertThat(deserializedPersons.get("katsuo")).extracting("firstName").isEqualTo("カツオ");
        assertThat(deserializedPersons.get("katsuo")).extracting("age").isEqualTo(11);

        assertThat(deserializedPersons.get("tarao")).isNotInstanceOf(Person.class);
        assertThat(deserializedPersons.get("tarao")).isInstanceOf(LinkedHashMap.class);
        assertThat(deserializedPersons.get("tarao")).extracting("lastName").isEqualTo("フグ田");
        assertThat(deserializedPersons.get("tarao")).extracting("firstName").isEqualTo("タラオ");
        assertThat(deserializedPersons.get("tarao")).extracting("age").isEqualTo(3);
    }

こちらは、値がLinkedHashMapMapとなります。

        List<Object> deserializedPersons = mapper.readValue(json, List.class);

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get(0)).isNotInstanceOf(Person.class);
        assertThat(deserializedPersons.get(0)).isInstanceOf(LinkedHashMap.class);
        assertThat(deserializedPersons.get(0)).extracting("lastName").isEqualTo("磯野");
        assertThat(deserializedPersons.get(0)).extracting("firstName").isEqualTo("カツオ");
        assertThat(deserializedPersons.get(0)).extracting("age").isEqualTo(11);

        assertThat(deserializedPersons.get(1)).isNotInstanceOf(Person.class);
        assertThat(deserializedPersons.get(1)).isInstanceOf(LinkedHashMap.class);
        assertThat(deserializedPersons.get(1)).extracting("lastName").isEqualTo("フグ田");
        assertThat(deserializedPersons.get(1)).extracting("firstName").isEqualTo("タラオ");
        assertThat(deserializedPersons.get(1)).extracting("age").isEqualTo(3);
TypeReferenceを使う

TypeReferenceを使った場合は、こんな感じに。

    @Test
    public void provideTypeReferenceAsMap() throws JsonProcessingException {
        Map<String, Person> persons = new LinkedHashMap<>();
        persons.put("katsuo", Person.create("磯野", "カツオ", 11));
        persons.put("tarao", Person.create("フグ田", "タラオ", 3));

        ObjectMapper mapper = new ObjectMapper();

        String json = mapper.writeValueAsString(persons);

        assertThat(json).isEqualTo("{\"katsuo\":{\"lastName\":\"磯野\",\"firstName\":\"カツオ\",\"age\":11},\"tarao\":{\"lastName\":\"フグ田\",\"firstName\":\"タラオ\",\"age\":3}}");

        Map<String, Person> deserializedPersons = mapper.readValue(json, new TypeReference<>() {
        });

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get("katsuo")).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get("katsuo").getLastName()).isEqualTo("磯野");
        assertThat(deserializedPersons.get("katsuo").getFirstName()).isEqualTo("カツオ");
        assertThat(deserializedPersons.get("katsuo").getAge()).isEqualTo(11);

        assertThat(deserializedPersons.get("tarao")).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get("tarao").getLastName()).isEqualTo("フグ田");
        assertThat(deserializedPersons.get("tarao").getFirstName()).isEqualTo("タラオ");
        assertThat(deserializedPersons.get("tarao").getAge()).isEqualTo(3);
    }

Listの時と同じように、TypeReferenceMapに関する型情報を指定してObjectMapper#readValueに与えることで、
Map<String, Person>としてデシリアライズできます。

        Map<String, Person> deserializedPersons = mapper.readValue(json, new TypeReference<>() {
        });

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get("katsuo")).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get("katsuo").getLastName()).isEqualTo("磯野");
        assertThat(deserializedPersons.get("katsuo").getFirstName()).isEqualTo("カツオ");
        assertThat(deserializedPersons.get("katsuo").getAge()).isEqualTo(11);

        assertThat(deserializedPersons.get("tarao")).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get("tarao").getLastName()).isEqualTo("フグ田");
        assertThat(deserializedPersons.get("tarao").getFirstName()).isEqualTo("タラオ");
        assertThat(deserializedPersons.get("tarao").getAge()).isEqualTo(3);
JavaTypeを使う

JavaTypeを使った場合。

    @Test
    public void provideJavaTypeAsMap() throws JsonProcessingException {
        Map<String, Person> persons = new LinkedHashMap<>();
        persons.put("katsuo", Person.create("磯野", "カツオ", 11));
        persons.put("tarao", Person.create("フグ田", "タラオ", 3));

        ObjectMapper mapper = new ObjectMapper();

        String json = mapper.writeValueAsString(persons);

        assertThat(json).isEqualTo("{\"katsuo\":{\"lastName\":\"磯野\",\"firstName\":\"カツオ\",\"age\":11},\"tarao\":{\"lastName\":\"フグ田\",\"firstName\":\"タラオ\",\"age\":3}}");

        TypeFactory typeFactory = mapper.getTypeFactory();
        MapType mapType = typeFactory.constructMapType(Map.class, String.class, Person.class);

        Map<String, Person> deserializedPersons = mapper.readValue(json, mapType);

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get("katsuo")).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get("katsuo").getLastName()).isEqualTo("磯野");
        assertThat(deserializedPersons.get("katsuo").getFirstName()).isEqualTo("カツオ");
        assertThat(deserializedPersons.get("katsuo").getAge()).isEqualTo(11);

        assertThat(deserializedPersons.get("tarao")).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get("tarao").getLastName()).isEqualTo("フグ田");
        assertThat(deserializedPersons.get("tarao").getFirstName()).isEqualTo("タラオ");
        assertThat(deserializedPersons.get("tarao").getAge()).isEqualTo(3);
    }

Mapを対象とする場合は、TypeFactory#constructMapTypeを使います。

        TypeFactory typeFactory = mapper.getTypeFactory();
        MapType mapType = typeFactory.constructMapType(Map.class, String.class, Person.class);

        Map<String, Person> deserializedPersons = mapper.readValue(json, mapType);
TypeReferenceからJavaTypeに変換して使う

最後は、TypeReferenceからJavaTypeに変換してObjectMapper#readValueに適用します。

    @Test
    public void provideTypeReferenceToJavaTypeAsMap() throws JsonProcessingException {
        Map<String, Person> persons = new LinkedHashMap<>();
        persons.put("katsuo", Person.create("磯野", "カツオ", 11));
        persons.put("tarao", Person.create("フグ田", "タラオ", 3));

        ObjectMapper mapper = new ObjectMapper();

        String json = mapper.writeValueAsString(persons);

        assertThat(json).isEqualTo("{\"katsuo\":{\"lastName\":\"磯野\",\"firstName\":\"カツオ\",\"age\":11},\"tarao\":{\"lastName\":\"フグ田\",\"firstName\":\"タラオ\",\"age\":3}}");

        TypeReference<Map<String, Person>> typeReference = new TypeReference<>() {
        };
        JavaType javaType = mapper.getTypeFactory().constructType(typeReference);

        Map<String, Person> deserializedPersons = mapper.readValue(json, javaType);

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get("katsuo")).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get("katsuo").getLastName()).isEqualTo("磯野");
        assertThat(deserializedPersons.get("katsuo").getFirstName()).isEqualTo("カツオ");
        assertThat(deserializedPersons.get("katsuo").getAge()).isEqualTo(11);

        assertThat(deserializedPersons.get("tarao")).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get("tarao").getLastName()).isEqualTo("フグ田");
        assertThat(deserializedPersons.get("tarao").getFirstName()).isEqualTo("タラオ");
        assertThat(deserializedPersons.get("tarao").getAge()).isEqualTo(3);
    }

こちらはListの時と同様に、TypeFactory#constructTypeを使ってTypeReferenceを元にJavaTypeを構築すればOKです。

        TypeReference<Map<String, Person>> typeReference = new TypeReference<>() {
        };
        JavaType javaType = mapper.getTypeFactory().constructType(typeReference);

        Map<String, Person> deserializedPersons = mapper.readValue(json, javaType);

まとめ

Jacksonを使って、型引数を持ったクラスにデシリアライズする方法を見てみました。

あまり考えたことがなかったのと、調べようとしてもちょっと見つけにくかった感じがしたので、自分でもまとめつつ
Javadocも眺めてみました。

調べるとTypeReferenceの方が最初に見つかるのですが、Javadocを見ているとJavaTypeのことに気づいたりするので、
見返してみると発見がありますね、と…。

systemdのユニット定義ファイルは、どこに置けばいい?

これは、なにをしたくて書いたもの?

systemdのユニット定義ファイルですが、どこに置くものだったかよく忘れるので。

ちょっと調べて、メモしておこうかなと。

manを見る

まずは、systemdのマニュアルを見てみます。

systemd(1) - Linux manual page

DIRECTORIESに、説明が書いてあります。

systemd / DIRECTORIES

System Unit Directoryと、User Unit Directoryの2つがあるようです。

完全なディレクトリのリストは、systemd.unitを見てください、と。

Full list of directories is provided in systemd.unit(5).

User Unitってなんでしょう?

systemd - ArchWiki

systemd/ユーザー - ArchWiki

どうやら、systemdにはシステムモードとユーザーモードの2つがあるようです。

--system, --user

it shall operate in system or per-user mode,

systemd / OPTIONS

ユーザーモードは、ユーザーがログインした時にユーザーごとのサービスを実行するモードのようです。ユーザーセッションが
残っている限り動作し続け、ユーザーのセッションがなくなると終了する、と。知らなかったです。

一方で、サーバー用途でのsystemdで馴染みがあるのはシステムモードだと思います。

というわけで、以降は基本的にはシステムモードの話でいきます。

systemd.unit

さて、systemd.unitのドキュメントを見るように書かれていたので、見てみましょう。

systemd.unit(5) - Linux manual page

システムモード、ユーザーモードそれぞれでsystemdが参照するパスは、以下のようです。

  • System Unit Search Path
    • /etc/systemd/system.control/*
    • /run/systemd/system.control/*
    • /run/systemd/transient/*
    • /run/systemd/generator.early/*
    • /etc/systemd/system/*
    • /etc/systemd/system.attached/*
    • /run/systemd/system/*
    • /run/systemd/system.attached/*
    • /run/systemd/generator/*
    • ...
    • /usr/lib/systemd/system/*
    • /run/systemd/generator.late/*
  • User Unit Search Path
    • ~/.config/systemd/user.control/*
    • $XDG_RUNTIME_DIR/systemd/user.control/*
    • $XDG_RUNTIME_DIR/systemd/transient/*
    • $XDG_RUNTIME_DIR/systemd/generator.early/*
    • ~/.config/systemd/user/*
    • $XDG_CONFIG_DIRS/systemd/user/*
    • /etc/systemd/user/*
    • $XDG_RUNTIME_DIR/systemd/user/*
    • /run/systemd/user/*
    • $XDG_RUNTIME_DIR/systemd/generator/*
    • $XDG_DATA_HOME/systemd/user/*
    • $XDG_DATA_DIRS/systemd/user/*
    • ...
    • /usr/lib/systemd/user/*
    • $XDG_RUNTIME_DIR/systemd/generator.late/*

システムモードの方のパスを見ると、/etc/systemd/system/*/usr/lib/systemd/system/*とよく見るパスが出てきたと思います。

それぞれのパスの説明は、UNIT FILE LOAD PATHに書かれています。

systemd.unit / UNIT FILE LOAD PATH

抜粋してみましょう。

  • /usr/lib/systemd/system … System units installed by the distribution package manager(パッケージマネージャーによってインストールされたシステムユニット)
  • /etc/systemd/system … System units created by the administrator(管理者によって作成されたシステムユニット)

systemd.unit / DESCRIPTION

この説明を見て、パッケージが使うのは/usr/lib/systemd/system、利用者(サーバーの管理者)が使うのは
/etc/systemd/systemということがなんとなくわかりますが、EXAMPLESにもう少し具体的に書かれています。

systemd.unit / EXAMPLES

たとえば、ベンダー(パッケージ提供者)の設定をオーバーライドするには/usr/lib/systemd/systemにあるユニット定義ファイルを
/etc/systemd/systemにコピーして変更します、と。

files: copying the unit file from /usr/lib/systemd/system to /etc/systemd/system and modifying the chosen settings.

または、unit.dディレクトリを作ってもよいみたいですが…こちらは今回は置いておきます。

Alternatively, one can create a directory named unit.d/ within /etc/systemd/system and place a drop-in file name.

usr/lib/systemd/systemから/etc/systemd/systemにコピーして変更した場合、ファイルは完全に上書きになり、
それ以降ベンダー側のユニット定義ファイルは読まれません。

The advantage of the first method is that one easily overrides the complete unit, the vendor unit is not parsed at all anymore.

つまり、/etc/systemd/systemの方が優先度が高いことになります。

というわけで、/usr/lib/systemd/system/etc/systemd/systemの使い分けはわかった気がします。

あと、知っておいた方が良さそうなのは/run/systemd/systemという実行時に自動的に作成されるユニット定義ファイルを
配置するディレクトリですね。

参考までに、Ret Hat Enterprise Linuxのsystemd ユニット定義ファイルのドキュメントも見ておいた方が良さそうです。

第17章 systemd ユニットファイルでの作業 Red Hat Enterprise Linux 8 | Red Hat Customer Portal

ここまでドキュメントをいろいろ見てきたので、少し実際に確認してみるとしましょう。

次の2つのパターンを試してみたいと思います。

  • パッケージをインストールして、そのユニット定義ファイルをカスタマイズする
  • 独自のユニット定義ファイルを作成する

環境

今回の環境は、こちらです。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:    20.04
Codename:   focal


$ uname -srvmpio
Linux 5.4.0-74-generic #83-Ubuntu SMP Sat May 8 02:35:39 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

Ubuntu Linux 20.04 LTSです。

Apacheをパッケージインストールして、カスタマイズしてみる

パッケージインストールの例として、Apacheを使ってみましょう。

まずはインストール。

$ sudo apt install apache2

ところでApacheをインストールしているとsystemctl enableのログらしきものが出てきますが、この時のシンボリックリンク
作成先は/lib/systemd/systemとなっています。

Created symlink /etc/systemd/system/multi-user.target.wants/apache2.service → /lib/systemd/system/apache2.service.
Created symlink /etc/systemd/system/multi-user.target.wants/apache-htcacheclean.service → /lib/systemd/system/apache-htcacheclean.service.

Ubuntu Linuxの場合だと/usr/lib/systemd/systemディレクトリではなく、/lib/systemd/systemディレクトリを見るようです。

第598回 systemdユニットの設定を変える:Ubuntu Weekly Recipe|gihyo.jp … 技術評論社

といっても、/libディレクトリを確認すると/usr/libディレクトリにリンクされているだけなので、結局のところ
/usr/lib/systemd/systemディレクトリにファイルはあるわけですが。

$ ll /lib
lrwxrwxrwx 1 root root 7 May 26 19:39 /lib -> usr/lib/

$ ll /usr/lib/systemd/system/apache2.service 
-rw-r--r-- 1 root root 395 Apr 13  2020 /usr/lib/systemd/system/apache2.service

ステータスを確認してみます。

$ systemctl status apache2
● apache2.service - The Apache HTTP Server
     Loaded: loaded (/lib/systemd/system/apache2.service; enabled; vendor preset: enabled)
     Active: active (running) since Sun 2021-06-06 10:47:38 UTC; 8min ago
       Docs: https://httpd.apache.org/docs/2.4/
   Main PID: 1641 (apache2)
      Tasks: 55 (limit: 2280)
     Memory: 5.5M
     CGroup: /system.slice/apache2.service
             ├─1641 /usr/sbin/apache2 -k start
             ├─1642 /usr/sbin/apache2 -k start
             └─1643 /usr/sbin/apache2 -k start

Jun 06 10:47:38 ubuntu2004.localdomain systemd[1]: Starting The Apache HTTP Server...
Jun 06 10:47:38 ubuntu2004.localdomain apachectl[1640]: AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using ubuntu2004.localdomain. Set>
Jun 06 10:47:38 ubuntu2004.localdomain systemd[1]: Started The Apache HTTP Server.

今回、ここで表示されているDescriptionのThe Apache HTTP Serverを書き換えて、カスタマイズしてみましょう。

まずは、/usr/lib/systemd/systemから/etc/systemd/systemへユニット定義ファイルをコピー。

$ sudo cp /usr/lib/systemd/system/apache2.service /etc/systemd/system/apache2.service

ユニット定義ファイルの、Descriptionを変更します。

/etc/systemd/system/apache2.service

[Unit]
Description=The Apache HTTP Server(Customized)
After=network.target remote-fs.target nss-lookup.target
Documentation=https://httpd.apache.org/docs/2.4/

[Service]
Type=forking
Environment=APACHE_STARTED_BY_SYSTEMD=true
ExecStart=/usr/sbin/apachectl start
ExecStop=/usr/sbin/apachectl stop
ExecReload=/usr/sbin/apachectl graceful
PrivateTmp=true
Restart=on-abort

[Install]
WantedBy=multi-user.target

そして、systemctl daemon-reloadを実行。

$ sudo systemctl daemon-reload

systemctl statusを見ると、ロードされているユニット定義ファイルが変更されていることと、Descriptionの変更が
反映されていることが確認できます。

$ systemctl status apache2
● apache2.service - The Apache HTTP Server(Customize)
     Loaded: loaded (/etc/systemd/system/apache2.service; enabled; vendor preset: enabled)
     Active: active (running) since Sun 2021-06-06 11:04:18 UTC; 38s ago
       Docs: https://httpd.apache.org/docs/2.4/
   Main PID: 3081 (apache2)
      Tasks: 55 (limit: 2280)
     Memory: 5.2M
     CGroup: /system.slice/apache2.service
             ├─3081 /usr/sbin/apache2 -k start
             ├─3082 /usr/sbin/apache2 -k start
             └─3083 /usr/sbin/apache2 -k start

Jun 06 11:04:18 ubuntu2004.localdomain systemd[1]: Starting The Apache HTTP Server...
Jun 06 11:04:18 ubuntu2004.localdomain apachectl[3064]: AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using ubuntu2004.localdomain. Set>
Jun 06 11:04:18 ubuntu2004.localdomain systemd[1]: Started The Apache HTTP Server.

ちなみに、/etc/systemd/system/multi-user.target.wantsから参照されているのは、/lib/systemd/systemから実は変わって
いなかったりします。

$ ll /etc/systemd/system/multi-user.target.wants/apache2.service
lrwxrwxrwx 1 root root 35 Jun  6 11:03 /etc/systemd/system/multi-user.target.wants/apache2.service -> /lib/systemd/system/apache2.service

ちょっと不思議な感じはしますが、systemctl daemon-reloadで反映させる、で合ってはいるみたいです。

ここでsystemctl enableを実行すると、シンボリックリンクも貼り替えてくれます。

$ sudo systemctl enable apache2
Synchronizing state of apache2.service with SysV service script with /lib/systemd/systemd-sysv-install.
Executing: /lib/systemd/systemd-sysv-install enable apache2
Removed /etc/systemd/system/multi-user.target.wants/apache2.service.
Created symlink /etc/systemd/system/multi-user.target.wants/apache2.service → /etc/systemd/system/apache2.service.

ここまでやった方がいいのでしょうか?

$ systemctl status apache2
● apache2.service - The Apache HTTP Server(Customize)
     Loaded: loaded (/etc/systemd/system/apache2.service; enabled; vendor preset: enabled)
     Active: active (running) since Sun 2021-06-06 11:07:09 UTC; 6min ago
       Docs: https://httpd.apache.org/docs/2.4/
   Main PID: 670 (apache2)
      Tasks: 55 (limit: 2280)
     Memory: 8.0M
     CGroup: /system.slice/apache2.service
             ├─670 /usr/sbin/apache2 -k start
             ├─671 /usr/sbin/apache2 -k start
             └─672 /usr/sbin/apache2 -k start

Jun 06 11:07:09 ubuntu2004.localdomain systemd[1]: Starting The Apache HTTP Server(Customize)...
Jun 06 11:07:09 ubuntu2004.localdomain apachectl[654]: AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using ubuntu2004.localdomain. Set >
Jun 06 11:07:09 ubuntu2004.localdomain systemd[1]: Started The Apache HTTP Server(Customize).

独自のユニット定義ファイルを作成する

最後に、自分でユニット定義ファイルを作成してみます。

お題は、Prometheusにしましょう。

Prometheus用のユーザーを作成。

$ sudo useradd -r prometheus

インストール先は、/opt/prometheusにしましょう。

$ cd /opt
$ sudo curl -OLs https://github.com/prometheus/prometheus/releases/download/v2.27.1/prometheus-2.27.1.linux-amd64.tar.gz
$ sudo tar xf prometheus-2.27.1.linux-amd64.tar.gz
$ sudo mv prometheus-2.27.1.linux-amd64 prometheus
$ sudo chown -R prometheus.prometheus prometheus

データ保存用のディレクトリを作成。

$ sudo mkdir -p /var/lib/prometheus
$ sudo chown prometheus.prometheus /var/lib/prometheus

Prometheus用のユニット定義ファイルを作成。

/etc/systemd/system/prometheus.service

[Unit]
Description=Prometheus - systems monitoring and alerting toolkit
Documentation=https://prometheus.io/docs/
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/opt/prometheus/prometheus --config.file=/opt/prometheus/prometheus.yml --storage.tsdb.path=/var/lib/prometheus/data
ExecReload=/bin/kill -HUP $MAINPID
User=prometheus

[Install]
WantedBy=multi-user.target

systemdのサービスとして、有効化。

$ sudo systemctl enable prometheus
Created symlink /etc/systemd/system/multi-user.target.wants/prometheus.service → /etc/systemd/system/prometheus.service.

起動。

$ sudo systemctl start prometheus

確認。

$ sudo systemctl status prometheus
● prometheus.service - Prometheus - systems monitoring and alerting toolkit
     Loaded: loaded (/etc/systemd/system/prometheus.service; enabled; vendor preset: enabled)
     Active: active (running) since Sun 2021-06-06 11:57:57 UTC; 13s ago
       Docs: https://prometheus.io/docs/
   Main PID: 5399 (prometheus)
      Tasks: 7 (limit: 2280)
     Memory: 18.7M
     CGroup: /system.slice/prometheus.service
             └─5399 /opt/prometheus/prometheus --config.file=/opt/prometheus/prometheus.yml --storage.tsdb.path=/var/lib/prometheus/data

Jun 06 11:57:58 ubuntu2004.localdomain prometheus[5399]: level=info ts=2021-06-06T11:57:58.047Z caller=head.go:755 component=tsdb msg="On-disk memory mappable chunks replay compl>
Jun 06 11:57:58 ubuntu2004.localdomain prometheus[5399]: level=info ts=2021-06-06T11:57:58.047Z caller=head.go:761 component=tsdb msg="Replaying WAL, this may take a while"
Jun 06 11:57:58 ubuntu2004.localdomain prometheus[5399]: level=info ts=2021-06-06T11:57:58.052Z caller=head.go:813 component=tsdb msg="WAL segment loaded" segment=0 maxSegment=1
Jun 06 11:57:58 ubuntu2004.localdomain prometheus[5399]: level=info ts=2021-06-06T11:57:58.053Z caller=head.go:813 component=tsdb msg="WAL segment loaded" segment=1 maxSegment=1
Jun 06 11:57:58 ubuntu2004.localdomain prometheus[5399]: level=info ts=2021-06-06T11:57:58.053Z caller=head.go:818 component=tsdb msg="WAL replay completed" checkpoint_replay_dur>
Jun 06 11:57:58 ubuntu2004.localdomain prometheus[5399]: level=info ts=2021-06-06T11:57:58.054Z caller=main.go:828 fs_type=EXT4_SUPER_MAGIC
Jun 06 11:57:58 ubuntu2004.localdomain prometheus[5399]: level=info ts=2021-06-06T11:57:58.054Z caller=main.go:831 msg="TSDB started"
Jun 06 11:57:58 ubuntu2004.localdomain prometheus[5399]: level=info ts=2021-06-06T11:57:58.054Z caller=main.go:957 msg="Loading configuration file" filename=/opt/prometheus/prome>
Jun 06 11:57:58 ubuntu2004.localdomain prometheus[5399]: level=info ts=2021-06-06T11:57:58.055Z caller=main.go:988 msg="Completed loading of configuration file" filename=/opt/pro>
Jun 06 11:57:58 ubuntu2004.localdomain prometheus[5399]: level=info ts=2021-06-06T11:57:58.055Z caller=main.go:775 msg="Server is ready to receive web requests."

なんとなく、リロードも対応しておきました。

$ sudo systemctl reload prometheus

停止。

$ sudo systemctl stop prometheus

こんなところでしょうか。

まとめ

systemdのユニット定義ファイルの置き場所をあらためて調べ直して、まとめておきました。

いつも雰囲気で見ていた気がするので、こういう機会も良いかな、と。