객체 직렬화하기

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:

참고

If you need special constraints or rules for your serialization (such as sorting the elements in a Set), then you should consider using your own TypeSerializer implementations.

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.

참고

ConfigurationOptions are immutable. Every time you try to modify the original instance a new instance is created; so you either have to use the (chained) result directly or update your variable accordingly.

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.

If you need the TypeToken.of(DiamondCounter.class) in multiple places, then you should consider creating a constant for it. You can do it in a similar fashion as Sponge does in the TypeTokens class, or just define the constant inside of your data or serializer class.

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가 새로운 객체를 인스턴스화할 빈 생성자가 필요합니다.

참고

You can also have fields that are not are not annotated with @Setting in your @ConfigSerializable classes. These fields won’t be persisted to config files and can be used to store temporary references for your plugin.

Using Default Values in ConfigSerializable Types

It is also possible to use default values inside of @ConfigSerializable types. You just have to use Java’s field initializers (or getters) to set some default values. As long as the entry is not present in the config file the value won’t be overwritten.

@ConfigSerializable
public class DiamondCounter {

    @Setting(value="player", comment="Player UUID")
    private UUID playerUUID;

    @Setting(comment="Number of diamonds mined")
    private int diamonds = 0;

    @Setting(comment="The time the player found a diamond last.")
    private LocalDateTime diamonds = LocalDateTime.now();

    [...]
}

Example: Loading a ConfigSerializable Config with Default Values

Instead of loading a default config from the plugin jar itself, it is also possible to just ask Configurate to create it if it is missing.

try {
    this.config = this.configManager.load().<Configuration>getValue(Configuration.TYPE, Configuration::generateDefault);
} catch (ObjectMappingException | IOException e) {
    this.logger.error("Failed to load the config - Using a default", e);
    this.config = Configuration.generateErrorDefault();
}

In this case you load the entire configuration into a Configuration object that contains all of your plugins configuration. Using such a class has the following benefits:

  • Type safety is guaranteed

  • No need to update the configuration file shipped in your plugin

  • You don’t need to store lots of references for each of your configuration options

  • You can pass this config (or its parts) into methods or reference it from other classes

  • It is easy to write comments for each attribute in a place that also helps you during development

참고

In this case Configuration.generateDefault() is called when the config file is missing or empty. If you still want to load the shipped default config asset you can load it inside of that method. Configuration.generateErrorDefault() is called when there is an error reading or parsing the config. It is not necessary to use separate methods for those cases; you can also use the no-arg constructor, or use an entirely custom solution.

Example: Saving a ConfigSerializable Config

Saving a @ConfigSerializable config is also very simple, as shown by the following example:

try {
    this.configManager.save(this.configManager.createEmptyNode().setValue(Configuration.TYPE, this.config));
} catch (IOException | ObjectMappingException e) {
    this.logger.error("Failed to save the config", e);
}

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", description = "Stolen")
public class StolenCodeExample {

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

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

참고

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