Spring Boot 3.4 Virtual Threads: Câu Hỏi Phỏng Vấn và Benchmark Hiệu Năng
Làm chủ Virtual Threads của Java 21 với Spring Boot 3.4: 15 câu hỏi phỏng vấn, benchmark hiệu năng và mẫu di chuyển để vượt qua các buổi phỏng vấn kỹ thuật.

Virtual Threads là một trong những cải tiến quan trọng nhất của Java 21, và Spring Boot 3.4 hỗ trợ chúng một cách native. Tính năng thuộc Project Loom này thay đổi cách xử lý concurrency trong các ứng dụng backend. Các buổi phỏng vấn kỹ thuật ngày nay đánh giá việc nắm vững cơ chế bên trong, các tình huống sử dụng phù hợp và những bẫy phổ biến cần tránh.
Người phỏng vấn phân biệt rõ giữa ứng viên thực sự hiểu Virtual Threads và những người dùng chúng một cách máy móc. Biết khi nào KHÔNG nên dùng quan trọng không kém việc biết lợi ích của chúng.
Nền Tảng Virtual Threads
Câu hỏi 1: Virtual Thread là gì và khác Platform Thread ra sao?
Virtual Thread là một thread nhẹ do JVM quản lý chứ không phải hệ điều hành. Khác với Platform Threads (thread truyền thống), Virtual Threads không ánh xạ trực tiếp tới thread của OS. JVM có thể tạo ra hàng triệu Virtual Thread với mức tiêu hao bộ nhớ tối thiểu.
// Comparing thread creation approaches
public class VirtualThreadDemo {
public void demonstrateDifference() {
// Platform Thread: ~1MB stack per thread
// Practical limit: a few thousand on a standard JVM
Thread platformThread = new Thread(() -> {
performBlockingOperation();
});
// Virtual Thread: ~a few KB per thread
// Can create millions without issues
Thread virtualThread = Thread.ofVirtual().start(() -> {
performBlockingOperation();
});
}
// A Virtual Thread "mounts" onto a Platform Thread (carrier)
// During I/O blocking, it releases the carrier for other Virtual Threads
private void performBlockingOperation() {
try {
Thread.sleep(1000); // Virtual Thread detaches from carrier here
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}Khác biệt cốt lõi nằm ở hành vi khi blocking. Khi Platform Thread bị blocking (I/O, sleep), nó vẫn dính chặt vào thread của OS. Trong khi đó Virtual Thread "unmount" khỏi carrier thread, giải phóng carrier cho các Virtual Thread khác.
Câu hỏi 2: Cách bật Virtual Threads trong Spring Boot 3.4?
Spring Boot 3.4 thu gọn việc bật Virtual Threads xuống còn một thuộc tính cấu hình duy nhất. Toàn bộ framework tự động thích ứng: Tomcat, REST controller và các lời gọi blocking lập tức hưởng lợi từ tối ưu này.
# application.yml
# Global Virtual Threads activation
spring:
threads:
virtual:
enabled: true
# Optional Tomcat pool configuration
server:
tomcat:
threads:
max: 200 # Less critical with Virtual Threads
min-spare: 10// Programmatic activation if needed
@Configuration
public class WebConfig {
@Bean
public TomcatProtocolHandlerCustomizer<?> virtualThreadCustomizer() {
// Each HTTP request runs on a Virtual Thread
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
}Việc bật Virtual Threads thay đổi hành vi của Tomcat: thay vì pool thread cố định, mỗi request nhận một Virtual Thread riêng. Cách tiếp cận này loại bỏ nút thắt cổ chai cố hữu của thread pool.
Câu hỏi 3: Giải thích khái niệm "mounting" và "unmounting"
Mounting là việc gắn Virtual Thread vào carrier thread (Platform Thread). Unmounting xảy ra trong các thao tác blocking và giải phóng carrier cho Virtual Thread khác. Cơ chế này cho phép tận dụng tối đa tài nguyên CPU.
// Illustrating the mounting/unmounting cycle
public class MountingDemo {
public void demonstrateMounting() {
Thread.ofVirtual().start(() -> {
// MOUNTED: Virtual Thread is using a carrier thread
System.out.println("Carrier: " + getCurrentCarrier());
// UNMOUNTING: releases carrier during blocking
performDatabaseQuery(); // Blocking JDBC call
// REMOUNTED: may be on a different carrier
System.out.println("New carrier: " + getCurrentCarrier());
});
}
// Blocking operations trigger unmounting automatically
private void performDatabaseQuery() {
// JDBC connection, file read, network call...
// Virtual Thread detaches during I/O wait
}
private String getCurrentCarrier() {
// Gets the current carrier thread name
return Thread.currentThread().toString();
}
}Cơ chế này hoàn toàn trong suốt với lập trình viên. Code vẫn được viết theo phong cách imperative cổ điển, song JVM tự tối ưu việc dùng carrier. Một pool nhỏ vài carrier có thể phục vụ hàng triệu Virtual Thread.
Số lượng carrier thread mặc định bằng số core CPU. JVM điều chỉnh pool này một cách động thông qua ForkJoinPool.
Tình Huống Sử Dụng và Anti-pattern
Câu hỏi 4: Khi nào Virtual Threads mang lại cải thiện hiệu năng?
Virtual Threads tỏa sáng với khối lượng công việc I/O-bound: gọi REST bên ngoài, truy vấn database, đọc file. Những thao tác này dành phần lớn thời gian để chờ, và trong khoảng thời gian đó Virtual Thread giải phóng carrier của mình.
// Ideal case for Virtual Threads
@Service
public class IOBoundService {
private final RestClient restClient;
private final UserRepository userRepository;
// Each call involves network and database waiting
// Virtual Threads shine here
public UserProfile enrichUserProfile(Long userId) {
// DB call - VT detaches during SQL query
User user = userRepository.findById(userId).orElseThrow();
// External REST call - VT detaches during HTTP wait
ExternalData externalData = restClient
.get()
.uri("/api/external/{id}", userId)
.retrieve()
.body(ExternalData.class);
// Data aggregation
return new UserProfile(user, externalData);
}
}Lợi ích đến từ việc xử lý số lượng request đồng thời lớn hơn nhiều. Với 200 Platform Threads và request 100ms, throughput tối đa là 2.000 req/s. Với Virtual Threads, con số này có thể vượt 50.000 req/s trên cùng một máy.
Câu hỏi 5: Anti-pattern nào cần tránh khi dùng Virtual Threads?
Virtual Threads không phù hợp với khối lượng CPU-bound hay các tình huống gây "pinning". Pinning xảy ra khi Virtual Thread vẫn dính vào carrier dù bị blocking, làm mất đi lợi ích của ảo hóa.
// Examples of cases to avoid
@Service
public class AntiPatterns {
// ANTI-PATTERN 1: CPU-intensive computation
// Virtual Threads provide no benefit here
public BigInteger computeFactorial(int n) {
// 100% CPU, no I/O, no unmounting possible
BigInteger result = BigInteger.ONE;
for (int i = 2; i <= n; i++) {
result = result.multiply(BigInteger.valueOf(i));
}
return result; // Carrier is monopolized throughout
}
// ANTI-PATTERN 2: Synchronized causes pinning
private final Object lock = new Object();
public void pinnedOperation() {
synchronized (lock) { // PINNING: VT stays on carrier
performDatabaseQuery(); // Unmounting doesn't happen!
}
}
// SOLUTION: Use ReentrantLock
private final ReentrantLock reentrantLock = new ReentrantLock();
public void unpinnedOperation() {
reentrantLock.lock();
try {
performDatabaseQuery(); // Unmounting possible
} finally {
reentrantLock.unlock();
}
}
}Xét về mặt tài nguyên, pinning biến Virtual Thread thành Platform Thread. Nguyên nhân chính là các khối synchronized và lời gọi native JNI. Chuyển sang ReentrantLock xử lý được trường hợp đầu tiên.
Câu hỏi 6: Làm sao phát hiện pinning trong ứng dụng?
JVM cung cấp các tùy chọn chẩn đoán để xác định các trường hợp pinning. Thông tin này rất quan trọng khi di chuyển sang Virtual Threads, vì pinning có thể làm xấu đi hiệu năng thay vì cải thiện.
// Configuration and pinning detection
public class PinningDiagnostics {
// JVM option to log pinning
// -Djdk.tracePinnedThreads=full (detailed)
// -Djdk.tracePinnedThreads=short (summary)
// Example code causing pinning
public void demonstratePinning() {
Thread.ofVirtual().start(() -> {
synchronized (this) {
// This log will appear with tracePinnedThreads enabled
try {
Thread.sleep(100); // Pinned during sleep!
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
}
}# Typical output with -Djdk.tracePinnedThreads=full
Thread[#23,VirtualThread[#1]/runnable@ForkJoinPool-1-worker-1,5,CarrierThreads]
java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:183)
java.base/java.lang.VirtualThread.parkOnCarrierThread(VirtualThread.java:661)
com.example.PinningDiagnostics.demonstratePinning(PinningDiagnostics.java:15)Phân tích log pinning sẽ chỉ ra các điểm nóng cần khắc phục. Một ứng dụng bị pinning thường xuyên sẽ không khai thác đầy đủ Virtual Threads, thậm chí còn chậm hơn so với Platform Threads.
Sẵn sàng chinh phục phỏng vấn Spring Boot?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Benchmark Hiệu Năng
Câu hỏi 7: Có thể kỳ vọng cải thiện hiệu năng tới đâu với Virtual Threads?
Các benchmark cho thấy mức cải thiện đáng kể với những ứng dụng I/O-bound điển hình. Mức cải thiện phụ thuộc vào tỉ lệ thời gian I/O so với CPU và mức concurrency cần đạt.
// Endpoint for measuring performance
@RestController
@RequestMapping("/api/benchmark")
public class BenchmarkController {
private final ExternalApiClient apiClient;
// Simulation of a typical endpoint with external calls
@GetMapping("/user/{id}")
public ResponseEntity<UserData> getUser(@PathVariable Long id) {
// Three sequential I/O calls
UserInfo info = apiClient.fetchUserInfo(id); // ~50ms
List<Order> orders = apiClient.fetchOrders(id); // ~80ms
CreditScore score = apiClient.fetchCreditScore(id); // ~100ms
return ResponseEntity.ok(new UserData(info, orders, score));
}
}# Benchmark results - 10,000 concurrent requests
# Configuration: 8 cores, 16GB RAM, simulated latency 230ms/request
Platform Threads (pool 200):
- Throughput: 850 req/s
- P99 latency: 1250ms
- Heap memory: 2.1GB
Virtual Threads:
- Throughput: 4200 req/s
- P99 latency: 280ms
- Heap memory: 850MB
Gain: 5x throughput, 4.5x P99 latency reductionVirtual Threads còn giảm tiêu thụ bộ nhớ vì mỗi thread chỉ chiếm vài KB thay vì khoảng 1 MB như Platform Thread.
Câu hỏi 8: Cấu hình connection pool ra sao khi dùng Virtual Threads?
Connection pool (HikariCP, Lettuce) trở thành nút thắt mới khi dùng Virtual Threads. Một pool 10 connection chỉ cho phép 10 truy vấn DB đồng thời, kể cả khi có hàng triệu Virtual Thread.
# application.yml
# HikariCP configuration optimized for Virtual Threads
spring:
datasource:
hikari:
# More connections since Virtual Threads allow more concurrency
maximum-pool-size: 50
minimum-idle: 10
# Shorter timeout due to more concurrent requests
connection-timeout: 5000
# Fast validation
validation-timeout: 3000
# Redis with Lettuce - already async-friendly
data:
redis:
lettuce:
pool:
max-active: 50
max-idle: 20// Pool monitoring to avoid contention
@Component
public class ConnectionPoolMonitor {
private final HikariDataSource dataSource;
@Scheduled(fixedRate = 10000)
public void logPoolStats() {
HikariPoolMXBean pool = dataSource.getHikariPoolMXBean();
log.info("Pool stats - Active: {}, Idle: {}, Waiting: {}",
pool.getActiveConnections(),
pool.getIdleConnections(),
pool.getThreadsAwaitingConnection());
// Alert if too many threads waiting
if (pool.getThreadsAwaitingConnection() > 100) {
log.warn("Connection pool contention detected!");
}
}
}Kích thước pool phụ thuộc vào sức chứa của database, không phụ thuộc số lượng Virtual Thread. PostgreSQL tiêu chuẩn xử lý được khoảng 100-200 connection đang hoạt động.
Câu hỏi 9: Đo lường tác động của Virtual Threads bằng Micrometer như thế nào?
Micrometer và Spring Boot Actuator cung cấp các metric thiết yếu để đánh giá hiệu quả của Virtual Threads. Những metric này xác nhận lợi ích và bộc lộ vấn đề tiềm ẩn.
// Custom metrics for Virtual Threads
@Component
public class VirtualThreadMetrics {
private final MeterRegistry registry;
private final AtomicLong activeVirtualThreads = new AtomicLong(0);
@PostConstruct
public void registerMetrics() {
// Active Virtual Threads counter
Gauge.builder("virtual.threads.active", activeVirtualThreads, AtomicLong::get)
.description("Number of active virtual threads")
.register(registry);
// JVM metrics for carriers
Gauge.builder("virtual.threads.carriers", this::getCarrierCount)
.description("Number of carrier threads")
.register(registry);
}
private double getCarrierCount() {
// Gets the ForkJoinPool carrier count
return ForkJoinPool.commonPool().getPoolSize();
}
// Interceptor to trace requests
public void trackVirtualThread(Runnable task) {
activeVirtualThreads.incrementAndGet();
try {
task.run();
} finally {
activeVirtualThreads.decrementAndGet();
}
}
}# application.yml
# Metrics exposure
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
tags:
application: ${spring.application.name}Việc phân tích metric cho thấy tỉ lệ Virtual Threads / Carriers và xác định các giai đoạn xảy ra contention trên connection pool.
Di Chuyển và Tương Thích
Câu hỏi 10: Thư viện nào tương thích với Virtual Threads?
Mức tương thích phụ thuộc vào việc dùng các khối synchronized và các lời gọi native. Hệ sinh thái Spring chủ yếu đã tương thích, nhưng một số thư viện đòi hỏi phiên bản cụ thể.
// Checking dependency compatibility
@Configuration
public class CompatibilityCheck {
// Compatible libraries (recommended versions)
// - Spring Boot 3.2+ (native support)
// - HikariCP 5.1+ (ReentrantLock instead of synchronized)
// - Lettuce 6.3+ (non-blocking I/O)
// - Jackson 2.16+ (no synchronized)
// Libraries requiring attention
// - JDBC drivers: check version
// - Some legacy HTTP clients
@Bean
public CommandLineRunner checkCompatibility() {
return args -> {
// Log versions for audit
log.info("Java version: {}", System.getProperty("java.version"));
log.info("Virtual threads available: {}",
Thread.ofVirtual() != null);
// Support verification
if (Runtime.version().feature() < 21) {
throw new IllegalStateException(
"Java 21+ required for Virtual Threads");
}
};
}
}Phần lớn các framework hiện đại đã chuyển sang ReentrantLock. Với các phụ thuộc cũ, một bài kiểm thử tải bằng -Djdk.tracePinnedThreads=short sẽ chỉ ra các vấn đề pinning.
Câu hỏi 11: Làm thế nào để di chuyển dần dần sang Virtual Threads?
Một cuộc di chuyển từng bước cho phép xác nhận lợi ích và phát hiện vấn đề mà không gây rủi ro cho production. Chiến lược được khuyến nghị là tách các endpoint dùng Virtual Threads khỏi endpoint truyền thống.
// Configuration for progressive migration
@Configuration
public class DualExecutorConfig {
// Traditional executor for legacy endpoints
@Bean("platformExecutor")
public ExecutorService platformExecutor() {
return Executors.newFixedThreadPool(200);
}
// Virtual Threads executor for new endpoints
@Bean("virtualExecutor")
public ExecutorService virtualExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
}
@RestController
@RequestMapping("/api/v2")
public class MigratedController {
@Qualifier("virtualExecutor")
private final ExecutorService executor;
// Endpoint migrated to Virtual Threads
@GetMapping("/users/{id}")
public CompletableFuture<User> getUser(@PathVariable Long id) {
return CompletableFuture.supplyAsync(() -> {
// Business code unchanged
return userService.findById(id);
}, executor);
}
}# application.yml
# Feature flag for migration
features:
virtual-threads:
enabled: true
endpoints:
- /api/v2/**
- /api/reports/**
# Disable for quick rollback
# features.virtual-threads.enabled: falseKhông nên bật Virtual Threads toàn cục trước khi kiểm thử mọi phụ thuộc. Pinning có thể làm hiệu năng giảm đáng kể.
Câu hỏi 12: Cần kiểm thử gì trước khi triển khai production?
Việc kiểm chứng Virtual Threads đòi hỏi kiểm thử tải, kiểm thử pinning và kiểm thử tương thích. Các bài kiểm thử này phải mô phỏng điều kiện production thực tế.
// Load test with JUnit and Virtual Threads
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class VirtualThreadLoadTest {
@LocalServerPort
private int port;
@Test
void shouldHandleHighConcurrency() throws Exception {
int concurrentRequests = 5000;
CountDownLatch latch = new CountDownLatch(concurrentRequests);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger errorCount = new AtomicInteger(0);
HttpClient client = HttpClient.newHttpClient();
// Launch 5000 simultaneous requests
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < concurrentRequests; i++) {
executor.submit(() -> {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:" + port + "/api/test"))
.build();
HttpResponse<String> response = client.send(
request, BodyHandlers.ofString());
if (response.statusCode() == 200) {
successCount.incrementAndGet();
} else {
errorCount.incrementAndGet();
}
} catch (Exception e) {
errorCount.incrementAndGet();
} finally {
latch.countDown();
}
});
}
}
latch.await(60, TimeUnit.SECONDS);
// Performance assertions
assertThat(successCount.get()).isGreaterThan(4900); // >98% success
assertThat(errorCount.get()).isLessThan(100);
}
}Kiểm thử cần bao quát các kịch bản contention (connection pool quá tải), timeout và tải kéo dài trong nhiều phút.
Câu Hỏi Nâng Cao
Câu hỏi 13: Virtual Threads tương tác với Structured Concurrency thế nào?
Structured Concurrency (JEP 453) bổ sung cho Virtual Threads bằng cách bảo đảm các task đồng thời chia sẻ cùng vòng đời. Cách tiếp cận này đơn giản hóa việc xử lý lỗi và hủy bỏ.
// Combining Virtual Threads + Structured Concurrency
public class StructuredConcurrencyExample {
public UserDashboard fetchDashboard(Long userId) throws Exception {
// StructuredTaskScope ensures all tasks complete together
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// Three parallel tasks on Virtual Threads
Subtask<UserProfile> profileTask = scope.fork(() ->
userService.getProfile(userId));
Subtask<List<Notification>> notifTask = scope.fork(() ->
notificationService.getRecent(userId));
Subtask<AccountBalance> balanceTask = scope.fork(() ->
accountService.getBalance(userId));
// Wait for all tasks or fail on first error
scope.join();
scope.throwIfFailed();
// All tasks succeeded - safe aggregation
return new UserDashboard(
profileTask.get(),
notifTask.get(),
balanceTask.get()
);
}
// If one task fails, others are automatically cancelled
}
}Structured Concurrency ngăn ngừa rò rỉ thread và làm việc debug dễ hơn vì call stack phản ánh đúng cấu trúc logic của code.
Câu hỏi 14: Khác biệt giữa Virtual Threads và lập trình reactive là gì?
Cả hai cách tiếp cận đều giải quyết cùng một vấn đề (hiệu quả I/O) nhưng theo các mô hình lập trình khác nhau. Virtual Threads cho phép viết code imperative cổ điển, trong khi reactive đòi hỏi viết lại theo dạng stream.
// Same logic: imperative vs reactive
@Service
public class ComparisonService {
// VIRTUAL THREADS APPROACH: classic imperative code
// Easy to read, debug, and maintain
public UserData getUserDataImperative(Long id) {
User user = userRepository.findById(id).orElseThrow();
List<Order> orders = orderRepository.findByUserId(id);
PaymentInfo payment = paymentService.getInfo(id);
return new UserData(user, orders, payment);
}
// REACTIVE APPROACH: streams and operators
// More complex but native backpressure
public Mono<UserData> getUserDataReactive(Long id) {
return userRepository.findById(id)
.zipWith(orderRepository.findByUserId(id).collectList())
.zipWith(paymentService.getInfo(id))
.map(tuple -> new UserData(
tuple.getT1().getT1(),
tuple.getT1().getT2(),
tuple.getT2()
));
}
}| Tiêu chí | Virtual Threads | Reactive | |----------|-----------------|----------| | Đường cong học | Thấp | Cao | | Debug | Stack trace cổ điển | Phức tạp | | Backpressure | Thủ công | Native | | Hệ sinh thái | Đang phát triển | Trưởng thành | | Di chuyển legacy | Dễ | Viết lại |
Virtual Threads được khuyên dùng cho ứng dụng mới và việc di chuyển. Reactive vẫn phù hợp khi cần backpressure tinh vi.
Câu hỏi 15: Quản lý ThreadLocal cùng Virtual Threads ra sao?
ThreadLocal vẫn dùng được với Virtual Threads nhưng tiêu tốn bộ nhớ cho mỗi instance. Scoped Values (JEP 446) là lựa chọn hiệu quả hơn để chia sẻ context.
// Comparing context approaches
public class ThreadLocalVsScopedValue {
// CLASSIC APPROACH: ThreadLocal
// Works but expensive with millions of VT
private static final ThreadLocal<RequestContext> requestContext =
new ThreadLocal<>();
public void processWithThreadLocal(Request request) {
requestContext.set(new RequestContext(request.getTraceId()));
try {
// Context accessible everywhere in the thread
processRequest();
} finally {
requestContext.remove(); // Important to prevent leaks
}
}
// MODERN APPROACH: ScopedValue (Java 21+)
// More efficient, immutable, explicit scope
private static final ScopedValue<RequestContext> CONTEXT =
ScopedValue.newInstance();
public void processWithScopedValue(Request request) {
ScopedValue.where(CONTEXT, new RequestContext(request.getTraceId()))
.run(() -> {
// Context accessible within this scope
processRequest();
// No cleanup needed - automatic at scope end
});
}
private void processRequest() {
// Context access
String traceId = CONTEXT.isBound()
? CONTEXT.get().getTraceId()
: "unknown";
log.info("Processing with trace: {}", traceId);
}
}ScopedValues được khuyên dùng cho các phát triển mới. Với code legacy đang dùng ThreadLocal, nên di chuyển dần dần.
Sẵn sàng chinh phục phỏng vấn Spring Boot?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Kết Luận
Virtual Threads thay đổi cách phát triển backend Java khi cho phép viết code đơn giản mà vẫn hiệu năng cao. Điểm mấu chốt:
Nền tảng:
- Bật bằng
spring.threads.virtual.enabled=true - Lý tưởng cho khối lượng I/O-bound (REST, DB, file)
- Tránh dùng cho các phép toán nặng CPU
Hiệu năng:
- Thiết lập kích thước connection pool đúng (nút thắt mới)
- Theo dõi pinning bằng
-Djdk.tracePinnedThreads - Chuyển
synchronizedsangReentrantLock
Di chuyển:
- Kiểm thử dần theo nhóm endpoint
- Xác nhận tính tương thích của các phụ thuộc
- Tận dụng Structured Concurrency cho thao tác song song
Làm chủ Virtual Threads tạo nên sự khác biệt cho ứng viên hiểu rõ thách thức hiệu năng của ứng dụng hiện đại. Các khái niệm này nay đã trở thành điều bắt buộc trong phỏng vấn kỹ thuật Spring Boot.
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Thẻ
Chia sẻ
Bài viết liên quan

Spring Modulith: Kiến trúc Monolith Mô-đun Giải thích
Học Spring Modulith để xây dựng monolith mô-đun trong Java. Kiến trúc, mô-đun, sự kiện bất đồng bộ và testing với ví dụ Spring Boot 3.

Phỏng vấn Spring Batch 5: Phân vùng, Chunk và Khả năng chịu lỗi
Chinh phục các buổi phỏng vấn Spring Batch 5: 15 câu hỏi cốt lõi về phân vùng, xử lý chunk và khả năng chịu lỗi với ví dụ Java 21.

Testcontainers Spring Boot: kiểm thử tích hợp không đau đầu
Hướng dẫn đầy đủ về cấu hình Testcontainers với Spring Boot 3.4. PostgreSQL, Redis và Kafka trong các container Docker để kiểm thử tích hợp đáng tin cậy và có thể tái lập.