Serverless與微服務探索(二)- SpringBoot專案部署實踐

Woody 發表於 2021-11-25
微服務 Spring Serverless

前言

上次的文章分享後,有粉絲反應內容太理論太抽象,看不到實際的樣子。

因此,我這裡就寫一篇教程,手把手教你如何把一個SpringBoot專案部署到Serverless並測試成功。

下面的連結是我發表到官方的文章,但官方的文章會綜合考慮,所以不會有那麼細的步驟。本文是最詳細的步驟。

SpringBoot + SCF 最佳實踐:實現待辦應用

本文章以騰訊雲Serverless雲函式為例,將分為事件函式和Web函式兩種教程。

事件函式就是指函式是由事件觸發的。

Web函式就是指函式可以直接傳送HTTP請求觸發函式。具體區別可以看這裡

兩者在Spring專案遷移改造上的區別在於:

  • 事件函式需要增加一個入口類。
  • Web函式需要修改埠為固定的9000。
  • 事件函式需要操作更多的控制檯配置。
  • Web函式需要增加一個scf_bootstrap啟動檔案,和不一樣的打包方式。

事件函式

Spring專案準備

事件函式示例程式碼下載地址:https://github.com/woodyyan/scf-springboot-java8/tree/eventfunction

示例程式碼介紹

@SpringBootApplication 類保持原狀不變。

package com.tencent.scfspringbootjava8;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ScfSpringbootJava8Application {

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

Controller類也會按照原來的寫法,保持不變。這裡以todo應用為例子。

記住此處的/todos 路徑,後面會用到。

程式碼如下:

package com.tencent.scfspringbootjava8.controller;

import com.tencent.scfspringbootjava8.model.TodoItem;
import com.tencent.scfspringbootjava8.repository.TodoRepository;
import org.springframework.web.bind.annotation.*;

import java.util.Collection;

@RestController
@RequestMapping("/todos")
public class TodoController {
    private final TodoRepository todoRepository;

    public TodoController() {
        todoRepository = new TodoRepository();
    }

    @GetMapping
    public Collection<TodoItem> getAllTodos() {
        return todoRepository.getAll();
    }

    @GetMapping("/{key}")
    public TodoItem getByKey(@PathVariable("key") String key) {
        return todoRepository.find(key);
    }

    @PostMapping
    public TodoItem create(@RequestBody TodoItem item) {
        todoRepository.add(item);
        return item;
    }

    @PutMapping("/{key}")
    public TodoItem update(@PathVariable("key") String key, @RequestBody TodoItem item) {
        if (item == null || !item.getKey().equals(key)) {
            return null;
        }

        todoRepository.update(key, item);
        return item;
    }

    @DeleteMapping("/{key}")
    public void delete(@PathVariable("key") String key) {
        todoRepository.remove(key);
    }
}

增加一個ScfHandler類,專案結構如下:
截圖2021-11-09 21.31.31.png

Scfhandle類主要用於接收事件觸發,並轉發訊息給Spring application,然後接收到Spring application的返回後把結果返回給呼叫方。

預設埠號為8080.

其程式碼內容如下:

package com.tencent.scfspringbootjava8;

import com.alibaba.fastjson.JSONObject;
import com.qcloud.services.scf.runtime.events.APIGatewayProxyRequestEvent;
import com.qcloud.services.scf.runtime.events.APIGatewayProxyResponseEvent;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import java.util.HashMap;
import java.util.Map;

public class ScfHandler {
    private static volatile boolean cold_launch;

    // initialize phase, initialize cold_launch
    static {
        cold_launch = true;
    }

    // function entry, use ApiGatewayEvent to get request
    // send to localhost:8080/hello as defined in helloSpringBoot.java
    public String mainHandler(APIGatewayProxyRequestEvent req) {
        System.out.println("start main handler");
        if (cold_launch) {
            System.out.println("start spring");
            ScfSpringbootJava8Application.main(new String[]{""});
            System.out.println("stop spring");
            cold_launch = false;
        }
        // 從api geteway event -> spring request -> spring boot port

        // System.out.println("request: " + req);
        // path to request
        String path = req.getPath();
        System.out.println("request path: " + path);

        String method = req.getHttpMethod();
        System.out.println("request method: " + method);

        String body = req.getBody();
        System.out.println("Body: " + body);

        Map<String, String> reqHeaders = req.getHeaders();
        // construct request
        HttpMethod httpMethod = HttpMethod.resolve(method);
        HttpHeaders headers = new HttpHeaders();
        headers.setAll(reqHeaders);
        RestTemplate client = new RestTemplate();
        HttpEntity<String> entity = new HttpEntity<>(body, headers);

        String url = "http://127.0.0.1:8080" + path;

        System.out.println("send request");
        ResponseEntity<String> response = client.exchange(url, httpMethod != null ? httpMethod : HttpMethod.GET, entity, String.class);
        //等待 spring 業務返回處理結構 -> api geteway response。
        APIGatewayProxyResponseEvent resp = new APIGatewayProxyResponseEvent();
        resp.setStatusCode(response.getStatusCodeValue());
        HttpHeaders responseHeaders = response.getHeaders();
        resp.setHeaders(new JSONObject(new HashMap<>(responseHeaders.toSingleValueMap())));
        resp.setBody(response.getBody());
        System.out.println("response body: " + response.getBody());
        return resp.toString();
    }
}

Gradle

這裡以gradle為例,與傳統開發不一樣的地方主要在於,build.gradle中需要加入全量打包的plugin,來保證所有用到的依賴都打入jar包中。

  1. 新增id 'com.github.johnrengelman.shadow' version '7.0.0' 這個plugin。
  2. 新增id 'application'
  3. 新增id 'io.spring.dependency-management' version '1.0.11.RELEASE'
  4. 指定mainClass

build.gradle具體內容如下:

plugins {
    id 'org.springframework.boot' version '2.5.5'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java-library'
    id 'application'
    id 'com.github.johnrengelman.shadow' version '7.0.0'
}

group = 'com.tencent'
version = '0.0.2-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
    mavenCentral()
}

dependencies {
    api 'org.springframework.boot:spring-boot-starter-web'
    api group: 'com.tencentcloudapi', name: 'tencentcloud-sdk-java', version: '3.1.356'
    api group: 'com.tencentcloudapi', name: 'scf-java-events', version: '0.0.4'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}

application {
    // Define the main class for the application.
    mainClass = 'com.tencent.scfspringbootjava8.ScfSpringbootJava8Application'
}

Maven

這裡以maven為例,與傳統開發不一樣的點主要在於,pom.xml需要加入maven-shade-plugin ,來保證所有用到的依賴都打入jar包中。同時需要指定mainClass,下面程式碼中的mainClass需要改為你自己的mainClass路徑。

pom.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>2.5.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>1.0</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
      <!-- Build an executable JAR -->
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <version>3.1.0</version>
      <configuration>
        <archive>
          <manifest>
            <addClasspath>true</addClasspath>
            <classpathPrefix>lib/</classpathPrefix>
            <mainClass>com.mypackage.MyClass</mainClass>
          </manifest>
        </archive>
      </configuration>
    </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <dependencies>
                    <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-maven-plugin</artifactId>
                        <version>2.1.1.RELEASE</version>
                    </dependency>
                </dependencies>
                <configuration>
                    <keepDependenciesWithProvidedScope>true</keepDependenciesWithProvidedScope>
                    <createDependencyReducedPom>true</createDependencyReducedPom>
                    <filters>
                        <filter>
                            <artifact>*:*</artifact>
                            <excludes>
                                <exclude>META-INF/*.SF</exclude>
                                <exclude>META-INF/*.DSA</exclude>
                                <exclude>META-INF/*.RSA</exclude>
                            </excludes>
                        </filter>
                    </filters>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                    <resource>META-INF/spring.handlers</resource>
                                </transformer>
                                <transformer
                                        implementation="org.springframework.boot.maven.PropertiesMergingResourceTransformer">
                                    <resource>META-INF/spring.factories</resource>
                                </transformer>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                    <resource>META-INF/spring.schemas</resource>
                                </transformer>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

編譯JAR包

下載程式碼之後,到該專案的根目錄,執行編譯命令:

  • Gradle專案執行:gradle build
  • Maven專案執行:mvn package

編譯完成後就能在當前專案的輸出目錄找到打包好的jar包。

  • Gradle專案:在build/libs目錄下看到打包好的jar包,這裡需要選擇字尾是-all的JAR包。如下圖。
  • Maven專案:在target目錄下能看到打包好的jar包,這裡需要選擇字首不帶orginal-的jar包。

一會部署函式的時候就用這個JAR包。

雲函式準備

雲函式建立

在函式服務中,點選新建,開始建立函式。

如下圖

  1. 選擇自定義建立
  2. 選擇事件函式
  3. 輸入一個函式名稱
  4. 執行環境選擇Java8
  5. 提交方法選擇本地上傳zip包
  6. 執行方法指定為包名.類名::入口函式名

    1. 比如此處是:com.tencent.scfspringbootjava8.ScfHandler::mainHandler
  7. 上傳那裡選擇前面編譯好的帶-all字尾的jar包。
    截圖2021-11-09 21.39.35.png

然後點選完成建立函式。

雲函式配置

建立完成之後,選擇函式管理-函式配置-編輯。如下圖。
Untitled.png

點開編輯之後,在環境配置中:

  1. 把記憶體修改為1024MB
  2. 把執行超時時間修改為15秒
    截圖2021-11-10 14.25.57.png

觸發器配置

在觸發管理中,建立觸發器。
1235.png

建立觸發器時,在下圖中:

  1. 觸發方式選擇API閘道器觸發。
  2. 整合響應勾選。
  3. 然後提交
    5325.png

建立完成之後需要修改一些API閘道器引數。點選API服務名進入修改。
235.png

點選右側的編輯按鈕修改。
23.png

第一個前端配置中,將路徑修改為Spring專案中的預設路徑。如下圖。
55.png

然後點選立即完成。

然後點選發布服務。
截圖2021-11-09 21.54.29.png

釋出完成之後回到雲函式控制檯。

開始測試

此處我們就以Controller裡面寫的第一個GET方法為例,如下圖,我們將獲得所有的todo items。
2345.png

在函式管理中,選擇函式程式碼,就可以很方便的進行測試。如下圖。

  1. 測試事件選擇“API Gateway事件模版”。
  2. 請求方式選擇GET
  3. Path填/todos
  4. 最後就可以點選測試按鈕。
    4443.png

測試結果和日誌將直接顯示在介面的右下方。如下圖。
2222.png

如果想要獲取完整的訪問URL,可以在觸發管理中,找到剛才建立的API閘道器觸發器,下面有可以訪問的URL。URL後面有複製按鈕。如下圖。
111.png


Web函式

Spring專案準備

示例程式碼介紹

Web函式示例程式碼下載地址:https://github.com/woodyyan/scf-springboot-java8/tree/webfunction

Web函式的專案程式碼相比事件函式更簡單。程式碼改造成本幾乎沒有。對原始碼的修改只有一個埠號。

Web函式則不需要ScfHandler入口類,專案結構如下:
666.png

因為web函式必須保證專案監聽埠為9000,所以需要將Spring監聽的埠改為9000。如下圖:
111235.png

程式碼部署包準備

程式碼包編譯方式參考上面的“編譯JAR包”。

然後新建一個scf_bootstrap啟動檔案,檔名字必須是scf_bootstrap,沒有字尾名。

  1. 第一行需有 #!/bin/bash
  2. java啟動命令必須是絕對路徑,java的絕對路徑是:/var/lang/java8/bin/java
  3. 請確保你的 scf_bootstrap 檔案具備777或755許可權,否則會因為許可權不足而無法執行。

因此啟動檔案內容如下:

#!/bin/bash
/var/lang/java8/bin/java -Dserver.port=9000 -jar scf-springboot-java8-0.0.2-SNAPSHOT-all.jar

接著,在scf_bootstrap檔案所在目錄執行下列命令來保證scf_bootstrap檔案可執行。

chmod 755 scf_bootstrap

然後將scf_bootstrap檔案和剛才編譯處理的scf-springboot-java8-0.0.2-SNAPSHOT-all.jar檔案,一起打包成zip檔案。如下圖。

打包好的zip檔案就是我們的部署包。
截圖2021-11-11 13.38.02.png

雲函式建立

在函式服務中,點選新建,開始建立函式。

如下圖

  1. 選擇自定義建立
  2. 選擇Web函式
  3. 輸入一個函式名稱
  4. 執行環境選擇Java8
  5. 提交方法選擇本地上傳zip包
  6. 上傳那裡選擇前面壓縮好的scf_spring_boot.zip包。
    截圖2021-11-11 13.40.28.png

然後在下面的高階配置中,寫上啟動命令,命令中的jar檔案應該是你編譯出來的jar檔案的名字。

因為web函式必須保證專案監聽埠為9000,所以命令中要指定一下埠。

更多關於啟動命令的寫法可以參考啟動檔案說明

如下圖:
13ewd.png

然後環境配置那裡,把記憶體改為512MB。執行超時時間設定為15秒。
23we.png

其他設定都使用預設的就可以了。然後點選完成。

點選完成之後如果沒有反應,是因為要先等待ZIP檔案上傳,才會開始建立函式。

因為Web函式預設會建立API閘道器觸發器,因此我們不需要單獨配置觸發器。

開始測試

此處我們就以Controller裡面寫的第一個GET方法為例,如下圖,我們將獲得所有的todo items。
awdz.png

在函式控制檯的函式程式碼裡面,我們可以直接測試我們的雲函式。

依據上面的程式碼,我們請求方式選擇GET,path填寫/todos,然後點選測試按鈕,然後就可以在右下角看到我們的結果了。

如果想在其他地方測試,可以複製下圖中的訪問路徑進行測試。
aeawe.png

最後

本教程沒有涉及映象函式,因為映象部署和原來的部署方式沒有差異。專案程式碼也不需要改造。理論上這是最適合微服務專案的方式。

下一篇文章中,我就會詳細分析Serverless中下面幾個話題了。

  • Serverless中的服務間呼叫
  • Serverless中的資料庫訪問
  • Serverless中的服務的註冊與發現
  • Serverless中的服務熔斷與降級
  • Serverless中的服務拆分