【橘子微服务】spring cloud function的编程模型

简介

在我们初探了saga的分布式事务之后,我们后面会基于springcloud function(简称:scf)和springcloud stream(scs)实现一下Choreography模式的saga。所以在此之前我们需要了解一下这两个组件的知识。首先我们来看scf的一些概念。

一、简单使用

在进入概念之前,我们先来看看这玩意咋用,然后我们再铺开讲设计和知识,不然上来一堆废话不知道说的是谁。

简单来创建一个spring项目,然后我们引入scf的依赖。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.3.6</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>xxx</groupId>
	<artifactId>xxx</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>cloudfunc</name>
	<description>Demo project for scf</description>
	<url/>
	<properties>
		<java.version>17</java.version>
		<spring-cloud.version>2023.0.4</spring-cloud.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-function-context</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-stream</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

然后我们创建一个springboot的启动类。

java 复制代码
@SpringBootApplication
public class CloudfuncApplication {

	public static void main(String[] args) {
		String input = "hello";
		ConfigurableApplicationContext context = SpringApplication.run(CloudfuncApplication.class, args);
		FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
		Function<String,String> uppercaseFunc = catalog.lookup("uppercase");
		String res = uppercaseFunc.apply("hello");
		System.out.println(input + " invoke res is:" + res);
	}

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

这个操作很简单,我们就是在容器中注册了一个Bean,只不过这个bean和你以前的那种不太一样。
它的类型是一个函数,准确的说是一个函数式接口。然后把你的输入转为大写。
然后我们在启动类中获取一个叫做FunctionCatalog.class,调用它的lookup方法找到我们
这个注册的函数,此时我们就能调用它。进而把我们输入的hello字符串转为大写。
其实到这里你应该明白了,它把函数当成了bean,然后管理这个bean,调用它。
因为spring的主要抽象就是bean,在这个大抽象下,根据一些语法特性可以轻松实现它。

这就是scf的基本操作,至于它还提供了很多的变体来实现不同的功能,我们后面慢慢说。

至此你大概知道他是怎么用的了。那么我们下面开始上理论。

二、编程模型

1、函数目录和灵活的函数签名

Spring Cloud Function(简称SCF) 的主要功能之一是适应和支持用户定义函数的一系列类型签名,同时提供一致的执行模型。这就是为什么 FunctionCatalog 将所有用户定义的函数转换为规范表示形式的原因(转为了java那几种函数式接口)。

虽然用户通常根本不需要关心 FunctionCatalog,但了解用户代码中支持哪些类型的函数是很有用的。

了解 SCF 为 Project Reactor 提供的反应式 API 提供一流的支持也很重要,它允许反应式原语,例如 Mono 和 Flux 用作用户定义函数中的类型,在为 您的函数实现。 反应式编程模型还支持对原本难以或不可能实现的功能提供支持 使用命令式编程风格。这个我们下面会说。

2、Java8 函数式支持

SCF 包含并构建在 Java 定义的 3 个核心功能接口之上,并且自 Java 8 以来可供我们使用。

  • Supplier
  • Function<I, O>
  • Consumer

如果你对java的函数式接口了解的话,这几个大哥还是挺面熟的。

为了避免不断提到 Supplier、Function 和 Consumer,我们将它们称为 Functional bean。

简而言之,Application Context 中任何作为 Functional bean 的 bean 都将延迟注册到 FunctionCatalog。

就像我们上面做的那样,你需要做的就是在应用程序配置中声明 Supplier、Function 或 Consumer 类型的 @Bean。然后,您可以访问 FunctionCatalog 并根据其名称查找特定函数。

重要的地方是你要理解,上面的例子uppercase 函数不在是一个函数,而是一个 bean,如果我们不使用SCF,那么你依然可以直接从 ApplicationContext 中获取它,因为它再怎么抽象它还是bean,此时就和你以前用的那些@Bean没啥区别了,你就没有SCF的功能了。

反之当你集成了SCF的时候,你通过 FunctionCatalog 查找函数时,你是能玩SCF的那些功能的。此外,重要的是要了解典型用户不会直接使用 Spring Cloud Function。相反,典型用户实现 Java Function/Supplier/Consumer 的想法是在不同的执行上下文中使用它,而无需额外的工作。例如,相同的 java 函数可以通过SCF 提供的适配器以及使用SCF 作为核心编程模型的其他框架(例如 Spring Cloud Stream)表示为 REST 端点或流式消息处理程序或 AWS Lambda 等。因此,总而言之, SCF 使用可在各种执行上下文中使用的附加功能来检测 java 函数。这个看着有点抽象,但是我们后面会和Spring Cloud Stream一起整合使用你就懂了。

2.1、功能定义

虽然前面的示例展示了如何以编程方式在 FunctionCatalog 中查找函数,但在 Spring Cloud Function 被另一个框架(例如 Spring Cloud Stream)用作编程模型的典型集成情况下,您可以通过 spring.cloud.function.definition 属性声明要使用的函数,后面我们整合SCS会看到。了解在 FunctionCatalog 中发现函数时,了解一些默认行为非常重要。例如,如果你的ApplicationContext中只有一个Functional bean被注册的时候,那你查找函数的时候实际上不需要指定函数名,因为FunctionCatalog中的单个函数可以通过空名称或任何名称来查找。这个我们来演示一下。

java 复制代码
@SpringBootApplication
public class CloudfuncApplication {


	public static void main(String[] args) {
		ConfigurableApplicationContext context = SpringApplication.run(CloudfuncApplication.class, args);

		FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
		Function<String,String> uppercaseFunc1 = catalog.lookup("uppercase");
		Function<String,String> uppercaseFunc2 = catalog.lookup(null);
		Function<String,String> uppercaseFunc3 = catalog.lookup("xjbd");
		System.out.println(uppercaseFunc1.apply("world"));
		System.out.println(uppercaseFunc2.apply("world"));
		System.out.println(uppercaseFunc3.apply("world"));
	}

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

我们看到当我们整个容器中只有一个函数Bean被注册的时候,此时你catalog.lookup的时候传啥都能找到
这其实没啥,你就一个,那不是你是谁,找到很简单。

2.2、筛选不合格的函数

典型的 Application Context 可能包括 Bean,这些 Bean 是有效的 java 函数,但不是要注册到 FunctionCatalog 的候选 Bean。 这样的 bean 可以是来自其他项目的自动配置,也可以是符合成为 Java 函数条件的任何其他 bean。 该框架提供了已知 bean 的默认过滤,这些 bean 不应该成为向函数 catalog 注册的候选 bean。 您还可以通过使用 spring.cloud.function.ineligible-definitions 来进行配置,从而就可以排除你不想注册的 Functional bean了。

那我们在配置文件中加一行配置。

xml 复制代码
spring.cloud.function.ineligible-definitions=uppercase

我们把我这个唯一的函数bean给排除了,此时运行代码。报错了。

因为我们已经把它排出了,以后要是有些三方的这种bean你不想要了就可以这样干掉它。

2.3、Supplier

这是java8提供的函数式接口的一个,我们来看一下它的签名。

java 复制代码
@FunctionalInterface
public interface Supplier<T> {
    T get();
}
很简单,不接受参数,但是会返回一个参数。

我们来明确一个概念,这类函数bean其实是作为一个输出端的,因为我们调用它么,那么

在SCF中,从调用的角度来看,这对此类函数的实现者应该没有影响,他就是一个输出,我们调用。但是,当在框架(例如Spring Cloud Stream)中使用时,尤其是响应式的类型,通常用于表示流的源,他可以是一个源源不断的输出,因此它们被调用一次以获取消费者可以订阅的流(例如,Flux,响应式知识,不知道没关系)。换句话说,这些Supplier相当于无限流。但是,相同的响应式suppliers 也可以表示有限流(例如,轮询的 JDBC 数据上的结果集)。在这些情况下,这种响应式Supplier必须连接到底层框架的某种轮询机制,换言之我们再使用这种流式的数据供应的时候,Supplier实际上提供了对你流数据来源的框架比如(mq)的轮训获取数据,而不是一锤子获取完,因为他是流式的源源不断的。

下面我来演示说明。

为了帮助实现这一点, Spring Cloud Function 提供了一个 注解org.springframework.cloud.function.context.PollableBean 来表示此类supplier生成有限流,并且可能需要再次轮询。也就是说,重要的是要了解 Spring Cloud Function 本身没有为此 注解 提供任何行为。

此外,PollableBean 注解暴露了一个 splittable 属性,以表示生成的流需要被分割,可以参考分割

java 复制代码
public static void main(String[] args) {
	ConfigurableApplicationContext context = SpringApplication.run(CloudfuncApplication.class, args);

	FunctionCatalog catalog = context.getBean(FunctionCatalog.class);

	Supplier<Flux<String>> someSupplier = catalog.lookup("someSupplier");
	Flux<String> flux = someSupplier.get();
	System.out.println(flux.blockLast());

}

@PollableBean(splittable = true)
public Supplier<Flux<String>> someSupplier() {
	return () -> {
		String v1 = "1";
		String v2 = "2";
		String v3 = "3";
		return Flux.just(v1, v2, v3);
	};
}
此时我们等于定义了一个有限流,他是响应式的。这个我们后面再说我们继续走。

2.4、Function

Function也可以以命令式或反应式方式编写,但与 Supplier 和 Consumer 不同的是,实现者没有特别的考虑,只需了解在 Spring Cloud Stream 等框架中使用时,反应式函数只调用一次以传递对流的引用(即 Flux 或 Mono)后面就是订阅通知了,不用你再调了,而命令式函数(我们经常写这种)需要在每个事件中每次都调用来触发。

实现就参考我们上面的uppercase。

2.5、BiFunction

如果需要通过有效负载接收一些额外的数据(元数据),可以将函数签名设置为接收一个 Message,其中包含包含此类附加信息的标头映射。

java 复制代码
public static void main(String[] args) {
		ConfigurableApplicationContext context = SpringApplication.run(CloudfuncApplication.class, args);

		FunctionCatalog catalog = context.getBean(FunctionCatalog.class);

		Function<Message<String>, String>  uppercaseFunc = catalog.lookup("uppercase2");
		Message<String>message = new Message<>() {
			@Override
			public String getPayload() {
				return "hello";
			}

			@Override
			public MessageHeaders getHeaders() {
				MessageHeaders headers = new MessageHeaders(Map.of("id","1234567890"));
				return headers;
			}
		};
		System.out.println(uppercaseFunc.apply(message));


	}

	@Bean
	public Function<Message<String>, String> uppercase2() {
		return message -> message.getPayload().toUpperCase();
	}

是不是看起来很笨,为了让你的函数签名更轻、更 POJO,还有另一种方法。BiFunction。

java 复制代码
public static void main(String[] args) {
	ConfigurableApplicationContext context = SpringApplication.run(CloudfuncApplication.class, args);

	FunctionCatalog catalog = context.getBean(FunctionCatalog.class);

	BiFunction<String, Map, String> uppercaseFunc = catalog.lookup("uppercase3");
	System.out.println(uppercaseFunc.apply("hello",Map.of("key","value")));
}

@Bean
public BiFunction<String, Map, String> uppercase3() {
	return (input, headers) -> input.toUpperCase();
}

这样你就能携带更多东西处理了,当然我这里就处理了一个参数的转大写。你可以按照你的业务实现。鉴于 Message 仅包含两个属性(payload 和 headers)和 BiFunction 需要两个输入参数,框架将自动识别此范例并从 Message 中提取 payload,将其作为第一个参数传递,将 header 映射作为第二个参数传递。在这种情况下,你的函数也没有与 Spring 的消息传递 API 耦合。请记住,BiFunction 需要严格的签名,其中第二个参数必须是 Map。同样的规则也适用于 BiConsumer。

2.6、Consumer

Consumer 有点特殊,因为它有一个 void 返回类型,这意味着阻塞,至少是潜在的。很可能您不需要编写 Consumer<Flux<?>>,但如果您确实需要这样做,请记得订阅 flux,遵循响应式的订阅规则。

3、函数组合

函数组合是一种功能,允许将多个函数组合成一个。核心支持基于 Function.andThen(...) 提供的函数组合功能 从 Java 8 开始提供支持。SCF中还有一些其他的扩展。

3.1、声明式函数组合

此功能允许在提供 spring.cloud.function.definition 属性时使用 |(竖线)或 (逗号)分隔符以声明性方式提供组合说明。

比如这样:

java 复制代码
--spring.cloud.function.definition=uppercase|reverse

在这里,我们有效地提供了一个函数的定义,它本身是由函数 uppercase 和函数 reverse 组成的。事实上,这就是属性 name 是 definition 而不是 name 的原因之一,因为函数的定义可以是多个命名函数的组合。如前所述,您可以使用 , 而不是管道(例如 ...​definition=uppercase,reverse )。

我来演示一下:

java 复制代码
public static void main(String[] args) {
	ConfigurableApplicationContext context = SpringApplication.run(CloudfuncApplication.class, args);

	FunctionCatalog catalog = context.getBean(FunctionCatalog.class);

	Function<String, String> lookup = catalog.lookup("uppercase|reverse");
	System.out.println(lookup.apply("hello"));
}

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

@Bean
public Function<String, String> reverse() {
	return input -> "xxx" + input;
}
我们定义了两个函数bean,可以以|或者,分割的方式来串行调用,他是一个链式的调用,
一个调用的结果会传递给第二个继续调用。

3.2、编写非函数

Spring Cloud Function 还支持将 Supplier 与 Consumer 或 Function 以及 Function 与 Consumer 组合在一起。这里重要的是理解这些定义的最终结果。使用 Function 组合 Supplier 仍然会产生 Supplier,而将 Supplier 与 Consumer 组合将有效地呈现 Runnable。遵循相同的逻辑组合 Function 和 Consumer 将产生 Consumer。

当然,你不能组合不可组合的,比如 Consumer 和 Function、Consumer 和 Supplier 等。

他的意思就是你可以组合起来的是输出一样的类型,不然下面的接收不住那就不行,这个用的不多,我们用到再说。

4、函数路由和筛选

从 2.2 版本开始,Spring Cloud Function 提供了路由功能,允许您调用单个函数,该函数充当您希望调用的实际函数的路由器。在某些 FAAS 环境中,此功能非常有用,因为在这些环境中,维护多个函数的配置可能很麻烦,或者无法公开多个函数。其实等于一个网关函数,由这个函数来转发请求去调用其他的函数。

路由函数 在 FunctionCatalog 中以名称 functionRouter 注册。为了简单和一致,您还可以引用 RoutingFunction.FUNCTION_NAME 常量。

此函数具有以下签名:

java 复制代码
public class RoutingFunction implements Function<Object, Object> {
. . .
}

路由指令可以通过多种方式进行通信。我们支持通过 Message headers、System properties 以及 pluggable strategy 提供指令。那么让我们看看一些细节

4.1、MessageRoutingCallback

MessageRoutingCallback 是一种帮助确定 route-to 函数定义名称的策略。

java 复制代码
@SpringBootApplication
public class CloudfuncApplication {


	public static void main(String[] args) {
		ConfigurableApplicationContext context = SpringApplication.run(CloudfuncApplication.class, args);

		FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
		// 我们是通过函数名称和期望的输出MIME类型查找函数,进而路由过去
		SimpleFunctionRegistry.FunctionInvocationWrapper function = catalog.lookup(RoutingFunction.FUNCTION_NAME, "application/json");
		// 发起请求,之后就会被我们的路由器拦截到,然后根据请求体来处理
		String foo = "{\"foo\":\"blah\"}";
		Message<byte[]> fooResult = (Message<byte[]>) function.apply(MessageBuilder.withPayload(foo.getBytes()).build());
		String bar = "{\"bar\":\"blah\"}";
		Message<byte[]> barResult = (Message<byte[]>) function.apply(MessageBuilder.withPayload(bar.getBytes()).build());
		System.out.println(new String(fooResult.getPayload()));
		System.out.println(new String(barResult.getPayload()));
	}

}
java 复制代码
package com.cloudfunc.levi;

import org.springframework.cloud.function.context.MessageRoutingCallback;
import org.springframework.cloud.function.json.JsonMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;

@Configuration
public  class SamppleConfiguration {


    // 注册一个路由器回调,根据消息内容决定路由到哪个函数
    @Bean
    public MessageRoutingCallback messageRoutingCallback(JsonMapper jsonMapper) {
        return new MessageRoutingCallback() {
            @Override
            public String routingResult(Message<?> message) {
                String payload = new String((byte[]) message.getPayload());

                MessageBuilder<?> builder;
                String functionDefinition;
                // 根据消息内容决定路由到哪个函数
                if (payload.contains("foo")) {
                    builder = MessageBuilder.withPayload(jsonMapper.fromJson(payload,Foo.class));
                    functionDefinition = "foo";
                }
                else {
                    builder = MessageBuilder.withPayload(jsonMapper.fromJson(payload, Bar.class));
                    functionDefinition = "bar";
                }
                Message<?> m = builder.copyHeaders(message.getHeaders()).build();
                // 返回最后调用的函数
                return functionDefinition;
            }
        };
    }

    @Bean
    public Function<Message<Foo>, Message<String>> foo() {
        return foo -> {
            // 包装一下返回
            Message m = MessageBuilder.withPayload("foo").setHeader("originalId", foo.getHeaders().getId()).build();
            return m;
        };
    }

    @Bean
    public Function<Message<Bar>, Message<String>> bar() {
        return bar -> {
            // 包装一下返回
            Message m = MessageBuilder.withPayload("bar").setHeader("originalId", bar.getHeaders().getId()).build();
            return m;
        };
    }

    public static class Foo {
        private String foo;

        public String getFoo() {
            return foo;
        }

        public void setFoo(String foo) {
            this.foo = foo;
        }
    }

    public static class Bar {
        private String bar;

        public String getBar() {
            return bar;
        }

        public void setBar(String bar) {
            this.bar = bar;
        }
    }
}

其余的用的不多,我们后面遇到慢慢看。

5、函数 Arity

有时需要对数据流进行分类和组织。例如,考虑一个经典的大数据使用案例,即处理包含"订单"和"发票"的无组织数据,您希望每个数据都进入单独的数据存储。这就是函数 arity (具有多个输入和输出的函数) 支持发挥作用的地方。

这种涉及到响应式编程,我们不用太深究,可以参考这个代码。Arity

鉴于 Project Reactor 是 SCF 的核心依赖项,我们正在使用它的 Tuple 库。元组通过向我们传达基数和类型信息,为我们提供了独特的优势。在 SCSt 的上下文中,这两者都非常重要。Cardinality 让我们知道需要创建多少个输入和输出绑定并将其绑定到函数的相应输入和输出。了解类型信息可确保正确的类型转换。

此外,这也是绑定名称命名约定的 'index' 部分发挥作用的地方,因为在这个函数中,两个输出绑定名称是 organise-out-0 和 organise-out-1。

这些我们在后面的SAGA实现中会再次提到,所以我们就完成了这些东西的了解,接下来我们就开始我们的stream之旅。

相关推荐
一休哥助手11 小时前
分布式超低耦合,事件驱动架构(EDA)深度解析
分布式·架构
颯沓如流星12 小时前
软件架构设计方法之The Clean Architecture 整洁架构
架构·系统架构
HsuYang12 小时前
Rollup源码学习(七)——重识Rollup生成生命周期
前端·javascript·架构
杨荧14 小时前
【开源免费】基于Vue和SpringBoot的靓车汽车销售网站(附论文)
java·前端·javascript·vue.js·spring boot·spring cloud·开源
jmoych15 小时前
我在华为的安全日常
大数据·运维·网络·安全·华为·架构·云计算
Onlooker-轩逸16 小时前
Docker安装与架构
docker·容器·架构
--地平线--17 小时前
如何将 Java 微服务引入云
java·开发语言·微服务