SpongeプラグインでSpringのDIコンテナを使う

MinecraftのSpongeプラグインでSpringを使ってみたのでその時のメモです。

Springを使うメリット
・テストがやりやすくなる
・AOPが使える←個人的にかなり重要

なぜAOPを使いたいか
継承では解決できない同じような処理がサーバーのプラグインでは結構出てくる(ロギングとか権限周り)
その辺をAOPで解決したい

プラグインのエントリポイント

package net.orekyuu.spring;

import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.game.state.GameConstructionEvent;
import org.spongepowered.api.plugin.Plugin;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

@Plugin(
        id = "net.orekyuu.spring",
        name = "SpringDemo",
        version = "1.0-SNAPSHOT"
)
public class SpringDemo {

    private ApplicationContext context;

    private static SpringDemo INSTANCE;

    @Listener
    public void onServerStart(GameConstructionEvent event) {
        INSTANCE = this;
        //net.orekyuu.spring以下をComponentScanしてる
        context = new AnnotationConfigApplicationContext("net.orekyuu.spring");
    }

    public static SpringDemo getInstance() {
        return INSTANCE;
    }

}

ゲーム起動時にApplicationContextを作るだけです。
プラグイン以下パッケージをComponentScanするようにしておくと、アノテーションベースの設定ができるようになります。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>net.orekyuu</groupId>
    <artifactId>spring</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>Spring</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <profiles>
        <profile>
            <id>release</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-source-plugin</artifactId>
                        <version>2.4</version>
                        <executions>
                            <execution>
                                <id>attach-sources</id>
                                <goals>
                                    <goal>jar-no-fork</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-javadoc-plugin</artifactId>
                        <version>2.10.3</version>
                        <executions>
                            <execution>
                                <id>attach-javadocs</id>
                                <goals>
                                    <goal>jar</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-gpg-plugin</artifactId>
                        <version>1.6</version>
                        <executions>
                            <execution>
                                <id>sign-artifacts</id>
                                <phase>verify</phase>
                                <goals>
                                    <goal>sign</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
                <resources>
                    <resource>
                        <directory>src/main/resources</directory>
                        <filtering>true</filtering>
                    </resource>
                </resources>
            </build>
        </profile>
    </profiles>

    <build>
        <resources>
            <resource>
                <directory>\${project.basedir}/src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.3</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>templating-maven-plugin</artifactId>
                <version>1.0-alpha-3</version>
                <executions>
                    <execution>
                        <id>filter-src</id>
                        <goals>
                            <goal>filter-sources</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <artifactId>maven-site-plugin</artifactId>
                <version>3.3</version>
                <dependencies>

                    <dependency>
                        <groupId>net.trajano.wagon</groupId>
                        <artifactId>wagon-git</artifactId>
                        <version>2.0.3</version>
                    </dependency>
                    <dependency>
                        <groupId>org.apache.maven.doxia</groupId>
                        <artifactId>doxia-module-markdown</artifactId>
                        <version>1.6</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <artifactId>maven-release-plugin</artifactId>
                <version>2.5.1</version>
                <configuration>
                    <autoVersionSubmodules>true</autoVersionSubmodules>
                    <tagNameFormat>@{project.version}</tagNameFormat>
                    <scmCommentPrefix xml:space="preserve">[RELEASE] </scmCommentPrefix>
                    <goals>install deploy site-deploy
                    </goals> <!-- install is here to fix javadoc generation in multi-module projects -->
                    <releaseProfiles>release</releaseProfiles>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <artifactSet>
                                <excludes>
                                    <exclude>junit:junit</exclude>
                                </excludes>
                            </artifactSet>
                        </configuration>
                    </execution>
                </executions>
                <configuration>
                    <relocations>
                        <relocation>
                            <pattern>org.apache</pattern>
                            <!--org.apacheパッケージのクラスをforgeが読み込んでくれないっぽいので回避-->
                            <shadedPattern>org.aapache</shadedPattern>
                        </relocation>
                    </relocations>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spongepowered-repo</id>
            <url>http://repo.spongepowered.org/maven/</url>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.spongepowered</groupId>
            <artifactId>spongeapi</artifactId>
            <version>4.1.0-SNAPSHOT</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.3.2.RELEASE</version>
            <exclusions>
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>commons-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.7.5</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.0.13</version>
        </dependency>

        <!--AOP-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>4.3.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>RELEASE</version>
        </dependency>

    </dependencies>

    <reporting>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-javadoc-plugin</artifactId>
                <version>2.10.3</version>
            </plugin>
        </plugins>
    </reporting>
</project>

これがpom.xmlです。
注意としてはそのままflat-jarにしてしまうとClassNotFoundExceptionが発生してしまいます。
原因はMinecraftのLaunchClassLoaderにあります。このClassLoaderでModが読み込まれるのですが、org.apache.logging以下パッケージが無視されています。正確にはLaunchClassLoader.class.getClassLoader()でロードされるのですが、LaunchClassLoaderとは親子関係がなくて見つからないみたいな流れ?

そのため解決策としてmaven-shade-pluginを使ってorg.apacheパッケージをリネームすることで回避しました。

イベントをSpringで
Spongeで発生するイベントをSpringのイベントに置き換えてみます。

@Component
public class EventConverter {

    @Autowired
    private ApplicationEventPublisher publisher;

    @Listener
    public void onSpongeEvent(Event event) {
        //SpongeのイベントをSpringに流す
        publisher.publishEvent(new SpongeEvent<>(SpringDemo.getInstance(), event));
    }
}

このComponentはSpongeのイベントをすべて受け取り、Springのイベントへ変換してイベントを通知します。

public class SpongeEvent<T extends Event> extends ApplicationEvent {

    private final T event;

    public SpongeEvent(Object source, T event) {
        super(source); //とりあえず適当に渡しておく
        Objects.requireNonNull(event);
        this.event = event;
    }

    public T getEvent() {
        return event;
    }
}

Spongeのイベントを持つだけのラッパークラスを作りました。ここは特に解説は必要ないと思います。

次に受け取り側です。

//プレイヤーがサーバーに入った時のイベントを受け取ってなにかする
@Component
public class PlayerJoinEventListener {

    @Autowired
    public PlayerMessageService playerMessageService;

    @EventListener
    public void handleOnPlayerJoin(SpongeEvent<ClientConnectionEvent.Join> event) {
        playerMessageService.sendHelloMessage(event.getEvent().getTargetEntity());
    }
}

Playerが入ってきた時のイベントを取ってみました。
受け取りたいイベントを引数にとって、EventListenerアノテーションをつけるだけです。

Spongeのイベント以外にも独自のイベントを作りたい場合はApplicationEventを継承したクラスを作り、ApplicationEventPublisherへ流すだけです。
詳しく知りたい方はこちらを見るのが良いかと思います。
https://spring.io/blog/2015/02/11/better-application-events-in-spring-framework-4-2

最後に設定です。

@Configuration
public class EventConfig {

    @Autowired
    private EventConverter eventConverter;

    //EventConverterをEventManagerに登録する
    @Bean
    public EventManager eventManager() {
        EventManager manager = Sponge.getEventManager();
        manager.registerListeners(SpringDemo.getInstance(), eventConverter);
        return manager;
    }

}

SpongeのEventManagerにEventConverterを登録しているだけです。

SpringAOPでロギング
AOPの定番ネタですがロギングです。

まずは設定から

@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
}

はい。EnableAspectJAutoProxyしてるだけです。

次にロギングのインターセプタを作ります。

@Component
@Aspect
public class LogInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(LogInterceptor.class);
    @AfterReturning("execution(public * net.orekyuu.spring.service..*.*(..))")
    public void printLog(JoinPoint joinPoint) {
        logger.info("ServiceLog: " + joinPoint.getSignature());
    }
}

net.orekyuu.spring.serviceパッケージ以下にある全てのpublicなメソッドのどれかが正常終了した時にログを出力しています。
ポイントカット式とかの書き方は以下を見ればよいかと。
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html

やりたい設計とか
Controller→Service→Repositoryの構造になんか近いような・・・?
EventListener→Service→Repositoryって結構よさ気では!?

テストとかDBに関してはまた後日

Minecraft1.8サーバープラグイン開発 データベース編

イベント編の続きです。
今回はデータベースを使ってプレイヤーのログインのログをとってみます。
データベースはMySQLを使用しています。

データベースといえばJPAを使用したかったんですが、試行錯誤してもダメでした。
理由としてはMETA-INF/services/の中にあるファイルを読み込めないのが原因のようでした。
というわけでBukkitで用意されているEbeanを使用していきます。

まずMySQLを使用するために依存関係にcompile ‘mysql:mysql-connector-java:5.1.34’を追加します。
次にplugin.ymlにdatabase: trueを追加。これを行うことでデータベースを使用できるようになります。

Ebeanで使用するためのエンティティを作成します。
JPAと同じでjavax.persistence.Entityアノテーションをクラスにつける感じで。
また、デフォルトコンストラクタとゲッタ/セッタが必要です。

@Entity
@Table(name = "PLAYER_JOIN_LOG")
public class PlayerJoinLog {
    @Id
    @GeneratedValue
    private Long id;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(nullable = false)
    private Date joinDate;

    @Column(nullable = false)
    private String playerName;

    @Column(nullable = false)
    private String playerUUID;

    @Column(nullable = false)
    private String joinIP;

    //コンストラクタとゲッタセッタは省略
}

PluginクラスでgetDatabaseClassesメソッドをオーバーライドしてデータベースで使用するエンティティのClassクラスのリストを返します。

@Override
public List<Class<?>> getDatabaseClasses() {
    return Arrays.asList(PlayerJoinLog.class);
}

onEnableされた時にテーブルの存在を確認し、テーブルを作成します。

private static EbeanServer database;
@Override
public void onEnable() {
    setupDatabase();
}

private void setupDatabase() {
    try {
        getDatabase().beginTransaction();
        getDatabase().find(PlayerJoinLog.class).findRowCount();
    } catch (PersistenceException ex) {
        installDDL();
    } finally {
        getDatabase().commitTransaction();
    }
}

public static EbeanServer getPluginDataBase() {
    return getPlugin().getDatabase();
}
public static Plugin getPlugin() {
    return getPlugin(ServerPlugin.class);
}

ListenerでPlayerがログインした時DBに情報を保存しましょう。

@EventHandler
public void onLogin(PlayerJoinEvent event) {
    Player player = event.getPlayer();
    UUID uuid = player.getUniqueId();
    PlayerJoinLog log = new PlayerJoinLog(player.getPlayerListName(), uuid.toString(),
            player.getAddress().getHostName());

    EbeanServer dataBase = MyPlugin.getPluginDatabase();
    try {
        dataBase.beginTransaction();
        dataBase.save(log);
    } finally {
        dataBase.commitTransaction();
    }
}

後はビルドして動作を確認して終了です。
このプラグインを使うためにはBukkit側で設定が必要になります。

実際動かすjarと同じディレクトリに生成されているbukkit.ymlに以下を記述します。

database:
  username: DBのユーザー名
  isolation: SERIALIZABLE
  driver: com.mysql.jdbc.Driver
  password: DBのパスワード
  url: DBのURL

これで設定は以上です。


おまけ

IntelliJでDBの設定(Ultimateの機能だけど)

追加からMySQLを選択
DatabaseTool1

ホストとポート、データベース名とユーザー名パスワードを入力
TestConnectionで接続確認をとれたらOK
DatabaseTool2

データはこのような形で見れるようになります。
データをダブルクリックで編集できます。
DatabaseTool3

SQL文はデータベースかテーブルを右クリックしてConsoleを選ぶと入力用のタブが開くのでそこで入力して実行。
補完めっちゃきいて良い感じです。
実行結果はこんなかんじで表示。
いいですね。IntelliJ
DatabaseTool4

Minecraft1.8サーバープラグイン開発 イベント編

HelloWorld編の続きです。
今回はイベントを使用してログインしてきたプレイヤーにかぼちゃをかぶせるプラグインを作成してみましょう。

Bukkitではサーバーで発生したいろいろなイベントを受け取る仕組みが用意されています。
これを使用することで一定範囲のブロックを破壊できないようにしたり、ログインしてきたプレイヤーにアイテムをプレゼントしたりなどの処理を行えます。

イベントを受け取るリスナはorg.bukkit.event.Listenerインターフェイスを実装します。
Listenerインターフェイスはマーカーインターフェイスになっていて実装しなければいけないメソッドはありません。
イベントを受け取るメソッドはorg.bukkit.event.Eventを継承しているクラス1つを引数に取り、@EventHandlerアノテーションがなければいけません。
引数のクラスによってメソッドは適切なタイミングで呼び出されるようになります。

プレイヤーがログインした時にかぼちゃをかぶせる処理を書いてみます。

public class MyListener implements Listener {

    @EventHandler
    public void onPlayerJoin(PlayerJoinEvent event) {
        Player player = event.getPlayer();
        player.getInventory().setHelmet(new ItemStack(Material.PUMPKIN));
    }
}

非常に簡単ですね。
最後にこのMyListenerをBukkitに登録する必要があります。
registerEventsには第一引数にListener、第二引数にPluginを与えます。

public class HelloWorld extends JavaPlugin {
    @Override
    public void onEnable() {
        getServer().getPluginManager().registerEvents(new MyListener(), this);
    }
}

イベントは独自で定義することも可能です。
複雑なプラグインを作ったりする場合は使うことになると思います。
独自のイベントを作成する場合はEventを継承し、staticなHandlerListを宣言します。
親に定義されているgetHandlersメソッドをオーバーライドしてHandlerListを返すようにします。
追加でHadlerListを返すpublic static HandlerList getHandlerList()を作成します。
これを用意しないとIllegalPluginAccessExceptionが発生します。

イベントを通知する場合はBukkit.getServer().getPluginManager().callEvent(customEvent);のようにcallEventメソッドを使用します。
リスナは@EventHandlerアノテーションのつけられたメソッドで引数にCustomEventを定義するだけです。

これでイベント編終了です。

Minecraft1.8サーバープラグイン開発 HelloWorld編

前回の続きです。
とりあえずHelloWorldでもやってみる。

まずはsrc/main/resources/にplugin.ymlを作成
これはサーバーがプラグインを読み込んだ時に必要な情報をこのファイルからロードします。
必須の項目はname, version, mainの3つです。
nameにはプラグインの名前を文字列で記述します。
versionにはプラグインのバージョンを文字列で記述します。
mainにはJavaPluginクラスを継承したエントリポイントとなるクラスを完全修飾名で記述します。

その他の設定についてはWikiを参考にすると良いと思います。

エントリポイントとなるクラスはJavaPluginクラスを継承します。
プラグインが有効化された時onEnableメソッドが呼び出されるので、プラグインの初期化はonEnableで行います。
コマンドなどでプラグインが無効化された時はonDisableメソッドが呼び出されるので、イベントリスナやタイマー処理はここで終了するようにしておきましょう。

今回は有効化された時にHelloWorldするだけにしておきましょう。

public class HelloWorld extends JavaPlugin {
    @Override
    public void onEnable() {
        System.out.println("HelloWorld!");
    }
}

Minecraft1.8サーバープラグイン開発 開発環境構築編

Bukkitの開発が止まってしまって1.8のプラグイン開発できないなーと思ってたんですけど、Spigotが1.8対応しているみたいだったのでそれで開発してみようと思います。

まずは本体やらを準備しないといけないんですが、自分でビルドを行う必要があります。
公式サイトのDownloadsからBuildTools.jarをダウンロードします。
適当なディレクトリに投げてjava -jar BuildTools.jarのようにjarを実行すればビルドしてくれます。

必要なものが用意できたら、build.gradleを用意します。

ビルドパスにbukkitとspigot-apiを入れたいのでcraftbukkit-1.8.jarとspigot-api-1.8-略.jarをlibsフォルダに入れます。
基本的にspigot-apiのクラスを使用するのですが、Databaseの機能を使いたいのでbukkitも一緒に入れています。必要なければ入れる必要はないと思います。
jarタスクは誰か添削頼みます。groovy力が足りない・・・!じゃけんあとで勉強しましょうね~

開発環境は整いました。おしまい。
気分が乗ったら続き書くと思う。