知識庫 / Spring RSS 訂閱

Spring框架設計模式

Architecture,Spring
HongKong
4
01:04 PM · Dec 06 ,2025

1. 引言

設計模式是軟件開發中的一個重要組成部分。這些解決方案不僅能解決反覆出現的難題,還能幫助開發者通過識別常見模式來理解框架的設計。

在本教程中,我們將探討 Spring 框架中四種最常用的設計模式:

  1. 單例模式
  2. 工廠方法模式
  3. 代理模式
  4. 模板方法模式

我們還將探討 Spring 如何利用這些模式來減輕開發人員的負擔,並幫助用户快速完成繁瑣的任務。

2. 單例模式

單例模式是一種機制,確保應用程序中只有一個對象實例。 該模式在管理共享資源或提供橫向服務(如日誌記錄)時非常有用。

2.1. 單例 Bean

通常,單例在應用程序中是全局唯一的,但在 Spring 中,這個約束被放寬。相反,Spring 限制單例為一個 Spring IoC 容器中的一個對象。實際上,這意味着 Spring 只會為每個類型在應用程序上下文中創建單個 Bean。

Spring 的方法與嚴格的單例定義不同,因為應用程序可以有多個 Spring 容器。因此,在擁有多個容器的情況下,同一類對象可以存在於單個應用程序中。

 

 

默認情況下,Spring 會將所有 Bean 創建為單例。

2.2. 自動注入單例

例如,我們可以在一個應用程序上下文中創建兩個控制器,並將相同類型的 Bean 注入到每個控制器中。

首先,我們創建一個名為 BookRepository 的 Bean,用於管理我們的 Book 領域對象。

接下來,我們創建一個名為 LibraryController 的 Bean,它使用 BookRepository 返回圖書館中的書籍數量:

@RestController
public class LibraryController {
    
    @Autowired
    private BookRepository repository;

    @GetMapping("/count")
    public Long findCount() {
        System.out.println(repository);
        return repository.count();
    }
}

最後,我們創建一個BookController,它專注於Book 相關的操作,例如通過ID查找書籍:

@RestController
public class BookController {
     
    @Autowired
    private BookRepository repository;
 
    @GetMapping("/book/{id}")
    public Book findById(@PathVariable long id) {
        System.out.println(repository);
        return repository.findById(id).get();
    }
}

我們隨後啓動該應用程序,並對 /count/book/1 執行 GET 請求。

curl -X GET http://localhost:8080/count
curl -X GET http://localhost:8080/book/1

在應用程序輸出中,我們看到兩個 BookRepository 對象具有相同的對象 ID:

com.baeldung.spring.patterns.singleton.BookRepository@3ea9524f
com.baeldung.spring.patterns.singleton.BookRepository@3ea9524f

BookRepository 對象在LibraryControllerBookController 中的 ID 相同,這證明了 Spring 將同一個 Bean 注入到了這兩個控制器中。

我們可以通過將BookRepository Bean 的作用域從 singleton 更改為 prototype (使用 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 註解) 來創建單獨的實例。

這樣做會指示 Spring 為創建的每個BookRepository Bean 創建單獨的對象。因此,如果我們再次檢查每個控制器的BookRepository 的 ID,我們會發現它們不再相同。

3. 工廠方法模式

工廠方法模式包含一個工廠類和一個抽象方法,用於創建所需的對象。

通常,我們希望根據特定上下文創建不同的對象。

例如,我們的應用程序可能需要一個車輛對象。在航海環境中,我們想要創建船隻,但在航空環境中,我們想要創建飛機:

 

 

為了實現這一點,我們可以為每個所需的對象創建具體的工廠實現,並從具體的工廠方法中返回所需的對象。

3.1. 應用上下文

Spring 在其依賴注入 (DI) 框架的根部使用這種技術。

根本上,Spring 將 Bean 容器視為一個生產 Bean 的工廠。

因此,Spring 將 BeanFactory 接口定義為 Bean 容器的抽象:

public interface BeanFactory {

    getBean(Class<T> requiredType);
    getBean(Class<T> requiredType, Object... args);
    getBean(String name);

    // ...
]

每個 getBean 方法都被視為工廠方法,它會返回與方法中提供的條件相匹配的 Bean,例如 Bean 的類型和名稱。

Spring 通過擴展 BeanFactory 接口並引入額外的應用程序配置,實現了 ApplicationContext 接口。Spring 使用這些配置來基於某些外部配置(例如 XML 文件或 Java 註解)啓動 Bean 容器。

通過使用 ApplicationContext 類實現(例如 AnnotationConfigApplicationContext),我們可以通過 BeanFactory 接口中繼承的各種工廠方法創建 Bean。

首先,我們創建一個簡單的應用程序配置:

@Configuration
@ComponentScan(basePackageClasses = ApplicationConfig.class)
public class ApplicationConfig {
}

接下來,我們創建一個簡單的類,Foo,它不接受任何構造函數參數:

@Component
public class Foo {
}

然後創建一個名為 Bar 的類,該類接受一個構造函數參數:

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Bar {
 
    private String name;
     
    public Bar(String name) {
        this.name = name;
    }
     
    // Getter ...
}

最後,我們通過使用 AnnotationConfigApplicationContext 實現的 ApplicationContext 創建我們的 Bean:

@Test
public void whenGetSimpleBean_thenReturnConstructedBean() {
    
    ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
    
    Foo foo = context.getBean(Foo.class);
    
    assertNotNull(foo);
}

@Test
public void whenGetPrototypeBean_thenReturnConstructedBean() {
    
    String expectedName = "Some name";
    ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
    
    Bar bar = context.getBean(Bar.class, expectedName);
    
    assertNotNull(bar);
    assertThat(bar.getName(), is(expectedName));
}

使用getBean工廠方法,我們可以僅使用類類型和—在Bar的情況下—構造參數來創建配置好的Bean。

3.2. 外部配置

該模式具有很高的靈活性,因為我們可以完全根據外部配置來改變應用程序的行為。

如果希望更改應用程序中自動注入對象的實現,可以調整我們使用的ApplicationContext實現。

例如,可以將AnnotationConfigApplicationContext更改為基於XML的配置類,例如ClassPathXmlApplicationContext

@Test 
public void givenXmlConfiguration_whenGetPrototypeBean_thenReturnConstructedBean() { 

    String expectedName = "Some name";
    ApplicationContext context = new ClassPathXmlApplicationContext("context.xml");
 
    // Same test as before ...
}

4. 代理模式

代理是我們在數字世界中常用的工具,我們經常在軟件之外使用它們(例如網絡代理)。在代碼中,代理模式是一種技術,它允許一個對象——代理——控制對另一個對象——主題或服務的訪問

 

 

4.1. 交易 (Transactions)

為了創建代理,我們創建一個實現與我們的主題(subject)相同的接口,並且包含對主題的引用。

我們可以使用代理來代替主題。

在 Spring 中,Bean 被代理以控制對底層 Bean 的訪問。這在事務中使用時可見。

@Service
public class BookManager {
    
    @Autowired
    private BookRepository repository;

    @Transactional
    public Book create(String author) {
        System.out.println(repository.getClass().getName());
        return repository.create(author);
    }
}

在我們的 BookManager 類中,我們使用 @Transactional 註解標註了 create 方法。該註解指示 Spring 以原子方式執行我們的 create 方法。如果沒有代理,Spring 將無法控制對我們的 BookRepository bean 的訪問,並確保其事務一致性。

4.2. CGLib 代理

Spring 創建了一個代理,該代理包裹我們的 BookRepository 組件,並對我們的組件進行包裝以原子方式執行我們的 create 方法。

當我們調用我們的 BookManager#create 方法時,可以觀察到輸出:

com.baeldung.patterns.proxy.BookRepository$$EnhancerBySpringCGLIB$$3dc2b55c

通常,我們期望看到標準的 BookRepository 對象 ID;但實際上,我們看到的是 EnhancerBySpringCGLIB 對象 ID。

在幕後,Spring 將我們的 BookRepository 對象包裹為 EnhancerBySpringCGLIB 對象。 從而 Spring 控制對我們 BookRepository 對象的訪問(確保事務一致性)。

通常,Spring 使用兩種類型的代理:

  1. CGLib 代理 – 用於代理類
  2. JDK 動態代理 – 用於代理接口

雖然我們使用事務來暴露底層代理,但 Spring 將在必須控制對 Bean 訪問的任何場景中使用代理

5. 模板方法模式 (Template Method Pattern)

在許多框架中,大量的代碼通常是樣板代碼。

例如,在對數據庫執行查詢時,必須完成相同的步驟:

  1. 建立連接
  2. 執行查詢
  3. 執行清理
  4. 關閉連接

這些步驟是模板方法模式的理想應用場景。

5.1. 模板與回調

模板方法模式是一種定義執行某些操作所需步驟的技術,它會實現樣板步驟,並將可定製步驟留為抽象狀態。子類可以實現該抽象類併為缺失的步驟提供具體的實現。

在數據庫查詢的案例中,我們可以創建模板。

public abstract DatabaseQuery {

    public void execute() {
        Connection connection = createConnection();
        executeQuery(connection);
        closeConnection(connection);
    } 

    protected Connection createConnection() {
        // Connect to database...
    }

    protected void closeConnection(Connection connection) {
        // Close connection...
    }

    protected abstract void executeQuery(Connection connection);
}

或者,我們可以通過提供回調方法來補充缺失的步驟。

回調方法是指允許主體向客户端發出已完成所需操作的信號。

在某些情況下,主體可以使用此回調方法執行操作——例如,映射結果。

 

 

例如,與其使用 executeQuery 方法,我們可以向 execute 方法提供查詢字符串和用於處理結果的回調方法。

首先,我們創建一個回調方法,該方法接受一個 Results 對象並將其映射到類型 T 的對象:

public interface ResultsMapper<T> {
    public T map(Results results);
}

然後我們修改我們的 DatabaseQuery 類,以利用這個回調:

public abstract DatabaseQuery {

    public <T> T execute(String query, ResultsMapper<T> mapper) {
        Connection connection = createConnection();
        Results results = executeQuery(connection, query);
        closeConnection(connection);
        return mapper.map(results);
    ]

    protected Results executeQuery(Connection connection, String query) {
        // Perform query...
    }
}

此回調機制正是 Spring 使用 JdbcTemplate 類的精髓。

5.2. JdbcTemplate

JdbcTemplate 類提供 query 方法,該方法接受一個查詢 String 和一個 ResultSetExtractor 對象:

public class JdbcTemplate {

    public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
        // Execute query...
    }

    // Other methods...
}

ResultSetExtractorResultSet 對象 — 代表查詢結果 — 轉換為類型為 T 的領域對象:

@FunctionalInterface
public interface ResultSetExtractor<T> {
    T extractData(ResultSet rs) throws SQLException, DataAccessException;
}

Spring 通過創建更具體的回調接口,進一步減少樣板代碼。

例如,RowMapper 接口用於將單行 SQL 數據轉換為類型為 T 的領域對象。

@FunctionalInterface
public interface RowMapper<T> {
    T mapRow(ResultSet rs, int rowNum) throws SQLException;
}

為了將 RowMapper 接口適應為預期的 ResultSetExtractor,Spring 創建了 RowMapperResultSetExtractor 類:

public class JdbcTemplate {

    public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
        return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper)));
    }

    // Other methods...
}

與其提供將整個 ResultSet 對象中的邏輯,包括遍歷行,不如提供如何轉換單個行的邏輯:

public class BookRowMapper implements RowMapper<Book> {

    @Override
    public Book mapRow(ResultSet rs, int rowNum) throws SQLException {

        Book book = new Book();
        
        book.setId(rs.getLong("id"));
        book.setTitle(rs.getString("title"));
        book.setAuthor(rs.getString("author"));
        
        return book;
    }
}

使用這個轉換器,我們可以使用 JdbcTemplate 查詢數據庫,並映射每個結果行:

JdbcTemplate template = // create template...
template.query("SELECT * FROM books", new BookRowMapper());

除了 JDBC 數據庫管理之外,Spring 還使用模板來:

  • Java 消息服務 (JMS)
  • Java 持久性 API (JPA)
  • Hibernate (已棄用)
  • 事務

6. 結論

在本教程中,我們探討了在 Spring 框架中應用最常見的四種設計模式。

我們還研究了 Spring 如何利用這些模式,以提供豐富的功能,同時減輕開發人員的負擔。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.