# 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ử. - Published: 2026-02-07 - Updated: 2026-04-28 - Author: SharpSkill - Tags: spring boot, java, interview, backend, java interview - Reading time: 18 min --- 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. > **Mẹo chuẩn bị** > > 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. ```java // Application.java // 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. ```java // CustomAutoConfiguration.java @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. ```xml my-company-starter org.springframework.boot spring-boot-starter com.mycompany logging-utils ``` Để 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. ```properties # application.properties server.port=8080 spring.datasource.url=jdbc:postgresql://localhost:5432/mydb spring.datasource.username=admin spring.jpa.hibernate.ddl-auto=update ``` ```yaml # application.yml - clear hierarchical structure server: port: 8080 spring: datasource: url: jdbc:postgresql://localhost:5432/mydb username: admin jpa: hibernate: ddl-auto: update ``` Profile đ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. ```java // DemoApplication.java // @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. ```java // UserRepository.java // CrudRepository: basic CRUD operations public interface UserCrudRepository extends CrudRepository { // save(), findById(), findAll(), deleteById(), count(), existsById() } // PagingAndSortingRepository: adds pagination and sorting public interface UserPagingRepository extends PagingAndSortingRepository { // Inherits from CrudRepository + findAll(Sort), findAll(Pageable) } // JpaRepository: complete JPA features public interface UserRepository extends JpaRepository { // Inherits from previous + flush(), saveAndFlush(), deleteInBatch() // Derived query methods List findByEmailContaining(String email); Optional 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 đủ. ```java // OrderRepository.java public interface OrderRepository extends JpaRepository { // JPQL with named parameters @Query("SELECT o FROM Order o WHERE o.customer.id = :customerId AND o.status = :status") List 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 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 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. ```java // PaymentService.java @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. > **Cạm bẫy phổ biến** > > `@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. ```java // User.java @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 roles; // Lazy: orders are only loaded on access @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) private List orders; } ``` ```java // UserService.java @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 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. ```java // ArticleRepository.java public interface ArticleRepository extends JpaRepository { // PROBLEM: this query generates N+1 queries List
findAll(); // SOLUTION 1: JOIN FETCH in JPQL @Query("SELECT a FROM Article a JOIN FETCH a.author JOIN FETCH a.comments") List
findAllWithDetails(); // SOLUTION 2: Entity Graph @EntityGraph(attributePaths = {"author", "comments"}) @Query("SELECT a FROM Article a") List
findAllWithGraph(); } ``` ```java // Article.java @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 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ể. ## 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. ```java // UserController.java @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 listUsers( @RequestParam(defaultValue = "0") @Min(0) int page, @RequestParam(defaultValue = "20") @Max(100) int size ) { return userService.findAll(PageRequest.of(page, size)); } } ``` ```java // CreateUserRequest.java 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ã. ```java // GlobalExceptionHandler.java @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 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); } } ``` ```java // ErrorResponse.java public record ErrorResponse( String code, String message, Map details, Instant timestamp ) { public ErrorResponse(String code, String message, Map 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. ```java // ProductController.java @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 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. ```java // 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); } } ``` ```java // 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); } } ``` ```java // 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. ```java // CorsConfig.java - Global configuration @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 } } ``` ```java // 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. ```java // SecurityConfig.java @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(); } } ``` ```java // JwtAuthenticationFilter.java @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. ```java // UserService.java @Service @PreAuthorize("isAuthenticated()") // All methods require authentication public class UserService { // Only ADMINs can list all users @PreAuthorize("hasRole('ADMIN')") public List 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 getUserProjects() { return projectRepository.findAll(); } } ``` ```java // SecurityConfig.java - Activation @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. ```java // SecurityConfig.java @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(); } } ``` ```java // CsrfController.java - Endpoint to retrieve token (SPA) @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 stateless và ứng dụng dùng phiên** > > 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. ```java // UserControllerIntegrationTest.java @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 response = restTemplate.postForEntity( "/api/users", request, User.class ); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat(response.getBody().getName()).isEqualTo("John"); } } ``` ```java // UserControllerUnitTest.java @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. ```java // UserRepositoryTest.java @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 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 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. ```java // IntegrationTestBase.java @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"); } ``` ```java // OrderServiceIntegrationTest.java 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`. ## 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. ```java // MailProperties.java @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; } } } ``` ```yaml # 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 ``` ```java // MailConfig.java @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. ```yaml # 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 ``` ```java // DevDataInitializer.java @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")); } } ``` ```java // 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. ```java // OrderMetrics.java @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(); } } ``` ```java // OrderService.java @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. ```java // ExternalApiHealthIndicator.java @Component public class ExternalApiHealthIndicator implements HealthIndicator { private final RestClient restClient; private final ExternalApiProperties properties; @Override public Health health() { try { long startTime = System.currentTimeMillis(); ResponseEntity 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(); } } } ``` ```java // DatabaseHealthContributor.java - Composite health @Component public class DatabaseHealthContributor implements CompositeHealthContributor { private final Map 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> 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. ```java // CacheConfig.java @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 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(); } } ``` ```java // ProductService.java @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 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. ```java // SchedulerConfig.java @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; } } ``` ```java // ScheduledTasks.java @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(); } } ``` ```java // ConditionalScheduling.java - Conditional activation @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. ```java // UserRegisteredEvent.java 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; } } ``` ```java // UserService.java - Event publishing @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; } } ``` ```java // UserEventListeners.java - Event listeners @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`. ```java // ExternalApiClient.java @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 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>() {}); } 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 findUser(Long id) { ResponseEntity 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. ```properties # application.properties spring.flyway.enabled=true spring.flyway.locations=classpath:db/migration spring.flyway.baseline-on-migrate=true spring.flyway.validate-on-migrate=true ``` ```sql -- 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); ``` ```sql -- 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); ``` ```sql -- 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); ``` ```java // FlywayConfig.java - Advanced configuration @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. ## 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/spring-boot/30-spring-boot-interview-questions