Homebrewで過去のversionをインストールする

Homebrewで過去のversionのpackageをインストールしたいときのメモ。

以前はGitHubのcommit URLを指定してbrew installできていたが、この方法は使えなくなったので、代わりにbrew extractコマンドを使う必要がある。

手順

自分のtapがない場合は作成する

% brew tap-new hirakida/tap

インストールしたいversionのpackageをextractする
今回はcmakeの3.19.7をインストールする

% brew extract cmake hirakida/tap --version 3.19.7

extractしたversionをインストールする

% brew install hirakida/tap/cmake@3.19.7

最新のversionがリンクされている場合は、インストールしたversionをリンクする

% brew unlink cmake
% brew link cmake@3.19.7

% cmake --version
cmake version 3.19.7

参考

https://stackoverflow.com/questions/3987683/homebrew-install-specific-version-of-formula
https://docs.brew.sh/Tips-N'-Tricks
https://docs.brew.sh/Manpage

JDK Flight Recorderのイベントを作る

JDK Flight Recorder (JFR) のイベントを作ったときのメモ。

ソースコード

import java.util.concurrent.TimeUnit;

import jdk.jfr.Category;
import jdk.jfr.Description;
import jdk.jfr.Event;
import jdk.jfr.Label;
import jdk.jfr.Name;

public class HelloMain {

    @Name("hirakida.Hello")
    @Label("Hello")
    @Category("JFR Demo")
    @Description("Hello Event")
    static class HelloEvent extends Event {
        @Label("Message")
        String message;
    }

    public static void main(String[] args) throws Exception {
        while (true) {
            HelloEvent event = new HelloEvent();
            event.message = "Hello!";
            event.begin();
            // 確認用に1秒sleep
            TimeUnit.SECONDS.sleep(1);
            event.commit();
        }
    }
}

手順

今回はAdoptOpenJDK 15を使う。

% sdk use java 15.0.2.hs-adpt  

Flight Recorderを有効にしてhello.jfrに記録する。

% java -XX:StartFlightRecording:filename=hello.jfr HelloMain.java 

確認

JDK Mission Control (JMC)

JMCでhello.jfrを開く。
https://adoptopenjdk.net/jmc.html

JMCのイベント・ブラウザで、追加したHelloイベントが確認できる。

f:id:hirakida29:20210124182810p:plain

jfrコマンド

JDK 12から追加されたjfrコマンドでも確認できる。

% jfr print --events Hello hello.jfr
hirakida.Hello {
  startTime = 01:13:44.276
  duration = 1.00 s
  message = "Hello!"
  eventThread = "main" (javaThreadId = 1)
  stackTrace = [
    com.example.HelloMain.main(String[]) line: 28
    jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Method, Object, Object[])
    jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Object, Object[]) line: 64
    jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 43
    java.lang.reflect.Method.invoke(Object, Object[]) line: 564
    ...
  ]
}

...

jsonで表示する場合。

% jfr print --json --events Hello hello.jfr
{
  "recording": {
    "events": [{
      "type": "hirakida.Hello", 
      "values": {
        "startTime": "2021-01-26T01:13:44.276284035+09:00", 
        "duration": "PT1.000242742S", 
        "eventThread": {
          "osName": "main", 
          "osThreadId": 9475, 
          "javaName": "main", 
          "javaThreadId": 1, 
          "group": {
            "parent": {
              "parent": null, 
              "name": "system"
            }, 
            "name": "main"
          }
        }, 
...

参考

https://www.youtube.com/watch?v=plYESjZ12hM
https://docs.oracle.com/en/java/javase/15/docs/api/jdk.jfr/jdk/jfr/Event.html

Spring BootのFlightRecorderApplicationStartupを試す

Spring Boot 2.4 (Spring Framework 5.3) から追加されたFlightRecorderApplicationStartupを試してみた。

ソースコード

plugins {
    id 'org.springframework.boot' version '2.4.2'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
}

確認用にダミーのBeanの生成に5秒sleepを入れる。

@SpringBootApplication
public class DemoApplication implements CommandLineRunner {

    public static class Foo {}

    @Bean
    public Foo foo() {
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (Exception ignored) { }
        return new Foo();
    }

    @Override
    public void run(String... args) {
        System.out.println("Hello!");
    }

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(DemoApplication.class);
        app.setApplicationStartup(new FlightRecorderApplicationStartup());
        app.run(args);
    }
}

jfrファイルを作成

今回はAdoptOpenJDK 11を使う。

% sdk use java 11.0.10.hs-adpt

Flight Recorderを有効にしてデモアプリを実行する。(パラメータはとりあえずマニュアルと同じ)

% java -XX:StartFlightRecording:filename=recording.jfr,duration=10s -jar build/libs/demo-0.0.1-SNAPSHOT.jar

実行すると、カレントディレクトリにrecoording.jfrが生成される。

JDK Mission Control (JMC) で確認

AdoptOpenJDKのJMCを使う。
https://adoptopenjdk.net/jmc.html

JMCを起動して、recoording.jfrを開く。

イベント・ブラウザの「Spring Application」カテゴリの「Startup Step」イベントを見ると、foo beanのspring.beans.instantiateに5秒かかっているのが確認できた。

f:id:hirakida29:20210123213430p:plain

参考

https://docs.spring.io/spring-boot/docs/2.4.x/reference/htmlsingle/#boot-features-application-startup-tracking
https://github.com/spring-projects/spring-framework/issues/24878
https://www.youtube.com/watch?t=1091s&v=lgyO9C9zdrg&feature=youtu.be

ソースコードはこの辺り
https://github.com/spring-projects/spring-framework/blob/v5.3.3/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java#L303
https://github.com/spring-projects/spring-framework/blob/v5.3.3/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderApplicationStartup.java
https://github.com/spring-projects/spring-framework/blob/v5.3.3/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupStep.java
https://github.com/spring-projects/spring-framework/blob/v5.3.3/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupEvent.java

Spring BootでRedis Cacheのmetricsを収集する

Spring Boot 2.4からRedis Cacheのmetricsが収集できるようになったので試してみた。

ソースコード

build.gradle

plugins {
    id 'org.springframework.boot' version '2.4.1'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-cache'
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'io.micrometer:micrometer-registry-prometheus'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}

Java

@SpringBootApplication
@EnableCaching
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
@RestController
public class HelloController {
    private final HelloService helloService;

    public HelloController(HelloService helloService) {
        this.helloService = helloService;
    }

    @GetMapping("/hello")
    public String hello() {
        return helloService.hello();
    }
}
@Service
public class HelloService {
    @Cacheable(cacheNames = "cache1")
    public String hello() {
        return "Hello!";
    }
}

application.yml

Redisはデフォルトのlocalhost:6379を使う。

spring:
  cache:
    cache-names: cache1
    redis:
      enable-statistics: true
management:
  endpoints:
    web:
      exposure:
        include: prometheus

動作確認

/helloに数回アクセスした後でSpring Boot Actuatorのprometheus endpointを参照すると、cacheのhitやmissなどのmetricsが確認できた。

http://localhost:8080/actuator/prometheus

# HELP cache_puts_total The number of entries added to the cache
# TYPE cache_puts_total counter
cache_puts_total{cache="cache1",cacheManager="cacheManager",name="cache1",} 1.0
# HELP cache_gets_total the number of times cache lookup methods have returned an uncached (newly loaded) value, or null
# TYPE cache_gets_total counter
cache_gets_total{cache="cache1",cacheManager="cacheManager",name="cache1",result="miss",} 1.0
cache_gets_total{cache="cache1",cacheManager="cacheManager",name="cache1",result="pending",} 0.0
cache_gets_total{cache="cache1",cacheManager="cacheManager",name="cache1",result="hit",} 6.0

参考

https://github.com/spring-projects/spring-boot/issues/22701
https://github.com/spring-projects/spring-data-redis/issues/961
https://docs.spring.io/spring-boot/docs/2.4.x/reference/html/production-ready-features.html#production-ready-metrics-cache

JITWatchを試す2

前回はJITWatchのmakeDemoLogFile.shを使ってHotSpotログファイルを作成したので、今回はソースコードを書いて試してみる。
手順は、ログファイルの作成以外は前回と同じなので割愛。
https://hirakida29.hatenablog.com/entry/2020/06/06/235407

ソースコード

build.gradle

plugins {
    id 'java'
    id 'application'
}

group 'com.example'
version '1.0-SNAPSHOT'
sourceCompatibility = JavaVersion.VERSION_11

mainClassName = 'com.example.Main'

run {
    jvmArgs = ['-XX:+UnlockDiagnosticVMOptions', '-XX:+TraceClassLoading', '-XX:+LogCompilation', '-XX:+PrintAssembly']
}

Main.java

デモアプリの一部を抜粋 https://github.com/AdoptOpenJDK/jitwatch/blob/master/core/src/main/java/org/adoptopenjdk/jitwatch/demo/MakeHotSpotLog.java

package com.example;

public class Main {

    public Main(int iterations) {
        addVariable(iterations);
    }

    private void addVariable(int iterations) {
        long count = 0;
        for (int i = 0; i < iterations; i++) {
            count = add(count, i);
        }
        System.out.println(count);
    }

    private long add(long a, long b) {
        return a + b;
    }

    public static void main(String[] args) throws Exception {
        new Main(1_000_000);
    }
}

HotSpotログファイルを作成

% ./gradlew run

JITWatchで解析

f:id:hirakida29:20201129161041p:plain

f:id:hirakida29:20201129161102p:plain

JUnit5とSpockを同じプロジェクトで使う

JUnit5の環境でSpockを使いたいときのメモ。

ソースコード

plugins {
    id 'org.springframework.boot' version '2.4.5'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
    id 'groovy'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = JavaVersion.VERSION_11

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.vintage:junit-vintage-engine'
    testImplementation 'org.spockframework:spock-core:1.3-groovy-2.5'
    testImplementation 'org.spockframework:spock-spring:1.3-groovy-2.5'
    testImplementation 'org.codehaus.groovy:groovy-all:2.5.14'
}

test {
    useJUnitPlatform()
    testLogging {
        showStandardStreams = true
    }
}
@RestController
public class HelloController {
    @GetMapping("/hello")
    public Map<String, String> hello() {
        return Map.of("message", "Hello!");
    }
}

テストコード

@WebMvcTest(controllers = HelloController.class)
class HelloControllerTest extends Specification {
    @Autowired
    MockMvc mvc

    def "hello"() {
        expect:
        mvc.perform(get("/hello"))
                .andExpect(status().isOk())
                .andExpect(content().json("""
                    {
                        "message":"Hello!"
                    }
               """))
    }
}

参考

unit testing - Can we run Spock Testcases and Junit 5 test cases together In one project? - Stack Overflow

Spring Data RedisのテストにTestcontainersを使う

Spring Data RedisのRedisTemplateのテストにTestcontainersを使いたいときのメモ。

ソースコード

plugins {
    id 'org.springframework.boot' version '2.4.5'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = JavaVersion.VERSION_11

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation platform('org.testcontainers:testcontainers-bom:1.15.3')
    testImplementation 'org.testcontainers:testcontainers'
    testImplementation 'org.testcontainers:junit-jupiter'
}

test {
    useJUnitPlatform()
}
@Component
public class RedisClient {
    private final StringRedisTemplate redisTemplate;

    public RedisClient(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public String get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    public void set(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }
}

テストコード

@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@Testcontainers
public class RedisClientTest {
    @Container
    private static final GenericContainer<?> CONTAINER =
        new GenericContainer<>("redis:6.2").withExposedPorts(6379);
    @Autowired
    private RedisClient client;

    @DynamicPropertySource
    static void redisProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.redis.port", () -> CONTAINER.getMappedPort(6379));
    }

    @BeforeEach
    void init() {
        client.set("key1", "value1");
    }

    @Test
    public void get() {
        String result = client.get("key1");
        assertNotNull(result);
        assertEquals("value1", result);
    }
}

参考

@DynamicPropertySource in Spring Framework 5.2.5 and Spring Boot 2.2.6
Networking and communicating with containers - Testcontainers
https://github.com/testcontainers/testcontainers-java/tree/master/examples/spring-boot/src/test/java/com/example