객체 직렬화하기

The Configurate library also provides the means to tweak automatic serialization and deserialization of objects. Per default, a set of data types can be (de)serialized: among others Strings, ints, doubles, UUIDs, Lists (of serializable values) and Maps (where both keys and values are serializable). But if you want to write your custom data structures to a config file, this will not be enough.

Imagine a data structure tracking how many diamonds a player has mined. It might look a little like this:

public class DiamondCounter {
    private UUID playerUUID;
    private int diamonds;

    ...
}

Also assume some methods to access those fields, a nice constructor setting both of those etc.

Creating a custom TypeSerializer

A very straightforward way of writing and loading such a data structure is providing a custom TypeSerializer. The TypeSerializer interface provides two methods, one to write the data from an object to a configuration node and one to create an object from a given configuration node.

import com.google.common.reflect.TypeToken;
import ninja.leaping.configurate.objectmapping.ObjectMappingException;
import ninja.leaping.configurate.objectmapping.serialize.TypeSerializer;

public class DiamondCounterSerializer implements TypeSerializer<DiamondCounter> {

    @Override
    public DiamondCounter deserialize(TypeToken<?> type, ConfigurationNode value)
      throws ObjectMappingException {
        UUID player = value.getNode("player").getValue(TypeToken.of(UUID.class));
        int diamonds = value.getNode("diamonds").getInt();
        return new DiamondCounter(player, diamonds);
    }

    @Override
    public void serialize(TypeToken<?> type, DiamondCounter obj, ConfigurationNode value)
      throws ObjectMappingException {
        value.getNode("player").setValue(obj.getPlayerUUID());
        value.getNode("diamonds").setValue(obj.getDiamonds());
    }
}

This TypeSerializer must then be registered with Configurate. This can be done either globally, by registering to the default TypeSerializerCollection or locally, by specifying it in the ConfigurationOptions when loading your config.

Code Example: Registering a TypeSerializer globally

import ninja.leaping.configurate.objectmapping.serialize.TypeSerializers;

TypeSerializers.getDefaultSerializers().registerType(TypeToken.of(DiamondCounter.class), new DiamondCounterSerializer());

Code Example: Registering a TypeSerializer locally

import ninja.leaping.configurate.ConfigurationNode;
import ninja.leaping.configurate.ConfigurationOptions;
import ninja.leaping.configurate.objectmapping.serialize.TypeSerializerCollection;
import ninja.leaping.configurate.objectmapping.serialize.TypeSerializers;

TypeSerializerCollection serializers = TypeSerializers.getDefaultSerializers().newChild();
serializers.registerType(TypeToken.of(DiamondCounter.class), new DiamondCounterSerializer());
ConfigurationOptions options = ConfigurationOptions.defaults().setSerializers(serializers);
ConfigurationNode rootNode = someConfigurationLoader.load(options);

경고

If you provide a custom TypeSerializer for types that are not introduced by your own plugin, you should only ever register them locally in order to avoid conflicts with other plugins or Sponge, caused by a TypeSerializer being overwritten.

ObjectMapper 사용하기

많은 경우에 (비)직렬화는 매핑될 필드를 설정 노드에 압축시킴으로써 구현합니다. TypeSerializer를 작성하는 것은 상당히 귀찮은 작업이고, Configurate가 이를 자동으로 처리할 수 있습니다. 이제 ConfigSerializable 어노테이션을 클래스에 붙이고, Setting 어노테이션을 사용해 봅시다.

import ninja.leaping.configurate.objectmapping.Setting;
import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;

@ConfigSerializable
public class DiamondCounter {

    @Setting(value="player", comment="Player UUID")
    private UUID playerUUID;
    @Setting(comment="Number of diamonds mined")
    private int diamonds;

    ...
}

위 예제의 코드는 특별한 등록 없이도 직렬화와 비직렬화가 됩니다. @Setting 어노테이션은 어노테이션이 붙은 필드에 설정 노드를 매핑합니다. 이 어노테이션은 선택적 매개값으로 value``와 ``comment``를 받아들입니다. ``value``는 해당 필드가 저장될 노드의 이름이 됩니다. 값을 지정하지 않으면, 필드 변수의 이름이 대신 사용됩니다. 따라서 우리의 예제에서, Setting 어노테이션은 필드의 내용 ``playerUUID``가 "player" 노드에 저장되며, 노드의 주석으로 "Player UUID"가 붙도록 만듭니다. 하지만, ``diamonds 필드는 comment 값만 정의되어 있으므로 동일한 이름의 노드에 저장될 것입니다. 만약 서버 구현 프로그램이 설정 파일에 주석 사용을 지원하지 않는다면, 주석의 내용은 저장되지 않습니다.

``@Setting(value=”someNode”)``을 ``@Setting(“someNode”)``으로 요약할 수 있습니다.

`@ConfigSerializable``은 Configurate가 해당 클래스에 대한 :javadoc:`ObjectMapper`를 생성하도록 지시하기 때문에 이 클래스를 임의로 등록할 필요가 없습니다. 다만 제약 조건이 있습니다. 어노테이션이 붙은 필드가 등록되려면 Configurate가 새로운 객체를 인스턴스화할 빈 생성자가 필요합니다.

Providing a custom ObjectMapperFactory

하지만 :javadoc:`ObjectMapperFactory`를 사용하면 이 제약을 없앨 수 있으며, 대표적인 예로 :javadoc:`GuiceObjectMapperFactory`가 있습니다. 이것은 빈 생성자를 요구하지 않으며, guice가 종속물 주입을 통해 만들 수 있는 모든 클래스에서 동작할 것이며,``@Inject``와 ``@Setting``이 붙은 필드 또한 허용합니다.

당신의 플러그인은 종속물 주입 (의존성 주입 참조) 을 통해 ``GuiceObjectMapperFactory``을 받아서 ``ConfigurationOptions``에 전달할 수 있습니다.

import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.game.state.GamePreInitializationEvent;
import org.spongepowered.api.plugin.Plugin;
import com.google.inject.Inject;
import ninja.leaping.configurate.commented.CommentedConfigurationNode;
import ninja.leaping.configurate.loader.ConfigurationLoader;
import ninja.leaping.configurate.objectmapping.GuiceObjectMapperFactory;

@Plugin(name="IStoleThisFromZml", id="shamelesslystolen", version="0.8.15")
public class StolenCodeExample {

    @Inject private GuiceObjectMapperFactory factory;
    @Inject private ConfigurationLoader<CommentedConfigurationNode> loader;

    @Listener
    public void enable(GamePreInitializationEvent event) {
        CommentedConfigurationNode node =
          loader.load(ConfigurationOptions.defaults().setObjectMapperFactory(factory));
        DiamondCounter myDiamonds = node.getValue(TypeToken.of(DiamondCounter.class));
    }
}

참고

위 예제 코드는 간결성을 위해 적절한 예외 처리를 생략했습니다.