CLOVER🍀

That was when it all began.

Spotless Maven Pluginとgoogle-java-formatで、ソースコードのフォーマットを行う

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

以前、Formatter Maven Pluginを使ってソースコードのフォーマットをしてみました。

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

今回はSpotless Maven Pluginとgoogle-java-formatでソースコードのフォーマットを行ってみます。

google-java-format

先にgoogle-java-formatから載せておきましょう。google-java-formatのGitHubリポジトリーはこちら。

GitHub - google/google-java-format: Reformats Java source code to comply with Google Java Style.

google-java-formatは、JavaソースコードGoogle Java Style Guideに準拠させるプログラムです。

Google Java Style Guide

IntelliJ IDEAのプラグインも提供されています。

google-java-format - IntelliJ IDEs Plugin | Marketplace

各種ビルドツールとのインテグレーションは、サードパーティーの提供に任されているようです。

google-java-format / Third-party integrations

Apache Maven向けの場合は、Spotless Maven Pluginがそのひとつとして挙げられています。

https://github.com/diffplug/spotless/tree/main/plugin-maven

その他にもMavenプラグインが紹介されていますが、実質的な選択肢はSpotless Maven Pluginになるように思います。

Spotless Maven Plugin

SpotlessのGitHubリポジトリーはこちら。

GitHub - diffplug/spotless: Keep your code spotless

Spotless自体は様々な言語のソースコードのフォーマッターで、Apache Maven、Gradleのプラグインとして動作します。

今回はApache Mavenプラグインを見ていきます。

https://github.com/diffplug/spotless/tree/maven/2.44.5/plugin-maven

Quickstartを見るとだいたい使い方の雰囲気はわかる気がします。

Spotless: Keep your code spotless with Maven / Quickstart

実行にはJava 11以上が必要なようです。

Spotless requires Maven to be running on JRE 11+. To use JRE 8, go back to 2.30.0 or older.

対応言語は多く、以下になっています。

  • Java (google-java-format, eclipse jdt, prettier, palantir-java-format, formatAnnotations, cleanthat)
  • Groovy (eclipse groovy)
  • Kotlin (ktfmt, ktlint, diktat, prettier)
  • Scala (scalafmt)
  • C/C++ (eclipse cdt, clang-format)
  • Python (black)
  • Antlr4 (antlr4formatter)
  • Sql (dbeaver)
  • Maven Pom (sortPom)
  • Markdown (flexmark)
  • Typescript (tsfmt, prettier, ESLint, Biome)
  • Javascript (prettier, ESLint, Biome)
  • JSON (simple, gson, jackson, Biome, jsonPatch)
  • YAML
  • Gherkin
  • Go
  • RDF
  • Protobuf (buf, clang-format)
  • Multiple languages
    • Prettier (plugins, npm detection, .npmrc detection, caching npm install results)
    • eclipse web tools platform
    • Biome (binary detection, config file, input language)

とはいえ、実質的に使うのはJava(もしくはJava VM上の言語)に関するものだと思いますが。

デフォルトではspotless:checkゴールがApache Mavenvalidateフェーズにマッピングされているようです。

Spotless: Keep your code spotless with Maven / Binding to maven phase

つまり、以下のように書いておくとmvn verifyで動作します。

                <executions>
                    <execution>
                        <goals>
                            <goal>check</goal>
                        </goals>
                    </execution>
                </executions>

compileフェーズでspotless:checkを実行する例がサンプルとして載せられています。

Spotless Maven Pluginのゴールについて説明しているセクションは特になさそうなのですが、spotless:checkで確認、
spotless:applyで修正となるようです(ドキュメントの最初に載っています)。

user@machine repo % mvn spotless:check
[ERROR]  > The following files had format violations:
[ERROR]  src\main\java\com\diffplug\gradle\spotless\FormatExtension.java
[ERROR]    -\t\t····if·(targets.length·==·0)·{
[ERROR]    +\t\tif·(targets.length·==·0)·{
[ERROR]  Run 'mvn spotless:apply' to fix these violations.
user@machine repo % mvn spotless:apply
[INFO] BUILD SUCCESS
user@machine repo % mvn spotless:check
[INFO] BUILD SUCCESS
Javaで使う

Javaでの使い方ですが、フォーマットはgoogle-java-format、palantir-java-format、eclipse jdtの3種から選びます。

Spotless: Keep your code spotless with Maven / Languages / Java

今回はgoogle-java-formatを使います。設定はこちら。

Spotless: Keep your code spotless with Maven / Languages / Java / google-java-format

設定項目自体は多くないですが、詳しくはソースコードを見てという感じみたいです。

https://github.com/diffplug/spotless/blob/maven/2.44.5/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/GoogleJavaFormat.java

また言語を問わない設定はこちらです。

Spotless: Keep your code spotless with Maven / Language independent / Generic steps

だいたい設定などは見た気がするので、とりあえず使っていってみましょう。

お題となるソースコードはFormatter Maven Pluginの時に使ったものをベースにすることにします。

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

環境

今回の環境はこちら。

$ java --version
openjdk 21.0.7 2025-04-15
OpenJDK Runtime Environment (build 21.0.7+6-Ubuntu-0ubuntu124.04)
OpenJDK 64-Bit Server VM (build 21.0.7+6-Ubuntu-0ubuntu124.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.10 (5f519b97e944483d878815739f519b2eade0a91d)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 21.0.7, vendor: Ubuntu, runtime: /usr/lib/jvm/java-21-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "6.8.0-60-generic", arch: "amd64", family: "unix"

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

では、簡単なサンプルアプリケーションを作成します。

先ほど書いたように、ベースはこちらです。Jakarta RESTful Web Services+Jakarta Contexts and Dependency Injectionな
Javaアプリケーションを、Java SE環境で動かすことにしましょう。

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

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

    <properties>
        <maven.compiler.release>21</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.12.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 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 {
    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 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 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!!";
        }
    }
}

動作確認。

$ 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 jakarta.ws.rs.SeBootstrap;

import org.jboss.logging.Logger;
import org.littlewings.formatter.rest.RestApplication;

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

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 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 java.util.Set;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@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 org.littlewings.formatter.service.MessageService;

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;

@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/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とgoogle-java-formatを組み込む

それでは、Spotless Maven Pluginとgoogle-java-formatを組み込んでみます。

    <build>
        <plugins>
            <plugin>
                <groupId>com.diffplug.spotless</groupId>
                <artifactId>spotless-maven-plugin</artifactId>
                <version>2.44.5</version>
                <configuration>
                    <java>
                        <googleJavaFormat>
                            <version>1.27.0</version>
                            <reorderImports>true</reorderImports>
                            <formatJavadoc>true</formatJavadoc>
                        </googleJavaFormat>

                        <indent>
                            <tabs>true</tabs>
                            <spacesPerTab>2</spacesPerTab>
                        </indent>
                        <indent>
                            <spaces>true</spaces>
                            <spacesPerTab>4</spacesPerTab>
                        </indent>
                    </java>
                </configuration>
            </plugin>
        </plugins>
    </build>

Java向けの設定で、google-java-formatを使うように設定します。

                <configuration>
                    <java>
                        <googleJavaFormat>
                            <version>1.27.0</version>
                            <reorderImports>true</reorderImports>
                            <formatJavadoc>true</formatJavadoc>
                        </googleJavaFormat>

                        ...

                    </java>
                </configuration>

最小構成だと以下でもいいのですが、google-java-formatのバージョンを明示的に指定したり、importの並び替えだったり
Javadocの設定を入れたりしています。

                <configuration>
                    <java>
                        <googleJavaFormat/>
                    </java>
                </configuration>

versionは明示的に指定しない場合は使っているJava VMに合わせたものを選んでくれるようですが、最新のものを選んでくれる
わけでもなさそうなので明示的に書くようにしました。

この設定は、インデントを2から4に変更する設定です。

                        <indent>
                            <tabs>true</tabs>
                            <spacesPerTab>2</spacesPerTab>
                        </indent>
                        <indent>
                            <spaces>true</spaces>
                            <spacesPerTab>4</spacesPerTab>
                        </indent>

Google Java Styleでは空白はスペース2つなのですが、ここは変えてみました。まあ、もはやこういうのは変えない方がいいのでは
という気もしますけどね。

設定はこちらを参考にしています。

How to set indent from 2(default) to 4 by spotless? · Issue #525 · google/google-java-format · GitHub

spotless:checkで確認。

$ mvn spotless:check

結果。まあ怒られますね。

[ERROR] Failed to execute goal com.diffplug.spotless:spotless-maven-plugin:2.44.5:check (default-cli) on project spotless-maven-plugin-example: The following files had format violations:
[ERROR]     src/main/java/org/littlewings/formatter/App.java
[ERROR]         @@ -1,26 +1,26 @@
[ERROR]          package·org.littlewings.formatter;
[ERROR]
[ERROR]          import·jakarta.ws.rs.SeBootstrap;
[ERROR]         -
[ERROR]         -import·org.jboss.logging.Logger;
[ERROR]         -import·org.littlewings.formatter.rest.RestApplication;
[ERROR]         -
[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]         -public·static·void·main(String...·args)·throws·ExecutionException,·InterruptedException·{
[ERROR]         -Logger·logger·=·Logger.getLogger(App.class);
[ERROR]         +····public·static·void·main(String...·args)·throws·ExecutionException,·InterruptedException·{
[ERROR]         +········Logger·logger·=·Logger.getLogger(App.class);
[ERROR]
[ERROR]         -SeBootstrap.Configuration·configuration·=·SeBootstrap.Configuration.builder().host("0.0.0.0").port(8080)
[ERROR]         -.build();
[ERROR]         +········SeBootstrap.Configuration·configuration·=
[ERROR]         +················SeBootstrap.Configuration.builder().host("0.0.0.0").port(8080).build();
[ERROR]
[ERROR]         -SeBootstrap.Instance·instance·=·SeBootstrap.start(new·RestApplication(),·configuration).toCompletableFuture()
[ERROR]         -.get();
[ERROR]         +········SeBootstrap.Instance·instance·=
[ERROR]         +················SeBootstrap.start(new·RestApplication(),·configuration).toCompletableFuture().get();
[ERROR]
[ERROR]         -logger.info("server·startup.");
[ERROR]         +········logger.info("server·startup.");
[ERROR]
[ERROR]         -while·(true){TimeUnit.SECONDS.sleep(5L);}
[ERROR]         +········while·(true)·{
[ERROR]         +············TimeUnit.SECONDS.sleep(5L);
[ERROR]         +········}
[ERROR]
[ERROR]          ········/*
[ERROR]          ·········*·instance.stop().toCompletableFuture().get();
[ERROR]     src/main/java/org/littlewings/formatter/rest/RestApplication.java
[ERROR]         @@ -1,13 +1,13 @@
[ERROR]          package·org.littlewings.formatter.rest;
[ERROR]
[ERROR]         -import·java.util.Set;
[ERROR]          import·jakarta.ws.rs.ApplicationPath;
[ERROR]          import·jakarta.ws.rs.core.Application;
[ERROR]         +import·java.util.Set;
[ERROR]
[ERROR]          @ApplicationPath("/")
[ERROR]     ... (10 more lines that didn't fit)
[ERROR] Violations also present in:
[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

4ファイルフォーマットされたことが確認できました。

[INFO] --- spotless:2.44.5:apply (default-cli) @ spotless-maven-plugin-example ---
[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/tosrc/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

結果は最初に戻るだけなので省略します。

コンパイル時に動作するようにする

もう少し設定を変えてみましょう。コンパイル時に実行するようにするのは、executions / executionを追加します。

        <plugins>
            <plugin>
                <groupId>com.diffplug.spotless</groupId>
                <artifactId>spotless-maven-plugin</artifactId>
                <version>2.44.5</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>check</goal>
                        </goals>
                        <phase>compile</phase>
                    </execution>
                </executions>
                <configuration>
                    <java>
                        <googleJavaFormat>
                            <version>1.27.0</version>
                            <reorderImports>true</reorderImports>
                            <formatJavadoc>true</formatJavadoc>
                        </googleJavaFormat>

                        <indent>
                            <tabs>true</tabs>
                            <spacesPerTab>2</spacesPerTab>
                        </indent>
                        <indent>
                            <spaces>true</spaces>
                            <spacesPerTab>4</spacesPerTab>
                        </indent>
                    </java>
                </configuration>
            </plugin>

これでmvn compileでチェックが行われ、フォーマットされていないファイルがあるとビルドが失敗するようになります。

$ mvn compile
フォーマットする対象を設定する

これはincludes / includeで行います。

こんな感じですね。

                    <java>
                        <includes>
                            <include>src/main/java/**/*.java</include>
                            <include>src/test/java/**/*.java</include>
                        </includes>

                        ...

                    <java>

こんなところでしょうか。

おわりに

Spotless Maven Pluginとgoogle-java-formatでソースコードのフォーマットを行ってみました。

この手のフォーマッターですが、微妙に結果が気になったりして個人的にあまり扱わないところがあったりしたのですが、
最近のAI事情もあって、そこを気にしても仕方がなくなるんだろうなと思い始めていたりします。

だとすると、インデントにこだわるなという話なのですが(笑)。
あと、設定変えるとIntelliJ IDEAのgoogle-java-formatと動きが合わなくなるとかありますが…。

というか、言語にフォーマッターが入ってくれるのがモメるところがなくて1番いい気がしますけどね。

まあ、今から入ることはそうそうないと思うので、もう合わせていきましょうか。

そういえば、Springにもフォーマッターがありましたね。

GitHub - spring-io/spring-javaformat