これは、なにをしたくて書いたもの?
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の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>
<mavencompilersource>17</mavencompilersource>
<mavencompilertarget>17</mavencompilertarget>
<projectbuildsourceEncoding>UTF-8</projectbuildsourceEncoding>
<projectreportingoutputEncoding>UTF-8</projectreportingoutputEncoding>
</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);
}
}
}
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);}
}
}
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を組み込んでみます。
まずはデフォルトの状態で。
<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>
<mavencompilersource>17</mavencompilersource>
<mavencompilertarget>17</mavencompilertarget>
</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
したりすると解釈が異なりフォーマットされていないと怒られたりしました…。