Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践

前言 上次的文章分享后,有粉丝反应内容太理论太抽象,看不到实际的样子。
因此,我这里就写一篇教程,手把手教你如何把一个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 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类,项目结构如下:
Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
文章图片

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 reqHeaders = req.getHeaders(); // construct request HttpMethod httpMethod = HttpMethod.resolve(method); HttpHeaders headers = new HttpHeaders(); headers.setAll(reqHeaders); RestTemplate client = new RestTemplate(); HttpEntity entity = new HttpEntity<>(body, headers); String url = "http://127.0.0.1:8080" + path; System.out.println("send request"); ResponseEntity 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具体内容如下:
4.0.0org.springframework.boot spring-boot-starter-parent 2.5.5 com.example demo 1.0 demo Demo project for Spring Boot1.8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.apache.maven.plugins maven-jar-plugin 3.1.0 true lib/ com.mypackage.MyClass org.apache.maven.plugins maven-shade-plugin org.springframework.boot spring-boot-maven-plugin 2.1.1.RELEASE true true *:* META-INF/*.SF META-INF/*.DSA META-INF/*.RSA package shade META-INF/spring.handlers META-INF/spring.factories META-INF/spring.schemas org.apache.maven.plugins maven-compiler-plugin 8 8

编译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包。
    Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
    文章图片
然后点击完成创建函数。
云函数配置
创建完成之后,选择函数管理-函数配置-编辑。如下图。
Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
文章图片

点开编辑之后,在环境配置中:
  1. 把内存修改为1024MB
  2. 把执行超时时间修改为15秒
    Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
    文章图片
触发器配置
在触发管理中,创建触发器。
Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
文章图片

创建触发器时,在下图中:
  1. 触发方式选择API网关触发。
  2. 集成响应勾选。
  3. 然后提交
    Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
    文章图片
创建完成之后需要修改一些API网关参数。点击API服务名进入修改。
Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
文章图片

点击右侧的编辑按钮修改。
Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
文章图片

第一个前端配置中,将路径修改为Spring项目中的默认路径。如下图。
Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
文章图片

然后点击立即完成。
然后点击发布服务。
Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
文章图片

【Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践】发布完成之后回到云函数控制台。
开始测试 此处我们就以Controller里面写的第一个GET方法为例,如下图,我们将获得所有的todo items。
Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
文章图片

在函数管理中,选择函数代码,就可以很方便的进行测试。如下图。
  1. 测试事件选择“API Gateway事件模版”。
  2. 请求方式选择GET
  3. Path填/todos
  4. 最后就可以点击测试按钮。
    Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
    文章图片
测试结果和日志将直接显示在界面的右下方。如下图。
Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
文章图片

如果想要获取完整的访问URL,可以在触发管理中,找到刚才创建的API网关触发器,下面有可以访问的URL。URL后面有复制按钮。如下图。
Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
文章图片

Web函数 Spring项目准备 示例代码介绍
Web函数示例代码下载地址:https://github.com/woodyyan/scf-springboot-java8/tree/webfunction
Web函数的项目代码相比事件函数更简单。代码改造成本几乎没有。对原代码的修改只有一个端口号。
Web函数则不需要ScfHandler入口类,项目结构如下:
Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
文章图片

因为web函数必须保证项目监听端口为9000,所以需要将Spring监听的端口改为9000。如下图:
Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
文章图片

代码部署包准备
代码包编译方式参考上面的“编译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文件就是我们的部署包。
Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
文章图片

云函数创建 在函数服务中,点击新建,开始创建函数。
如下图
  1. 选择自定义创建
  2. 选择Web函数
  3. 输入一个函数名称
  4. 运行环境选择Java8
  5. 提交方法选择本地上传zip包
  6. 上传那里选择前面压缩好的scf_spring_boot.zip包。
    Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
    文章图片
然后在下面的高级配置中,写上启动命令,命令中的jar文件应该是你编译出来的jar文件的名字。
因为web函数必须保证项目监听端口为9000,所以命令中要指定一下端口。
更多关于启动命令的写法可以参考启动文件说明。
如下图:
Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
文章图片

然后环境配置那里,把内存改为512MB。执行超时时间设置为15秒。
Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
文章图片

其他设置都使用默认的就可以了。然后点击完成。
点击完成之后如果没有反应,是因为要先等待ZIP文件上传,才会开始创建函数。
因为Web函数默认会创建API网关触发器,因此我们不需要单独配置触发器。
开始测试 此处我们就以Controller里面写的第一个GET方法为例,如下图,我们将获得所有的todo items。
Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
文章图片

在函数控制台的函数代码里面,我们可以直接测试我们的云函数。
依据上面的代码,我们请求方式选择GET,path填写/todos,然后点击测试按钮,然后就可以在右下角看到我们的结果了。
如果想在其他地方测试,可以复制下图中的访问路径进行测试。
Serverless与微服务探索(二)-|Serverless与微服务探索(二)- SpringBoot项目部署实践
文章图片

最后 本教程没有涉及镜像函数,因为镜像部署和原来的部署方式没有差异。项目代码也不需要改造。理论上这是最适合微服务项目的方式。
下一篇文章中,我就会详细分析Serverless中下面几个话题了。
  • Serverless中的服务间调用
  • Serverless中的数据库访问
  • Serverless中的服务的注册与发现
  • Serverless中的服务熔断与降级
  • Serverless中的服务拆分

    推荐阅读