在 Spring 的容器中存放著各種元件,它們彼此依賴。元件除了依賴其他元件,它們也可以擁有自己的資料成員。比方說在 Service 中建立 List、Map 或其它自己的物件,在業務邏輯中使用。
但我們可能會想在元件建立時,對這些資料成員做一些初始化。此時可選擇將 @PostConstruct 標記冠在用來初始化的方法上,以便在元件建立完成後自動執行。雖然很方便,但相對地也增加了元件的程式碼,甚至耦合度。
本文將介紹使用 @Bean 標記來對元件的建立做客製化。並延伸第13課的寄信程式,將元件的初始化過程從原類別抽離,配置出「Gmail」與「Yahoo Mail」的服務。
本文的練習用專案:
https://github.com/ntub46010/SpringBootTutorial/tree/Ch13-fin
一、元件建立方法
我們可以在類別簡單地加上 @Service、@Component 等標記,讓 Spring 建立出元件。本文要示範的則是自己寫一個方法,來實作建立元件的程式邏輯。
請找到 ProductService,宣告它的建構子。該 Service 還有一個成員,即 ProductRepository,請在建構子指派它的值。另外也一併移除 @Service 與 @Autowired 標記,因為這些元件將會用另一種方式被建立與注入。
public class ProductService {
private ProductRepository repository;
public ProductService(ProductRepository repository) {
this.repository = repository;
}
// 其餘略過
}
接著建立一個類別叫做 ServiceConfig,並加上 @Configuration 標記。習慣上,建立元件的方法會寫在像這樣的 Configuration 類別。下面是透過建構子建立 ProductService 元件的程式,是最基本的範例。
@Configuration
public class ServiceConfig {
@Bean
public ProductService productService(ProductRepository repository) {
return new ProductService(repository);
}
}
這邊在方法冠上 @Bean 標記,它會在 Spring 啟動時執行,並根據方法的參數,從容器中找到對應類別的元件傳入進來。接著根據程式邏輯,ProductRepository 被傳入建構子,於是 ProductService 元件就建立完成了。
並不是只有自己建立的類別才能利用這種元件建立方法。假如讀者想將其他函式庫中的某個類別建立為元件,也可利用這種做法。
二、初始化元件的資料成員
本文的前言提到,元件的資料成員可能會需要初始化。以第一節的做法來思考,我們可以在元件的建立方法中處理好,再傳入建構子。
在範例程式中的 MailService,可以看到 init 方法對 JavaMailSenderImpl 添加了一些參數。如果 MailService 可以直接拿到初始化完畢的 JavaMailSenderImpl,而不用自己做,這個類別的程式碼會更簡潔,也不必依賴 MailConfig 了。
本節將以 MailService 做為練習對象,在建立元件的過程中就完成需要的初始化。請開啟 MailService,如同改寫 ProductService 一樣,宣告出建構子並指派資料成員。別忘了移除 @Service 標記
public class MailService {
// 其餘略過
private final JavaMailSenderImpl mailSender;
public MailService(JavaMailSenderImpl mailSender) {
this.mailSender = mailSender;
}
public void sendMail(SendMailRequest request) {
// 其餘略過
try {
mailSender.send(message);
} catch (MailAuthenticationException e) {
LOGGER.error(e.getMessage());
} catch (Exception e) {
LOGGER.warn(e.getMessage());
}
}
}
接著打開 MailConfig,讓我們將 MailService 的元件建立方法寫在這裡。
@Bean
public MailService mailService() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(host);
mailSender.setPort(port);
mailSender.setUsername(username);
mailSender.setPassword(password);
Properties props = mailSender.getJavaMailProperties();
props.put("mail.smtp.auth", authEnabled);
props.put("mail.smtp.starttls.enable", starttlsEnabled);
props.put("mail.transport.protocol", protocol);
return new MailService(mailSender);
}
由於各個參數已經在此處用來初始化 JavaMailSenderImpl,因此 MailService 不必再從 MailConfig 取值了。
三、建立多個同類別元件
假設想在專案同時備有多個寄信服務,讀者可宣告多個建立 MailService 的方法。本文採用「Gmail」與「Yahoo Mail」,請修改 mail.properties 配置檔,分別準備這兩種服務的主機、帳密等環境參數。
mail.auth.enabled=true
mail.starttls.enabled=true
mail.protocol=smtp
mail.gmail.host=smtp.gmail.com
mail.gmail.port=587
mail.gmail.username=your_gmail_account@gmail.com
mail.gmail.password=your_gmail_password
mail.yahoo.host=smtp.mail.yahoo.com
mail.yahoo.port=587
mail.yahoo.username=your_yahoo_account@yahoo.com.tw
mail.yahoo.password=your_yahoo_password
準備好後,也要改寫 MailConfig,才能讀取這些參數。其中 host、port、username 與 password 的參數需要根據 Gmail 和 Yahoo Mail 分開宣告。
@Configuration
@PropertySource("classpath:mail.properties")
public class MailConfig {
// 其餘略過
@Value("${mail.gmail.host}")
private String gmailHost;
@Value("${mail.gmail.port}")
private int gmailPort;
@Value("${mail.gmail.username}")
private String gmailUsername;
@Value("${mail.gmail.password}")
private String gmailPassword;
@Value("${mail.yahoo.host}")
private String yahooHost;
@Value("${mail.yahoo.port}")
private int yahooPort;
@Value("${mail.yahoo.username}")
private String yahooUsername;
@Value("${mail.yahoo.password}")
private String yahooPassword;
// 其餘略過
}
最後,讀者可直接複製上一節寫好建立 MailService 的程式碼,分別將參數換成 Gmail 和 Yahoo Mail 的即可。
@Bean
public MailService gmailMailService() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(gmailHost);
mailSender.setPort(gmailPort);
// 其餘略過
return new MailService(mailSender);
}
@Bean
public MailService yahooMailService() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(yahooHost);
mailSender.setPort(yahooPort);
// 其餘略過
return new MailService(mailSender);
}
此時啟動 Spring,會發現失敗了。這是因為專案中有兩個冠有 @Bean 標記的方法是用來建立 MailService,意味著我們 new 了兩個 MailService 到元件容器中。而 MailController 依賴了 MailService,Spring 不知道要注入哪一個。
因應的方式是使用元件的「名稱」來識別它們,並在注入的地方指定其中一個。元件名稱是在 @Bean 標記中定義,若不定義,預設為方法名稱。
public static final String GMAIL_SERVICE = "gmailService";
public static final String YAHOO_MAIL_SERVICE = "yahooMailService";
@Bean(name = GMAIL_SERVICE)
public MailService gmailService() throws Exception {
// 其餘略過
}
@Bean(name = YAHOO_MAIL_SERVICE)
public MailService yahooMailService() throws Exception {
// 其餘略過
}
接著在注入的地方加上 @Qualifier 標記,並傳入元件名稱即可順利注入。
@RestController
@RequestMapping(value = "/mail", produces = MediaType.APPLICATION_JSON_VALUE)
public class MailController {
@Autowired
@Qualifier(MailConfig.GMAIL_SERVICE)
private MailService mailService;
// 其餘略過
}
附帶一提,若要在 @Service、@Component 等標記定義元件名稱,直接傳入字串參數即可,如下。
@Service("gmailService")
四、用環境參數配置元件
現在專案中具有 Gmail 與 Yahoo Mail 的寄信服務,並且能用 @Qualifier 標記指定要用注入哪一種。然而考慮到第13課第五節所提到的,假設有不同伺服器要使用不同服務的情況,那麼藉由 @Qualifier 標記來指定,相當於寫死。意即啟動程式時,無法用外部的配置檔控制要用何種寄信服務。
為了保持彈性,本節我們來做一些調整。請在 mail.properties 配置檔添加一個參數來指定服務,並在 MailConfig 讀取它。
# gmail or yahoo
mail.platform=gmail
接著在 MailConfig 中改寫建立 MailService 元件的方法。根據環境參數的值,對 JavaMailSenderImpl 設定不同的參數。以下範例的程式邏輯為,若參數 platform 參數的值是「gmail」,就設定 Gmail 相關的參數,否則設定 Yahoo Mail 的。
@Configuration
@PropertySource("classpath:mail.properties")
public class MailConfig {
@Value("${mail.platform}")
private String platform;
// 其餘略過
@Bean
public MailService mailService() {
JavaMailSenderImpl mailSender = "gmail".equals(platform)
? gmailSender()
: yahooSender();
Properties props = mailSender.getJavaMailProperties();
// 其餘略過
return new MailService(mailSender);
}
private JavaMailSenderImpl gmailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(gmailHost);
// 其餘略過
return mailSender;
}
private JavaMailSenderImpl yahooSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(yahooHost);
// 其餘略過
return mailSender;
}
}
由於已經調整成根據 platform 參數來控制要建立哪一種 MailService,因此可以從 MailController 注入 MailService 的地方,移除 @Qualifier 標記。
雖然將 @Qualifier 標記移除,但不代表這個標記不好。如果所注入的元件,是以介面或抽象類別的型態宣告,而它有多個實作類別,此時同樣需要用該標記來指定。
本文的完成範例:
https://github.com/ntub46010/SpringBootTutorial/tree/Ch14
留言
張貼留言