Spring 5 設計模式 - Dependency Injection
Spring 5 設計模式 - Dependency Injection
依賴注入模式
Spring使用依賴注入,解決不同層的不同元件之間的組裝問題。
先看下面的類圖:
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的依賴-以前和實現緊耦合,現在只引用介面:
現在,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類的依賴問題:
它的程式碼是這樣的:
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如何工作的高階檢視:
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:
通過使用這些註解,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();
}
}