# 30 คำถามสัมภาษณ์ Spring Boot: คู่มือฉบับเต็มสำหรับนักพัฒนา Java > เตรียมพร้อมสัมภาษณ์ Spring Boot ด้วย 30 คำถามหลักครอบคลุม auto-configuration, starter, Spring Data JPA, ความปลอดภัย และการทดสอบ - Published: 2026-02-07 - Updated: 2026-04-28 - Author: SharpSkill - Tags: spring boot, java, interview, backend, java interview - Reading time: 18 min --- Spring Boot กลายเป็นเฟรมเวิร์กหลักสำหรับการพัฒนา Java ในระดับองค์กร การสัมภาษณ์เชิงเทคนิคจะประเมินความเข้าใจกลไกภายใน หลักปฏิบัติที่ดี และระบบนิเวศของ Spring คู่มือฉบับนี้รวม 30 คำถามที่พบบ่อย ตั้งแต่แนวคิดพื้นฐานจนถึงหัวข้อขั้นสูง > **เคล็ดลับเตรียมตัว** > > ผู้สัมภาษณ์ชื่นชอบผู้สมัครที่เข้าใจ "เหตุผล" เบื้องหลังของแต่ละฟีเจอร์ นอกเหนือจาก syntax ควรอธิบายว่า Spring Boot แก้ปัญหาอะไรได้ ## พื้นฐานของ Spring Boot ### 1. Spring กับ Spring Boot ต่างกันอย่างไร? Spring เป็นเฟรมเวิร์กแบบโมดูลที่ให้ dependency injection, การจัดการธุรกรรม และการเชื่อมต่อกับเทคโนโลยีหลายชนิด ส่วน Spring Boot คือเลเยอร์นามธรรมที่อยู่บน Spring เพื่อทำให้การตั้งค่าและการเริ่มต้นแอปพลิเคชันง่ายขึ้น ```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 มอบ auto-configuration, dependency starter, เซิร์ฟเวอร์ฝังตัว และเมตริกระดับโปรดักชันผ่าน Actuator เป้าหมายคือไปจากไอเดียสู่โค้ดที่ทำงานได้ภายในไม่กี่นาที ### 2. Auto-configuration ทำงานอย่างไร? Auto-configuration วิเคราะห์ classpath แล้วตั้งค่า bean ที่จำเป็นโดยอัตโนมัติ กลไกนี้อาศัยแอนโนเทชัน `@EnableAutoConfiguration` (รวมอยู่ใน `@SpringBootApplication`) และเงื่อนไขที่ประกาศไว้ในคลาส 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 ตรวจสอบเงื่อนไข (`@ConditionalOn*`) เพื่อตัดสินใจว่าจะสร้าง bean ใดบ้าง วิธีนี้ช่วยลดความขัดแย้งและทำให้สามารถเขียนทับการตั้งค่าเริ่มต้นได้ง่าย ### 3. Starter คืออะไรและสร้างได้อย่างไร? Starter คือโมดูล Maven/Gradle ที่รวบรวม dependency และค่าตั้งค่าที่จำเป็นต่อฟีเจอร์ใดฟีเจอร์หนึ่ง ตัวอย่างเช่น `spring-boot-starter-web` รวม Spring MVC, Jackson, Tomcat ฝังตัว และ validation ```xml my-company-starter org.springframework.boot spring-boot-starter com.mycompany logging-utils ``` หากต้องการสร้าง starter เอง ให้นิยามคลาส auto-configuration และอ้างอิงไว้ใน `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` ### 4. application.properties กับ application.yml ต่างกันอย่างไร? รูปแบบทั้งสองช่วยแยกการตั้งค่าออกจากโค้ด YAML เหมาะกับโครงสร้างซ้อนกันด้วย syntax ที่อ่านง่าย ส่วน properties ยังเหมาะกับโครงสร้างแบบราบมากกว่า ```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 ช่วยปรับการตั้งค่าให้เหมาะกับสภาพแวดล้อม: `application-dev.yml`, `application-prod.yml` เปิดใช้งานผ่าน `spring.profiles.active` ### 5. @SpringBootApplication ทำงานอย่างไร? แอนโนเทชันนี้รวมแอนโนเทชันสำคัญสามตัวที่กำหนดพฤติกรรมของแอป 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); } } ``` การวางคลาสนี้ไว้ที่ root ของแพ็กเกจสำคัญมาก: `@ComponentScan` จะสแกนแพ็กเกจนั้นพร้อมแพ็กเกจย่อยทั้งหมดเพื่อค้นหาคอมโพเนนต์ ## Spring Data และการคงอยู่ของข้อมูล ### 6. JpaRepository, CrudRepository และ PagingAndSortingRepository ต่างกันอย่างไร? อินเทอร์เฟซเหล่านี้เป็นลำดับชั้นที่ให้ความสามารถในการเข้าถึงข้อมูลเพิ่มขึ้นเรื่อย ๆ ```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); } ``` สำหรับการใช้งานทั่วไป `JpaRepository` เป็นตัวเลือกที่แนะนำ เพราะให้ฟังก์ชันที่จำเป็นต่อการพัฒนาแอป JPA อย่างครบถ้วน ### 7. กำหนด query เองด้วย @Query อย่างไร? แอนโนเทชัน `@Query` ทำให้สามารถเขียน JPQL หรือ SQL native ได้เมื่อ derived method ไม่เพียงพอ ```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); } ``` Query แบบ JPQL พกพาได้ระหว่างฐานข้อมูล ในขณะที่ native query ให้เข้าถึงฟีเจอร์เฉพาะของ DBMS ### 8. การจัดการธุรกรรมด้วย @Transactional แอนโนเทชันนี้ระบุขอบเขตธุรกรรมของเมธอดหรือคลาส Spring จะจัดการ commit, rollback และการส่งต่อให้อัตโนมัติ ```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)); } } ``` ระดับ propagation (`REQUIRED`, `REQUIRES_NEW`, `NESTED` ฯลฯ) กำหนดวิธีที่ธุรกรรมซ้อนกันเมื่อเรียกเมธอดที่เป็น transactional ต่อกันเป็นทอด ๆ > **กับดักที่พบบ่อย** > > `@Transactional` ทำงานเฉพาะการเรียกที่ผ่าน proxy ของ Spring การเรียกภายในคลาสเดียวกันจะข้าม proxy และไม่สนใจแอนโนเทชัน ### 9. จัดการความสัมพันธ์ Lazy กับ Eager Loading อย่างไร? โหมดการโหลดความสัมพันธ์ส่งผลโดยตรงต่อประสิทธิภาพ Lazy Loading จะหน่วงการโหลดจนกว่าจะมีการเข้าถึง ส่วน Eager Loading จะโหลดทันที ```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()); } } ``` ปัญหา N+1 เกิดเมื่อ lazy loading สร้าง query หนึ่งคำสั่งต่อสมาชิกหนึ่งตัวในคอลเลกชัน การใช้ `JOIN FETCH` หรือ projection ช่วยแก้ได้ ### 10. ปัญหา N+1 คืออะไรและแก้อย่างไร? ปัญหา N+1 เกิดเมื่อหลังจาก query เริ่มต้น (1) ตามมาด้วย query เพิ่มอีก N รายการเพื่อโหลดความสัมพันธ์ของแต่ละ 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; } ``` การใช้ `@EntityGraph` หรือ `JOIN FETCH` ช่วยลด N+1 ให้เหลือ query เดียวที่มี join ทำให้ประสิทธิภาพดีขึ้นชัดเจน ## REST API และเว็บ ### 11. สร้าง REST controller พร้อม validation อย่างไร? Spring Boot ผสาน `@RestController` กับ Bean Validation เพื่อสร้าง API ที่แข็งแรงและตรวจสอบอินพุตอัตโนมัติ ```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 ) {} ``` สามารถนำข้อความ validation ออกไปอยู่ในไฟล์ message เพื่อรองรับหลายภาษาได้ ### 12. จัดการ exception แบบรวมศูนย์ด้วย @ControllerAdvice อย่างไร? ตัวจัดการ exception รวมศูนย์ช่วยให้รูปแบบของการตอบกลับข้อผิดพลาดเป็นมาตรฐานและลดโค้ดซ้ำ ```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()); } } ``` วิธีนี้ช่วยให้ทุกข้อผิดพลาดอยู่ในรูปแบบเดียวกันและฝั่งไคลเอ็นต์จัดการง่ายขึ้น ### 13. @RequestParam, @PathVariable และ @RequestBody แตกต่างกันอย่างไร? แอนโนเทชันทั้งสามดึงข้อมูลจากส่วนต่าง ๆ ของ HTTP request ```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` ใช้ระบุ resource เจาะจง, `@RequestParam` ใช้กรองหรือแบ่งหน้าผลลัพธ์ และ `@RequestBody` ใช้ส่งข้อมูลที่ซับซ้อน ### 14. ทำ versioning ของ API ได้อย่างไร? มีหลายกลยุทธ์ในการกำหนดเวอร์ชันให้ REST API แต่ละแบบมีจุดเด่นต่างกัน ```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); } } ``` การกำหนดเวอร์ชันผ่าน URL ยังคงได้รับความนิยมมากที่สุด เพราะเรียบง่ายและเห็นชัดในล็อกและเอกสาร ### 15. ตั้งค่า CORS ใน Spring Boot อย่างไร? CORS (Cross-Origin Resource Sharing) ควบคุมคำขอจากต้นทางต่าง ๆ มีหลายระดับการตั้งค่าให้เลือกใช้ ```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); } } ``` บนโปรดักชันควรจำกัด origin ที่อนุญาตและใช้ HTTPS อย่าใช้ `*` ร่วมกับ `allowCredentials(true)` เด็ดขาด ## Spring Security ### 16. ตั้งค่า Spring Security กับ JWT Spring Security 6+ ใช้แนวทางเชิงฟังก์ชันสำหรับการตั้งค่าความปลอดภัย ```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); } } ``` การตั้งค่านี้สร้าง API แบบไม่มีสถานะที่ทุกคำขอต้องแนบ JWT token ที่ถูกต้องในเฮดเดอร์ Authorization ### 17. ป้องกันเมธอดด้วย @PreAuthorize และ @PostAuthorize ความปลอดภัยระดับเมธอดให้การควบคุมที่ละเอียดตาม role, สิทธิ์ หรือข้อมูล ```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` ตรวจก่อนการเรียก `@PostAuthorize` ตรวจหลังการเรียก ส่วน SpEL ทำให้สามารถเขียนกฎซับซ้อนได้ ### 18. ป้องกัน endpoint จากการโจมตี CSRF CSRF (Cross-Site Request Forgery) ใช้ประโยชน์จากเซสชันของผู้ใช้ที่ล็อกอินอยู่ การป้องกันถูกเปิดใช้งานโดยปริยายสำหรับแอปที่ใช้เซสชัน ```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; } } ``` สำหรับ REST API แบบ stateless ที่ใช้ JWT สามารถปิด CSRF ได้เพราะไม่มีคุกกี้เซสชันให้แสวงประโยชน์ ส่วนแอปแบบดั้งเดิมที่ใช้เซสชันต้องเปิดป้องกัน CSRF เสมอ > **API stateless กับแอปที่ใช้เซสชัน** > > API ที่ใช้ JWT จะปลอดภัยจาก CSRF โดยธรรมชาติ เพราะ token ต้องถูกแนบมาทุกคำขอ ส่วนแอปที่ใช้คุกกี้เซสชันจำเป็นต้องเปิด CSRF protection เสมอ ## การทดสอบใน Spring Boot ### 19. @SpringBootTest กับ @WebMvcTest แตกต่างอย่างไร? แอนโนเทชันเหล่านี้กำหนดบริบทการทดสอบต่างกันตามความต้องการ ```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` เร็วกว่าเพราะโหลดเฉพาะ controller ที่ระบุและ dependency โดยตรง ส่วน `@SpringBootTest` ต้องใช้สำหรับการทดสอบแบบ integration เต็มรูปแบบ ### 20. ทดสอบ repository ด้วย @DataJpaTest แอนโนเทชันนี้สร้างบริบทขั้นต่ำเพื่อทดสอบเลเยอร์ persistence ด้วยฐานข้อมูลฝังตัว ```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` มีเมธอดอำนวยความสะดวกสำหรับการทดสอบ JPA โดยเฉพาะ `persistAndFlush()` ที่บังคับให้เขียนลงฐานข้อมูลทันที ### 21. ใช้ Testcontainers สำหรับการทดสอบเชิงผสาน Testcontainers จะรัน Docker container ที่มีฐานข้อมูลหรือบริการจริงระหว่างการทดสอบ ```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 } } ``` แอนโนเทชัน `@ServiceConnection` ตั้งค่าการเชื่อมต่ออัตโนมัติ ทำให้ไม่ต้องใช้ `@DynamicPropertySource` ## การตั้งค่าและการมอนิเตอร์ ### 22. แยกการตั้งค่าออกด้วย @ConfigurationProperties แอนโนเทชันนี้ผูก property เข้ากับออบเจ็กต์ Java ที่มีประเภทพร้อมการตรวจสอบความถูกต้อง ```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 ของ Java (ตั้งแต่ Java 16) เหมาะมากกับ `@ConfigurationProperties` เพราะแก้ไม่ได้และกระชับ ### 23. ใช้ profile ของ Spring สำหรับสภาพแวดล้อมต่างกัน Profile ปรับการตั้งค่าให้สอดคล้องกับสภาพแวดล้อมการรัน ```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); } } } ``` สามารถรวมหลาย profile ได้: `spring.profiles.active=prod,metrics` จะเปิดทั้งสองพร้อมกัน ### 24. ปล่อยเมตริกเฉพาะของแอปด้วย Actuator และ Micrometer Micrometer ทำหน้าที่เป็น facade สำหรับระบบมอนิเตอร์ เมตริกที่กำหนดเองทำให้สังเกตการณ์ระบบได้ดีขึ้น ```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); } } } ``` เมตริกเหล่านี้เปิดให้ Prometheus ที่ `/actuator/prometheus` หรือดึงเป็น JSON ที่ `/actuator/metrics` ### 25. สร้าง HealthIndicator เฉพาะของระบบ Health indicator คอยติดตามสถานะของส่วนประกอบที่สำคัญ ```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` รวมข้อมูลของ indicator ทั้งหมด ตั้งค่า `management.endpoint.health.show-details=always` เพื่อแสดงรายละเอียด ## หัวข้อขั้นสูง ### 26. ทำ caching ด้วย Spring Cache และ Redis ลำดับชั้นนามธรรมของ Spring Cache ทำให้เชื่อมต่อแคชแบบกระจาย เช่น Redis ได้ง่าย ```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); } } ``` แอนโนเทชัน cache เป็นแบบโปร่งใส โค้ดทางธุรกิจไม่เปลี่ยน และจัดการแคชด้วยรูปแบบ declarative ### 27. สร้างงานตามตารางเวลาด้วย @Scheduled Spring มีหลายวิธีในการตั้งเวลางานที่รันซ้ำ ```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 } } ``` งานตามกำหนดทำงานบน thread pool แยกต่างหาก เพื่อไม่ให้ขัดขวางการประมวลผลคำขอ ### 28. จัดการอีเวนต์ด้วย ApplicationEvent ระบบอีเวนต์ช่วยให้ส่วนประกอบของแอปถูกเชื่อมโยงแบบหลวม ```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()); } } ``` อีเวนต์แบบ transactional (`@TransactionalEventListener`) รักษาความสอดคล้องระหว่างฐานข้อมูลกับผลข้างเคียง ### 29. ใช้ HTTP client ด้วย RestClient (Spring Boot 3+) `RestClient` คือ API ที่ทันสมัยและไหลลื่นสำหรับการเรียก HTTP แบบซิงโครนัส แทนที่ `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` มี API คล้าย `WebClient` แต่ใช้สำหรับการเรียกแบบ blocking เหมาะกับแอปที่ไม่ใช้การเขียนโปรแกรมเชิงปฏิกิริยา ### 30. จัดการ migration ฐานข้อมูลด้วย Flyway Flyway ทำให้การ migrate schema เป็นอัตโนมัติพร้อมระบบเวอร์ชันและสามารถทำซ้ำได้ ```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"); } } }; } } ``` ไฟล์ migration แต่ละไฟล์จะรันเพียงครั้งเดียวและตรวจ checksum เพื่อตรวจจับการแก้ไขโดยบังเอิญ ## บทสรุป 30 คำถามนี้ครอบคลุมแก่นของ Spring Boot ที่ใช้ประเมินในการสัมภาษณ์เชิงเทคนิค การเข้าใจแนวคิดเหล่านี้แสดงให้เห็นถึงความเชี่ยวชาญในเฟรมเวิร์กและแนวทางที่ดีในการพัฒนา Java **เช็กลิสต์เตรียมตัว:** - ✅ เข้าใจกลไก auto-configuration และเงื่อนไขที่เกี่ยวข้อง - ✅ ชำนาญ Spring Data JPA: query, ธุรกรรม, ปัญหา N+1 - ✅ ตั้งค่า Spring Security ด้วย JWT และความปลอดภัยระดับเมธอดได้ - ✅ รู้จักแอนโนเทชันทดสอบต่าง ๆ และกรณีการใช้งาน - ✅ ใช้ profile และ @ConfigurationProperties เพื่อแยกการตั้งค่า - ✅ ทำ caching, scheduling และจัดการอีเวนต์ - ✅ เผยแพร่เมตริกและ health indicator ที่กำหนดเอง - ✅ ใช้ RestClient สำหรับการเรียก HTTP สมัยใหม่ การฝึกฝนสม่ำเสมอบนโปรเจกต์จริงยังคงเป็นวิธีที่ดีที่สุดในการตอกย้ำความรู้และตอบคำถามทางเทคนิคได้อย่างมั่นใจ --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/spring-boot/30-spring-boot-interview-questions