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に関してはまた後日