介绍

Spring Cloud Function 是一个使用函数实现业务逻辑的框架,允许使用相同的代码执行 Web 端点、流处理和任务,而不依赖于执行环境。
您可以在 AWS Lambda 或 Azure Functions 等无服务器环境中运行它。
关键是您可以在使用 Spring 的便捷特性的同时开发功能。

在本文中,我将尝试 Spring Cloud Functions × Azure Functions。
由于使用 Maven 的示例很多,我想使用 Gradle 创建一个项目。

创建项目

弹簧初始化创建一个 Spring Boot 项目模板。

Project  : Gradle Project
Language : Java
Packaging: Jar
Java     : 11 or 8 (17ではデプロイできなかった: 2022/11/02 時点)
その他は任意

Spring Cloud FunctionでAzure Functionsアプリ

设置文件

修改生成设置,以便可以使用 Azure Functions 的功能。

构建.gradle
plugins {
	id 'org.springframework.boot' version '2.7.5'
	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
	id "com.microsoft.azure.azurefunctions" version "1.11.0" // 追加
	id 'java'
}

group = 'azure.functions'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

compileJava.options.encoding = 'UTF-8'  // 追加

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.cloud:spring-cloud-function-adapter-azure:3.2.7' // 追加
	implementation 'com.microsoft.azure.functions:azure-functions-java-library:2.1.0'    // 追加
    // ↓は任意
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
     useJUnitPlatform()
}

// ↓追加
jar {
	enabled = true
    // jarファイル名を指定しないと、"${rootProject.name}-${version}-plan.jar"みたいなファイル名になって
    // 実行時に「jarが見つからない」と怒られたので、明示的に指定
	archiveFileName = "${rootProject.name}-${version}.jar"
    manifest {
        // メインクラスを指定
        attributes 'Main-Class' : 'azure.functions.springcloudapp.SpringCloudApplication'
    }
}

// Azureへのデプロイ設定
azurefunctions {
	resourceGroup = 'xxxxxxxxxxxxx'        // リソースグループ名
	appName = 'springcloudapp'             // Functionアプリ名
	region = 'eastus'                      // リージョン
	appServicePlanName = 'xxxxxxxxxxxxx'   // App Service Plan名
    // ↑他にも色々リソースの指定ができます
	runtime {
        os = 'windows'                     // 'linux'にしたらなぜかデプロイできなかった
	}
	appSettings {
		WEBSITE_RUN_FROM_PACKAGE = '1'
        FUNCTIONS_EXTENSION_VERSION = '~4'
    	FUNCTIONS_WORKER_RUNTIME = 'java'
    	MAIN_CLASS = 'azure.functions.springcloudapp.SpringCloudApplication'
	}
	auth {
		type = 'azure_cli'
	}
    // ローカルでデバッグするときは必要
	localDebug = 'transport=dt_socket,server=y,suspend=n,address=5005'
	deployment {
		type = 'run_from_blob'
	}
}
// ↑追加

将以下文件添加到项目根目录

host.json(使用 Azure Functions 扩展的设置)
{
  "version": "2.0",
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[3.*, 4.0.0)"
  }
}
local.settings.json(本地调试所需)
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "java",
    "MAIN_CLASS": "azure.functions.springcloudapp.SpringCloudApplication",
    "AzureWebJobsDashboard": ""
  }
}

函数创建

粗略地说,您需要创建 Handler 来处理每个触发器和 Function,这是实际的逻辑。
使用 Handler → Call Function 接收请求。

处理程序创建

创建一个处理程序类来处理触发器。
我在这里使用 HttpTrigger。

C 罕见的手和沙子。爪哇
// FunctionInvokerというクラスを継承する
public class CreateTodoHandler extends FunctionInvoker<TodoDto, TodoDto> {

  @FunctionName("saveTodo")  // デプロイ時の関数アプリ名
  public HttpResponseMessage saveTodo(
    // @HttpTriggerにHTTPの受付情報を設定する
    @HttpTrigger(
      name = "req",
      methods = { HttpMethod.POST },
      route = "todos",
      authLevel = AuthorizationLevel.FUNCTION
    ) HttpRequestMessage<TodoDto> request,
    ExecutionContext context
  ) {
    final TodoDto payload = request.getBody();

    if (payload == null) {
      return request.createResponseBuilder(HttpStatus.BAD_REQUEST).build();
    }

    // handleRequestメソッドを呼び出すと別で定義したFunctionが呼び出されて結果が返る
    // @FunctionNameで指定した名前でDIコンテナ内のFunctionを検索する
    final TodoDto todoCreated = handleRequest(payload, context);

    return request
      .createResponseBuilder(HttpStatus.CREATED)
      .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
      .header(
        HttpHeaders.LOCATION,
        UriComponentsBuilder
          .fromUri(request.getUri())
          .path("/{id}")
          .buildAndExpand(todoCreated.getId())
          .toString()
      )
      .body(todoCreated)
      .build();
  }
}

函数创建

实现实际的逻辑部分。
有两种写法。 (我个人更喜欢第一个)

  1. @Bean定义一个返回函数类型的方法
  2. @Component创建Funtion接口的实现类
    *除了功能,供应商和消费者也是可能的
    TodoFunction.java(@Bean 版本)
    @Component
    @RequiredArgsConstructor
    public class TodoFunction {
    
      // DIも使える
      private final TodoService todoService;
    
      // Handlerの@FunctionNameで指定した名前と同じ名前でDIコンテナに登録する
      // デフォルトではメソッド名で登録されるので、メソッド名を合わせておけば@Beanで明示的に名前指定しなくてもOK
      @Bean("saveTodo")
      public Function<TodoDto, TodoDto> saveTodo() {
        return (payload) -> {
          return todoService.saveTodo(payload);
        };
      }
    
      // 複数Functionを定義する場合はメソッドを追加する
    }
    
    TodoFunction.java(函数实现类版本)
    // Handlerの@FunctionNameで指定した名前と同じ名前でDIコンテナに登録する
    // デフォルトではクラス名(先頭は小文字)で登録される
    // ※この場合だとクラス名が「SaveTodo」だったら@Componentで明示的に名前指定しなくてもOK
    @Component("saveTodo")
    @RequiredArgsConstructor
    public class TodoFunction implements Function<TodoDto, TodoDto> {
    
      private final TodoService todoService;
    
      @Override
      public TodoDto apply(TodoDto payload) {
          return todoService.saveTodo(payload);
      }
    }
    
    // 複数Functionを定義する場合はクラスを追加する
    

    本地调试

    在项目的根目录下执行以下命令。

    ./gradlew azureFunctionsRun
    

    部署到 Azure

    在项目的根目录下执行以下命令。

    ./gradlew azureFunctionsDeploy
    

    奖金

    访问 ExecutionContext

    您可以在 Handler 的触发方法中接收ExecutionContext
    我认为这将主要用于日志记录。

    context.getLogger().info("hogehoge");
    

    在Function端使用这个上下文有点麻烦,但是需要使用Message
    这有点不方便,尽管它似乎很有可能登录到 Function 端。

    鲷鱼 c 地面温度。爪哇
    // Functionの入力をMessage(org.springframework.messaging.Message)にする
    @Bean
    public Function<Message<TodoDto>, TodoDto> saveTodo() {
      return message -> {
        // MessageのHeadersに"executionContext"というキーでExecutionContextがセットされてくる
        ExecutionContext context = 
          ExecutionContext.class.cast(message.getHeaders().get("executionContext"));
        context.getLogger().info("Invoke saveTodo");
        return todoService.saveTodo(message.getPayload());
      };
    } 
    

    每个Function都写这个比较麻烦,所以我创建了一个接口,将Function接口封装起来,使其通用。

    天蓝色 c 地面温度。爪哇
    public interface AzureFunction<I, O> extends Function<Message<I>, O> {
      public static final String CONTEXT_HEADER_KEY = "executionContext";
    
      @Override
      default O apply(Message<I> message) {
        ExecutionContext context =
          ExecutionContext.class.cast(
              message.getHeaders().get((CONTEXT_HEADER_KEY))
            );
    
        return apply(message.getPayload(), context);
      }
    
      /** handleRequestの引数とExecutionContextを受け取るメソッドを定義 */
      O apply(I payload, ExecutionContext context);
    }
    

    用法

    鲷鱼 c 地面温度。爪哇
      @Bean
      public AzureFunction<TodoDto, TodoDto> saveTodo() {
        return (payload, context) -> {
          context.getLogger().info("Invoke saveTodo");
          return todoService.saveTodo(payload);
        };
      }
    

    更容易使用......(?)
    但是,如果函数的参数是Publisher(Mono,Flux),则无法很好地转换类型。
    请让我知道是否有更聪明的方法。

    调用函数时的类型转换

    当你用Handler的handleRequest方法调用Function时,它会根据Function的参数自动转换类型。
    当我粗略检查时,它是这样的。

    输入类型
    (handleRequest 的参数)
    转换类型
    (函数论证)
    可兑换性 评论
    T。 集合<T> 转换为具有 1 个元素的集合
    T。 东西<T>
    T。 通量<T> 用 1 个元素转换为通量
    T。 消息<T> 输入值可以用Message::getPayload获取
    集合<T> T。 函数针对Collection的数量执行
    handleRequest 的返回值为 ArrayList<O1成为 >
    但是,如果 O 是一个集合,它就会变成一个平面列表。
    通量<T> T。 对 Flux 的数量执行函数
    handleRequest的返回值与↑相同
    消息<T> 消息<T> ×
    消息<T> T。 传递了Message::getPayload 的值
    HttpRequestMessage<T> HttpRequestMessage<T> ×
    HttpRequestMessage<T> T。 传递了HttpRequestMessage::getBody 的值
    handleRequest 返回HttpResponseMessage

    HttpTrigger 以外的触发器

    貌似HttpTrigger以外的触发器都可以正常使用了。
    看起来您可以使用以下触发器:
    (触发器本身是由 Azure 提供的,不是 Spring Cloud Function 提供的)

    • Blob 触发器
    • 定时器触发器
    • 卡夫卡触发器
    • 队列触发器
    • 预热触发器
    • EventHub 触发器
    • 事件网格触发器
    • ServiceBus 主题触发器
    • ServiceBusQueueTrigger

    我尝试使用TimerTriggerEventGridTrigger

    1. Function1 使用 TimerTrigger 发出 EventGrid 事件
    2. Function2 接收 EventGrid 事件并请求 Function3 (API)
    3. Function3 接收 Http 请求并将数据注册到 Cosmos DB

      创造一些你不太了解的东西。

      Function1 定时器触发
      public class TimerHandler extends FunctionInvoker<String, EventGridEvent> {
      
        public static final String FUNCTION_NAME = "timer";
      
        @FunctionName(FUNCTION_NAME)
        public void timer(
          // 1分毎に発火
          @TimerTrigger(name = "timer", schedule = "* * * * *") String timerInfo,
          @EventGridOutput(
            name = "outputEvent",
            topicEndpointUri = "MyEventGridTopicUriSetting",
            topicKeySetting = "MyEventGridTopicKeySetting"
          ) OutputBinding<EventGridEvent> outputEvent,
          ExecutionContext context
        ) {
          // handleRequestの戻り値をoutputEventにセットしている
          handleOutput(timerInfo, outputEvent, context);
        }
      }
      
      @Component
      @RequiredArgsConstructor
      public class TimerFunction {
      
        @Bean
        public AzureFunction<String, EventGridEvent> timer() {
          return (payload, context) -> {
            context.getLogger().info("Invoke timer: " + payload);
      
            final EventGridEvent document = new EventGridEvent();
            document.setId(UUID.randomUUID().toString());
            document.setEventType("MyCustomEvent");
            document.setEventTime(DateTimeUtil.formatDateTime(LocalDateTime.now()));
            document.setDataVersion("1.0");
            document.setSubject("EventGridSample");
            document.setData(new TodoDto(null, "Content", false, null));
      
            return document;
          };
        }
      }
      
      Function2 事件网格触发器
      public class EventGridHandler extends FunctionInvoker<EventSchema, TodoDto> {
      
        public static final String FUNCTION_NAME = "eventGrid";
      
        @FunctionName(FUNCTION_NAME)
        public void eventGrid(
          @EventGridTrigger(name = "event") EventSchema event,
          ExecutionContext context
        ) {
          final TodoDto output = handleRequest(event, context);
          context.getLogger().info(output.toString());
        }
      }
      
      @Component
      @RequiredArgsConstructor
      public class EventGridFunction {
      
        private final RestTemplate restTemplate;
      
        @Value("${FUNCTION_ENDPOINT}")
        private String baseEndpoint;
      
        @Value("${FUNCTION_KEY}")
        private String functionKey;
      
        @Bean
        public AzureFunction<EventSchema, TodoDto> eventGrid() {
          return (event, context) -> {
            context.getLogger().info("Event: " + event);
      
            // Function3 API 呼び出し
            RequestEntity<TodoDto> request = RequestEntity
              .post(baseEndpoint + "/todos?code={code}", functionKey)
              .contentType(MediaType.APPLICATION_JSON)
              .body(event.getData());
            ResponseEntity<TodoDto> response = restTemplate.exchange(
              request,
              TodoDto.class
            );
      
            return response.getBody();
          };
        }
      }
      

      功能3是处理程序创建,函数创建由...制作。

      概括

      通过使用 Spring Cloud Function,您将能够使用 Spring 函数开发 Functions。
      就个人而言,我很高兴能够进行 DI。
      也可以使用 HttpTrigger 以外的触发器,所以我想尝试各种事情。
      (貌似官方库也在开发中,所以以后可能会默认使用DI。)

      参考

      1. O 是函数的返回类型


原创声明:本文系作者授权爱码网发表,未经许可,不得转载;

原文地址:https://www.likecs.com/show-308632750.html

相关文章: