Micronaut 環境指南
1. 概述
在 Micronaut 中,與其他 Java 框架類似, Environment
介面是與設定檔相關的抽象。設定檔是一個我們可以將其視為容器的概念,它保存特定於它們的屬性和 bean。
通常,設定檔與.yaml
環境相關,例如 local- .properties
、docker-profile、k8s-profile 等。雲端等上執行我們的應用程式。
在本教程中,我們將介紹 Micronaut 中的Environment
抽象,並將看到正確設定它的不同方法。最後,我們將學習如何使用特定於環境的屬性和 bean,以及如何使用環境來應用不同的實作。
2. Micronaut 環境與 Spring 設定文件
如果我們熟悉 Spring 設定文件,那麼理解 Micronaut 環境就很容易了。有很多相似之處,但也有一些關鍵的區別。
使用 Micronaut 環境,我們可以以與 Spring 中類似的方式設定屬性。這意味著我們可以:
- 使用
@ConfigurationProperties
註解的屬性文件 - 使用
@Value
註解將特定屬性注入到類別中 - 透過注入整個
Environment
實例來向類別注入特定屬性,然後使用getProperty()
方法
Spring 和Micronaut 之間的一個令人困惑的區別是,儘管兩者都允許多個活動環境/配置文件,但在Micronaut 中通常會看到許多活動環境,而在Spring 配置文件中我們很少看到多個活動設定檔。這會導致對許多活動環境中指定的屬性或 bean 產生一些混淆。為了克服這個問題,我們可以設定環境優先事項。稍後會詳細介紹。
另一個值得注意的區別是 Micronaut 提供了完全停用環境的選項。這與 Spring 設定檔無關,因為當不設定活動設定檔時,通常會使用預設值。相較之下,Micronaut 可能具有由所使用的不同框架或工具設定的不同活動環境。例如:
- JUnit 在活動環境中新增了「測試」環境
- Cucumber 新增了「cucumber」環境
- OCI 可能會新增「雲端」和/或「k8s」等。
為了停用環境,我們可以使用java -Dmicronaut.env.deduction=false -jar myapp.jar.
3. 設定 Micronaut 環境
設定 Micronaut 環境的方法有多種。最常見的是:
- 使用
micronaut.environments
參數:java -Dmicronaut.environments=cloud,production -jar myapp.jar.
- 在
main()
中使用defaultEnvironment()
方法:Micronaut.build(args).defaultEnvironments('local').mainClass(MicronautServiceApi.class).start();.
- 將
MICRONAUT_ENV,
中的值設定為環境變數。 - 正如我們之前提到的,有時會扣除環境,也就是從後台框架(如 JUnit 和 Cucumber)設定的環境。
我們決定設定環境的方式沒有最佳實踐。我們可以選擇最適合我們需求的一種。
4. Micronaut 環境優先順序和解決方案
由於允許多個活動的 Micronaut 環境,因此在某些情況下,可能在多個環境中明確定義了某個屬性或 bean,或沒有在其中明確定義。這會導致衝突,有時還會導致運行時異常。處理屬性和 bean 的優先權和解析度的方式是不同的。
4.1.特性
當一個屬性存在於多個活動屬性來源中時,環境順序決定它要取得哪個值。從最低到最高的層次結構是:
- 從其他工具/框架推導出環境
-
micronaut.environments
參數中設定的環境 -
MICRONAUT_ENV
環境變數中設定的環境 - Micronaut 建構器中載入的環境
假設我們有一個屬性service.test.property
,並且我們希望在不同的環境中為它設定不同的值。我們在application-dev.yml
和application-test.yml
檔案中設定不同的值:
@Test
public void whenEnvironmentIsNotSet_thenTestPropertyGetsValueFromDeductedEnvironment() {
ApplicationContext applicationContext = Micronaut.run(ServerApplication.class);
applicationContext.start();
assertThat(applicationContext.getEnvironment()
.getActiveNames()).containsExactly("test");
assertThat(applicationContext.getProperty("service.test.property", String.class)).isNotEmpty();
assertThat(applicationContext.getProperty("service.test.property", String.class)
.get()).isEqualTo("something-in-test");
}
@Test
public void whenEnvironmentIsSetToBothProductionAndDev_thenTestPropertyGetsValueBasedOnPriority() {
ApplicationContext applicationContext = ApplicationContext.builder("dev", "production").build();
applicationContext.start();
assertThat(applicationContext.getEnvironment()
.getActiveNames()).containsExactly("test", "dev", "production");
assertThat(applicationContext.getProperty("service.test.property", String.class)).isNotEmpty();
assertThat(applicationContext.getProperty("service.test.property", String.class)
.get()).isEqualTo("something-in-dev");
}
在第一個測試中,我們沒有設定任何活動環境,但有JUnit的扣除test
。在本例中,該屬性從application-test.yml
取得其值。但在第二個範例中,我們在具有更高順序的ApplicationContext,
中設定了dev
環境。在這種情況下,該屬性從application-dev.yml
取得其值。
但是,如果我們嘗試注入任何活動環境中都不存在的屬性,我們將收到運行時錯誤DependencyInjectionException,
因為缺少屬性:
@Test
public void whenEnvironmentIsSetToBothProductionAndDev_thenMissingPropertyIsEmpty() {
ApplicationContext applicationContext = ApplicationContext.builder("dev", "production")
.build();
applicationContext.start();
assertThat(applicationContext.getEnvironment()
.getActiveNames()).containsExactly("test", "dev", "production");
assertThat(applicationContext.getProperty("service.dummy.property", String.class)).isEmpty();
}
在此範例中,我們嘗試直接從 ApplicationContext 檢索遺失的屬性system.dummy.property
ApplicationContext.
這將傳回一個空的Optional.
如果該屬性被注入到某個 bean 中,則會導致執行時期異常。
4.2.豆子
對於特定環境的 bean,事情會稍微複雜一些。假設我們有一個EventSourcingService
接口,它有一個方法sendEvent()
(它應該是void
,但我們出於演示目的返回String
):
public interface EventSourcingService {
String sendEvent(String event);
}
此介面只有兩種實現,一種用於環境dev
,一種用於production
:
@Singleton
@Requires(env = Environment.DEVELOPMENT)
public class VoidEventSourcingService implements EventSourcingService {
@Override
public String sendEvent(String event) {
return "void service. [" + event + "] was not sent";
}
}
@Singleton
@Requires(env = "production")
public class KafkaEventSourcingService implements EventSourcingService {
@Override
public String sendEvent(String event) {
return "using kafka to send message: [" + event + "]";
}
}
@Requires
註解通知框架此實作僅在一個或多個指定環境處於活動狀態時才有效。否則,這個 bean 永遠不會被創建。
我們可以假設VoidEventSourcingService
不執行任何操作,只回傳一個String
,因為我們可能不想在開發環境上發送事件。 KafkaEventSourcingService
實際上在 Kafka 上傳送事件,然後傳回一個String
。
現在,如果我們忘記在活動環境中設定其中一個,會發生什麼?在這種情況下,我們將傳回一個NoSuchBeanException
異常:
public class InvalidEnvironmentEventSourcingUnitTest {
@Test
public void whenEnvironmentIsNotSet_thenEventSourcingServiceBeanIsNotCreated() {
ApplicationContext applicationContext = Micronaut.run(ServerApplication.class);
applicationContext.start();
assertThat(applicationContext.getEnvironment().getActiveNames()).containsExactly("test");
assertThatThrownBy(() -> applicationContext.getBean(EventSourcingService.class))
.isInstanceOf(NoSuchBeanException.class)
.hasMessageContaining("None of the required environments [production] are active: [test]");
}
}
在本次測試中,我們沒有設定任何活動環境。首先,我們斷言唯一的活動環境是test
,這是透過使用 JUnit 框架推導出來的。然後我們斷言,如果我們嘗試取得EventSourcingService
實作的 bean,我們實際上會得到一個異常,並顯示一個錯誤,表示所需的環境都不處於活動狀態。
相反,如果我們設定兩個環境,我們會再次收到錯誤,因為介面的兩個實作不能同時存在:
public class MultipleEnvironmentsEventSourcingUnitTest {
@Test
public void whenEnvironmentIsSetToBothProductionAndDev_thenEventSourcingServiceBeanHasConflict() {
ApplicationContext applicationContext = ApplicationContext.builder("dev", "production").build();
applicationContext.start();
assertThat(applicationContext.getEnvironment()
.getActiveNames()).containsExactly("test", "dev", "production");
assertThatThrownBy(() -> applicationContext.getBean(EventSourcingService.class))
.isInstanceOf(NonUniqueBeanException.class)
.hasMessageContaining("Multiple possible bean candidates found: [VoidEventSourcingService, KafkaEventSourcingService]");
}
}
這不是錯誤或錯誤的編碼。這可能是現實生活中的場景,當我們忘記設定正確的環境時,我們可能會想要發生故障。但是,如果我們想確保在這種情況下永遠不會出現運行時錯誤,我們可以透過不新增@Requires
註解來設定預設實作。對於我們想要覆蓋預設值的環境,我們應該加入@Requires
和@Replaces
註解:
public interface LoggingService {
// methods omitted
}
@Singleton
@Requires(env = { "production", "canary-production" })
@Replaces(LoggingService.class)
public class FileLoggingServiceImpl implements LoggingService {
// implementation of the methods omitted
}
@Singleton
public class ConsoleLoggingServiceImpl implements LoggingService {
// implementation of methods omitted
}
LoggingService
介面定義了一些方法。預設實作是ConsoleLoggingServiceImpl
,它適用於所有環境。 FileLoggingServiceImpl
類別會覆寫production
和canary-production
環境中的預設實作。
5. 在實務上使用 Micronaut 環境
除了特定於環境的屬性和 bean 之外,我們還可以在更多情況下使用環境。透過注入Environment
變數並使用getActiveNames()
方法,我們可以在程式碼中檢查活動環境是什麼並更改一些實作細節:
if (environment.getActiveNames().contains("production")
|| environment.getActiveNames().contains("canary-production")) {
sendAuditEvent();
}
此程式碼片段檢查環境是production
環境還是canary-production
,並僅在這兩個環境中呼叫sendAuditEvent()
方法。這當然是個不好的做法。相反,我們應該使用策略設計模式或特定的 bean,如前所述。
但我們仍然有選擇。一些更常見的場景是在測試中使用此程式碼,因為我們的測試程式碼有時更簡單而不是更清晰:
if (environment.getActiveNames().contains("cloud")) {
assertEquals(400, response.getStatusCode());
} else {
assertEquals(500, response.getStatusCode());
}
這是一個測試片段,可能會在本機環境中收到500
狀態回應,因為服務未處理某些錯誤請求。另一方面,在已部署的環境中會給予400
狀態回應,因為 API 閘道會在請求傳送到服務之前回應。
六、結論
在本文中,我們了解了 Micronaut 環境。我們介紹了主要概念以及與 Spring 設定檔的相似之處,並列出了設定活動環境的不同方法。然後,我們了解了在設定了多個環境或未設定多個環境的情況下如何解析特定於環境的 bean 和屬性。最後,我們討論瞭如何直接在程式碼中使用環境,這通常不是一個好的做法。
與往常一樣,所有原始程式碼都可以在 GitHub 上取得。