函式式 Bean 定義

Spring Cloud Function 支援一種“函式式”風格的 bean 宣告,適用於需要快速啟動的小型應用。函式式 bean 宣告風格是 Spring Framework 5.0 的一項特性,並在 5.1 版本中進行了顯著增強。

比較函式式與傳統 Bean 定義

這是一個使用熟悉的 @Configuration@Bean 宣告風格的普通 Spring Cloud Function 應用

@SpringBootApplication
public class DemoApplication {

  @Bean
  public Function<String, String> uppercase() {
    return value -> value.toUpperCase();
  }

  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }

}

現在是函式式 bean:使用者應用程式碼可以重寫成“函式式”形式,如下所示

@SpringBootConfiguration
public class DemoApplication implements ApplicationContextInitializer<GenericApplicationContext> {

  public static void main(String[] args) {
    FunctionalSpringApplication.run(DemoApplication.class, args);
  }

  public Function<String, String> uppercase() {
    return value -> value.toUpperCase();
  }

  @Override
  public void initialize(GenericApplicationContext context) {
    context.registerBean("demo", FunctionRegistration.class,
        () -> new FunctionRegistration<>(uppercase())
            .type(FunctionTypeUtils.functionType(String.class, String.class)));
  }

}

主要區別在於

  • 主類是一個 ApplicationContextInitializer

  • @Bean 方法已轉換為呼叫 context.registerBean()

  • @SpringBootApplication 已替換為 @SpringBootConfiguration,以表示我們沒有啟用 Spring Boot 自動配置,但仍將該類標記為“入口點”。

  • Spring Boot 的 SpringApplication 已替換為 Spring Cloud Function 的 FunctionalSpringApplication(它是其子類)。

在 Spring Cloud Function 應用中註冊的業務邏輯 bean 型別是 FunctionRegistration。這是一個包含函式及其輸入和輸出型別資訊的包裝器。在 @Bean 形式的應用中,這些資訊可以透過反射推匯出來,但在函式式 bean 註冊中,如果不使用 FunctionRegistration,部分資訊會丟失。

除了使用 ApplicationContextInitializerFunctionRegistration 外,另一種方法是讓應用程式本身實現 Function(或 ConsumerSupplier)。例如(等同於上面)

@SpringBootConfiguration
public class DemoApplication implements Function<String, String> {

  public static void main(String[] args) {
    FunctionalSpringApplication.run(DemoApplication.class, args);
  }

  @Override
  public String apply(String value) {
    return value.toUpperCase();
  }

}

如果新增一個獨立的 Function 型別類,並使用 SpringApplication 的另一種 run() 方法形式註冊它,這也將奏效。關鍵是泛型型別資訊必須在執行時透過類宣告可用。

假設你有

@Component
public class CustomFunction implements Function<Flux<Foo>, Flux<Bar>> {
	@Override
	public Flux<Bar> apply(Flux<Foo> flux) {
		return flux.map(foo -> new Bar("This is a Bar object from Foo value: " + foo.getValue()));
	}

}

你將其註冊為

@Override
public void initialize(GenericApplicationContext context) {
		context.registerBean("function", FunctionRegistration.class,
				() -> new FunctionRegistration<>(new CustomFunction()).type(CustomFunction.class));
}

函式式 Bean 宣告的侷限性

大多數 Spring Cloud Function 應用的範圍相對較小,與整個 Spring Boot 相比,因此我們可以輕鬆地將其適配到這些函式式 bean 定義。如果超出這個有限範圍,可以透過切換回 @Bean 風格的配置,或採用混合方法來擴充套件 Spring Cloud Function 應用。例如,如果想利用 Spring Boot 自動配置與外部資料儲存整合,則需要使用 @EnableAutoConfiguration。如果願意,你的函式仍然可以使用函式式宣告定義(即“混合”風格),但在這種情況下,需要透過設定 spring.functional.enabled=false 顯式關閉“完全函式式模式”,以便 Spring Boot 可以重新獲得控制權。

函式視覺化與控制

Spring Cloud Function 透過 Actuator 端點和程式設計方式支援對 FunctionCatalog 中可用函式的視覺化。

程式設計方式

要以程式設計方式檢視應用上下文中可用的函式,只需要訪問 FunctionCatalog。在那裡可以找到獲取目錄大小、查詢函式以及列出所有可用函式名稱的方法。

例如,

FunctionCatalog functionCatalog = context.getBean(FunctionCatalog.class);
int size = functionCatalog.size(); // will tell you how many functions available in catalog
Set<String> names = functionCatalog.getNames(null); will list the names of all the Function, Suppliers and Consumers available in catalog
. . .

Actuator

由於 actuator 和 web 是可選的,必須首先手動新增其中一個 web 依賴以及 actuator 依賴。以下示例展示瞭如何新增 Web 框架的依賴

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
</dependency>

以下示例展示瞭如何新增 WebFlux 框架的依賴

<dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

可以按如下方式新增 Actuator 依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

還必須透過設定以下屬性來啟用 functions actuator 端點:--management.endpoints.web.exposure.include=functions

訪問以下 URL 檢視 FunctionCatalog 中的函式:<host>:<port>/actuator/functions

例如,

curl https://:8080/actuator/functions

輸出應如下所示

{"charCounter":
	{"type":"FUNCTION","input-type":"string","output-type":"integer"},
 "logger":
 	{"type":"CONSUMER","input-type":"string"},
 "functionRouter":
 	{"type":"FUNCTION","input-type":"object","output-type":"object"},
 "words":
 	{"type":"SUPPLIER","output-type":"string"}. . .

測試函式式應用

Spring Cloud Function 還提供了一些整合測試工具,Spring Boot 使用者會非常熟悉。

假設這是你的應用程式

@SpringBootApplication
public class SampleFunctionApplication {

    public static void main(String[] args) {
        SpringApplication.run(SampleFunctionApplication.class, args);
    }

    @Bean
    public Function<String, String> uppercase() {
        return v -> v.toUpperCase();
    }
}

這是一個包裝此應用程式的 HTTP 伺服器的整合測試

@SpringBootTest(classes = SampleFunctionApplication.class,
            webEnvironment = WebEnvironment.RANDOM_PORT)
public class WebFunctionTests {

    @Autowired
    private TestRestTemplate rest;

    @Test
    public void test() throws Exception {
        ResponseEntity<String> result = this.rest.exchange(
            RequestEntity.post(new URI("/uppercase")).body("hello"), String.class);
        System.out.println(result.getBody());
    }
}

或者當使用函式 bean 定義風格時

@FunctionalSpringBootTest
public class WebFunctionTests {

    @Autowired
    private TestRestTemplate rest;

    @Test
    public void test() throws Exception {
        ResponseEntity<String> result = this.rest.exchange(
            RequestEntity.post(new URI("/uppercase")).body("hello"), String.class);
        System.out.println(result.getBody());
    }
}

這個測試幾乎與為同一應用的 @Bean 版本編寫的測試相同 - 唯一的區別是使用了 @FunctionalSpringBootTest 註解,而不是常規的 @SpringBootTest。所有其他部分,如 @Autowired TestRestTemplate,都是標準的 Spring Boot 特性。

為了幫助正確新增依賴,這裡是 POM 中的摘錄

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    . . . .
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-function-web</artifactId>
        <version>4.2.2</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

或者,可以只使用 FunctionCatalog 為非 HTTP 應用編寫測試。例如

@FunctionalSpringBootTest
public class FunctionalTests {

	@Autowired
	private FunctionCatalog catalog;

	@Test
	public void words() {
		Function<String, String> function = catalog.lookup(Function.class,
				"uppercase");
		assertThat(function.apply("hello")).isEqualTo("HELLO");
	}

}