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

痛IntelliJを作ろう

IntelliJを痛くしろという神の啓示を受けたので、痛くしてみました。

intellij

今回は上のようなエディタを作ってみます。

今まではSexy Editorというプラグインを使う方法がありましたが、2016.2からは”Set Background Image”というアクションが追加され、そこから設定画面を開いて背景画像を設定できるようになりました。

このアクションは上のメニューバーには見つからなかったので、Find Action(WinならCtrl+Shift+A)からSet Background Imageというアクションを検索して呼び出します。

background_image

明るい写真だと文字が見づらいので透明度を下げて調整します。
これで設定は完了です。

それでは良い痛IntelliJライフを!

JJUG CCC 2016 springに行ってきた

Type safe Annotation #ccc_gh1

桜庭さんのタイプアノテーションの話からスタート
JSR-305はずっと通っているものだと思っていたんだけど、ステータスが休止中になっているのをその時知って1年ぶりの衝撃を受けました。
そこで気づいて急いで発表予定のスライドを修正するなんてことになったり。
あとChecker Frameworkなんてものがあるの知らなかったので使ってみたいと思います。

Thymeleaf3を使ってみよう! #ccc_f2

Spring祭りその1。
去年からSpringBootでThymeleafを使っていて、次作るWebアプリでも使う予定なので聞きに行きました。
今までWebデザイナの人にhtml書いてもらうとThymeleafで「brタグが閉じられてない」みたいなエラーが出ていたので、それが次のバージョンからなくなるって話がすごい嬉しかったです。
逆に言えばそれ以外はめぼしい機能はないのかな?という印象。

SpringBootでBootした後に作るWebアプリケーション基盤 #ccc_e3

Spring祭りその2。
SpringBootを入門読んでHelloWorldした後に読むべき資料!という感じでうまくまとまっていて良いセッションでした。
学校の人に見せたい資料です!

テストゼロからイチへ進むための戦略と戦術 #ccc_e4

テストゼロの状態からテストを書くまでの話。
テストほぼ書いていない人間だったので結構耳が痛くなる話でした。テスト書かねば・・・!
コードのゴミ掃除で1コミット1.7万行削除には笑いましたw

Spring Framework/Bootの最新情報とPivotalがすすめるクラウドネイティブなアプローチ #ccc_gh5

Spring祭りその3。
バナーがついに画像に対応!
絶対にアピールする点が間違っている気がwww
SpringBoot1.4から@GetMapping@PostMappingのようなアノテーションが増えてちょっとだけ楽になりました。いいぞ〜!

OpenJDK コミュニティに参加してみよう #ccc_i62

OpenJDKのコミュニティに参加する方法の解説。
プロジェクトが大量にあったり、どうやり取りすればいいんや!みたいな一歩目の敷居が高そうに感じるところをうまく解説されてました。
このセッション終わったあとnokoに「明日にはコミッタになってるんだよね?ん?ん?」みたいな不当な煽りを受けました。

古のJavaを使うということ #ccc_m71

J2SE 1.4の環境で働いている方のセッション。
テストコードを書こうと思っても上司が納得しなかったり、不要なコメントを削除するのにも苦労するという話を聞いて辛さが溢れてました。
環境が改善されると良いですね・・・。

SpringBoot+Kotlin 劇的ビフォーアフター #ccc_i72

僕のセッションです。
緊張で結構カミカミになりましたけど。大体良い時間で話しきれて良かったです。
cccの当日にイケてないところの解決策が出てきたりして資料修正したりしてましたが、解決策を知れるJJUG CCC最高!というかんじでした。

IntelliJ IDEAでSteamブロードキャスト

突然ですが最近ItelliJ IDEAというゲームにはまってます。

最近SteamでIntelliJ IDEAのプレイ風景を配信できることに気付いたので方法を紹介します。

続きを読む

4/2(土) Kotlin 1.0リリース記念勉強会 in 京都に行ってきた

Kotlinの勉強会にSpringBoot+Kotlinネタでしゃべりにいきました。
発表したスライドはこちらになります。

会場でAndroid以外でKotlinを使ってる人がどれくらい居るか質問したんですが、予想よりも多くて驚きました。
Androidアプリで「Java7までしか使えないからKotlin使うかー。使うしか無いかー。」みたいな使われ方するのがメインだと思ってたんですけど、普通にJavaの置き換えとして使うみたいな人が多いんですかね?

続きを読む

Workbenchの使い方を紹介

学校の講義でSpringBoot使ってCIツール的なWebアプリを作ってみました。
1年ほどかけて作ってみたけど、ある程度動く形になったので使い方を紹介。


何が出来るの

機能としてはタスク管理と、サーバーでもビルドやテストを行えます。

Ticketboard
タスク管理機能

PullRequest
プルリクエスト

イメージ的にはJenkinsとBitbucket当たりをくっつけてみました的な感じを想像すると近いかもしれないです。


インストール方法

公式サイトからWorkbenchのjarファイルをダウンロードしてWorkbench.jarにリネームします。
適当なディレクトリに配置して以下のコマンドを実行。
java -jar Workbench.jar

起動を確認したらhttp://localhost:8080/へアクセスすれば動いてます。簡単ですね。

login


セットアップ

このままの状態だとユーザーを作れないのでadminユーザーでログインします。
デフォルトではIDがadmin、パスワードがpasswordに設定されています。

このままの設定だとセキュリティ的にまずいのでadminのパスワードを変更します。

admin_setting
赤丸で囲んだSettingを開いてChange Passwordタブから適当なパスワードに変更します。

次にサインアップを有効化して一般ユーザーを作成できるようにします。
global_setting
赤丸で囲んだGlobalSettingから設定を開いてサインアップを許可にチェックを入れます。

signup
ログアウトすると下に「CREATE AN ACCOUNT」と出てくるので、クリックしてアカウントを作成し、ログインします。

ここまでが初回セットアップです。他のユーザーにアカウントを作られないようにする場合は、改めてadminでログインしてサインアップを許可のチェックを外します。


プロジェクトを作成

今回はGithubを使っている前提でプロジェクトを作ります。
まずはpushを出来るようにGithubのアカウントをアカウントにひも付けます。認証に使えるのは今のところベーシック認証のみです。

git
Settingsを開いてGit AccountタブでGithubの認証用アカウントとパスワードを入力して終了です。

projects
次に左からProjectページを選択して、右上のCreateをクリック

必要な項目を入力してプロジェクトを作成。
projectcreate

プロジェクト名: プロジェクトの名前
Gitリポジトリ: gitのリポジトリのurl
作業ブランチ: 作業ブランチのマージ先。大体はmasterだと思う。
ビルドの有効化: ビルド機能を有効化します。
ビルドコマンド: ビルドを実行する時のコマンド。リポジトリをクローンしたrootで実行されます。
成果物のパス: ビルド後に保存する成果物のパスを正規表現で指定します。複数ファイルマッチした場合はzipにまとめられます。
テストの有効化: JUnitを使ったユニットテスト機能を有効化します。
テストコマンド: テストを実行するときのコマンド。リポジトリをクローンしたrootで実行されます。
テスト結果のパス: JUnitのテスト結果のパスを指定します。


課題の作成

左からTicketboardを選択。
NewカラムからAddボタンをクリック。

ticketboard

タイトルなどの必要な情報を入力してOKで課題を作成できます。
ステータスはD&Dで別のカラムへ移動させることで更新できる。詳細を確認する場合などは課題をクリックすることでダイアログを開くことが出来ます。


プルリクエストを作成

左からPullRequestを選択。
右上のNewボタンをクリック。

new_pr

タイトル説明・ブランチの他に関連するチケット(課題)の選択が必要。
プルリクエストを作ると、自動的に関連チケットのステータスがREVIEWに更新されます。

pr
レビューがおわったら右上からマージをクリック。
マージするとチケットのステータスがRESOLVEDに更新されます。
さらに、ビルドやテストの機能が有効化されていた場合はこの状態でビルドとテストが実行されます。


成果物のダウンロードとテスト結果

成果物はFilesページから、テストの結果はTestsページから確認できます。


おわりに

SpringBootで頑張ってみたけど、作業量がすごい多くて大変だった。
Webデザイナー1人とプログラマ3人で1年頑張ったけど(プログラマうち1人は11月頃に途中参加)もうすこし時間欲しかった感じはある。
最終的に750コミット越えてたし結構頑張ったんじゃないかな。今年はもう少し楽な作品作りたい思う。

ちなみにダウンロードは以下からできます。
http://workbench.orekyuu.net/

Java8 Postfixプラグインが公式に取り込まれた

以前作っていたJava8のpostfixプラグインがIntelliJ IDEA CEに取り込まれた。

Add a new postfix completion template #303

メール確認した時「マージまじ?」ってなった。
たぶん次のアップデートで使えるようになるのかな?
IDEA16 EAPには入ってました。


おまけ

英語苦手なんや 本当にごめん
https://github.com/JetBrains/intellij-community/commit/8a20eaf86ef12fa1411557b41586350d40221427

AquaFXがいい感じ

この投稿はJavaFXアドベントカレンダーの22日目の記事です。

JavaFXのライブラリで最近いいなと思ったのがAquaFXというライブラリ。
コンポーネント追加とかではなくて見た目をMac風にしてくれるライブラリです。

使い方

build.gradleに依存関係追加

compile 'com.aquafx-project:aquafx:0.1'

AquaFx#style()をApplication#start(Stage)あたりで呼び出せばとりあえず動きます。

コンポーネントの見た目を変える

AquaFxではButtonやLabelなどのコンポーネントにも複数のデザインが用意されています。
形などを変えたい場合はAquaFx#create○○Styler()からメソッドチェーンで値を設定して、styleメソッドに反映させたいコンポーネントを与えれば変更できます。

サンプルとして以下のコードを実行してみました。

@Override
public void start(Stage primaryStage) {
    AquaFx.style();

    VBox box = new VBox();
    //Buttons
    Arrays.stream(ButtonType.values()).forEach(type -> {
        Button button = new Button(type.getStyleName());
        AquaFx.createButtonStyler().setType(type).style(button);
        box.getChildren().add(button);
    });

    box.getChildren().add(new Separator());
    //TextFields
    Arrays.stream(TextFieldType.values()).forEach(type -> {
        TextField field = new TextField();
        AquaFx.createTextFieldStyler().setType(type).style(field);
        box.getChildren().add(field);
    });

    Scene scene = new Scene(box);
    primaryStage.setScene(scene);
    primaryStage.show();
}

実行結果
スクリーンショット 2015-12-21 0.28.33

AquaFx以外にもWindows風のAeroFx、今風なフラットデザインのflatterがあるみたいです。
結構手軽に使えてかっこいい見た目にできるのでおすすめです。
こういった見た目系ライブラリが今後も増えるといいなー。

明日は@s_kozakeさんです。

IntelliJ IDEAのPostfix補完プラグインを作る

JetBrains IDE Advent Calendar2日目の記事です。前日はlaco0416氏のWebStormのTypeScript統合機能でした。

ところでPostfix補完使ってますか?当然使ってますよね?
Postfix補完をキメると気持ちいいですよね?Postfix補完でガシガシコードがかけると何でもできちゃいそうな気分になりますよね。

けどPostfix補完標準のテンプレートだけだと足りないですよね・・・。たとえばOptional.ofNullable(obj)とかobj.optから出したい・・・。というので作ったのがJava8Postfixってプラグイン。


IntelliJのSettings→PluginsでPostfixとかで検索してインストールできるので、Java8使ってる方は良ければ入れてください。


宣伝はここまでにして、本題に入ってPostfixプラグインの作り方。
まず開発環境としてIntelliJのプラグイン開発用プラグインを入れておきます。その後NewProjectすればこんな画面が出てくるはず。
plugindev1

あとはプロジェクト名とか決めて終了。
プラグインのプロジェクト構成はこんな感じ。
dev2

よく見かけるやつなので特にコメントもないかな。
plugin.xmlがプラグインの情報を記述する所。識別用のIDとか説明文とか。
この辺は調べるといくつか資料出てくるので飛ばします。

ここからPostfix補完のプラグインの話。
新しいPostfixのテンプレートを登録するためにJavaPostfixTemplateProviderを継承した新しいクラスを作成します。
JavaPostfixTemplateProvider#getTemplatesの戻り値には新しくプラグインで追加するPostfixTemplateのSetを返すようにしておきます。
Java8Postfixではこんな感じ。

public class Java8Postfix
        extends JavaPostfixTemplateProvider {
    private final HashSet<PostfixTemplate> templates;

    public Java8Postfix() {
        this.templates = ContainerUtil.newHashSet(new PostfixTemplate[] { new NullableOptionalPostfixTemplate(), new OptionalPostfixTemplate(), new ArrayToStreamPostfixTemplate(), new MethodToLambdaPostfixTemplate() });
    }

    @NotNull
    public Set<PostfixTemplate> getTemplates()
    {
        return templates;
    }
}

このままでは当然何も動作しないので、plugin.xmlにProviderを読み込んでもらうように指定します。

<extensions defaultExtensionNs="com.intellij">
  <codeInsight.template.postfixTemplateProvider language="JAVA" implementationClass="net.orekyuu.postfix.Java8Postfix"/>
</extensions>

次はPostfixTemplateを実装する。
Java8PostfixではPostfixTemplateWithExpressionSelectorを継承しています。
今回の例では.optのコードを例に出します。

public class OptionalPostfixTemplate
        extends PostfixTemplateBase
{
    public OptionalPostfixTemplate()
    {
        //(1)
        super("opt", "Optional.of(Object)", ConditionMerger.or(JavaPostfixTemplatesUtils.IS_NOT_PRIMITIVE, new Condition[] { MyConditions.IS_DOUBLE, MyConditions.IS_INT, MyConditions.IS_LONG }));
    }

    //(2)
    protected void expandForChooseExpression(@NotNull PsiElement context, @NotNull Editor editor)
    {
        //(3)
        PsiExpression expression = JavaPostfixTemplatesUtils.getTopmostExpression(context);
        if (expression == null) {
            return;
        }
        PsiType type = expression.getType();
        String optionalClass = "Optional";
        if (PsiType.DOUBLE.equals(type)) {
            optionalClass = "OptionalDouble";
        } else if (PsiType.LONG.equals(type)) {
            optionalClass = "OptionalLong";
        } else if (PsiType.INT.equals(type)) {
            optionalClass = "OptionalInt";
        }
        Project project = context.getProject();
        Document document = editor.getDocument();

        TextRange range = expression.getTextRange();
        document.deleteString(range.getStartOffset(), range.getEndOffset());

        TemplateManager manager = TemplateManager.getInstance(project);
        //(4)
        Template template = manager.createTemplate("", "");
        template.setToReformat(true);
        template.addTextSegment(optionalClass + ".of(");
        template.addTextSegment(expression.getText());
        template.addTextSegment(")");

        manager.startTemplate(editor, template);
    }
}

abstract class PostfixTemplateBase
        extends PostfixTemplateWithExpressionSelector
{
    public PostfixTemplateBase(String postfix, String desc, PostfixTemplateExpressionSelector selector)
    {
        super(postfix, desc, selector);
    }

    public PostfixTemplateBase(String postfix, String desc, Condition<PsiElement> condition)
    {
        this(postfix, desc, JavaPostfixTemplatesUtils.selectorAllExpressionsWithCurrentOffset(condition));
    }
}

public final class ConditionMerger
{
    private ConditionMerger()
    {
        throw new UnsupportedOperationException();
    }

    public static Condition<PsiElement> or(final Condition<PsiElement> first, final Condition<PsiElement>... option)
    {
        if (first == null) {
            throw new NullPointerException("first condition is null");
        }
        return new Condition<PsiElement>() {
            public boolean value(PsiElement element) {
                if (first.value(element)) {
                    return true;
                }
                for (Condition<PsiElement> condition : option) {
                    if (condition.value(element)) {
                        return true;
                    }
                }
                return false;
            }
        };
    }
}

public enum MyConditions
        implements Condition<PsiElement>
{
    IS_ARRAY {
        @Override
        public boolean value(PsiElement element) {
            if(!(element instanceof PsiExpression)) {
                return false;
            } else {
                PsiType type = ((PsiExpression)element).getType();
                return JavaPostfixTemplatesUtils.isArray(type);
            }
        }
    },
    IS_LAMBDA {
        @Override
        public boolean value(PsiElement element) {
            return element instanceof PsiLambdaExpression;
        }
    },
    IS_METHOD_CALL {
        @Override
        public boolean value(PsiElement element) {
            return element instanceof PsiMethodCallExpression;
        }
    },
    IS_DOUBLE {
        @Override
        public boolean value(PsiElement element) {
            if(!(element instanceof PsiExpression)) {
                return false;
            } else {
                PsiType type = ((PsiExpression)element).getType();
                return PsiType.DOUBLE.equals(type);
            }
        }
    },
    IS_LONG {
        @Override
        public boolean value(PsiElement element) {
            if(!(element instanceof PsiExpression)) {
                return false;
            } else {
                PsiType type = ((PsiExpression)element).getType();
                return PsiType.LONG.equals(type);
            }
        }
    },
    IS_INT {
        @Override
        public boolean value(PsiElement element) {
            if(!(element instanceof PsiExpression)) {
                return false;
            } else {
                PsiType type = ((PsiExpression)element).getType();
                return PsiType.INT.equals(type);
            }
        }
    };
}

(1)
第一引数に補完を書ける時に使用する文字列。optを渡しているので.optで補完できるようになる。
第二引数は説明文。
第三引数は補完をかけるための条件。今回の場合はプリミティブでないオブジェクト || double || int || longのみ補完できる。

(2)
補完をかける時のイベント。ここでテンプレートを展開する。

(3)
PsiExpressionを取得してくる。これで対象の式の戻り値とか解析しながら補完をかけれる。

(4)
Templateを使って変換後の文字列を作る。文字列の操作が楽だったりsetToReformat(true)としておけばフォーマット整えてくれたりするので基本はこれを使うんじゃないかなー?
最後にTemplateManager#startTemplateを呼び出さないといけないので注意。

Postfix補完プラグインではPsiHogehogeの扱いが難しいので色々調べないといけない感じある。
調べるとIntelliJ IDEA PSI Cookbookってサイトがあったので参考になると思う。

最後にEditor>General>Postfix Completionに説明文を入れる必要がある。書かないとエラーになった記憶。
dev3

この画面の説明文は”postfixTemplates/テンプレートのクラス名”パッケージに配置する。
dev4
before.java.templateにはBeforeに表示されるコードをそのまま記述。
after.java.templateにはAfterに表示されるコードをそのまま記述。
description.htmlにはDescriptionに表示する説明文をhtmlで記述する。
これでおしまい。動作確認後ビルドしてPluginRepositoryで配布しようね。

明日はhiromikai_green氏がなんか書いてくれると思います。期待。