Functional Bean Definitions

Spring Cloud Function 支持小应用程序的功能样式 bean 声明,这些应用程序需要快速启动。功能样式的 bean 声明是 Spring Framework 5.0 的一项功能,在 5.1 中得到显著增强。

Spring Cloud Function supports a "functional" style of bean declarations for small apps where you need fast startup. The functional style of bean declaration was a feature of Spring Framework 5.0 with significant enhancements in 5.1.

Comparing Functional with Traditional Bean Definitions

下面是一个来自熟悉的 @Configuration@Bean 声明样式的纯 Spring Cloud Function 应用程序:

Here’s a vanilla Spring Cloud Function application from with the familiar @Configuration and @Bean declaration style:

@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:用户应用程序代码可以重新转换为“功能”形式,如下所示:

Now for the functional beans: the user application code can be recast into "functional" form, like this:

@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)));
  }

}

主要差异如下:

The main differences are:

  • The main class is an ApplicationContextInitializer.

  • The @Bean methods have been converted to calls to context.registerBean()

  • The @SpringBootApplication has been replaced with @SpringBootConfiguration to signify that we are not enabling Spring Boot autoconfiguration, and yet still marking the class as an "entry point".

  • The SpringApplication from Spring Boot has been replaced with a FunctionalSpringApplication from Spring Cloud Function (it’s a subclass).

在 Spring Cloud Function 应用程序中注册的业务逻辑 bean 类型为 FunctionRegistration。这是一个包装器,其中包含有关输入类型和输出类型的功能和信息。在应用程序的 @Bean 形式中,可以通过反射导出该信息,但在功能 bean 注册中,除非我们使用 FunctionRegistration,否则其中一些信息会丢失。

The business logic beans that you register in a Spring Cloud Function app are of type FunctionRegistration. This is a wrapper that contains both the function and information about the input and output types. In the @Bean form of the application that information can be derived reflectively, but in a functional bean registration some of it is lost unless we use a FunctionRegistration.

使用 ApplicationContextInitializerFunctionRegistration 的替代方法是让应用程序本身实现 Function(或 ConsumerSupplier)。示例(等效于上述示例):

An alternative to using an ApplicationContextInitializer and FunctionRegistration is to make the application itself implement Function (or Consumer or Supplier). Example (equivalent to the above):

@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 的类,并使用 run() 方法的替代形式使用 SpringApplication 注册它,也可以工作。最重要的是,泛型类型信息在运行时通过类声明提供。

It would also work if you add a separate, standalone class of type Function and register it with the SpringApplication using an alternative form of the run() method. The main thing is that the generic type information is available at runtime through the class declaration.

假设您有

Suppose you have

@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()));
	}

}

您已将其注册为:

You register it as such:

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

Limitations of Functional Bean Declaration

与整个 Spring Boot 相比,大多数 Spring Cloud Function 应用程序的范围相对较小,因此我们能够轻松地将它适应这些功能 bean 定义。如果您超出该有限范围,则可以通过切换回 @Bean 样式配置或使用混合方法来扩展 Spring Cloud Function 应用程序。例如,如果您想利用 Spring Boot 自动配置来实现与外部数据存储的集成,那么您需要使用 @EnableAutoConfiguration。如果您愿意,仍然可以使用功能声明来定义您的功能(即“混合”样式),但在这种情况下,您需要使用 spring.functional.enabled=false 明确关闭“完全功能模式”,以便 Spring Boot 可以重新控制。

Most Spring Cloud Function apps have a relatively small scope compared to the whole of Spring Boot, so we are able to adapt it to these functional bean definitions easily. If you step outside that limited scope, you can extend your Spring Cloud Function app by switching back to @Bean style configuration, or by using a hybrid approach. If you want to take advantage of Spring Boot autoconfiguration for integrations with external datastores, for example, you will need to use @EnableAutoConfiguration. Your functions can still be defined using the functional declarations if you want (i.e. the "hybrid" style), but in that case you will need to explicitly switch off the "full functional mode" using spring.functional.enabled=false so that Spring Boot can take back control.

Function visualization and control

Spring Cloud Function 支持通过 Actuator 端点以及编程方式可视化 FunctionCatalog 中可用的函数。

Spring Cloud Function supports visualization of functions available in FunctionCatalog through Actuator endpoints as well as programmatic way.

Programmatic way

要通过编程方式查看应用程序上下文中可用的函数,您只需要访问 FunctionCatalog。在那里,您可以找到获取编目大小、查找函数以及列出所有可用函数名称的方法。

To see function available within your application context programmatically all you need is access to FunctionCatalog. There you can finds methods to get the size of the catalog, lookup functions as well as list the names of all the available functions.

例如,

For example,

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 框架的依赖项:

Since actuator and web are optional, you must first add one of the web dependencies as well as add the actuator dependency manually. The following example shows how to add the dependency for the Web framework:

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

以下示例展示了如何添加 WebFlux 框架的依赖项:

The following example shows how to add the dependency for the WebFlux framework:

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

您可以按如下方式添加 Actuator 依赖项:

You can add the Actuator dependency as follows:

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

您还必须通过设置以下属性来启用 functions Actuator 端点:--management.endpoints.web.exposure.include=functions

You must also enable the functions actuator endpoints by setting the following property: --management.endpoints.web.exposure.include=functions.

访问以下 URL 以查看 FunctionCatalog 中的函数:http://<host>:<port>/actuator/functions

Access the following URL to see the functions in FunctionCatalog: http://<host>:<port>/actuator/functions

例如,

For example,

curl http://localhost:8080/actuator/functions

您的输出应如下所示:

Your output should look something like this:

{"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"}. . .

Testing Functional Applications

Spring Cloud Function 还有一些对 Spring Boot 用户来说非常熟悉的用于集成测试的实用工具。

Spring Cloud Function also has some utilities for integration testing that will be very familiar to Spring Boot users.

假设这是您的应用程序:

Suppose this is your application:

@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 服务器的集成测试:

Here is an integration test for the HTTP server wrapping this application:

@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());
    }
}

或当 function bean 定义样式被使用:

or when function bean definition style is used:

@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 功能。

This test is almost identical to the one you would write for the @Bean version of the same app - the only difference is the @FunctionalSpringBootTest annotation, instead of the regular @SpringBootTest. All the other pieces, like the @Autowired TestRestTemplate, are standard Spring Boot features.

为了帮助解决正确的依赖项,这里是从 POM 中摘录的内容

And to help with correct dependencies here is the excerpt from 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>{project-version}</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 应用程序编写测试。例如:

Or you could write a test for a non-HTTP app using just the FunctionCatalog. For example:

@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");
	}

}