これは、なにをしたくて書いたもの?
以前、Spotless Maven Pluginとgoogle-java-formatでソースコードのフォーマットをしてみました。
Spotless Maven Pluginとgoogle-java-formatで、ソースコードのフォーマットを行う - CLOVER🍀
今回はSpotless Maven PluginとPalantir Java Formatの組み合わせを使ってみたいと思います。
Palantir Java Format
Palantir Java FormatのGitHubリポジトリーはこちら。
Palantir Java Formatは、google-java-formatをベースに作られた、Lambdaフレンドリーで120文字のJavaフォーマッターです。
google-java-formatはそれそれでよいのですが、以下の点が気になっていました。
- インデントがスペース2つ
- Lambdaやメソッドチェーンのフォーマットが微妙
インデントは調整可能ですし、他はそういうものだと割り切ってもいいかなと思っていたのですが、Palantir Java Formatの
README.mdを見てこのあたりが気になりました。
(1) google-java-format output:
private static void configureResolvedVersionsWithVersionMapping(Project project) { project.getPluginManager() .withPlugin( "maven-publish", plugin -> { project.getExtensions() .getByType(PublishingExtension.class) .getPublications() .withType(MavenPublication.class) .configureEach( publication -> publication.versionMapping( mapping -> { mapping.allVariants( VariantVersionMappingStrategy ::fromResolutionResult); })); }); }
(1) palantir-java-format output:
private static void configureResolvedVersionsWithVersionMapping(Project project) { project.getPluginManager().withPlugin("maven-publish", plugin -> { project.getExtensions() .getByType(PublishingExtension.class) .getPublications() .withType(MavenPublication.class) .configureEach(publication -> publication.versionMapping(mapping -> { mapping.allVariants(VariantVersionMappingStrategy::fromResolutionResult); })); }); }
(2) google-java-format output:
private static GradleException notFound( String group, String name, Configuration configuration) { String actual = configuration.getIncoming().getResolutionResult().getAllComponents().stream() .map(ResolvedComponentResult::getModuleVersion) .map( mvi -> String.format( "\t- %s:%s:%s", mvi.getGroup(), mvi.getName(), mvi.getVersion())) .collect(Collectors.joining("\n")); // ... }
(2) palantir-java-format output:
private static GradleException notFound(String group, String name, Configuration configuration) { String actual = configuration.getIncoming().getResolutionResult().getAllComponents().stream() .map(ResolvedComponentResult::getModuleVersion) .map(mvi -> String.format("\t- %s:%s:%s", mvi.getGroup(), mvi.getName(), mvi.getVersion())) .collect(Collectors.joining("\n")); // ... }
Palantir Java Format / Motivation & examples
こういうメソッドチェーンの場合、長くなった時に1行にまとめられてしまうとちょっと読みにくいのですが、
var foo = SomeType.builder().thing1(thing1).thing2(thing2).thing3(thing3).build();
Palantir Java Formatの場合はメソッドチェーンが80文字を超えると折り返してくれるようです。
var foo = SomeType.builder()
.thing1(thing1)
.thing2(thing2)
.thing3(thing3)
.build();
Palantir Java Format / Optimised for code review
なかなかよさそうだなと思ったので、使ってみることにしました。
Gradleプラグイン、Spotless、IntelliJプラグインとして使えるようです。今回はSpotlessのMavenプラグインと組み合わせて
使ってみます。
なお、こちらのエントリーを書いた時にもPalantir Java Formatの名前は載っていたのですが、どういうフォーマッターなのか
まったく見ていませんでした…。
Spotless Maven Pluginとgoogle-java-formatで、ソースコードのフォーマットを行う - CLOVER🍀
環境
今回の環境はこちら。
$ java --version openjdk 25.0.2 2026-01-20 OpenJDK Runtime Environment (build 25.0.2+10-Ubuntu-124.04) OpenJDK 64-Bit Server VM (build 25.0.2+10-Ubuntu-124.04, mixed mode, sharing) $ mvn --version Apache Maven 3.9.13 (39d686bd50d8e054301e3a68ad44781df6f80dda) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 25.0.2, vendor: Ubuntu, runtime: /usr/lib/jvm/java-25-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "6.8.0-101-generic", arch: "amd64", family: "unix"
準備
お題で使うソースコードは、こちらと同じものにします。
Spotless Maven Pluginとgoogle-java-formatで、ソースコードのフォーマットを行う - CLOVER🍀
Maven依存関係などはこちら。
<properties> <maven.compiler.release>25</maven.compiler.release> <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.15.Final</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>com.diffplug.spotless</groupId> <artifactId>spotless-maven-plugin</artifactId> <version>3.3.0</version> <configuration> <java> <palantirJavaFormat> <version>2.89.0</version> <style>PALANTIR</style> <formatJavadoc>true</formatJavadoc> </palantirJavaFormat> </java> </configuration> </plugin> </plugins> </build>
Palantir Java Formatは設定済みですが、あとで説明します。
ソースコードを用意しましょう。
空のbeans.xml。
src/main/resources/META-INF/beans.xml
フォーマットは最初から崩しておきます。
src/main/java/org/littlewings/formatter/App.java
package org.littlewings.formatter; import org.jboss.logging.Logger; import org.littlewings.formatter.rest.RestApplication; import jakarta.ws.rs.SeBootstrap; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; public class App { 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 RestApplication(), configuration) .toCompletableFuture() .get(); logger.info("server startup."); while (true) { TimeUnit.SECONDS.sleep(5L); } /* * instance.stop().toCompletableFuture().get(); */ } }
src/main/java/org/littlewings/formatter/rest/RestApplication.java
package org.littlewings.formatter.rest; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; import java.util.Set; @ApplicationPath("/") public class RestApplication extends Application { @Override public Set<Class<?>> getClasses() { return Set.of(HelloResource.class); } }
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 private MessageService messageService; @GET @Produces(MediaType.TEXT_PLAIN) public String message(@QueryParam("word") String word) {return messageService.create(word);} }
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!!"; } } }
ちょっと見た目的にわかりにくいですが、簡単なJakarta RESTful Web Servicesを使ったアプリケーションです。
動作確認しておきます。
$ mvn compile exec:java -Dexec.mainClass=org.littlewings.formatter.App
OKですね。
$ curl localhost:8080/hello?word=RESTEasy
Hello RESTEasy!!
Spotless Maven PluginでPalantir Java Formatを使う
では、Spotless Maven PluginとPalantir Java Formatについて見ていきます。こういう設定をしていました。
<configuration> <java> <palantirJavaFormat> <version>2.89.0</version> <style>PALANTIR</style> <formatJavadoc>true</formatJavadoc> </palantirJavaFormat> </java> </configuration>
ちなみに、最小構成としてはこちらになります。
<configuration> <java> <palantirJavaFormat /> </java> </configuration>
まずは確認。
$ mvn spotless:check
当然ですが、フォーマットがめちゃくちゃなので怒られます。
[ERROR] Failed to execute goal com.diffplug.spotless:spotless-maven-plugin:3.3.0:check (default-cli) on project palantir-java-format-spotless-example: The following files had format violations: [ERROR] src/main/java/org/littlewings/formatter/App.java [ERROR] @@ -1,35 +1,30 @@ [ERROR] package·org.littlewings.formatter; [ERROR] [ERROR] -import·org.jboss.logging.Logger; [ERROR] -import·org.littlewings.formatter.rest.RestApplication; [ERROR] - [ERROR] - [ERROR] import·jakarta.ws.rs.SeBootstrap; [ERROR] import·java.util.concurrent.ExecutionException; [ERROR] import·java.util.concurrent.TimeUnit; [ERROR] +import·org.jboss.logging.Logger; [ERROR] +import·org.littlewings.formatter.rest.RestApplication; [ERROR] [ERROR] public·class·App·{ [ERROR] -static·void·main(String...·args)·throws·ExecutionException,·InterruptedException·{ [ERROR] -Logger·logger·= [ERROR] -········Logger.getLogger(App.class); [ERROR] +····static·void·main(String...·args)·throws·ExecutionException,·InterruptedException·{ [ERROR] +········Logger·logger·=·Logger.getLogger(App.class); [ERROR] [ERROR] -SeBootstrap.Configuration·configuration·=················SeBootstrap [ERROR] -.Configuration.builder().host("0.0.0.0").port(8080).build(); [ERROR] +········SeBootstrap.Configuration·configuration·= [ERROR] +················SeBootstrap.Configuration.builder().host("0.0.0.0").port(8080).build(); [ERROR] [ERROR] -········SeBootstrap.Instance [ERROR] -················instance·= [ERROR] -SeBootstrap.start(new·RestApplication(),·configuration) [ERROR] -.toCompletableFuture() [ERROR] -.get(); [ERROR] +········SeBootstrap.Instance·instance·=·SeBootstrap.start(new·RestApplication(),·configuration) [ERROR] +················.toCompletableFuture() [ERROR] +················.get(); [ERROR] [ERROR] -logger.info("server·startup."); [ERROR] +········logger.info("server·startup."); [ERROR] [ERROR] -while·(true)·{ [ERROR] -TimeUnit.SECONDS.sleep(5L); [ERROR] -} [ERROR] +········while·(true)·{ [ERROR] +············TimeUnit.SECONDS.sleep(5L); [ERROR] +········} [ERROR] [ERROR] ········/* [ERROR] ·········*·instance.stop().toCompletableFuture().get(); [ERROR] ·········*/ [ERROR] -} [ERROR] +····} [ERROR] ... (1 more lines that didn't fit) [ERROR] Violations also present in: [ERROR] src/main/java/org/littlewings/formatter/rest/RestApplication.java [ERROR] src/main/java/org/littlewings/formatter/rest/HelloResource.java [ERROR] src/main/java/org/littlewings/formatter/service/MessageService.java [ERROR] Run 'mvn spotless:apply' to fix these violations. [ERROR] -> [Help 1]
spotless:applyでフォーマットします。
$ mvn spotless:apply
フォーマットされました。
[INFO] --- spotless:3.3.0:apply (default-cli) @ palantir-java-format-spotless-example --- WARNING: A terminally deprecated method in sun.misc.Unsafe has been called WARNING: sun.misc.Unsafe::staticFieldBase has been called by com.diffplug.spotless.java.ModuleHelper (file:/$HOME/.m2/repository/com/diffplug/spotless/spotless-lib/4.4.0/spotless-lib-4.4.0.jar) WARNING: Please consider reporting this to the maintainers of class com.diffplug.spotless.java.ModuleHelper WARNING: sun.misc.Unsafe::staticFieldBase will be removed in a future release [INFO] clean file: /path/to/src/main/java/org/littlewings/formatter/App.java [INFO] clean file: /path/to/src/main/java/org/littlewings/formatter/rest/RestApplication.java [INFO] clean file: /path/to/src/main/java/org/littlewings/formatter/rest/HelloResource.java [INFO] clean file: /path/to/src/main/java/org/littlewings/formatter/service/MessageService.java [INFO] Spotless.Java is keeping 4 files clean - 4 were changed to be clean, 0 were already clean, 0 were skipped because caching determined they were already clean
確認してみましょう。
src/main/java/org/littlewings/formatter/App.java
package org.littlewings.formatter; import jakarta.ws.rs.SeBootstrap; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.jboss.logging.Logger; import org.littlewings.formatter.rest.RestApplication; public class App { 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 RestApplication(), configuration) .toCompletableFuture() .get(); logger.info("server startup."); while (true) { TimeUnit.SECONDS.sleep(5L); } /* * instance.stop().toCompletableFuture().get(); */ } }
src/main/java/org/littlewings/formatter/rest/RestApplication.java
package org.littlewings.formatter.rest; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; import java.util.Set; @ApplicationPath("/") public class RestApplication extends Application { @Override public Set<Class<?>> getClasses() { return Set.of(HelloResource.class); } }
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 private MessageService messageService; @GET @Produces(MediaType.TEXT_PLAIN) public String message(@QueryParam("word") String word) { return messageService.create(word); } }
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!!"; } } }
フォーマットされました。
ちなみにSpotless Maven Pluginはデフォルトでvalidateフェーズにマッピングされているので、以下のように設定すると
mvn verifyでspotless:checkが動作するようになります。
<plugin> <groupId>com.diffplug.spotless</groupId> <artifactId>spotless-maven-plugin</artifactId> <version>3.3.0</version> <executions> <execution> <goals> <goal>check</goal> </goals> </execution> </executions> <configuration> <java> <palantirJavaFormat> <version>2.89.0</version> <style>PALANTIR</style> <formatJavadoc>true</formatJavadoc> </palantirJavaFormat> </java> </configuration> </plugin>
個人的にはmvn compile時にspotless:applyしてしまう、でいいのではないかと思いますが。
<executions> <execution> <goals> <goal>apply</goal> </goals> <phase>compile</phase> </execution> </executions>
ただ、この設定はテストコードもフォーマットしようとするので、mvn test-compileの前に動作してしまう点には注意です。
設定を確認する
ところでいきなりこちらを載せましたが、バージョンの指定とformatJavadoc以外はデフォルトです。
<palantirJavaFormat> <version>2.89.0</version> <style>PALANTIR</style> <formatJavadoc>true</formatJavadoc> </palantirJavaFormat>
formatJavadocは文字通りJavadocをフォーマットするかどうかですね。デフォルトはfalseです。
設定できるものは、実は多くありません。というか、上記がすべてです。
styleにはPALANTIR(デフォルト)以外にGOOGLEとAOSPが指定できます。
GOOGLEを指定するとなにが起こるのか?と思いますが、インデントが2になります。
<palantirJavaFormat> <version>2.89.0</version> <style>GOOGLE</style> <formatJavadoc>true</formatJavadoc> </palantirJavaFormat>
public class App { 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 RestApplication(), configuration) .toCompletableFuture() .get(); logger.info("server startup."); while (true) { TimeUnit.SECONDS.sleep(5L); } /* * instance.stop().toCompletableFuture().get(); */ } }
こう見ると、スタイルの差はインデントと行あたりの最大長ですね。
AOSPを選ぶとインデントは4になりますが、行あたりの最大長が100になります。
google-format-javaと比べる
少しgoogle-format-javaのフォマート結果と比べてみましょうか。
こちらがPalantir Java Formatでフォーマットした結果でした。
src/main/java/org/littlewings/formatter/App.java
package org.littlewings.formatter; import jakarta.ws.rs.SeBootstrap; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.jboss.logging.Logger; import org.littlewings.formatter.rest.RestApplication; public class App { 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 RestApplication(), configuration) .toCompletableFuture() .get(); logger.info("server startup."); while (true) { TimeUnit.SECONDS.sleep(5L); } /* * instance.stop().toCompletableFuture().get(); */ } }
google-java-formatに変えてみます。
<googleJavaFormat> <version>1.35.0</version> <style>GOOGLE</style> <reorderImports>true</reorderImports> <formatJavadoc>true</formatJavadoc> </googleJavaFormat>
フォーマットを実行。
$ mvn spotless:apply
こうなりました。
src/main/java/org/littlewings/formatter/App.java
package org.littlewings.formatter; import jakarta.ws.rs.SeBootstrap; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.jboss.logging.Logger; import org.littlewings.formatter.rest.RestApplication; public class App { 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 RestApplication(), configuration).toCompletableFuture().get(); logger.info("server startup."); while (true) { TimeUnit.SECONDS.sleep(5L); } /* * instance.stop().toCompletableFuture().get(); */ } }
インデント以外にも、このあたりに差が出ますね。
// Palantir Java Format SeBootstrap.Instance instance = SeBootstrap.start(new RestApplication(), configuration) .toCompletableFuture() .get(); // google-java-format SeBootstrap.Instance instance = SeBootstrap.start(new RestApplication(), configuration).toCompletableFuture().get();
こんなところでしょうか。
おわりに
Spotless Maven PluginとPalantir Java Formatでソースコードをフォーマットしてみました。
Javaのフォーマッターはなかなか悩ましいのですが、あまりいろいろチューニングしたいわけでもないので、Palantir Java Format
くらいのフォーマットで個人的にはよさそうな気がします。
フォーマッターを使う時はこれかな、と。