ការណែនាំ Spring Framework
Spring Framework គឺជាអ្វី?
Spring គឺជា Java framework ដ៏ល្បីបំផុតសម្រាប់ការសរសេរ enterprise application។ វាត្រូវបានបង្កើតឡើងដើម្បីធ្វើឲ្យការអភិវឌ្ឍ Java ងាយស្រួលជាងមុន។
ហេតុអ្វីត្រូវប្រើ Spring?
Spring ផ្ដល់នូវ dependency injection, AOP, MVC pattern, security, data access ក្នុងទម្រង់ modular ដែលអាចប្រើបន្ថែមបាន។
Spring Ecosystem
Spring Boot, Spring MVC, Spring Data, Spring Security, Spring Cloud — ជា modules ផ្សេងៗក្នុង Spring ecosystem។
🔧 Setup Project ជាមួយ Maven
ដើម្បីចាប់ផ្ដើម Spring project, អ្នកត្រូវការ Java 17+ និង Maven/Gradle។
<!-- pom.xml --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.0</version> </parent> <dependencies> <!-- Spring Boot Web (MVC + REST) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Data JPA --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> </dependencies>
IoC Container & Dependency Injection
🔄 IoC (Inversion of Control) គឺជាអ្វី?
IoC មានន័យថា "ការបញ្ច្រាសសិទ្ធិគ្រប់គ្រង"។ ជំនួសឲ្យ object បង្កើត dependency ខ្លួនឯង, Spring Container ទទួលខុសត្រូវបង្កើតនិងគ្រប់គ្រង object ទាំងអស់ជំនួស។
Constructor Injection
Inject dependency តាមរយៈ constructor — វិធីល្អបំផុតដែល Spring ណែនាំ។
Setter Injection
Inject dependency តាម setter method — ល្អសម្រាប់ optional dependencies។
Field Injection
Inject ដោយប្រើ @Autowired ដោយផ្ទាល់លើ field — ងាយប្រើប៉ុន្តែ Spring មិនណែនាំ។
// ❌ មិនល្អ — Object បង្កើត dependency ខ្លួនឯង public class OrderService { private EmailService emailService = new EmailService(); // tight coupling! } // ✅ ល្អ — Constructor Injection (IoC principle) @Service public class OrderService { private final EmailService emailService; private final PaymentService paymentService; // Spring inject dependencies តាម constructor ដោយស្វ័យប្រវត្តិ public OrderService(EmailService emailService, PaymentService paymentService) { this.emailService = emailService; this.paymentService = paymentService; } public void placeOrder(Order order) { paymentService.processPayment(order); emailService.sendConfirmation(order); } }
final field ជាមួយ Constructor Injection
ដើម្បីធានាថា dependency មិនអាចផ្លាស់ប្ដូរបន្ទាប់ពី
inject។ Lombok
@RequiredArgsConstructor ក៏អាចកាត់ code
ដ៏ច្រើន។
import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor // Lombok auto-generates constructor public class OrderService { private final EmailService emailService; private final PaymentService paymentService; // Constructor ត្រូវបាន generate ដោយ Lombok ស្វ័យប្រវត្តិ }
Spring Beans & Application Context
🫘 Spring Bean គឺជាអ្វី?
Bean គឺជា object ណាមួយដែល Spring IoC Container គ្រប់គ្រង។ Spring Container ទទួលខុសត្រូវចំពោះ lifecycle ទាំងមូលរបស់ bean: ការបង្កើត, ការ configure និងការ destroy។
| Scope | ន័យ | ករណីប្រើ |
|---|---|---|
| singleton | Bean មួយ instance (default) | Services, Repositories |
| prototype | Bean ថ្មីរៀងរាល់ request | Stateful objects |
| request | Bean ១ ក្នុង HTTP request | Web-layer beans |
| session | Bean ១ ក្នុង HTTP session | User session data |
// 1. Java-based Configuration @Configuration public class AppConfig { @Bean public DataSource dataSource() { HikariDataSource ds = new HikariDataSource(); ds.setJdbcUrl("jdbc:mysql://localhost:3306/mydb"); ds.setUsername("root"); ds.setPassword("password"); return ds; } @Bean @Scope("prototype") // ថ្មីរៀងរាល់ request public ReportGenerator reportGenerator() { return new ReportGenerator(); } } // 2. Bean Lifecycle Callbacks @Component public class DatabaseInitializer { @PostConstruct // ហៅបន្ទាប់ bean inject រួច public void init() { System.out.println("Database initialized!"); } @PreDestroy // ហៅនៅពេល application shutdown public void cleanup() { System.out.println("Cleaning up resources..."); } }
Spring Annotations សំខាន់ៗ
| Annotation | ន័យ | ប្រើក្នុង Layer |
|---|---|---|
| @Component | Generic Spring bean | ណាក៏បាន |
| @Service | Business logic layer | Service Layer |
| @Repository | Data access layer + exception translation | DAO/Repository |
| @Controller | Web MVC controller | Web Layer |
| @RestController | @Controller + @ResponseBody | REST API |
| @Configuration | Java config class | Config |
| @Autowired | Auto-inject dependency | ណាក៏បាន |
| @Value | Inject properties value | ណាក៏បាន |
| @Transactional | Database transaction management | Service/Repository |
// === Controller Layer === @RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @GetMapping public List<User> getAllUsers() { return userService.findAll(); } } // === Service Layer === @Service @Transactional public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public List<User> findAll() { return userRepository.findAll(); } } // === Repository Layer === @Repository public interface UserRepository extends JpaRepository<User, Long> { // Spring Data auto-implements CRUD methods }
Spring Boot — Auto-Configuration
🚀 Spring Boot ដោះស្រាយអ្វី?
- Auto-configure dependencies ដោយស្វ័យប្រវត្តិ
- Embedded server (Tomcat/Jetty) — មិនចាំបាច់ deploy WAR ដាច់ដោយឡែក
- Production-ready features (Actuator, Metrics, Health checks)
- Opinionated defaults ដើម្បីចាប់ផ្ដើមលឿន
package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; // @SpringBootApplication = @Configuration + @EnableAutoConfiguration + @ComponentScan @SpringBootApplication public class DemoApplication { public static void main(String[] args) { // ចាប់ផ្ដើម embedded Tomcat server SpringApplication.run(DemoApplication.class, args); } }
# Server Port server.port=8080 # Database Configuration spring.datasource.url=jdbc:mysql://localhost:3306/mydb spring.datasource.username=root spring.datasource.password=password spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # JPA / Hibernate spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true # Logging Level logging.level.org.springframework=INFO logging.level.com.example=DEBUG
ទៅ start.spring.io
ចូលទៅ https://start.spring.io ហើយជ្រើស dependencies ដែលត្រូវការ (Web, JPA, Security, etc.)
Generate & Download
ចុច Generate ដើម្បីទាញ ZIP file ហើយ extract វា។
Import ទៅ IntelliJ/Eclipse
បើក IDE ហើយ import project ជា Maven/Gradle project។
Run Application
Run DemoApplication.java ឬ
mvn spring-boot:run
Spring MVC — Web Controller
🌐 Spring MVC Request Flow
នៅពេល HTTP request ចូលមក, Spring MVC ដំណើរការតាមដំណាក់ការដូចខាងក្រោម:
@RestController @RequestMapping("/api/products") @CrossOrigin(origins = "*") // Allow CORS public class ProductController { private final ProductService productService; public ProductController(ProductService productService) { this.productService = productService; } // GET /api/products — ទទួលបញ្ជី products ទាំងអស់ @GetMapping public ResponseEntity<List<Product>> getAllProducts() { return ResponseEntity.ok(productService.findAll()); } // GET /api/products/5 — ទទួល product ១ @GetMapping("/{id}") public ResponseEntity<Product> getById(@PathVariable Long id) { return productService.findById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } // POST /api/products — បង្កើត product ថ្មី @PostMapping public ResponseEntity<Product> create( @Valid @RequestBody ProductRequest request) { Product created = productService.create(request); return ResponseEntity.status(201).body(created); } // PUT /api/products/5 — update product @PutMapping("/{id}") public ResponseEntity<Product> update( @PathVariable Long id, @Valid @RequestBody ProductRequest request) { return ResponseEntity.ok(productService.update(id, request)); } // DELETE /api/products/5 — លុប product @DeleteMapping("/{id}") public ResponseEntity<Void> delete(@PathVariable Long id) { productService.deleteById(id); return ResponseEntity.noContent().build(); } // GET /api/products/search?name=phone&minPrice=100 @GetMapping("/search") public List<Product> search( @RequestParam(required = false) String name, @RequestParam(defaultValue = "0") Double minPrice) { return productService.search(name, minPrice); } }
Spring Data JPA — Database Access
@Entity @Table(name = "products") @Data // Lombok: getters, setters, toString @NoArgsConstructor @AllArgsConstructor @Builder public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, length = 200) private String name; @Column(precision = 10, scale = 2) private BigDecimal price; @Column(name = "stock_quantity") private Integer stockQuantity; @ManyToOne @JoinColumn(name = "category_id") private Category category; @CreatedDate @Column(updatable = false) private LocalDateTime createdAt; }
@Repository public interface ProductRepository extends JpaRepository<Product, Long> { // Spring Data auto-generates query from method name! List<Product> findByNameContainingIgnoreCase(String name); List<Product> findByPriceBetween(BigDecimal min, BigDecimal max); List<Product> findByCategoryNameAndStockQuantityGreaterThan( String categoryName, Integer quantity); // Custom JPQL Query @Query("SELECT p FROM Product p WHERE p.price > :minPrice ORDER BY p.price DESC") List<Product> findExpensiveProducts(@Param("minPrice") BigDecimal minPrice); // Native SQL Query @Query(value = "SELECT * FROM products WHERE YEAR(created_at) = :year", nativeQuery = true) List<Product> findByYear(@Param("year") int year); // Pagination Page<Product> findByCategoryId(Long categoryId, Pageable pageable); }
productRepository.findAll(PageRequest.of(0, 10,
Sort.by("price").descending()))ទទួលបាន 10 products ដំបូង តម្រៀបតម្លៃ ពីខ្ពស់ទៅទាប។
Spring Security & JWT Authentication
Authentication
ផ្ទៀងផ្ទាត់ "នរណាមួយ?" — Login ជាមួយ username/password ហើយទទួល JWT token។
Authorization
ចម្លើយ "អ្នកទទួលបានសិទ្ធិអ្វី?" — ROLE_USER, ROLE_ADMIN control access។
JWT Token
JSON Web Token — stateless authentication ដែលប្រើ encode user info ទៅក្នុង token ។
@Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final JwtAuthFilter jwtAuthFilter; private final UserDetailsService userDetailsService; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) // Disable CSRF for REST API .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // JWT = Stateless .authorizeHttpRequests(auth -> auth // Public endpoints .requestMatchers("/api/auth/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll() // Admin only .requestMatchers("/api/admin/**").hasRole("ADMIN") // Require authentication for everything else .anyRequest().authenticated()) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // Bcrypt hash password } }
@Service public class JwtService { @Value("${jwt.secret}") private String secretKey; @Value("${jwt.expiration}") private long jwtExpiration; public String generateToken(UserDetails userDetails) { return Jwts.builder() .setSubject(userDetails.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + jwtExpiration)) .signWith(getSignKey(), SignatureAlgorithm.HS256) .compact(); } public boolean isTokenValid(String token, UserDetails userDetails) { final String username = extractUsername(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } }
REST API Best Practices & Exception Handling
@RestControllerAdvice // Handle exceptions globally for all controllers public class GlobalExceptionHandler { // Resource not found @ExceptionHandler(ResourceNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ErrorResponse handleNotFound(ResourceNotFoundException ex) { return new ErrorResponse(404, ex.getMessage(), LocalDateTime.now()); } // Validation errors (@Valid) @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Map<String, String> handleValidation(MethodArgumentNotValidException ex) { Map<String, String> errors = new HashMap<>(); ex.getBindingResult().getFieldErrors().forEach(error -> errors.put(error.getField(), error.getDefaultMessage())); return errors; } // Unauthorized @ExceptionHandler(AccessDeniedException.class) @ResponseStatus(HttpStatus.FORBIDDEN) public ErrorResponse handleForbidden(AccessDeniedException ex) { return new ErrorResponse(403, "Access denied", LocalDateTime.now()); } } // DTO Validation Example public record ProductRequest( @NotBlank(message = "ឈ្មោះមិនអាចទទេ") @Size(max = 200, message = "ឈ្មោះវែងពេក") String name, @NotNull(message = "តម្លៃចាំបាច់") @Positive(message = "តម្លៃត្រូវតែជាចំនួនវិជ្ជមាន") BigDecimal price, @Min(value = 0, message = "ស្តុកមិនអាចតិចជាង 0") Integer stockQuantity ) {}
AOP — Aspect Oriented Programming
🎯 AOP គឺជាអ្វី?
AOP (Aspect-Oriented Programming) អនុញ្ញាតឲ្យអ្នកបញ្ចូល cross-cutting concerns (Logging, Security, Transaction, Caching) ទៅក្នុង code ដោយមិនចាំបាច់ copy-paste logic ក្នុងគ្រប់ method។
@Aspect @Component public class LoggingAspect { private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class); // Pointcut: apply to all methods in service package @Pointcut("execution(* com.example.service.*.*(..))") public void serviceMethods() {} @Before("serviceMethods()") public void logBefore(JoinPoint joinPoint) { log.info("▶ Calling: {} with args: {}", joinPoint.getSignature().getName(), Arrays.toString(joinPoint.getArgs())); } @Around("serviceMethods()") public Object measureExecutionTime(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); Object result = pjp.proceed(); // Execute the actual method long duration = System.currentTimeMillis() - start; log.info("⏱ {} completed in {}ms", pjp.getSignature().getName(), duration); return result; } // Custom annotation-based pointcut @Around("@annotation(com.example.annotation.Cacheable)") public Object cacheResult(ProceedingJoinPoint pjp) throws Throwable { // Implement caching logic here return pjp.proceed(); } }
Spring Cloud Microservices
Eureka Service Discovery
Services ចុះឈ្មោះខ្លួននៅ Eureka Server ហើយ services ផ្សេងអាចស្វែងរកគ្នាបាន។
API Gateway
ច្រកតែមួយសម្រាប់ client requests ទាំងអស់ — routing, load balancing, auth។
Config Server
Centralize configuration ពី Git repository សម្រាប់ services ទាំងអស់។
Feign Client
HTTP client ដ៏ងាយស្រួលសម្រាប់ call services ផ្សេង ដូចជាហៅ local method។
// Order Service calling Inventory Service via Feign @FeignClient(name = "inventory-service", fallback = InventoryFallback.class) public interface InventoryClient { @GetMapping("/api/inventory/{productId}") InventoryResponse checkStock(@PathVariable Long productId); @PostMapping("/api/inventory/reserve") void reserveItems(@RequestBody ReservationRequest request); } // Fallback when Inventory Service is down (Circuit Breaker) @Component public class InventoryFallback implements InventoryClient { @Override public InventoryResponse checkStock(Long productId) { return new InventoryResponse(productId, 0, false); // Default: out of stock } } // Order Service using Feign Client @Service @RequiredArgsConstructor public class OrderService { private final InventoryClient inventoryClient; private final OrderRepository orderRepository; @Transactional public Order createOrder(OrderRequest request) { // Check inventory in another microservice InventoryResponse inventory = inventoryClient.checkStock(request.getProductId()); if (!inventory.isAvailable()) { throw new InsufficientStockException("ស្តុកអស់ហើយ!"); } inventoryClient.reserveItems(new ReservationRequest(request)); return orderRepository.save(buildOrder(request)); } }
Testing, Docker & Deployment
@ExtendWith(MockitoExtension.class) class ProductServiceTest { @Mock private ProductRepository productRepository; @InjectMocks private ProductService productService; @Test @DisplayName("ត្រូវ return product នៅពេល ID ត្រឹមត្រូវ") void shouldReturnProductWhenIdExists() { // Arrange Product mockProduct = Product.builder() .id(1L).name("iPhone 15").price(new BigDecimal("999.99")) .build(); Mockito.when(productRepository.findById(1L)) .thenReturn(Optional.of(mockProduct)); // Act Product result = productService.findById(1L); // Assert assertNotNull(result); assertEquals("iPhone 15", result.getName()); Mockito.verify(productRepository, Mockito.times(1)).findById(1L); } @Test @DisplayName("ត្រូវ throw exception នៅពេល ID មិនពិត") void shouldThrowExceptionWhenNotFound() { Mockito.when(productRepository.findById(99L)) .thenReturn(Optional.empty()); assertThrows(ResourceNotFoundException.class, () -> productService.findById(99L)); } }
# Stage 1: Build FROM eclipse-temurin:17-jdk-alpine AS builder WORKDIR /app COPY pom.xml . COPY src ./src RUN mvn clean package -DskipTests # Stage 2: Run (smaller image) FROM eclipse-temurin:17-jre-alpine WORKDIR /app # Create non-root user for security RUN addgroup -S spring && adduser -S spring -G spring USER spring:spring COPY --from=builder /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "-Xmx512m", "app.jar"]
version: '3.8' services: app: build: . ports: - "8080:8080" environment: SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/mydb SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: secret depends_on: db: condition: service_healthy db: image: mysql:8 environment: MYSQL_DATABASE: mydb MYSQL_ROOT_PASSWORD: secret volumes: - mysql-data:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s retries: 5 volumes: mysql-data:
🎓 ការរីកចម្រើនក្នុងការសិក្សា
🎉 អបអរសាទរ! អ្នកបានបញ្ចប់ Spring Framework
- ✅ IoC Container & Dependency Injection
- ✅ Spring Boot Auto-Configuration
- ✅ RESTful API ជាមួយ Spring MVC
- ✅ Database Access ជាមួយ Spring Data JPA
- ✅ Security ជាមួយ JWT Authentication
- ✅ AOP — Cross-cutting Concerns
- ✅ Microservices ជាមួយ Spring Cloud
- ✅ Testing & Docker Deployment