编程模型

函数目录与灵活函数签名

Spring Cloud 函数的主要特点之一是适应并支持多种类型签名以适应用户自定义函数,同时提供一致的执行模型。 这就是为什么所有用户定义函数都通过以下方式转换为规范表示功能目录.spring-doc.cadn.net.cn

虽然用户通常不必关心功能目录至少,了解用户代码中支持哪些功能是有用的。spring-doc.cadn.net.cn

还需要理解的是,Spring Cloud Function 为 Project Reactor 提供的响应式 API 提供了一流的支持。 这允许响应式原语,如通量作为用户自定义函数中的类型使用,从而在选择函数实现的编程模型时提供更大的灵活性。 响应式编程模型还能支持那些在命令式编程风格下难以或不可能实现的功能。 欲了解更多内容,请阅读功能值部分。spring-doc.cadn.net.cn

Java 8 函数支持

Spring Cloud 函数承载并构建了自 Java 8 以来 Java 定义的三个核心功能接口。spring-doc.cadn.net.cn

总是避免提及提供商,功能消费者,在本手册其余部分,我们会适当地称它们为功能豆。spring-doc.cadn.net.cn

简而言之,任何豆子都可能在你应用上下文也就是说,功能性豆会被懒惰地注册为功能目录. 这意味着它本参考手册中描述的所有额外功能都能受益。spring-doc.cadn.net.cn

在最简单的应用中,你只需声明一个@Bean类型提供商,功能消费者在你的应用配置中。 然后,你可以使用功能目录根据函数的名称查找特定函数。spring-doc.cadn.net.cn

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

// . . .

FunctionCatalog catalog = applicationContext.getBean(FunctionCatalog.class);
Function uppercase =  catalog.lookup(“uppercase”);

重要的是要明白,鉴于大写是豆子,你当然可以从中获得应用上下文直接,但你只能拿到你申报的豆子,没有SCF提供的额外功能。 当你通过功能目录您接收的实例被本手册中描述的额外功能(如类型转换、组合等)包裹(instrumented)。spring-doc.cadn.net.cn

此外,需要理解的是,普通用户并不会直接使用 Spring Cloud Function。 相反,典型用户会实现一个 Java功能,提供商消费者想法是可以在不同的执行环境中使用它,而无需额外工作。spring-doc.cadn.net.cn

例如,同一Java函数可以表示为REST端点流式消息处理程序AWS Lambda,甚至更多,通过Spring Cloud Function提供的适配器以及其他以Spring Cloud Function为核心编程模型的框架(例如Spring Cloud Stream)。spring-doc.cadn.net.cn

总之,Spring Cloud 函数为 Java 函数提供了可在多种执行环境中使用的额外功能。spring-doc.cadn.net.cn

函数定义

而前面的例子展示了如何查找函数功能目录在程序化方面,在典型集成情形下,当 Spring Cloud 函数被其他框架(例如 Spring Cloud Stream)用作编程模型时,你可以通过spring.cloud.function.definition财产。 了解和理解在发现函数时的默认行为非常重要功能目录.spring-doc.cadn.net.cn

比如,如果你只有一个功能性豆子应用上下文spring.cloud.function.definition性质通常不需要,因为 只有一个函数功能目录可以用空名或任意名称查询。 例如,假设大写是你目录中唯一的函数,可以查找为catalog.lookup(null),catalog.lookup(“”),catalog.lookup(“foo”).spring-doc.cadn.net.cn

话虽如此,如果你使用像 Spring Cloud Stream 这样的框架,它使用spring.cloud.function.definition建议始终使用spring.cloud.function.definition财产。spring-doc.cadn.net.cn

spring.cloud.function.definition=uppercase

过滤不合格函数

典型的应用上下文可能包含一些是有效 Java 函数的 Beans,但并非作为候选注册对象功能目录. 这些 Beans 可以是来自其他项目的自动配置,或者任何符合 Java 函数条件的其他 Bean 代码。spring-doc.cadn.net.cn

该框架默认过滤了已知豆子,这些豆子不应被注册为候选功能目录. 你也可以通过提供一个逗号分隔的Beans定义名称列表,使用spring.cloud.function.ineligible-definitions财产。spring-doc.cadn.net.cn

spring.cloud.function.ineligible-definitions=foo,bar

提供商

提供商可能反应迅速 - 提供商<Flux<T>>祈使式 - 提供商<T>. 从召唤的角度来看,这对执行者来说应该没有区别提供商.spring-doc.cadn.net.cn

然而,在框架内使用(例如Spring Cloud Stream)时,提供商,尤其是响应式的,常被用来表示流的源头。 因此,它们只需调用一次即可获取流(例如:通量)消费者可以订阅。 换句话说,这些提供商相当于无限的流spring-doc.cadn.net.cn

不过,同样的响应式提供商也可以表示有限的流(例如,在轮询的JDBC数据中的结果集)。 在这种情况下,这些响应式提供商必须连接到底层框架的某种轮询机制。spring-doc.cadn.net.cn

为了帮助实现这一点,春季云函数提供了一个标记注释org.springframework.cloud.function.context.PollableBean以表示该提供商产生有限流,可能需要再次轮询。 然而,需要理解的是,Spring Cloud 函数本身并未为该注释提供任何行为。spring-doc.cadn.net.cn

此外,可投票豆注释通过暴露一个可拆分属性,用于表示需要对产生的流进行分割(参见分流器EIP))spring-doc.cadn.net.cn

这里有一个例子:spring-doc.cadn.net.cn

@PollableBean(splittable = true)
public Supplier<Flux<String>> someSupplier() {
	return () -> {
		String v1 = String.valueOf(System.nanoTime());
		String v2 = String.valueOf(System.nanoTime());
		String v3 = String.valueOf(System.nanoTime());
		return Flux.just(v1, v2, v3);
	};
}

功能

函数也可以以命令式或响应式方式写成。 然而,不同的是提供商消费者,实现者没有特别的考虑,除了理解在框架中使用时,如Spring Cloud Stream,反应函数只调用一次以传递流的引用(即通量而每事件调用一次命令式函数。spring-doc.cadn.net.cn

public Function<String, String> uppercase() {
    . . . .
}

双功能

如果你需要随有效载荷接收一些额外的数据(元数据),你总可以声明函数签名来接收消息包含一个带有额外信息的头部映射。spring-doc.cadn.net.cn

public Function<Message<String>, String> uppercase() {
    . . . .
}

为了让你的函数签名更轻量、更像 POJO,还有另一种方法。你可以使用双功能.spring-doc.cadn.net.cn

public BiFunction<String, Map, String> uppercase() {
    . . . .
}

假设消息仅包含两个属性(有效载荷和头部),并且双功能需要两个输入参数,框架会自动识别该签名并从中提取有效载荷消息将它作为第一个参数传递,并且地图第二个是头球。 因此,你的函数不会与 Spring 的消息 API 耦合。 请记住双功能需要严格签名,且第二个参数必须地图. 同样的规则适用于双消费者.spring-doc.cadn.net.cn

消费者

消费者有点特别,因为它拥有无效返回类型,这意味着至少潜在的阻塞。 你很可能不需要写作Consumer<Flux<?>>但如果你真的需要,记得订阅输入通量.spring-doc.cadn.net.cn

函数合成

函数组合是一种功能,允许将多个函数合成为一个。 核心支持基于 Function.andThen(..) 提供的函数组合功能,自 Java 8 起可用。 不过,Spring Cloud Function 还提供了一些额外的功能。spring-doc.cadn.net.cn

声明式函数组合

该功能允许您以声明式方式提供作文指导,使用|(管道)或,(逗号)设置 时的分隔符spring.cloud.function.definition财产。spring-doc.cadn.net.cn

--spring.cloud.function.definition=uppercase|reverse

在这里,我们有效地定义了一个函数本身是函数的复合大写以及功能反向. 事实上,这也是性质名称为定义而非名称的原因之一,因为函数的定义可以是多个命名函数的组合。 如前所述,你可以使用,而不是|…​定义=大写,反向.spring-doc.cadn.net.cn

非函数的组合

Spring Cloud 函数还支持作曲提供商消费者功能以及功能消费者. 重要的是要理解这些定义的最终结果。 构成提供商功能结果依然是提供商作曲时提供商消费者将有效地渲染可跑. 遵循同样的逻辑,作曲功能消费者将导致消费者.spring-doc.cadn.net.cn

当然,你不能组合不可组合的对象,比如消费者功能,消费者提供商等。spring-doc.cadn.net.cn

功能路由与过滤

自2.2版本起,Spring Cloud Function提供了路由功能,允许你调用单个函数,作为你想调用的实际函数的路由器。 该功能在某些FAAS环境中非常有用,因为维护多个功能的配置可能繁琐或无法暴露多个功能。spring-doc.cadn.net.cn

路由函数功能目录中以函数路由. 为了简便和一致,你也可以参考RoutingFunction.FUNCTION_NAME不断。spring-doc.cadn.net.cn

该函数具有以下签名:spring-doc.cadn.net.cn

public class RoutingFunction implements Function<Object, Object> {
// . . .
}

路由指令可以通过多种方式传递。 我们支持通过消息头、系统属性以及可插拔策略提供指令。 让我们来看看一些细节。spring-doc.cadn.net.cn

消息路由回调

消息路由回调是一种帮助确定路径到函数定义名称的策略。spring-doc.cadn.net.cn

public interface MessageRoutingCallback {
    default String routingResult(Message<?> message) {
	    return (String) message.getHeaders().get(FunctionProperties.FUNCTION_DEFINITION);
    }
}

你所需要做的就是实现并注册消息路由回调作为一颗豆子,被路由函数. 例如:spring-doc.cadn.net.cn

@Bean
public MessageRoutingCallback customRouter() {
	return new MessageRoutingCallback() {
		@Override
		public String routingResult(Message<?> message) {
			return (String) message.getHeaders().get(FunctionProperties.FUNCTION_DEFINITION);
		}
	};
}

在前面的例子中,你可以看到一个非常简单的实现消息路由回调,该函数定义由FunctionProperties.FUNCTION_DEFINITION 消息输入的头部消息,返回一个实例字符串表示调用函数的定义。spring-doc.cadn.net.cn

如果输入参数类型为留言<?>,你可以通过设置以下一个来传递路由指令spring.cloud.function.definitionSpring.cloud.function.routing-expression 消息头。 正如房产名称所示,Spring.cloud.function.routing-expression依赖于 Spring 表达式语言(SpEL)。 对于更静态的情况,你可以使用spring.cloud.function.definition头部,允许你提供单个函数的名称(例如,…​定义=foo)或作文指导(例如:…​定义=foo|bar|baz). 对于更动态的情况,可以使用Spring.cloud.function.routing-expression并提供 SpEL 表达式,应解析为函数的定义(如上所述)。spring-doc.cadn.net.cn

SpEL 评估上下文的根对象是实际的输入参数,因此在留言<?>你可以构建一个既能访问这两者又能访问的表达式有效载荷(例如:spring.cloud.function.routing-expression=headers.function_name).
SpEL允许用户提供Java代码的字符串表示。 鉴于Spring.cloud.function.routing-expression可以通过消息头提供,意味着设置此类表达式的能力可能暴露给终端用户(例如使用网页模块时的HTTP头),这可能导致一些问题(例如恶意代码)。 为此,所有通过Message头部的表达式都将被评估为SimpleEvaluationContext,功能有限,仅用于评估上下文对象(我们为Message)。 另一方面,所有通过属性或系统环境变量设置的表达式都会被对 进行评估标准评估上下文这使得 Java 语言能够实现全部的灵活性。 虽然通过系统/应用属性或环境变量设置表达式通常被认为是安全的,因为在正常情况下不会向终端用户开放,但在某些情况下,通过其他Spring项目提供的Spring Boot Actuator端点,最终用户确实会向终端用户开放系统、应用和环境变量的可视化和更新能力, 第三方,或最终用户自定义实现。 此类端点必须采用行业标准的网络安全措施进行保护。 Spring Cloud Function 不会暴露任何此类端点。

在特定的执行环境/模型中,适配器负责转换和通信spring.cloud.function.definition和/或Spring.cloud.function.routing-expression通过消息页眉。 例如,使用 spring-cloud-function-web 时,你可以提供spring.cloud.function.definition作为 HTTP 头部,框架将它与其他 HTTP 头一起传播为 Message 头部。spring-doc.cadn.net.cn

路由指令也可以通过以下方式传递spring.cloud.function.definitionSpring.cloud.function.routing-expression作为应用属性。 前一节所述的规则同样适用于此。唯一的区别是你提供这些指令作为应用属性(例如,--spring.cloud.function.definition=foo).spring-doc.cadn.net.cn

重要的是要明白,提供spring.cloud.function.definitionSpring.cloud.function.routing-expression因为消息头只能用于命令式函数(例如:功能<福,酒吧>). 也就是说,我们只能用命令式函数为每条消息进行路由。 对于响应式函数,我们无法为每个消息进行路由。 因此,你只能作为应用属性提供路由指令。 这完全是关于工作单位。 在命令式函数中,工作单位是消息,因此我们可以基于该工作单元进行路由。 对于响应式函数,工作单元是整个流,因此我们只对应用属性提供的指令进行作,并路由整个流。

路由指令的优先级顺序spring-doc.cadn.net.cn

鉴于我们有多种提供路由指令的机制,理解在同时使用多个机制时冲突解决的优先级非常重要。 顺序如下:spring-doc.cadn.net.cn

  1. 消息路由回调(当函数是命令式时优先,无论是否定义其他)spring-doc.cadn.net.cn

  2. 消息头部(如果函数是命令式且消息路由回调提供)spring-doc.cadn.net.cn

  3. 应用属性(任意函数)spring-doc.cadn.net.cn

无法路由的消息spring-doc.cadn.net.cn

如果目录中没有路径功能,你会看到异常说明。spring-doc.cadn.net.cn

有些情况下,这种行为并不理想,你可能需要某种“包容”式函数来处理这类消息。 为此,该框架提供了org.springframework.cloud.function.context.DefaultMessageRoutingHandler策略。 你只需要注册为豆子。 其默认实现会记录消息不可路由的事实,但允许消息流继续且不触发该例外,从而有效地丢弃不可路由消息。 如果你需要更复杂的方法,只需提供你自己的策略实现,并将其注册为一个“豆子”。spring-doc.cadn.net.cn

@Bean
public DefaultMessageRoutingHandler defaultRoutingHandler() {
	return new DefaultMessageRoutingHandler() {
		@Override
		public void accept(Message<?> message) {
			// do something really cool
		}
	};
}

功能过滤

过滤是一种只有两条路径的路由方式——“去”或“丢弃”。在函数方面,这意味着只有当某个条件返回“true”时才想调用某个函数,否则你要丢弃输入。spring-doc.cadn.net.cn

然而,在剔除输入时,对于其在申请背景下可能的含义有多种解释。 例如,你可能想记录,或者维护一个被丢弃消息的计数器。 你也可以什么都不做。spring-doc.cadn.net.cn

由于这些不同的路径,我们没有提供处理丢弃消息的通用配置选项。 相反,我们建议简单定义一个简单的消费者这表示“弃置”路径:spring-doc.cadn.net.cn

@Bean
public Consumer<?> devNull() {
   // log, count, or whatever
}

现在你可以有一个只有两条路径的路由表达式,实际上就成了一个过滤器。 例如:spring-doc.cadn.net.cn

--spring.cloud.function.routing-expression=headers.contentType.toString().equals('text/plain') ? 'echo' : 'devNull'

所有不符合“echo”功能的消息都会被送到“devNull”,在那里你根本无法处理它。 签名消费者<?>同时确保不会尝试类型转换,几乎没有执行开销。spring-doc.cadn.net.cn

处理响应式输入(例如Publisher)时,路由指令只能通过函数属性提供。 这是因为反应函数的特性只调用一次以传递发行人其余部分由反应器处理,因此我们无法访问和/或依赖通过单个值(如消息)传递的路由指令。

多路由器

默认情况下,框架始终配置为单一路由函数,如前述所述。 不过,有时你可能需要多个路由功能。 在这种情况下,你可以创建自己的实例路由函数除了现有的豆子,只要你给它起个名字,不就是函数路由.spring-doc.cadn.net.cn

你可以通过Spring.cloud.function.routing-expressionspring.cloud.function.definition路由函数作为地图中的键/值对。spring-doc.cadn.net.cn

这里有一个简单的例子:spring-doc.cadn.net.cn

@Configuration
protected static class MultipleRouterConfiguration {

	@Bean
	RoutingFunction mySpecialRouter(FunctionCatalog functionCatalog, BeanFactory beanFactory, @Nullable MessageRoutingCallback routingCallback) {
		Map<String, String> propertiesMap = new HashMap<>();
		propertiesMap.put(FunctionProperties.PREFIX + ".routing-expression", "'reverse'");
		return new RoutingFunction(functionCatalog, propertiesMap, new BeanFactoryResolver(beanFactory), routingCallback);
	}

	@Bean
	public Function<String, String> reverse() {
		return v -> new StringBuilder(v).reverse().toString();
	}

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

这里有一个测试来演示其工作原理:spring-doc.cadn.net.cn

@Test
public void testMultipleRouters() {
	System.setProperty(FunctionProperties.PREFIX + ".routing-expression", "'uppercase'");
	FunctionCatalog functionCatalog = this.configureCatalog(MultipleRouterConfiguration.class);
	Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME);
	assertThat(function).isNotNull();
	Message<String> message = MessageBuilder.withPayload("hello").build();
	assertThat(function.apply(message)).isEqualTo("HELLO");

	function = functionCatalog.lookup("mySpecialRouter");
	assertThat(function).isNotNull();
	message = MessageBuilder.withPayload("hello").build();
	assertThat(function.apply(message)).isEqualTo("olleh");
}

输入/输出丰富

通常你需要修改或优化进出消息,并保持代码干净,避免非功能性问题。 你不想在业务逻辑里做这件事。spring-doc.cadn.net.cn

你总可以通过函数写作来实现。 这种方法带来了多项好处:spring-doc.cadn.net.cn

  • 它允许你将这个非功能性关注点隔离到一个独立的函数中,你可以用业务函数作为函数定义来组合。spring-doc.cadn.net.cn

  • 它给你提供了完全的自由(也包括风险)在收到消息到达实际业务前可以修改的内容。spring-doc.cadn.net.cn

@Bean
public Function<Message<?>, Message<?>> enrich() {
    return message -> MessageBuilder.fromMessage(message).setHeader("foo", "bar").build();
}

@Bean
public Function<Message<?>, Message<?>> myBusinessFunction() {
    // do whatever
}

然后,通过提供以下函数定义来构写你的函数:enrich|myBusinessFunction.spring-doc.cadn.net.cn

虽然上述方法最灵活,但也最为复杂。 你需要先写代码,然后把它做成一个 bean,或者手动注册为函数,才能用业务函数组合,正如你从前面的例子中看到的。spring-doc.cadn.net.cn

但如果你试图做的修改(丰富)像前面的例子一样微不足道呢? 有没有更简单、更动态、更可配置的机制来实现同样的效果?spring-doc.cadn.net.cn

自3.1.3版本起,框架允许你提供SpEL表达式,丰富单个消息头部,涵盖输入到函数和输出的两个。 我们来看一个测试作为例子。spring-doc.cadn.net.cn

@Test
public void testMixedInputOutputHeaderMapping() throws Exception {
	try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
			SampleFunctionConfiguration.class).web(WebApplicationType.NONE).run(
					"--logging.level.org.springframework.cloud.function=DEBUG",
					"--spring.main.lazy-initialization=true",
					"--spring.cloud.function.configuration.split.output-header-mapping-expression.keyOut1='hello1'",
					"--spring.cloud.function.configuration.split.output-header-mapping-expression.keyOut2=headers.contentType",
					"--spring.cloud.function.configuration.split.input-header-mapping-expression.key1=headers.path.split('/')[0]",
					"--spring.cloud.function.configuration.split.input-header-mapping-expression.key2=headers.path.split('/')[1]",
					"--spring.cloud.function.configuration.split.input-header-mapping-expression.key3=headers.path")) {

		FunctionCatalog functionCatalog = context.getBean(FunctionCatalog.class);
		FunctionInvocationWrapper function = functionCatalog.lookup("split");
		Message<byte[]> result = (Message<byte[]>) function.apply(MessageBuilder.withPayload("hello")
				.setHeader(MessageHeaders.CONTENT_TYPE, "application/json")
				.setHeader("path", "foo/bar/baz")
				.build());
		assertThat(result.getHeaders()).containsKey("keyOut1"));
		assertThat(result.getHeaders().get("keyOut1")).isEqualTo("hello1");
		assertThat(result.getHeaders()).containsKey("keyOut2"));
		assertThat(result.getHeaders().get("keyOut2")).isEqualTo("application/json");
	}
}

这里你看到的属性称为输入-头-映射-表达式输出-头-映射-表达式前面加上函数名称(即分裂) 后接你想设置的消息头键名称和 SpEL 表达式的值。 第一个表达式(针对“keyOut1”)是一个用单引号包住的字面SpEL表达式,实际上将“keyOut1”设为值你好1. 这keyOut2设置为现有“contentType”头部的值。spring-doc.cadn.net.cn

你还可以在输入头映射中观察到一些有趣的特征,比如我们实际上是将现有头部“路径”的一个值拆分,将key1和key2的单个值设置为基于索引的拆分元素值。spring-doc.cadn.net.cn

如果由于某种原因,所提供的表达式评估失败,函数的执行将如同从未发生过一样继续。 不过,你会在日志中看到WARN消息,告知你这件事。
o.s.c.f.context.catalog.InputEnricher    : Failed while evaluating expression "hello1"  on incoming message. . .

如果你处理的是有多个输入的函数(下一节),你可以在之后立即使用索引输入-头-映射-表达式:spring-doc.cadn.net.cn

--spring.cloud.function.configuration.echo.input-header-mapping-expression[0].key1=‘hello1'
--spring.cloud.function.configuration.echo.input-header-mapping-expression[1].key2='hello2'

函数元数

有时数据流需要分类和整理。 例如,考虑一个经典的大数据用例,处理包含“订单”和“发票”的无序数据,你希望它们分别存放在一个独立的数据存储中。 这就是函数元(具有多输入和多输出的函数)支持发挥作用的地方。spring-doc.cadn.net.cn

让我们来看一个这样的函数示例。消息路由回调spring-doc.cadn.net.cn

完整的实施细节请见此处
@Bean
public Function<Flux<Integer>, Tuple2<Flux<String>, Flux<String>>> organise() {
	return flux -> ...;
}

鉴于Project Reactor是SCF的核心依赖,我们使用了它的元库库。 元组通过向我们传递基数类型信息,给了我们独特的优势。 这两者在SCSt的语境中都极为重要。基数让我们知道需要创建多少输入和输出绑定,并绑定到函数的相应输入和输出。 对类型信息的了解确保了正确的类型转换。spring-doc.cadn.net.cn

此外,这也是绑定名称命名惯例中“索引”部分的作用,因为在此函数中,两个输出绑定名称分别为组织-出-0组织出局-1.spring-doc.cadn.net.cn

目前,函数元数仅支持反应函数(函数<TupleN<Flux<?>......>,TupleN<Flux<?>......>>)中心于复杂的事件处理,其中对事件汇聚的评估和计算通常需要对事件流进行观察,而非单一事件。

输入头传播

在典型场景中,输入消息头不会传播到输出,这也是理所当然的,因为函数的输出可能是其他需要独立消息头集合的输入。 然而,有时这种传播是必要的,因此Spring Cloud Function提供了多种机制来实现这一目标。spring-doc.cadn.net.cn

首先,你总可以手动复制头部。 例如,如果你有一个函数,其签名为消息以及返回消息(即,功能<消息,消息>),你可以简单且有选择地自己复制页头。 记住,如果你的函数返回了 Message,框架不会对它做任何作,除了正确转换其有效载荷。 然而,这种方法可能会显得有些繁琐,尤其是在你只是想复制所有头部的情况下。 为了帮助这种情况,我们提供了一个简单的属性,允许你在需要传播输入头部的函数上设置布尔标志。 该性质为复制输入头.spring-doc.cadn.net.cn

例如,假设你的配置如下:spring-doc.cadn.net.cn

@EnableAutoConfiguration
@Configuration
protected static class InputHeaderPropagationConfiguration {

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

如你所知,你仍然可以通过发送消息调用这个函数(框架会处理类型转换和有效载荷提取)。spring-doc.cadn.net.cn

简单地设置spring.cloud.function.configuration.uppercase.copy-input-headerstrue,以下断言同样成立spring-doc.cadn.net.cn

Function<Message<String>, Message<byte[]>> uppercase = catalog.lookup("uppercase", "application/json");
Message<byte[]> result = uppercase.apply(MessageBuilder.withPayload("bob").setHeader("foo", "bar").build());
assertThat(result.getHeaders()).containsKey("foo");

类型转换(内容-类型协商)

内容类型协商是 Spring Cloud Function 的核心功能之一,因为它不仅允许将输入数据转换为声明的类型 通过函数签名,但在函数复合时进行相同的变换,使得原本无法组合(按类型)的函数可组合。spring-doc.cadn.net.cn

为了更好地理解内容类型协商的机制和必要性,我们来看一个非常简单的用例: 以以下函数为例:spring-doc.cadn.net.cn

@Bean
public Function<Person, String> personFunction {..}

前述示例中所示的函数期望为object 作为参数,输出 String 类型。如果调用该函数类型为然后一切正常。 但通常函数作为处理者,处理接收数据,这些数据通常以原始格式出现,例如字节[],JSON 字符串等。 为了让框架成功将输入数据作为该函数的参数传递,它必须以某种方式将输入数据转换为类型。spring-doc.cadn.net.cn

Spring Cloud Function 依赖于 Spring 原生的两种机制来实现这一点。spring-doc.cadn.net.cn

  1. MessageConverter - 将接收的消息数据转换为函数声明的类型。spring-doc.cadn.net.cn

  2. ConversionService——将非消息数据转换为函数声明的类型。spring-doc.cadn.net.cn

这意味着根据原始数据的类型(消息或非消息),Spring Cloud 函数会应用其中一种机制。spring-doc.cadn.net.cn

在大多数情况下,当处理作为其他请求(例如HTTP、消息等)调用的函数时,框架依赖于消息转换器,因为此类请求已转换为Spring消息. 换句话说,该框架定位并应用了适当的消息转换器. 为了实现这一点,框架需要用户提供一些指令。 其中一条指令已经由函数本身的签名(Person类型)提供。 因此,理论上,这应该(在某些情况下确实)足够。 然而,对于大多数使用场景,为了选择合适的消息转换器,框架还需要补充一条信息。 那个缺失的拼图是内容类型页眉。spring-doc.cadn.net.cn

此类头部通常作为消息的一部分出现,由最初创建该消息的相应适配器注入。 例如,HTTP POST 请求的内容类型 HTTP 头部会被复制到内容类型消息的头部。spring-doc.cadn.net.cn

对于不存在此类头部的情况,框架依赖默认内容类型application/json.spring-doc.cadn.net.cn

内容类型与论元类型

如前所述,框架选择合适的消息转换器它需要参数类型,并且可选地提供内容类型信息。 选择合适 的逻辑消息转换器存在于参数解析器,参数解析器在调用用户自定义函数之前触发(此时框架已知实际参数类型)。 如果参数类型与当前有效载荷类型不匹配,框架会委派到预配置的负载栈中消息转换器看看他们中是否有人能转换有效载荷。spring-doc.cadn.net.cn

组合内容类型参数类型是框架判断消息是否可以通过找到合适的目标类型来转换为目标类型的机制消息转换器. 如果不合适消息转换器如果被发现,就会抛出一个异常,你可以通过添加自定义来处理消息转换器(参见用户自定义消息转换器).spring-doc.cadn.net.cn

别抱太大期望消息仅基于内容类型. 记住内容类型与目标类型互补。 这是一个暗示,消息转换器可能会考虑,也可能不会。

消息转换器

消息转换器定义两种方法:spring-doc.cadn.net.cn

Object fromMessage(Message<?> message, Class<?> targetClass);

Message<?> toMessage(Object payload, @Nullable MessageHeaders headers);

了解这些方法的契约及其使用方式非常重要,特别是在春云流的背景下。spring-doc.cadn.net.cn

发件消息方法转换一个输入消息变成了争吵类型。 有效载荷消息可以是任何类型,这取决于实际实现消息转换器支持多种类型。spring-doc.cadn.net.cn

提供的消息转换器

如前所述,该框架已经提供了消息转换器以应对大多数常见的使用场景。 以下列表描述了所提供的消息转换器,按优先顺序(第一个)消息转换器该方法被使用):spring-doc.cadn.net.cn

  1. JsonMessageConverter支持对 的有效载荷转换消息对于当内容类型application/json使用Jackson(默认)或Gson库。该消息转换器还知道类型参数(例如,application/json;type=foo.bar.Person)。这对于在函数开发时类型可能未知的情况非常有用,因此函数签名可能看起来像功能<?, ?>功能功能<对象,对象>.换句话说,对于类型转换,我们通常从函数签名中推导出类型。拥有 mime 类型的参数可以让你以更动态的方式传达类型。spring-doc.cadn.net.cn

  2. 字节阵列消息转换器支持对 的有效载荷转换消息字节[]字节[]对于内容类型应用/八位元组流.它本质上是一个直通,主要用于向下兼容。spring-doc.cadn.net.cn

  3. 字符串消息转换器:支持任意类型的转换字符串什么时候内容类型文本/纯文字.spring-doc.cadn.net.cn

当找不到合适的转换器时,框架会抛出异常。当这种情况发生时,你应该检查你的代码和配置,确保没有遗漏任何内容(也就是说,确保你提供了内容类型通过使用绑定或头部)。 不过,很可能你遇到了一些不常见的情况(比如习惯内容类型也许)以及当前提供的堆栈消息转换器不知道如何转换。 如果是这样,你可以添加自定义消息转换器.参见用户自定义消息转换器spring-doc.cadn.net.cn

用户自定义的消息转换器

Spring Cloud 函数提供了定义和注册额外信息的机制消息转换器. 要用它,就实现org.springframework.messaging.converter.MessageConverter,将其配置为@Bean. 然后它被附加到现有的“MessageConverter”堆栈中。spring-doc.cadn.net.cn

理解这种习俗非常重要消息转换器实现会被添加到现有栈的头部。 因此,习俗消息转换器实现优先于现有的,这让你既能覆盖也能添加现有转换器。

以下示例展示了如何创建消息转换器豆,以支持名为应用/律师资格:spring-doc.cadn.net.cn

@SpringBootApplication
public static class SinkApplication {

    ...

    @Bean
    public MessageConverter customMessageConverter() {
        return new MyCustomMessageConverter();
    }
}

public class MyCustomMessageConverter extends AbstractMessageConverter {

    public MyCustomMessageConverter() {
        super(new MimeType("application", "bar"));
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return (Bar.class.equals(clazz));
    }

    @Override
    protected Object convertFromInternal(Message<?> message, Class<?> targetClass, Object conversionHint) {
        Object payload = message.getPayload();
        return (payload instanceof Bar ? payload : new Bar((byte[]) payload));
    }
}

关于JSON选项的说明

在 Spring Cloud Function 中,我们支持 Jackson 和 Gson 机制来处理 JSON。 为了方便你,我已经将其抽象化为以下org.springframework.cloud.function.json.JsonMapper它本身知道两种机制,会使用你选择的那一种,或者遵循默认规则。 默认规则如下:spring-doc.cadn.net.cn

  • 无论类路径上哪个库,那就是将要使用的机制。所以如果你有com.fasterxml.jackson.*到班级路径,Jackson将被使用,如果你已经使用com.google.code.gson,则使用 Gson。spring-doc.cadn.net.cn

  • 如果你两者都有,那么Gson将成为默认选项,或者你可以设置Spring.cloud.function.preferred-json-mapper具有以下两个取值中的任意一个的属性:Jackson.spring-doc.cadn.net.cn

不过,类型转换对开发者来说通常是透明的。 然而,鉴于org.springframework.cloud.function.json.JsonMapper也注册为豆子,如果需要,可以很容易地注入代码。spring-doc.cadn.net.cn

Kotlin Lambda 支持

我们也支持 Kotlin lambda(自 v2.0 起)。 请考虑以下几点:spring-doc.cadn.net.cn

@Bean
open fun kotlinSupplier(): () -> String {
    return  { "Hello from Kotlin" }
}

@Bean
open fun kotlinFunction(): (String) -> String {
    return  { it.toUpperCase() }
}

@Bean
open fun kotlinConsumer(): (String) -> Unit {
    return  { println(it) }
}

上面表示了Kotlin lambdas的配置为春豆。每个的签名映射到 Java 等价的提供商,功能消费者因此,框架支持/认可签名。 虽然Kotlin到Java映射的机制不在本文档范围内,但需要理解的是,这里也应用了“Java 8函数支持”部分中描述的签名转换规则。spring-doc.cadn.net.cn

要启用Kotlin支持,只需在类路径上添加Kotlin SDK库,即可触发相应的自动配置和支持类。spring-doc.cadn.net.cn

功能组件扫描

Spring Cloud 函数将扫描 的实现功能,消费者提供商在一个名为功能如果它存在的话。 利用这个功能,你可以写出不依赖 Spring 的函数——甚至不依赖于@Component需要注释。 如果你想用不同的包,可以设置spring.cloud.function.scan.packages. 你也可以使用spring.cloud.function.scan.enabled=false完全关闭扫描。spring-doc.cadn.net.cn

数据掩蔽

典型的应用包含多个层次的记录。 某些云/无服务器平台可能会在数据包中包含敏感数据,供所有人查看。 虽然检查所记录数据是个别开发者的责任,但由于日志来自框架本身,从4.1版本起,我们引入了JsonMasker最初帮助掩蔽AWS Lambda载荷中的敏感数据。 然而,JsonMasker是通用的,适用于任何模块。 目前它只能支持像JSON这样的结构化数据。 你只需要指定想要遮蔽的密钥,其他的都会被它处理。 密钥应在文件中指定META-INF/mask.keys. 文件格式非常简单,可以用逗号、换行或两者都用来分隔多个键。spring-doc.cadn.net.cn

以下是该文件内容的示例:spring-doc.cadn.net.cn

eventSourceARN
asdf1, SS

这里你可以看到三个关键点。 一旦存在这样的文件,就JsonMasker将用它来掩盖指定键的值。spring-doc.cadn.net.cn

这里有示例代码,展示了使用情况:spring-doc.cadn.net.cn

private final static JsonMasker masker = JsonMasker.INSTANCE();
// . . .
logger.info("Received: " + masker.mask(new String(payload, StandardCharsets.UTF_8)));