Spring Boot 1.5.xから2.0.xに上げた Spring Web編

Spring Boot 2系にあげたときにやったことをまとめようと思います。(関連記事) 今回は、Spring Webの話です。Spring Boot 2.0.9.RELEASEに上げた話になります。2.2対応はまたいつか書きます。

依存しているライブラリのバージョン

Spring Bootのバージョンを1.5.22から2.0.9に上げると依存しているライブラリのバージョンが次のようになります。

Spring Boot 1.5.22.RELEASESpring Boot 2.0.9.RELEASE

Hibernate Validator

@NotBlank, @NotEmpty, @Emailが非推奨になった

Hibernate Validator 6.0.0.Finalからorg.hibernate.validator.constraints.NotBlank, org.hibernate.validator.constraints.NotEmpty, org.hibernate.validator.constraints.Emailが非推奨になりました。参考 Spring Boot 2系からはjavax-validationが提供するアノテーションを利用します。

- org.hibernate.validator.constraints.NotBlank
+ javax.validation.constraints.NotBlank

Jackson /JSON Support

Spring Boot 2からspring-boot-starter-jsonが作られました。jackson-databind, jackson-datatype-jdk8, jackson-datatype-jsr310, jackson-module-parameter-namesが含まれていますので、これらをspring-boot-starter-jsonに置き換えることができます。参考

Spring Web

WebMvcConfigurerAdapter.classが非推奨

Spring Boot 2(Spring 5)からWebMvcConfigurerAdapter.classが非推奨となり、先のクラスを拡張するのではなく、WebMvcConfigurer.classを実装するようになりました。 Spring 5からJava8+になってdefaultメソッドが使えるようになり、WebMvcConfigurerAdapter.classを使わずともインターフェースに実装できるようになったからです。Javadoc

パッケージ変更

Spring Boot 2(Spring 5)からWebと言ってもServletとReactiveに分かれたためです。

- org.springframework.boot.autoconfigure.web.ErrorAttributes
+ org.springframework.boot.web.servlet.error.ErrorAttributes
- org.springframework.boot.autoconfigure.web.ErrorController
+ org.springframework.boot.web.servlet.error.ErrorController

プロパティ変更

Boot 2.0.9.RELEASEの設定値一覧を見れば書いてあります。

- server.context-path
+ server.servlet.context-path

おまけ

org.springframework.web.util.HtmlUtils.htmlEscape(String input, String encoding)の実装が変わった

見出しの通りです。4.3.x.RELEASEから5.0.x.RELEASEに変わりました。4.3.xでは、input == nullのときnullが返ってきていましたが、5.0.xからはExceptionを投げるようになりました。

このように書いていたので本番で障害が起きてしまいました...みなさんお気をつけて

// inputはnullのとき
String result = HtmlUtils.htmlEscape(input, encoding);
if (result == null) {
  何か処理
}

Flyway 3.x -> 5.xに上げた

Spring Boot2系にあげたときにやったことをまとめようと思います。(関連記事) 今回は、Flywayの話です。バージョンアップ後のSpring Bootのバージョンは2.0.9.RELEASEです。2.2対応はまたいつか書きます。

FlywayとSpring Boot

Flywayは、データベースマイグレーションライブラリです。 Spring Bootとの連携も簡単にできます。詳しくは、コチラを読んでください。

Spring Boot 1.5.22のFlywayのバージョンは3.2.1でしたが、Spring Boot 2.0.9では、5.0.7とメジャーバージョンが2つも上がりました。ちなみにSpring Boot 2.2.2では6.0.8になっています。

Flywayを3.x -> 5.xへ

まずカジュアルに3.x -> 5.xにあげてみましたが、マイグレーションが失敗しました...。

2019-09-11 03:43:15.152 ERROR --- Application startup failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'flywayInitializer' defined in class path resource [org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayConfiguration.class]: Invocation of init method failed; nested exception is org.flywaydb.core.api.FlywayException: Validate failed: Migration checksum mismatch for migration 1
-> Applied to database : 1750005324
-> Resolved locally    : -558371367
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1583)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:545)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:296)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1076)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:851)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:541)
    at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:122)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:761)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:371)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1186)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1175)
    at com.b1a9idps.sample.view.Application.main(Application.java:19)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:48)
    at org.springframework.boot.loader.Launcher.launch(Launcher.java:87)
    at org.springframework.boot.loader.Launcher.launch(Launcher.java:50)
    at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:58)
Caused by: org.flywaydb.core.api.FlywayException: Validate failed: Migration checksum mismatch for migration 1

変更点

調べてみたら3 -> 44 -> 5で大きな変更が入っていました。

● 3 -> 4
チェックサム計算時に、改行コードを無視するようになりました。つまり、チェックサムの計算方法が4系から変更になりました。背景等は、[コチラ](https://flywaydb.org/blog/flyway-4.0)
● 4 -> 5
Flywayのメタデータを管理するテーブル名が `schema_version` から `flyway_schema_history` に変更になりました。詳しくは、[コチラ](https://github.com/flyway/flyway/issues/1848)

Flyway3 -> 5にするときの方法はちゃんとリリースノートに書いてありました。

Important note for users upgrading from Flyway 3.x: This release no longer supports a schema history table upgrade from Flyway 3.x. You must upgrade to Flyway 4.2.0 first before upgrading to Flyway 5.0.0.

Flyway 4系に上げる

Spring Boot 1.5.xのFlywayを4.2.0に上げます。

dependencies {
    compile "org.flywaydb:flyway-core:4.2.0"
}

起動します。

$ gradle bootRun

2019-09-11 03:43:14.467  INFO --- Upgrading metadata table `sample`.`schema_version` to the Flyway 4.0 format ...
2019-09-11 03:43:15.329  INFO --- Repairing metadata for version 1 (Description: CREATE TABLE SAMPLE DATA, Checksum: -558371367)  ...
2019-09-11 03:43:15.349  INFO --- Repairing metadata for version 2 (Description: MODIFY SAMPLE DATA LENGTH, Checksum: -1031537806)  ...
2019-09-11 03:43:15.370  INFO --- Repairing metadata for version 3 (Description: SAMPLE DATA ADD INDEX, Checksum: 194652164)  ...
2019-09-11 03:43:15.412  INFO --- Metadata table schema_version successfully upgraded to the Flyway 4.0 format.

初回起動時に、既存のメタデータを管理しているテーブルを自動的でアップデートしてくれました。

Flyway 5系に上げる

Spring Bootのバージョンを2.0.9.RELEASEあたりに上げて、設定ファイルのメタテーブルの名前を変更します。

spring.flyway.table=schema_version

Flywayが5.0.7になっていることを確認して起動すると特に問題なく起動できます。

おまけ

4 -> 5にするときにメタテーブルの名前を変更しなかった場合、6系からフォールバック機能がなくなるからテーブル名変更しろよって注意されます。

2019-09-25 23:05:33.882  WARN --- Could not find schema history table `sample`.`flyway_schema_history`, but found `test`.`schema_version` instead. You are seeing this message because Flyway changed its default for flyway.table in version 5.0.0 to flyway_schema_history and you are still relying on the old default (schema_version). Set flyway.table=schema_version in your configuration to fix this. This fallback mechanism will be removed in Flyway 6.0.0.

謝辞

カジュアルに3 -> 5に上げたときにエラーログ見てもさっぱり何もわかりませんでしたが、w氏の記事のおかげで助かりました。ありがとうございました。

Gradle 4.x -> Gradle 5.xに上げた

Spring Boot2系にあげたときにやったことをまとめようと思います。関連記事 今回はGradleの話です。Upgrading your build from Gradle 4.x to 5.0を参考に作業しました。 これだけじゃなかったと思うので、思い出したら追記します。もうGradle 6出ているんですが、まだそんなに対応していないのでとりあえず5の話です。

4系の最新までバージョンをあげる

まず、4系の最新までバージョンをあげます。 build.gradleにこのように書いて、 ./gradlew wrapper を実行します。

wrapper {
  gradleVersion = "4.10.2"
  distributionType = Wrapper.DistributionType.ALL
}

ビルド

次にビルドです。

./gradlew build

コマンドを実行すると、Gradle 5.0で非互換になる機能を利用しているとメッセージが出力されることがあります。 --warning-mode all オプションをつけて実行すると詳細を確認することができます。参考

build.gradleの書き換え

大きな書き換えは、dependencies指定するcompilieimplementationに書き換えることでした。4.7からcompileとかruntimeなどが非推奨になっています。参考implementationに変更することで依存関係が伝播しなくなるため明確になり、結合度が低くなります。

fooモジュールとbarモジュールで構成されたマルチモジュールプロジェクトがあるとします。 build.gradle(fooモジュール)

dependencies {
    implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-csv"
}

build.gradle(barモジュール)

dependencies {
    implementation project (":foo")
}

jackson-dataformat-csvimplementationで指定されているので、barモジュールではjackson-dataformat-csvの依存関係は伝播しません。

アーカイブファイルの名前を変更する

5.1からアーカイブファイルの名前を変更するときarchivesBaseName=hoge を使うようになりました。 jar.baseName=hoge は非推奨になりました。参考

おまけ

Gradle 3.x -> 4.10.2にするときにLombokが怒ってました。Lombok 1.18.2以上にしてねとのことでした。参考

Spring BootのGradleプラグイン 実行可能Jar(or War)作成タスクが bootRepackage から bootJarbootWarに変更になった。jarタスクやwarタスクは呼び出されない。参考

Spring Cloud Contractで遊んでみた #1

Spring Cloud Contractを触ってみようと思ったきっかけは、会社での SwaggerでAPI仕様書を書く -> レビュー -> 実装 という開発フロー問題を感じているからです。 このフローには 1.実装とAPI仕様書で乖離してしまう恐れがある2.いちいちAPI仕様書を書くのがめんどくさい3.実装が仕様を満たしている保証はできない という問題があります。

そこで、実装とAPI仕様書が乖離しないいい感じのライブラリを探してたら、「Spring Fox」と「Spring REST Docs」が見つかりました。しかし、どちらもコードからAPI仕様書を生成してくれるライブラリなのですが、微妙だなと感じました。 Spring Foxは、公式でないしプロダクトコードに手を加えないといけないです。Spring REST Docsは理想に近かったのですが、TDD向きで今から始めるには辛いかなと。 このような相談を知り合いのエンジニアに話したら、「Spring Cloud Contract」があるよと教えてくれました。

Spring Cloud Contractとは

Spring Cloud Contractは、CDC(Consumer Driven Contracts)をサポートするためのプロジェクトでマイクロサービス化されたアプリケーションに嬉しいプロジェクトです。

Consumer Driven Contracts testingとは

Consumer Driven Contracts(CDC) testingは主にマイクロサービスに役立つテスト手法です。 Consumer(サービスを使う側)が定義したContract(契約)をProvider(サービスを提供する側)が守らなければなりません。

サンプルアプリケーション

ソースコードGitHubに置いてあります。ProducerConsumer

サンプルの内容

  • ProducerとConsumerを実装
  • Spring Cloud Contractを使ってCDC testingできるようにする

サンプルは、Java13、Spring Boot2.2.0で作っています。

Producer

Controllerの実装

BrandController.java

@RestController
@RequestMapping("/brands")
public class BrandController {
  @GetMapping
  public BrandListDto list() {
      Brand stof = new Brand();
      stof.setName("STOF");
      stof.setDesigner("Tanita");

      BrandListDto listDto = new BrandListDto();
      listDto.setBrands(List.of(stof));
      return listDto;
  }
}

Spring Cloud Contractを依存関係に追加

pom.xml

<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-verifier</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

<dependencyManagement>
  <dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>${spring-cloud.version}</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<build>
  <plugins>
    <plugin>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-contract-maven-plugin</artifactId>
        <version>${spring-cloud-contract-maven-plugin.verison}</version>
        <extensions>true</extensions>
        <configuration>
        <!-- 生成するテストがextendsするクラス -->
            <baseClassForTests>
                com.b1a9idps.springcloudcontractsample.producer.TestBase
            </baseClassForTests>
        <!-- JUnit5で利用できるように(デフォルトはJUnit4) -->
            <testFramework>JUNIT5</testFramework>
        </configuration>
    </plugin>
  </plugins>
</build>

TestBase.java

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
// DIコンテナを破棄するタイミングをコントロールする
@DirtiesContext
public abstract class TestBase {
  @BeforeEach
  public void setup(WebApplicationContext context) {
      RestAssuredMockMvc.mockMvc(
              MockMvcBuilders.webAppContextSetup(context).build());
  }
}

Contractを追加

GroovyかYAMLで記述します。デフォルトのパスは、 $rootDir/src/test/resources/contracts です。

brand.yml

request:
  method: GET
  url: /brands
  headers:
    Content-Type: application/json
response:
  status: 200
  body:
    brands:
      - name: "STOF"
        designer:: "Tanita"
  headers:
    Content-Type: application/json

期待するリクエストとレスポンスをそれぞれ記述します。bodyを別でjsonファイルを定義することもできます。

brand.yml

request:
  method: GET
  url: /brands
  headers:
    Content-Type: application/json
response:
  status: 200
  bodyFromFile: brand_response.json
  headers:
    Content-Type: application/json

テストクラスを生成する

./mvnw clean install を実行することでテストクラスが生成して、スタブを作ってローカルレポジトリにインストールします。

...

[INFO] Generating server tests source code for Spring Cloud Contract Verifier contract verification
[INFO] Will use contracts provided in the folder [/Users/uchitate/study/producer/src/test/resources/contracts]
[INFO] Directory with contract is present at [/Users/uchitate/study/producer/src/test/resources/contracts]
[INFO] Test Source directory: /Users/uchitate/study/producer/target/generated-test-sources/contracts added.
[INFO] Using [com.b1a9idps.springcloudcontractsample.producer.TestBase] as base class for test classes, [null] as base package for tests, [null] as package with base classes, base class mappings []
[INFO] Creating new class file [/Users/uchitate/study/producer/target/generated-test-sources/contracts/com/b1a9idps/springcloudcontractsample/producer/ContractVerifierTest.java]
[INFO] Generated 1 test classes.
[INFO]
[INFO] --- spring-cloud-contract-maven-plugin:2.1.3.RELEASE:convert (default-convert) @ producer ---
[INFO] Will use contracts provided in the folder [/Users/uchitate/study/producer/src/test/resources/contracts]
[INFO] Copying Spring Cloud Contract Verifier contracts to [/Users/uchitate/study/producer/target/stubs/META-INF/com.b1a9idps.spring-cloud-contract-sample/producer/0.0.1-SNAPSHOT/contracts]. Only files matching [.*] pattern will end up in the final JAR with stubs.
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 2 resources
[INFO] Converting from Spring Cloud Contract Verifier contracts to WireMock stubs mappings
[INFO]      Spring Cloud Contract Verifier contracts directory: /Users/uchitate/study/producer/src/test/resources/contracts
[INFO] Stub Server stubs mappings directory: /Users/uchitate/study/producer/target/stubs/META-INF/com.b1a9idps.spring-cloud-contract-sample/producer/0.0.1-SNAPSHOT/mappings
[INFO] Creating new stub [/Users/uchitate/study/producer/target/stubs/META-INF/com.b1a9idps.spring-cloud-contract-sample/producer/0.0.1-SNAPSHOT/mappings/brand.json]
[INFO]
[INFO] --- maven-resources-plugin:3.1.0:testResources (default-testResources) @ producer ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 2 resources
[INFO] skip non existing resourceDirectory /Users/uchitate/study/producer/target/generated-test-resources/contracts
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ producer ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 2 source files to /Users/uchitate/study/producer/target/test-classes
[INFO]
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ producer ---

...
// テスト実行
...

[INFO] --- spring-cloud-contract-maven-plugin:2.1.3.RELEASE:generateStubs (default-generateStubs) @ producer ---
[INFO] Files matching this pattern will be excluded from stubs generation []
[INFO] Building jar: /Users/uchitate/study/producer/target/producer-0.0.1-SNAPSHOT-stubs.jar
[INFO]
[INFO] --- maven-jar-plugin:3.1.2:jar (default-jar) @ producer ---
[INFO] Building jar: /Users/uchitate/study/producer/target/producer-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- spring-boot-maven-plugin:2.2.0.RELEASE:repackage (repackage) @ producer ---
[INFO] Replacing main artifact with repackaged archive
[INFO]
[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ producer ---
[INFO] Installing /Users/uchitate/study/producer/target/producer-0.0.1-SNAPSHOT.jar to /Users/uchitate/.m2/repository/com/b1a9idps/spring-cloud-contract-sample/producer/0.0.1-SNAPSHOT/producer-0.0.1-SNAPSHOT.jar
[INFO] Installing /Users/uchitate/study/producer/pom.xml to /Users/uchitate/.m2/repository/com/b1a9idps/spring-cloud-contract-sample/producer/0.0.1-SNAPSHOT/producer-0.0.1-SNAPSHOT.pom
[INFO] Installing /Users/uchitate/study/producer/target/producer-0.0.1-SNAPSHOT-stubs.jar to /Users/uchitate/.m2/repository/com/b1a9idps/spring-cloud-contract-sample/producer/0.0.1-SNAPSHOT/producer-0.0.1-SNAPSHOT-stubs.jar

生成されたテストクラス(ContractVerifierTest.java

public class ContractVerifierTest extends TestBase {

    @Test
    public void validate_brand() throws Exception {
        // given:
            MockMvcRequestSpecification request = given()
                    .header("Content-Type", "application/json");

        // when:
            ResponseOptions response = given().spec(request)
                    .get("/brands");

        // then:
            assertThat(response.statusCode()).isEqualTo(200);
            assertThat(response.header("Content-Type")).isEqualTo("application/json");
        // and:
            DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
            assertThatJson(parsedJson).array("['brands']").contains("['designer']").isEqualTo("Tanita");
            assertThatJson(parsedJson).array("['brands']").contains("['name']").isEqualTo("STOF");
    }

}

Consumer

BrandServiceの実装

BrandServiceImpl.java

@Service
public class BrandServiceImpl implements BrandService {
  private final RestTemplate restTemplate;
  private final URI producerUrl;

  public BrandServiceImpl(RestTemplateBuilder restTemplateBuilder, @Value("${producer.url}") String producerUrl) {
      this.restTemplate = restTemplateBuilder.build();
      this.producerUrl = UriComponentsBuilder.fromHttpUrl(producerUrl).build().toUri();
  }

  @Override
  public BrandListDto list() {
      HttpHeaders httpHeaders = new HttpHeaders();
      httpHeaders.add(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON_VALUE);

      ResponseEntity<BrandListDto> responseEntity = restTemplate.exchange(
              producerUrl,
              HttpMethod.GET,
              new HttpEntity<>(httpHeaders),
              new ParameterizedTypeReference<>() {});
      return responseEntity.getBody();
  }
}

Spring Cloud Contractを依存関係に追加

Producer側とほぼ同じなので説明は省略します。

pom.xml

<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

<dependencyManagement>
  <dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>${spring-cloud.version}</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<build>
  <plugins>
    <plugin>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-contract-maven-plugin</artifactId>
        <version>${spring-cloud-contract-maven-plugin.verison}</version>
        <configuration>
            <testFramework>JUNIT5</testFramework>
        </configuration>
    </plugin>
  </plugins>
</build>

テストを書く

/test/resources/application.yaml

producer:
  url: http://localhost:8080/brands

BrandServiceImplTest.java

// StubRunnnerExtension.classを登録する
@ExtendWith({SpringExtension.class, StubRunnerExtension.class})
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureStubRunner(ids = {"com.b1a9idps.spring-cloud-contract-sample:producer:+:stubs:8080"}, stubsMode = StubsMode.LOCAL)
class BrandServiceImplTest {
  @Autowired
  private BrandService brandService;

  @Test
  void list() {
      Assertions.assertThat(brandService.list().getBrands())
              .extracting(Brand::getName, Brand::getDesigner)
              .containsExactly(Tuple.tuple("STOF", "Tanita"));
  }
}

@AutoConfigureStubRunner を付与してスタブ生成の対象を指定します。idsに指定する値は、 [groupId]:artifactId[:version][:classifier][:port] です。.m2レポジトリに .m2/repository/com/b1a9idps/spring-cloud-contract-sample/producer/0.0.1-SNAPSHOT/producer-0.0.1-SNAPSHOT-stubs.jar とjarがインストールされているので、 com.b1a9idps.spring-cloud-contract-sample:producer:+:stubs:8080 と指定します。バージョンを + で指定すると最新バージョンを設定します。

テストを実行

2019-11-19 00:07:15.872  INFO 24517 --- [           main] o.s.c.c.s.AetherStubDownloaderBuilder    : Will download stubs and contracts via Aether
2019-11-19 00:07:15.874  INFO 24517 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Remote repos not passed but the switch to work offline was set. Stubs will be used from your local Maven repository.
2019-11-19 00:07:15.984  INFO 24517 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Desired version is [+] - will try to resolve the latest version
2019-11-19 00:07:15.997  INFO 24517 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved version is [0.0.1-SNAPSHOT]
2019-11-19 00:07:16.007  INFO 24517 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved artifact [com.b1a9idps.spring-cloud-contract-sample:producer:jar:stubs:0.0.1-SNAPSHOT] to /Users/uchitate/.m2/repository/com/b1a9idps/spring-cloud-contract-sample/producer/0.0.1-SNAPSHOT/producer-0.0.1-SNAPSHOT-stubs.jar
2019-11-19 00:07:16.010  INFO 24517 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacking stub from JAR [URI: file:/Users/uchitate/.m2/repository/com/b1a9idps/spring-cloud-contract-sample/producer/0.0.1-SNAPSHOT/producer-0.0.1-SNAPSHOT-stubs.jar]
2019-11-19 00:07:16.016  INFO 24517 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacked file to [/var/folders/0z/n7t2zxm566zfmzbtg7qdc6_80000gp/T/contracts17319340210365511943]
2019-11-19 00:07:16.705  INFO 24517 --- [           main] wiremock.org.eclipse.jetty.util.log      : Logging initialized @4424ms
2019-11-19 00:07:16.770  INFO 24517 --- [           main] w.org.eclipse.jetty.server.Server        : jetty-9.2.z-SNAPSHOT
2019-11-19 00:07:16.784  INFO 24517 --- [           main] w.o.e.j.server.handler.ContextHandler    : Started w.o.e.j.s.ServletContextHandler@5c60c08{/__admin,null,AVAILABLE}
2019-11-19 00:07:16.785  INFO 24517 --- [           main] w.o.e.j.server.handler.ContextHandler    : Started w.o.e.j.s.ServletContextHandler@212e39ee{/,null,AVAILABLE}
2019-11-19 00:07:16.810  INFO 24517 --- [           main] w.o.e.j.s.NetworkTrafficServerConnector  : Started NetworkTrafficServerConnector@173e960b{HTTP/1.1}{0.0.0.0:8080}
2019-11-19 00:07:16.811  INFO 24517 --- [           main] w.org.eclipse.jetty.server.Server        : Started @4532ms
2019-11-19 00:07:16.812  INFO 24517 --- [           main] o.s.c.contract.stubrunner.StubServer     : Started stub server for project [com.b1a9idps.spring-cloud-contract-sample:producer:0.0.1-SNAPSHOT:stubs] on port 8080
2019-11-19 00:07:17.082  INFO 24517 --- [tp2142521143-31] /__admin                                 : RequestHandlerClass from context returned com.github.tomakehurst.wiremock.http.AdminRequestHandler. Normalized mapped under returned 'null'
2019-11-19 00:07:17.106  INFO 24517 --- [tp2142521143-31] WireMock                                 : Admin request received:
127.0.0.1 - POST /mappings

Connection: [keep-alive]
User-Agent: [Apache-HttpClient/4.5.5 (Java/13)]
Host: [localhost:8080]
Content-Length: [225]
Content-Type: [text/plain; charset=UTF-8]
{
  "id" : "a18b18d9-bf53-478a-be02-c4b05d417911",
  "request" : {
    "url" : "/ping",
    "method" : "GET"
  },
  "response" : {
    "status" : 200,
    "body" : "OK"
  },
  "uuid" : "a18b18d9-bf53-478a-be02-c4b05d417911"
}

2019-11-19 00:07:17.180  INFO 24517 --- [tp2142521143-32] WireMock                                 : Admin request received:
127.0.0.1 - POST /mappings

Connection: [keep-alive]
User-Agent: [Apache-HttpClient/4.5.5 (Java/13)]
Host: [localhost:8080]
Content-Length: [227]
Content-Type: [text/plain; charset=UTF-8]
{
  "id" : "d4c408f0-344e-4994-9118-f3844404a3fc",
  "request" : {
    "url" : "/health",
    "method" : "GET"
  },
  "response" : {
    "status" : 200,
    "body" : "OK"
  },
  "uuid" : "d4c408f0-344e-4994-9118-f3844404a3fc"
}

2019-11-19 00:07:17.246  INFO 24517 --- [tp2142521143-33] WireMock                                 : Admin request received:
127.0.0.1 - POST /mappings

Connection: [keep-alive]
User-Agent: [Apache-HttpClient/4.5.5 (Java/13)]
Host: [localhost:8080]
Content-Length: [493]
Content-Type: [text/plain; charset=UTF-8]
{
  "id" : "3e1ac4f8-4565-42b5-946a-e136e1822941",
  "request" : {
    "url" : "/brands",
    "method" : "GET",
    "headers" : {
      "Content-Type" : {
        "equalTo" : "application/json"
      }
    }
  },
  "response" : {
    "status" : 200,
    "body" : "{\"brands\":[{\"name\":\"STOF\",\"designer\":\"Tanita\"}]}",
    "headers" : {
      "Content-Type" : "application/json"
    },
    "transformers" : [ "response-template" ]
  },
  "uuid" : "3e1ac4f8-4565-42b5-946a-e136e1822941"
}

2019-11-19 00:07:17.313  INFO 24517 --- [           main] o.s.c.c.stubrunner.StubRunnerExecutor    : All stubs are now running RunningStubs [namesAndPorts={com.b1a9idps.spring-cloud-contract-sample:producer:0.0.1-SNAPSHOT:stubs=8080}]
2019-11-19 00:07:17.358  INFO 24517 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 53672 (http) with context path ''
2019-11-19 00:07:17.362  INFO 24517 --- [           main] c.b.s.c.s.impl.BrandServiceImplTest      : Started BrandServiceImplTest in 3.981 seconds (JVM running for 5.083)
2019-11-19 00:07:17.365  WARN 24517 --- [           main] o.s.c.c.stubrunner.StubRunnerFactory     : No stubs to download have been passed. Most likely you have forgotten to pass them either via annotation or a property

2019-11-19 00:07:17.735  INFO 24517 --- [tp2142521143-31] /                                        : RequestHandlerClass from context returned com.github.tomakehurst.wiremock.http.StubRequestHandler. Normalized mapped under returned 'null'
2019-11-19 00:07:17.801  INFO 24517 --- [tp2142521143-31] WireMock                                 : Request received:
127.0.0.1 - GET /brands

Connection: [keep-alive]
User-Agent: [Apache-HttpClient/4.5.9 (Java/13)]
Host: [localhost:8080]
Accept-Encoding: [gzip,deflate]
Accept: [application/json, application/*+json]
Content-Type: [application/json]

Matched response definition:
{
  "status" : 200,
  "body" : "{\"brands\":[{\"name\":\"STOF\",\"designer\":\"Tanita\"}]}",
  "headers" : {
    "Content-Type" : "application/json"
  },
  "transformers" : [ "response-template" ]
}

Response:
HTTP/1.1 200
Content-Type: [application/json]
Matched-Stub-Id: [3e1ac4f8-4565-42b5-946a-e136e1822941]

2019-11-19 00:07:17.913  WARN 24517 --- [           main] .StubRunnerWireMockTestExecutionListener : You've used fixed ports for WireMock setup - will mark context as dirty. Please use random ports, as much as possible. Your tests will be faster and more reliable and this warning will go away
2019-11-19 00:07:17.923  INFO 24517 --- [           main] w.o.e.j.s.NetworkTrafficServerConnector  : Stopped NetworkTrafficServerConnector@173e960b{HTTP/1.1}{0.0.0.0:8080}
2019-11-19 00:07:17.924  INFO 24517 --- [           main] w.o.e.j.server.handler.ContextHandler    : Stopped w.o.e.j.s.ServletContextHandler@212e39ee{/,null,UNAVAILABLE}
2019-11-19 00:07:17.924  INFO 24517 --- [           main] w.o.e.j.server.handler.ContextHandler    : Stopped w.o.e.j.s.ServletContextHandler@5c60c08{/__admin,null,UNAVAILABLE}
2019-11-19 00:07:17.925  WARN 24517 --- [           main] w.o.e.j.util.thread.QueuedThreadPool     : qtp2142521143{STOPPING,8<=8<=10,i=3,q=6} Couldn't stop Thread[qtp2142521143-28,5,]
2019-11-19 00:07:17.925  WARN 24517 --- [           main] w.o.e.j.util.thread.QueuedThreadPool     : qtp2142521143{STOPPING,8<=8<=10,i=0,q=4} Couldn't stop Thread[qtp2142521143-29,5,]
2019-11-19 00:07:17.926  INFO 24517 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
Class transformation time: 0.040030372s for 10526 classes or 3.8029994299828998E-6s per class

Process finished with exit code 0

これがテストを実行時に吐き出されるログです。テストが実行されるまでに「1. ローカルレポジトリからstubのjarをインストール」「2. 8080番ポートでモックサーバを起動」が行われています。

まとめ

Producer側では、Contractに基づいたテストが生成され、Consumer側で利用するためのスタブを生成してローカルレポジトリにインストールします。Consumer側では、生成されたスタブを使ってテストを実行します。 Spring Cloud Contractを使えば、Consumerが定義したContractをProviderが守っていることが担保できます。また、Consumerも実サービスに近いテストを行うことができます。とてもよいですね!!!

長くなりすぎたので、別記事でSpring REST Docsとの連携は書きます!

ユニットテストで@ConfigurationPropertiesが有効になるようにしたい!

ここ数年で何回かぶち当たってたことが解決したので備忘録として。内容はタイトルの通りです。 @SpringBootTest を使ってテスト書けば設定ファイルを読み込んでくれるので悩む必要はないんですけど、大人の事情でそういうことができない場合もありますよね。そうすると当然、設定ファイルを読み込んでくれないわけです。

今回、@SpringBootTest を使ってテストを書かなくても設定ファイルを読み込めるようなテストを書きました。

app.properties

app.domain=b1a9idps.com

AppProperties.java

@ConfigurationProperties("app")
public class AppProperties {
    private String domain;

    // getterとsetterは省略
}

TestConfig.java

@TestConfiguration
// @EnableConfigurationProperties は、@ConfigurationProperties が付与されたクラスに設定ファイルの値をセットする
@EnableConfigurationProperties(value = AppProperties.class)
public class TestConfig {}

ConfigurationPropertyTest.java

@ExtendWith(SpringExtension.class)
@TestPropertySource(properties = "spring.config.name=app")
// TestConfigクラスを適用して、設定ファイルを読み込む。 https://docs.spring.io/spring-boot/docs/2.2.0.RELEASE/reference/html/spring-boot-features.html#boot-features-configfileapplicationcontextinitializer-test-utility
@ContextConfiguration(classes = {TestConfig.class}, initializers = ConfigFileApplicationContextInitializer.class)
class ConfigurationPropertyTest {

    @Autowired
    private AppProperties appProperties;

    @Test
    void getValue() {
        assertEquals("b1a9idps.com", appProperties.getDomain());
    }
}

おまけ(メソッドに @ConfigurationProperties を付与している場合)

app.properties

app.app.name=uchitate

AppAppProperties.java

public class AppAppProperties {
    private String name;

    // getterとsetterは省略
}

AppAppConfig.java

@Configuration
public class AppAppConfig {

    @Bean
    @ConfigurationProperties("app.app")
    public AppAppProperties appAppProperties() {
        return new AppAppProperties();
    }
}

TestConfig.java

@TestConfiguration
// ConfigクラスをBean登録する
@Import(AppAppConfig.class)
@EnableConfigurationProperties
public class TestConfig {}

ConfigurationPropertiesTest.java

@ExtendWith(SpringExtension.class)
@TestPropertySource(properties = "spring.config.name=app")
@ContextConfiguration(classes = {TestConfig.class}, initializers = ConfigFileApplicationContextInitializer.class)
class ConfigurationPropertyTest {

    @Autowired
    private AppAppProperties appAppProperties;

    @Test
    void getValue() {
        assertEquals("uchitate", appAppProperties.getName());
    }
}

テストクラスは、AppProperties.javaのテストとほぼ同じです

今回の環境

Java11
Spring Boot 2.2.RELEASE

今回のソースコードこちら

@DataJpaTestでRepositoryのテスト

テスト書いてますか?私は好きです!嫌いな人は多いみたいですけど...

Springのテスト部分のリファレンス読んで忘れないうちにまとめよう精神が働いたので書きます。 今回は、@DataJpaTestというレポジトリのテストを書くためのアノテーションを紹介します。

@DataJpaTestアノテーションは、デフォルトでインメモリDBの設定をしたり、@EntityがついたクラスをBean登録したり、@RepositoryついたクラスをBean登録するなどSpring Data JPAレポジトリの設定してくれたりします。 @DataJpaTestのソースコードをのぞいてみるとこんな感じの設定が行われているようです。

...
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
...

Data JPAテストは、トランザクショナルで各テストの終わりにロールバックします。 もし、こうしたくない場合は、次のように書いてください。

@Transactional(propagation = Propagation.NOT_SUPPORTED)

また、data JPAテストでは、標準的なJPA EntityManagerの代わりにTestEntityManagerのBeanがインジェクトされます。

ソースコード

GitHub

pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-launcher</artifactId>
    <version>1.2.0</version>
    <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.2.0</version>
    <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>1.2.0</version>
    <scope>test</scope>
    </dependency>
</dependencies>
  • Java8
  • SpringBoot 2.0.3.RELEASE
  • H2
  • JUnit5

Brand.java

@Entity
public class Brand implements Serializable {
    enum Gender {
        MAN, WOMAN, UNISEX
    }
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    @Column(nullable = false)
    private String name;
    @Enumerated(EnumType.STRING)
    private Gender gender;
    public Brand(String name, Gender gender) {
        this.name = name;
        this.gender = gender;
    }
    ... getter, setter
}

id、ブランド名、ブランドの対象性別をもつBrandエンティティで話を進めていきます。

BrandRepository.java(今回のテスト対象)

@Repository
public interface BrandRepository extends JpaRepository<Brand, Integer> {
    List<Brand> findByGender(Gender gender);
    Integer countByGender(Gender gender);
}

性別でブランドを検索する findByGenderメソッドと性別ごとのブランド数を検索する countByGenderメソッドを準備しました。

BrandRepositoryTest.java

// SpringExtention.classは、Junit 5上でSpring TestContext Frameworkを使えるようにしている
@ExtendWith(SpringExtension.class)
class BrandRepositoryTest {

    @Nested
    @DataJpaTest
    class FindByGender {
        @Autowired
    private TestEntityManager entityManager;
        @Autowired
        private BrandRepository brandRepository;

    @BeforeEach
    void beforeEach() {
            entityManager.persist(new Brand("STOF", Gender.UNISEX));
        entityManager.persist(new Brand("ETHOSENS", Gender.MAN));
        entityManager.persist(new Brand("dulcamara", Gender.UNISEX));
    }

    @Test
    void man() {
        List<Brand> brands = brandRepository.findByGender(Gender.MAN);
        org.assertj.core.api.Assertions.assertThat(brands)
            .extracting(Brand::getName, Brand::getGender)
            .containsExactly(Tuple.tuple("ETHOSENS", Gender.MAN));
        }

    @Test
    void woman() {
            List<Brand> brands = brandRepository.findByGender(Gender.WOMAN);
        org.assertj.core.api.Assertions.assertThat(brands)
                    .hasSize(0);
    }
    }

    @Nested
    @DataJpaTest
    class CountByGender {
        @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private BrandRepository brandRepository;

    @BeforeEach
    void beforeEach() {
        entityManager.persist(new Brand("STOF", Gender.UNISEX));
        entityManager.persist(new Brand("ETHOSENS", Gender.MAN));
        entityManager.persist(new Brand("dulcamara", Gender.UNISEX));
        }

    @Test
    void man() {
        int count = brandRepository.countByGender(Gender.MAN);
        Assertions.assertEquals(1, count);
    }

    @Test
    void woman() {
        int count = brandRepository.countByGender(Gender.WOMAN);
        Assertions.assertEquals(0, count);
    }
    }
}

検証には一部AssertJを使ってます。AssertJについては、こちらで簡単に触れてます。

BrandRepositoryTest.java実行する

実行時のログを抜粋して紹介します。

...
2018-07-05 00:20:30.440  INFO 5456 --- [           main] o.s.j.d.e.EmbeddedDatabaseFactory        : Starting embedded database: url='jdbc:h2:mem:393e83d1-908e-4fb0-91d2-8becd045235b;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false', username='sa'
...

H2データベースを使ってることがわかります。

...
Hibernate: drop table brand if exists
Hibernate: drop sequence if exists hibernate_sequence
Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate: create table brand (id integer not null, gender varchar(255), name varchar(255) not null, primary key (id))
...

brandテーブルを作ってます。

...
Hibernate: insert into brand (gender, name, id) values (?, ?, ?)
Hibernate: insert into brand (gender, name, id) values (?, ?, ?)
Hibernate: insert into brand (gender, name, id) values (?, ?, ?)
...

beforeEachの処理が行われている様子です。

...
Hibernate: select count(brand0_.id) as col_0_0_ from brand brand0_ where brand0_.gender=?
...
Hibernate: select brand0_.id as id1_0_, brand0_.gender as gender2_0_, brand0_.name as name3_0_ from brand brand0_ where brand0_.gender=?
...

countByGenderとfindByGenderが実行されていることがわかります。

...
Hibernate: drop table brand if exists
Hibernate: drop sequence if exists hibernate_sequenceでテーブルとhibernate_sequenceが削除されていることがわかります。

テストが終わるとテーブルとhibernate_sequenceを削除しています。 簡単にレポジトリのテストができることがわかっていただけたかと思います。

hibernate_sequenceは最後にしか消されないので、ずっと連番が振られる続けるので、自動採番のIDの値の検証する場合はお気をつけてください。テストごとに初期データをinsertするの美しくないのでうまい方法見つけたいです。

参考

43.3.11 Auto-configured Data JPA Tests

追記

2018/7/12 create tableやdrop tableをFlywayを使って行なっているため、application.propertiesに以下のように書いているとテーブルが作成されずに「xxxテーブルがありません」みたいに怒られますので、Flywayは使えるようにしてください。

spring.flyway.enabled=false

Javaパフォーマンス本を読んで #2

第3章 Javaパフォーマンスのツールボックス

パフォーマンス分析は可視化と表裏一体です。アプリケーションの内部や、その実行環境で起こっていることを知るのがパフォーマンス分析で、可視化にはツールが不可欠です。

オペレーティングシステム付属のツールと分析

プログラムの分析の手始めになるツールは、Javaとの直接の関係はまったくなく、OSに付属している、基本的な監視のツールを利用します。 パフォーマンステストを行う際には、OSからもCPUとメモリ、ディスクの使用率は最低限収集すべきです。ネットワークを利用するプログラムでは、ネットワークの使用率も必要です。

CPUの使用率

CPUの使用率の値はある一定の期間(1秒 or 5秒 or 30秒)の平均値を表しています。CPUの使用率とは、プログラムがどの程度効率的にCPUを使えているかということを表しているので、この値が大きければ大きいほど望ましい状態になります。

ディスクの使用率

ディスクの使用率を監視することには、2つの大きな意味があります。1つ目は、アプリケーション自体に関わるものです。2つ目は、システムがスワッピングを行なっているかどうかわかるという点です。

ネットワークの使用率

ネットワークを利用するアプリケーションでは、ネットワークのトラフィックも監視しなければなりません。UNIXシステムでの基本的なネットワーク監視ツールとしては、netstatがよく使われます。

Javaの監視ツール

JVM自体に関する情報を得るには、Java向けの監視ツールが必要です。以下のようなツールがJDKに付属しています。

  • jcmd 指定されたJavaのプロセスについて、クラスやスレッドそしてJVMの基本的な情報を出力します。
  • jconsole JVMのアクティビティをグラフィカルに表現します。スレッドやクラスの利用状況、ガベージコレクションの内容などがわかります。
  • jhat メモリのヒープダンプを分析しやすい形で出力します。事後的な分析に利用されます。
  • jmap ヒープダンプや、JVMでのメモリの使用に関する情報を出力します。
  • jinfo JVMのシステムプロパティを表示します。一部のシステムプロパティについては、あたいの変更も行えます。
  • jstack Javaのプロセスのスタックをダンプします。
  • jstat ガベージコレクションやクラスローディングのアクティビティに関する情報を出力します。
  • jvisualvm JVMの監視、実行中のアプリケーションに対するプロファイリング、JVMのヒープダンプの分析のためのGUIツールです。

JVMの基本的な情報

  • 実行時間 JVMが実行されている時間を取得します。 jcmd [PID] vm.uptime
  • システムプロパティSystem.getProperties() を通じて得られるのと同じ情報が出力されます。 jcmd [PID] vm.system_properties jinfo -sysprops [PID]
  • JVMのバージョン `jcmd [PID] VM.version
  • JVMコマンドライン引数 jcmd [PID] VM.command_line
  • JVMのチューニングフラグ アプリケーションの中で有効かされているチューニングフラグは以下のようにして取得します。 jcmd [PID] VM.flags [-all]

スレッドの情報

jconsoleやjvisualvmを使うと、アプリケーション内で実行されているスレッドの数をリアルタイムに表示できます。スレッドの実行がブロックされているかどうかを知りたい場合には、スレッドスタックを確認するとよいでしょう。これにはjstackを利用します。

jstack [PID]jcmd

[PID] Thread.print

クラスの情報

アプリケーションの中で利用されているクラスの数を調べるには、jconsoleやjstatを使います。jstatではクラスのコンパイルに関する情報を得ることもできます。

ガベージコレクションの動的な分析

jconsoleはヒープの使用率をグラフとしてリアルタイムに表示でき、jcmdはガベージコレクションを実行させることができます。

プロファイリングツール

パフォーマンスアナリストにとって、プロファイラは最も重要なツールの1つです。 プロファイラはソケットなどの通信手段を使って対象のアプリケーションに接続し、アプリケーションのふるまいに関する情報を取得します。したがって、プロファイリング対象のアプリケーションと同じくらいプロファイラ自身についてもチューニングを行わなければなりません。

Java Mission Control

商用版のJava 7(7u40以降)とJava 8には、Java Mission Controlと呼ばれる新しい監視と制御の仕組みが含まれています。利用には商用ライセンスが必要です。 Java Mission Controlのプログラム(jmc)を起動すると、マシン上でのJVMのプロセスがウインドウ内に表示されます。