30 Câu Hỏi Phỏng Vấn Spring Boot: Hướng Dẫn Đầy Đủ cho Lập Trình Viên Java
Chuẩn bị các buổi phỏng vấn Spring Boot với 30 câu hỏi cốt lõi về auto-configuration, starter, Spring Data JPA, bảo mật và kiểm thử.

Spring Boot đã trở thành framework chủ lực trong phát triển Java doanh nghiệp. Các buổi phỏng vấn kỹ thuật đánh giá khả năng hiểu cơ chế nội bộ, các thực hành tốt và hệ sinh thái Spring. Hướng dẫn này tổng hợp 30 câu hỏi được hỏi nhiều nhất, từ khái niệm cơ bản đến chủ đề nâng cao.
Người phỏng vấn đánh giá cao ứng viên hiểu "vì sao" đứng sau từng tính năng. Ngoài cú pháp, hãy giải thích Spring Boot giải quyết vấn đề gì.
Nền tảng Spring Boot
1. Khác biệt giữa Spring và Spring Boot là gì?
Spring là framework dạng module cung cấp dependency injection, quản lý transaction và tích hợp với rất nhiều công nghệ. Spring Boot là một lớp trừu tượng nằm trên Spring nhằm đơn giản hóa cấu hình và quá trình khởi động ứng dụng.
// With Spring Boot: a single annotation to start
@SpringBootApplication
public class Application {
public static void main(String[] args) {
// SpringApplication configures and starts the Spring context
SpringApplication.run(Application.class, args);
}
}Spring Boot mang lại auto-configuration, các starter phụ thuộc, máy chủ nhúng và bộ chỉ số sản xuất qua Actuator. Mục tiêu là đi từ ý tưởng đến mã chạy được trong vài phút.
2. Auto-configuration hoạt động ra sao?
Auto-configuration phân tích classpath và tự động cấu hình các bean cần thiết. Cơ chế dựa trên annotation @EnableAutoConfiguration (đã có trong @SpringBootApplication) cùng các điều kiện được khai báo trong các lớp auto-configuration.
@Configuration
@ConditionalOnClass(DataSource.class) // Activates if DataSource is on classpath
@ConditionalOnMissingBean(DataSource.class) // Only acts if no DataSource exists
public class CustomAutoConfiguration {
@Bean
@ConditionalOnProperty(name = "app.datasource.enabled", havingValue = "true")
public DataSource dataSource() {
// Default DataSource configuration
return DataSourceBuilder.create()
.driverClassName("org.h2.Driver")
.url("jdbc:h2:mem:testdb")
.build();
}
}Spring Boot xét các điều kiện (@ConditionalOn*) để quyết định tạo bean nào. Cách tiếp cận này tránh xung đột và cho phép ghi đè cấu hình mặc định một cách dễ dàng.
3. Starter là gì và tạo ra sao?
Starter là một module Maven/Gradle gom sẵn các phụ thuộc và cấu hình cần thiết cho một tính năng. Ví dụ spring-boot-starter-web bao gồm Spring MVC, Jackson, Tomcat nhúng và validation.
<!-- pom.xml for a custom starter -->
<project>
<artifactId>my-company-starter</artifactId>
<dependencies>
<!-- Base Spring Boot dependency -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Company-specific libraries -->
<dependency>
<groupId>com.mycompany</groupId>
<artifactId>logging-utils</artifactId>
</dependency>
</dependencies>
</project>Để tạo starter tùy biến, hãy định nghĩa các lớp auto-configuration và khai báo chúng trong META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.
4. So sánh application.properties và application.yml
Cả hai định dạng đều giúp đưa cấu hình ra ngoài mã. YAML có cú pháp dễ đọc cho cấu trúc lồng nhau, còn properties đơn giản hơn cho cấu hình phẳng.
# application.properties
server.port=8080
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=admin
spring.jpa.hibernate.ddl-auto=update# application.yml - clear hierarchical structure
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: admin
jpa:
hibernate:
ddl-auto: updateProfile điều chỉnh cấu hình theo môi trường: application-dev.yml, application-prod.yml. Kích hoạt qua spring.profiles.active.
5. @SpringBootApplication hoạt động thế nào?
Annotation này hợp nhất ba annotation cốt lõi định hình hành vi của ứng dụng Spring Boot.
// @SpringBootApplication is equivalent to these three annotations:
@SpringBootConfiguration // Equivalent to @Configuration
@EnableAutoConfiguration // Activates auto-configuration
@ComponentScan // Scans components in package and sub-packages
public class DemoApplication {
public static void main(String[] args) {
// Creates context, runs configurations, and starts server
ConfigurableApplicationContext context =
SpringApplication.run(DemoApplication.class, args);
// The context contains all configured beans
UserService userService = context.getBean(UserService.class);
}
}Đặt lớp này ở gốc package là rất quan trọng: @ComponentScan quét chính package đó và mọi sub-package để tìm component.
Spring Data và lưu trữ
6. JpaRepository, CrudRepository và PagingAndSortingRepository khác nhau ra sao?
Các interface này tạo thành một hệ thống phân cấp với mức độ chức năng truy cập dữ liệu tăng dần.
// CrudRepository: basic CRUD operations
public interface UserCrudRepository extends CrudRepository<User, Long> {
// save(), findById(), findAll(), deleteById(), count(), existsById()
}
// PagingAndSortingRepository: adds pagination and sorting
public interface UserPagingRepository extends PagingAndSortingRepository<User, Long> {
// Inherits from CrudRepository + findAll(Sort), findAll(Pageable)
}
// JpaRepository: complete JPA features
public interface UserRepository extends JpaRepository<User, Long> {
// Inherits from previous + flush(), saveAndFlush(), deleteInBatch()
// Derived query methods
List<User> findByEmailContaining(String email);
Optional<User> findByUsernameAndActiveTrue(String username);
}Trong đa số trường hợp, JpaRepository là lựa chọn được khuyến nghị vì cung cấp đầy đủ chức năng cần cho ứng dụng JPA.
7. Cách định nghĩa truy vấn tùy biến với @Query?
Annotation @Query cho phép viết truy vấn JPQL hoặc SQL native khi các phương thức suy luận không đủ.
public interface OrderRepository extends JpaRepository<Order, Long> {
// JPQL with named parameters
@Query("SELECT o FROM Order o WHERE o.customer.id = :customerId AND o.status = :status")
List<Order> findCustomerOrdersByStatus(
@Param("customerId") Long customerId,
@Param("status") OrderStatus status
);
// Native SQL query for specific needs
@Query(value = "SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '7 days'",
nativeQuery = true)
List<Order> findRecentOrders();
// Modifying query with @Modifying
@Modifying
@Transactional
@Query("UPDATE Order o SET o.status = :status WHERE o.id IN :ids")
int updateOrderStatuses(@Param("ids") List<Long> ids, @Param("status") OrderStatus status);
}Truy vấn JPQL khả chuyển giữa các CSDL, còn truy vấn native cho phép tận dụng tính năng riêng của DBMS.
8. Quản lý transaction với @Transactional
Annotation này khai báo phạm vi transaction cho phương thức hoặc lớp. Spring tự động xử lý commit, rollback và propagation.
@Service
public class PaymentService {
private final AccountRepository accountRepository;
private final TransactionLogRepository logRepository;
// Transaction with custom configuration
@Transactional(
propagation = Propagation.REQUIRED, // Creates or joins a transaction
isolation = Isolation.READ_COMMITTED, // Isolation level
timeout = 30, // Timeout in seconds
rollbackFor = PaymentException.class // Rollback on this exception
)
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId)
.orElseThrow(() -> new AccountNotFoundException(fromId));
Account to = accountRepository.findById(toId)
.orElseThrow(() -> new AccountNotFoundException(toId));
from.debit(amount); // Throws exception if insufficient balance
to.credit(amount);
accountRepository.save(from);
accountRepository.save(to);
// Log will be rolled back with everything else if an exception occurs
logRepository.save(new TransactionLog(fromId, toId, amount));
}
}Các mức propagation (REQUIRED, REQUIRES_NEW, NESTED v.v.) quyết định cách các transaction lồng nhau khi gọi liên hoàn các phương thức transactional.
@Transactional chỉ hoạt động với các lời gọi đi qua proxy của Spring. Lời gọi nội bộ trong cùng lớp sẽ bỏ qua proxy và không chú ý annotation.
9. Xử lý quan hệ Lazy và Eager Loading thế nào?
Chế độ tải quan hệ ảnh hưởng trực tiếp đến hiệu năng. Lazy Loading hoãn việc tải đến khi truy cập, Eager Loading tải ngay lập tức.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Eager: roles are loaded with the user
@ManyToMany(fetch = FetchType.EAGER)
private Set<Role> roles;
// Lazy: orders are only loaded on access
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Order> orders;
}@Service
public class UserService {
@Transactional(readOnly = true)
public UserDTO getUserWithOrders(Long userId) {
User user = userRepository.findById(userId).orElseThrow();
// Accessing orders triggers an additional SQL query
// This works because the session is open (@Transactional)
List<OrderDTO> orderDTOs = user.getOrders().stream()
.map(this::toDTO)
.collect(Collectors.toList());
return new UserDTO(user, orderDTOs);
}
// Alternative: query with JOIN FETCH to avoid N+1
public UserDTO getUserWithOrdersOptimized(Long userId) {
User user = userRepository.findByIdWithOrders(userId);
// Orders are already loaded, no additional query
return new UserDTO(user, user.getOrders());
}
}Vấn đề N+1 phát sinh khi lazy loading sinh một truy vấn cho mỗi phần tử của tập hợp. JOIN FETCH hoặc projection sẽ khắc phục.
10. Vấn đề N+1 là gì và cách giải quyết?
Vấn đề N+1 xảy ra khi sau truy vấn ban đầu (1) lại có N truy vấn nữa để tải các quan hệ của từng entity.
public interface ArticleRepository extends JpaRepository<Article, Long> {
// PROBLEM: this query generates N+1 queries
List<Article> findAll();
// SOLUTION 1: JOIN FETCH in JPQL
@Query("SELECT a FROM Article a JOIN FETCH a.author JOIN FETCH a.comments")
List<Article> findAllWithDetails();
// SOLUTION 2: Entity Graph
@EntityGraph(attributePaths = {"author", "comments"})
@Query("SELECT a FROM Article a")
List<Article> findAllWithGraph();
}@Entity
@NamedEntityGraph(
name = "Article.withDetails",
attributeNodes = {
@NamedAttributeNode("author"),
@NamedAttributeNode("comments")
}
)
public class Article {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Author author;
@OneToMany(mappedBy = "article", fetch = FetchType.LAZY)
private List<Comment> comments;
}Dùng @EntityGraph hoặc JOIN FETCH giúp gom N+1 truy vấn về một truy vấn có join, cải thiện hiệu năng đáng kể.
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.
REST API và Web
11. Tạo REST controller có validation thế nào?
Spring Boot kết hợp @RestController với Bean Validation để xây dựng API mạnh mẽ với khả năng kiểm tra đầu vào tự động.
@RestController
@RequestMapping("/api/users")
@Validated // Activates validation on method parameters
public class UserController {
private final UserService userService;
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserResponse createUser(@Valid @RequestBody CreateUserRequest request) {
// Validation is performed automatically before execution
return userService.createUser(request);
}
@GetMapping("/{id}")
public UserResponse getUser(
@PathVariable @Min(1) Long id // Validation on PathVariable
) {
return userService.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
@GetMapping
public Page<UserResponse> listUsers(
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "20") @Max(100) int size
) {
return userService.findAll(PageRequest.of(page, size));
}
}public record CreateUserRequest(
@NotBlank(message = "Name is required")
@Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
String name,
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
String email,
@NotBlank
@Pattern(regexp = "^(?=.*[A-Z])(?=.*[0-9]).{8,}$",
message = "Password must contain at least 8 characters, one uppercase and one digit")
String password
) {}Có thể tách thông điệp validation ra các file message để hỗ trợ đa ngôn ngữ.
12. Xử lý exception toàn cục với @ControllerAdvice ra sao?
Một bộ xử lý exception tập trung giúp chuẩn hóa phản hồi lỗi và tránh lặp mã.
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// Handle validation errors
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidationErrors(MethodArgumentNotValidException ex) {
Map<String, String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage,
(existing, replacement) -> existing
));
return new ErrorResponse("VALIDATION_ERROR", "Invalid data", errors);
}
// Handle resource not found
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(ResourceNotFoundException ex) {
return new ErrorResponse("NOT_FOUND", ex.getMessage(), null);
}
// Handle unexpected errors
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleUnexpectedError(Exception ex) {
log.error("Unexpected error", ex);
return new ErrorResponse("INTERNAL_ERROR", "An error occurred", null);
}
}public record ErrorResponse(
String code,
String message,
Map<String, String> details,
Instant timestamp
) {
public ErrorResponse(String code, String message, Map<String, String> details) {
this(code, message, details, Instant.now());
}
}Cách tiếp cận này đảm bảo mọi lỗi có cùng định dạng, giúp phía client xử lý dễ dàng.
13. @RequestParam, @PathVariable và @RequestBody khác gì nhau?
Ba annotation này lấy dữ liệu từ các phần khác nhau của yêu cầu HTTP.
@RestController
@RequestMapping("/api/products")
public class ProductController {
// @PathVariable: extracts values from the URL
// GET /api/products/123
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return productService.findById(id);
}
// @RequestParam: extracts query parameters
// GET /api/products?category=electronics&minPrice=100&page=0
@GetMapping
public Page<Product> searchProducts(
@RequestParam(required = false) String category,
@RequestParam(defaultValue = "0") BigDecimal minPrice,
@RequestParam(defaultValue = "0") int page
) {
return productService.search(category, minPrice, PageRequest.of(page, 20));
}
// @RequestBody: deserializes the JSON request body
// POST /api/products with {"name": "...", "price": ...}
@PostMapping
public Product createProduct(@Valid @RequestBody CreateProductRequest request) {
return productService.create(request);
}
// Combination of all three in the same method
// PUT /api/products/123?notify=true with JSON body
@PutMapping("/{id}")
public Product updateProduct(
@PathVariable Long id,
@RequestParam(defaultValue = "false") boolean notify,
@Valid @RequestBody UpdateProductRequest request
) {
Product updated = productService.update(id, request);
if (notify) {
notificationService.sendUpdateNotification(updated);
}
return updated;
}
}@PathVariable xác định một tài nguyên cụ thể, @RequestParam lọc hoặc phân trang kết quả, còn @RequestBody truyền dữ liệu phức tạp.
14. Triển khai versioning API thế nào?
Có nhiều chiến lược để đặt phiên bản cho REST API, mỗi chiến lược có ưu thế riêng.
// Version via URL (recommended for clarity)
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
@GetMapping("/{id}")
public UserV1Response getUser(@PathVariable Long id) {
return userService.findByIdV1(id);
}
}
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
@GetMapping("/{id}")
public UserV2Response getUser(@PathVariable Long id) {
// V2 includes additional fields
return userService.findByIdV2(id);
}
}// Version via custom header
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping(value = "/{id}", headers = "X-API-Version=1")
public UserV1Response getUserV1(@PathVariable Long id) {
return userService.findByIdV1(id);
}
@GetMapping(value = "/{id}", headers = "X-API-Version=2")
public UserV2Response getUserV2(@PathVariable Long id) {
return userService.findByIdV2(id);
}
}// Version via media type (content negotiation)
@RestController
@RequestMapping("/api/users")
public class UserMediaTypeController {
@GetMapping(value = "/{id}", produces = "application/vnd.myapi.v1+json")
public UserV1Response getUserV1(@PathVariable Long id) {
return userService.findByIdV1(id);
}
@GetMapping(value = "/{id}", produces = "application/vnd.myapi.v2+json")
public UserV2Response getUserV2(@PathVariable Long id) {
return userService.findByIdV2(id);
}
}Versioning qua URL vẫn phổ biến nhất nhờ sự đơn giản và dễ thấy trong log lẫn tài liệu.
15. Cấu hình CORS trong Spring Boot ra sao?
CORS (Cross-Origin Resource Sharing) kiểm soát các yêu cầu xuất phát từ origin khác. Có nhiều mức cấu hình.
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://frontend.example.com", "https://admin.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("X-Total-Count", "X-Page-Count")
.allowCredentials(true)
.maxAge(3600); // Cache preflight for 1 hour
}
}// Per-controller or method configuration
@RestController
@RequestMapping("/api/public")
@CrossOrigin(origins = "*", maxAge = 3600)
public class PublicApiController {
@GetMapping("/data")
public DataResponse getPublicData() {
return dataService.getPublicData();
}
// Override at method level
@CrossOrigin(origins = "https://specific-client.com")
@PostMapping("/submit")
public SubmitResponse submitData(@RequestBody SubmitRequest request) {
return dataService.submit(request);
}
}Ở môi trường sản xuất, hãy giới hạn origin cho phép và dùng HTTPS. Không nên kết hợp * với allowCredentials(true).
Spring Security
16. Cấu hình Spring Security với JWT
Spring Security 6+ dùng cách tiếp cận hàm cho cấu hình bảo mật.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // Disabled for stateless API
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/**").authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain
) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
String jwt = authHeader.substring(7);
String username = jwtService.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
chain.doFilter(request, response);
}
}Cấu hình này tạo ra API stateless trong đó mỗi yêu cầu phải kèm theo token JWT hợp lệ ở header Authorization.
17. Bảo vệ phương thức bằng @PreAuthorize và @PostAuthorize
Bảo mật cấp phương thức cho phép kiểm soát chi tiết theo vai trò, quyền hạn hoặc dữ liệu.
@Service
@PreAuthorize("isAuthenticated()") // All methods require authentication
public class UserService {
// Only ADMINs can list all users
@PreAuthorize("hasRole('ADMIN')")
public List<User> findAll() {
return userRepository.findAll();
}
// User can view their profile OR admins can view any profile
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
// Post-execution check: user can only see their own data
@PostAuthorize("returnObject.owner.id == authentication.principal.id or hasRole('ADMIN')")
public Document getDocument(Long documentId) {
return documentRepository.findById(documentId)
.orElseThrow(() -> new DocumentNotFoundException(documentId));
}
// Collection filtering: returns only authorized elements
@PostFilter("filterObject.owner.id == authentication.principal.id")
public List<Project> getUserProjects() {
return projectRepository.findAll();
}
}@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// Additional configuration if needed
}@PreAuthorize kiểm tra trước khi thực thi, @PostAuthorize kiểm tra sau. Biểu thức SpEL hỗ trợ luật phức tạp.
18. Bảo vệ endpoint khỏi tấn công CSRF
CSRF (Cross-Site Request Forgery) khai thác phiên của người dùng đã đăng nhập. Bảo vệ được bật mặc định cho ứng dụng dùng phiên.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// CSRF configuration for session-based applications
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
// Exclude certain endpoints from CSRF protection
.ignoringRequestMatchers("/api/webhooks/**")
)
.build();
}
}@RestController
public class CsrfController {
@GetMapping("/api/csrf")
public CsrfToken csrf(CsrfToken token) {
// Returns CSRF token to frontend
return token;
}
}Với REST API stateless dùng JWT, CSRF có thể tắt vì không có cookie phiên để khai thác. Còn ứng dụng truyền thống dùng phiên thì cần kích hoạt bảo vệ.
API JWT vốn được bảo vệ khỏi CSRF vì token phải được kèm rõ ràng trong từng yêu cầu. Ứng dụng dùng cookie phiên cần CSRF protection chủ động.
Kiểm thử trong Spring Boot
19. @SpringBootTest và @WebMvcTest khác nhau ra sao?
Hai annotation này dựng ngữ cảnh kiểm thử khác nhau tùy nhu cầu.
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {
// Loads full context: all beans, database, etc.
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldCreateUser() {
CreateUserRequest request = new CreateUserRequest("John", "john@example.com");
ResponseEntity<User> response = restTemplate.postForEntity(
"/api/users", request, User.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody().getName()).isEqualTo("John");
}
}@WebMvcTest(UserController.class) // Loads only the web layer
class UserControllerUnitTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean // Replaces @MockBean (deprecated)
private UserService userService;
@Test
void shouldReturnUser() throws Exception {
// Mock configuration
when(userService.findById(1L))
.thenReturn(Optional.of(new User(1L, "John", "john@example.com")));
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John"))
.andExpect(jsonPath("$.email").value("john@example.com"));
}
@Test
void shouldReturn404WhenUserNotFound() throws Exception {
when(userService.findById(999L)).thenReturn(Optional.empty());
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound());
}
}@WebMvcTest nhanh hơn vì chỉ tải controller được chỉ định và các phụ thuộc trực tiếp. @SpringBootTest cần thiết cho kiểm thử tích hợp đầy đủ.
20. Kiểm thử repository với @DataJpaTest
Annotation này dựng ngữ cảnh tối thiểu để kiểm thử lớp persistence với CSDL nhúng.
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE) // Use Testcontainers
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void shouldFindByEmail() {
// Arrange: create and persist an entity
User user = new User("John", "john@example.com");
entityManager.persistAndFlush(user);
entityManager.clear(); // Clear L1 cache
// Act
Optional<User> found = userRepository.findByEmail("john@example.com");
// Assert
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("John");
}
@Test
void shouldFindActiveUsersWithOrders() {
// Testing a complex query with joins
User activeUser = entityManager.persistAndFlush(new User("Active", "active@test.com", true));
User inactiveUser = entityManager.persistAndFlush(new User("Inactive", "inactive@test.com", false));
entityManager.persistAndFlush(new Order(activeUser, BigDecimal.valueOf(100)));
List<User> result = userRepository.findActiveUsersWithOrders();
assertThat(result).hasSize(1);
assertThat(result.get(0).getEmail()).isEqualTo("active@test.com");
}
}TestEntityManager cung cấp các tiện ích cho kiểm thử JPA, đáng chú ý là persistAndFlush() đảm bảo dữ liệu được ghi xuống DB tức thì.
21. Sử dụng Testcontainers cho kiểm thử tích hợp
Testcontainers khởi động container Docker chứa CSDL hoặc dịch vụ thật trong khi chạy test.
@SpringBootTest
@Testcontainers
abstract class IntegrationTestBase {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Container
@ServiceConnection
static RedisContainer redis = new RedisContainer("redis:7");
}class OrderServiceIntegrationTest extends IntegrationTestBase {
@Autowired
private OrderService orderService;
@Autowired
private OrderRepository orderRepository;
@Test
void shouldProcessOrderWithRealDatabase() {
// Create an order
Order order = orderService.createOrder(
new CreateOrderRequest(1L, List.of(
new OrderItem(101L, 2),
new OrderItem(102L, 1)
))
);
// Verify in database
Order saved = orderRepository.findById(order.getId()).orElseThrow();
assertThat(saved.getItems()).hasSize(2);
assertThat(saved.getStatus()).isEqualTo(OrderStatus.PENDING);
}
@Test
void shouldCacheOrderInRedis() {
Order order = orderService.createOrder(createSampleOrderRequest());
// First call: database
Order fetched1 = orderService.findById(order.getId());
// Second call: Redis cache
Order fetched2 = orderService.findById(order.getId());
assertThat(fetched1).isEqualTo(fetched2);
// Verify cache metrics if needed
}
}Annotation @ServiceConnection tự động cấu hình thuộc tính kết nối, không cần dùng @DynamicPropertySource.
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.
Cấu hình và giám sát
22. Tách cấu hình ra ngoài bằng @ConfigurationProperties
Annotation này ánh xạ properties sang đối tượng Java có kiểu kèm validation.
@ConfigurationProperties(prefix = "app.mail")
@Validated
public record MailProperties(
@NotBlank String host,
@Min(1) @Max(65535) int port,
@NotBlank String username,
@NotBlank String password,
@Valid SenderConfig sender,
@Valid RetryConfig retry
) {
public record SenderConfig(
@NotBlank @Email String address,
@NotBlank String name
) {}
public record RetryConfig(
@Min(1) int maxAttempts,
@Min(100) long delayMs
) {
public RetryConfig {
// Default values
if (maxAttempts == 0) maxAttempts = 3;
if (delayMs == 0) delayMs = 1000;
}
}
}# application.yml
app:
mail:
host: smtp.example.com
port: 587
username: ${MAIL_USERNAME}
password: ${MAIL_PASSWORD}
sender:
address: noreply@example.com
name: MyApp Notifications
retry:
max-attempts: 3
delay-ms: 1000@Configuration
@EnableConfigurationProperties(MailProperties.class)
public class MailConfig {
@Bean
public JavaMailSender mailSender(MailProperties props) {
JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost(props.host());
sender.setPort(props.port());
sender.setUsername(props.username());
sender.setPassword(props.password());
return sender;
}
}Record trong Java (từ Java 16) đặc biệt phù hợp với @ConfigurationProperties vì bất biến và cô đọng.
23. Dùng profile Spring cho các môi trường khác nhau
Profile cho phép tùy chỉnh cấu hình theo môi trường thực thi.
# application.yml - Default configuration
spring:
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
datasource:
url: ${DATABASE_URL:jdbc:h2:mem:testdb}
---
# application-dev.yml
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:postgresql://localhost:5432/myapp_dev
jpa:
show-sql: true
hibernate:
ddl-auto: update
logging:
level:
com.example: DEBUG
---
# application-prod.yml
spring:
config:
activate:
on-profile: prod
datasource:
url: ${DATABASE_URL}
hikari:
maximum-pool-size: 20
jpa:
show-sql: false
hibernate:
ddl-auto: validate
logging:
level:
com.example: INFO@Component
@Profile("dev") // Only active in development
public class DevDataInitializer implements CommandLineRunner {
private final UserRepository userRepository;
@Override
public void run(String... args) {
// Create test data
userRepository.save(new User("dev@example.com", "Dev User", "password"));
userRepository.save(new User("test@example.com", "Test User", "password"));
}
}// Service with conditional behavior
@Service
public class NotificationService {
@Value("${app.notifications.enabled:true}")
private boolean notificationsEnabled;
@Autowired(required = false) // Optional based on profile
private EmailService emailService;
public void sendNotification(String message) {
if (notificationsEnabled && emailService != null) {
emailService.send(message);
} else {
log.info("Notification (disabled): {}", message);
}
}
}Có thể kết hợp profile: spring.profiles.active=prod,metrics kích hoạt cả hai cùng lúc.
24. Phơi bày metric tùy chỉnh với Actuator và Micrometer
Micrometer cung cấp lớp facade cho các hệ thống giám sát. Metric tùy chỉnh giúp tăng khả năng quan sát.
@Component
public class OrderMetrics {
private final Counter ordersCreated;
private final Counter ordersFailed;
private final Timer orderProcessingTime;
private final AtomicInteger activeOrders;
public OrderMetrics(MeterRegistry registry) {
// Counter for orders created by status
this.ordersCreated = Counter.builder("orders.created")
.description("Number of orders created")
.tag("application", "order-service")
.register(registry);
this.ordersFailed = Counter.builder("orders.failed")
.description("Number of failed orders")
.register(registry);
// Timer to measure processing time
this.orderProcessingTime = Timer.builder("orders.processing.time")
.description("Order processing time")
.publishPercentiles(0.5, 0.95, 0.99)
.register(registry);
// Gauge for active orders count
this.activeOrders = new AtomicInteger(0);
Gauge.builder("orders.active", activeOrders, AtomicInteger::get)
.description("Number of orders being processed")
.register(registry);
}
public void recordOrderCreated() {
ordersCreated.increment();
}
public void recordOrderFailed() {
ordersFailed.increment();
}
public Timer.Sample startProcessing() {
activeOrders.incrementAndGet();
return Timer.start();
}
public void stopProcessing(Timer.Sample sample) {
sample.stop(orderProcessingTime);
activeOrders.decrementAndGet();
}
}@Service
public class OrderService {
private final OrderMetrics metrics;
public Order processOrder(CreateOrderRequest request) {
Timer.Sample sample = metrics.startProcessing();
try {
Order order = createOrder(request);
metrics.recordOrderCreated();
return order;
} catch (Exception e) {
metrics.recordOrderFailed();
throw e;
} finally {
metrics.stopProcessing(sample);
}
}
}Các metric này có thể truy cập qua /actuator/prometheus cho Prometheus hoặc /actuator/metrics cho API JSON.
25. Tạo HealthIndicator tùy chỉnh
Health indicator giám sát trạng thái các thành phần trọng yếu.
@Component
public class ExternalApiHealthIndicator implements HealthIndicator {
private final RestClient restClient;
private final ExternalApiProperties properties;
@Override
public Health health() {
try {
long startTime = System.currentTimeMillis();
ResponseEntity<Void> response = restClient.get()
.uri(properties.getHealthEndpoint())
.retrieve()
.toBodilessEntity();
long responseTime = System.currentTimeMillis() - startTime;
if (response.getStatusCode().is2xxSuccessful()) {
return Health.up()
.withDetail("responseTime", responseTime + "ms")
.withDetail("endpoint", properties.getHealthEndpoint())
.build();
} else {
return Health.down()
.withDetail("status", response.getStatusCode().value())
.build();
}
} catch (Exception e) {
return Health.down()
.withException(e)
.withDetail("endpoint", properties.getHealthEndpoint())
.build();
}
}
}@Component
public class DatabaseHealthContributor implements CompositeHealthContributor {
private final Map<String, HealthIndicator> indicators;
public DatabaseHealthContributor(
DataSource primaryDataSource,
@Qualifier("replicaDataSource") DataSource replicaDataSource
) {
this.indicators = Map.of(
"primary", new DataSourceHealthIndicator(primaryDataSource),
"replica", new DataSourceHealthIndicator(replicaDataSource)
);
}
@Override
public HealthContributor getContributor(String name) {
return indicators.get(name);
}
@Override
public Iterator<NamedContributor<HealthContributor>> iterator() {
return indicators.entrySet().stream()
.map(e -> NamedContributor.of(e.getKey(), e.getValue()))
.iterator();
}
}Endpoint /actuator/health tổng hợp toàn bộ indicator. Cấu hình management.endpoint.health.show-details=always sẽ hiển thị chi tiết.
Khái niệm nâng cao
26. Triển khai cache với Spring Cache và Redis
Lớp trừu tượng Spring Cache giúp tích hợp các cache phân tán như Redis dễ dàng hơn.
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
// Specific configurations per cache
Map<String, RedisCacheConfiguration> cacheConfigs = Map.of(
"users", config.entryTtl(Duration.ofHours(1)),
"products", config.entryTtl(Duration.ofMinutes(30)),
"sessions", config.entryTtl(Duration.ofMinutes(5))
);
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.withInitialCacheConfigurations(cacheConfigs)
.build();
}
}@Service
public class ProductService {
// Automatic result caching
@Cacheable(value = "products", key = "#id")
public Product findById(Long id) {
log.info("Fetching product {} from database", id);
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
// Cache update on modification
@CachePut(value = "products", key = "#product.id")
public Product update(Product product) {
return productRepository.save(product);
}
// Cache invalidation
@CacheEvict(value = "products", key = "#id")
public void delete(Long id) {
productRepository.deleteById(id);
}
// Clear entire cache
@CacheEvict(value = "products", allEntries = true)
public void clearProductCache() {
log.info("Product cache cleared");
}
// Composite key for searches
@Cacheable(value = "productSearch", key = "#category + '-' + #minPrice + '-' + #maxPrice")
public List<Product> search(String category, BigDecimal minPrice, BigDecimal maxPrice) {
return productRepository.findByCategoryAndPriceBetween(category, minPrice, maxPrice);
}
}Các annotation cache là trong suốt: mã nghiệp vụ giữ nguyên còn cache được khai báo theo lối declarative.
27. Thực thi tác vụ định kỳ với @Scheduled
Spring có nhiều cách để lập lịch tác vụ lặp lại.
@Configuration
@EnableScheduling
public class SchedulerConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5);
scheduler.setThreadNamePrefix("scheduled-");
scheduler.setErrorHandler(t -> log.error("Scheduled task error", t));
return scheduler;
}
}@Component
public class ScheduledTasks {
private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);
// Execute every 5 minutes
@Scheduled(fixedRate = 5 * 60 * 1000)
public void syncExternalData() {
log.info("Starting external data sync");
// Sync with external system
}
// Execute 10 seconds after the previous one ends
@Scheduled(fixedDelay = 10000, initialDelay = 5000)
public void processQueue() {
// Queue processing with delay between executions
}
// Cron expression: every day at 2 AM
@Scheduled(cron = "0 0 2 * * *", zone = "Europe/Paris")
public void dailyCleanup() {
log.info("Running daily cleanup");
cleanupService.removeExpiredSessions();
cleanupService.archiveOldData();
}
// Configurable cron via properties
@Scheduled(cron = "${app.reports.cron:0 0 6 * * MON}")
public void generateWeeklyReport() {
reportService.generateAndSend();
}
}@Component
@ConditionalOnProperty(name = "app.scheduling.enabled", havingValue = "true")
public class ConditionalScheduledTasks {
@Scheduled(fixedRate = 60000)
public void monitorSystem() {
// Monitoring active only if configured
}
}Tác vụ định kỳ chạy trên một thread pool riêng để không cản trở việc xử lý request.
28. Xử lý sự kiện với ApplicationEvent
Hệ thống sự kiện giúp tách rời các thành phần trong ứng dụng.
public class UserRegisteredEvent extends ApplicationEvent {
private final User user;
private final Instant registeredAt;
public UserRegisteredEvent(Object source, User user) {
super(source);
this.user = user;
this.registeredAt = Instant.now();
}
public User getUser() { return user; }
public Instant getRegisteredAt() { return registeredAt; }
}@Service
public class UserService {
private final ApplicationEventPublisher eventPublisher;
@Transactional
public User registerUser(CreateUserRequest request) {
User user = new User(request.email(), request.name());
user = userRepository.save(user);
// Publish event after transaction
eventPublisher.publishEvent(new UserRegisteredEvent(this, user));
return user;
}
}@Component
public class UserEventListeners {
private static final Logger log = LoggerFactory.getLogger(UserEventListeners.class);
// Synchronous listener
@EventListener
public void handleUserRegistered(UserRegisteredEvent event) {
log.info("User registered: {}", event.getUser().getEmail());
}
// Asynchronous listener to avoid blocking the main thread
@Async
@EventListener
public void sendWelcomeEmail(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.getUser());
}
// Transactional listener: executes after commit
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void notifyExternalSystem(UserRegisteredEvent event) {
// External notification only if transaction succeeds
externalApiClient.notifyNewUser(event.getUser().getId());
}
// Conditional listener with SpEL
@EventListener(condition = "#event.user.role == 'PREMIUM'")
public void handlePremiumUserRegistered(UserRegisteredEvent event) {
premiumService.initializePremiumFeatures(event.getUser());
}
}Sự kiện transactional (@TransactionalEventListener) đảm bảo nhất quán giữa CSDL và các tác vụ phụ trợ.
29. Triển khai HTTP client với RestClient (Spring Boot 3+)
RestClient là API hiện đại, fluent dành cho gọi HTTP đồng bộ, thay thế cho RestTemplate.
@Component
public class ExternalApiClient {
private final RestClient restClient;
public ExternalApiClient(RestClient.Builder builder, ExternalApiProperties props) {
this.restClient = builder
.baseUrl(props.getBaseUrl())
.defaultHeader("Authorization", "Bearer " + props.getApiKey())
.defaultHeader("Accept", "application/json")
.requestInterceptor((request, body, execution) -> {
log.debug("Request: {} {}", request.getMethod(), request.getURI());
return execution.execute(request, body);
})
.build();
}
public User fetchUser(Long id) {
return restClient.get()
.uri("/users/{id}", id)
.retrieve()
.body(User.class);
}
public List<Product> searchProducts(String query, int page) {
return restClient.get()
.uri(uriBuilder -> uriBuilder
.path("/products/search")
.queryParam("q", query)
.queryParam("page", page)
.build())
.retrieve()
.body(new ParameterizedTypeReference<List<Product>>() {});
}
public Order createOrder(CreateOrderRequest request) {
return restClient.post()
.uri("/orders")
.contentType(MediaType.APPLICATION_JSON)
.body(request)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (req, res) -> {
throw new OrderValidationException("Invalid order: " + res.getStatusCode());
})
.body(Order.class);
}
// Error handling with ResponseEntity
public Optional<User> findUser(Long id) {
ResponseEntity<User> response = restClient.get()
.uri("/users/{id}", id)
.retrieve()
.toEntity(User.class);
return response.getStatusCode().is2xxSuccessful()
? Optional.ofNullable(response.getBody())
: Optional.empty();
}
}RestClient có API tương tự WebClient nhưng cho lời gọi đồng bộ, lý tưởng cho các ứng dụng không dùng lập trình phản ứng.
30. Quản lý migration CSDL với Flyway
Flyway tự động hóa migration schema theo phiên bản và có thể tái lập.
# application.properties
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
spring.flyway.baseline-on-migrate=true
spring.flyway.validate-on-migrate=true-- db/migration/V1__create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);-- db/migration/V2__add_user_status.sql
ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'ACTIVE';
CREATE INDEX idx_users_status ON users(status);-- db/migration/V3__create_orders_table.sql
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
total_amount DECIMAL(10, 2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);@Configuration
public class FlywayConfig {
@Bean
public FlywayMigrationStrategy migrationStrategy() {
return flyway -> {
// Validation before migration
flyway.validate();
// Execute migrations
flyway.migrate();
};
}
// Callback for post-migration actions
@Bean
public Callback flywayCallback() {
return new BaseCallback() {
@Override
public void handle(Event event, Context context) {
if (event == Event.AFTER_MIGRATE) {
log.info("Migrations completed successfully");
}
}
};
}
}Mỗi file migration chỉ chạy một lần và checksum của nó được kiểm tra để phát hiện sửa đổi vô tình.
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.
Kết luận
30 câu hỏi này bao quát những phần quan trọng của Spring Boot mà các buổi phỏng vấn kỹ thuật thường đánh giá. Nắm vững các khái niệm này thể hiện sự hiểu biết sâu về framework cũng như thực hành phát triển Java.
Danh sách chuẩn bị:
- ✅ Hiểu cơ chế auto-configuration cùng các điều kiện đi kèm
- ✅ Thông thạo Spring Data JPA: truy vấn, transaction, vấn đề N+1
- ✅ Cấu hình Spring Security với JWT và bảo mật cấp phương thức
- ✅ Biết các annotation kiểm thử và trường hợp sử dụng
- ✅ Sử dụng profile cùng @ConfigurationProperties cho cấu hình
- ✅ Triển khai cache, scheduling và sự kiện
- ✅ Phơi bày metric tùy chỉnh và health indicator
- ✅ Thông thạo RestClient cho lời gọi HTTP hiện đại
Luyện tập đều đặn trên dự án thực tế vẫn là cách tốt nhất để củng cố kiến thức và tự tin trả lời các câu hỏi kỹ thuật.
Thẻ
Chia sẻ
Bài viết liên quan

Phỏng vấn Spring Boot: Lan truyền Giao dịch
Làm chủ lan truyền giao dịch Spring Boot: REQUIRED, REQUIRES_NEW, NESTED và hơn thế. 12 câu hỏi phỏng vấn với mã ví dụ và bẫy thường gặp.

Spring Boot 3.4: Tất cả tính năng mới được giải thích chi tiết
Spring Boot 3.4 mang đến structured logging native, virtual threads mở rộng, graceful shutdown mặc định và MockMvcTester. Hướng dẫn đầy đủ các tính năng mới.

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.