Spring Framework 教學手冊

目錄

  1. Spring Framework 概述

  2. 核心概念

  3. IoC 容器與依賴注入

  4. Bean 管理

  5. 面向切面程式設計 (AOP)

  6. Spring 核心模組

  7. Spring MVC 與 Web 開發

  8. 資料存取層

  9. Spring Security

  10. Spring Boot 整合

  11. 最佳實務

  12. 認證考試重點

  13. 練習題與答案

  14. 檢查清單


1. Spring Framework 概述

1.1 什麼是 Spring Framework

Spring Framework 是一個輕量級的 Java 企業級應用程式開發框架,由 Rod Johnson 於 2003 年創建。它提供了一個全面的程式設計和配置模型,用於現代 Java 企業應用程式。

核心特色:

  • 輕量級:不依賴容器,可在任何 Java 環境中運行
  • 非侵入性:應用程式代碼不需要實現 Spring 特定的接口
  • 控制反轉 (IoC):管理物件的創建和依賴關係
  • 面向切面程式設計 (AOP):將橫切關注點與業務邏輯分離
  • 容器功能:管理應用程式中的物件生命週期

1.2 Spring 生態系統

graph TB A[Spring Framework] --> B[Spring Core] A --> C[Spring Context] A --> D[Spring AOP] A --> E[Spring Web MVC] A --> F[Spring Data] A --> G[Spring Security] A --> H[Spring Boot] A --> I[Spring Cloud] B --> B1[IoC Container] B --> B2[Dependency Injection] C --> C1[ApplicationContext] C --> C2[Configuration]

1.3 為什麼使用 Spring Framework

傳統 Java 開發的問題:

// 傳統方式 - 緊密耦合
public class UserService {
    private UserDAO userDAO = new UserDAOImpl(); // 直接創建依賴
    
    public User getUser(Long id) {
        return userDAO.findById(id);
    }
}

Spring 解決方案:

// Spring 方式 - 鬆散耦合
@Service
public class UserService {
    @Autowired
    private UserDAO userDAO; // Spring 自動注入依賴
    
    public User getUser(Long id) {
        return userDAO.findById(id);
    }
}

1.4 認證考點提示

  • 理解 Spring Framework 的核心價值主張
  • 熟悉 Spring 生態系統的主要模組
  • 了解 Spring 與傳統 Java 開發的差異

1.5 實務案例

在企業專案中,Spring Framework 常用於:

  • 微服務架構:使用 Spring Boot 快速建立服務
  • Web 應用程式:使用 Spring MVC 建立 RESTful API
  • 資料存取:使用 Spring Data 簡化資料庫操作
  • 安全性:使用 Spring Security 實現認證和授權

2. 核心概念

2.1 控制反轉 (Inversion of Control, IoC)

控制反轉是一種程式設計原則,將物件的創建和依賴關係的管理從應用程式代碼轉移到外部容器。

概念圖解:

graph LR A[傳統方式] --> A1[物件自己創建依賴] B[IoC 方式] --> B1[容器創建並注入依賴] A1 --> A2[緊密耦合] B1 --> B2[鬆散耦合]

範例說明:

傳統方式:

public class OrderService {
    private PaymentService paymentService;
    private EmailService emailService;
    
    public OrderService() {
        // 物件自己創建依賴 - 控制權在物件本身
        this.paymentService = new PaymentServiceImpl();
        this.emailService = new EmailServiceImpl();
    }
}

IoC 方式:

@Service
public class OrderService {
    private final PaymentService paymentService;
    private final EmailService emailService;
    
    // 控制權交給 Spring 容器
    public OrderService(PaymentService paymentService, EmailService emailService) {
        this.paymentService = paymentService;
        this.emailService = emailService;
    }
}

2.2 依賴注入 (Dependency Injection, DI)

依賴注入是實現 IoC 的一種技術,透過外部容器將依賴物件注入到目標物件中。

三種注入方式:

2.2.1 建構子注入 (Constructor Injection)

@Service
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;
    
    // 推薦方式:建構子注入
    public UserService(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }
}

2.2.2 Setter 注入 (Setter Injection)

@Service
public class UserService {
    private UserRepository userRepository;
    private EmailService emailService;
    
    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    @Autowired
    public void setEmailService(EmailService emailService) {
        this.emailService = emailService;
    }
}

2.2.3 欄位注入 (Field Injection)

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private EmailService emailService;
}

最佳實務建議:

  • 優先使用建構子注入:保證依賴的不可變性和必要性
  • 避免欄位注入:難以測試和可能的循環依賴
  • Setter 注入用於可選依賴:非必要的依賴項

2.3 Bean 的概念

在 Spring 中,Bean 是由 IoC 容器管理的物件。

Bean 的特徵:

  • 由 Spring 容器實例化
  • 由 Spring 容器管理生命週期
  • 由 Spring 容器注入依賴

Bean 定義方式:

2.3.1 註解方式

@Component
public class UserService {
    // Spring 會自動創建 UserService 的實例
}

@Service
public class OrderService {
    // @Service 是 @Component 的特化註解
}

@Repository
public class UserRepository {
    // @Repository 是 @Component 的特化註解
}

@Controller
public class UserController {
    // @Controller 是 @Component 的特化註解
}

2.3.2 Java 配置方式

@Configuration
public class AppConfig {
    
    @Bean
    public UserService userService() {
        return new UserService();
    }
    
    @Bean
    public UserRepository userRepository() {
        return new UserRepositoryImpl();
    }
}

2.4 ApplicationContext

ApplicationContext 是 Spring 的 IoC 容器,負責管理 Bean 的生命週期和依賴注入。

ApplicationContext 層級結構:

graph TB A[BeanFactory] --> B[ApplicationContext] B --> C[WebApplicationContext] B --> D[ConfigurableApplicationContext] C --> E[ServletWebServerApplicationContext]

常用 ApplicationContext 實現:

// 註解配置
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

// XML 配置
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

// Web 應用程式
// 通常由 Spring Boot 自動配置

2.5 認證考點提示

  • IoC vs DI:理解概念差異和實現方式
  • 注入類型:掌握三種注入方式的優缺點
  • Bean 生命週期:了解 Bean 的創建、初始化、銷毀過程
  • ApplicationContext 功能:超越 BeanFactory 的額外功能

2.6 實務案例

// 實際專案中的服務層設計
@Service
@Transactional
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentService paymentService;
    private final EmailService emailService;
    
    public OrderService(OrderRepository orderRepository,
                       PaymentService paymentService,
                       EmailService emailService) {
        this.orderRepository = orderRepository;
        this.paymentService = paymentService;
        this.emailService = emailService;
    }
    
    public Order createOrder(OrderRequest request) {
        // 1. 創建訂單
        Order order = new Order(request);
        order = orderRepository.save(order);
        
        // 2. 處理付款
        Payment payment = paymentService.processPayment(order);
        
        // 3. 發送確認郵件
        emailService.sendOrderConfirmation(order);
        
        return order;
    }
}

注意事項:

  • 避免循環依賴
  • 合理設計 Bean 的作用域
  • 注意線程安全性
  • 適當使用 @Lazy 延遲初始化

3. IoC 容器與依賴注入

3.1 IoC 容器深入解析

Spring IoC 容器是 Spring Framework 的核心,負責創建物件、配置物件、組裝物件之間的依賴關係,並管理這些物件的完整生命週期。

容器階層結構:

graph TB A[BeanFactory] --> B[HierarchicalBeanFactory] A --> C[ListableBeanFactory] B --> D[ConfigurableBeanFactory] C --> E[ApplicationContext] D --> E E --> F[ConfigurableApplicationContext] E --> G[WebApplicationContext]

3.2 BeanFactory vs ApplicationContext

3.2.1 BeanFactory

  • 基本的 IoC 容器
  • 延遲載入 Bean
  • 較少的功能
public interface BeanFactory {
    Object getBean(String name) throws BeansException;
    <T> T getBean(String name, Class<T> requiredType) throws BeansException;
    boolean containsBean(String name);
    boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
    // ... 其他方法
}

3.2.2 ApplicationContext

  • 企業級應用程式功能
  • 預先載入所有 Bean
  • 支援國際化、事件發布、AOP
// ApplicationContext 的額外功能
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

// 1. 國際化支援
String message = context.getMessage("welcome.message", null, Locale.getDefault());

// 2. 事件發布
context.publishEvent(new UserRegisteredEvent(user));

// 3. 資源載入
Resource resource = context.getResource("classpath:config.properties");

3.3 Bean 定義與註冊

3.3.1 註解驅動的配置

基本註解:

@Configuration
@ComponentScan(basePackages = "com.tutorial")
@EnableAutoConfiguration
public class AppConfig {
}

// 各種 Component 註解
@Component        // 通用元件
@Service         // 服務層
@Repository      // 資料存取層
@Controller      // 控制層
@RestController  // REST 控制層

Bean 作用域:

@Service
@Scope("singleton")    // 預設,單例
public class UserService { }

@Service
@Scope("prototype")    // 每次請求新實例
public class OrderService { }

@Controller
@Scope("session")      // Web 環境,每個 session 一個實例
public class UserController { }

3.3.2 Java 配置方式

@Configuration
public class DatabaseConfig {
    
    @Bean
    @Primary
    public DataSource primaryDataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/primary");
        dataSource.setUsername("user");
        dataSource.setPassword("password");
        return dataSource;
    }
    
    @Bean("secondaryDS")
    public DataSource secondaryDataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/secondary");
        return dataSource;
    }
    
    @Bean
    @Profile("development")
    public UserService devUserService() {
        return new DevUserService();
    }
    
    @Bean
    @Profile("production")
    public UserService prodUserService() {
        return new ProdUserService();
    }
}

3.4 依賴注入的進階特性

3.4.1 條件式注入

@Service
public class NotificationService {
    private final List<NotificationProvider> providers;
    
    // 注入所有 NotificationProvider 實現
    public NotificationService(List<NotificationProvider> providers) {
        this.providers = providers;
    }
}

@Component
@ConditionalOnProperty(name = "email.enabled", havingValue = "true")
public class EmailNotificationProvider implements NotificationProvider {
    // 只有在配置 email.enabled=true 時才會創建
}

@Component
@ConditionalOnClass(SmsClient.class)
public class SmsNotificationProvider implements NotificationProvider {
    // 只有在 classpath 中存在 SmsClient 類別時才會創建
}

3.4.2 Qualifier 與 Primary

public interface PaymentProcessor {
    void process(Payment payment);
}

@Component
@Primary
public class CreditCardProcessor implements PaymentProcessor {
    @Override
    public void process(Payment payment) {
        // 信用卡處理邏輯
    }
}

@Component
@Qualifier("paypal")
public class PayPalProcessor implements PaymentProcessor {
    @Override
    public void process(Payment payment) {
        // PayPal 處理邏輯
    }
}

@Service
public class OrderService {
    private final PaymentProcessor defaultProcessor;
    private final PaymentProcessor paypalProcessor;
    
    public OrderService(PaymentProcessor defaultProcessor,
                       @Qualifier("paypal") PaymentProcessor paypalProcessor) {
        this.defaultProcessor = defaultProcessor;    // 注入 @Primary
        this.paypalProcessor = paypalProcessor;      // 注入指定的 Qualifier
    }
}

3.5 Bean 生命週期

graph TD A[容器啟動] --> B[讀取 Bean 定義] B --> C[實例化 Bean] C --> D[設定屬性值] D --> E[BeanNameAware.setBeanName] E --> F[BeanFactoryAware.setBeanFactory] F --> G[ApplicationContextAware.setApplicationContext] G --> H[BeanPostProcessor.postProcessBeforeInitialization] H --> I[@PostConstruct 方法] I --> J[InitializingBean.afterPropertiesSet] J --> K[自定義 init-method] K --> L[BeanPostProcessor.postProcessAfterInitialization] L --> M[Bean 可用] M --> N[容器關閉] N --> O[@PreDestroy 方法] O --> P[DisposableBean.destroy] P --> Q[自定義 destroy-method]

生命週期實作範例:

@Component
public class DatabaseConnectionBean implements BeanNameAware, 
                                             BeanFactoryAware,
                                             ApplicationContextAware,
                                             InitializingBean,
                                             DisposableBean {
    
    private String beanName;
    private BeanFactory beanFactory;
    private ApplicationContext applicationContext;
    
    @Override
    public void setBeanName(String name) {
        this.beanName = name;
        System.out.println("BeanNameAware: " + name);
    }
    
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
        System.out.println("BeanFactoryAware 被調用");
    }
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
        System.out.println("ApplicationContextAware 被調用");
    }
    
    @PostConstruct
    public void postConstruct() {
        System.out.println("@PostConstruct 被調用");
        // 初始化資料庫連接
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("InitializingBean.afterPropertiesSet 被調用");
    }
    
    @PreDestroy
    public void preDestroy() {
        System.out.println("@PreDestroy 被調用");
        // 清理資源
    }
    
    @Override
    public void destroy() throws Exception {
        System.out.println("DisposableBean.destroy 被調用");
    }
}

3.6 ApplicationContext 事件機制

3.6.1 內建事件

@Component
public class ApplicationEventListener {
    
    @EventListener
    public void handleContextRefreshed(ContextRefreshedEvent event) {
        System.out.println("ApplicationContext 已刷新");
    }
    
    @EventListener
    public void handleContextStarted(ContextStartedEvent event) {
        System.out.println("ApplicationContext 已啟動");
    }
    
    @EventListener
    public void handleContextStopped(ContextStoppedEvent event) {
        System.out.println("ApplicationContext 已停止");
    }
}

3.6.2 自定義事件

// 自定義事件
public class UserRegisteredEvent extends ApplicationEvent {
    private final User user;
    
    public UserRegisteredEvent(Object source, User user) {
        super(source);
        this.user = user;
    }
    
    public User getUser() {
        return user;
    }
}

// 事件發布者
@Service
public class UserService {
    private final ApplicationEventPublisher eventPublisher;
    
    public UserService(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }
    
    public User registerUser(UserRegistrationRequest request) {
        User user = new User(request);
        // 保存用戶邏輯...
        
        // 發布事件
        eventPublisher.publishEvent(new UserRegisteredEvent(this, user));
        
        return user;
    }
}

// 事件監聽器
@Component
public class UserEventHandler {
    
    @EventListener
    @Async
    public void handleUserRegistered(UserRegisteredEvent event) {
        User user = event.getUser();
        // 發送歡迎郵件
        sendWelcomeEmail(user);
    }
    
    @EventListener
    @Order(1)
    public void updateUserStatistics(UserRegisteredEvent event) {
        // 更新用戶統計
    }
}

3.7 認證考點提示

重要考點:

  • BeanFactory 與 ApplicationContext 的差異
  • Bean 的作用域和生命週期
  • 依賴注入的三種方式及最佳實務
  • @Qualifier、@Primary 的使用
  • Bean 的初始化和銷毀回調方法
  • ApplicationContext 的事件機制

3.8 實務案例

// 完整的服務配置範例
@Configuration
@EnableScheduling
@EnableAsync
public class ServiceConfig {
    
    @Bean
    @ConfigurationProperties(prefix = "app.cache")
    public CacheProperties cacheProperties() {
        return new CacheProperties();
    }
    
    @Bean
    @ConditionalOnProperty(name = "app.cache.enabled", havingValue = "true")
    public CacheManager cacheManager(CacheProperties properties) {
        RedisCacheManager.Builder builder = RedisCacheManager
            .RedisCacheManagerBuilder
            .fromConnectionFactory(redisConnectionFactory())
            .cacheDefaults(cacheConfiguration(properties));
            
        return builder.build();
    }
    
    @Bean
    @Profile("!test")
    public RedisConnectionFactory redisConnectionFactory() {
        LettuceConnectionFactory factory = new LettuceConnectionFactory();
        factory.setHostName("localhost");
        factory.setPort(6379);
        return factory;
    }
}

注意事項:

  • 避免循環依賴,使用 @Lazy 或重構設計
  • 合理使用 Bean 作用域,注意線程安全
  • 適當使用條件式 Bean 創建
  • 妥善處理 Bean 的初始化和清理工作
  • 利用事件機制實現鬆散耦合的架構

4. Bean 管理

4.1 Bean 的作用域

Spring 支援多種 Bean 作用域,每種作用域決定了 Bean 實例的創建方式和生命週期。

作用域類型:

作用域說明使用場景
singleton預設值,整個 ApplicationContext 中只有一個實例無狀態的服務類
prototype每次請求都創建新實例有狀態的物件
requestWeb 環境中每個 HTTP 請求一個實例Web 請求特定的資料
sessionWeb 環境中每個 HTTP Session 一個實例用戶會話資料
applicationWeb 環境中每個 ServletContext 一個實例應用程式級別的資料

4.1.1 Singleton 作用域

@Component
@Scope("singleton") // 或 @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class UserService {
    private int counter = 0; // 注意:線程安全問題
    
    public void incrementCounter() {
        counter++; // 在多線程環境下不安全
    }
}

// 正確的 singleton 設計
@Service
public class UserService {
    private final UserRepository userRepository;
    
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    // 無狀態方法,線程安全
    public User findById(Long id) {
        return userRepository.findById(id);
    }
}

4.1.2 Prototype 作用域

@Component
@Scope("prototype")
public class OrderProcessor {
    private Order currentOrder;
    private List<OrderItem> items = new ArrayList<>();
    
    public void setOrder(Order order) {
        this.currentOrder = order;
    }
    
    public void addItem(OrderItem item) {
        items.add(item);
    }
}

// 使用 prototype Bean
@Service
public class OrderService {
    private final ApplicationContext applicationContext;
    
    public OrderService(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }
    
    public Order processOrder(OrderRequest request) {
        // 每次獲取新的 OrderProcessor 實例
        OrderProcessor processor = applicationContext.getBean(OrderProcessor.class);
        
        Order order = new Order(request);
        processor.setOrder(order);
        
        for (OrderItemRequest itemRequest : request.getItems()) {
            processor.addItem(new OrderItem(itemRequest));
        }
        
        return processor.process();
    }
}

4.1.3 Web 作用域

@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestScopedBean {
    private String requestId = UUID.randomUUID().toString();
    
    public String getRequestId() {
        return requestId;
    }
}

@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserSession {
    private User currentUser;
    private LocalDateTime loginTime;
    
    public void login(User user) {
        this.currentUser = user;
        this.loginTime = LocalDateTime.now();
    }
    
    public boolean isLoggedIn() {
        return currentUser != null;
    }
}

4.2 Bean 的初始化和銷毀

4.2.1 初始化方法

@Component
public class DatabaseService {
    
    private DataSource dataSource;
    
    // 方法1:使用 @PostConstruct
    @PostConstruct
    public void initialize() {
        System.out.println("DatabaseService 正在初始化...");
        // 初始化資料庫連接池
        setupConnectionPool();
    }
    
    // 方法2:實現 InitializingBean 接口
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("InitializingBean.afterPropertiesSet 被調用");
        validateConfiguration();
    }
    
    private void setupConnectionPool() {
        // 設定連接池邏輯
    }
    
    private void validateConfiguration() {
        // 驗證配置邏輯
    }
}

// 方法3:Java 配置中指定初始化方法
@Configuration
public class DatabaseConfig {
    
    @Bean(initMethod = "init", destroyMethod = "cleanup")
    public CustomDatabaseService customDatabaseService() {
        return new CustomDatabaseService();
    }
}

public class CustomDatabaseService {
    
    public void init() {
        System.out.println("自定義初始化方法被調用");
    }
    
    public void cleanup() {
        System.out.println("自定義清理方法被調用");
    }
}

4.2.2 銷毀方法

@Component
public class ResourceManager {
    
    private ExecutorService executorService;
    
    @PostConstruct
    public void setUp() {
        executorService = Executors.newFixedThreadPool(10);
    }
    
    // 方法1:使用 @PreDestroy
    @PreDestroy
    public void shutdown() {
        System.out.println("ResourceManager 正在關閉...");
        if (executorService != null && !executorService.isShutdown()) {
            executorService.shutdown();
            try {
                if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) {
                    executorService.shutdownNow();
                }
            } catch (InterruptedException e) {
                executorService.shutdownNow();
                Thread.currentThread().interrupt();
            }
        }
    }
    
    // 方法2:實現 DisposableBean 接口
    @Override
    public void destroy() throws Exception {
        System.out.println("DisposableBean.destroy 被調用");
        // 額外的清理邏輯
    }
}

4.3 Bean 的延遲初始化

// 全局延遲初始化
@Configuration
@Lazy
public class AppConfig {
    // 所有 Bean 都會延遲初始化
}

// 單個 Bean 延遲初始化
@Service
@Lazy
public class HeavyService {
    
    @PostConstruct
    public void init() {
        System.out.println("HeavyService 被初始化了"); // 只有在第一次使用時才會輸出
    }
}

// 在依賴注入時指定延遲
@Controller
public class UserController {
    
    private final UserService userService;
    
    // 只有在第一次調用 userService 時才會初始化
    public UserController(@Lazy UserService userService) {
        this.userService = userService;
    }
}

4.4 條件式 Bean 創建

4.4.1 內建條件註解

@Configuration
public class ConditionalConfig {
    
    @Bean
    @ConditionalOnProperty(name = "email.provider", havingValue = "gmail")
    public EmailService gmailEmailService() {
        return new GmailEmailService();
    }
    
    @Bean
    @ConditionalOnProperty(name = "email.provider", havingValue = "outlook")
    public EmailService outlookEmailService() {
        return new OutlookEmailService();
    }
    
    @Bean
    @ConditionalOnMissingBean
    public EmailService defaultEmailService() {
        return new DefaultEmailService();
    }
    
    @Bean
    @ConditionalOnClass(Redis.class)
    public CacheService redisCacheService() {
        return new RedisCacheService();
    }
    
    @Bean
    @ConditionalOnMissingClass("redis.clients.jedis.Jedis")
    public CacheService memoryCacheService() {
        return new MemoryCacheService();
    }
}

4.4.2 自定義條件

// 自定義條件類
public class WindowsCondition implements Condition {
    
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment environment = context.getEnvironment();
        String osName = environment.getProperty("os.name");
        return osName != null && osName.toLowerCase().contains("windows");
    }
}

@Configuration
public class OSSpecificConfig {
    
    @Bean
    @Conditional(WindowsCondition.class)
    public FileService windowsFileService() {
        return new WindowsFileService();
    }
    
    @Bean
    @ConditionalOnProperty(name = "os.name", havingValue = "Linux")
    public FileService linuxFileService() {
        return new LinuxFileService();
    }
}

4.5 Profile 環境配置

@Configuration
public class EnvironmentConfig {
    
    @Bean
    @Profile("development")
    public DataSource devDataSource() {
        return DataSourceBuilder.create()
            .url("jdbc:h2:mem:devdb")
            .username("sa")
            .password("")
            .build();
    }
    
    @Bean
    @Profile("test")
    public DataSource testDataSource() {
        return DataSourceBuilder.create()
            .url("jdbc:h2:mem:testdb")
            .username("sa")
            .password("")
            .build();
    }
    
    @Bean
    @Profile("production")
    public DataSource prodDataSource() {
        return DataSourceBuilder.create()
            .url("jdbc:mysql://prod-server:3306/proddb")
            .username("${db.username}")
            .password("${db.password}")
            .build();
    }
}

// 啟動時指定 Profile
// java -Dspring.profiles.active=development -jar app.jar
// 或在 application.properties 中設定:spring.profiles.active=development

@Component
@Profile("!production")  // 除了 production 環境外都會創建
public class DebugService {
    // 除錯服務只在非正式環境中可用
}

4.6 Factory Bean 模式

// 實現 FactoryBean 接口
@Component
public class ConnectionFactoryBean implements FactoryBean<Connection> {
    
    @Value("${database.url}")
    private String url;
    
    @Value("${database.username}")
    private String username;
    
    @Value("${database.password}")
    private String password;
    
    @Override
    public Connection getObject() throws Exception {
        return DriverManager.getConnection(url, username, password);
    }
    
    @Override
    public Class<?> getObjectType() {
        return Connection.class;
    }
    
    @Override
    public boolean isSingleton() {
        return false; // 每次請求都創建新的連接
    }
}

// 複雜物件的 Factory
@Component
public class HttpClientFactoryBean implements FactoryBean<CloseableHttpClient> {
    
    @Override
    public CloseableHttpClient getObject() throws Exception {
        RequestConfig requestConfig = RequestConfig.custom()
            .setConnectionRequestTimeout(5000)
            .setConnectTimeout(5000)
            .setSocketTimeout(30000)
            .build();
            
        return HttpClients.custom()
            .setDefaultRequestConfig(requestConfig)
            .setMaxConnTotal(100)
            .setMaxConnPerRoute(20)
            .build();
    }
    
    @Override
    public Class<?> getObjectType() {
        return CloseableHttpClient.class;
    }
    
    @Override
    public boolean isSingleton() {
        return true;
    }
}

4.7 認證考點提示

重要考點:

  • Bean 作用域:singleton vs prototype 的差異和使用場景
  • 初始化回調:@PostConstruct vs InitializingBean vs init-method 的執行順序
  • 銷毀回調:@PreDestroy vs DisposableBean vs destroy-method
  • 條件式 Bean:各種 @Conditional 註解的使用
  • Profile 配置:環境隔離和動態配置
  • FactoryBean:複雜物件創建的設計模式

4.8 實務案例

// 完整的服務配置範例
@Configuration
@EnableConfigurationProperties(AppProperties.class)
public class ServiceConfiguration {
    
    private final AppProperties appProperties;
    
    public ServiceConfiguration(AppProperties appProperties) {
        this.appProperties = appProperties;
    }
    
    @Bean
    @Scope("prototype")
    public OrderProcessor orderProcessor() {
        return new OrderProcessor();
    }
    
    @Bean
    @ConditionalOnProperty(name = "app.features.cache.enabled", havingValue = "true")
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("users", "orders", "products");
    }
    
    @Bean
    @Profile("!test")
    @Scope(value = "singleton", proxyMode = ScopedProxyMode.TARGET_CLASS)
    public EmailService emailService() {
        if (appProperties.getEmail().getProvider().equals("smtp")) {
            return new SmtpEmailService(appProperties.getEmail());
        } else {
            return new ApiEmailService(appProperties.getEmail());
        }
    }
    
    @Bean
    @Profile("test")
    public EmailService mockEmailService() {
        return new MockEmailService();
    }
}

@ConfigurationProperties(prefix = "app")
@Data
public class AppProperties {
    private Features features = new Features();
    private Email email = new Email();
    
    @Data
    public static class Features {
        private Cache cache = new Cache();
        
        @Data
        public static class Cache {
            private boolean enabled = false;
        }
    }
    
    @Data
    public static class Email {
        private String provider = "smtp";
        private String host;
        private int port = 587;
        private String username;
        private String password;
    }
}

注意事項:

  • Singleton Bean 必須設計為線程安全
  • Prototype Bean 的銷毀方法不會自動調用
  • Web 作用域需要配置代理模式
  • 適當使用延遲初始化避免循環依賴
  • Profile 可以組合使用:@Profile(“prod & cloud”)

5. 面向切面程式設計 (AOP)

5.1 AOP 核心概念

面向切面程式設計 (Aspect-Oriented Programming) 是一種程式設計典範,用於將橫切關注點 (Cross-cutting Concerns) 從業務邏輯中分離出來。

核心概念圖解:

graph TB A[業務邏輯] --> B[橫切關注點] B --> C[日誌記錄] B --> D[安全檢查] B --> E[事務管理] B --> F[性能監控] B --> G[錯誤處理] H[AOP 解決方案] --> I[Aspect 切面] I --> J[Advice 通知] I --> K[Pointcut 切點] I --> L[Join Point 連接點]

5.1.1 AOP 術語

術語說明範例
Aspect切面,橫切關注點的模組化日誌切面、安全切面
Join Point連接點,程式執行過程中的特定點方法調用、異常拋出
Pointcut切點,定義在哪些 Join Point 上應用 Advice所有 Service 方法
Advice通知,在特定 Join Point 執行的代碼方法執行前記錄日誌
Target Object目標物件,被一個或多個切面所通知的物件UserService 實例
AOP ProxyAOP 代理,框架創建的物件JDK 動態代理或 CGLIB 代理

5.2 Spring AOP vs AspectJ

特性Spring AOPAspectJ
編織時機運行時編譯時/載入時/運行時
功能完整性基本功能完整 AOP 功能
性能較低較高
複雜度簡單複雜
支援的 Join Point僅方法調用所有 Join Point

5.3 Advice 類型

5.3.1 Before Advice

@Aspect
@Component
public class LoggingAspect {
    
    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
    
    @Before("execution(* com.tutorial.service.*.*(..))")
    public void logMethodEntry(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();
        Object[] args = joinPoint.getArgs();
        
        logger.info("進入方法: {}.{}, 參數: {}", className, methodName, Arrays.toString(args));
    }
    
    @Before("@annotation(secured)")
    public void checkSecurity(JoinPoint joinPoint, Secured secured) {
        String role = secured.value();
        // 檢查當前用戶是否有指定角色
        if (!SecurityContextHolder.getContext().getAuthentication().getAuthorities()
            .stream().anyMatch(auth -> auth.getAuthority().equals(role))) {
            throw new AccessDeniedException("權限不足: 需要 " + role + " 角色");
        }
    }
}

5.3.2 After Advice

@Aspect
@Component
public class AuditAspect {
    
    @Autowired
    private AuditService auditService;
    
    @After("execution(* com.tutorial.service.UserService.create*(..))")
    public void auditUserCreation(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        if (args.length > 0 && args[0] instanceof User) {
            User user = (User) args[0];
            auditService.logUserCreation(user.getId(), getCurrentUser());
        }
    }
    
    @AfterReturning(value = "execution(* com.tutorial.service.*Service.save*(..))", 
                   returning = "result")
    public void logSuccessfulSave(JoinPoint joinPoint, Object result) {
        logger.info("成功保存: {}, 結果: {}", joinPoint.getSignature().getName(), result);
    }
    
    @AfterThrowing(value = "execution(* com.tutorial.service.*.*(..))", 
                  throwing = "exception")
    public void logException(JoinPoint joinPoint, Exception exception) {
        logger.error("方法 {} 拋出異常: {}", joinPoint.getSignature().getName(), 
                    exception.getMessage(), exception);
    }
}

5.3.3 Around Advice

@Aspect
@Component
public class PerformanceAspect {
    
    private static final Logger logger = LoggerFactory.getLogger(PerformanceAspect.class);
    
    @Around("execution(* com.tutorial.service.*.*(..))")
    public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        String methodName = joinPoint.getSignature().getName();
        
        try {
            // 執行目標方法
            Object result = joinPoint.proceed();
            
            long endTime = System.currentTimeMillis();
            long executionTime = endTime - startTime;
            
            logger.info("方法 {} 執行時間: {} ms", methodName, executionTime);
            
            return result;
        } catch (Exception e) {
            long endTime = System.currentTimeMillis();
            long executionTime = endTime - startTime;
            
            logger.error("方法 {} 執行失敗,耗時: {} ms, 異常: {}", 
                        methodName, executionTime, e.getMessage());
            throw e;
        }
    }
    
    @Around("@annotation(cacheable)")
    public Object handleCaching(ProceedingJoinPoint joinPoint, Cacheable cacheable) throws Throwable {
        String cacheKey = generateCacheKey(joinPoint, cacheable);
        
        // 檢查快取
        Object cachedResult = cacheManager.get(cacheKey);
        if (cachedResult != null) {
            logger.debug("快取命中: {}", cacheKey);
            return cachedResult;
        }
        
        // 執行方法並快取結果
        Object result = joinPoint.proceed();
        cacheManager.put(cacheKey, result);
        logger.debug("結果已快取: {}", cacheKey);
        
        return result;
    }
}

5.4 Pointcut 表達式

5.4.1 執行切點

@Aspect
@Component
public class PointcutExamples {
    
    // 1. 方法執行切點
    @Pointcut("execution(public * com.tutorial.service.*.*(..))")
    public void serviceLayer() {}
    
    @Pointcut("execution(* com.tutorial.repository.*Repository.*(..))")
    public void repositoryLayer() {}
    
    @Pointcut("execution(* com.tutorial.controller.*Controller.*(..))")
    public void controllerLayer() {}
    
    // 2. 參數匹配
    @Pointcut("execution(* com.tutorial.service.*.*(Long, ..))")
    public void methodsWithLongFirstParam() {}
    
    // 3. 返回類型匹配
    @Pointcut("execution(com.tutorial.model.User com.tutorial.service.*.*(..))")
    public void methodsReturningUser() {}
    
    // 4. 註解切點
    @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
    public void transactionalMethods() {}
    
    @Pointcut("@within(org.springframework.stereotype.Service)")
    public void serviceAnnotatedClasses() {}
    
    // 5. 組合切點
    @Pointcut("serviceLayer() && transactionalMethods()")
    public void transactionalServiceMethods() {}
    
    @Pointcut("serviceLayer() || repositoryLayer()")
    public void dataAccessLayers() {}
    
    @Pointcut("serviceLayer() && !execution(* com.tutorial.service.*Service.get*(..))")
    public void nonGetterServiceMethods() {}
}

5.4.2 常用切點表達式

@Component
@Aspect
public class CommonPointcuts {
    
    // 所有公共方法
    @Pointcut("execution(public * *(..))")
    public void anyPublicMethod() {}
    
    // 特定包下的所有方法
    @Pointcut("within(com.tutorial.service..*)")
    public void inServicePackage() {}
    
    // 實現特定接口的類
    @Pointcut("this(com.tutorial.service.UserService)")
    public void userServiceProxy() {}
    
    // 目標物件實現特定接口
    @Pointcut("target(com.tutorial.service.UserService)")
    public void userServiceTarget() {}
    
    // Bean 名稱匹配
    @Pointcut("bean(userService)")
    public void userServiceBean() {}
    
    @Pointcut("bean(*Service)")
    public void anyServiceBean() {}
    
    // 自定義註解
    @Pointcut("@annotation(com.tutorial.annotation.Auditable)")
    public void auditableMethods() {}
    
    @Pointcut("@args(com.tutorial.annotation.Validated)")
    public void methodsWithValidatedArgs() {}
}

5.5 自定義註解與 AOP

5.5.1 創建自定義註解

// 日誌註解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Loggable {
    String value() default "";
    boolean includeArgs() default true;
    boolean includeResult() default true;
}

// 快取註解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cacheable {
    String value() default "";
    String key() default "";
    int timeout() default 300; // 秒
}

// 重試註解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {
    int maxAttempts() default 3;
    long delay() default 1000; // 毫秒
    Class<? extends Exception>[] retryFor() default {Exception.class};
}

5.5.2 實現註解切面

@Aspect
@Component
public class CustomAnnotationAspect {
    
    @Around("@annotation(loggable)")
    public Object handleLogging(ProceedingJoinPoint joinPoint, Loggable loggable) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        String logMessage = loggable.value().isEmpty() ? methodName : loggable.value();
        
        if (loggable.includeArgs()) {
            logger.info("開始執行 {}, 參數: {}", logMessage, Arrays.toString(joinPoint.getArgs()));
        } else {
            logger.info("開始執行 {}", logMessage);
        }
        
        try {
            Object result = joinPoint.proceed();
            
            if (loggable.includeResult()) {
                logger.info("完成執行 {}, 結果: {}", logMessage, result);
            } else {
                logger.info("完成執行 {}", logMessage);
            }
            
            return result;
        } catch (Exception e) {
            logger.error("執行 {} 時發生錯誤: {}", logMessage, e.getMessage());
            throw e;
        }
    }
    
    @Around("@annotation(retryable)")
    public Object handleRetry(ProceedingJoinPoint joinPoint, Retryable retryable) throws Throwable {
        int maxAttempts = retryable.maxAttempts();
        long delay = retryable.delay();
        Class<? extends Exception>[] retryFor = retryable.retryFor();
        
        Exception lastException = null;
        
        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
            try {
                return joinPoint.proceed();
            } catch (Exception e) {
                lastException = e;
                
                // 檢查是否是需要重試的異常類型
                boolean shouldRetry = Arrays.stream(retryFor)
                    .anyMatch(exceptionType -> exceptionType.isAssignableFrom(e.getClass()));
                    
                if (!shouldRetry || attempt == maxAttempts) {
                    throw e;
                }
                
                logger.warn("方法 {} 第 {} 次嘗試失敗,將在 {} ms 後重試: {}", 
                           joinPoint.getSignature().getName(), attempt, delay, e.getMessage());
                
                try {
                    Thread.sleep(delay);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("重試被中斷", ie);
                }
            }
        }
        
        throw lastException;
    }
}

5.6 AOP 配置

5.6.1 啟用 AOP

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true) // 強制使用 CGLIB 代理
public class AopConfig {
    
    // AOP 相關的 Bean 配置
}

// 或在 Spring Boot 中自動啟用
@SpringBootApplication
// @EnableAspectJAutoProxy 自動啟用
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

5.6.2 代理機制

// JDK 動態代理 - 基於接口
public interface UserService {
    User findById(Long id);
    User save(User user);
}

@Service
public class UserServiceImpl implements UserService {
    @Override
    public User findById(Long id) {
        // 實現邏輯
    }
    
    @Override
    public User save(User user) {
        // 實現邏輯
    }
}

// CGLIB 代理 - 基於類別
@Service
public class OrderService { // 無接口
    
    public Order findById(Long id) {
        // 實現邏輯
    }
    
    // final 方法無法被代理
    public final void finalMethod() {
        // 此方法不會被 AOP 攔截
    }
}

5.7 認證考點提示

重要考點:

  • AOP 概念:Aspect、Advice、Pointcut、Join Point 的理解
  • Advice 類型:Before、After、Around 的使用場景和執行順序
  • Pointcut 表達式:execution、within、@annotation 等表達式語法
  • 代理機制:JDK 動態代理 vs CGLIB 代理的差異
  • AOP 限制:self-invocation 問題、final 方法限制
  • 配置方式:@EnableAspectJAutoProxy 的作用

5.8 實務案例

// 完整的 AOP 應用範例
@Aspect
@Component
@Order(1) // 設定切面執行順序
public class BusinessAspect {
    
    private final MeterRegistry meterRegistry;
    private final AuditService auditService;
    
    public BusinessAspect(MeterRegistry meterRegistry, AuditService auditService) {
        this.meterRegistry = meterRegistry;
        this.auditService = auditService;
    }
    
    // 業務方法性能監控
    @Around("execution(* com.tutorial.service.*Service.*(..))")
    public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        Timer.Sample sample = Timer.start(meterRegistry);
        String methodName = joinPoint.getSignature().toShortString();
        
        try {
            Object result = joinPoint.proceed();
            sample.stop(Timer.builder("method.execution")
                .tag("method", methodName)
                .tag("status", "success")
                .register(meterRegistry));
            return result;
        } catch (Exception e) {
            sample.stop(Timer.builder("method.execution")
                .tag("method", methodName)
                .tag("status", "error")
                .register(meterRegistry));
            throw e;
        }
    }
    
    // 敏感操作審計
    @AfterReturning("@annotation(auditable)")
    public void auditSensitiveOperation(JoinPoint joinPoint, Auditable auditable) {
        String operation = auditable.operation();
        String user = SecurityContextHolder.getContext().getAuthentication().getName();
        Object[] args = joinPoint.getArgs();
        
        auditService.logOperation(operation, user, args);
    }
}

// 使用範例
@Service
@Transactional
public class UserService {
    
    @Loggable("查詢用戶")
    @Retryable(maxAttempts = 3, retryFor = {DataAccessException.class})
    public User findById(Long id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException("用戶不存在: " + id));
    }
    
    @Auditable(operation = "創建用戶")
    @Cacheable(key = "user:#{result.id}", timeout = 600)
    public User createUser(CreateUserRequest request) {
        User user = new User(request);
        return userRepository.save(user);
    }
}

注意事項:

  • AOP 代理只能攔截外部調用,內部方法調用無效
  • final 類別和方法無法被 CGLIB 代理
  • 注意切面的執行順序,使用 @Order 控制
  • Around Advice 必須調用 proceed() 方法
  • 異常處理要謹慎,避免吞掉業務異常

6. Spring 核心模組

6.1 Spring Core 模組

Spring Core 是整個 Spring Framework 的基礎,提供了 IoC 容器的基本功能。

6.1.1 Core 模組架構

graph TB A[Spring Core] --> B[IoC Container] A --> C[Resource Abstraction] A --> D[Type Conversion] A --> E[Validation] A --> F[Data Binding] B --> G[BeanFactory] B --> H[ApplicationContext] C --> I[Resource Interface] C --> J[ResourceLoader]

6.1.2 Resource 抽象化

// Resource 接口的使用
@Service
public class ConfigurationService {
    
    @Autowired
    private ResourceLoader resourceLoader;
    
    public Properties loadConfiguration(String location) throws IOException {
        Resource resource = resourceLoader.getResource(location);
        Properties properties = new Properties();
        
        try (InputStream inputStream = resource.getInputStream()) {
            properties.load(inputStream);
        }
        
        return properties;
    }
    
    public void demonstrateResourceTypes() throws IOException {
        // 1. Classpath 資源
        Resource classpathResource = resourceLoader.getResource("classpath:config.properties");
        
        // 2. 文件系統資源
        Resource fileResource = resourceLoader.getResource("file:/path/to/config.properties");
        
        // 3. URL 資源
        Resource urlResource = resourceLoader.getResource("http://example.com/config.properties");
        
        // 4. 相對路徑資源
        Resource relativeResource = resourceLoader.getResource("config/app.properties");
        
        // 檢查資源是否存在
        if (classpathResource.exists()) {
            System.out.println("資源存在: " + classpathResource.getDescription());
            System.out.println("檔案大小: " + classpathResource.contentLength());
            System.out.println("最後修改時間: " + new Date(classpathResource.lastModified()));
        }
    }
}

6.1.3 類型轉換系統

// 自定義類型轉換器
@Component
public class StringToDateConverter implements Converter<String, Date> {
    
    private final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
    
    @Override
    public Date convert(String source) {
        try {
            return dateFormat.parse(source);
        } catch (ParseException e) {
            throw new IllegalArgumentException("無效的日期格式: " + source, e);
        }
    }
}

// 配置轉換服務
@Configuration
public class ConversionConfig {
    
    @Bean
    public ConversionService conversionService() {
        DefaultConversionService conversionService = new DefaultConversionService();
        conversionService.addConverter(new StringToDateConverter());
        return conversionService;
    }
}

// 使用轉換服務
@Service
public class UserService {
    
    @Autowired
    private ConversionService conversionService;
    
    public User createUser(String birthDateString) {
        Date birthDate = conversionService.convert(birthDateString, Date.class);
        return new User(birthDate);
    }
}

6.2 Spring Context 模組

Spring Context 建立在 Core 模組之上,提供了企業級功能。

6.2.1 ApplicationContext 功能

// ApplicationContext 的進階使用
@Component
public class ApplicationContextService implements ApplicationContextAware {
    
    private ApplicationContext applicationContext;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
    
    public void demonstrateContextFeatures() {
        // 1. Bean 管理
        UserService userService = applicationContext.getBean(UserService.class);
        boolean containsBean = applicationContext.containsBean("userService");
        String[] beanNames = applicationContext.getBeanNamesForType(UserService.class);
        
        // 2. 環境和 Profile
        Environment environment = applicationContext.getEnvironment();
        String[] activeProfiles = environment.getActiveProfiles();
        String property = environment.getProperty("app.name", "default-app");
        
        // 3. 國際化
        Locale locale = Locale.getDefault();
        String message = applicationContext.getMessage("welcome.message", 
            new Object[]{"Spring"}, locale);
        
        // 4. 事件發布
        applicationContext.publishEvent(new CustomApplicationEvent(this, "測試事件"));
        
        // 5. 資源載入
        Resource resource = applicationContext.getResource("classpath:config.xml");
    }
}

6.2.2 國際化支援

// 國際化配置
@Configuration
public class InternationalizationConfig {
    
    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = 
            new ReloadableResourceBundleMessageSource();
        messageSource.setBasename("classpath:messages/messages");
        messageSource.setDefaultEncoding("UTF-8");
        messageSource.setCacheSeconds(3600); // 快取 1 小時
        return messageSource;
    }
    
    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver localeResolver = new SessionLocaleResolver();
        localeResolver.setDefaultLocale(Locale.TRADITIONAL_CHINESE);
        return localeResolver;
    }
}

// 使用國際化
@Service
public class MessageService {
    
    @Autowired
    private MessageSource messageSource;
    
    public String getMessage(String code, Object[] args, Locale locale) {
        return messageSource.getMessage(code, args, locale);
    }
    
    public String getLocalizedWelcomeMessage(String username, Locale locale) {
        return messageSource.getMessage("welcome.user", 
            new Object[]{username}, "歡迎 {0}", locale);
    }
}

// messages_zh_TW.properties
// welcome.user=歡迎 {0}!
// error.user.notfound=找不到用戶 {0}
// success.user.created=成功創建用戶 {0}

// messages_en.properties  
// welcome.user=Welcome {0}!
// error.user.notfound=User {0} not found
// success.user.created=User {0} created successfully

6.3 Spring Beans 模組

6.3.1 BeanWrapper 和屬性存取

// BeanWrapper 的使用
@Service
public class BeanManipulationService {
    
    public void demonstrateBeanWrapper() {
        User user = new User();
        BeanWrapper beanWrapper = new BeanWrapperImpl(user);
        
        // 設定屬性值
        beanWrapper.setPropertyValue("name", "張三");
        beanWrapper.setPropertyValue("age", 30);
        beanWrapper.setPropertyValue("email", "zhangsan@example.com");
        
        // 批量設定屬性
        Map<String, Object> properties = new HashMap<>();
        properties.put("phone", "123-456-7890");
        properties.put("address.city", "台北");
        properties.put("address.zipCode", "100");
        beanWrapper.setPropertyValues(properties);
        
        // 讀取屬性值
        String name = (String) beanWrapper.getPropertyValue("name");
        Integer age = (Integer) beanWrapper.getPropertyValue("age");
        
        // 檢查屬性
        boolean hasName = beanWrapper.isReadableProperty("name");
        boolean canWriteAge = beanWrapper.isWritableProperty("age");
        
        System.out.println("用戶: " + name + ", 年齡: " + age);
    }
    
    public void copyProperties(Object source, Object target) {
        BeanWrapper sourceWrapper = new BeanWrapperImpl(source);
        BeanWrapper targetWrapper = new BeanWrapperImpl(target);
        
        PropertyDescriptor[] propertyDescriptors = sourceWrapper.getPropertyDescriptors();
        
        for (PropertyDescriptor pd : propertyDescriptors) {
            String propertyName = pd.getName();
            if (sourceWrapper.isReadableProperty(propertyName) && 
                targetWrapper.isWritableProperty(propertyName)) {
                
                Object value = sourceWrapper.getPropertyValue(propertyName);
                targetWrapper.setPropertyValue(propertyName, value);
            }
        }
    }
}

6.3.2 Bean 定義的程式化創建

// 程式化創建 Bean 定義
@Configuration
public class DynamicBeanConfig {
    
    @Bean
    public BeanDefinitionRegistryPostProcessor dynamicBeanRegistrar() {
        return new BeanDefinitionRegistryPostProcessor() {
            
            @Override
            public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) 
                    throws BeansException {
                // 動態註冊 Bean
                registerDataSourceBeans(registry);
            }
            
            @Override
            public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) 
                    throws BeansException {
                // Bean 工廠後處理
            }
            
            private void registerDataSourceBeans(BeanDefinitionRegistry registry) {
                // 讀取配置檔案中的資料源配置
                List<DataSourceConfig> configs = loadDataSourceConfigs();
                
                for (DataSourceConfig config : configs) {
                    BeanDefinitionBuilder builder = BeanDefinitionBuilder
                        .genericBeanDefinition(HikariDataSource.class);
                    
                    builder.addPropertyValue("jdbcUrl", config.getUrl());
                    builder.addPropertyValue("username", config.getUsername());
                    builder.addPropertyValue("password", config.getPassword());
                    builder.addPropertyValue("maximumPoolSize", config.getMaxPoolSize());
                    
                    registry.registerBeanDefinition(config.getName() + "DataSource", 
                        builder.getBeanDefinition());
                }
            }
        };
    }
}

6.4 Spring Expression Language (SpEL)

6.4.1 SpEL 基本語法

// SpEL 表達式範例
@Component
public class SpELDemonstration {
    
    // 字面值
    @Value("#{100}")
    private int literalNumber;
    
    @Value("#{'Hello World'}")
    private String literalString;
    
    @Value("#{true}")
    private boolean literalBoolean;
    
    // 屬性引用
    @Value("#{systemProperties['user.name']}")
    private String userName;
    
    @Value("#{environment['spring.application.name']}")
    private String appName;
    
    // Bean 引用
    @Value("#{userService.getUserCount()}")
    private int userCount;
    
    @Value("#{@userService.findActiveUsers().size()}")
    private int activeUserCount;
    
    // 運算符
    @Value("#{10 + 5}")
    private int addition;
    
    @Value("#{userService.getUserCount() > 100}")
    private boolean hasLargeUserBase;
    
    @Value("#{userService.getUserCount() > 100 ? 'Large' : 'Small'}")
    private String userBaseSize;
    
    // 正則表達式
    @Value("#{'123-456-7890' matches '\\d{3}-\\d{3}-\\d{4}'}")
    private boolean isValidPhoneNumber;
    
    // 集合操作
    @Value("#{userService.getAllUsers().?[age > 18]}")
    private List<User> adultUsers;
    
    @Value("#{userService.getAllUsers().![name]}")
    private List<String> userNames;
    
    @Value("#{userService.getAllUsers().^[age > 18]}")
    private User firstAdultUser;
    
    @Value("#{userService.getAllUsers().$[age > 18]}")
    private User lastAdultUser;
}

6.4.2 SpEL 在配置中的應用

// 條件式配置
@Configuration
@ConditionalOnExpression("#{environment['app.features.advanced'] == 'true'}")
public class AdvancedFeatureConfig {
    
    @Bean
    @ConditionalOnExpression("#{T(java.lang.Runtime).getRuntime().availableProcessors() > 4}")
    public TaskExecutor highPerformanceTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(8);
        executor.setMaxPoolSize(16);
        return executor;
    }
}

// 動態屬性注入
@Component
public class DynamicConfiguration {
    
    @Value("#{environment['server.port'] ?: 8080}")
    private int serverPort;
    
    @Value("#{environment['app.timeout'] ?: 30000}")
    private long timeout;
    
    @Value("#{environment['app.features'].split(',')}")
    private List<String> enabledFeatures;
    
    @Value("#{T(java.time.LocalDateTime).now()}")
    private LocalDateTime startupTime;
    
    @Value("#{T(java.util.UUID).randomUUID().toString()}")
    private String instanceId;
}

// 方法安全性中的 SpEL
@Service
public class DocumentService {
    
    @PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
    public Document getDocument(Long documentId, Long userId) {
        return documentRepository.findById(documentId);
    }
    
    @PreAuthorize("@documentSecurityService.hasPermission(authentication.principal, #doc, 'READ')")
    public void viewDocument(Document doc) {
        // 查看文檔
    }
    
    @PostAuthorize("returnObject.owner == authentication.principal.username")
    public Document createDocument(DocumentRequest request) {
        Document doc = new Document(request);
        doc.setOwner(getCurrentUser().getUsername());
        return documentRepository.save(doc);
    }
}

6.5 Environment 抽象化

6.5.1 Profile 和屬性管理

// Environment 的進階使用
@Service
public class EnvironmentService {
    
    @Autowired
    private Environment environment;
    
    public void demonstrateEnvironment() {
        // 1. Profile 管理
        String[] activeProfiles = environment.getActiveProfiles();
        String[] defaultProfiles = environment.getDefaultProfiles();
        boolean isDevMode = environment.acceptsProfiles(Profiles.of("development"));
        
        // 2. 屬性獲取
        String appName = environment.getProperty("app.name");
        String appVersion = environment.getProperty("app.version", "1.0.0");
        Integer maxConnections = environment.getProperty("app.max-connections", Integer.class);
        
        // 3. 必需屬性
        String requiredProperty = environment.getRequiredProperty("database.url");
        
        // 4. 屬性佔位符解析
        String template = "應用程式 ${app.name} 版本 ${app.version} 正在運行";
        String resolved = environment.resolvePlaceholders(template);
        
        // 5. 檢查屬性是否存在
        boolean hasProperty = environment.containsProperty("spring.datasource.url");
        
        System.out.println("活動 Profile: " + Arrays.toString(activeProfiles));
        System.out.println("應用程式資訊: " + resolved);
    }
}

// 自定義 PropertySource
@Configuration
public class CustomPropertySourceConfig {
    
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        PropertySourcesPlaceholderConfigurer configurer = 
            new PropertySourcesPlaceholderConfigurer();
        
        // 添加自定義屬性源
        YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
        yaml.setResources(new ClassPathResource("custom-config.yml"));
        configurer.setProperties(yaml.getObject());
        
        return configurer;
    }
    
    @EventListener
    public void handleApplicationReady(ApplicationReadyEvent event) {
        ConfigurableEnvironment environment = event.getApplicationContext().getEnvironment();
        
        // 程式化添加屬性源
        Map<String, Object> customProperties = new HashMap<>();
        customProperties.put("custom.startup.time", System.currentTimeMillis());
        customProperties.put("custom.instance.id", UUID.randomUUID().toString());
        
        MapPropertySource customPropertySource = new MapPropertySource(
            "customProperties", customProperties);
        environment.getPropertySources().addFirst(customPropertySource);
    }
}

6.6 認證考點提示

重要考點:

  • Resource 抽象化:不同資源類型的存取方式
  • ApplicationContext 功能:超越 BeanFactory 的額外功能
  • SpEL 語法:常用表達式和在 Spring 中的應用
  • Environment 抽象化:Profile 和屬性管理
  • 類型轉換:內建轉換器和自定義轉換器
  • 國際化:MessageSource 的配置和使用

6.7 實務案例

// 完整的核心模組應用範例
@Configuration
@EnableConfigurationProperties(AppProperties.class)
public class CoreModuleDemo {
    
    @Autowired
    private Environment environment;
    
    @Bean
    @ConditionalOnExpression("#{environment['app.features.cache'] == 'true'}")
    public CacheManager cacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        
        // 使用 SpEL 動態配置快取
        String cacheNames = environment.getProperty("app.cache.names", "users,orders");
        List<Cache> caches = Arrays.stream(cacheNames.split(","))
            .map(name -> new ConcurrentMapCache(name.trim()))
            .collect(Collectors.toList());
            
        cacheManager.setCaches(caches);
        return cacheManager;
    }
    
    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = 
            new ReloadableResourceBundleMessageSource();
        
        String basename = environment.getProperty("app.messages.basename", 
            "classpath:messages/messages");
        messageSource.setBasename(basename);
        messageSource.setDefaultEncoding("UTF-8");
        
        return messageSource;
    }
    
    @Bean
    @Profile("!test")
    public ResourceLoader customResourceLoader() {
        return new DefaultResourceLoader() {
            @Override
            public Resource getResource(String location) {
                // 自定義資源載入邏輯
                if (location.startsWith("encrypted:")) {
                    return new EncryptedResource(location.substring(10));
                }
                return super.getResource(location);
            }
        };
    }
}

@ConfigurationProperties(prefix = "app")
@Data
public class AppProperties {
    private String name = "Spring Application";
    private String version = "1.0.0";
    private Features features = new Features();
    private Cache cache = new Cache();
    
    @Data
    public static class Features {
        private boolean cache = false;
        private boolean advanced = false;
    }
    
    @Data
    public static class Cache {
        private String names = "default";
        private int ttl = 300;
    }
}

注意事項:

  • Resource 載入要處理異常情況
  • SpEL 表達式要注意安全性,避免注入攻擊
  • Profile 可以組合使用,支援複雜的環境配置
  • Environment 屬性的優先級順序很重要
  • 自定義 PropertySource 要注意載入順序

7. Spring MVC 與 Web 開發

7.1 Spring MVC 架構概述

Spring MVC 是基於 Model-View-Controller 設計模式的 Web 框架,提供了靈活的 Web 應用程式開發方式。

MVC 架構圖:

graph TB A[用戶請求] --> B[DispatcherServlet] B --> C[HandlerMapping] C --> D[Controller] D --> E[Service Layer] E --> F[Repository Layer] F --> G[Database] D --> H[ModelAndView] H --> I[ViewResolver] I --> J[View] J --> K[Response] B --> L[HandlerInterceptor] L --> D D --> M[HandlerExceptionResolver]

7.2 DispatcherServlet 配置

7.2.1 Java 配置方式

// Web 應用程式初始化器
public class WebAppInitializer implements WebApplicationInitializer {
    
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        // 創建 Root ApplicationContext
        AnnotationConfigWebApplicationContext rootContext = 
            new AnnotationConfigWebApplicationContext();
        rootContext.register(RootConfig.class);
        
        // 註冊 ContextLoaderListener
        servletContext.addListener(new ContextLoaderListener(rootContext));
        
        // 創建 Web ApplicationContext
        AnnotationConfigWebApplicationContext webContext = 
            new AnnotationConfigWebApplicationContext();
        webContext.register(WebConfig.class);
        
        // 註冊 DispatcherServlet
        ServletRegistration.Dynamic dispatcher = servletContext.addServlet(
            "dispatcher", new DispatcherServlet(webContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");
        
        // 註冊編碼過濾器
        FilterRegistration.Dynamic encodingFilter = servletContext.addFilter(
            "encodingFilter", new CharacterEncodingFilter());
        encodingFilter.addMappingForUrlPatterns(null, false, "/*");
        encodingFilter.setInitParameter("encoding", "UTF-8");
        encodingFilter.setInitParameter("forceEncoding", "true");
    }
}

// 根配置類
@Configuration
@ComponentScan(basePackages = "com.tutorial", 
               excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, 
                                                     value = EnableWebMvc.class))
public class RootConfig {
    // 非 Web 層的配置
}

// Web 配置類
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.tutorial.web")
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.jsp("/WEB-INF/views/", ".jsp");
    }
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("/static/")
                .setCacheControl(CacheControl.maxAge(Duration.ofDays(365)));
    }
    
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

7.2.2 Spring Boot 簡化配置

@SpringBootApplication
public class WebApplication {
    public static void main(String[] args) {
        SpringApplication.run(WebApplication.class, args);
    }
}

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://localhost:3000")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*")
                .allowCredentials(true);
    }
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoggingInterceptor())
                .addPathPatterns("/api/**");
        
        registry.addInterceptor(new AuthenticationInterceptor())
                .addPathPatterns("/api/admin/**");
    }
    
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new MappingJackson2HttpMessageConverter());
        converters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8));
    }
}

7.3 Controller 開發

7.3.1 基本 Controller

@Controller
@RequestMapping("/users")
public class UserController {
    
    private final UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    // 顯示用戶列表
    @GetMapping
    public String listUsers(Model model, 
                          @RequestParam(defaultValue = "0") int page,
                          @RequestParam(defaultValue = "10") int size) {
        Page<User> userPage = userService.findUsers(PageRequest.of(page, size));
        model.addAttribute("users", userPage.getContent());
        model.addAttribute("currentPage", page);
        model.addAttribute("totalPages", userPage.getTotalPages());
        return "users/list";
    }
    
    // 顯示用戶詳情
    @GetMapping("/{id}")
    public String showUser(@PathVariable Long id, Model model) {
        User user = userService.findById(id);
        if (user == null) {
            throw new UserNotFoundException("用戶不存在: " + id);
        }
        model.addAttribute("user", user);
        return "users/detail";
    }
    
    // 顯示創建用戶表單
    @GetMapping("/new")
    public String newUserForm(Model model) {
        model.addAttribute("user", new User());
        return "users/form";
    }
    
    // 處理用戶創建
    @PostMapping
    public String createUser(@Valid @ModelAttribute User user, 
                           BindingResult bindingResult,
                           RedirectAttributes redirectAttributes) {
        if (bindingResult.hasErrors()) {
            return "users/form";
        }
        
        User savedUser = userService.save(user);
        redirectAttributes.addFlashAttribute("message", 
            "用戶創建成功: " + savedUser.getName());
        return "redirect:/users/" + savedUser.getId();
    }
    
    // 顯示編輯用戶表單
    @GetMapping("/{id}/edit")
    public String editUserForm(@PathVariable Long id, Model model) {
        User user = userService.findById(id);
        model.addAttribute("user", user);
        return "users/form";
    }
    
    // 處理用戶更新
    @PutMapping("/{id}")
    public String updateUser(@PathVariable Long id,
                           @Valid @ModelAttribute User user,
                           BindingResult bindingResult,
                           RedirectAttributes redirectAttributes) {
        if (bindingResult.hasErrors()) {
            return "users/form";
        }
        
        user.setId(id);
        userService.update(user);
        redirectAttributes.addFlashAttribute("message", "用戶更新成功");
        return "redirect:/users/" + id;
    }
    
    // 刪除用戶
    @DeleteMapping("/{id}")
    public String deleteUser(@PathVariable Long id, 
                           RedirectAttributes redirectAttributes) {
        userService.delete(id);
        redirectAttributes.addFlashAttribute("message", "用戶刪除成功");
        return "redirect:/users";
    }
}

7.3.2 REST API Controller

@RestController
@RequestMapping("/api/users")
@Validated
public class UserRestController {
    
    private final UserService userService;
    
    public UserRestController(UserService userService) {
        this.userService = userService;
    }
    
    @GetMapping
    public ResponseEntity<PagedResponse<UserDTO>> getUsers(
            @RequestParam(defaultValue = "0") @Min(0) int page,
            @RequestParam(defaultValue = "10") @Min(1) @Max(100) int size,
            @RequestParam(required = false) String search) {
        
        Pageable pageable = PageRequest.of(page, size);
        Page<User> userPage = userService.findUsers(search, pageable);
        
        List<UserDTO> userDTOs = userPage.getContent().stream()
            .map(UserDTO::from)
            .collect(Collectors.toList());
            
        PagedResponse<UserDTO> response = new PagedResponse<>(
            userDTOs, 
            userPage.getNumber(), 
            userPage.getSize(),
            userPage.getTotalElements(),
            userPage.getTotalPages()
        );
        
        return ResponseEntity.ok(response);
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<UserDTO> getUser(@PathVariable @Positive Long id) {
        User user = userService.findById(id);
        return ResponseEntity.ok(UserDTO.from(user));
    }
    
    @PostMapping
    public ResponseEntity<UserDTO> createUser(@Valid @RequestBody CreateUserRequest request) {
        User user = userService.create(request);
        UserDTO userDTO = UserDTO.from(user);
        
        URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(user.getId())
            .toUri();
            
        return ResponseEntity.created(location).body(userDTO);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<UserDTO> updateUser(@PathVariable @Positive Long id,
                                            @Valid @RequestBody UpdateUserRequest request) {
        User user = userService.update(id, request);
        return ResponseEntity.ok(UserDTO.from(user));
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable @Positive Long id) {
        userService.delete(id);
        return ResponseEntity.noContent().build();
    }
    
    @PostMapping("/{id}/avatar")
    public ResponseEntity<UserDTO> uploadAvatar(@PathVariable @Positive Long id,
                                              @RequestParam("file") MultipartFile file) {
        if (file.isEmpty()) {
            throw new BadRequestException("檔案不能為空");
        }
        
        // 檢查檔案類型
        String contentType = file.getContentType();
        if (!Arrays.asList("image/jpeg", "image/png", "image/gif").contains(contentType)) {
            throw new BadRequestException("不支援的檔案類型");
        }
        
        // 檢查檔案大小 (5MB)
        if (file.getSize() > 5 * 1024 * 1024) {
            throw new BadRequestException("檔案大小不能超過 5MB");
        }
        
        User user = userService.updateAvatar(id, file);
        return ResponseEntity.ok(UserDTO.from(user));
    }
}

7.4 請求處理和參數綁定

7.4.1 各種參數類型

@RestController
@RequestMapping("/api/demo")
public class ParameterBindingController {
    
    // 路徑變數
    @GetMapping("/users/{userId}/orders/{orderId}")
    public ResponseEntity<String> getOrder(@PathVariable Long userId,
                                         @PathVariable("orderId") Long id) {
        return ResponseEntity.ok("User: " + userId + ", Order: " + id);
    }
    
    // 請求參數
    @GetMapping("/search")
    public ResponseEntity<String> search(
            @RequestParam String keyword,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(required = false) String category) {
        return ResponseEntity.ok("搜尋: " + keyword);
    }
    
    // 請求頭
    @GetMapping("/headers")
    public ResponseEntity<String> getHeaders(
            @RequestHeader("User-Agent") String userAgent,
            @RequestHeader(value = "Accept-Language", defaultValue = "zh-TW") String language,
            @RequestHeader Map<String, String> allHeaders) {
        return ResponseEntity.ok("User-Agent: " + userAgent);
    }
    
    // Cookie
    @GetMapping("/cookies")
    public ResponseEntity<String> getCookies(
            @CookieValue(value = "session", defaultValue = "none") String sessionId,
            HttpServletRequest request) {
        return ResponseEntity.ok("Session: " + sessionId);
    }
    
    // 請求體
    @PostMapping("/data")
    public ResponseEntity<String> postData(@RequestBody Map<String, Object> data,
                                         @RequestHeader("Content-Type") String contentType) {
        return ResponseEntity.ok("接收到資料: " + data.size() + " 項");
    }
    
    // 表單資料
    @PostMapping("/form")
    public ResponseEntity<String> handleForm(
            @RequestParam String name,
            @RequestParam String email,
            @RequestParam MultipartFile file) {
        return ResponseEntity.ok("表單提交成功: " + name);
    }
    
    // 模型屬性
    @PostMapping("/user")
    public ResponseEntity<String> createUser(@ModelAttribute User user,
                                           BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return ResponseEntity.badRequest().body("驗證失敗");
        }
        return ResponseEntity.ok("用戶創建: " + user.getName());
    }
    
    // 會話屬性
    @PostMapping("/session")
    public ResponseEntity<String> sessionData(@SessionAttribute(required = false) User currentUser,
                                            HttpSession session) {
        if (currentUser == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("請先登入");
        }
        return ResponseEntity.ok("當前用戶: " + currentUser.getName());
    }
}

7.4.2 自定義參數解析器

// 自定義註解
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurrentUser {
}

// 自定義參數解析器
@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
    
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(CurrentUser.class) &&
               parameter.getParameterType().equals(User.class);
    }
    
    @Override
    public Object resolveArgument(MethodParameter parameter, 
                                ModelAndViewContainer mavContainer,
                                NativeWebRequest webRequest, 
                                WebDataBinderFactory binderFactory) throws Exception {
        
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        String authorization = request.getHeader("Authorization");
        
        if (authorization != null && authorization.startsWith("Bearer ")) {
            String token = authorization.substring(7);
            return userService.getUserFromToken(token);
        }
        
        throw new UnauthorizedException("無效的認證令牌");
    }
}

// 註冊解析器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    
    @Autowired
    private CurrentUserArgumentResolver currentUserArgumentResolver;
    
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(currentUserArgumentResolver);
    }
}

// 使用自定義解析器
@RestController
@RequestMapping("/api/profile")
public class ProfileController {
    
    @GetMapping
    public ResponseEntity<UserDTO> getProfile(@CurrentUser User currentUser) {
        return ResponseEntity.ok(UserDTO.from(currentUser));
    }
    
    @PutMapping
    public ResponseEntity<UserDTO> updateProfile(@CurrentUser User currentUser,
                                                @Valid @RequestBody UpdateProfileRequest request) {
        User updatedUser = userService.updateProfile(currentUser.getId(), request);
        return ResponseEntity.ok(UserDTO.from(updatedUser));
    }
}

7.5 異常處理

7.5.1 全域異常處理

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
    // 處理驗證異常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(
            MethodArgumentNotValidException e) {
        
        Map<String, String> errors = new HashMap<>();
        e.getBindingResult().getFieldErrors().forEach(error -> 
            errors.put(error.getField(), error.getDefaultMessage()));
            
        ErrorResponse errorResponse = ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(HttpStatus.BAD_REQUEST.value())
            .error("Validation Failed")
            .message("請求參數驗證失敗")
            .path(getCurrentRequest().getRequestURI())
            .validationErrors(errors)
            .build();
            
        return ResponseEntity.badRequest().body(errorResponse);
    }
    
    // 處理約束違反異常
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<ErrorResponse> handleConstraintViolationException(
            ConstraintViolationException e) {
        
        Map<String, String> errors = new HashMap<>();
        e.getConstraintViolations().forEach(violation -> {
            String propertyPath = violation.getPropertyPath().toString();
            String message = violation.getMessage();
            errors.put(propertyPath, message);
        });
        
        ErrorResponse errorResponse = ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(HttpStatus.BAD_REQUEST.value())
            .error("Constraint Violation")
            .message("參數約束違反")
            .validationErrors(errors)
            .build();
            
        return ResponseEntity.badRequest().body(errorResponse);
    }
    
    // 處理業務異常
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
        logger.warn("業務異常: {}", e.getMessage());
        
        ErrorResponse errorResponse = ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(e.getStatus().value())
            .error(e.getStatus().getReasonPhrase())
            .message(e.getMessage())
            .build();
            
        return ResponseEntity.status(e.getStatus()).body(errorResponse);
    }
    
    // 處理資源不存在異常
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFoundException(
            ResourceNotFoundException e) {
        logger.warn("資源不存在: {}", e.getMessage());
        
        ErrorResponse errorResponse = ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(HttpStatus.NOT_FOUND.value())
            .error("Not Found")
            .message(e.getMessage())
            .build();
            
        return ResponseEntity.notFound().build();
    }
    
    // 處理權限不足異常
    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<ErrorResponse> handleAccessDeniedException(
            AccessDeniedException e) {
        logger.warn("權限不足: {}", e.getMessage());
        
        ErrorResponse errorResponse = ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(HttpStatus.FORBIDDEN.value())
            .error("Forbidden")
            .message("權限不足")
            .build();
            
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorResponse);
    }
    
    // 處理通用異常
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(Exception e) {
        logger.error("未處理的異常", e);
        
        ErrorResponse errorResponse = ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(HttpStatus.INTERNAL_SERVER_ERROR.value())
            .error("Internal Server Error")
            .message("系統內部錯誤")
            .build();
            
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
    }
    
    private HttpServletRequest getCurrentRequest() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        return ((ServletRequestAttributes) requestAttributes).getRequest();
    }
}

// 錯誤響應 DTO
@Data
@Builder
public class ErrorResponse {
    private LocalDateTime timestamp;
    private int status;
    private String error;
    private String message;
    private String path;
    private Map<String, String> validationErrors;
}

7.6 攔截器

7.6.1 自定義攔截器

// 日誌攔截器
public class LoggingInterceptor implements HandlerInterceptor {
    
    private static final Logger logger = LoggerFactory.getLogger(LoggingInterceptor.class);
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        String method = request.getMethod();
        
        logger.info("請求開始 - {} {}", method, requestURI);
        
        // 記錄請求開始時間
        request.setAttribute("startTime", System.currentTimeMillis());
        
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, 
                         HttpServletResponse response,
                         Object handler, 
                         ModelAndView modelAndView) throws Exception {
        // Controller 處理完成後執行
        logger.debug("Controller 處理完成");
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, 
                              HttpServletResponse response,
                              Object handler, 
                              Exception ex) throws Exception {
        Long startTime = (Long) request.getAttribute("startTime");
        long endTime = System.currentTimeMillis();
        long duration = endTime - startTime;
        
        String requestURI = request.getRequestURI();
        String method = request.getMethod();
        int status = response.getStatus();
        
        logger.info("請求結束 - {} {} - 狀態: {} - 耗時: {}ms", 
                   method, requestURI, status, duration);
        
        if (ex != null) {
            logger.error("請求處理異常", ex);
        }
    }
}

// 認證攔截器
public class AuthenticationInterceptor implements HandlerInterceptor {
    
    private final AuthService authService;
    
    public AuthenticationInterceptor(AuthService authService) {
        this.authService = authService;
    }
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
        
        // 檢查是否需要認證
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            
            // 檢查方法或類別是否有 @NoAuth 註解
            if (handlerMethod.hasMethodAnnotation(NoAuth.class) ||
                handlerMethod.getBeanType().isAnnotationPresent(NoAuth.class)) {
                return true;
            }
        }
        
        String authorization = request.getHeader("Authorization");
        if (authorization == null || !authorization.startsWith("Bearer ")) {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"error\":\"未提供認證令牌\"}");
            return false;
        }
        
        String token = authorization.substring(7);
        try {
            User user = authService.validateToken(token);
            request.setAttribute("currentUser", user);
            return true;
        } catch (Exception e) {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"error\":\"無效的認證令牌\"}");
            return false;
        }
    }
}

7.7 認證考點提示

重要考點:

  • DispatcherServlet:Spring MVC 的前端控制器角色
  • HandlerMapping:URL 到 Controller 方法的映射機制
  • ViewResolver:視圖解析的過程
  • HandlerInterceptor:請求攔截的生命週期
  • 異常處理:@ExceptionHandler 和 @ControllerAdvice 的使用
  • 參數綁定:各種參數註解的功能和使用場景

7.8 實務案例

// 完整的 REST API 範例
@RestController
@RequestMapping("/api/v1/orders")
@Validated
public class OrderController {
    
    private final OrderService orderService;
    private final OrderMapper orderMapper;
    
    public OrderController(OrderService orderService, OrderMapper orderMapper) {
        this.orderService = orderService;
        this.orderMapper = orderMapper;
    }
    
    @GetMapping
    @PreAuthorize("hasRole('USER')")
    public ResponseEntity<PagedResponse<OrderDTO>> getOrders(
            @CurrentUser User currentUser,
            @RequestParam(defaultValue = "0") @Min(0) int page,
            @RequestParam(defaultValue = "20") @Min(1) @Max(100) int size,
            @RequestParam(required = false) OrderStatus status) {
        
        Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
        Page<Order> orderPage = orderService.findUserOrders(currentUser.getId(), status, pageable);
        
        List<OrderDTO> orderDTOs = orderPage.getContent().stream()
            .map(orderMapper::toDTO)
            .collect(Collectors.toList());
            
        PagedResponse<OrderDTO> response = new PagedResponse<>(
            orderDTOs,
            orderPage.getNumber(),
            orderPage.getSize(),
            orderPage.getTotalElements(),
            orderPage.getTotalPages()
        );
        
        return ResponseEntity.ok(response);
    }
    
    @PostMapping
    @PreAuthorize("hasRole('USER')")
    public ResponseEntity<OrderDTO> createOrder(
            @CurrentUser User currentUser,
            @Valid @RequestBody CreateOrderRequest request) {
        
        Order order = orderService.createOrder(currentUser.getId(), request);
        OrderDTO orderDTO = orderMapper.toDTO(order);
        
        URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(order.getId())
            .toUri();
            
        return ResponseEntity.created(location).body(orderDTO);
    }
    
    @PutMapping("/{orderId}/cancel")
    @PreAuthorize("hasRole('USER')")
    public ResponseEntity<OrderDTO> cancelOrder(
            @CurrentUser User currentUser,
            @PathVariable @Positive Long orderId) {
        
        Order order = orderService.cancelOrder(currentUser.getId(), orderId);
        return ResponseEntity.ok(orderMapper.toDTO(order));
    }
}

注意事項:

  • Controller 應該只處理 HTTP 相關邏輯,業務邏輯放在 Service 層
  • 合理使用 HTTP 狀態碼和響應頭
  • 實現適當的異常處理和錯誤響應
  • 注意請求和響應的序列化/反序列化
  • 使用攔截器實現橫切關注點
  • 適當的參數驗證和安全檢查

8. 資料存取層

8.1 Spring JDBC

Spring JDBC 提供了對原始 JDBC 的抽象,簡化了資料庫操作並減少了樣板代碼。

8.1.1 JdbcTemplate 基本使用

// 資料源配置
@Configuration
public class DatabaseConfig {
    
    @Bean
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSourceProperties primaryDataSourceProperties() {
        return new DataSourceProperties();
    }
    
    @Bean
    @Primary
    public DataSource primaryDataSource() {
        return primaryDataSourceProperties()
            .initializeDataSourceBuilder()
            .type(HikariDataSource.class)
            .build();
    }
    
    @Bean
    public JdbcTemplate jdbcTemplate(@Qualifier("primaryDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
    
    @Bean
    public NamedParameterJdbcTemplate namedParameterJdbcTemplate(
            @Qualifier("primaryDataSource") DataSource dataSource) {
        return new NamedParameterJdbcTemplate(dataSource);
    }
}

// 基本的 JDBC Repository
@Repository
public class UserJdbcRepository {
    
    private final JdbcTemplate jdbcTemplate;
    private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    
    public UserJdbcRepository(JdbcTemplate jdbcTemplate,
                             NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
        this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
    }
    
    // 查詢單個物件
    public User findById(Long id) {
        String sql = "SELECT id, name, email, created_at FROM users WHERE id = ?";
        return jdbcTemplate.queryForObject(sql, new UserRowMapper(), id);
    }
    
    // 查詢列表
    public List<User> findAll() {
        String sql = "SELECT id, name, email, created_at FROM users ORDER BY name";
        return jdbcTemplate.query(sql, new UserRowMapper());
    }
    
    // 條件查詢
    public List<User> findByEmail(String email) {
        String sql = "SELECT id, name, email, created_at FROM users WHERE email LIKE ?";
        return jdbcTemplate.query(sql, new UserRowMapper(), "%" + email + "%");
    }
    
    // 統計查詢
    public long count() {
        String sql = "SELECT COUNT(*) FROM users";
        return jdbcTemplate.queryForObject(sql, Long.class);
    }
    
    // 插入資料
    public User save(User user) {
        if (user.getId() == null) {
            return insert(user);
        } else {
            return update(user);
        }
    }
    
    private User insert(User user) {
        String sql = "INSERT INTO users (name, email, created_at) VALUES (?, ?, ?)";
        
        KeyHolder keyHolder = new GeneratedKeyHolder();
        jdbcTemplate.update(connection -> {
            PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            ps.setString(1, user.getName());
            ps.setString(2, user.getEmail());
            ps.setTimestamp(3, Timestamp.valueOf(LocalDateTime.now()));
            return ps;
        }, keyHolder);
        
        user.setId(keyHolder.getKey().longValue());
        return user;
    }
    
    private User update(User user) {
        String sql = "UPDATE users SET name = ?, email = ? WHERE id = ?";
        int rowsAffected = jdbcTemplate.update(sql, user.getName(), user.getEmail(), user.getId());
        
        if (rowsAffected == 0) {
            throw new RuntimeException("用戶不存在: " + user.getId());
        }
        
        return user;
    }
    
    // 刪除資料
    public void deleteById(Long id) {
        String sql = "DELETE FROM users WHERE id = ?";
        int rowsAffected = jdbcTemplate.update(sql, id);
        
        if (rowsAffected == 0) {
            throw new RuntimeException("用戶不存在: " + id);
        }
    }
    
    // 批次操作
    public void batchInsert(List<User> users) {
        String sql = "INSERT INTO users (name, email, created_at) VALUES (?, ?, ?)";
        
        jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                User user = users.get(i);
                ps.setString(1, user.getName());
                ps.setString(2, user.getEmail());
                ps.setTimestamp(3, Timestamp.valueOf(LocalDateTime.now()));
            }
            
            @Override
            public int getBatchSize() {
                return users.size();
            }
        });
    }
}

// RowMapper 實現
public class UserRowMapper implements RowMapper<User> {
    
    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        User user = new User();
        user.setId(rs.getLong("id"));
        user.setName(rs.getString("name"));
        user.setEmail(rs.getString("email"));
        
        Timestamp createdAt = rs.getTimestamp("created_at");
        if (createdAt != null) {
            user.setCreatedAt(createdAt.toLocalDateTime());
        }
        
        return user;
    }
}

8.1.2 NamedParameterJdbcTemplate 使用

@Repository
public class OrderJdbcRepository {
    
    private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    
    public OrderJdbcRepository(NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
        this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
    }
    
    public List<Order> findByUserIdAndStatus(Long userId, OrderStatus status) {
        String sql = """
            SELECT o.id, o.user_id, o.total_amount, o.status, o.created_at,
                   oi.id as item_id, oi.product_name, oi.quantity, oi.price
            FROM orders o
            LEFT JOIN order_items oi ON o.id = oi.order_id
            WHERE o.user_id = :userId AND o.status = :status
            ORDER BY o.created_at DESC
            """;
        
        Map<String, Object> params = Map.of(
            "userId", userId,
            "status", status.name()
        );
        
        return namedParameterJdbcTemplate.query(sql, params, new OrderResultSetExtractor());
    }
    
    public Order save(Order order) {
        if (order.getId() == null) {
            return insertOrder(order);
        } else {
            return updateOrder(order);
        }
    }
    
    private Order insertOrder(Order order) {
        String orderSql = """
            INSERT INTO orders (user_id, total_amount, status, created_at) 
            VALUES (:userId, :totalAmount, :status, :createdAt)
            """;
        
        SqlParameterSource orderParams = new BeanPropertySqlParameterSource(order) {
            @Override
            public Object getValue(String paramName) {
                if ("status".equals(paramName)) {
                    return order.getStatus().name();
                }
                return super.getValue(paramName);
            }
        };
        
        KeyHolder keyHolder = new GeneratedKeyHolder();
        namedParameterJdbcTemplate.update(orderSql, orderParams, keyHolder);
        
        Long orderId = keyHolder.getKey().longValue();
        order.setId(orderId);
        
        // 插入訂單項目
        if (!order.getItems().isEmpty()) {
            insertOrderItems(orderId, order.getItems());
        }
        
        return order;
    }
    
    private void insertOrderItems(Long orderId, List<OrderItem> items) {
        String sql = """
            INSERT INTO order_items (order_id, product_name, quantity, price) 
            VALUES (:orderId, :productName, :quantity, :price)
            """;
        
        List<Map<String, Object>> batchParams = items.stream()
            .map(item -> Map.of(
                "orderId", orderId,
                "productName", item.getProductName(),
                "quantity", item.getQuantity(),
                "price", item.getPrice()
            ))
            .collect(Collectors.toList());
        
        namedParameterJdbcTemplate.batchUpdate(sql, batchParams.toArray(new Map[0]));
    }
}

// ResultSetExtractor 實現
public class OrderResultSetExtractor implements ResultSetExtractor<List<Order>> {
    
    @Override
    public List<Order> extractData(ResultSet rs) throws SQLException, DataAccessException {
        Map<Long, Order> orderMap = new LinkedHashMap<>();
        
        while (rs.next()) {
            Long orderId = rs.getLong("id");
            Order order = orderMap.get(orderId);
            
            if (order == null) {
                order = new Order();
                order.setId(orderId);
                order.setUserId(rs.getLong("user_id"));
                order.setTotalAmount(rs.getBigDecimal("total_amount"));
                order.setStatus(OrderStatus.valueOf(rs.getString("status")));
                order.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
                order.setItems(new ArrayList<>());
                
                orderMap.put(orderId, order);
            }
            
            Long itemId = rs.getLong("item_id");
            if (itemId > 0) {
                OrderItem item = new OrderItem();
                item.setId(itemId);
                item.setProductName(rs.getString("product_name"));
                item.setQuantity(rs.getInt("quantity"));
                item.setPrice(rs.getBigDecimal("price"));
                
                order.getItems().add(item);
            }
        }
        
        return new ArrayList<>(orderMap.values());
    }
}

8.2 Spring Data JPA

Spring Data JPA 提供了更高層次的抽象,大大簡化了 JPA 的使用。

8.2.1 基本配置

// JPA 配置
@Configuration
@EnableJpaRepositories(basePackages = "com.tutorial.repository")
@EnableTransactionManagement
public class JpaConfig {
    
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setPackagesToScan("com.tutorial.model");
        em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        
        Properties properties = new Properties();
        properties.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect");
        properties.setProperty("hibernate.hbm2ddl.auto", "validate");
        properties.setProperty("hibernate.show_sql", "true");
        properties.setProperty("hibernate.format_sql", "true");
        properties.setProperty("hibernate.use_sql_comments", "true");
        properties.setProperty("hibernate.jdbc.batch_size", "20");
        properties.setProperty("hibernate.order_inserts", "true");
        properties.setProperty("hibernate.order_updates", "true");
        
        em.setJpaProperties(properties);
        return em;
    }
    
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}

// 實體類別
@Entity
@Table(name = "users")
@EntityListeners(AuditingEntityListener.class)
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, length = 100)
    private String name;
    
    @Column(nullable = false, unique = true, length = 255)
    private String email;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private UserStatus status = UserStatus.ACTIVE;
    
    @CreatedDate
    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
    
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Order> orders = new ArrayList<>();
    
    @ManyToMany
    @JoinTable(
        name = "user_roles",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<Role> roles = new HashSet<>();
    
    // 建構子、getter、setter
}

@Entity
@Table(name = "orders")
public class Order {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
    
    @Column(name = "total_amount", nullable = false, precision = 10, scale = 2)
    private BigDecimal totalAmount;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private OrderStatus status;
    
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<OrderItem> items = new ArrayList<>();
    
    @CreatedDate
    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;
    
    // 建構子、getter、setter
}

8.2.2 Repository 介面

// 基本 Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    // 方法名稱查詢
    Optional<User> findByEmail(String email);
    
    List<User> findByNameContainingIgnoreCase(String name);
    
    List<User> findByStatusAndCreatedAtAfter(UserStatus status, LocalDateTime date);
    
    boolean existsByEmail(String email);
    
    long countByStatus(UserStatus status);
    
    void deleteByStatus(UserStatus status);
    
    // 分頁查詢
    Page<User> findByStatus(UserStatus status, Pageable pageable);
    
    Slice<User> findByNameContaining(String name, Pageable pageable);
}

// 自定義查詢
public interface OrderRepository extends JpaRepository<Order, Long> {
    
    // @Query 註解
    @Query("SELECT o FROM Order o WHERE o.user.id = :userId AND o.status = :status")
    List<Order> findByUserAndStatus(@Param("userId") Long userId, 
                                  @Param("status") OrderStatus status);
    
    @Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
    Optional<Order> findByIdWithItems(@Param("id") Long id);
    
    // 原生 SQL 查詢
    @Query(value = "SELECT * FROM orders WHERE user_id = ?1 AND total_amount > ?2", 
           nativeQuery = true)
    List<Order> findExpensiveOrdersByUser(Long userId, BigDecimal amount);
    
    // 統計查詢
    @Query("SELECT COUNT(o) FROM Order o WHERE o.status = :status")
    long countByStatus(@Param("status") OrderStatus status);
    
    @Query("SELECT SUM(o.totalAmount) FROM Order o WHERE o.user.id = :userId")
    BigDecimal getTotalAmountByUser(@Param("userId") Long userId);
    
    // 更新查詢
    @Modifying
    @Query("UPDATE Order o SET o.status = :status WHERE o.id = :id")
    int updateOrderStatus(@Param("id") Long id, @Param("status") OrderStatus status);
    
    @Modifying
    @Query("DELETE FROM Order o WHERE o.status = :status AND o.createdAt < :date")
    int deleteOldOrdersByStatus(@Param("status") OrderStatus status, 
                               @Param("date") LocalDateTime date);
    
    // 複雜查詢
    @Query("""
        SELECT o FROM Order o 
        WHERE o.user.id = :userId 
        AND o.createdAt BETWEEN :startDate AND :endDate
        AND o.totalAmount >= :minAmount
        ORDER BY o.createdAt DESC
        """)
    List<Order> findOrdersByCriteria(@Param("userId") Long userId,
                                   @Param("startDate") LocalDateTime startDate,
                                   @Param("endDate") LocalDateTime endDate,
                                   @Param("minAmount") BigDecimal minAmount);
}

8.2.3 自定義 Repository 實現

// 自定義 Repository 介面
public interface UserRepositoryCustom {
    List<User> findUsersWithComplexCriteria(UserSearchCriteria criteria);
    Page<UserStatistics> getUserStatistics(Pageable pageable);
}

// 自定義 Repository 實現
@Repository
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    @Override
    public List<User> findUsersWithComplexCriteria(UserSearchCriteria criteria) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = cb.createQuery(User.class);
        Root<User> user = query.from(User.class);
        
        List<Predicate> predicates = new ArrayList<>();
        
        // 動態條件建構
        if (criteria.getName() != null) {
            predicates.add(cb.like(cb.lower(user.get("name")), 
                "%" + criteria.getName().toLowerCase() + "%"));
        }
        
        if (criteria.getEmail() != null) {
            predicates.add(cb.like(cb.lower(user.get("email")), 
                "%" + criteria.getEmail().toLowerCase() + "%"));
        }
        
        if (criteria.getStatus() != null) {
            predicates.add(cb.equal(user.get("status"), criteria.getStatus()));
        }
        
        if (criteria.getCreatedAfter() != null) {
            predicates.add(cb.greaterThanOrEqualTo(user.get("createdAt"), 
                criteria.getCreatedAfter()));
        }
        
        if (criteria.getCreatedBefore() != null) {
            predicates.add(cb.lessThanOrEqualTo(user.get("createdAt"), 
                criteria.getCreatedBefore()));
        }
        
        query.where(predicates.toArray(new Predicate[0]));
        query.orderBy(cb.desc(user.get("createdAt")));
        
        TypedQuery<User> typedQuery = entityManager.createQuery(query);
        
        if (criteria.getMaxResults() != null) {
            typedQuery.setMaxResults(criteria.getMaxResults());
        }
        
        return typedQuery.getResultList();
    }
    
    @Override
    public Page<UserStatistics> getUserStatistics(Pageable pageable) {
        String queryStr = """
            SELECT new com.tutorial.dto.UserStatistics(
                u.id, u.name, u.email, 
                COUNT(o.id), 
                COALESCE(SUM(o.totalAmount), 0)
            )
            FROM User u LEFT JOIN u.orders o
            GROUP BY u.id, u.name, u.email
            """;
        
        TypedQuery<UserStatistics> query = entityManager.createQuery(queryStr, UserStatistics.class);
        query.setFirstResult((int) pageable.getOffset());
        query.setMaxResults(pageable.getPageSize());
        
        List<UserStatistics> content = query.getResultList();
        
        // 計算總數
        String countQueryStr = "SELECT COUNT(DISTINCT u.id) FROM User u";
        Long total = entityManager.createQuery(countQueryStr, Long.class).getSingleResult();
        
        return new PageImpl<>(content, pageable, total);
    }
}

// 合併介面
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
    // 繼承基本方法和自定義方法
}

// DTO 類別
@Data
@AllArgsConstructor
public class UserStatistics {
    private Long userId;
    private String name;
    private String email;
    private Long orderCount;
    private BigDecimal totalOrderAmount;
}

// 搜尋條件類別
@Data
public class UserSearchCriteria {
    private String name;
    private String email;
    private UserStatus status;
    private LocalDateTime createdAfter;
    private LocalDateTime createdBefore;
    private Integer maxResults;
}

8.3 事務管理

8.3.1 聲明式事務

// 事務配置
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
    
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(emf);
        return transactionManager;
    }
    
    // 自定義事務通知器
    @Bean
    public TransactionInterceptor transactionInterceptor(PlatformTransactionManager transactionManager) {
        NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
        
        // 只讀事務
        RuleBasedTransactionAttribute readOnlyTx = new RuleBasedTransactionAttribute();
        readOnlyTx.setReadOnly(true);
        readOnlyTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS);
        
        // 讀寫事務
        RuleBasedTransactionAttribute readWriteTx = new RuleBasedTransactionAttribute();
        readWriteTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        readWriteTx.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT);
        readWriteTx.setTimeout(30);
        
        // 方法名稱映射
        source.addTransactionalMethod("get*", readOnlyTx);
        source.addTransactionalMethod("find*", readOnlyTx);
        source.addTransactionalMethod("query*", readOnlyTx);
        source.addTransactionalMethod("count*", readOnlyTx);
        source.addTransactionalMethod("*", readWriteTx);
        
        return new TransactionInterceptor(transactionManager, source);
    }
}

// 服務層事務管理
@Service
@Transactional(readOnly = true)
public class OrderService {
    
    private final OrderRepository orderRepository;
    private final UserRepository userRepository;
    private final PaymentService paymentService;
    private final EmailService emailService;
    
    public OrderService(OrderRepository orderRepository,
                       UserRepository userRepository,
                       PaymentService paymentService,
                       EmailService emailService) {
        this.orderRepository = orderRepository;
        this.userRepository = userRepository;
        this.paymentService = paymentService;
        this.emailService = emailService;
    }
    
    // 只讀事務
    public Order findById(Long id) {
        return orderRepository.findById(id)
            .orElseThrow(() -> new OrderNotFoundException("訂單不存在: " + id));
    }
    
    public Page<Order> findUserOrders(Long userId, Pageable pageable) {
        return orderRepository.findByUserId(userId, pageable);
    }
    
    // 讀寫事務
    @Transactional
    public Order createOrder(Long userId, CreateOrderRequest request) {
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException("用戶不存在: " + userId));
        
        Order order = new Order();
        order.setUser(user);
        order.setTotalAmount(request.getTotalAmount());
        order.setStatus(OrderStatus.PENDING);
        
        // 建立訂單項目
        List<OrderItem> items = request.getItems().stream()
            .map(itemRequest -> {
                OrderItem item = new OrderItem();
                item.setOrder(order);
                item.setProductName(itemRequest.getProductName());
                item.setQuantity(itemRequest.getQuantity());
                item.setPrice(itemRequest.getPrice());
                return item;
            })
            .collect(Collectors.toList());
        
        order.setItems(items);
        Order savedOrder = orderRepository.save(order);
        
        // 發送通知郵件(不影響事務)
        emailService.sendOrderConfirmation(savedOrder);
        
        return savedOrder;
    }
    
    // 複雜事務處理
    @Transactional(
        isolation = Isolation.REPEATABLE_READ,
        timeout = 60,
        rollbackFor = {Exception.class},
        noRollbackFor = {EmailException.class}
    )
    public Order processOrder(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException("訂單不存在: " + orderId));
        
        if (order.getStatus() != OrderStatus.PENDING) {
            throw new InvalidOrderStatusException("訂單狀態不正確");
        }
        
        try {
            // 處理付款
            PaymentResult paymentResult = paymentService.processPayment(order);
            
            if (paymentResult.isSuccess()) {
                order.setStatus(OrderStatus.CONFIRMED);
                order = orderRepository.save(order);
                
                // 發送確認郵件(如果失敗不影響事務)
                try {
                    emailService.sendOrderConfirmation(order);
                } catch (EmailException e) {
                    // 記錄日誌但不回滾事務
                    logger.warn("發送確認郵件失敗: {}", e.getMessage());
                }
            } else {
                order.setStatus(OrderStatus.FAILED);
                order = orderRepository.save(order);
                throw new PaymentFailedException("付款失敗: " + paymentResult.getErrorMessage());
            }
            
        } catch (PaymentException e) {
            order.setStatus(OrderStatus.FAILED);
            orderRepository.save(order);
            throw e;
        }
        
        return order;
    }
    
    // 事務傳播示例
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logOrderActivity(Long orderId, String activity) {
        // 獨立事務記錄活動日誌
        OrderLog log = new OrderLog();
        log.setOrderId(orderId);
        log.setActivity(activity);
        log.setTimestamp(LocalDateTime.now());
        orderLogRepository.save(log);
    }
}

8.3.2 程式化事務

@Service
public class BatchProcessingService {
    
    private final PlatformTransactionManager transactionManager;
    private final OrderRepository orderRepository;
    
    public BatchProcessingService(PlatformTransactionManager transactionManager,
                                OrderRepository orderRepository) {
        this.transactionManager = transactionManager;
        this.orderRepository = orderRepository;
    }
    
    public void processBatchOrders(List<Order> orders) {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        def.setTimeout(300); // 5 分鐘超時
        
        TransactionStatus status = transactionManager.getTransaction(def);
        
        try {
            for (Order order : orders) {
                processOrder(order);
            }
            
            transactionManager.commit(status);
            logger.info("批次處理完成,處理 {} 筆訂單", orders.size());
            
        } catch (Exception e) {
            transactionManager.rollback(status);
            logger.error("批次處理失敗,回滾事務", e);
            throw new BatchProcessingException("批次處理失敗", e);
        }
    }
    
    public void processOrdersWithSavepoint(List<Order> orders) {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        TransactionStatus status = transactionManager.getTransaction(def);
        
        try {
            for (int i = 0; i < orders.size(); i++) {
                Object savepoint = status.createSavepoint();
                
                try {
                    processOrder(orders.get(i));
                } catch (Exception e) {
                    logger.warn("處理訂單 {} 失敗,回滾到保存點", orders.get(i).getId(), e);
                    status.rollbackToSavepoint(savepoint);
                }
                
                status.releaseSavepoint(savepoint);
            }
            
            transactionManager.commit(status);
            
        } catch (Exception e) {
            transactionManager.rollback(status);
            throw e;
        }
    }
    
    private void processOrder(Order order) {
        // 處理訂單邏輯
        order.setStatus(OrderStatus.PROCESSED);
        orderRepository.save(order);
    }
}

8.4 認證考點提示

重要考點:

  • JdbcTemplate vs NamedParameterJdbcTemplate:使用場景和優缺點
  • Spring Data JPA:方法名稱查詢規則和 @Query 註解
  • 事務管理:@Transactional 的各種屬性設定
  • 事務傳播行為:七種傳播行為的差異
  • 事務隔離級別:四種隔離級別的特性
  • 異常處理:哪些異常會觸發回滾

8.5 實務案例

// 完整的資料存取層實現
@Service
@Transactional(readOnly = true)
public class UserService {
    
    private final UserRepository userRepository;
    private final RoleRepository roleRepository;
    private final AuditService auditService;
    
    public UserService(UserRepository userRepository,
                      RoleRepository roleRepository,
                      AuditService auditService) {
        this.userRepository = userRepository;
        this.roleRepository = roleRepository;
        this.auditService = auditService;
    }
    
    public Page<User> findUsers(UserSearchCriteria criteria, Pageable pageable) {
        if (criteria.hasComplexCriteria()) {
            // 使用自定義查詢
            List<User> users = userRepository.findUsersWithComplexCriteria(criteria);
            return new PageImpl<>(users, pageable, users.size());
        } else {
            // 使用基本查詢
            return userRepository.findByStatus(criteria.getStatus(), pageable);
        }
    }
    
    @Transactional
    public User createUser(CreateUserRequest request) {
        // 檢查郵箱是否已存在
        if (userRepository.existsByEmail(request.getEmail())) {
            throw new EmailAlreadyExistsException("郵箱已存在: " + request.getEmail());
        }
        
        User user = new User();
        user.setName(request.getName());
        user.setEmail(request.getEmail());
        user.setStatus(UserStatus.ACTIVE);
        
        // 設定默認角色
        Role defaultRole = roleRepository.findByName("USER")
            .orElseThrow(() -> new RoleNotFoundException("默認角色不存在"));
        user.getRoles().add(defaultRole);
        
        User savedUser = userRepository.save(user);
        
        // 記錄審計日誌
        auditService.logUserCreation(savedUser.getId());
        
        return savedUser;
    }
    
    @Transactional
    public void assignRole(Long userId, String roleName) {
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException("用戶不存在: " + userId));
        
        Role role = roleRepository.findByName(roleName)
            .orElseThrow(() -> new RoleNotFoundException("角色不存在: " + roleName));
        
        if (user.getRoles().contains(role)) {
            throw new RoleAlreadyAssignedException("用戶已擁有該角色");
        }
        
        user.getRoles().add(role);
        userRepository.save(user);
        
        auditService.logRoleAssignment(userId, role.getId());
    }
}

注意事項:

  • 合理使用事務的隔離級別和傳播行為
  • 注意 N+1 查詢問題,適當使用 JOIN FETCH
  • 大批次操作要分批處理,避免記憶體溢出
  • 只讀事務可以提升效能
  • 注意事務邊界,避免過大或過小的事務
  • 適當處理樂觀鎖和悲觀鎖衝突

9. Spring Security

9.1 Spring Security 概述

Spring Security 是一個功能強大且高度可自定義的認證和存取控制框架,為 Spring 應用程式提供全面的安全性解決方案。

安全架構圖:

graph TB A[HTTP Request] --> B[Security Filter Chain] B --> C[Authentication Filter] C --> D[Authentication Manager] D --> E[Authentication Provider] E --> F[UserDetailsService] F --> G[User Database] D --> H[Authentication Object] H --> I[Security Context] I --> J[Authorization Filter] J --> K[Access Decision Manager] K --> L[Voter] L --> M[Controller Method]

9.2 基本配置

9.2.1 Security 配置類

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig {
    
    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtRequestFilter jwtRequestFilter;
    
    public SecurityConfig(UserDetailsService userDetailsService,
                         PasswordEncoder passwordEncoder,
                         JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
                         JwtRequestFilter jwtRequestFilter) {
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
        this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
        this.jwtRequestFilter = jwtRequestFilter;
    }
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // CSRF 配置
            .csrf(csrf -> csrf
                .ignoringRequestMatchers("/api/**")
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            )
            
            // CORS 配置
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            
            // 會話管理
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            
            // 異常處理
            .exceptionHandling(exceptions -> exceptions
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .accessDeniedHandler(customAccessDeniedHandler())
            )
            
            // 授權規則
            .authorizeHttpRequests(authz -> authz
                // 公開端點
                .requestMatchers("/api/auth/**", "/api/public/**").permitAll()
                .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
                
                // 需要認證的端點
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers(HttpMethod.POST, "/api/products/**").hasAnyRole("ADMIN", "MANAGER")
                .requestMatchers(HttpMethod.PUT, "/api/products/**").hasAnyRole("ADMIN", "MANAGER")
                .requestMatchers(HttpMethod.DELETE, "/api/products/**").hasRole("ADMIN")
                
                // 其他請求需要認證
                .anyRequest().authenticated()
            )
            
            // 添加 JWT 過濾器
            .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
            
            // HTTP Basic 認證(可選)
            .httpBasic(Customizer.withDefaults())
            
            // 表單登入(可選)
            .formLogin(form -> form
                .loginPage("/login")
                .loginProcessingUrl("/api/auth/login")
                .defaultSuccessUrl("/dashboard")
                .failureUrl("/login?error=true")
                .permitAll()
            )
            
            // 登出配置
            .logout(logout -> logout
                .logoutUrl("/api/auth/logout")
                .logoutSuccessUrl("/")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
                .permitAll()
            );
        
        return http.build();
    }
    
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
    
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder);
        return authProvider;
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(Arrays.asList("http://localhost:3000", "https://*.example.com"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
    
    @Bean
    public AccessDeniedHandler customAccessDeniedHandler() {
        return new CustomAccessDeniedHandler();
    }
}

9.2.2 UserDetailsService 實現

@Service
public class CustomUserDetailsService implements UserDetailsService {
    
    private final UserRepository userRepository;
    
    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(username)
            .orElseThrow(() -> new UsernameNotFoundException("用戶不存在: " + username));
        
        if (!user.isActive()) {
            throw new DisabledException("用戶帳號已被停用");
        }
        
        return UserPrincipal.create(user);
    }
}

// 自定義 UserDetails 實現
public class UserPrincipal implements UserDetails {
    
    private Long id;
    private String name;
    private String email;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;
    private boolean enabled;
    private boolean accountNonExpired;
    private boolean accountNonLocked;
    private boolean credentialsNonExpired;
    
    public UserPrincipal(Long id, String name, String email, String password,
                        Collection<? extends GrantedAuthority> authorities,
                        boolean enabled, boolean accountNonExpired,
                        boolean accountNonLocked, boolean credentialsNonExpired) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.password = password;
        this.authorities = authorities;
        this.enabled = enabled;
        this.accountNonExpired = accountNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.credentialsNonExpired = credentialsNonExpired;
    }
    
    public static UserPrincipal create(User user) {
        List<GrantedAuthority> authorities = user.getRoles().stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
            .collect(Collectors.toList());
        
        return new UserPrincipal(
            user.getId(),
            user.getName(),
            user.getEmail(),
            user.getPassword(),
            authorities,
            user.getStatus() == UserStatus.ACTIVE,
            true, // accountNonExpired
            true, // accountNonLocked
            true  // credentialsNonExpired
        );
    }
    
    // UserDetails 介面方法實現
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }
    
    @Override
    public String getPassword() {
        return password;
    }
    
    @Override
    public String getUsername() {
        return email;
    }
    
    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }
    
    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }
    
    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }
    
    @Override
    public boolean isEnabled() {
        return enabled;
    }
    
    // 額外的 getter 方法
    public Long getId() {
        return id;
    }
    
    public String getName() {
        return name;
    }
    
    public String getEmail() {
        return email;
    }
}

9.3 JWT 認證實現

9.3.1 JWT 工具類

@Component
public class JwtTokenUtil {
    
    private static final Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);
    
    @Value("${app.jwtSecret}")
    private String jwtSecret;
    
    @Value("${app.jwtExpirationInMs}")
    private int jwtExpirationInMs;
    
    public String generateToken(UserPrincipal userPrincipal) {
        Date expiryDate = new Date(System.currentTimeMillis() + jwtExpirationInMs);
        
        return Jwts.builder()
            .setSubject(userPrincipal.getId().toString())
            .claim("email", userPrincipal.getEmail())
            .claim("name", userPrincipal.getName())
            .claim("authorities", userPrincipal.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList()))
            .setIssuedAt(new Date())
            .setExpiration(expiryDate)
            .signWith(SignatureAlgorithm.HS512, jwtSecret)
            .compact();
    }
    
    public String generateRefreshToken(UserPrincipal userPrincipal) {
        Date expiryDate = new Date(System.currentTimeMillis() + jwtExpirationInMs * 7); // 7倍時間
        
        return Jwts.builder()
            .setSubject(userPrincipal.getId().toString())
            .claim("type", "refresh")
            .setIssuedAt(new Date())
            .setExpiration(expiryDate)
            .signWith(SignatureAlgorithm.HS512, jwtSecret)
            .compact();
    }
    
    public Long getUserIdFromToken(String token) {
        Claims claims = Jwts.parser()
            .setSigningKey(jwtSecret)
            .parseClaimsJws(token)
            .getBody();
        
        return Long.parseLong(claims.getSubject());
    }
    
    public boolean validateToken(String authToken) {
        try {
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
            return true;
        } catch (SignatureException ex) {
            logger.error("無效的 JWT 簽名");
        } catch (MalformedJwtException ex) {
            logger.error("無效的 JWT 令牌");
        } catch (ExpiredJwtException ex) {
            logger.error("JWT 令牌已過期");
        } catch (UnsupportedJwtException ex) {
            logger.error("不支援的 JWT 令牌");
        } catch (IllegalArgumentException ex) {
            logger.error("JWT 聲明字串為空");
        }
        return false;
    }
    
    public Date getExpirationDateFromToken(String token) {
        Claims claims = Jwts.parser()
            .setSigningKey(jwtSecret)
            .parseClaimsJws(token)
            .getBody();
        
        return claims.getExpiration();
    }
    
    public boolean isTokenExpired(String token) {
        Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }
}

9.3.2 JWT 認證過濾器

@Component
public class JwtRequestFilter extends OncePerRequestFilter {
    
    private static final Logger logger = LoggerFactory.getLogger(JwtRequestFilter.class);
    
    private final UserDetailsService userDetailsService;
    private final JwtTokenUtil jwtTokenUtil;
    
    public JwtRequestFilter(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil) {
        this.userDetailsService = userDetailsService;
        this.jwtTokenUtil = jwtTokenUtil;
    }
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                  HttpServletResponse response, 
                                  FilterChain chain) throws ServletException, IOException {
        
        final String requestTokenHeader = request.getHeader("Authorization");
        
        String username = null;
        String jwtToken = null;
        
        // JWT 令牌格式為 "Bearer token"
        if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
            jwtToken = requestTokenHeader.substring(7);
            try {
                username = jwtTokenUtil.getUserIdFromToken(jwtToken).toString();
            } catch (IllegalArgumentException e) {
                logger.warn("無法獲取 JWT 令牌");
            } catch (ExpiredJwtException e) {
                logger.warn("JWT 令牌已過期");
            }
        } else {
            logger.debug("JWT 令牌不是以 Bearer 開頭");
        }
        
        // 驗證令牌
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
            
            if (jwtTokenUtil.validateToken(jwtToken)) {
                UsernamePasswordAuthenticationToken authToken = 
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        
        chain.doFilter(request, response);
    }
}

// JWT 認證入口點
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    
    private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
    
    @Override
    public void commence(HttpServletRequest request, 
                        HttpServletResponse response,
                        AuthenticationException authException) throws IOException, ServletException {
        
        logger.error("未授權的請求 - {}", authException.getMessage());
        
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        
        ObjectMapper mapper = new ObjectMapper();
        Map<String, Object> errorResponse = Map.of(
            "timestamp", LocalDateTime.now().toString(),
            "status", HttpStatus.UNAUTHORIZED.value(),
            "error", "Unauthorized",
            "message", "請先登入",
            "path", request.getRequestURI()
        );
        
        response.getWriter().write(mapper.writeValueAsString(errorResponse));
    }
}

9.4 認證控制器

@RestController
@RequestMapping("/api/auth")
@Validated
public class AuthController {
    
    private static final Logger logger = LoggerFactory.getLogger(AuthController.class);
    
    private final AuthenticationManager authenticationManager;
    private final UserService userService;
    private final JwtTokenUtil jwtTokenUtil;
    private final PasswordEncoder passwordEncoder;
    
    public AuthController(AuthenticationManager authenticationManager,
                         UserService userService,
                         JwtTokenUtil jwtTokenUtil,
                         PasswordEncoder passwordEncoder) {
        this.authenticationManager = authenticationManager;
        this.userService = userService;
        this.jwtTokenUtil = jwtTokenUtil;
        this.passwordEncoder = passwordEncoder;
    }
    
    @PostMapping("/login")
    public ResponseEntity<JwtAuthenticationResponse> authenticateUser(
            @Valid @RequestBody LoginRequest loginRequest) {
        
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                loginRequest.getEmail(),
                loginRequest.getPassword()
            )
        );
        
        SecurityContextHolder.getContext().setAuthentication(authentication);
        
        UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
        String jwt = jwtTokenUtil.generateToken(userPrincipal);
        String refreshToken = jwtTokenUtil.generateRefreshToken(userPrincipal);
        
        logger.info("用戶登入成功: {}", userPrincipal.getEmail());
        
        return ResponseEntity.ok(new JwtAuthenticationResponse(
            jwt, 
            refreshToken,
            "Bearer",
            jwtTokenUtil.getExpirationDateFromToken(jwt),
            UserDTO.from(userPrincipal)
        ));
    }
    
    @PostMapping("/register")
    public ResponseEntity<ApiResponse> registerUser(@Valid @RequestBody SignUpRequest signUpRequest) {
        
        if (userService.existsByEmail(signUpRequest.getEmail())) {
            return ResponseEntity.badRequest()
                .body(new ApiResponse(false, "郵箱已被使用!"));
        }
        
        // 創建新用戶
        User user = new User();
        user.setName(signUpRequest.getName());
        user.setEmail(signUpRequest.getEmail());
        user.setPassword(passwordEncoder.encode(signUpRequest.getPassword()));
        user.setStatus(UserStatus.ACTIVE);
        
        User result = userService.save(user);
        
        logger.info("新用戶註冊成功: {}", result.getEmail());
        
        return ResponseEntity.ok(new ApiResponse(true, "用戶註冊成功"));
    }
    
    @PostMapping("/refresh")
    public ResponseEntity<JwtAuthenticationResponse> refreshToken(
            @Valid @RequestBody RefreshTokenRequest refreshTokenRequest) {
        
        String refreshToken = refreshTokenRequest.getRefreshToken();
        
        if (!jwtTokenUtil.validateToken(refreshToken)) {
            throw new TokenRefreshException("無效的刷新令牌");
        }
        
        Long userId = jwtTokenUtil.getUserIdFromToken(refreshToken);
        User user = userService.findById(userId);
        UserPrincipal userPrincipal = UserPrincipal.create(user);
        
        String newToken = jwtTokenUtil.generateToken(userPrincipal);
        String newRefreshToken = jwtTokenUtil.generateRefreshToken(userPrincipal);
        
        return ResponseEntity.ok(new JwtAuthenticationResponse(
            newToken,
            newRefreshToken,
            "Bearer",
            jwtTokenUtil.getExpirationDateFromToken(newToken),
            UserDTO.from(userPrincipal)
        ));
    }
    
    @PostMapping("/logout")
    @PreAuthorize("hasRole('USER')")
    public ResponseEntity<ApiResponse> logout(HttpServletRequest request) {
        // 在實際應用中,可以將令牌加入黑名單
        SecurityContextHolder.clearContext();
        
        logger.info("用戶登出成功");
        
        return ResponseEntity.ok(new ApiResponse(true, "登出成功"));
    }
    
    @GetMapping("/me")
    @PreAuthorize("hasRole('USER')")
    public ResponseEntity<UserDTO> getCurrentUser(@CurrentUser UserPrincipal currentUser) {
        return ResponseEntity.ok(UserDTO.from(currentUser));
    }
}

// DTO 類別
@Data
@AllArgsConstructor
public class JwtAuthenticationResponse {
    private String accessToken;
    private String refreshToken;
    private String tokenType;
    private Date expiryDate;
    private UserDTO user;
}

@Data
public class LoginRequest {
    @NotBlank
    @Email
    private String email;
    
    @NotBlank
    @Size(min = 6, max = 20)
    private String password;
}

@Data
public class SignUpRequest {
    @NotBlank
    @Size(min = 2, max = 40)
    private String name;
    
    @NotBlank
    @Size(max = 40)
    @Email
    private String email;
    
    @NotBlank
    @Size(min = 6, max = 20)
    private String password;
}

@Data
public class RefreshTokenRequest {
    @NotBlank
    private String refreshToken;
}

@Data
@AllArgsConstructor
public class ApiResponse {
    private Boolean success;
    private String message;
}

9.5 方法級安全

9.5.1 方法安全註解

@Service
@PreAuthorize("hasRole('USER')")
public class DocumentService {
    
    private final DocumentRepository documentRepository;
    
    public DocumentService(DocumentRepository documentRepository) {
        this.documentRepository = documentRepository;
    }
    
    // 檢查用戶是否為文檔擁有者
    @PreAuthorize("hasRole('ADMIN') or @documentSecurityService.isOwner(#id, authentication.principal.id)")
    public Document getDocument(Long id) {
        return documentRepository.findById(id)
            .orElseThrow(() -> new DocumentNotFoundException("文檔不存在: " + id));
    }
    
    // 檢查返回的物件是否屬於當前用戶
    @PostAuthorize("returnObject.ownerId == authentication.principal.id or hasRole('ADMIN')")
    public Document findDocument(Long id) {
        return documentRepository.findById(id).orElse(null);
    }
    
    // 過濾集合中的元素
    @PostFilter("filterObject.ownerId == authentication.principal.id or hasRole('ADMIN')")
    public List<Document> findAllDocuments() {
        return documentRepository.findAll();
    }
    
    // 過濾輸入參數
    @PreFilter("filterObject.ownerId == authentication.principal.id")
    public void deleteDocuments(List<Document> documents) {
        documentRepository.deleteAll(documents);
    }
    
    // 複雜的權限檢查
    @PreAuthorize("hasRole('ADMIN') or " +
                 "(hasRole('MANAGER') and @documentSecurityService.isInSameDepartment(#id, authentication.principal.id)) or " +
                 "(@documentSecurityService.isOwner(#id, authentication.principal.id) and @documentSecurityService.canModify(#id))")
    public Document updateDocument(Long id, UpdateDocumentRequest request) {
        Document document = getDocument(id);
        document.setTitle(request.getTitle());
        document.setContent(request.getContent());
        return documentRepository.save(document);
    }
    
    // 使用 @Secured 註解
    @Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
    public void deleteDocument(Long id) {
        Document document = getDocument(id);
        documentRepository.delete(document);
    }
    
    // 使用 @RolesAllowed 註解 (JSR-250)
    @RolesAllowed({"ADMIN", "MANAGER"})
    public List<Document> getAllDocuments() {
        return documentRepository.findAll();
    }
}

// 自定義安全服務
@Service("documentSecurityService")
public class DocumentSecurityService {
    
    private final DocumentRepository documentRepository;
    private final UserRepository userRepository;
    
    public DocumentSecurityService(DocumentRepository documentRepository,
                                 UserRepository userRepository) {
        this.documentRepository = documentRepository;
        this.userRepository = userRepository;
    }
    
    public boolean isOwner(Long documentId, Long userId) {
        return documentRepository.findById(documentId)
            .map(document -> document.getOwnerId().equals(userId))
            .orElse(false);
    }
    
    public boolean canModify(Long documentId) {
        return documentRepository.findById(documentId)
            .map(document -> document.getStatus() == DocumentStatus.DRAFT)
            .orElse(false);
    }
    
    public boolean isInSameDepartment(Long documentId, Long userId) {
        Optional<Document> documentOpt = documentRepository.findById(documentId);
        Optional<User> userOpt = userRepository.findById(userId);
        
        if (documentOpt.isPresent() && userOpt.isPresent()) {
            Document document = documentOpt.get();
            User user = userOpt.get();
            
            User owner = userRepository.findById(document.getOwnerId()).orElse(null);
            return owner != null && owner.getDepartmentId().equals(user.getDepartmentId());
        }
        
        return false;
    }
    
    public boolean hasPermission(UserPrincipal user, Document document, String permission) {
        // 實現複雜的權限邏輯
        if (user.getAuthorities().stream()
            .anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN"))) {
            return true;
        }
        
        if (document.getOwnerId().equals(user.getId())) {
            return true;
        }
        
        // 檢查共享權限
        return checkSharedPermission(user.getId(), document.getId(), permission);
    }
    
    private boolean checkSharedPermission(Long userId, Long documentId, String permission) {
        // 實現共享權限檢查邏輯
        return false;
    }
}

9.6 認證考點提示

重要考點:

  • Security Filter Chain:過濾器的執行順序和作用
  • Authentication vs Authorization:認證和授權的區別
  • UserDetails 和 UserDetailsService:用戶資訊的載入機制
  • PasswordEncoder:密碼編碼的最佳實務
  • 方法安全性:@PreAuthorize、@PostAuthorize 的使用
  • CSRF 保護:何時啟用/停用 CSRF 保護
  • 會話管理:不同的會話策略

9.7 實務案例

// 完整的安全配置範例
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class ProductionSecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // API 模式停用 CSRF
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authorizeHttpRequests(authz -> authz
                // 公開 API
                .requestMatchers("/api/public/**", "/api/auth/**").permitAll()
                .requestMatchers("/actuator/health", "/actuator/info").permitAll()
                
                // 管理員 API
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers("/actuator/**").hasRole("ADMIN")
                
                // 業務 API
                .requestMatchers(HttpMethod.GET, "/api/products/**").hasAnyRole("USER", "MANAGER", "ADMIN")
                .requestMatchers(HttpMethod.POST, "/api/orders/**").hasAnyRole("USER", "MANAGER", "ADMIN")
                .requestMatchers(HttpMethod.PUT, "/api/products/**").hasAnyRole("MANAGER", "ADMIN")
                
                // 其他請求需要認證
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtDecoder(jwtDecoder()))
            )
            .exceptionHandling(exceptions -> exceptions
                .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                .accessDeniedHandler(new CustomAccessDeniedHandler())
            );
        
        return http.build();
    }
    
    @Bean
    public JwtDecoder jwtDecoder() {
        // 配置 JWT 解碼器
        return NimbusJwtDecoder.withJwkSetUri("https://your-auth-server/.well-known/jwks.json").build();
    }
}

// 審計和日誌
@Component
@Slf4j
public class SecurityAuditListener {
    
    @EventListener
    public void handleAuthenticationSuccess(AbstractAuthenticationEvent event) {
        Authentication authentication = event.getAuthentication();
        log.info("認證成功: user={}, authorities={}", 
                authentication.getName(), 
                authentication.getAuthorities());
    }
    
    @EventListener
    public void handleAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
        Authentication authentication = event.getAuthentication();
        Exception exception = event.getException();
        log.warn("認證失敗: user={}, reason={}", 
                authentication.getName(), 
                exception.getMessage());
    }
    
    @EventListener
    public void handleAccessDenied(AuthorizationDeniedEvent event) {
        log.warn("存取被拒絕: user={}, resource={}", 
                event.getAuthentication().getName(),
                event.getAuthorizationDecision());
    }
}

注意事項:

  • 密碼必須使用安全的編碼方式(如 BCrypt)
  • JWT 令牌要設定適當的過期時間
  • 敏感操作要記錄審計日誌
  • 實現適當的訪問控制和權限管理
  • 注意 CORS 配置的安全性
  • 定期更新安全相關的依賴庫

10. Spring Boot 整合

10.1 Spring Boot 概述

Spring Boot 是 Spring 團隊提供的全新框架,設計目的是簡化 Spring 應用程式的初始搭建和開發過程。它採用「約定優於配置」的設計理念,提供了大量的自動配置功能。

Spring Boot vs Spring Framework 比較:

特性Spring FrameworkSpring Boot
配置方式XML/Java 配置自動配置 + 屬性檔案
啟動方式需要外部容器內嵌式容器
依賴管理手動管理版本Starter 依賴
監控功能需要額外配置Actuator 內建
開發效率需要大量配置快速啟動

10.1.1 Spring Boot 核心特性

graph TB A[Spring Boot] --> B[自動配置] A --> C[Starter 依賴] A --> D[內嵌式容器] A --> E[Production-ready 功能] B --> B1[條件註解] B --> B2[配置類別] C --> C1[依賴管理] C --> C2[版本控制] D --> D1[Tomcat] D --> D2[Jetty] D --> D3[Undertow] E --> E1[Actuator] E --> E2[Metrics] E --> E3[Health Checks]

10.2 Spring Boot 專案設定

10.2.1 Maven 配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <!-- Spring Boot Parent -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>
    
    <groupId>com.tutorial</groupId>
    <artifactId>spring-boot-tutorial</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    
    <name>Spring Boot Tutorial</name>
    <description>Spring Boot 教學專案</description>
    
    <properties>
        <java.version>17</java.version>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Web Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Spring Boot Data JPA Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        
        <!-- Spring Boot Security Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        
        <!-- Spring Boot Validation Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        
        <!-- Spring Boot Actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        
        <!-- Spring Boot Test Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
        <!-- MySQL Driver -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- H2 Database (for testing) -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>test</scope>
        </dependency>
        
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <!-- Spring Boot Maven Plugin -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
            
            <!-- Maven Compiler Plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

10.2.2 主應用程式類別

@SpringBootApplication
@EnableJpaAuditing
public class SpringBootTutorialApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(SpringBootTutorialApplication.class, args);
    }
    
    // 自定義 CommandLineRunner
    @Bean
    public CommandLineRunner initData(UserRepository userRepository, 
                                    PasswordEncoder passwordEncoder) {
        return args -> {
            // 檢查是否已有管理員用戶
            if (!userRepository.existsByEmail("admin@example.com")) {
                User admin = new User();
                admin.setName("管理員");
                admin.setEmail("admin@example.com");
                admin.setPassword(passwordEncoder.encode("admin123"));
                admin.setStatus(UserStatus.ACTIVE);
                userRepository.save(admin);
                
                System.out.println("已創建預設管理員用戶");
            }
        };
    }
    
    // 自定義應用程式啟動監聽器
    @EventListener
    public void handleApplicationReady(ApplicationReadyEvent event) {
        System.out.println("應用程式啟動完成!");
        System.out.println("可用的端點:");
        System.out.println("- http://localhost:8080/api/health");
        System.out.println("- http://localhost:8080/actuator/health");
    }
}

// 分離的配置類別
@Configuration
@ConditionalOnProperty(name = "app.async.enabled", havingValue = "true", matchIfMissing = true)
@EnableAsync
public class AsyncConfig {
    
    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-");
        executor.initialize();
        return executor;
    }
}

@Configuration
@EnableScheduling
@ConditionalOnProperty(name = "app.scheduling.enabled", havingValue = "true")
public class SchedulingConfig {
    
    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(5);
        scheduler.setThreadNamePrefix("scheduler-");
        scheduler.initialize();
        return scheduler;
    }
}

10.3 配置管理

10.3.1 application.yml 配置

# application.yml
spring:
  application:
    name: spring-boot-tutorial
    
  profiles:
    active: development
    
  datasource:
    url: jdbc:mysql://localhost:3306/tutorial_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: ${DB_USERNAME:root}
    password: ${DB_PASSWORD:password}
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      idle-timeout: 300000
      connection-timeout: 20000
      leak-detection-threshold: 60000
      
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect
        format_sql: true
        use_sql_comments: true
        jdbc:
          batch_size: 20
        order_inserts: true
        order_updates: true
        
  jackson:
    serialization:
      write-dates-as-timestamps: false
    time-zone: Asia/Taipei
    date-format: yyyy-MM-dd HH:mm:ss
    
  security:
    user:
      name: admin
      password: admin
      roles: ADMIN
      
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 50MB
      
server:
  port: 8080
  servlet:
    context-path: /
    encoding:
      charset: UTF-8
      enabled: true
      force: true
  compression:
    enabled: true
    mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json
    min-response-size: 1024
    
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
      base-path: /actuator
  endpoint:
    health:
      show-details: when-authorized
    metrics:
      enabled: true
  metrics:
    export:
      prometheus:
        enabled: true
        
logging:
  level:
    com.tutorial: DEBUG
    org.springframework.security: DEBUG
    org.hibernate.SQL: DEBUG
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
    file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
  file:
    name: logs/application.log
    max-size: 10MB
    max-history: 30
    
app:
  jwt:
    secret: ${JWT_SECRET:mySecretKey}
    expiration: 86400000 # 24 hours
  cors:
    allowed-origins: http://localhost:3000,http://localhost:3001
  async:
    enabled: true
  scheduling:
    enabled: true
  features:
    user-registration: true
    email-notifications: true
    file-upload: true

---
# Development Profile
spring:
  config:
    activate:
      on-profile: development
      
  datasource:
    url: jdbc:h2:mem:devdb
    username: sa
    password: 
    driver-class-name: org.h2.Driver
    
  h2:
    console:
      enabled: true
      path: /h2-console
      
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true
    
logging:
  level:
    root: INFO
    com.tutorial: DEBUG

---
# Test Profile  
spring:
  config:
    activate:
      on-profile: test
      
  datasource:
    url: jdbc:h2:mem:testdb
    username: sa
    password:
    driver-class-name: org.h2.Driver
    
  jpa:
    hibernate:
      ddl-auto: create-drop
      
app:
  features:
    email-notifications: false

---
# Production Profile
spring:
  config:
    activate:
      on-profile: production
      
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false
    
logging:
  level:
    root: WARN
    com.tutorial: INFO
    
management:
  endpoints:
    web:
      exposure:
        include: health,info

10.3.2 配置屬性類別

@ConfigurationProperties(prefix = "app")
@Data
@Component
@Validated
public class AppProperties {
    
    private Jwt jwt = new Jwt();
    private Cors cors = new Cors();
    private Features features = new Features();
    private Upload upload = new Upload();
    
    @Data
    public static class Jwt {
        @NotBlank
        private String secret;
        
        @Min(1)
        private long expiration = 86400000; // 24 hours
    }
    
    @Data
    public static class Cors {
        private List<String> allowedOrigins = Arrays.asList("http://localhost:3000");
        private List<String> allowedMethods = Arrays.asList("GET", "POST", "PUT", "DELETE");
        private List<String> allowedHeaders = Arrays.asList("*");
        private boolean allowCredentials = true;
    }
    
    @Data
    public static class Features {
        private boolean userRegistration = true;
        private boolean emailNotifications = true;
        private boolean fileUpload = true;
        private boolean socialLogin = false;
    }
    
    @Data
    public static class Upload {
        @Size(max = 20)
        private String maxFileSize = "10MB";
        
        @Size(max = 50)
        private String maxRequestSize = "50MB";
        
        private List<String> allowedTypes = Arrays.asList(
            "image/jpeg", "image/png", "image/gif", "application/pdf"
        );
    }
}

// 條件配置類別
@Configuration
@ConditionalOnProperty(name = "app.features.email-notifications", havingValue = "true")
public class EmailConfig {
    
    @Bean
    @ConditionalOnMissingBean
    public EmailService emailService() {
        return new SmtpEmailService();
    }
}

@Configuration
@ConditionalOnProperty(name = "app.features.file-upload", havingValue = "true")
public class FileUploadConfig {
    
    @Bean
    public MultipartConfigElement multipartConfigElement(AppProperties appProperties) {
        MultipartConfigFactory factory = new MultipartConfigFactory();
        factory.setMaxFileSize(DataSize.parse(appProperties.getUpload().getMaxFileSize()));
        factory.setMaxRequestSize(DataSize.parse(appProperties.getUpload().getMaxRequestSize()));
        return factory.createMultipartConfig();
    }
}

10.4 自動配置

10.4.1 自定義自動配置類別

// 自動配置類別
@Configuration
@ConditionalOnClass(RedisTemplate.class)
@ConditionalOnProperty(name = "app.cache.redis.enabled", havingValue = "true")
@EnableConfigurationProperties(RedisProperties.class)
public class RedisAutoConfiguration {
    
    @Bean
    @ConditionalOnMissingBean
    public LettuceConnectionFactory redisConnectionFactory(RedisProperties properties) {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName(properties.getHost());
        config.setPort(properties.getPort());
        config.setDatabase(properties.getDatabase());
        
        if (properties.getPassword() != null) {
            config.setPassword(properties.getPassword());
        }
        
        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
            .commandTimeout(Duration.ofSeconds(properties.getTimeout()))
            .poolConfig(connectionPoolConfig(properties.getPool()))
            .build();
            
        return new LettuceConnectionFactory(config, clientConfig);
    }
    
    @Bean
    @ConditionalOnMissingBean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        
        // 設定序列化器
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);
        
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);
        
        template.afterPropertiesSet();
        return template;
    }
    
    @Bean
    @ConditionalOnMissingBean
    public CacheManager cacheManager(LettuceConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(1))
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new Jackson2JsonRedisSerializer<>(Object.class)));
                
        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(config)
            .build();
    }
    
    private GenericObjectPoolConfig<?> connectionPoolConfig(RedisProperties.Pool pool) {
        GenericObjectPoolConfig<?> config = new GenericObjectPoolConfig<>();
        config.setMaxTotal(pool.getMaxActive());
        config.setMaxIdle(pool.getMaxIdle());
        config.setMinIdle(pool.getMinIdle());
        config.setMaxWaitMillis(pool.getMaxWait().toMillis());
        return config;
    }
}

// 配置屬性
@ConfigurationProperties(prefix = "app.cache.redis")
@Data
public class RedisProperties {
    
    private boolean enabled = false;
    private String host = "localhost";
    private int port = 6379;
    private String password;
    private int database = 0;
    private Duration timeout = Duration.ofSeconds(5);
    private Pool pool = new Pool();
    
    @Data
    public static class Pool {
        private int maxActive = 8;
        private int maxIdle = 8;
        private int minIdle = 0;
        private Duration maxWait = Duration.ofMillis(-1);
    }
}

// spring.factories 檔案內容 (src/main/resources/META-INF/spring.factories)
// org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
// com.tutorial.config.RedisAutoConfiguration

10.4.2 條件註解的使用

@Configuration
public class ConditionalConfiguration {
    
    // 當類別存在時創建 Bean
    @Bean
    @ConditionalOnClass(name = "org.springframework.data.redis.core.RedisTemplate")
    public RedisService redisService() {
        return new RedisServiceImpl();
    }
    
    // 當類別不存在時創建 Bean
    @Bean
    @ConditionalOnMissingClass("org.springframework.data.redis.core.RedisTemplate")
    public CacheService memoryCacheService() {
        return new MemoryCacheService();
    }
    
    // 當 Bean 不存在時創建
    @Bean
    @ConditionalOnMissingBean(DataSource.class)
    public DataSource defaultDataSource() {
        return DataSourceBuilder.create()
            .url("jdbc:h2:mem:defaultdb")
            .build();
    }
    
    // 當屬性存在且值匹配時創建
    @Bean
    @ConditionalOnProperty(name = "app.cache.type", havingValue = "redis")
    public CacheManager redisCacheManager() {
        return new RedisCacheManager.Builder(redisConnectionFactory()).build();
    }
    
    // 當 Web 應用程式時創建
    @Bean
    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**").allowedOrigins("*");
            }
        };
    }
    
    // 當特定 Profile 激活時創建
    @Bean
    @Profile("development")
    public DataInitializer developmentDataInitializer() {
        return new DevelopmentDataInitializer();
    }
    
    // 複合條件
    @Bean
    @ConditionalOnProperty(name = "app.features.advanced", havingValue = "true")
    @ConditionalOnClass(AdvancedService.class)
    @ConditionalOnMissingBean(AdvancedService.class)
    public AdvancedService advancedService() {
        return new AdvancedServiceImpl();
    }
}

10.5 Spring Boot Actuator

10.5.1 健康檢查

// 自定義健康檢查
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
    
    private final DataSource dataSource;
    
    public DatabaseHealthIndicator(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    @Override
    public Health health() {
        try (Connection connection = dataSource.getConnection()) {
            if (connection.isValid(1)) {
                return Health.up()
                    .withDetail("database", "MySQL")
                    .withDetail("validationQuery", "SELECT 1")
                    .build();
            } else {
                return Health.down()
                    .withDetail("error", "Database connection validation failed")
                    .build();
            }
        } catch (Exception e) {
            return Health.down()
                .withDetail("error", e.getMessage())
                .build();
        }
    }
}

@Component
public class ExternalServiceHealthIndicator implements HealthIndicator {
    
    private final RestTemplate restTemplate;
    
    public ExternalServiceHealthIndicator(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }
    
    @Override
    public Health health() {
        try {
            ResponseEntity<String> response = restTemplate.getForEntity(
                "https://api.external-service.com/health", String.class);
                
            if (response.getStatusCode().is2xxSuccessful()) {
                return Health.up()
                    .withDetail("externalService", "Available")
                    .withDetail("responseTime", "< 100ms")
                    .build();
            } else {
                return Health.down()
                    .withDetail("externalService", "Unavailable")
                    .withDetail("statusCode", response.getStatusCode())
                    .build();
            }
        } catch (Exception e) {
            return Health.down()
                .withDetail("externalService", "Error")
                .withDetail("error", e.getMessage())
                .build();
        }
    }
}

10.5.2 自定義指標

// 自定義指標
@Component
public class CustomMetrics {
    
    private final MeterRegistry meterRegistry;
    private final Counter userRegistrationCounter;
    private final Timer orderProcessingTimer;
    private final Gauge activeUsersGauge;
    
    public CustomMetrics(MeterRegistry meterRegistry, UserService userService) {
        this.meterRegistry = meterRegistry;
        
        // 計數器
        this.userRegistrationCounter = Counter.builder("user.registrations")
            .description("用戶註冊數量")
            .tag("type", "new")
            .register(meterRegistry);
            
        // 計時器
        this.orderProcessingTimer = Timer.builder("order.processing.time")
            .description("訂單處理時間")
            .register(meterRegistry);
            
        // 量表
        this.activeUsersGauge = Gauge.builder("user.active.count")
            .description("活躍用戶數量")
            .register(meterRegistry, userService, UserService::getActiveUserCount);
    }
    
    public void incrementUserRegistration() {
        userRegistrationCounter.increment();
    }
    
    public void recordOrderProcessingTime(Duration duration) {
        orderProcessingTimer.record(duration);
    }
    
    public Timer.Sample startOrderProcessingTimer() {
        return Timer.start(meterRegistry);
    }
}

// 在服務中使用指標
@Service
public class OrderService {
    
    private final CustomMetrics customMetrics;
    private final OrderRepository orderRepository;
    
    public OrderService(CustomMetrics customMetrics, OrderRepository orderRepository) {
        this.customMetrics = customMetrics;
        this.orderRepository = orderRepository;
    }
    
    public Order processOrder(CreateOrderRequest request) {
        Timer.Sample sample = customMetrics.startOrderProcessingTimer();
        
        try {
            // 處理訂單邏輯
            Order order = new Order(request);
            Order savedOrder = orderRepository.save(order);
            
            // 記錄成功指標
            sample.stop(Timer.builder("order.processing.time")
                .tag("status", "success")
                .register(meterRegistry));
                
            return savedOrder;
        } catch (Exception e) {
            // 記錄失敗指標
            sample.stop(Timer.builder("order.processing.time")
                .tag("status", "error")
                .register(meterRegistry));
            throw e;
        }
    }
}

10.6 認證考點提示

重要考點:

  • 自動配置原理:@EnableAutoConfiguration 的工作機制
  • Starter 依賴:如何創建自定義 Starter
  • 配置屬性:@ConfigurationProperties 的使用
  • 條件註解:各種 @Conditional 註解的應用場景
  • Profile 配置:多環境配置管理
  • Actuator:生產環境監控和管理功能

10.7 實務案例

// 完整的 Spring Boot 應用程式範例
@SpringBootApplication
public class ECommerceApplication {
    
    public static void main(String[] args) {
        System.setProperty("spring.profiles.default", "development");
        SpringApplication app = new SpringApplication(ECommerceApplication.class);
        
        // 添加應用程式監聽器
        app.addListeners(new ApplicationStartupListener());
        
        // 設定額外屬性
        app.setDefaultProperties(Map.of(
            "management.endpoints.web.exposure.include", "health,info,metrics"
        ));
        
        app.run(args);
    }
    
    @Bean
    @Profile("!test")
    public CommandLineRunner dataLoader(UserService userService) {
        return args -> {
            userService.createDefaultUsers();
        };
    }
}

// 啟動監聽器
public class ApplicationStartupListener implements ApplicationListener<ApplicationReadyEvent> {
    
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        ConfigurableApplicationContext context = event.getApplicationContext();
        Environment env = context.getEnvironment();
        
        String port = env.getProperty("server.port", "8080");
        String contextPath = env.getProperty("server.servlet.context-path", "");
        String[] activeProfiles = env.getActiveProfiles();
        
        System.out.println("========================================");
        System.out.println("應用程式啟動成功!");
        System.out.println("Local: http://localhost:" + port + contextPath);
        System.out.println("Profile: " + Arrays.toString(activeProfiles));
        System.out.println("Actuator: http://localhost:" + port + contextPath + "/actuator");
        System.out.println("========================================");
    }
}

// 全局配置
@Configuration
@EnableConfigurationProperties({AppProperties.class, DatabaseProperties.class})
public class GlobalConfig {
    
    @Bean
    @Primary
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
        return mapper;
    }
    
    @Bean
    @ConditionalOnMissingBean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
}

注意事項:

  • 合理使用 @ConditionalOn* 註解避免不必要的 Bean 創建
  • Profile 配置要覆蓋合理的場景
  • Actuator 端點在生產環境要適當限制存取
  • 自動配置類別要提供適當的預設值
  • 配置屬性要進行驗證
  • 注意 Spring Boot 版本兼容性

11. 最佳實務

11.1 Spring 開發最佳實務

11.1.1 依賴注入最佳實務

優先使用建構子注入:

// ✅ 推薦:建構子注入
@Service
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;
    private final ValidationService validationService;
    
    // 單一建構子,Spring 會自動使用
    public UserService(UserRepository userRepository, 
                      EmailService emailService,
                      ValidationService validationService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
        this.validationService = validationService;
    }
}

// ❌ 避免:欄位注入
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository; // 難以測試
    @Autowired
    private EmailService emailService;     // 可能為 null
}

依賴管理原則:

  • 必要依賴使用建構子注入
  • 可選依賴使用 Setter 注入
  • 避免欄位注入,難以進行單元測試
  • 使用 final 關鍵字保證依賴不可變

11.1.2 Bean 配置最佳實務

// ✅ 良好的配置類別
@Configuration
@EnableConfigurationProperties(DatabaseProperties.class)
public class DatabaseConfig {
    
    private final DatabaseProperties properties;
    
    public DatabaseConfig(DatabaseProperties properties) {
        this.properties = properties;
    }
    
    @Bean
    @Primary
    @ConditionalOnProperty(name = "database.type", havingValue = "mysql")
    public DataSource mysqlDataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(properties.getUrl());
        config.setUsername(properties.getUsername());
        config.setPassword(properties.getPassword());
        config.setMaximumPoolSize(properties.getMaxPoolSize());
        config.setMinimumIdle(properties.getMinIdle());
        return new HikariDataSource(config);
    }
    
    @Bean
    @ConditionalOnMissingBean
    public TransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

// ✅ 配置屬性類別
@ConfigurationProperties(prefix = "database")
@Data
@Validated
public class DatabaseProperties {
    
    @NotBlank
    private String url;
    
    @NotBlank
    private String username;
    
    @NotBlank
    private String password;
    
    @Min(1)
    @Max(100)
    private int maxPoolSize = 20;
    
    @Min(0)
    @Max(50)
    private int minIdle = 5;
    
    @Min(1000)
    private long connectionTimeout = 30000;
}

Bean 配置原則:

  • 使用 @ConfigurationProperties 外部化配置
  • 提供合理的預設值
  • 使用條件註解避免不必要的 Bean 創建
  • 配置類別要進行依賴驗證
  • 使用 @Primary 明確主要 Bean
  • 適當使用 @ConditionalOnMissingBean 提供後備方案

11.2 測試最佳實務

11.2.1 單元測試

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @Mock
    private EmailService emailService;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    @DisplayName("成功創建用戶")
    void createUser_Success() {
        // Given
        CreateUserRequest request = new CreateUserRequest();
        request.setUsername("testuser");
        request.setEmail("test@example.com");
        
        User savedUser = new User();
        savedUser.setId(1L);
        savedUser.setUsername("testuser");
        
        when(userRepository.existsByUsername("testuser")).thenReturn(false);
        when(userRepository.save(any(User.class))).thenReturn(savedUser);
        
        // When
        User result = userService.createUser(request);
        
        // Then
        assertThat(result).isNotNull();
        assertThat(result.getId()).isEqualTo(1L);
        verify(emailService).sendWelcomeEmail("test@example.com", "testuser");
    }
    
    @Test
    @DisplayName("創建用戶時用戶名已存在應拋出異常")
    void createUser_UsernameExists_ThrowsException() {
        // Given
        CreateUserRequest request = new CreateUserRequest();
        request.setUsername("existinguser");
        
        when(userRepository.existsByUsername("existinguser")).thenReturn(true);
        
        // When & Then
        assertThatThrownBy(() -> userService.createUser(request))
            .isInstanceOf(UserAlreadyExistsException.class)
            .hasMessage("用戶名 'existinguser' 已存在");
            
        verify(userRepository, never()).save(any(User.class));
        verify(emailService, never()).sendWelcomeEmail(anyString(), anyString());
    }
}

單元測試最佳實務:

  • 使用 @ExtendWith(MockitoExtension.class) 啟用 Mockito
  • 使用 @Mock 創建依賴的模擬物件
  • 使用 @InjectMocks 自動注入模擬依賴
  • 使用 @DisplayName 提供有意義的測試名稱
  • 使用 Given-When-Then 結構組織測試
  • 驗證方法調用次數和參數
  • 測試正常流程和異常情況

11.2.2 整合測試

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(locations = "classpath:test.properties")
@Sql(scripts = "/test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
class UserControllerIntegrationTest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    @DisplayName("API 創建用戶整合測試")
    void createUser_Integration_Success() {
        // Given
        CreateUserRequest request = new CreateUserRequest();
        request.setUsername("newuser");
        request.setEmail("newuser@example.com");
        request.setPassword("password123");
        
        // When
        ResponseEntity<User> response = restTemplate.postForEntity(
            "/api/users", request, User.class);
        
        // Then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody()).isNotNull();
        assertThat(response.getBody().getUsername()).isEqualTo("newuser");
        
        // 驗證資料庫中確實創建了用戶
        Optional<User> savedUser = userRepository.findByUsername("newuser");
        assertThat(savedUser).isPresent();
        assertThat(savedUser.get().getEmail()).isEqualTo("newuser@example.com");
    }
    
    @Test
    @DisplayName("API 獲取用戶列表分頁測試")
    void getUsers_Pagination_Success() {
        // When
        ResponseEntity<PagedModel<User>> response = restTemplate.exchange(
            "/api/users?page=0&size=5&sort=username,asc",
            HttpMethod.GET,
            null,
            new ParameterizedTypeReference<PagedModel<User>>() {}
        );
        
        // Then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        PagedModel<User> pagedUsers = response.getBody();
        assertThat(pagedUsers).isNotNull();
        assertThat(pagedUsers.getContent()).hasSizeLessThanOrEqualTo(5);
    }
}

// Web 層測試
@WebMvcTest(UserController.class)
class UserControllerWebTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @Test
    @DisplayName("Controller 層用戶創建測試")
    void createUser_Controller_Success() throws Exception {
        // Given
        CreateUserRequest request = new CreateUserRequest();
        request.setUsername("testuser");
        request.setEmail("test@example.com");
        
        User createdUser = new User();
        createdUser.setId(1L);
        createdUser.setUsername("testuser");
        createdUser.setEmail("test@example.com");
        
        when(userService.createUser(any(CreateUserRequest.class))).thenReturn(createdUser);
        
        // When & Then
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isCreated())
            .andExpected(jsonPath("$.id").value(1L))
            .andExpected(jsonPath("$.username").value("testuser"))
            .andExpected(jsonPath("$.email").value("test@example.com"));
            
        verify(userService).createUser(any(CreateUserRequest.class));
    }
}

// Repository 層測試
@DataJpaTest
class UserRepositoryTest {
    
    @Autowired
    private TestEntityManager entityManager;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    @DisplayName("根據用戶名查找用戶")
    void findByUsername_Exists_ReturnsUser() {
        // Given
        User user = new User();
        user.setUsername("testuser");
        user.setEmail("test@example.com");
        entityManager.persistAndFlush(user);
        
        // When
        Optional<User> found = userRepository.findByUsername("testuser");
        
        // Then
        assertThat(found).isPresent();
        assertThat(found.get().getUsername()).isEqualTo("testuser");
    }
}

整合測試最佳實務:

  • 使用 @SpringBootTest 進行完整的應用程式測試
  • 使用 @WebMvcTest 只測試 Web 層
  • 使用 @DataJpaTest 只測試 JPA Repository 層
  • 使用 @TestPropertySource 指定測試配置
  • 使用 @Sql 準備和清理測試資料
  • 使用 TestRestTemplate 進行 HTTP 請求測試
  • 分離不同層級的測試關注點

### 11.3 安全性最佳實務

#### 11.3.1 輸入驗證

```java
@Data
@Validated
public class CreateUserRequest {
    
    @NotBlank(message = "用戶名不能為空")
    @Size(min = 3, max = 20, message = "用戶名長度必須在 3-20 字符之間")
    @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用戶名只能包含字母、數字和下劃線")
    private String username;
    
    @NotBlank(message = "密碼不能為空")
    @Size(min = 8, max = 100, message = "密碼長度必須在 8-100 字符之間")
    @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]", 
             message = "密碼必須包含大小寫字母、數字和特殊字符")
    private String password;
    
    @Email(message = "電子郵件格式無效")
    @NotBlank(message = "電子郵件不能為空")
    private String email;
    
    @Pattern(regexp = "^\\+?[1-9]\\d{1,14}$", message = "電話號碼格式無效")
    private String phoneNumber;
}

// 自定義驗證註解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = StrongPasswordValidator.class)
public @interface StrongPassword {
    String message() default "密碼強度不足";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

@Component
public class StrongPasswordValidator implements ConstraintValidator<StrongPassword, String> {
    
    @Override
    public boolean isValid(String password, ConstraintValidatorContext context) {
        if (password == null) return false;
        
        return password.length() >= 8 &&
               password.matches(".*[a-z].*") &&
               password.matches(".*[A-Z].*") &&
               password.matches(".*\\d.*") &&
               password.matches(".*[@$!%*?&].*");
    }
}

輸入驗證最佳實務:

  • 使用 Bean Validation 進行資料驗證
  • 創建自定義驗證註解處理複雜邏輯
  • 在 Controller 層使用 @Valid 或 @Validated
  • 提供有意義的錯誤訊息
  • 驗證所有外部輸入
  • 使用白名單而非黑名單方式驗證
  • 對敏感資料進行額外驗證

11.3.2 認證和授權

// JWT 安全配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
    
    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    
    @Autowired
    private JwtRequestFilter jwtRequestFilter;
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12); // 強度 12
    }
    
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/users").hasRole("USER")
                .requestMatchers(HttpMethod.POST, "/api/users").hasRole("ADMIN")
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .headers(headers -> headers
                .frameOptions().deny()
                .contentTypeOptions(contentTypeOptions -> {})
                .httpStrictTransportSecurity(hstsConfig -> hstsConfig
                    .maxAgeInSeconds(31536000)
                    .includeSubdomains(true)
                )
            );
            
        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
}

// 方法級安全
@Service
public class UserService {
    
    @PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
    public User getUserById(Long userId) {
        return userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException("用戶不存在"));
    }
    
    @PreAuthorize("hasPermission(#user, 'WRITE')")
    public User updateUser(User user) {
        return userRepository.save(user);
    }
    
    @PostAuthorize("returnObject.owner == authentication.name or hasRole('ADMIN')")
    public Document getDocument(Long documentId) {
        return documentRepository.findById(documentId).orElse(null);
    }
}

// 安全配置屬性
@ConfigurationProperties(prefix = "app.security")
@Data
public class SecurityProperties {
    
    private Jwt jwt = new Jwt();
    private Cors cors = new Cors();
    
    @Data
    public static class Jwt {
        private String secret;
        private long expiration = 86400; // 24小時
        private String issuer = "myapp";
    }
    
    @Data
    public static class Cors {
        private String[] allowedOrigins = {"http://localhost:3000"};
        private String[] allowedMethods = {"GET", "POST", "PUT", "DELETE", "OPTIONS"};
        private String[] allowedHeaders = {"*"};
        private boolean allowCredentials = true;
        private long maxAge = 3600;
    }
}

認證和授權最佳實務:

  • 使用強密碼編碼器(BCryptPasswordEncoder)
  • 實施 JWT 令牌過期機制
  • 配置適當的 CORS 策略
  • 使用 HTTPS 傳輸敏感資料
  • 實施方法級安全控制
  • 記錄安全相關事件
  • 定期輪換密鑰
  • 實施速率限制防止暴力攻擊

11.4 效能優化最佳實務

11.4.1 連接池配置

@ConfigurationProperties(prefix = "app.datasource")
@Data
@Validated
public class DataSourceProperties {
    
    @NotBlank
    private String url;
    
    @NotBlank
    private String username;
    
    @NotBlank
    private String password;
    
    @Min(1)
    @Max(100)
    private int maximumPoolSize = 20;
    
    @Min(0)
    @Max(50)
    private int minimumIdle = 5;
    
    @Min(1000)
    private long connectionTimeout = 30000;
    
    @Min(10000)
    private long idleTimeout = 600000; // 10分鐘
    
    @Min(30000)
    private long maxLifetime = 1800000; // 30分鐘
    
    @Min(1000)
    private long leakDetectionThreshold = 60000; // 1分鐘
}

// HikariCP 配置
@Configuration
public class DataSourceConfig {
    
    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource.hikari")
    public HikariDataSource dataSource(DataSourceProperties properties) {
        HikariConfig config = new HikariConfig();
        
        // 基本連接資訊
        config.setJdbcUrl(properties.getUrl());
        config.setUsername(properties.getUsername());
        config.setPassword(properties.getPassword());
        
        // 連接池配置
        config.setMaximumPoolSize(properties.getMaximumPoolSize());
        config.setMinimumIdle(properties.getMinimumIdle());
        config.setConnectionTimeout(properties.getConnectionTimeout());
        config.setIdleTimeout(properties.getIdleTimeout());
        config.setMaxLifetime(properties.getMaxLifetime());
        config.setLeakDetectionThreshold(properties.getLeakDetectionThreshold());
        
        // 連接驗證
        config.setConnectionTestQuery("SELECT 1");
        config.setValidationTimeout(5000);
        
        // 其他配置
        config.setPoolName("HikariCP-Pool");
        config.setAutoCommit(true);
        config.setConnectionInitSql("SET NAMES utf8mb4");
        
        return new HikariDataSource(config);
    }
}

連接池配置最佳實務:

  • 根據應用程式負載調整連接池大小
  • 設置適當的連接超時時間
  • 啟用連接洩漏檢測
  • 配置連接驗證查詢
  • 監控連接池指標
  • 在高併發環境下適當增加最大連接數
  • 設置合理的空閒連接超時時間

11.4.2 快取策略

// Spring Cache 配置
@Configuration
@EnableCaching
public class CacheConfig {
    
    @Bean
    @Primary
    public CacheManager cacheManager() {
        RedisCacheManager.Builder builder = RedisCacheManager
            .RedisCacheManagerBuilder
            .fromConnectionFactory(redisConnectionFactory())
            .cacheDefaults(cacheConfiguration());
            
        return builder.build();
    }
    
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        LettuceConnectionFactory factory = new LettuceConnectionFactory(
            new RedisStandaloneConfiguration("localhost", 6379));
        factory.setValidateConnection(true);
        return factory;
    }
    
    private RedisCacheConfiguration cacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10))
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()))
            .disableCachingNullValues();
    }
}

// 快取使用示例
@Service
public class UserService {
    
    @Cacheable(value = "users", key = "#id", condition = "#id != null")
    public User getUserById(Long id) {
        log.info("從資料庫查詢用戶: {}", id);
        return userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException("用戶不存在"));
    }
    
    @Cacheable(value = "users", key = "#username", unless = "#result == null")
    public User getUserByUsername(String username) {
        log.info("從資料庫查詢用戶: {}", username);
        return userRepository.findByUsername(username).orElse(null);
    }
    
    @CacheEvict(value = "users", key = "#user.id")
    public User updateUser(User user) {
        log.info("更新用戶並清除快取: {}", user.getId());
        return userRepository.save(user);
    }
    
    @CacheEvict(value = "users", allEntries = true)
    public void deleteUser(Long id) {
        log.info("刪除用戶並清除所有用戶快取: {}", id);
        userRepository.deleteById(id);
    }
    
    @CachePut(value = "users", key = "#result.id")
    public User createUser(CreateUserRequest request) {
        log.info("創建用戶並更新快取");
        User user = new User();
        // 設置用戶屬性
        return userRepository.save(user);
    }
}

// 自定義快取配置
@Component
public class CustomCacheConfig {
    
    @Bean
    public CacheManager customCacheManager() {
        ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
        cacheManager.setCacheNames(Arrays.asList("users", "products", "orders"));
        return cacheManager;
    }
    
    // 快取鍵生成器
    @Bean
    public KeyGenerator customKeyGenerator() {
        return (target, method, params) -> {
            StringBuilder key = new StringBuilder();
            key.append(target.getClass().getSimpleName()).append(":");
            key.append(method.getName()).append(":");
            for (Object param : params) {
                key.append(param.toString()).append(",");
            }
            return key.toString();
        };
    }
}

// 分層快取策略
@Service
public class ProductService {
    
    private final Map<String, Object> localCache = new ConcurrentHashMap<>();
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    public Product getProduct(Long id) {
        String key = "product:" + id;
        
        // L1: 本地快取
        Product product = (Product) localCache.get(key);
        if (product != null) {
            return product;
        }
        
        // L2: Redis 快取
        product = (Product) redisTemplate.opsForValue().get(key);
        if (product != null) {
            localCache.put(key, product);
            return product;
        }
        
        // L3: 資料庫
        product = productRepository.findById(id).orElse(null);
        if (product != null) {
            redisTemplate.opsForValue().set(key, product, Duration.ofMinutes(30));
            localCache.put(key, product);
        }
        
        return product;
    }
}

快取策略最佳實務:

  • 選擇合適的快取類型(本地、分布式)
  • 設置適當的 TTL(生存時間)
  • 使用分層快取提高效能
  • 實施快取預熱策略
  • 處理快取穿透、快取雪崩問題
  • 監控快取命中率
  • 合理使用快取更新策略
  • 避免快取大物件導致記憶體問題

11.5 認證考點提示

最佳實務相關考點:

  • 依賴注入:建構子注入 vs Setter 注入的選擇
  • 配置管理:@ConfigurationProperties 的使用
  • 異常處理:@ControllerAdvice 全局異常處理
  • 事務管理:@Transactional 的正確使用
  • 測試策略:單元測試與整合測試的區別
  • 效能優化:連接池配置、快取策略

12. 認證考試重點

12.1 Spring 專業認證概述

Spring Professional Certification 是 VMware(原 Pivotal)提供的官方認證,驗證開發者對 Spring Framework 和 Spring Boot 的深度理解。

認證覆蓋領域:

  • Spring Framework 核心概念
  • IoC 容器和依賴注入
  • AOP (面向切面程式設計)
  • Spring MVC Web 開發
  • 資料存取和事務管理
  • Spring Security
  • Spring Boot
  • 測試

12.2 核心概念考點

12.2.1 IoC 和依賴注入

重點知識:

// 1. 依賴注入類型的比較
public class ServiceComparison {
    
    // 建構子注入 - 推薦方式
    private final UserRepository userRepository;
    private final EmailService emailService;
    
    public ServiceComparison(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository; // 必需依賴,不可變
        this.emailService = emailService;
    }
    
    // Setter 注入 - 用於可選依賴
    private NotificationService notificationService;
    
    @Autowired(required = false)
    public void setNotificationService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }
}

常見考題:

  1. Q: 建構子注入相比欄位注入有什麼優點?

    • A:
      • 確保依賴的不可變性(final 欄位)
      • 更容易進行單元測試
      • 明確表達必需的依賴
      • 避免循環依賴問題
      • 失敗快速原則
  2. Q: @Autowired、@Resource 和 @Inject 的區別?

    • A:
      • @Autowired:Spring 註解,按類型注入,可配合 @Qualifier
      • @Resource:Java 標準註解,按名稱注入
      • @Inject:Java 標準註解,按類型注入,需要額外依賴

12.2.2 Bean 的生命週期

@Component
public class LifecycleBean implements InitializingBean, DisposableBean {
    
    @PostConstruct
    public void postConstruct() {
        System.out.println("@PostConstruct");
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("InitializingBean.afterPropertiesSet()");
    }
    
    @PreDestroy
    public void preDestroy() {
        System.out.println("@PreDestroy");
    }
    
    @Override
    public void destroy() throws Exception {
        System.out.println("DisposableBean.destroy()");
    }
}

執行順序:

  1. 建構子
  2. 依賴注入
  3. @PostConstruct
  4. InitializingBean.afterPropertiesSet()
  5. 自定義 init-method
  6. Bean 可用
  7. @PreDestroy
  8. DisposableBean.destroy()
  9. 自定義 destroy-method

12.2.3 Bean 的作用域

// Singleton(預設)
@Component
@Scope("singleton")
public class SingletonBean { }

// Prototype
@Component
@Scope("prototype")
public class PrototypeBean { }

// Web 作用域
@Component
@Scope("request")
public class RequestScopedBean { }

@Component
@Scope("session")
public class SessionScopedBean { }

12.3 AOP 考點

12.3.1 AOP 概念與術語

重要術語:

  • Aspect: 切面,橫切關注點的模組化
  • Join Point: 連接點,程式執行中的特定點
  • Pointcut: 切點,定義在哪些 Join Point 應用通知
  • Advice: 通知,在特定 Join Point 執行的動作
  • Weaving: 織入,將切面應用到目標物件的過程

12.3.2 通知類型

@Aspect
@Component
public class LoggingAspect {
    
    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("Before: " + joinPoint.getSignature().getName());
    }
    
    @After("execution(* com.example.service.*.*(..))")
    public void logAfter(JoinPoint joinPoint) {
        System.out.println("After: " + joinPoint.getSignature().getName());
    }
    
    @AfterReturning(value = "execution(* com.example.service.*.*(..))", returning = "result")
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
        System.out.println("AfterReturning: " + result);
    }
    
    @AfterThrowing(value = "execution(* com.example.service.*.*(..))", throwing = "exception")
    public void logAfterThrowing(JoinPoint joinPoint, Exception exception) {
        System.out.println("AfterThrowing: " + exception.getMessage());
    }
    
    @Around("execution(* com.example.service.*.*(..))")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();
        System.out.println("執行時間: " + (end - start) + "ms");
        return result;
    }
}

12.4 Spring MVC 考點

12.4.1 MVC 架構

@Controller
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping("/{id}")
    @ResponseBody
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        User user = userService.findById(id);
        return ResponseEntity.ok(user);
    }
    
    @PostMapping
    @ResponseBody
    public ResponseEntity<User> createUser(@RequestBody @Valid CreateUserRequest request) {
        User user = userService.create(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }
}

12.4.2 請求處理流程

  1. DispatcherServlet 接收請求
  2. HandlerMapping 查找處理器
  3. HandlerAdapter 調用處理器
  4. ViewResolver 解析視圖
  5. 渲染響應

12.5 事務管理考點

12.5.1 事務傳播行為

@Service
@Transactional
public class TransactionService {
    
    @Transactional(propagation = Propagation.REQUIRED)
    public void required() { }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void requiresNew() { }
    
    @Transactional(propagation = Propagation.NESTED)
    public void nested() { }
    
    @Transactional(propagation = Propagation.SUPPORTS)
    public void supports() { }
    
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void notSupported() { }
    
    @Transactional(propagation = Propagation.MANDATORY)
    public void mandatory() { }
    
    @Transactional(propagation = Propagation.NEVER)
    public void never() { }
}

12.5.2 事務隔離級別

@Transactional(isolation = Isolation.READ_UNCOMMITTED)  // 讀未提交
@Transactional(isolation = Isolation.READ_COMMITTED)    // 讀已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)   // 可重複讀
@Transactional(isolation = Isolation.SERIALIZABLE)     // 串列化

12.3 Spring Boot 考點

12.3.1 自動配置

條件註解:

@Configuration
@ConditionalOnClass(DataSource.class)
@ConditionalOnProperty(prefix = "app.datasource", name = "enabled", havingValue = "true")
@EnableConfigurationProperties(DataSourceProperties.class)
public class CustomDataSourceAutoConfiguration {
    
    @Bean
    @ConditionalOnMissingBean
    @Primary
    public DataSource dataSource(DataSourceProperties properties) {
        return DataSourceBuilder
            .create()
            .url(properties.getUrl())
            .username(properties.getUsername())
            .password(properties.getPassword())
            .build();
    }
}

常見考題:

  1. Q: @SpringBootApplication 等於哪三個註解的組合?

    • A: @Configuration + @EnableAutoConfiguration + @ComponentScan
  2. Q: 如何排除特定的自動配置?

    • A: 使用 exclude 屬性:@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

12.7 測試考點

12.7.1 Spring 測試註解

// 整合測試
@SpringBootTest
@TestPropertySource(locations = "classpath:test.properties")
class IntegrationTest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Test
    void testEndpoint() {
        ResponseEntity<String> response = restTemplate.getForEntity("/test", String.class);
        assertEquals(HttpStatus.OK, response.getStatusCode());
    }
}

// 單元測試
@ExtendWith(MockitoExtension.class)
class UnitTest {
    
    @Mock
    private Repository repository;
    
    @InjectMocks
    private Service service;
    
    @Test
    void testService() {
        when(repository.findById(1L)).thenReturn(Optional.of(new Entity()));
        Entity result = service.getById(1L);
        assertNotNull(result);
    }
}

// Web 層測試
@WebMvcTest(UserController.class)
class ControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @Test
    void testGetUser() throws Exception {
        when(userService.findById(1L)).thenReturn(new User());
        
        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk())
            .andExpected(jsonPath("$.id").value(1));
    }
}

12.8 常見考試陷阱

12.8.1 循環依賴

// ❌ 循環依賴問題
@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
}

@Service  
public class ServiceB {
    @Autowired
    private ServiceA serviceA;
}

// ✅ 解決方案:使用 @Lazy
@Service
public class ServiceA {
    @Autowired
    @Lazy
    private ServiceB serviceB;
}

12.8.2 代理限制

// ❌ 自調用不會觸發 AOP
@Service
public class UserService {
    
    @Transactional
    public void publicMethod() {
        privateMethod(); // 不會開啟事務
    }
    
    @Transactional
    private void privateMethod() {
        // 這個事務不會生效
    }
}

// ✅ 正確做法
@Service
public class UserService {
    
    @Autowired
    private UserService self; // 注入代理物件
    
    public void publicMethod() {
        self.privateMethod(); // 會觸發事務
    }
    
    @Transactional
    public void privateMethod() {
        // 事務生效
    }
}

12.9 準備策略

學習建議:

  1. 熟悉 Spring 官方文檔:特別是核心容器和 Web MVC 部分
  2. 動手實作:建立完整的 Spring Boot 專案
  3. 理解原理:不只會用,更要知道為什麼
  4. 練習測試:掌握各種測試註解和技巧
  5. 關注最佳實務:了解常見的反模式和解決方案

考試技巧:

  • 仔細閱讀題目,注意關鍵字
  • 排除明顯錯誤的選項
  • 如果不確定,選擇最符合 Spring 設計理念的答案
  • 注意版本差異,考試通常基於 Spring 5.x 和 Spring Boot 2.x

13. 練習題與答案

13.1 IoC 容器與依賴注入

13.1.1 基礎題

題目 1: 以下哪種依賴注入方式是 Spring 官方推薦的最佳實務?

A) 欄位注入 (Field Injection) B) Setter 注入 (Setter Injection)
C) 建構子注入 (Constructor Injection) D) 介面注入 (Interface Injection)

答案:C

詳解: 建構子注入是推薦的方式,因為:

  • 確保依賴的不可變性(可使用 final 關鍵字)
  • 更容易進行單元測試
  • 明確表達必需的依賴關係
  • 避免循環依賴問題
  • 遵循失敗快速原則

正確答案:A

13.2 AOP 相關練習

練習題 4

以下 AOP 通知類型中,哪一個可以修改方法的返回值?

A) @Before
B) @After
C) @AfterReturning
D) @Around

正確答案:D

解釋: 只有 @Around 通知可以完全控制方法的執行,包括修改參數、返回值或完全跳過方法執行。

練習題 5

以下切點表達式的含義是什麼?

execution(* com.example.service.*.*(..))

A) 匹配 com.example.service 包下所有類別的所有方法
B) 匹配 com.example.service 包及其子包下所有類別的所有方法
C) 匹配 com.example.service 包下所有以 service 結尾的類別的所有方法
D) 匹配 com.example 包下所有類別的 service 方法

正確答案:A

解釋:

  • 第一個 * 表示任意返回類型
  • com.example.service.* 表示該包下的類別(不包含子包)
  • 第二個 * 表示任意方法名
  • (..) 表示任意參數

13.3 Spring MVC 練習

練習題 6

在 Spring MVC 中,@RequestMapping 註解可以用在哪些地方?

A) 只能用於方法
B) 只能用於類別
C) 可以用於類別和方法
D) 可以用於類別、方法和參數

正確答案:C

解釋: @RequestMapping 可以用於類別(定義基礎路徑)和方法(定義具體的請求映射)。

練習題 7

以下哪個註解用於綁定請求路徑中的變數?

A) @RequestParam
B) @PathVariable
C) @RequestHeader
D) @ModelAttribute

正確答案:B

解釋: @PathVariable 用於綁定 URL 路徑中的變數,如 /users/{id} 中的 {id}。

13.4 事務管理練習

練習題 8

以下關於 @Transactional 註解的說法哪個是錯誤的?

A) 可以用於類別和方法
B) 預設只對 RuntimeException 進行回滾
C) 可以指定事務的隔離級別
D) 私有方法上的 @Transactional 註解會生效

正確答案:D

解釋: 由於 Spring 使用代理機制實現事務,私有方法上的 @Transactional 註解不會生效。

練習題 9

事務傳播行為 PROPAGATION_REQUIRES_NEW 的含義是什麼?

A) 如果存在事務則加入,否則創建新事務
B) 總是創建新事務,如果存在事務則暫停
C) 必須在已存在的事務中執行
D) 從不在事務中執行

正確答案:B

13.5 Spring Boot 練習

練習題 10

@SpringBootApplication 註解等效於以下哪些註解的組合?

A) @Configuration + @EnableAutoConfiguration + @ComponentScan
B) @Component + @EnableAutoConfiguration + @ComponentScan
C) @Configuration + @AutoConfigurationPackage + @ComponentScan
D) @Service + @EnableAutoConfiguration + @ComponentScan

正確答案:A

練習題 11

以下哪個是 Spring Boot 自動配置的條件註解?

A) @ConditionalOnBean
B) @ConditionalOnClass
C) @ConditionalOnProperty
D) 以上都是

正確答案:D

13.6 測試相關練習

練習題 12

@WebMvcTest 註解主要用於測試什麼?

A) 完整的 Spring Boot 應用程式
B) JPA Repository 層
C) Web 控制器層
D) Service 業務邏輯層

正確答案:C

練習題 13

在 Spring 測試中,@MockBean 和 @Mock 的區別是什麼?

A) 沒有區別
B) @MockBean 會加入 Spring 上下文,@Mock 不會
C) @Mock 會加入 Spring 上下文,@MockBean 不會
D) @MockBean 只能用於 Spring Boot,@Mock 只能用於單元測試

正確答案:B

13.7 綜合練習

練習題 14

以下代碼中,如何解決循環依賴問題?

@Service
public class UserService {
    @Autowired
    private OrderService orderService;
}

@Service
public class OrderService {
    @Autowired
    private UserService userService;
}

A) 使用 @Lazy 註解
B) 使用 Setter 注入
C) 重新設計類別結構
D) 以上都可以

正確答案:D

練習題 15

在生產環境中,以下哪個配置是推薦的資料庫連接池配置?

A) 最大連接數設為 1000
B) 最小連接數設為 0
C) 根據應用程式負載合理配置最大和最小連接數
D) 不限制連接數

正確答案:C

13.8 答案解析總結

重要知識點回顧:

  1. 依賴注入:優先使用建構子注入
  2. AOP:理解各種通知類型的執行時機和用途
  3. Spring MVC:掌握請求處理流程和常用註解
  4. 事務管理:了解傳播行為和隔離級別
  5. Spring Boot:理解自動配置原理和條件註解
  6. 測試:區分不同測試註解的用途和範圍
  7. 最佳實務:避免常見的反模式和陷阱

14. 檢查清單

14.1 學習進度檢查清單

14.1.1 Spring Framework 核心 ✓

IoC 容器與依賴注入

  • 理解 IoC 和 DI 的概念和區別
  • 掌握三種依賴注入方式的優缺點
  • 熟悉 Bean 的生命週期和回調方法
  • 了解 ApplicationContext 的功能和層級結構
  • 掌握 Bean 的作用域 (singleton, prototype, request, session)

14.2 總結

本教學手冊全面覆蓋了 Spring Framework 和 Spring Boot 的核心概念、實用技巧和最佳實務。通過系統性的學習和實踐,您將能夠:

核心能力

  • 深入理解 Spring 的設計理念和架構原則
  • 熟練運用 IoC 容器和依賴注入進行鬆散耦合的設計
  • 掌握 AOP 程式設計解決橫切關注點
  • 建構健壯的 Web 應用程式和 REST API
  • 實現完整的安全認證和授權機制
  • 進行有效的資料存取和事務管理

希望這份教學手冊能成為您 Spring 學習旅程中的有力助手。持續學習,不斷實踐,您將成為優秀的 Spring 開發者!

14.1.2 AOP 面向切面程式設計

  • 理解 AOP 的基本概念和術語
  • 掌握各種通知類型的使用場景
  • 熟練使用切點表達式
  • 了解 AOP 的實際應用場景
  • 理解代理機制的工作原理

14.1.3 Spring MVC Web 開發

  • 理解 MVC 架構模式
  • 掌握 Controller 的開發方式
  • 熟悉請求處理流程
  • 能夠設計 RESTful API
  • 掌握異常處理機制

14.1.4 資料存取與事務管理

  • 熟悉 JDBC Template 的使用
  • 理解 JPA 與 Spring 的整合
  • 掌握事務管理的配置和使用
  • 了解事務傳播行為和隔離級別
  • 能夠配置資料庫連接池

14.1.5 Spring Security

  • 理解 Spring Security 的架構
  • 掌握認證和授權的配置
  • 熟悉 JWT 的整合使用
  • 了解 CORS 的配置
  • 掌握安全最佳實務

14.1.6 Spring Boot

  • 理解 Spring Boot 的設計理念
  • 掌握自動配置的原理
  • 熟悉 Starter 依賴的使用
  • 了解配置外部化
  • 掌握 Actuator 監控功能

14.2 程式碼品質檢查清單

14.2.1 依賴注入

  • 優先使用建構子注入而非欄位注入
  • 使用 final 關鍵字保證依賴不可變
  • 避免循環依賴
  • 合理使用 @Qualifier 處理多個候選者
  • 適當使用 @Lazy 延遲初始化

14.2.2 配置管理

  • 使用 @ConfigurationProperties 外部化配置
  • 提供合理的預設值
  • 使用 Profile 區分不同環境
  • 敏感資訊不要硬編碼
  • 配置驗證是否完整

14.2.3 異常處理

  • 實現全局異常處理器
  • 提供有意義的錯誤訊息
  • 適當的日誌記錄
  • 不要忽略異常
  • 區分業務異常和系統異常

14.2.4 測試覆蓋

  • 編寫單元測試
  • 編寫整合測試
  • 測試覆蓋率達到要求
  • 使用適當的測試註解
  • Mock 外部依賴

14.2.5 效能考量

  • 合理配置連接池
  • 適當使用快取
  • 避免 N+1 查詢問題
  • 使用分頁處理大量資料
  • 監控關鍵指標

14.3 部署前檢查清單

14.3.1 環境配置

  • 生產環境配置檔案正確
  • 資料庫連接配置正確
  • 日誌級別設定適當
  • JVM 記憶體配置合理
  • 外部依賴服務可用

14.3.2 安全性檢查

  • 移除或禁用調試端點
  • 設定適當的 CORS 策略
  • 驗證 HTTPS 配置
  • 檢查密碼編碼方式
  • 確認權限控制正確

14.3.3 監控與日誌

  • 啟用 Actuator 健康檢查
  • 配置應用程式指標
  • 設定日誌輪轉策略
  • 配置告警規則
  • 準備故障排除文檔

14.4 認證考試檢查清單

14.4.1 理論知識

  • Spring Framework 的核心概念
  • IoC 容器的工作原理
  • AOP 的實現機制
  • Spring MVC 的請求處理流程
  • 事務管理的原理
  • Spring Boot 的自動配置原理

14.4.2 實務技能

  • 能夠從零建立 Spring 專案
  • 熟練配置各種 Bean
  • 能夠實現 AOP 切面
  • 熟練開發 RESTful API
  • 能夠整合資料庫
  • 熟練使用 Spring Boot

14.4.3 除錯技能

  • 能夠分析 Spring 啟動問題
  • 理解常見的配置錯誤
  • 能夠解決循環依賴問題
  • 熟悉事務失效的原因
  • 能夠診斷效能問題

14.5 持續學習檢查清單

14.5.1 進階主題

  • Spring Cloud 微服務開發
  • Spring WebFlux 響應式程式設計
  • Spring Native 原生映像
  • Spring Integration 企業整合
  • Spring Batch 批次處理

14.5.2 生態系統

  • 了解 Spring 生態系統的最新發展
  • 關注 Spring 官方部落格和文檔
  • 參與社群討論和開源專案
  • 學習相關技術棧(如 Docker、Kubernetes)
  • 持續關注最佳實務的發展

14.6 結語

通過本教學手冊的學習,您應該已經掌握了 Spring Framework 的核心知識和實務技能。Spring 是一個龐大且不斷發展的生態系統,持續學習和實踐是保持技能更新的關鍵。

學習建議:

  • 多動手實作,理論結合實務
  • 閱讀 Spring 官方文檔和原始碼
  • 參與開源專案,貢獻社群
  • 關注行業趨勢,學習新技術
  • 建立個人技術部落格,分享經驗

祝您在 Spring 開發的道路上越走越遠,成為優秀的 Spring 開發者!

檢查清單使用方法

  1. 開發前:確保基礎知識牢固
  2. 開發中:遵循最佳實務
  3. 部署前:確保生產就緒
  4. 考試前:檢驗知識完整性
  5. 學習中:指引學習方向

希望這份檢查清單能夠幫助您系統性地掌握 Spring Framework,並在實際專案開發和認證考試中取得成功。記住,持續的學習和實踐是成為 Spring 專家的必經之路。