1. 程式人生 > >Spring 5 設計模式 - 開始

Spring 5 設計模式 - 開始

Spring 5 設計模式 - 開始

依賴注入

物件之間的依賴增加複雜性,導致物件之間緊耦合。
比如下面的TransferService元件依賴其他兩個元件:TransferRepository和AccountRepository:
TransferService

如果直接使用例項:

public class TransferService {
    private AccountRepository accountRepository;
    
    public
TransferService () { this.accountRepository = new AccountRepository(); } public void transferMoney(Account a, Account b) { accountRepository.transfer(a, b); } }

TransferService物件需要一個AccountRepository物件,實現賬戶A到賬戶B的轉賬功能。於是,直接增加AccountRepository的例項。這樣很難維護,也很難做TransferService的單元測試。

也可以使用工廠模式實現該功能:

public class TransferService {
    private AccountRepository accountRepository;
    
    public TransferService() {
        this.accountRepository = AccountRepositoryFactory.getInstance("jdbc");
    }
    
    public void transferMoney(Account a, Account b) {
        accountRepository.
transfer(a, b); } }

上面的程式碼,我們使用工廠模式增加AccountRepository類的物件。更好的做法是program-to-interface,針對這個具體問題,我們可以增加一個介面,降低耦合:

public interface AccountRepository {
    void transfer();
    //other methods
}

我們可以增加JdbcAccountRepositry類,實現AccountRepository介面:

public class JdbcAccountRepositry implements AccountRepositry{
    //實現AccountRepositry定義的方法
    // 實現其他方法
}

這樣,由工廠類增加的物件很容易維護。使用工廠類,也可以增加可配置的物件。但是,為了降低耦合,獲取協作元件的時候在業務元件裡增加了工廠類。讓我們看看依賴注入怎麼解決這個問題。

對於依賴注入模式,我們只是定義依賴性,而不用解決物件之間的依賴。看下圖,我們會在需要物件的時候注入他們:

Dependency injection

TransferService依賴AccountRepository和TransferRepository。TransferService可以使用多種TransferRepository完成轉賬功能,比如可以使用JdbcTransferRepository或者是JpaTransferRepository,依賴哪個,由部署環境決定。

public class TransferServiceImpl implements TransferService {
    private TransferRepository transferRepository;
    private AccountRepository accountRepository;
    
    public TransferServiceImpl(TransferRepository transferRepository, AccountRepository accountRepository) {
        this.transferRepository = transferRepository;//TransferRepository is injected
        this.accountRepository = accountRepository;//AccountRepository is injected
    }
    public void transferMoney(Long a, Long b, Amount amount) {
        Account accountA = accountRepository.findByAccountId(a);
        Account accountB = accountRepository.findByAccountId(b);
        transferRepository.transfer(accountA, accountB, amount);
    }
}

TransferServiceImpl沒有增加自己的repositories實現,而是在構造器裡通過構造引數傳遞的。這叫構造器注入。這樣,TransferServiceImpl沒有和任何repositories的實現相耦合,可以根據需要切換不同的實現。
Spring提供了把程式的各個部件組裝成程式的支援:

  • 部件不用擔心找不到對方
  • 任何部件都可以很容易地被替換

配置程式碼可以是這的:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {
    @Bean
    public TransferService transferService(){
        return new TransferServiceImpl(accountRepository(), transferRepository());
    }
    @Bean
    public AccountRepository accountRepository() {
        return new JdbcAccountRepository();
    }
    @Bean
    public TransferRepository transferRepository() {
        return new JdbcTransferRepository();
    }
}

Spring程式,使用程式上下文的一個實現載入bean定義,放進Spring容器。比如下面的程式碼,就使用AnnotationConfigApplicationContext載入配置類AppConfig,得到AccountService的一個物件:

import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class TransferMain {
    public static void main(String[] args) {
        //載入上下文
        ConfigurableApplicationContext applicationContext =
                new AnnotationConfigApplicationContext(AppConfig.class);
        //得到 TransferService bean
        TransferService transferService =
                applicationContext.getBean(TransferService.class);
        //轉賬
        transferService.transferAmmount(100l, 200l, new Amount(2000.0));
        applicationContext.close();
    }
}

AOP

依賴注入讓元件之間鬆耦合,面向切面程式設計(Spring AOP)可以捕獲程式中重複出現的常用功能。能以最優雅的方式,把下列功能分開。可以通過宣告的方式,透明地應用這些服務:

  • 日誌和跟蹤
  • 事務管理
  • 安全
  • 快取
  • 錯誤處理
  • 效能監視
  • 自定義業務規則

列表中的元件不是你核心程式的一部分,但是他們是額外的責任,通常稱為cross-cutting concerns,在核心責任之上跨多個元件。如果你把這些元件放到你的核心功能,而沒有模組化,會導致兩個主要問題:

  • Code tangling:比如安全、事務、日誌等程式碼和業務邏輯耦合在一起
  • Code scattering:相同的concern散佈在模組之間
    Cross-cutting concerns

Spring AOP可以把cross-cutting concern模組化。Spring AOP是通過代理模式實現的:

  • 實現程式邏輯:你實現業務邏輯的時候,不需要擔心附加功能,比如日誌、安全等。
  • 寫aspects,實現cross-cutting concerns
  • 在程式裡Weave aspects:把cross-cutting行為放到正確的位置,可以通過宣告的方式注入

AOP-based

比如,使用LoggingAspect實現日誌功能:

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class LoggingAspect {
    @Before("execution(* *.transferAmount(..))")
    public void logBeforeTransfer(){
        System.out.println("####LoggingAspect.logBeforeTransfer() method called before transfer amount####");
    }
    @After("execution(* *.transferAmount(..))")
    public void logAfterTransfer(){
        System.out.println("####LoggingAspect.logAfterTransfer() method called after transfer amount####");
    }
}

修改AppConfig:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
    @Bean
    public TransferService transferService(){
        return new TransferServiceImpl(accountRepository(), transferRepository());
    }
    @Bean
    public AccountRepository accountRepository() {
        return new JdbcAccountRepository();
    }
    @Bean
    public TransferRepository transferRepository() {
        return new JdbcTransferRepository();
    }
    @Bean
    public LoggingAspect loggingAspect() {
        return new LoggingAspect();
    }
}

template

在企業程式裡,經常會看到很多相似的程式碼。比如使用JDBC,經常會寫相同的程式碼,處理下列問題:

  • 從連線池獲取連線
  • 增加PreparedStatement物件
  • 繫結SQL引數
  • 執行PreparedStatement物件
  • 從ResultSet物件檢索資料,填充資料容器
  • 釋放資料庫資源

比如getAccountById方法的程式碼可能是這樣的:

    public Account getAccountById(long id) {
        Connection conn = null;
        PreparedStatement stmt = null;
        ResultSet rs = null;
        try {
            conn = dataSource.getConnection();
            stmt = conn.prepareStatement(
                    "select id, name, amount from " +
                            "account where id=?");
            stmt.setLong(1, id);
            rs = stmt.executeQuery();
            Account account = null;
            if (rs.next()) {
                account = new Account();
                account.setId(rs.getLong("id"));
                account.setName(rs.getString("name"));
                account.setAmount(rs.getString("amount"));
            }
            return account;
        } catch (SQLException e) {
        } finally {
            if(rs != null) {
                try {
                    rs.close();
                } catch(SQLException e) {}
            }
            if(stmt != null) {
                try {
                    stmt.close();
                } catch(SQLException e) {}
            }
            if(conn != null) {
                try {
                    conn.close();
                } catch(SQLException e) {}
            }
        }
        return null;
    }

Spring JDBC使用模板設計模式,讓資料訪問程式碼更簡潔,也能防止連線洩漏:

    public Account getAccountById(long id) {
        return jdbcTemplate.queryForObject(
                "select id, name, amoount" +
                        "from account where id=?",
                new RowMapper<Account>() {
                    public Account mapRow(ResultSet rs, int rowNum) throws SQLException {
                        account = new Account();
                        account.setId(rs.getLong("id"));
                        account.setName(rs.getString("name"));
                        account.setAmount(rs.getString("amount"));
                        return account;
                    }
                },
                id);
    }

Spring容器

有兩種不同的Spring容器:

  • Bean factory:物件池,通過配置建立和管理物件
  • Application contexts:bean工廠的簡單包裝,還提供了額外的程式上下文

bean的生命週期

life cycle

  • 載入所有的bean定義,增加有序圖(ordered graph)
  • 例項化,執行BeanFactoryPostProcessors(可以在這裡修改bean定義)
  • 例項化每個bean
  • 把值和bean引用注入bean的屬性
  • 如果bean實現了BeanNameAware介面,就呼叫setBeanName()方法設定bean的ID
  • 如果bean實現了BeanFactoryAware,就呼叫setBeanFactory()方法設定bean工廠的引用
  • 如果bean實現了ApplicationContextAware,就呼叫setApplicationContext()方法設定程式上下文的引用
  • 如果bean實現了BeanPostProcessor,呼叫postProcessBeforeInitialization()修改bean的例項
  • 如果bean實現了InitializingBean,呼叫afterPropertiesSet()方法做初始化或者載入資源。也可以用其他方法實現這一步,比如@PostConstruct註解
  • 如果bean實現了BeanPostProcessor,呼叫postProcessAfterInitialization(),在初始化之後修改bean。此時,bean已經可以使用,你的程式可以通過使用getBean()訪問這個bean。呼叫程式上下文的close()之前,bean一直活著
  • 如果bean實現了DisposibleBean,Spring呼叫它的destroy()方法執行銷燬過程,釋放資源。也可以用其他方法實現這一步,@PreDestroy