概要
デフォルトで使用できる Redis クライアントがありこれを使って簡単に非同期処理が実現できるので試してみました
メッセージのやり取りには DTO クラスを使い Json メッセージを DTO に変換してエンキュー/デキュー時に使用するようにしてみます
環境
- macOS 26.4.1
- openjdk 26.0.1
- SpringBoot 4.0.6
- jasypt-spring-boot 4.0.4
- jackson-databind 2.21.2
- gradle 9.5.1
- VSCode 1.121.0
- MySQL 9.6.0
- Redis 8.6.3
build.gradle
SpringBoot から Redis に接続するためのライブラリとメッセージのやり取りをしやすくするための jackson-databind を追加でインストールします
- org.springframework.boot:spring-boot-starter-json
- org.springframework.boot:spring-boot-starter-data-redis
- com.fasterxml.jackson.core:jackson-databind
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.springframework.boot:spring-boot-starter-json'
implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:4.0.4'
developmentOnly 'org.springframework.boot:spring-boot-docker-compose'
runtimeOnly 'com.mysql:mysql-connector-j'
testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.testcontainers:testcontainers-junit-jupiter'
testImplementation 'org.testcontainers:testcontainers-mysql'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
ワーカーの作成(デキュー)
まずはワーカーを作成します
Json メッセージを受け取り処理する部分です
今回は Json -> DTO に変換してから使います
- vim src/main/java/com/example/demo/worker/RedisMessageReceiver.java
package com.example.demo.worker;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.example.demo.dto.RedisMessageRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
public class RedisMessageReceiver {
private static final Logger LOGGER = LoggerFactory.getLogger(RedisMessageReceiver.class);
private AtomicInteger counter = new AtomicInteger();
private ObjectMapper objectMapper;
public RedisMessageReceiver(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public void receiveMessage(String message) {
try {
// JSON から RedisMessageRequest に変換
RedisMessageRequest request = objectMapper.readValue(message, RedisMessageRequest.class);
// ワーカー側でメッセージ情報を使用
LOGGER.info("Received message - Name: {}, Email: {}", request.getName(), request.getEmail());
counter.incrementAndGet();
} catch (Exception e) {
LOGGER.error("Failed to parse Redis message", e);
}
}
public int getCount() {
return counter.get();
}
}
DTO の作成
メッセージ情報管理する DTO クラスです
ビジネスロジックを含まないバリデーションや加工処理が必要な場合はここで行います
- vim src/main/java/com/example/demo/dto/RedisMessageRequest.java
package com.example.demo.dto;
import jakarta.validation.constraints.NotBlank;
public class RedisMessageRequest {
@NotBlank
private String name;
@NotBlank
private String email;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
Redis接続設定
SpringBoot では Redis への接続情報は @Bean を使うのが定石です
@Bean の定義は @SpringBootApplication または @Configuration 配下で使えます
@Bean としてオブジェクトを定義しておくと各種クラウで @Autowired できるようになります
送信トピックやワーカーの登録などを行います
- vim src/main/java/com/example/demo/config/RedisConfig.java
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import com.example.demo.worker.RedisMessageReceiver;
import com.fasterxml.jackson.databind.ObjectMapper;
@Configuration
public class RedisConfig {
@Bean
ObjectMapper objectMapper() {
return new ObjectMapper();
}
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(listenerAdapter, new PatternTopic("chat"));
return container;
}
@Bean
MessageListenerAdapter listenerAdapter(RedisMessageReceiver receiver) {
return new MessageListenerAdapter(receiver, "receiveMessage");
}
@Bean
RedisMessageReceiver receiver(ObjectMapper objectMapper) {
return new RedisMessageReceiver(objectMapper);
}
}
application.properties の修正
Redis の接続先情報はここで定義します
- vim src/main/resources/application.properties
spring.data.redis.host=localhost
spring.data.redis.port=6379
サービス側でエンキュー処理
エンキューはサービス側で行います
StringRedisTemplate を DI するだけで使えます
今回はメッセージをエンキューする際も RedisMessageRequest を使いますが本当はエンキュー時とデキュー時は別の DTO クラスを定義するほうがキレイです
jackson を使って DTO クラスを自動的に Json に形式に変換しエンキューします
- 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.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import com.example.demo.dto.RedisMessageRequest;
import com.example.demo.dto.UserRequest;
import com.example.demo.dto.UserResponse;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ObjectMapper objectMapper;
public void createUser(UserRequest req) {
// 例:ビジネスルール
if (userRepository.existsByEmail(req.getEmail())) {
throw new IllegalArgumentException("既に登録されています");
}
User user = new User();
user.setName(req.getName());
user.setEmail(req.getEmail());
userRepository.save(user);
// Redis キューにメッセージを JSON 形式で送信
try {
RedisMessageRequest message = new RedisMessageRequest();
message.setName(req.getName());
message.setEmail(req.getEmail());
String jsonMessage = objectMapper.writeValueAsString(message);
redisTemplate.convertAndSend("chat", jsonMessage);
} catch (Exception e) {
throw new RuntimeException("Failed to send Redis message", e);
}
}
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;
}
}
動作確認
アプリを起動し curl を実行します
アプリ側のログにワーカーで出力しているメッセージが表示されれば OK です
redis が必要なのでローカルで起動しておきます (もしくは compose.yaml の docker compose plugin でも対応できます)
-
brew services run redis
-
./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-05-25T10:17:20.254+09:00 INFO 34105 --- [demo] [ container-1] c.e.demo.worker.RedisMessageReceiver : Received message - Name: First, Email: someemail5@someemailprovider.com
おまけ: テストの修正
サービスのテストで Redis 部分 (StringRedisTemplate) をモックするように書き換えます
- vim src/test/java/com/example/demo/service/UserServiceTest.java
package com.example.demo.service;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.StringRedisTemplate;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.List;
import com.example.demo.dto.UserRequest;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private StringRedisTemplate redisTemplate;
@Mock
private ObjectMapper objectMapper;
@InjectMocks
private UserService userService;
@Test
void ユーザー作成できる() throws Exception {
UserRequest req = new UserRequest();
req.setName("taro");
req.setEmail("taro@example.com");
when(objectMapper.writeValueAsString(any())).thenReturn("{\"name\":\"taro\",\"email\":\"taro@example.com\"}");
when(redisTemplate.convertAndSend(eq("chat"), anyString())).thenReturn(1L);
userService.createUser(req);
verify(userRepository).save(any(User.class));
verify(redisTemplate).convertAndSend(eq("chat"), anyString());
}
@Test
void findAllメソッドが呼ばれたか() {
userService.getAllUsers();
verify(userRepository).findAll();
}
@Test
void ユーザーの一覧が取得できる() {
User user = new User();
user.setName("taro");
user.setEmail("taro@example.com");
when(userRepository.findAll()).thenReturn(List.of(user));
var result = userService.getAllUsers();
assertEquals(1, result.size());
assertEquals("taro", result.get(0).getName());
}
@Test
void メールが重複している場合はエラー() {
UserRequest req = new UserRequest();
req.setName("taro");
req.setEmail("taro@example.com");
// モックの振る舞いを定義
when(userRepository.existsByEmail("taro@example.com")).thenReturn(true);
// 例外が投げられることを検証
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> userService.createUser(req));
assertEquals("既に登録されています", ex.getMessage());
// saveが呼ばれていないことも重要
verify(userRepository, never()).save(any());
}
}
最後に
SpringBoot で非同期処理を行ってみました
簡単にできますがいくつか課題があります
- プロセス分離できていない
- 管理画面がない
- リトライやデッドレターキューなどがない
などなど非同期処理に欠かせない機能が単純に欠如しています
本当に簡単な非同期処理なら良いですがそうでない場合はやはりデフォルトの機能ではなく外部のライブラリを使う他なさそうです