CLOVER🍀

That was when it all began.

Formatter Maven Pluginで、ソースコードのフォーマットを行う

これは、なにをしたくて書いたもの?

Javaでプログラムを書いていて、ソースコードを一括でフォーマットしたり、フォーマットするのを忘れないようにしたいとかいう
話があると思います。

こういうのを調べてみると、たいていEclipseのフォーマッターをCLIで動かそうとかIntelliJのフォーマッターをCLIで動かそうみたいなものが
見つかります。

How to Run the Eclipse Formatter From the Command Line

Format files from the command line | IntelliJ IDEA Documentation

この目的のためにIDEを持ち出すのは微妙だなと思ってもうちょっと調べていたら、Mavenプラグインがあったのでちょっと試してみました。

以下の4つを見つけました(Spotlessは教えてもらいました)。

今回は、Eclipseのフォーマットを使うFormatter Maven Pluginを試してみようと思います。
※ Prettier Javaは、Node.jsが必要になります

IDEで保存時に自動フォーマットするようにしておけばいいのでは、という話はここでは置いておきます。

Eclipse Tip - Format Source Code on Save | Cody Burleson

Reformat code | IntelliJ IDEA Documentation

Formatter Maven Plugin

Formatter Maven PluginのWebサイトはこちら。

formatter-maven-plugin – Introduction

GitHubリポジトリはこちら。

GitHub - revelc/formatter-maven-plugin: Formatter Maven Plugin

Formatter Maven Pluginがどういうものかというと、Eclipseのコードフォーマッターを使用してJavaソースコードをフォーマットする
Mavenプラグインです。

現時点で、Eclipseとの互換性は以下のようになっています。

formatter-maven-plugin – Eclipse Version Compatibility

ゴールはformatter:formatとformatter:validateの2つがあり、以下の違いがあります。

formatter:validateの方は、CIに組み込んだりすることを想定しているようです。

機能・設定的には、以下のようなことができます。

ゴールは単独で動かすこともできますが、executions / execution / goals / goalに指定することでcompileゴールの実行時に
formatter:formatやformatter:validateを動かすこともできます。

formatter-maven-plugin – Plugin Usage

サンプルはこちら。

formatter-maven-plugin – Examples

では、適当にソースコードを用意して簡単に試してみます。

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.5 2022-10-18
OpenJDK Runtime Environment (build 17.0.5+8-Ubuntu-2ubuntu122.04)
OpenJDK 64-Bit Server VM (build 17.0.5+8-Ubuntu-2ubuntu122.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.7 (b89d5959fcde851dcb1c8946a785a163f14e1e29)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.5, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-57-generic", arch: "amd64", family: "unix"

サンプルアプリケーションを作成する

では、簡単なアプリケーションを作成します。JAX-RS+CDIなJavaアプリケーションを、Java SE環境で動かすことにしましょう。

Maven依存関係などはこちら。

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-undertow-cdi</artifactId>
            <version>6.2.2.Final</version>
        </dependency>
    </dependencies>

空のbeans.xml。

src/main/resources/META-INF/beans.xml




ソースコードはこちら。フォーマット後です。

src/main/java/org/littlewings/formatter/App.java

package org.littlewings.formatter;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import jakarta.ws.rs.SeBootstrap;
import org.jboss.logging.Logger;
import org.littlewings.formatter.rest.JaxrsActivator;

public class App {
    public static void main(String... args) throws ExecutionException, InterruptedException {
        Logger logger = Logger.getLogger(App.class);

        SeBootstrap.Configuration configuration = SeBootstrap.Configuration.builder().host("0.0.0.0").port(8080)
                .build();

        SeBootstrap.Instance instance = SeBootstrap.start(new JaxrsActivator(), configuration).toCompletableFuture()
                .get();

        logger.info("server startup.");

        while (true) {
            TimeUnit.SECONDS.sleep(5L);
        }

        /*
         * instance .stop() .toCompletableFuture() .get();
         */
    }
}

src/main/java/org/littlewings/formatter/rest/HelloResource.java

package org.littlewings.formatter.rest;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import org.littlewings.formatter.service.MessageService;

@Path("hello")
@ApplicationScoped
public class HelloResource {
    @Inject
    MessageService messageService;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String message(@QueryParam("word") String word) {
        return messageService.create(word);
    }
}

src/main/java/org/littlewings/formatter/rest/JaxrsActivator.java

package org.littlewings.formatter.rest;

import java.util.Set;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("")
public class JaxrsActivator extends Application {
    @Override
    public Set<Class<?>> getClasses() {
        return Set.of(HelloResource.class);
    }
}

src/main/java/org/littlewings/formatter/service/MessageService.java

package org.littlewings.formatter.service;

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class MessageService {
    public String create(String word) {
        if (word != null) {
            return String.format("Hello %s!!", word);
        } else {
            return "Hello World!!";
        }
    }
}

動作確認。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.formatter.App

OKですね。

$ curl localhost:8080/hello?word=RESTEasy
Hello RESTEasy!!

では、ここで適当にソースコードのフォーマットを崩しておきます。

src/main/java/org/littlewings/formatter/App.java

package org.littlewings.formatter;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import jakarta.ws.rs.SeBootstrap;
import org.jboss.logging.Logger;
import org.littlewings.formatter.rest.JaxrsActivator;

public class App {
public static void main(String... args) throws ExecutionException, InterruptedException {
Logger logger = Logger.getLogger(App.class);

SeBootstrap.Configuration configuration = SeBootstrap.Configuration.builder().host("0.0.0.0").port(8080)
.build();

SeBootstrap.Instance instance = SeBootstrap.start(new JaxrsActivator(), configuration).toCompletableFuture()
.get();

logger.info("server startup.");

while (true){TimeUnit.SECONDS.sleep(5L);}

        /*
         * instance .stop() .toCompletableFuture() .get();
         */
    }
}

src/main/java/org/littlewings/formatter/rest/HelloResource.java

package org.littlewings.formatter.rest;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import org.littlewings.formatter.service.MessageService;

    @Path("hello")
    @ApplicationScoped
    public class HelloResource {
@Inject
MessageService messageService;

@GET
@Produces(MediaType.TEXT_PLAIN)
public String message(
        @QueryParam("word")
        String word
) {
return messageService.create(word);
    }
}

src/main/java/org/littlewings/formatter/rest/JaxrsActivator.java

package org.littlewings.formatter.rest;

import java.util.Set;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

    @ApplicationPath("")
public class JaxrsActivator extends Application {
@Override
public Set<Class<?>> getClasses() {
return Set.of(HelloResource.class);
}
}

src/main/java/org/littlewings/formatter/service/MessageService.java

package org.littlewings.formatter.service;

import jakarta.enterprise.context.ApplicationScoped;

    @ApplicationScoped
public class MessageService {
public String create(String word) {
if (word != null) {return String.format("Hello %s!!", word); }
else {
return "Hello World!!";
 }
}
}

Formatter Maven Pluginを組み込む

それでは、Formatter Maven Pluginを組み込んでみます。

まずはデフォルトの状態で。

    <build>
        <plugins>
            <plugin>
                <groupId>net.revelc.code.formatter</groupId>
                <artifactId>formatter-maven-plugin</artifactId>
                <version>2.21.0</version>
            </plugin>
        </plugins>
    </build>

formatter:validateで、フォーマット済みかどうかを確認してみます。

$ mvn formatter:validate

エラーになりました。フォーマットを崩したので当然ですね。

[INFO] --- formatter-maven-plugin:2.21.0:validate (default-cli) @ formatter-maven-plugin-example ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.202 s
[INFO] Finished at: 2023-01-09T22:22:05+09:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal net.revelc.code.formatter:formatter-maven-plugin:2.21.0:validate (default-cli) on project formatter-maven-plugin-example: File '/paht/to/src/main/java/org/littlewings/formatter/App.java' has not been previously formatted. Please format file (for example by invoking `mvn net.revelc.code.formatter:formatter-maven-plugin:2.21.0:format`) and commit before running validation! -> [Help 1]

ひとつNGのものを見つけると、その時点で失敗するようですね。

formatter:formatでフォーマットしてみます。

$ mvn formatter:format

4ファイルフォーマットされました。結果は最初に載せたソースコードと同じになるので割愛します。

[INFO] --- formatter-maven-plugin:2.21.0:format (default-cli) @ formatter-maven-plugin-example ---
[INFO] Processed 4 files in 868ms (Formatted: 4, Unchanged: 0, Failed: 0, Readonly: 0)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.346 s
[INFO] Finished at: 2023-01-09T22:29:24+09:00
[INFO] ------------------------------------------------------------------------

これが1番簡単な使い方ですね。

デフォルトでは、src/main/javaとsrc/test/javaの2つのディレクトリが対象になります。それぞれソースディレクトリ、テストディレクトリ
としてFormatter Maven Pluginも認識します。

src/test/javaの方には、今回はなにも入れていませんが…。

設定してみる

Formatter Maven Pluginの設定を変えていってみます。

各種設定はそれぞれのゴールに関するドキュメントを見ればよいのですが、設定可能な項目はほぼ同じですね。

formatter-maven-plugin – formatter:format

formatter-maven-plugin – formatter:validate

それから、Examplesのページを見るとほとんど同じようなことをやっています。

formatter-maven-plugin – Examples

compile時に実行する

以下のようにすることで、compile時にformatter:formatを実行するようになります。

                <executions>
                    <execution>
                        <goals>
                            <goal>format</goal>
                        </goals>
                    </execution>
                </executions>

コンパイルに合わせて、勝手にフォーマットされる動きになりますね。

以下のようにすると、compile時にformatter:validateを実行するようになります。

                <executions>
                    <execution>
                        <goals>
                            <goal>validate</goal>
                        </goals>
                    </execution>
                </executions>

この場合、フォーマットされていないソースコードがあるとビルドに失敗するようになります。

確認は、compileで。

$ mvn compile

こちらに関するドキュメントは、こちら。

formatter-maven-plugin – Plugin Usage

コンパイラーの設定を行う

以下のように設定しておくと、Formatter Maven Pluginもこの値に従って動作します。

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

明示的に指定する場合は、以下のようになります。

                <configuration>
                    <compilerSource>17</compilerSource>
                    <compilerCompliance>17</compilerCompliance>
                    <compilerTargetPlatform>17</compilerTargetPlatform>
                </configuration>

compilerSourceとcompilerComplianceにはmaven.compiler.source相当の値を、compilerTargetPlatformにはmaven.compiler.target相当の
値を指定すべきですね。

ファイルのエンコーディングを指定する

encodingで、ファイルのエンコーディングを指定できます。

                <configuration>
                    <encoding>UTF-8</encoding>
                </configuration>

なんとなく、これでいいのではという気も…。

                <configuration>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
ファイルの改行コードを指定する

lineEndingで改行コードを指定できます。

                <configuration>
                    <lineEnding>LF</lineEnding>
                </configuration>
フォーマットする対象を指定する

includesで、フォーマット対象に含めるファイルを指定できます。

                <configuration>
                    <includes>
                        <include>**/*.java</include>
                    </includes>
                </configuration>

ドキュメント上は以下のように書かれているのですが、

When not specified, the default include is */.java

実際のデフォルト値は以下のようになっています。

    /** The Constant DEFAULT_INCLUDES. */
    private static final String[] DEFAULT_INCLUDES = { "**/*.css", "**/*.json", "**/*.html", "**/*.java", "**/*.js",
            "**/*.xml" };

https://github.com/revelc/formatter-maven-plugin/blob/formatter-maven-plugin-2.21.0/src/main/java/net/revelc/code/formatter/FormatterMojo.java#L86-L88

フォーマット対象のディレクトリがデフォルトでsrc/main/javaとsrc/test/javaなので、実質Javaソースコードのみが対象と捉えても
差し支えないですけどね。

特定のファイルを除外したい場合は、excludesを使います。

                <configuration>
                    <excludes>
                        <exclude>org/littlewings/formatter/service/**/*Service.java</exclude>
                        <exclude>org/littlewings/formatter/rest/**/*.java</exclude>
                    </excludes>
                </configuration>
外部の設定ファイルを指定する

特に指定しない場合、Formatter Maven Pluginは各ファイルを以下の設定に従ってフォーマットします。

これを、それぞれ以下の設定で独自のファイルを指定することができます。

  • Java … configFile
  • JavaScript … configJsFile
  • JSON … configJsonFile
  • XML … configXmlFile
  • HTML … configHtmlFile
  • CSS … configCssFile

たとえば、Google Style GuideでJavaのEclipseのフォーマットが公開されているので、こちらを例にしてみましょう。

ファイルを取得して

$ curl -LO https://raw.githubusercontent.com/google/styleguide/gh-pages/eclipse-java-google-style.xml

以下のように設定します。

                <configuration>
                    <configFile>${project.basedir}/eclipse-java-google-style.xml</configFile>
                </configuration>

これで、指定したフォーマットが適用されるようになります。

フォーマット対象とするディレクトリを変更する

これは、directoriesを使えばよいようです。

                <configuration>
                    <directories>
                        <directory>${project.build.sourceDirectory}</directory>
                        <directory>${project.build.directory}/generated-sources</directory>
                    </directories>
                </configuration>

まとめ

Formatter Maven Pluginを試してみました。

けっこう簡単に使えるので便利なのですが、IDEでの保存時にフォーマットしておくのがやっぱり良いような気もしますね…。

IntelliJでソースコードを書いていると、Eclipseのフォーマッター自体はインポート可能なのですが、Formatter Maven Pluginでvalidate
したりすると解釈が異なりフォーマットされていないと怒られたりしました…。