2026年6月10日水曜日

SpringBoot で actuator を有効にアプリのプロファイリングをする

SpringBoot で actuator を有効にアプリのプロファイリングをする

概要

spring-boot-starter-actuator を使います

環境

  • macOS 26.4.1
  • openjdk 26.0.1
  • SpringBoot 4.0.6
    • jasypt-spring-boot 4.0.4
    • jackson-databind 2.21.2
    • jobrunr 8.6.1
  • gradle 9.5.1
  • VSCode 1.121.0
  • MySQL 9.6.0
  • Redis 8.6.3

インストール

dependencies に以下を追記しましょう

  • vim build.gradle
dependencies {
       implementation 'org.springframework.boot:spring-boot-starter-actuator'
}

有効化

プロパティファイルを編集するだけです
既存の application.properties に以下を追記しましょう

  • vim src/main/resources/application.properties
# Spring Boot Actuator
management.endpoints.web.exposure.include=health,info,metrics,env,beans,mappings,loggers,threaddump
management.endpoint.health.show-details=always

動作確認

  • ./gradlew clean && ./gradlew bootRun --args='--jasypt.encryptor.password=xxx'

以下のエンドポイントが使えるようになります

# ヘルスチェック(DB/Redis含む接続状態)
curl http://localhost:8080/actuator/health

# メトリクス一覧
curl http://localhost:8080/actuator/metrics
# 個別メトリクス例: JVMヒープ使用量
curl http://localhost:8080/actuator/metrics/jvm.memory.used
# 個別メトリクス例: HTTPリクエスト統計
curl "http://localhost:8080/actuator/metrics/http.server.requests"

# 環境変数・設定値一覧(パスワード等はマスクされる)
curl http://localhost:8080/actuator/env

# 登録済みSpring Bean一覧
curl http://localhost:8080/actuator/beans

# 登録済みHTTPルート一覧
curl http://localhost:8080/actuator/mappings

# ロガーレベル一覧 / 動的変更
curl http://localhost:8080/actuator/loggers
curl -X POST http://localhost:8080/actuator/loggers/com.example.demo \
  -H 'Content-Type: application/json' \
  -d '{"configuredLevel":"DEBUG"}'

# スレッドダンプ
curl http://localhost:8080/actuator/threaddump

最後に

SpringBoot の actuator を有効にし様々なメトリイックスを取得する方法を紹介しました
Spring コンテナないの Bean 情報なども取得できます

Prometheus 専用のメトリックスはデフォルトではないので https://mvnrepository.com/artifact/io.micrometer/micrometer-registry-prometheus を追加する必要があります

2026年6月9日火曜日

SpringBoot のエラーハンドリングで500エラー時にトレースバックを表示する方法

SpringBoot のエラーハンドリングで500エラー時にトレースバックを表示する方法

概要

これまでカスタムエラーを作成しそれをハンドリングし返却できるようにしました
今回はそれにトレースバックを表示する方法を紹介します

環境

  • macOS 26.4.1
  • openjdk 26.0.1
  • SpringBoot 4.0.6
    • jasypt-spring-boot 4.0.4
    • jackson-databind 2.21.2
    • jobrunr 8.6.1
  • gradle 9.5.1
  • VSCode 1.121.0
  • MySQL 9.6.0
  • Redis 8.6.3

エラーハンドラの修正

各種エラーをハンドリングした際に Exception 情報は受け取れるのでそこからスタックトレース情報を取得します
今回は dev の場合だけレスポンスにも trace 情報を含めるようにしていますがこの機能は不要であれば削除して OK です

@RestControllerAdvice で定義したエラーハンドラのクラスでは引数で Environment が受け取れるのでこれを元にデプロイ環境を判断しています

  • vim src/main/java/com/example/demo/exception/GlobalExceptionHandler.java
package com.example.demo.exception;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.LocalDateTime;
import java.util.Arrays;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import com.example.demo.exception.dto.ErrorResponse;

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

@RestControllerAdvice
public class GlobalExceptionHandler {

        private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
        private final Environment environment;

        public GlobalExceptionHandler(Environment environment) {
                this.environment = environment;
        }

        @ExceptionHandler(BusinessException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public ErrorResponse handleBusinessException(BusinessException ex) {
                return new ErrorResponse(ex.getCode(), ex.getMessage(), LocalDateTime.now());
        }

        @ExceptionHandler(Exception.class)
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        public ErrorResponse handleException(Exception ex) {
                log.error("Unexpected internal server error", ex);
                String trace = isDevProfile() ? getStackTrace(ex) : null;
                return new ErrorResponse("INTERNAL_SERVER_ERROR", "予期しないエラーが発生しました", LocalDateTime.now(), trace);
        }

        private boolean isDevProfile() {
                return Arrays.stream(environment.getActiveProfiles()).anyMatch("dev"::equalsIgnoreCase);
        }

        private String getStackTrace(Exception ex) {
                StringWriter sw = new StringWriter();
                PrintWriter pw = new PrintWriter(sw);
                ex.printStackTrace(pw);
                pw.flush();
                return sw.toString();
        }
}

エラーレスポンス用の DTO の修正

レスポンスに trace 情報を含めたい場合は追加してください
不要な場合はこちらの修正は不要です

  • vim src/main/java/com/example/demo/exception/dto/ErrorResponse.java
package com.example.demo.exception.dto;

import java.time.LocalDateTime;

public class ErrorResponse {

        private String code;
        private String message;
        private LocalDateTime timestamp;
        private String trace;

        public ErrorResponse(String code, String message, LocalDateTime timestamp) {
                this(code, message, timestamp, null);
        }

        public ErrorResponse(String code, String message, LocalDateTime timestamp, String trace) {
                this.code = code;
                this.message = message;
                this.timestamp = timestamp;
                this.trace = trace;
        }

        public String getCode() {
                return code;
        }

        public String getMessage() {
                return message;
        }

        public LocalDateTime getTimestamp() {
                return timestamp;
        }

        public String getTrace() {
                return trace;
        }
}

動作確認

  • ./gradlew clean && ./gradlew bootRun --args='--jasypt.encryptor.password=xxx'

で500エラーを発生させトレースバックが表示されることを確認しましょう

最後に

ハンドリングできている40x系のエラーはトレース不要なケースが多いですが500系のエラーは基本ハンドリングできていない予期せぬエラーの場合が多いのでその場合はトレースバックをちゃんと出力させましょう

2026年6月8日月曜日

SpringBoot のダッシュボードと本体の Web アプリを分離する方法

SpringBoot のダッシュボードと本体の Web アプリを分離する方法

概要

前回 Web アプリとワーカーを分離しました
更にダッシュボードを追加しましたが Web アプリと同じプロセスで起動するのでセキュリティ的に微妙でした
今回はダッシュボードアプリと Web アプリのプロセスを分離する方法を紹介します

環境

  • macOS 26.4.1
  • openjdk 26.0.1
  • SpringBoot 4.0.6
    • jasypt-spring-boot 4.0.4
    • jackson-databind 2.21.2
    • jobrunr 8.6.1
  • gradle 9.5.1
  • VSCode 1.121.0
  • MySQL 9.6.0
  • Redis 8.6.3

ダッシュボード用アプリの作成

JobRunr の設定が必要なので @Import で取り込みます
JobRunrConfig はこちらです

  • vim src/main/java/com/example/demo/DashboardApplication.java
package com.example.demo;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import com.example.demo.config.JobRunrConfig;
import com.example.demo.config.JobRunrDashboardProperties;
import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties;

@EnableEncryptableProperties
@Configuration
@EnableAutoConfiguration
@ConfigurationPropertiesScan(basePackageClasses = JobRunrDashboardProperties.class)
@Import(JobRunrConfig.class)
public class DashboardApplication {

        public static void main(String[] args) {
                new SpringApplicationBuilder(DashboardApplication.class).profiles("dashboard").run(args);
        }
}

ダッシュボード用設定の追加

Web アプリとしての設定とワーカーとしてのはオフにしダッシュボードだけ有効にします

  • vim src/main/resources/application-dashboard.properties
spring.main.web-application-type=none

app.jobrunr.background-job-server.enabled=false
app.jobrunr.dashboard.enabled=true
app.jobrunr.dashboard.port=8000

Web アプリケーションとワーカーの設定は以下の通りです

  • vim src/main/resources/application.properties
spring.application.name=demo
# update はアプリケーション起動時に、Entityに対応するテーブルがなければ作成します
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

spring.datasource.url=jdbc:mysql://${MYSQL_HOST:127.0.0.1}:3306/mydatabase
spring.datasource.username=ENC(bFMupckSRHJ/9QmXd1EVTw==)
spring.datasource.password=ENC(QFCt95vHHcJpgm8oKhghkJoEknQv3tF0)

spring.data.redis.host=localhost
spring.data.redis.port=6379

# Web起動時はJobRunrのBackgroundJobServerを無効化(worker jarで有効化)
app.jobrunr.background-job-server.enabled=false

# JobRunr Dashboard (管理画面)
app.jobrunr.dashboard.enabled=false
app.jobrunr.dashboard.port=8000

# Jasypt 暗号化キーの設定(環境変数から取得)
jasypt.encryptor.algorithm=PBEWithMD5AndDES
jasypt.encryptor.iv-generator-classname=org.jasypt.iv.NoIvGenerator
jasypt.encryptor.password=${JASYPT_PASSWORD:default_password}
  • vim src/main/resources/application-worker.properties
spring.main.web-application-type=none

app.jobrunr.background-job-server.enabled=true
app.jobrunr.dashboard.enabled=false

build.gradle の修正

ダッシュボードアプリを起動するタスクとダッシュボード用の jar を作成するタスクを追加します

  • vim build.gradle
tasks.register('dashboardBootRun', org.springframework.boot.gradle.tasks.run.BootRun) {
       group = 'application'
       description = 'Runs the JobRunr dashboard process using DashboardApplication.'
       mainClass = 'com.example.demo.DashboardApplication'
       classpath = sourceSets.main.runtimeClasspath
}

tasks.register('dashboardBootJar', org.springframework.boot.gradle.tasks.bundling.BootJar) {
       group = 'build'
       description = 'Builds the executable dashboard jar.'
       archiveClassifier = 'dashboard'
       mainClass = 'com.example.demo.DashboardApplication'
       classpath = sourceSets.main.runtimeClasspath
       targetJavaVersion = webBootJar.get().targetJavaVersion
       dependsOn tasks.named('classes')
}

tasks.named('assemble') {
       dependsOn tasks.named('workerBootJar')
       dependsOn tasks.named('workerBootJar'), tasks.named('dashboardBootJar')
}

動作確認

  • ./gradlew clean && ./gradlew dashboardBootRun -Dspring-boot.run.mainClass=com.example.demo.DashboardApplication --args='--jasypt.encryptor.password=asdfghjkl'

で localhost:8000/dashboard にアクセスすると JobRunr のダッシュボードにアクセスできます

jar は JASYPT_PASSWORD="xxx" ./gradlew clean build で作成します

  • JASYPT_PASSWORD=asdfghjkl java -jar build/libs/demo-0.0.1-SNAPSHOT-dashboard.jar

で実行します

最後に

SpringBoot のダッシュボード機能を Web アプリケーション本体と分離する方法を紹介しました
MySQL の設定を変更するだけで他のホストでも変更できるので基本はアプリのサーバとは別のサーバで動作させるのがいいでしょう

2026年6月6日土曜日

SpringBoot + JobRunr で JobRunr の管理画面を追加する方法

SpringBoot + JobRunr で JobRunr の管理画面を追加する方法

概要

JobRunr にはデフォルトでキューやジョブ情報を確認するための管理画面機能があるのでそれを追加する方法を紹介します

環境

  • macOS 26.4.1
  • openjdk 26.0.1
  • SpringBoot 4.0.6
    • jasypt-spring-boot 4.0.4
    • jackson-databind 2.21.2
    • jobrunr 8.6.1
  • gradle 9.5.1
  • VSCode 1.121.0
  • MySQL 9.6.0
  • Redis 8.6.3

管理画面の設定

まずはプロパティファイルに管理画面の設定情報を記載します
今回追加したのは app.jobrunr.dashboard.enabled=trueapp.jobrunr.dashboard.port=8000 です

  • vim src/main/resources/application.properties
spring.application.name=demo
# update はアプリケーション起動時に、Entityに対応するテーブルがなければ作成します
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

spring.datasource.url=jdbc:mysql://${MYSQL_HOST:127.0.0.1}:3306/mydatabase
spring.datasource.username=ENC(bFMupckSRHJ/9QmXd1EVTw==)
spring.datasource.password=ENC(QFCt95vHHcJpgm8oKhghkJoEknQv3tF0)

spring.data.redis.host=localhost
spring.data.redis.port=6379

# Web起動時はJobRunrのBackgroundJobServerを無効化(worker jarで有効化)
app.jobrunr.background-job-server.enabled=false

# JobRunr Dashboard (管理画面)
app.jobrunr.dashboard.enabled=true
app.jobrunr.dashboard.port=8000

# Jasypt 暗号化キーの設定(環境変数から取得)
jasypt.encryptor.algorithm=PBEWithMD5AndDES
jasypt.encryptor.iv-generator-classname=org.jasypt.iv.NoIvGenerator
jasypt.encryptor.password=${JASYPT_PASSWORD:default_password}

またワーカー側の設定ファイルではそれらを無効にする設定を記載します

  • vim src/main/resources/application-worker.properties
spring.main.web-application-type=none

app.jobrunr.background-job-server.enabled=true
app.jobrunr.dashboard.enabled=false

管理画面の設定値を Bean に追加

先ほどプロパティファイルに追加した値を Bean に追加するためにプロパティファイルの値を管理するクラスを作成します

前回も説明しましたが Bean ではプロパティファイルの値を自動で読み込んでくれないためクラスを準備する必要があります

  • vim src/main/java/com/example/demo/config/JobRunrDashboardProperties.java
package com.example.demo.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "app.jobrunr.dashboard")
public class JobRunrDashboardProperties {

        private boolean enabled = true;
        private int port = 8000;

        public boolean isEnabled() {
                return enabled;
        }

        public void setEnabled(boolean enabled) {
                this.enabled = enabled;
        }

        public int getPort() {
                return port;
        }

        public void setPort(int port) {
                this.port = port;
        }
}

管理画面の有効化

先ほど追加したプロパティの値を取得するクラスを使ってダッシュボードを有効化します

JobRunr の設定は Bean として登録してあるので先程のクラスを使う必要があります

有効化する実体は jobRunrConfiguration.useDashboard でここにプロパティの値を渡します

  • vim src/main/java/com/example/demo/config/JobRunrConfig.java
package com.example.demo.config;

import javax.sql.DataSource;

import org.jobrunr.configuration.JobRunr;
import org.jobrunr.scheduling.JobScheduler;
import org.jobrunr.server.JobActivator;
import org.jobrunr.storage.StorageProvider;
import org.jobrunr.storage.StorageProviderUtils.DatabaseOptions;
import org.jobrunr.storage.sql.mysql.MySqlStorageProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JobRunrConfig {

        @Bean
        StorageProvider storageProvider(DataSource dataSource) {
                return new MySqlStorageProvider(dataSource, DatabaseOptions.CREATE);
        }

        @Bean
        JobActivator jobActivator(ApplicationContext applicationContext) {
                return applicationContext::getBean;
        }

        @Bean
        JobScheduler jobScheduler(StorageProvider storageProvider, JobActivator jobActivator,
                        JobRunrBackgroundJobServerProperties jobRunrBackgroundJobServerProperties,
                        JobRunrDashboardProperties jobRunrDashboardProperties) {
                var jobRunrConfiguration = JobRunr.configure().useStorageProvider(storageProvider).useJobActivator(jobActivator)
                                .useBackgroundJobServerIf(jobRunrBackgroundJobServerProperties.isEnabled());

                if (jobRunrDashboardProperties.isEnabled()) {
                        jobRunrConfiguration.useDashboard(jobRunrDashboardProperties.getPort());
                }

                return jobRunrConfiguration.initialize().getJobScheduler();
        }
}

動作確認

localhost:8000/dashboard にアクセスすると管理画面にアクセスできます

最後に

JobRunr の管理画面を追加する方法を紹介しました
このままだと Web アプリケーションの本体に管理画面が付属してしまい予期せぬアクセスが発生する場合があるので本来はダッシュボードのアプリだけを切り出して別プロセスで起動するのが定石です

次回はダッシュボードを分離する方法を紹介します

参考サイト

2026年6月5日金曜日

SpringBoot + JobRunr で Web アプリとワーカープロセスを分離する方法

SpringBoot + JobRunr で Web アプリとワーカープロセスを分離する方法

概要

これまでは同じプロセス内で Web アプリと JobRunr ワーカーを起動させていました
これだとワーカーだけをスケールアウトさせたい場合に不便です
今回は Web アプリとワーカーを別々のプロセスで起動する方法を紹介します

環境

  • macOS 26.4.1
  • openjdk 26.0.1
  • SpringBoot 4.0.6
    • jasypt-spring-boot 4.0.4
    • jackson-databind 2.21.2
    • jobrunr 8.6.1
  • gradle 9.5.1
  • VSCode 1.121.0
  • MySQL 9.6.0
  • Redis 8.6.3

Web アプリケーション

Web アプリ専用のアプリケーションを作成します
こちらは SpringBoot が必要なので @SpringBootApplication にします

また専用のプロパティ (application.properties と application-web.properties) を読み込むので profiles に web を指定します

  • vim src/main/java/com/example/demo/AccessingDataMysqlApplication.java
package com.example.demo;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties;

@EnableEncryptableProperties
@ConfigurationPropertiesScan
@SpringBootApplication
public class AccessingDataMysqlApplication {

        public static void main(String[] args) {
                new SpringApplicationBuilder(AccessingDataMysqlApplication.class).profiles("web").run(args);
        }

}

Web アプリケーション用設定ファイル

今回は application.properties だけを読み込むので application-web.properties は作成しなくて OK です

もし Web アプリにだけ必要な設定や application.properties の設定を上書きしたい場合には application-web.properties を作成して設定しましょう

  • vim src/main/resources/application.properties
spring.application.name=demo
# update はアプリケーション起動時に、Entityに対応するテーブルがなければ作成します
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

spring.datasource.url=jdbc:mysql://${MYSQL_HOST:127.0.0.1}:3306/mydatabase
spring.datasource.username=ENC(bFMupckSRHJ/9QmXd1EVTw==)
spring.datasource.password=ENC(QFCt95vHHcJpgm8oKhghkJoEknQv3tF0)

spring.data.redis.host=localhost
spring.data.redis.port=6379

# Web起動時はJobRunrのBackgroundJobServerを無効化(worker jarで有効化)
app.jobrunr.background-job-server.enabled=false

# Jasypt 暗号化キーの設定(環境変数から取得)
jasypt.encryptor.algorithm=PBEWithMD5AndDES
jasypt.encryptor.iv-generator-classname=org.jasypt.iv.NoIvGenerator
jasypt.encryptor.password=${JASYPT_PASSWORD:default_password}

ワーカーアプリケーション

@SpringBootApplication は不要です
JobRunr として動かすために SpringApplicationBuilder を使い定義します
こうすることで SpringBoot の Bean 機能などが使えます

profiles には worker を指定し application-worker.properties を使えるようにします

  • vim src/main/java/com/example/demo/WorkerApplication.java
package com.example.demo;

import org.springframework.boot.builder.SpringApplicationBuilder;
public class WorkerApplication {

        public static void main(String[] args) {
                new SpringApplicationBuilder(AccessingDataMysqlApplication.class).profiles("worker").run(args);
        }
}

ワーカーアプリケーション用設定ファイル

ワーカー専用の設定ファイルを作成します
Web アプリとして動作しないための設定を記載します
application.properties の設定を上書きします

  • vim src/main/resources/application-worker.properties
spring.main.web-application-type=none

app.jobrunr.background-job-server.enabled=true

Bean に設定を注入するためのクラス

application-worker.properties の値は自動的に読み込まれます
しかし Bean には読み込まれないのでプロパティファイルから設定を取得し Bean に注入します

  • vim src/main/java/com/example/demo/config/JobRunrBackgroundJobServerProperties.java
package com.example.demo.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "app.jobrunr.background-job-server")
public class JobRunrBackgroundJobServerProperties {

        private boolean enabled = false;

        public boolean isEnabled() {
                return enabled;
        }

        public void setEnabled(boolean enabled) {
                this.enabled = enabled;
        }
}

そしてこの設定を使って JobRunr の設定 (Bean) を作成します

  • vim src/main/java/com/example/demo/config/JobRunrConfig.java
package com.example.demo.config;

import javax.sql.DataSource;

import org.jobrunr.configuration.JobRunr;
import org.jobrunr.scheduling.JobScheduler;
import org.jobrunr.server.JobActivator;
import org.jobrunr.storage.StorageProvider;
import org.jobrunr.storage.StorageProviderUtils.DatabaseOptions;
import org.jobrunr.storage.sql.mysql.MySqlStorageProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JobRunrConfig {

        @Bean
        StorageProvider storageProvider(DataSource dataSource) {
                return new MySqlStorageProvider(dataSource, DatabaseOptions.CREATE);
        }

        @Bean
        JobActivator jobActivator(ApplicationContext applicationContext) {
                return applicationContext::getBean;
        }

        @Bean
        JobScheduler jobScheduler(StorageProvider storageProvider, JobActivator jobActivator,
                        JobRunrBackgroundJobServerProperties jobRunrBackgroundJobServerProperties) {
                return JobRunr.configure().useStorageProvider(storageProvider).useJobActivator(jobActivator)
                                .useBackgroundJobServerIf(jobRunrBackgroundJobServerProperties.isEnabled()).initialize()
                                .getJobScheduler();
        }
}

サービスなどの設定

これは前回と同様です

build.gradle の修正

以下の設定を追記します
Web アプリケーションだけを起動する設定とビルドする設定、そしてワーカーアプリケーションだけを起動しビルドする設定を追加します

  • vim build.gradle
tasks.named('bootJar') {
       archiveClassifier = 'web'
       mainClass = 'com.example.demo.AccessingDataMysqlApplication'
}

tasks.named('bootRun', org.springframework.boot.gradle.tasks.run.BootRun) {
       mainClass = 'com.example.demo.AccessingDataMysqlApplication'
}

def webBootJar = tasks.named('bootJar', org.springframework.boot.gradle.tasks.bundling.BootJar)

tasks.register('workerBootRun', org.springframework.boot.gradle.tasks.run.BootRun) {
       group = 'application'
       description = 'Runs the worker process (non-web) using WorkerApplication.'
       mainClass = 'com.example.demo.WorkerApplication'
       classpath = sourceSets.main.runtimeClasspath
}

tasks.register('workerBootJar', org.springframework.boot.gradle.tasks.bundling.BootJar) {
       group = 'build'
       description = 'Builds the executable worker jar.'
       archiveClassifier = 'worker'
       mainClass = 'com.example.demo.WorkerApplication'
       classpath = sourceSets.main.runtimeClasspath
       targetJavaVersion = webBootJar.get().targetJavaVersion
       dependsOn tasks.named('classes')
}

tasks.named('assemble') {
       dependsOn tasks.named('workerBootJar')
}

動作確認

  • ./gradlew clean && ./gradlew bootRun --args='--jasypt.encryptor.password=xxx'
  • ./gradlew clean && ./gradlew workerBootRun -Dspring-boot.run.mainClass=com.example.demo.WorkerApplication --args='--jasypt.encryptor.password=xxx'

Web アプリケーション、ワーカーアプリケーションそれぞれのプロセスで立ち上げます
これで curl で動作確認するとワーカー側のロギング処理がワーカー側の起動したターミナルだけに表示されることが確認できます

ビルド

jar を作成するには以下の通りです

  • JASYPT_PASSWORD="xxx" ./gradlew clean build

これで build/libs 配下に各種 jar が作成できます
jar を実行するには以下のようにします

  • JASYPT_PASSWORD=asdfghjkl java -jar build/libs/demo-0.0.1-SNAPSHOT-web.jar
  • JASYPT_PASSWORD=asdfghjkl java -jar build/libs/demo-0.0.1-SNAPSHOT-worker.jar

最後に

JobRunr で Web アプリケーションとワーカーアプリケーションを分離させる方法を紹介しました
もしワーカーを増やしたい場合はワーカーの jar だけ追加で起動すれば OK です

試しに複数起動してみましたが2つのワーカーが処理することはなかったので大丈夫だと思います

2026年6月4日木曜日

SpringBoot で JobRunr を使って非同期処理する方法

SpringBoot で JobRunr を使って非同期処理する方法

概要

過去にデフォルトの非同期ジョブを使ってみました
ジョブキューには Redis を使いましたが JobRunr では MySQL などいろいろなものをジョブキューに設定できます
また管理画面やスケジュール実行も備えておりジョブキューとして必須の機能を実装しています

今回は SpringBoot のプロジェクトに簡単な JobRunr を使った非同期処理を実装してみます

環境

  • macOS 26.4.1
  • openjdk 26.0.1
  • SpringBoot 4.0.6
    • jasypt-spring-boot 4.0.4
    • jackson-databind 2.21.2
    • jobrunr 8.6.1
  • gradle 9.5.1
  • VSCode 1.121.0
  • MySQL 9.6.0
  • Redis 8.6.3

インストール

  • vim build.gradle
dependencies {
       implementation 'com.fasterxml.jackson.core:jackson-databind:2.21.4'
       implementation 'org.jobrunr:jobrunr:8.6.1'
}

JobRunr 接続設定

設定は @Configuration を使って定義します
SpringApplication 配下であれば定義できるます

storageProvider でジョブキューを保存するストレージを定義します
今回は MySQL を使います
MySQL の接続先は application.properties にあるデータベースをデフォルトでは使用します (参考)

DatabaseOptions.CREATE をオプションに指定することで自動で的にジョブキューに必要なテーブルを作成してくれます

jobrunr_jobs
jobrunr_recurring_jobs
jobrunr_backgroundjobservers
jobrunr_metadata

jobActivator はワーカーがジョブを実行する際に自動的に Bean オブジェクトをワーカーが取得できるようにする設定です

jobScheduler で実際にワーカー処理を登録します
先ほどまでに作成したデータストレージやアクティベータを登録します
useBackgroundJobServer を呼び出すことで非同期ワーカーを登録します

  • vim src/main/java/com/example/demo/config/JobRunrConfig.java
package com.example.demo.config;

import javax.sql.DataSource;

import org.jobrunr.configuration.JobRunr;
import org.jobrunr.scheduling.JobScheduler;
import org.jobrunr.server.JobActivator;
import org.jobrunr.storage.StorageProvider;
import org.jobrunr.storage.StorageProviderUtils.DatabaseOptions;
import org.jobrunr.storage.sql.mysql.MySqlStorageProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JobRunrConfig {

       @Bean
       StorageProvider storageProvider(DataSource dataSource) {
               return new MySqlStorageProvider(dataSource, DatabaseOptions.CREATE);
       }

       @Bean
       JobActivator jobActivator(ApplicationContext applicationContext) {
               return applicationContext::getBean;
       }

       @Bean
       JobScheduler jobScheduler(StorageProvider storageProvider, JobActivator jobActivator) {
               return JobRunr.configure().useStorageProvider(storageProvider).useJobActivator(jobActivator)
                               .useBackgroundJobServer().initialize().getJobScheduler();
       }
}

ワーカーの作成

実際にジョブを受け取ったワーカーが処理する内容を定義します
このあと定義しますがワーカー側ではエンキュー時に登録した引数の情報をそのまま受け取ることができます

package com.example.demo.worker;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class UserCreatedWorker {

       private static final Logger LOGGER = LoggerFactory.getLogger(UserCreatedWorker.class);

       public void processUserCreated(String name, String email) {
               LOGGER.info("Processing user created job - Name: {}, Email: {}", name, email);
       }
}

サービス側の修正

エンキューする処理をサービス側に定義します
基本的にはサービスレイヤーがある場合にはサービスレイヤーでエンキュー処理を行うほうが良いです

今までは StringRedisTemplate を使ってジョブキューを実行していましたがそれを JobRunr を使うように修正します

jobScheduler を使いエンキューし userCreatedWorker を使ってデキュー時のワーカー処理を呼び出してあげるようにします

  • vim src/main/java/com/example/demo/service/UserService.java
package com.example.demo.service;

import java.util.ArrayList;
import java.util.List;

import org.jobrunr.scheduling.JobScheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.demo.dto.UserRequest;
import com.example.demo.dto.UserResponse;
import com.example.demo.entity.User;
import com.example.demo.exception.BusinessException;
import com.example.demo.repository.UserRepository;
import com.example.demo.worker.UserCreatedWorker;

@Service
public class UserService {

        @Autowired
        private UserRepository userRepository;

        @Autowired
        private JobScheduler jobScheduler;

        @Autowired
        private UserCreatedWorker userCreatedWorker;

        public void createUser(UserRequest req) {

                // ビジネスルール
                if (userRepository.existsByEmail(req.getEmail())) {
                        throw new BusinessException("USER_ALREADY_EXISTS", "既に登録されています");
                }

                User user = new User();
                user.setName(req.getName());
                user.setEmail(req.getEmail());

                userRepository.save(user);

                // ユーザー作成後の後続処理をJobRunrで非同期実行
                jobScheduler.enqueue(() -> userCreatedWorker.processUserCreated(req.getName(), req.getEmail()));
        }

        public List<UserResponse> getAllUsers() {
                List<User> users = new ArrayList<>();
                userRepository.findAll().forEach(users::add);

                return users.stream().map(this::toResponse).toList();
        }

        private UserResponse toResponse(User user) {
                UserResponse response = new UserResponse();
                response.setName(user.getName());
                return response;
        }
}

動作確認

アプリを起動し POST リクエストを送信しましょう
ジョブがエンキューされワーカー側のロギング処理が表示されれば OK です

  • ./gradlew bootRun --args='--jasypt.encryptor.password=xxx'
  • curl -XPOST http://localhost:8080/demo/add -d '{"name": "First", "email": "someemail5@someemailprovider.com"}' -H 'content-type: application/json'
2026-06-02T16:43:10.805+09:00  INFO 705817 --- [demo] [roundjob-worker] c.example.demo.worker.UserCreatedWorker  : Processing user created job - Name: First, Email: someemail5@someemailprov

最後に

SpringBoot + JobRunr を試してみました
これだとデフォルトの非同期処理とあまり変わらないので次回は管理画面の導入や別のストレージプロバイダーを使ってみたいと思います

参考サイト