前言
上次的文章分享後,有粉絲反應內容太理論太抽象,看不到實際的樣子。
因此,我這裡就寫一篇教程,手把手教你如何把一個SpringBoot專案部署到Serverless並測試成功。
下面的連結是我發表到官方的文章,但官方的文章會綜合考慮,所以不會有那麼細的步驟。本文是最詳細的步驟。
本文章以騰訊雲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
類,專案結構如下:
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包中。
- 新增
id 'com.github.johnrengelman.shadow' version '7.0.0'
這個plugin。 - 新增
id 'application'
- 新增
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
- 指定
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包。
雲函式準備
雲函式建立
在函式服務中,點選新建,開始建立函式。
如下圖
- 選擇自定義建立
- 選擇事件函式
- 輸入一個函式名稱
- 執行環境選擇Java8
- 提交方法選擇本地上傳zip包
執行方法指定為
包名.類名::入口函式名
- 比如此處是:
com.tencent.scfspringbootjava8.ScfHandler::mainHandler
- 比如此處是:
- 上傳那裡選擇前面編譯好的帶
-all
字尾的jar包。
然後點選完成建立函式。
雲函式配置
建立完成之後,選擇函式管理-函式配置-編輯。如下圖。
點開編輯之後,在環境配置中:
- 把記憶體修改為1024MB
- 把執行超時時間修改為15秒
觸發器配置
在觸發管理中,建立觸發器。
建立觸發器時,在下圖中:
- 觸發方式選擇API閘道器觸發。
- 整合響應勾選。
- 然後提交
建立完成之後需要修改一些API閘道器引數。點選API服務名進入修改。
點選右側的編輯按鈕修改。
第一個前端配置中,將路徑修改為Spring專案中的預設路徑。如下圖。
然後點選立即完成。
然後點選發布服務。
釋出完成之後回到雲函式控制檯。
開始測試
此處我們就以Controller裡面寫的第一個GET
方法為例,如下圖,我們將獲得所有的todo items。
在函式管理中,選擇函式程式碼,就可以很方便的進行測試。如下圖。
- 測試事件選擇“API Gateway事件模版”。
- 請求方式選擇
GET
- Path填
/todos
- 最後就可以點選測試按鈕。
測試結果和日誌將直接顯示在介面的右下方。如下圖。
如果想要獲取完整的訪問URL,可以在觸發管理中,找到剛才建立的API閘道器觸發器,下面有可以訪問的URL。URL後面有複製按鈕。如下圖。
Web函式
Spring專案準備
示例程式碼介紹
Web函式示例程式碼下載地址:https://github.com/woodyyan/scf-springboot-java8/tree/webfunction
Web函式的專案程式碼相比事件函式更簡單。程式碼改造成本幾乎沒有。對原始碼的修改只有一個埠號。
Web函式則不需要ScfHandler
入口類,專案結構如下:
因為web函式必須保證專案監聽埠為9000
,所以需要將Spring監聽的埠改為9000。如下圖:
程式碼部署包準備
程式碼包編譯方式參考上面的“編譯JAR包”。
然後新建一個scf_bootstrap啟動檔案,檔名字必須是scf_bootstrap
,沒有字尾名。
- 第一行需有
#!/bin/bash
。 - java啟動命令必須是絕對路徑,java的絕對路徑是:
/var/lang/java8/bin/java
- 請確保你的 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檔案就是我們的部署包。
雲函式建立
在函式服務中,點選新建,開始建立函式。
如下圖
- 選擇自定義建立
- 選擇Web函式
- 輸入一個函式名稱
- 執行環境選擇Java8
- 提交方法選擇本地上傳zip包
- 上傳那裡選擇前面壓縮好的
scf_spring_boot.zip
包。
然後在下面的高階配置中,寫上啟動命令,命令中的jar檔案應該是你編譯出來的jar檔案的名字。
因為web函式必須保證專案監聽埠為9000
,所以命令中要指定一下埠。
更多關於啟動命令的寫法可以參考啟動檔案說明。
如下圖:
然後環境配置那裡,把記憶體改為512MB。執行超時時間設定為15秒。
其他設定都使用預設的就可以了。然後點選完成。
點選完成之後如果沒有反應,是因為要先等待ZIP檔案上傳,才會開始建立函式。
因為Web函式預設會建立API閘道器觸發器,因此我們不需要單獨配置觸發器。
開始測試
此處我們就以Controller裡面寫的第一個GET
方法為例,如下圖,我們將獲得所有的todo items。
在函式控制檯的函式程式碼裡面,我們可以直接測試我們的雲函式。
依據上面的程式碼,我們請求方式選擇GET
,path填寫/todos
,然後點選測試按鈕,然後就可以在右下角看到我們的結果了。
如果想在其他地方測試,可以複製下圖中的訪問路徑進行測試。
最後
本教程沒有涉及映象函式,因為映象部署和原來的部署方式沒有差異。專案程式碼也不需要改造。理論上這是最適合微服務專案的方式。
下一篇文章中,我就會詳細分析Serverless中下面幾個話題了。
- Serverless中的服務間呼叫
- Serverless中的資料庫訪問
- Serverless中的服務的註冊與發現
- Serverless中的服務熔斷與降級
- Serverless中的服務拆分