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<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包中。

  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包。

然后点击完成创建函数。

云函数配置

创建完成之后,选择函数管理-函数配置-编辑。如下图。

点开编辑之后,在环境配置中:

  1. 把内存修改为1024MB
  2. 把执行超时时间修改为15秒

触发器配置

在触发管理中,创建触发器。

创建触发器时,在下图中:

  1. 触发方式选择API网关触发。
  2. 集成响应勾选。
  3. 然后提交

创建完成之后需要修改一些API网关参数。点击API服务名进入修改。

点击右侧的编辑按钮修改。

第一个前端配置中,将路径修改为Spring项目中的默认路径。如下图。

然后点击立即完成。

然后点击发布服务。

发布完成之后回到云函数控制台。

开始测试

此处我们就以Controller里面写的第一个 GET 方法为例,如下图,我们将获得所有的todo items。

在函数管理中,选择函数代码,就可以很方便的进行测试。如下图。

  1. 测试事件选择“API Gateway事件模版”。
  2. 请求方式选择 GET
  3. Path填 /todos
  4. 最后就可以点击测试按钮。

测试结果和日志将直接显示在界面的右下方。如下图。

如果想要获取完整的访问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 ,没有后缀名。

  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文件就是我们的部署包。

云函数创建

在函数服务中,点击新建,开始创建函数。

如下图

  1. 选择自定义创建
  2. 选择Web函数
  3. 输入一个函数名称
  4. 运行环境选择Java8
  5. 提交方法选择本地上传zip包
  6. 上传那里选择前面压缩好的 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中的服务拆分

原文:Serverless与微服务探索(二)- SpringBoot项目部署实践 - Woody的专栏 - SegmentFault 思否
作者: Woody