1. 程式人生 > >Spring 5 設計模式 - Dependency Injection

Spring 5 設計模式 - Dependency Injection

Spring 5 設計模式 - Dependency Injection

依賴注入模式

Spring使用依賴注入,解決不同層的不同元件之間的組裝問題。
先看下面的類圖:
Direct Instantiation

TransferService有兩個成員變數,AccountRepository和TransferRepository。他們在TransferService的構造器中初始化。TransferService通過硬編碼方式,決定使用repositories的哪個實現:

public interface TransferService {
	void transferAmmount(Long a, Long b, Amount amount);
}

public class TransferServiceImpl implements TransferService {
    AccountRepository accountRepository;
TransferRepository transferRepository; public TransferServiceImpl(AccountRepository accountRepository, TransferRepository transferRepository) { super(); this.accountRepository = new JdbcAccountRepository(); this.transferRepository = new
JdbcTransferRepository(); } @Override public void transferAmmount(Long a, Long b, Amount amount) { Account accountA = accountRepository.findByAccountId(a); Account accountB = accountRepository.findByAccountId(b); transferRepository.transfer(accountA, accountB, amount); } }

這樣,TransferServiceImpl和JdbcAccountRepository、JdbcTransferRepository緊耦合在一起。如果你想把JDBC實現改為JPA實現,你需要修改TransferServiceImpl類。
根據SOLID(Single Responsibility-單一責任、Open Closed-開放封閉、Liskov’s Substitution-里氏替換、Interface Segregation-介面分離、Dependency Inversion-依賴倒置)原則,程式中的一個類只有一個責任。但是,前面的例子,TransferServiceImpl還承擔了JdbcAccountRepository和JdbcTransferRepository的構造責任。我們不應該在這個類裡直接例項化其他物件。
要避免在TransferServiceImpl類裡直接生成其他例項,可以使用工廠類增加它的例項。這樣,TransferServiceImpl最小化了對AccountRepository和TransferRepository的依賴-以前和實現緊耦合,現在只引用介面:
Factory of repositories

現在,TransferServiceImpl類和RepositoryFactory緊耦合了。如果有更多的依賴,這樣做就更不合適了-不論是增加工廠類還是工廠類的組合。
看一下使用了工廠的TransferServiceImpl類的程式碼:

public class TransferServiceImpl implements TransferService {
    AccountRepository accountRepository;
    TransferRepository transferRepository;
    
    public TransferServiceImpl() {
        this.accountRepository = RepositoryFactory.getAccountRepositoryInstance();
        this.transferRepository = RepositoryFactory.getTransferRepositoryInstance();
    }

    @Override
    public void transferAmount(Long a, Long b, Amount amount) {
        Account accountA = accountRepository.findByAccountId(a);
        Account accountB = accountRepository.findByAccountId(b);
        transferRepository.transfer(accountA, accountB, amount);
    }
}

依賴注入基於Inversion of Control模式。通過IoC,容器負責物件例項化,解決程式中類之間的依賴問題。
看下圖,我們使用依賴注入模式解決TransferServiceImpl類的依賴問題:
dependency injection

它的程式碼是這樣的:

public class TransferServiceImpl implements TransferService {
    AccountRepository accountRepository;
    TransferRepository transferRepository;
    public TransferServiceImpl(AccountRepository accountRepository,
                               TransferRepository transferRepository) {
        this.accountRepository = accountRepository;
        this.transferRepository = transferRepository;
    }
    @Override
    public void transferAmmount(Long a, Long b, Amount amount) {
        Account accountA = accountRepository.findByAccountId(a);
        Account accountB = accountRepository.findByAccountId(b);
        
        transferRepository.transfer(accountA, accountB, amount);
    }
}

這樣,TransferServiceImpl類的構造器傳入的是AccountRepository和TransferRepository介面的引用。TransferServiceImpl和repository的實現鬆耦合了(可以是JDBC實現,也可以是JPA實現),框架把類和他依賴的其他類連線起來。鬆耦合帶來了更好的可重用性、可維護性和可測試性。
Spring通過依賴注入模式解決類之間的依賴關係。Spring DI基於IoC-Spring有一個容器,用來增加、管理和銷燬物件。
依賴Spring容器的物件叫Spring bean。下來,我們看最常用的三種配置Spring容器的方法。

依賴注入模式的型別

  • Constructor-based
  • Setter-based

基於構造器的

構造器注入在物件例項化的時候設定物件的屬性。一個物件有public構造器,接受依賴的類當作構造器引數,並注入依賴。
你可以宣告多個構造器。
構造器注入容易帶來迴圈依賴(比如A依賴B,B也依賴A)。
前面的TransferServiceImpl類就是使用了構造器注入的方式。

repositories也可以由Spring容器管理,比如,把資料來源注入JdbcAccountRepository:

public class JdbcAccountRepository implements AccountRepository {
    JdbcTemplate jdbcTemplate;
    public JdbcAccountRepository(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
    // ...
}

基於Setter方法的

setter注入使用setter方法設定依賴。物件有public setter方法,接受依賴類作為方法引數注入依賴。此時,不需要構造器。

優點:

  • 可讀性更好
  • 解決了迴圈依賴問題
  • 允許比較費時的資源晚一點增加-在需要的時候增加
  • 不用修改構造器就可以修改依賴關係

缺點:

  • 不夠安全,因為setter方法可以被覆蓋
  • 程式碼結構不緊湊
  • 使用的時候要小心,因為它不是必須的依賴項(比如可以為空)

下面的TransferServiceImpl就使用了setter方法注入:

public class TransferServiceImpl implements TransferService {
    AccountRepository accountRepository;
    TransferRepository transferRepository;
    public void setAccountRepository(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }
    public void setTransferRepository(TransferRepository transferRepository) {
        this.transferRepository = transferRepository;
    }
    // ...
}

二者的比較

構造器注入 Setter注入
使用構造器接受引數;有時候很緊湊,知道自己增加了什麼 不知道屬性是否被初始化了
如果必須有這個依賴,是很好的選擇 適合用於不是必須的依賴
允許隱藏物件的屬性,確保不被修改 不能保證不可變性
迴圈依賴 解決迴圈依賴
不適合標量值依賴 如果有字串或者整數這樣的簡單的引數,適合使用setter注入

使用Spring配置依賴注入模式

主流的注入有Google Guice、Spring和Weld。
我們先看下圖,一個關於Spring如何工作的高階檢視:
How Spring works

Configuration Instruction是程式的元配置。在這裡,我們定義程式類的依賴,初始化Spring容器,解決依賴關係。
Spring容器在程式中增加beans,通過DI模式組裝它們。Spring容器基於配置增加bean。
主要有三種配置辦法:

  • Java-based:使用Java程式碼做明確地配置
  • Annotation-based:隱式的bean發現,和自動連線
  • XML-based:在XML檔案中明確地配置

Spring可以混合處理。

Java-based

Spring 3.0開始,支援Java-based的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();
    }
}


@Bean註解指示例項化、配置和初始化一個Spring IoC容器管理的物件。每個bean有唯一ID。預設地,bean的ID就是所註解的方法名。
可以這樣指定bean的ID:

@Bean(name="service")
public TransferService transferService(){
    return new TransferServiceImpl();
}

最好使用下面的方法:

    @Bean
    public TransferService transferService(AccountRepository accountRepository, TransferRepository transferRepository){
        return new TransferServiceImpl(accountRepository, transferRepository);
    }

這是因為,accountRepository()、transferRepository()可能和transferService()方法不在一個類中。

Annotation-based

什麼是Stereotype註解

Spring提供了一些特定的註解。這些註解被用來在程式上下文中自動增加Springbean。其中一個是@Component,通過使用這個註解,Spring提供了更多的Stereotype元註解,比如@Service,用來在Service層增加Spring bean;@Repository用來增加DAO層的bean;@Controller用來增加controller層的bean:
stereotype

通過使用這些註解,Spring有兩個辦法自動增加連線:

  • Component scanning:Spring自動在IoC容器中尋找bean
  • Autowiring:Spring自動在IoC容器中尋找bean依賴

看TransferService:

public interface TransferService {
    void transferAmmount(Long a, Long b, Amount amount);
}

@Service
public class TransferServiceImpl implements TransferService {
    @Override
    public void transferAmmount(Long a, Long b, Amount amount) {
        //business code here
    }
}

再看JdbcAccountRepository:

public interface AccountRepository {	
	Account findByAccountId(Long accountId);
}

@Repository
public class JdbcAccountRepository implements AccountRepository {

	@Override
	public Account findByAccountId(Long accountId) {
		return new Account(accountId, "Arnav Rajput", new Amount(3000.0));
	}

}

使用元件掃描搜尋bean

你需要開啟元件掃描功能,因為它沒有預設開啟。你不得不增加一個配置類,使用@Configuration和@ComponentScan註解。此類用於搜尋使用@Component註釋的類,並從中建立bean。

@Configuration
@ComponentScan
public class AppConfig {
}

可以指定包名

@Configuration
@ComponentScan("test")
public class AppConfig {
}

也可以指定多個包名

@Configuration
@ComponentScan(basePackages = {"test.repository.jdbc", "test..service"})
public class AppConfig {
}

也可以指定類名

@Configuration
@ComponentScan(basePackageClasses = {TransferService.class,AccountRepository.class})
public class AppConfig {
}

autowiring

@Service
public class TransferServiceImpl implements TransferService {
    AccountRepository accountRepository;
    TransferRepository transferRepository;
    @Autowired
    public TransferServiceImpl(AccountRepository accountRepository,
                               TransferRepository transferRepository) {
        super();
        this.accountRepository = accountRepository;
        this.transferRepository = transferRepository;
    }
    @Override
    public void transferAmmount(Long a, Long b, Amount amount) {
        Account accountA = accountRepository.findByAccountId(a);
        Account accountB = accountRepository.findByAccountId(b);
        transferRepository.transfer(accountA, accountB, amount);
    }
}

從Spring 4.3開始,如果類裡只有一個帶引數的構造器,可以不需要@Autowired註解。

@Autowired註解可用於構造器、setter方法、甚至屬性。

public class TransferServiceImpl implements TransferService {
    //...
    @Autowired
    public void setAccountRepository(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }
    @Autowired
    public void setTransferRepository(TransferRepository transferRepository) {
        this.transferRepository = transferRepository;
    }
    //...
}

或者

public class TransferServiceImpl implements TransferService {
    @Autowired
    AccountRepository accountRepository;
    @Autowired
    TransferRepository transferRepository;
    //...
}

預設地,@Autowired依賴是一個必須依賴-如果依賴不能解析,會拋異常。

消除歧義

如果有兩個AccountRepository:

@Repository
public class JdbcAccountRepository implements AccountRepository {
    //
}
@Repository
public class JpaAccountRepository implements AccountRepository {
    //
}

Spring容器會在啟動時拋異常:

At startup: NoSuchBeanDefinitionException, no unique bean of type [AccountRepository] is defined: expected single bean but found 2...

解決

@Service
public class TransferServiceImpl implements TransferService {
    @Autowired
    public TransferServiceImpl( @Qualifier("jdbcAccountRepository") AccountRepository accountRepository) {
        //
    }
}

@Repository("jdbcAccountRepository")
public class JdbcAccountRepository implements AccountRepository {
    //
}
@Repository("JpaAccountRepository")
public class JpaAccountRepository implements AccountRepository {
    //
}

使用抽象工廠解決

Spring提供了FactoryBean介面,實現抽象工廠模式。可以用它定製IoC容器的例項化邏輯。你可以實現該介面,實現了該介面的bwan是自定探測的。

public interface FactoryBean<T> {
    T getObject() throws Exception;
    Class<T> getObjectType();
    boolean isSingleton();
}

加入你有一個TransferService類:

public class TransferService {
    IAccountRepository accountRepository;
    public TransferService(IAccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }
    public void transfer(String accountA, String accountB, Double amount){
        System.out.println("Amount has been tranferred");
    }
}

然後實現FactoryBean:

public class AccountRepositoryFactoryBean implements FactoryBean<IAccountRepository> {
    @Override
    public IAccountRepository getObject() throws Exception {
        return new AccountRepository();
    }
    @Override
    public Class<?> getObjectType() {
        return IAccountRepository.class;
    }
    @Override
    public boolean isSingleton() {
        return false;
    }
}

這樣寫配置類:

@Configuration
public class AppConfig {
    public TransferService transferService() throws Exception {
        return new TransferService(accountRepository().getObject());
    }
    @Bean
    public AccountRepositoryFactoryBean accountRepository() {
        return new AccountRepositoryFactoryBean();
    }
}