簡介:本項目是一個典型的企業級Java Web應用——學生選課系統,採用SSH(Hibernate+Spring+Struts)三大主流開源框架集成開發,實現系統的高效性、穩定性與可維護性。系統涵蓋用户登錄、課程瀏覽、在線選課、成績管理等核心功能模塊,通過MVC架構模式和分層設計,實現表現層、業務邏輯層與數據訪問層的解耦。項目提供完整源碼,適合學習Java EE開發、框架整合及企業級應用構建,幫助開發者掌握ORM映射、依賴注入、事務管理、請求控制等關鍵技術,是深入理解Java Web開發流程與SSH框架協同工作的優質實踐案例。

基於Java(Spring+Struts+Hibernate 框架)實現(Web)學生課程管理系統【100010038】_xml

1. SSH框架整合原理與項目結構搭建

1.1 SSH架構核心思想與整合機制

SSH(Struts+Spring+Hibernate)通過分層解耦實現高內聚、低耦合的Web應用架構。其中, Struts 負責MVC控制流程, Hibernate 提供ORM持久化支持,而 Spring 作為核心容器,承擔Bean管理、依賴注入(DI)與聲明式事務控制的“粘合劑”角色。

Spring通過 ApplicationContext 統一管理Struts的Action和Hibernate的SessionFactory,利用AOP實現事務織入,替代傳統EJB的複雜性,顯著提升開發效率與系統可維護性。

<!-- applicationContext.xml 片段:整合Hibernate會話工廠 -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
    <property name="configLocation" value="classpath:hibernate.cfg.xml"/>
</bean>

上述配置由Spring託管Hibernate會話工廠,為後續DAO注入提供基礎。

1.2 基於Maven的項目結構設計

採用標準Maven多模塊結構,清晰劃分職責:

目錄

用途

src/main/java

Java源碼(Action/Service/DAO/Entity)

src/main/resources

配置文件(struts.xml, hibernate.cfg.xml, applicationContext.xml)

src/main/webapp/WEB-INF

Web資源與web.xml部署描述符

使用 pom.xml 統一管理SSH依賴版本,避免衝突:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>5.3.21</version>
</dependency>

1.3 開發環境搭建流程

  1. 安裝JDK 8+ 並配置 JAVA_HOME
  2. 部署Tomcat 9.x 作為Servlet容器
  3. 安裝MySQL並創建 student_system 數據庫
  4. 在IDE(如IntelliJ IDEA或Eclipse)中導入Maven項目,配置Facet為Web Module
  5. target/classes 輸出至 WEB-INF/classes ,確保配置文件加載路徑正確

最終形成如下工程結構:

student-selection/
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/
│   │   ├── resources/
│   │   └── webapp/
│   │       └── WEB-INF/web.xml
└── target/student-selection.war

該結構為後續Hibernate映射與Struts請求處理奠定工程基礎。

2. Hibernate實現ORM映射與數據庫操作

Hibernate作為Java領域最成熟的對象關係映射(ORM)框架之一,其核心目標是將面向對象的編程模型與關係型數據庫的數據結構進行無縫銜接。在學生選課系統中,實體如“學生”、“課程”、“成績”等天然適合以類的形式建模,而Hibernate通過元數據配置或註解的方式,自動完成這些類與數據庫表之間的映射,極大提升了開發效率並降低了SQL硬編碼帶來的維護成本。

本章將從Hibernate的核心機制出發,深入剖析持久化類的設計規範、會話管理策略、CRUD操作流程以及複雜關聯關係的建模方法。重點在於理解 對象狀態生命週期 緩存機制對性能的影響 HQL查詢語言的優勢與侷限性 ,並通過具體的學生-課程多對多關係實戰案例,展示如何在真實業務場景中合理使用級聯、懶加載、分頁查詢等關鍵技術點。

2.1 Hibernate核心機制與持久化類設計

Hibernate的工作原理建立在JDBC之上,但它屏蔽了底層數據庫訪問的複雜性,開發者無需手動編寫大量重複的SQL語句即可完成數據持久化。它通過一個 持久化上下文(Persistence Context) 管理實體對象的狀態轉換,並利用 Session接口 作為與數據庫交互的主要入口。整個過程由 SessionFactory 工廠創建,該工廠在整個應用生命週期內通常只初始化一次,保證線程安全和資源高效複用。

持久化類(Persistent Class)是Hibernate映射的基礎單元,必須遵循一定的編碼規範才能被正確識別和管理。例如,每個類應具備無參構造函數、提供getter/setter方法、避免final修飾符等。更重要的是,需要通過註解或XML配置明確指定類與表的對應關係。

2.1.1 ORM概念解析與Hibernate工作原理

ORM(Object-Relational Mapping)即對象-關係映射,是一種將程序中的對象與數據庫中的記錄相互轉換的技術。傳統JDBC開發中,開發者需手動編寫SQL查詢並將結果集逐字段映射到Java對象,這一過程繁瑣且容易出錯。而Hibernate通過反射機制和元數據描述,實現了全自動化的雙向映射。

Hibernate採用 第一方緩存(一級緩存) 事務邊界控制 來優化性能。當執行 session.get(Student.class, 1) 時,Hibernate首先檢查一級緩存是否存在ID為1的Student實例;若存在則直接返回,避免重複查詢。只有在緩存未命中時才會發送SQL到數據庫。此外,Hibernate還支持延遲加載(Lazy Loading),僅在真正訪問關聯對象時才觸發查詢,減少不必要的數據加載。

以下是Hibernate整體工作流程的Mermaid流程圖:

graph TD
    A[應用程序調用Session API] --> B{對象是否已存在緩存?}
    B -- 是 --> C[返回緩存對象]
    B -- 否 --> D[生成SQL語句]
    D --> E[通過JDBC連接執行SQL]
    E --> F[獲取ResultSet結果集]
    F --> G[使用反射構建Java對象]
    G --> H[放入一級緩存]
    H --> I[返回對象給應用]

此流程體現了Hibernate“透明持久化”的設計理念:開發者只需關注對象的操作,Hibernate負責背後的所有數據庫交互細節。

2.1.2 實體類編寫規範與註解驅動映射(@Entity, @Table, @Id)

在現代Hibernate開發中,普遍採用註解方式進行映射定義。以下是一個典型的學生實體類示例:

@Entity
@Table(name = "t_student")
public class Student implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "stu_name", nullable = false, length = 50)
    private String name;

    @Column(name = "age")
    private Integer age;

    @Column(name = "gender")
    private String gender;

    // 必須提供無參構造函數
    public Student() {}

    // Getter and Setter methods...
}
參數説明與邏輯分析:

註解

作用

@Entity

標識該類為持久化實體,Hibernate會將其映射到一張數據庫表

@Table(name="t_student")

指定對應的數據庫表名;若不指定,默認使用類名

@Id

標記主鍵字段

@GeneratedValue(strategy = ...)

定義主鍵生成策略,此處使用自增(適用於MySQL)

@Column(...)

映射屬性到具體列,可設置列名、是否允許為空、長度等

關鍵點説明

  • 所有持久化類建議實現 Serializable 接口,便於分佈式環境下的序列化傳輸。
  • 屬性應儘量使用包裝類型(如 Integer 而非 int ),以便表示 null 值。
  • 避免將集合屬性聲明為 final ,否則Hibernate無法動態代理初始化懶加載集合。

Hibernate在啓動時會掃描所有帶有 @Entity 註解的類,並根據 @Column 等元數據構建映射元模型(Metadata),用於後續的SQL生成和對象填充。

2.1.3 主鍵生成策略選擇與關係映射建模(一對一、一對多、多對多)

主鍵生成策略直接影響數據插入行為和數據庫兼容性。常見的策略包括:

策略

描述

適用場景

IDENTITY

使用數據庫自增字段(如 MySQL AUTO_INCREMENT)

單機部署,MySQL/SQL Server

SEQUENCE

利用數據庫序列(Oracle、PostgreSQL)

Oracle、PG等支持序列的數據庫

TABLE

使用專用表模擬序列(跨數據庫兼容)

需要高度可移植性的項目

AUTO

自動選擇合適的策略

開發階段快速上手

對於複雜的關係建模,Hibernate提供了強大的註解支持:

一對多關係示例:班級 ↔ 學生
@Entity
public class Grade {
    @Id
    private Long id;
    private String gradeName;

    @OneToMany(mappedBy = "grade", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<Student> students = new HashSet<>();
}

@Entity
public class Student {
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "grade_id")
    private Grade grade;
}
  • mappedBy 表示由 Student.grade 維護外鍵關係。
  • cascade = CascadeType.ALL 表示父實體操作時級聯影響子實體。
  • fetch = FetchType.LAZY 表示默認延遲加載學生列表。
多對多關係:學生 ↔ 課程(中間表)

將在 2.4 節詳細展開。

2.2 映射文件配置與會話工廠管理

Hibernate的運行依賴於正確的配置信息,主要包括數據庫連接參數、方言設置、緩存配置等。雖然現代開發更多使用註解+Java配置方式,但傳統的 hibernate.cfg.xml 仍廣泛應用於SSH整合項目中。

2.2.1 hibernate.cfg.xml配置詳解(連接池、方言、DDL導出)

以下是典型的 hibernate.cfg.xml 配置文件內容:

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
        <!-- 數據庫連接信息 -->
        <property name="connection.driver_class">com.mysql.cj.jdbc.Driver</property>
        <property name="connection.url">jdbc:mysql://localhost:3306/school_db?useSSL=false&serverTimezone=UTC</property>
        <property name="connection.username">root</property>
        <property name="connection.password">password</property>

        <!-- 方言設置 -->
        <property name="dialect">org.hibernate.dialect.MySQL8Dialect</property>

        <!-- 連接池配置(使用C3P0) -->
        <property name="c3p0.min_size">5</property>
        <property name="c3p0.max_size">20</property>
        <property name="c3p0.timeout">300</property>
        <property name="c3p0.max_statements">50</property>

        <!-- DDL生成策略 -->
        <property name="hbm2ddl.auto">update</property>

        <!-- 顯示SQL -->
        <property name="show_sql">true</property>
        <property name="format_sql">true</property>

        <!-- 啓用二級緩存 -->
        <property name="cache.use_second_level_cache">true</property>
        <property name="cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>

        <!-- 實體類註冊 -->
        <mapping class="com.example.entity.Student"/>
        <mapping class="com.example.entity.Course"/>
        <mapping class="com.example.entity.Grade"/>
    </session-factory>
</hibernate-configuration>
參數説明:

屬性

説明

dialect

指定數據庫方言,使Hibernate能生成符合特定數據庫語法的SQL

c3p0.*

配置C3P0連接池參數,提升併發訪問能力

hbm2ddl.auto

取值包括 create , update , validate , none ;生產環境建議設為 validate none

show_sql / format_sql

調試時啓用,便於查看生成的SQL語句

cache.*

開啓二級緩存支持,需引入Ehcache依賴

注意 hbm2ddl.auto=update 在開發階段非常有用,可以自動同步表結構變更,但在生產環境中極易導致數據丟失或結構混亂,嚴禁開啓。

2.2.2 SessionFactory初始化與ThreadLocal模式下的Session管理

SessionFactory 是線程安全的對象,代表Hibernate配置的全局上下文。通常在應用啓動時創建一次,並在整個生命週期中複用。

public class HibernateUtil {
    private static final SessionFactory sessionFactory = buildSessionFactory();

    private static SessionFactory buildSessionFactory() {
        try {
            return new Configuration().configure().buildSessionFactory();
        } catch (Throwable ex) {
            System.err.println("Initial SessionFactory creation failed." + ex);
            throw new ExceptionInInitializerError(ex);
        }
    }

    public static SessionFactory getSessionFactory() {
        return sessionFactory;
    }

    // 使用ThreadLocal綁定當前線程的Session
    private static final ThreadLocal<Session> sessionHolder = new ThreadLocal<>();

    public static Session getCurrentSession() {
        Session s = sessionHolder.get();
        if (s == null || !s.isOpen()) {
            s = sessionFactory.openSession();
            sessionHolder.set(s);
        }
        return s;
    }

    public static void closeCurrentSession() {
        Session s = sessionHolder.get();
        if (s != null) {
            s.close();
            sessionHolder.remove();
        }
    }
}
代碼邏輯逐行解讀:
  • ThreadLocal<Session> :確保每個線程擁有獨立的Session實例,防止併發衝突。
  • getCurrentSession() :如果當前線程沒有Session,則創建並綁定。
  • closeCurrentSession() :顯式關閉並清理ThreadLocal,避免內存泄漏。

該模式常用於Web應用中,在Filter或Interceptor中統一開啓和關閉Session。

2.2.3 一級緩存、二級緩存與查詢緩存的應用場景優化

Hibernate提供三級緩存體系:

緩存層級

作用範圍

默認啓用

説明

一級緩存(Session級)

單個Session內


基於 HashMap 存儲已加載對象

二級緩存(SessionFactory級)

整個應用


需手動配置,如Ehcache、Redis

查詢緩存

緩存HQL查詢結果ID列表


配合二級緩存使用

示例:啓用查詢緩存
Query query = session.createQuery("FROM Student WHERE age > :age");
query.setParameter("age", 20);
query.setCacheable(true); // 啓用查詢緩存
List<Student> list = query.list();

性能提示 :對於頻繁執行但結果變化較少的查詢(如字典表、靜態配置),啓用查詢緩存可顯著降低數據庫壓力。

2.3 數據庫CRUD操作與HQL查詢語言

Hibernate通過 Session 接口封裝了完整的CRUD操作,開發者不再需要關心Connection、Statement、ResultSet的手動管理。

2.3.1 使用Session完成增刪改查操作

Session session = HibernateUtil.getCurrentSession();
Transaction tx = null;

try {
    tx = session.beginTransaction();

    // CREATE
    Student student = new Student();
    student.setName("張三");
    student.setAge(20);
    session.save(student);

    // READ
    Student loaded = session.get(Student.class, 1L);

    // UPDATE
    loaded.setAge(21);
    session.update(loaded); // 或 merge()

    // DELETE
    session.delete(loaded);

    tx.commit();
} catch (Exception e) {
    if (tx != null) tx.rollback();
    e.printStackTrace();
}

重要區別

  • save() :保證立即分配OID(主鍵)
  • persist() :不保證立即分配,更適合長事務
  • update() vs merge() merge() 返回託管副本,原對象仍遊離

2.3.2 HQL語法結構與參數綁定機制(命名參數、位置參數)

HQL(Hibernate Query Language)是面向對象的查詢語言,語法類似於SQL,但操作的是實體類而非表名。

String hql = "SELECT s FROM Student s WHERE s.age BETWEEN :minAge AND :maxAge";
Query query = session.createQuery(hql);
query.setParameter("minAge", 18);
query.setParameter("maxAge", 25);
List<Student> results = query.list();

支持兩種參數綁定方式:

類型

示例

特點

命名參數

:name

可讀性強,推薦使用

位置參數

?1 , ?2

順序敏感,易出錯

2.3.3 分頁查詢、聚合函數與原生SQL執行策略

分頁查詢:
Query query = session.createQuery("FROM Student");
query.setFirstResult((pageNo - 1) * pageSize);
query.setMaxResults(pageSize);
List<Student> pageData = query.list();
聚合查詢:
Number count = (Number) session.createQuery("SELECT COUNT(*) FROM Student").uniqueResult();
Double avgAge = (Double) session.createQuery("SELECT AVG(age) FROM Student").uniqueResult();
原生SQL(Native SQL):
SQLQuery sqlQuery = session.createSQLQuery("SELECT * FROM t_student WHERE age > ?");
sqlQuery.addEntity(Student.class); // 映射結果到實體
sqlQuery.setParameter(0, 20);
List<Student> nativeResult = sqlQuery.list();

使用建議 :優先使用HQL,保持數據庫無關性;僅在複雜統計或視圖查詢時使用原生SQL。

2.4 關聯映射實戰:學生、課程與成績的關係建模

學生選課系統中最複雜的部分是 學生與課程的多對多關係 ,涉及中間表設計、級聯操作與懶加載控制。

2.4.1 多對多關係處理:學生與課程中間表設計

@Entity
@Table(name = "t_student")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private Set<Course> courses = new HashSet<>();
}

@Entity
@Table(name = "t_course")
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToMany(mappedBy = "courses", fetch = FetchType.LAZY)
    private Set<Student> students = new HashSet<>();
}

生成的中間表為 student_course(student_id, course_id) ,由 @JoinTable 指定。

2.4.2 級聯保存與更新策略設置(cascade=all)

student.getCourses().add(course);
session.save(student); // 自動保存中間表記錄

警告 cascade = CascadeType.ALL 可能導致意外刪除關聯數據,應謹慎使用。

2.4.3 懶加載與急加載模式的選擇與性能調優

場景

推薦策略

列表展示(僅需主信息)

fetch = LAZY

詳情頁需加載全部關聯

JOIN FETCH HQL預加載

高頻訪問的小數據量

EAGER 加載

// 使用JOIN FETCH避免N+1問題
String hql = "SELECT DISTINCT s FROM Student s LEFT JOIN FETCH s.courses WHERE s.id = :id";

N+1問題 :循環遍歷學生列表並訪問 student.getCourses() 會導致每條記錄都發起一次SQL查詢,嚴重影響性能。

綜上所述,Hibernate不僅是ORM工具,更是現代企業級Java應用中不可或缺的數據持久化引擎。通過科學設計實體映射、合理配置緩存與會話策略,結合HQL靈活查詢,能夠構建出高性能、高可維護性的數據訪問層。

3. Spring配置IoC容器與AOP事務管理

在企業級Java開發中,Spring框架扮演着核心角色。其兩大支柱——控制反轉(Inversion of Control, IoC)和麪向切面編程(Aspect-Oriented Programming, AOP)——不僅極大地提升了代碼的可維護性與擴展性,也使得服務層的事務管理變得簡潔而強大。本章將深入探討Spring如何通過IoC容器實現組件的鬆耦合管理,並結合AOP機制完成聲明式事務控制,確保學生選課系統中的關鍵業務操作具備數據一致性與高可靠性。

3.1 Spring IoC容器的核心作用與Bean管理

Spring的IoC容器是整個框架的基礎運行環境,它負責創建、配置和管理應用中的所有Java對象(即Bean),並將這些對象之間的依賴關係自動裝配,從而取代傳統的手動 new 實例化方式,降低模塊間的耦合度。

3.1.1 控制反轉(IoC)與依賴注入(DI)原理剖析

傳統開發模式下,一個類若需要使用另一個類的功能,通常會直接在其內部通過 new 關鍵字創建依賴對象,這種硬編碼方式導致類之間高度耦合,難以測試和維護。例如:

public class CourseService {
    private CourseDAO courseDAO = new CourseDAO(); // 硬編碼依賴
}

而在IoC思想中,對象不再主動獲取依賴,而是被動接收由外部容器“注入”的依賴項。這一過程稱為 依賴注入 (Dependency Injection, DI)。Spring容器在啓動時讀取配置信息(XML或註解),實例化Bean並建立它們之間的依賴關係。

以學生選課系統的 StudentService 為例,它依賴於 StudentDAO 來訪問數據庫:

@Service
public class StudentService {
    @Autowired
    private StudentDAO studentDAO;

    public List<Student> getAllStudents() {
        return studentDAO.findAll();
    }
}

上述代碼中, @Autowired 註解告訴Spring:“請為我自動注入一個 StudentDAO 類型的Bean”。這個動作的背後,正是IoC容器在起作用。

特性

説明

控制反轉

控制權從程序代碼轉移到容器,由Spring決定何時創建對象、如何組裝對象

依賴注入方式

支持構造器注入、Setter注入、字段注入(推薦使用構造器或Setter)

優勢

解耦、便於單元測試、支持動態替換實現類(如Mock對象)

graph TD
    A[應用程序代碼] --> B[不再主動創建對象]
    C[Spring IoC Container] --> D[根據配置創建Bean]
    D --> E[解析依賴關係]
    E --> F[執行依賴注入]
    F --> G[嚮應用提供就緒的Bean實例]
    B --> G

該流程圖展示了IoC的工作機制:開發者只需定義Bean及其依賴,Spring容器負責完成對象生命週期的全過程管理。

字段注入 vs 構造器注入對比分析

雖然字段注入寫法簡潔,但存在以下問題:
- 難以進行單元測試(無法在不啓動容器的情況下傳入Mock依賴)
- 容易隱藏強依賴關係
- 不利於不可變對象設計

更推薦使用構造器注入:

@Service
public class StudentService {

    private final StudentDAO studentDAO;

    @Autowired
    public StudentService(StudentDAO studentDAO) {
        this.studentDAO = studentDAO;
    }

    // 方法省略...
}

這種方式保證了 studentDAO 不會為null,且易於測試:

@Test
public void testGetAllStudents() {
    StudentDAO mockDAO = Mockito.mock(StudentDAO.class);
    when(mockDAO.findAll()).thenReturn(Arrays.asList(new Student("張三")));

    StudentService service = new StudentService(mockDAO); // 直接注入mock對象

    assertEquals(1, service.getAllStudents().size());
}

參數説明:
- Mockito.mock() :創建指定類的代理對象
- when(...).thenReturn(...) :設定方法調用返回值
- 構造器注入使測試無需Spring上下文即可完成

3.1.2 基於XML與註解方式的Bean定義與自動裝配

Spring支持兩種主要的Bean配置方式:XML配置和基於註解的配置。隨着現代開發趨勢演進,註解方式已成為主流,但仍有必要理解XML的底層機制。

XML配置示例(applicationContext.xml)
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="studentDAO" class="com.example.dao.StudentDAOImpl"/>
    <bean id="studentService" class="com.example.service.StudentServiceImpl">
        <property name="studentDAO" ref="studentDAO"/>
    </bean>
</beans>

邏輯分析:
- <bean> 標籤定義了一個Spring管理的對象
- id 屬性是Bean的唯一標識符
- class 指定具體實現類
- <property> 用於Setter注入, name 對應JavaBean屬性名, ref 指向另一個Bean

相比之下,註解方式更加簡潔:

@Component("studentDAO")
public class StudentDAOImpl implements StudentDAO { ... }

@Service("studentService")
public class StudentServiceImpl implements StudentService {

    @Autowired
    private StudentDAO studentDAO;
}

啓用註解掃描需添加配置:

<context:component-scan base-package="com.example"/>

或在Java Config中:

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

對比維度

XML配置

註解配置

可讀性

集中統一,適合複雜結構

分散在類上,直觀但分散

修改成本

不修改源碼即可更換實現

需重新編譯

耦合度

完全解耦

類與框架耦合

推薦場景

大型企業項目、第三方類集成

快速開發、微服務架構

實際項目中常採用混合模式:核心組件用XML定義,業務組件用註解標註。

3.1.3 Bean的作用域(singleton/prototype)與生命週期回調

Spring中每個Bean都有明確的作用域,最常見的兩種是:

  • singleton :默認作用域,整個容器中僅存在一個共享實例
  • prototype :每次請求都創建一個新的實例

其他還包括 request session global-session 等Web相關作用域。

作用域配置示例
@Component
@Scope("prototype") // 每次getBean都返回新實例
public class TemporaryReportGenerator { }

或在XML中:

<bean id="reportGen" class="..." scope="prototype"/>
生命週期鈎子方法

Spring允許在Bean初始化和銷燬時執行自定義邏輯:

@PostConstruct
public void init() {
    System.out.println("Bean初始化完成,連接池已建立");
}

@PreDestroy
public void destroy() {
    System.out.println("Bean即將銷燬,釋放數據庫連接");
}

也可實現 InitializingBean DisposableBean 接口:

public class MyDAO implements InitializingBean, DisposableBean {

    @Override
    public void afterPropertiesSet() throws Exception {
        // 初始化邏輯
    }

    @Override
    public void destroy() throws Exception {
        // 銷燬前清理資源
    }
}

或者通過XML配置指定初始化/銷燬方法:

<bean id="myDao" class="com.example.MyDAO"
      init-method="customInit"
      destroy-method="customDestroy"/>

典型應用場景:
- 數據源初始化時建立連接池
- 緩存預熱(如加載常用課程列表到內存)
- 日誌記錄器關閉前刷新緩衝區

sequenceDiagram
    participant Container
    participant Bean
    Container->>Bean: 實例化
    Container->>Bean: 屬性賦值(DI)
    alt singleton
        Container->>Bean: 調用@PostConstruct / init-method
        Note right of Container: 加入單例緩存池
    end
    Bean-->>Container: 提供可用Bean
    ...
    Container->>Bean: 容器關閉時調用@PreDestroy / destroy-method

此序列圖清晰地描繪了singleton Bean的完整生命週期流程。

3.2 服務層組件設計與事務控制

在學生選課系統中,許多業務操作涉及多個數據庫操作,必須保證原子性。例如“選課”操作需同時檢查課程容量、插入選課記錄、更新已選人數,任何一步失敗都應整體回滾。這就引出了Spring強大的聲明式事務管理能力。

3.2.1 Service接口抽象與實現類注入DAO組件

遵循分層架構原則,Service層應定義清晰的接口,便於後期擴展和測試:

public interface EnrollmentService {
    boolean enrollStudent(Long studentId, Long courseId) throws BusinessException;
    List<Course> getEnrolledCourses(Long studentId);
}

@Service
@Transactional
public class EnrollmentServiceImpl implements EnrollmentService {

    @Autowired
    private EnrollmentDAO enrollmentDAO;

    @Autowired
    private CourseDAO courseDAO;

    @Override
    public boolean enrollStudent(Long studentId, Long courseId) {
        Course course = courseDAO.findById(courseId);
        if (course.getEnrolledCount() >= course.getCapacity()) {
            throw new BusinessException("課程人數已滿");
        }

        enrollmentDAO.save(new Enrollment(studentId, courseId));
        courseDAO.incrementEnrolledCount(courseId);

        return true;
    }
}

參數説明:
- @Transactional :開啓事務管理
- enrollmentDAO.save() :插入中間表記錄
- courseDAO.incrementEnrolledCount() :更新課程人數

若未使用事務,當 save() 成功但 increment() 失敗時,會導致數據不一致。而Spring的事務機制能確保兩者要麼全部成功,要麼全部失敗。

3.2.2 聲明式事務管理配置(@Transactional註解與AOP代理機制)

Spring通過AOP(面向切面編程)實現聲明式事務。當調用帶有 @Transactional 的方法時,Spring會生成一個代理對象,在方法執行前後自動開啓和提交事務。

啓用事務註解驅動
<tx:annotation-driven transaction-manager="transactionManager"/>

或Java配置:

@EnableTransactionManagement
@Configuration
public class TransactionConfig {
    @Bean
    public PlatformTransactionManager transactionManager(SessionFactory sessionFactory) {
        return new HibernateTransactionManager(sessionFactory);
    }
}
AOP代理機制詳解

Spring使用JDK動態代理或CGLIB生成代理類:

  • JDK代理:基於接口的代理,要求目標類實現接口
  • CGLIB代理:通過繼承生成子類,適用於沒有接口的類
classDiagram
    class TransactionProxy {
        +invoke(): Object
    }
    class EnrollmentService {
        +enrollStudent()
    }
    TransactionProxy --> "implements InvocationHandler" java.lang.reflect.InvocationHandler
    EnrollmentService <|-- TransactionProxy : JDK Proxy

調用流程如下:
1. 客户端調用 enrollmentService.enrollStudent()
2. 實際調用的是代理對象的方法
3. 代理先開啓事務
4. 再調用真實目標方法
5. 根據異常情況決定提交或回滾

@Transactional 參數詳解
@Transactional(
    propagation = Propagation.REQUIRED,
    isolation = Isolation.READ_COMMITTED,
    timeout = 30,
    readOnly = false,
    rollbackFor = {BusinessException.class}
)

參數

説明

propagation

事務傳播行為,默認REQUIRED(加入當前事務)

isolation

隔離級別,防止髒讀、不可重複讀等問題

timeout

超時時間(秒),避免長時間鎖定

readOnly

是否只讀事務,優化性能

rollbackFor

指定哪些異常觸發回滾

特別注意:Spring默認只對 RuntimeException Error 回滾,檢查型異常(如IOException)不會自動回滾,除非顯式聲明。

3.2.3 事務傳播行為與隔離級別在選課業務中的應用

傳播行為實戰案例

假設有一個批量選課功能:

@Service
public class BatchEnrollmentService {

    @Autowired
    private EnrollmentService enrollmentService;

    @Transactional
    public void batchEnroll(List<EnrollmentRequest> requests) {
        for (EnrollmentRequest req : requests) {
            try {
                enrollmentService.enrollStudent(req.getStuId(), req.getCourseId());
            } catch (BusinessException e) {
                log.warn("選課失敗:{}", e.getMessage());
                // 繼續處理下一個,不影響整體事務?
            }
        }
    }
}

問題來了:如果 enrollStudent 本身也是 @Transactional 方法,會發生什麼?

答案取決於 傳播行為 設置:

傳播行為

行為描述

適用場景

REQUIRED (默認)

若有事務則加入,否則新建

普通業務方法

REQUIRES_NEW

總是新建事務,掛起當前事務

記錄日誌、發送通知等獨立操作

SUPPORTS

支持當前事務,無則非事務執行

查詢類方法

NOT_SUPPORTED

不支持事務,總是非事務執行

批量導入臨時數據

改進方案:

@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean enrollStudent(Long studentId, Long courseId) {
    // 單個選課獨立事務,失敗不影響其他
}

這樣即使某次選課失敗,也不會導致整個批次中斷。

隔離級別的選擇

MySQL默認為 REPEATABLE_READ ,但在高併發選課場景下可能出現幻讀問題。建議調整為:

@Transactional(isolation = Isolation.SERIALIZABLE)
public boolean enrollStudent(...) {
    // 嚴格串行化執行,避免超賣
}

儘管性能較低,但對於關鍵業務值得犧牲部分吞吐量換取絕對一致性。

3.3 Spring與Hibernate整合關鍵點

要讓Spring全面管理Hibernate,必須正確配置SessionFactory、事務管理器以及解決常見的延遲加載問題。

3.3.1 LocalSessionFactoryBean配置與HibernateTemplate使用

XML配置SessionFactory
<bean id="sessionFactory" 
      class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="packagesToScan" value="com.example.entity"/>
    <property name="hibernateProperties">
        <props>
            <prop key="hibernate.dialect">org.hibernate.dialect.MySQL8Dialect</prop>
            <prop key="hibernate.hbm2ddl.auto">update</prop>
            <prop key="hibernate.show_sql">true</prop>
            <prop key="hibernate.format_sql">true</prop>
        </props>
    </property>
</bean>

參數説明:
- dataSource :引用數據源Bean
- packagesToScan :自動掃描實體類包路徑
- hbm2ddl.auto :schema更新策略(create/update/create-drop/validate)

使用HibernateTemplate(舊版方式)
@Repository
public class StudentDAOImpl implements StudentDAO {

    @Autowired
    private HibernateTemplate hibernateTemplate;

    @Override
    public List<Student> findAll() {
        return hibernateTemplate.loadAll(Student.class);
    }
}

優點:簡化模板代碼,自動處理Session開閉
缺點:已被 SessionFactory.getCurrentSession() 取代

現代做法:

@Autowired
private SessionFactory sessionFactory;

public List<Student> findAll() {
    return sessionFactory.getCurrentSession()
                        .createQuery("FROM Student", Student.class)
                        .list();
}

3.3.2 編程式事務管理與PlatformTransactionManager配置

除了聲明式事務,有時需要精細控制事務邊界:

@Autowired
private PlatformTransactionManager transactionManager;

public void transferCredits() {
    TransactionDefinition def = new DefaultTransactionDefinition();
    TransactionStatus status = transactionManager.getTransaction(def);

    try {
        // 執行多個操作
        deductCredits(100);
        addCredits(100);
        transactionManager.commit(status);
    } catch (Exception e) {
        transactionManager.rollback(status);
        throw e;
    }
}

適用場景:
- 跨多個不同事務管理器的操作
- 條件性提交/回滾邏輯
- 需要捕獲特定異常後繼續嘗試

3.3.3 OpenSessionInView模式解決延遲加載異常問題

當啓用了懶加載(lazy=true),在View層訪問未初始化的關聯對象時會拋出 LazyInitializationException

解決方案:使用 OpenSessionInViewFilter 保持Session開啓直到渲染結束。

web.xml配置
<filter>
    <filter-name>openSessionInViewFilter</filter-name>
    <filter-class>org.springframework.orm.hibernate5.support.OpenSessionInViewFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>openSessionInViewFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

工作原理:

sequenceDiagram
    participant Client
    participant Filter
    participant Controller
    participant View
    Client->>Filter: 請求到達
    Filter->>Filter: Open Session
    Filter->>Controller: 轉發請求
    Controller->>DB: 查詢(返回代理對象)
    Controller->>View: 返回ModelAndView
    View->>DB: 訪問lazy屬性(Session仍有效)
    View-->>Client: 渲染頁面
    Filter->>Filter: Close Session

注意事項:
- 增加數據庫連接持有時間,可能影響性能
- 應儘量在Service層提前初始化必要數據
- 可配合 JOIN FETCH HQL語句優化查詢

3.4 applicationContext.xml配置文件深度解析

作為Spring的核心配置文件, applicationContext.xml 集中定義了數據源、事務管理器、組件掃描等全局配置。

3.4.1 數據源(DataSource)配置與C3P0連接池集成

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
    <property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
    <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/course_db"/>
    <property name="user" value="root"/>
    <property name="password" value="password"/>
    <property name="minPoolSize" value="5"/>
    <property name="maxPoolSize" value="20"/>
    <property name="maxIdleTime" value="3000"/>
</bean>

C3P0參數説明:
- minPoolSize :最小連接數,系統啓動時創建
- maxPoolSize :最大連接數,按需增長
- maxIdleTime :空閒連接超時時間(毫秒)

替代方案如HikariCP性能更優:

<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    <property name="jdbcUrl" value="..."/>
    <property name="username" value="..."/>
    <property name="password" value="..."/>
    <property name="maximumPoolSize" value="15"/>
</bean>

3.4.2 事務管理器註冊與AOP切面織入配置

<!-- 事務管理器 -->
<bean id="transactionManager" 
      class="org.springframework.orm.hibernate5.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory"/>
</bean>

<!-- 開啓註解事務 -->
<tx:annotation-driven transaction-manager="transactionManager"/>

<!-- AOP配置 -->
<aop:config>
    <aop:pointcut id="serviceMethods" 
                  expression="execution(* com.example.service.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethods"/>
</aop:config>

<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="get*" read-only="true"/>
        <tx:method name="find*" read-only="true"/>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

該配置實現了:
- 所有Service方法默認啓用事務
- 以 get / find 開頭的方法設為只讀事務,提升性能
- 其他方法使用REQUIRED傳播行為

3.4.3 組件掃描路徑設置與Bean依賴關係維護

<context:component-scan base-package="com.example">
    <context:exclude-filter type="annotation" 
                            expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

排除Controller是因為Web層由Spring MVC容器管理,避免重複掃描。

最終完整的 applicationContext.xml 結構如下表所示:

配置區域

主要內容

示例Bean

數據源

連接池配置

dataSource

SessionFactory

Hibernate整合

sessionFactory

事務管理器

事務控制中樞

transactionManager

組件掃描

自動註冊Bean

component-scan

AOP切面

事務織入規則

tx:advice, aop:config

緩存管理

可選二級緩存

cacheManager

通過合理組織 applicationContext.xml ,可以構建出穩定高效的後端支撐體系,為學生選課系統提供堅實的基礎設施保障。

4. Struts實現MVC模式下的請求處理與流程控制

在現代Java Web開發中,MVC(Model-View-Controller)架構已成為構建可維護、可擴展Web應用的標準範式。Struts2作為Apache基金會推出的成熟MVC框架,在SSH整合體系中承擔着表現層的核心職責——負責接收用户請求、調用業務邏輯、並返回相應的視圖響應。它通過攔截器鏈、值棧機制和OGNL表達式語言,實現了高度靈活的請求處理流程。本章將深入剖析Struts2的運行機制,從底層組件協作到上層控制器開發,再到配置管理與視圖渲染,全面揭示其如何支撐學生選課系統中的各類交互場景。

4.1 Struts2框架運行機制與核心組件

Struts2並非基於Servlet MVC的傳統實現,而是構建於FilterDispatcher(早期版本)或StrutsPrepareAndExecuteFilter(新版本)之上的攔截式架構。這種設計使得整個請求生命週期可以被多個攔截器精確控制,從而實現權限校驗、日誌記錄、參數封裝等橫切關注點的統一管理。理解其核心組件及其協作關係,是掌握Struts2開發的關鍵前提。

4.1.1 MVC設計模式在Web層的落地實現

MVC模式將應用程序劃分為三個部分: 模型(Model) 負責數據與業務邏輯; 視圖(View) 負責展示界面; 控制器(Controller) 負責協調兩者之間的交互。在Struts2中,這一模式的具體映射如下:

  • Model :由Spring管理的Service和DAO組件構成,通常通過依賴注入進入Action;
  • View :JSP頁面配合Struts2標籤庫完成動態內容渲染;
  • Controller :Action類扮演控制器角色,接收請求、調用服務、決定跳轉路徑。

當客户端發起一個HTTP請求(如 /student/enroll.action ),該請求首先被 web.xml 中配置的 StrutsPrepareAndExecuteFilter 捕獲。該過濾器會根據 struts.xml 中的映射規則查找對應的Action,並創建一個 ActionProxy 實例來代理執行過程。隨後,一系列攔截器按序執行(如文件上傳、類型轉換、參數注入等),最終調用目標Action的方法。方法執行完畢後,返回結果碼(如”success”),框架依據此碼選擇對應的結果視圖進行渲染。

這種分層結構不僅提升了代碼的可測試性,也便於團隊分工協作。例如前端開發者專注JSP頁面佈局,而後端工程師則集中於Action邏輯編寫。

表格:Struts2中MVC各層職責對比

層級

組件示例

主要職責

技術依賴

Model

StudentService, CourseDAO

數據存取、業務規則執行

Spring + Hibernate

View

enroll.jsp, listCourses.jsp

用户界面展示、表單輸入輸出

JSP + Struts2標籤庫

Controller

EnrollmentAction, LoginAction

接收請求、調用服務、返回結果

Struts2 ActionSupport

該表格清晰地展示了各層的技術邊界與協作方式,為後續模塊化開發提供指導。

4.1.2 ActionProxy、Interceptor、ResultType工作機制解析

Struts2的請求處理流程本質上是一條“責任鏈”模式的應用。其中三大核心組件協同工作,確保請求能夠安全、高效地完成流轉。

ActionProxy

ActionProxy 是Struts2內部用於封裝Action調用過程的對象。它不直接處理業務邏輯,而是作為調度中心,協調攔截器、Action實例和Result處理器。每一個匹配的URL都會生成一個唯一的 ActionProxy 實例,保證線程安全性。

Interceptor(攔截器)

攔截器是Struts2最具特色的機制之一。它們以鏈式結構組織,每個攔截器可在Action執行前後插入自定義邏輯。常見的內置攔截器包括:
- defaultStack :包含參數注入、類型轉換、模型驅動等功能。
- validation :支持基於XML或註解的字段驗證。
- fileUpload :處理multipart/form-data類型的文件上傳請求。

開發者也可以編寫自定義攔截器,比如用於登錄狀態檢查:

public class LoginInterceptor extends AbstractInterceptor {
    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
        Map<String, Object> session = invocation.getInvocationContext().getSession();
        if (session.get("user") == null) {
            return "login"; // 未登錄跳轉至登錄頁
        }
        return invocation.invoke(); // 放行請求
    }
}

上述代碼繼承自 AbstractInterceptor ,重寫 intercept 方法。通過 invocation.getInvocationContext().getSession() 獲取當前會話,判斷是否存在”user”對象。若不存在,則直接返回 "login" 結果碼,阻止後續操作;否則調用 invocation.invoke() 繼續執行下一個攔截器或目標Action。

邏輯分析
- ActionInvocation 代表一次Action調用上下文,可用於訪問Action實例、Session、值棧等。
- invoke() 方法觸發攔截器鏈的下一環節,形成遞歸調用結構。
- 返回字符串即為邏輯結果名,需在 struts.xml 中配置對應視圖。

ResultType(結果類型)

結果類型決定了Action執行完成後如何響應客户端。Struts2支持多種內置結果類型:

結果類型

描述

典型用途

dispatcher

轉發到JSP或其他資源(默認)

頁面跳轉

redirect

重定向到新URL

防止重複提交

json

返回JSON數據

AJAX接口

stream

輸出二進制流

文件下載

例如,在添加課程成功後希望刷新列表頁面,可使用重定向避免刷新時重複提交:

<result name="success" type="redirect">listCourses.action</result>

這比簡單的 dispatcher 更符合冪等性原則。

Mermaid流程圖:Struts2請求處理流程
graph TD
    A[HTTP Request] --> B{StrutsPrepareAndExecuteFilter}
    B --> C[Create ActionProxy]
    C --> D[Build Interceptor Stack]
    D --> E[Execute Pre-Action Interceptors]
    E --> F[Call Action Method]
    F --> G[Execute Post-Action Interceptors]
    G --> H{Return Result Code}
    H -- "success" --> I[Render JSP via Dispatcher]
    H -- "error" --> J[Show Error Page]
    H -- "input" --> K[Re-render Form with Errors]

該流程圖完整呈現了從請求到達至視圖渲染的全過程,突出了攔截器的雙向介入能力以及結果類型的分支決策作用。

4.1.3 值棧(ValueStack)與OGNL表達式語言的數據訪問機制

Struts2引入了 值棧(ValueStack) 概念,作為Action與View之間共享數據的核心容器。值棧本質上是一個OgnlContext,包含兩個主要區域:
- Root對象棧 :存放Action實例本身及壓入的其他Java對象;
- Context Map :封裝Request、Session、Application、Parameters等環境變量。

當在JSP中使用 <s:property value="name"/> 時,Struts2會通過OGNL(Object-Graph Navigation Language)從值棧中查找 name 屬性。查找順序為:
1. 先在Action實例中找 getName() 方法;
2. 若未找到,則依次搜索棧中其他對象;
3. 最後嘗試從Parameters或Session中獲取。

示例:值棧中的數據訪問

假設有一個 StudentAction 類:

public class StudentAction extends ActionSupport {
    private String name;
    private List<Course> courses;

    public String execute() {
        name = "張三";
        courses = courseService.findAll();
        return SUCCESS;
    }

    // getter/setter省略
}

在JSP中可通過以下方式訪問:

<s:property value="name"/> <!-- 輸出"張三" -->
<s:iterator value="courses">
    <s:property value="courseName"/>
</s:iterator>

這裏 value="courses" 會被解析為從Action中獲取 getCourses() 返回的集合,然後迭代輸出每門課程名稱。

OGNL特性説明
- 支持方法調用: value="getCourseById(1).teacher.name"
- 支持集合操作: value="courses.{? #this.credit > 3}"
- 支持投影與篩選:強大但需謹慎使用以防性能問題

此外,可通過 ActionContext.getContext().getValueStack().push() 手動向值棧壓入對象,供多個Action共享數據。

綜上所述,值棧與OGNL的結合極大簡化了跨層數據傳遞,使開發者無需顯式設置Request屬性即可實現視圖綁定,顯著提升開發效率。

4.2 用户請求攔截與Action控制器開發

在實際項目中,Action不僅是請求入口,更是連接前端與後端的橋樑。正確設計Action類、合理選擇參數接收方式,並利用攔截器增強安全性,是保障系統健壯性的關鍵。

4.2.1 Action類編寫規範與方法返回值定義(success/input/error)

Struts2中的Action通常繼承自 ActionSupport 類,該類實現了 Action 接口並提供了國際化、驗證、日誌等基礎功能。標準Action應遵循以下規範:

  1. 命名規範 :以業務功能命名,如 EnrollmentAction LoginAction
  2. 方法簽名 :公共無參方法,返回 String 類型結果碼
  3. 結果碼約定 :推薦使用標準值 "success" "input" "error" "login"

示例:學生登錄Action

public class LoginAction extends ActionSupport implements SessionAware {
    private String username;
    private String password;
    private Map<String, Object> session;

    @Autowired
    private UserService userService;

    public String login() {
        try {
            User user = userService.authenticate(username, password);
            if (user != null) {
                session.put("user", user);
                return SUCCESS;
            } else {
                addFieldError("username", "用户名或密碼錯誤");
                return INPUT;
            }
        } catch (Exception e) {
            addActionError("系統異常,請稍後再試");
            return ERROR;
        }
    }

    // getter/setter 省略
    public void setSession(Map<String, Object> session) {
        this.session = session;
    }
}

邏輯分析
- implements SessionAware 允許框架自動注入Session Map,避免直接操作HttpServletRequest。
- addFieldError() 用於標記特定字段錯誤,配合 <s:fielderror> 標籤顯示。
- addActionError() 添加全局錯誤信息,適合非字段級異常提示。
- 返回 INPUT 表示需要重新顯示錶單(常用於驗證失敗),而 ERROR 表示系統級故障。

配置示例(struts.xml):
<action name="login" class="com.example.LoginAction" method="login">
    <result name="success">/index.jsp</result>
    <result name="input">/login.jsp</result>
    <result name="error">/error.jsp</result>
</action>

該配置明確了不同結果碼對應的視圖路徑,形成清晰的導航邏輯。

4.2.2 參數接收方式對比:屬性驅動 vs 模型驅動(ModelDriven)

Struts2提供兩種主流參數綁定方式: 屬性驅動(Property-Driven) 模型驅動(Model-Driven)

屬性驅動

適用於簡單表單,直接在Action中聲明與表單字段同名的屬性:

<form action="saveStudent.action">
    <input type="text" name="name"/>
    <input type="text" name="age"/>
    <button>提交</button>
</form>

對應Action:

public class StudentAction extends ActionSupport {
    private String name;
    private int age;

    public String save() {
        Student s = new Student(name, age);
        studentService.save(s);
        return SUCCESS;
    }
    // getter/setter...
}

優點:簡單直觀;缺點:污染Action類,難以複用。

模型驅動

通過實現 ModelDriven<T> 接口,指定一個模型對象作為參數載體:

public class StudentAction extends ActionSupport implements ModelDriven<Student> {
    private Student student = new Student();

    public Student getModel() {
        return student;
    }

    public String save() {
        studentService.save(student); // 直接使用model
        return SUCCESS;
    }
}

此時表單仍為:

<s:form action="saveStudent">
    <s:textfield name="name" label="姓名"/>
    <s:textfield name="age" label="年齡"/>
    <s:submit/>
</s:form>

優勢分析
- 解耦Action與數據模型,提升可維護性;
- 支持複雜嵌套對象(如 address.city );
- 更符合OO設計原則。

因此,在學生選課系統中,涉及 Student Course Enrollment 等實體的操作,推薦採用 模型驅動 方式。

4.2.3 自定義攔截器實現登錄驗證與日誌記錄功能

為防止未授權訪問敏感功能(如成績錄入、課程管理),需實現統一的認證攔截機制。

創建登錄攔截器
@Intercepts({@InterceptorRef("defaultStack"), @InterceptorRef("login")})
public class AuthInterceptor extends AbstractInterceptor {

    public String intercept(ActionInvocation invocation) throws Exception {
        ActionContext ctx = invocation.getInvocationContext();
        Map<String, Object> session = ctx.getSession();
        String actionName = invocation.getProxy().getActionName();

        // 白名單放行
        if ("login".equals(actionName) || "doLogin".equals(actionName)) {
            return invocation.invoke();
        }

        User user = (User) session.get("user");
        if (user == null) {
            return "login";
        }

        // 記錄訪問日誌
        HttpServletRequest request = (HttpServletRequest) ctx.get(StrutsStatics.HTTP_REQUEST);
        System.out.println(user.getUsername() + " 訪問: " + request.getRequestURI());

        return invocation.invoke();
    }
}

參數説明
- @Intercepts 註解聲明該攔截器引用哪些已有攔截器棧;
- ActionInvocation 提供對當前執行環境的完整訪問;
- StrutsStatics.HTTP_REQUEST 用於獲取原生Servlet API對象。

註冊攔截器(struts.xml)
<interceptors>
    <interceptor name="auth" class="com.example.AuthInterceptor"/>
    <interceptor-stack name="secureStack">
        <interceptor-ref name="defaultStack"/>
        <interceptor-ref name="auth"/>
    </interceptor-stack>
</interceptors>

<default-interceptor-ref name="secureStack"/>

通過設置為默認攔截器棧,所有Action都將自動受保護,除非顯式排除。

4.3 struts.xml配置與導航流控制

struts.xml 是Struts2的中樞配置文件,決定了URL路由、Action映射、結果跳轉等核心行為。良好的配置結構能顯著提升系統的可維護性和擴展性。

4.3.1 包結構劃分與命名空間(namespace)管理

Struts2支持通過 <package> 元素對Action進行模塊化組織,常用策略如下:

<package name="student" namespace="/student" extends="struts-default">
    <action name="enroll" class="EnrollmentAction" method="enroll">
        <result name="success">/student/success.jsp</result>
    </action>
</package>

<package name="admin" namespace="/admin" extends="struts-default">
    <action name="addCourse" class="CourseAction" method="add">
        <result name="success">/admin/courseList.jsp</result>
    </action>
</package>
  • namespace="/student" 表示該包下所有Action的訪問路徑前綴為 /student/*
  • 不同模塊獨立部署,避免命名衝突
  • 可針對不同包設置專屬攔截器棧

4.3.2 Action映射配置與結果視圖跳轉類型

除了基本的 dispatcher redirect 外,還可使用 chain 類型實現Action間跳轉而不刷新頁面:

<result name="next" type="chain">confirmEnrollment</result>

chain 保持值棧不變,適合多步表單嚮導場景。

4.3.3 全局結果集與異常處理機制配置

為減少重複配置,可定義全局結果和異常映射:

<global-results>
    <result name="error">/common/error.jsp</result>
    <result name="login" type="redirect">/login.jsp</result>
</global-results>

<global-exception-mappings>
    <exception-mapping result="error" exception="java.lang.Exception"/>
</global-exception-mappings>

一旦拋出未捕獲異常,自動跳轉至統一錯誤頁,提升用户體驗一致性。

4.4 JSP視圖層開發與Struts標籤庫應用

JSP結合Struts2標籤庫可高效構建動態頁面,尤其在表單處理、數據展示和國際化方面表現出色。

4.4.1 表單標籤與下拉框動態填充

<s:form action="enroll">
    <s:textfield name="student.name" label="學生姓名"/>
    <s:select 
        name="course.id" 
        label="選擇課程"
        list="availableCourses" 
        listKey="id" 
        listValue="courseName + ' (' + credit + '學分)'"/>
    <s:submit value="選課"/>
</s:form>

其中 list 屬性綁定Action中暴露的 availableCourses 集合,實現動態選項加載。

4.4.2 數據展示標籤與條件判斷

<s:if test="hasRole('ADMIN')">
    <a href="editCourse.action?id=<s:property value='id'/>">編輯</a>
</s:if>

test 屬性支持OGNL布爾表達式,實現細粒度UI控制。

4.4.3 國際化支持與錯誤消息輸出

配置資源文件 messages_zh_CN.properties

prompt.username=請輸入用户名
error.invalid.login=登錄失敗,請檢查賬號密碼

在JSP中使用:

<s:text name="prompt.username"/>
<s:actionerror/>
<s:fielderror/>

框架自動根據Locale選擇對應語言包,實現真正的多語言支持。

5. 課程信息管理與選課業務邏輯實現

5.1 DAO、Service、Action三層架構協作機制

在SSH整合架構中,DAO(Data Access Object)、Service 和 Action 三層結構是實現分層解耦的核心設計模式。每一層各司其職,通過Spring的IoC容器進行依賴注入,確保系統具備良好的可維護性與擴展性。

5.1.1 各層職責劃分與接口契約設計原則

  • DAO層 :負責與數據庫交互,封裝Hibernate對實體的操作,如增刪改查、HQL查詢等。通常定義為接口+實現類的形式。
  • Service層 :處理業務邏輯,協調多個DAO操作,保證事務一致性。例如選課過程需同時更新學生選課記錄和課程容量。
  • Action層 :Struts2控制器,接收HTTP請求參數,調用Service方法,並將結果傳遞給JSP視圖展示。

以“學生選課”功能為例,其接口契約設計如下:

// CourseDAO.java
public interface CourseDAO {
    List<Course> findAll();
    Course findById(Integer id);
    void update(Course course);
}

// EnrollmentService.java
public interface EnrollmentService {
    boolean enrollStudent(Student student, Course course) throws EnrollmentException;
}

這種基於接口的設計使得上層組件不依賴具體實現,便於單元測試與AOP增強。

5.1.2 依賴注入實現層間通信

Spring通過 @Autowired 註解完成跨層對象注入。以下為Service注入DAO、Action注入Service的典型配置:

<!-- applicationContext.xml -->
<context:component-scan base-package="com.student.system" />
<tx:annotation-driven transaction-manager="transactionManager"/>
@Service("enrollmentService")
@Transactional
public class EnrollmentServiceImpl implements EnrollmentService {

    @Autowired
    private EnrollmentDAO enrollmentDAO;

    @Autowired
    private CourseDAO courseDAO;

    @Override
    public boolean enrollStudent(Student student, Course course) {
        if (course.getEnrolled() >= course.getCapacity()) {
            throw new EnrollmentException("課程人數已達上限");
        }
        // 更新課程已選人數
        course.setEnrolled(course.getEnrolled() + 1);
        courseDAO.update(course);

        // 保存選課記錄
        Enrollment record = new Enrollment(student, course);
        enrollmentDAO.save(record);
        return true;
    }
}

在Struts2的Action中注入Service:

@Controller
@Scope("prototype")
public class EnrollmentAction extends ActionSupport {

    @Autowired
    private EnrollmentService enrollmentService;

    private Integer courseId;
    private Student currentUser;

    public String execute() {
        try {
            enrollmentService.enrollStudent(currentUser, new Course(courseId));
            addActionMessage("選課成功!");
            return SUCCESS;
        } catch (EnrollmentException e) {
            addActionError(e.getMessage());
            return ERROR;
        }
    }
}

5.1.3 異常統一處理機制設計與全局異常映射配置

為了提升用户體驗與系統健壯性,應建立統一異常處理機制。在 struts.xml 中配置全局異常攔截:

<global-exception-mappings>
    <exception-mapping exception="com.student.system.exception.EnrollmentException"
                       result="input"/>
    <exception-mapping exception="java.lang.Exception"
                       result="error"/>
</global-exception-mappings>

<global-results>
    <result name="error">/error.jsp</result>
    <result name="input">/enroll.jsp</result>
</global-results>

同時,在基礎Action中定義通用錯誤反饋方法:

protected void handleException(Exception e, String defaultMsg) {
    log.error("業務執行異常", e);
    if (e instanceof EnrollmentException) {
        addActionError(e.getMessage());
    } else {
        addActionError(defaultMsg);
    }
}

該機制結合日誌記錄,可有效追蹤問題源頭並提供友好提示。

層級

職責

技術實現

DAO

數據持久化

Hibernate + HQL

Service

事務控制、業務規則

Spring @Transactional

Action

請求響應、數據綁定

Struts2 ModelDriven

View

頁面渲染

JSP + Struts標籤庫

此外,使用 ModelDriven<T> 接口可簡化表單數據綁定流程:

public class CourseAction extends ActionSupport implements ModelDriven<Course> {
    private Course course = new Course();

    @Override
    public Course getModel() {
        return course;
    }
}

此時JSP中的 <s:textfield name="name"/> 將自動綁定到 course.name 屬性。

整個三層協作流程可通過以下mermaid流程圖表示:

sequenceDiagram
    participant Browser
    participant Action
    participant Service
    participant DAO
    participant DB

    Browser->>Action: 提交選課請求
    Action->>Service: 調用enrollStudent()
    Service->>DAO: 查詢課程容量
    DAO->>DB: 執行SQL
    DB-->>DAO: 返回結果
    DAO-->>Service: Course對象
    Service->>DAO: 更新課程+插入記錄
    DAO->>DB: 批量操作(事務內)
    DB-->>DAO: 提交成功
    DAO-->>Service: 操作完成
    Service-->>Action: 返回true
    Action-->>Browser: 跳轉成功頁

此分層結構不僅符合單一職責原則,也為後續引入緩存、異步處理等優化手段提供了良好擴展基礎。