【Spring Boot】第14課-自行控制元件的建立方式

在 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

上一篇:【Spring Boot】第13課-在 application.properties 配置檔提供參數

下一篇:【Spring Boot】第15課-元件的作用範圍(scope)

留言