これは、なにをしたくて書いたもの?
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は教えてもらいました)。
- formatter-maven-plugin – Introduction
- GitHub - google/google-java-format: Reformats Java source code to comply with Google Java Style.
- GitHub - diffplug/spotless: Keep your code spotless
- GitHub - HubSpot/prettier-maven-plugin
今回は、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 - 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:format … 設定に従ってソースコードをフォーマットする
- formatter:validate … 設定に従ってソースコードがフォーマットされていない場合、ビルドに失敗する
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" };
フォーマット対象のディレクトリがデフォルトで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 … https://github.com/revelc/formatter-maven-plugin/blob/formatter-maven-plugin-2.21.0/src/main/resources/formatter-maven-plugin/eclipse/java.xml
- JavaScript … https://github.com/revelc/formatter-maven-plugin/blob/formatter-maven-plugin-2.21.0/src/main/resources/formatter-maven-plugin/eclipse/javascript.xml
- JSON … https://github.com/revelc/formatter-maven-plugin/blob/formatter-maven-plugin-2.21.0/src/main/resources/formatter-maven-plugin/jackson/json.properties
- XML … https://github.com/revelc/formatter-maven-plugin/blob/formatter-maven-plugin-2.21.0/src/main/resources/formatter-maven-plugin/eclipse/xml.properties
- HTML … https://github.com/revelc/formatter-maven-plugin/blob/formatter-maven-plugin-2.21.0/src/main/resources/formatter-maven-plugin/jsoup/html.properties
- CSS … https://github.com/revelc/formatter-maven-plugin/blob/formatter-maven-plugin-2.21.0/src/main/resources/formatter-maven-plugin/ph-css/css.properties
これを、それぞれ以下の設定で独自のファイルを指定することができます。
- 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
したりすると解釈が異なりフォーマットされていないと怒られたりしました…。