Serverless and microservice exploration - SpringBoot project deployment practice

preface

After sharing the last article, some fans responded that the content was too theoretical and abstract to see the actual appearance.

Therefore, I will write a tutorial here to teach you how to deploy a SpringBoot project to Serverless and test it successfully.

The following link is my official article, but the official article will be considered comprehensively, so there will be no such detailed steps. This article is the most detailed step.

SpringBoot + SCF best practice: implementing to-do applications

Taking the Serverless cloud function of Tencent cloud as an example, this article will be divided into two tutorials: event function and Web function.

Event function means that the function is triggered by an event.

Web functions are functions that can trigger HTTP requests directly. The specific differences can be seen here.

The differences between the two in Spring project migration and transformation are as follows:

  • The event function needs to add an entry class.
  • The Web function needs to change the port to fixed 9000.
  • Event functions need to operate more console configurations.
  • The Web function needs to add an scf_bootstrap startup files are packaged differently from.

Event function

Spring project preparation

Event function sample code download address: https://github.com/woodyyan/scf-springboot-java8/tree/eventfunction

Sample code introduction

@The SpringBootApplication class remains unchanged.

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);
    }
}

The Controller class will also be written in the original way and remain unchanged. Here, take todo application as an example.

Remember the / todos path here, which will be used later.

The code is as follows:

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);
    }
}

Add a ScfHandler class. The project structure is as follows:

Scfhandle class is mainly used to receive event triggers, forward messages to Spring application, and then return the results to the caller after receiving the return of Spring application.

The default port number is 8080

The code is as follows:

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;
        }
        // From 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);
        //Wait for the spring business to return the processing structure - > 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

Taking gradle as an example, the main difference from traditional development is that a fully packaged plugin needs to be added to build.gradle to ensure that all dependencies used are entered into the jar package.

  1. Add the plugin ID 'com. GitHub. John rengelman. Shadow' version '7.0.0'.
  2. Add id 'application'
  3. Add ID 'io. Spring. Dependency management' version '1.0.11. Release'
  4. Specify mainClass.

The details of build.gradle are as follows:

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

Taking maven as an example, the main difference from traditional development is that pom.xml needs to add maven shade plugin to ensure that all dependencies used are entered into the jar package. At the same time, you need to specify mainClass. The mainClass in the following code needs to be changed to your own mainClass path.

pom.xml is as follows:

<?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>

Compile JAR package

After downloading the code, go to the root directory of the project and run the compilation command:

  • Gradle project run: gradle build
  • Maven project run: mvn package

After compilation, you can find the packaged jar package in the output directory of the current project.

  • Gradle project: see the packaged jar package in the build/libs directory. Here, you need to select the jar package with the suffix - all. See the figure below.
  • Maven project: you can see the packaged jar package in the target directory. Here, you need to select the jar package without prefix original -.

Use this JAR package when deploying functions later.

Cloud function preparation

Cloud function creation

In the function service, click new to start creating the function.

As shown below

  1. Select custom create
  2. Select event function
  3. Enter a function name
  4. Select Java 8 as the running environment
  5. Select the local upload zip package as the submission method
  6. The execution method is specified as package name. Class name:: entry function name

    1. The comparison is: com.tencent.scfspringbootjava8.ScfHandler::mainHandler
  7. Upload there and select the previously compiled jar package with - all suffix.

Then click Finish to create the function.

Cloud function configuration

After creation, select function management - function configuration - edit. See the figure below.

After clicking Edit, in the environment configuration:

  1. Change the memory to 1024MB
  2. Modify the execution timeout to 15 seconds

Trigger configuration

In trigger management, create a trigger.

When creating a trigger, in the following figure:

  1. Select API gateway as the trigger method.
  2. Integration response check.
  3. Then submit

After creation, you need to modify some API gateway parameters. Click API service name to enter modify.

Click the Edit button on the right to modify.

In the first front-end configuration, change the path to the default path in the Spring project. See the figure below.

Then click finish now.

Then click publish service.

After publishing, return to the cloud function console.

Start test

Here we take the first GET method written in the Controller as an example. As shown in the figure below, we will GET all todo items.

In function management, you can easily test by selecting function code. See the figure below.

  1. Select "API Gateway event template" for the test event.
  2. Select GET as the request method
  3. Path / todos
  4. Finally, you can click the test button.

The test results and logs will be displayed directly at the bottom right of the interface. See the figure below.

If you want to obtain the complete access URL, you can find the API gateway trigger just created in trigger management. The accessible URL is shown below. There is a copy button after the URL. See the figure below.

Web function

Spring project preparation

Sample code introduction

Web function sample code download address: https://github.com/woodyyan/scf-springboot-java8/tree/webfunction

The project code of the Web function is simpler than the event function. The cost of code transformation is almost zero. The modification of the original code has only one port number.

The ScfHandler entry class is not required for Web functions. The project structure is as follows:

Because the web function must ensure that the listening port of the project is 9000, it is necessary to change the listening port of Spring to 9000. As shown below:

Code deployment package preparation

Refer to "compiling JAR package" above for the compilation method of code package.

Then create a new one scf_bootstrap boot file , file name must be scf_bootstrap, no suffix.

  1. The first line needs to have  #!/ bin/bash.
  2. The Java startup command must be an absolute path. The absolute path of Java is: / var/lang/java8/bin/java
  3. Please make sure your SCF_ The bootstrap file has 777 or 755 permissions, otherwise it cannot be executed because of insufficient permissions.

Therefore, the contents of the startup file are as follows:

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

Then, in SCF_ Execute the following command in the directory where the bootstrap file is located to ensure SCF_ The bootstrap file is executable.

chmod 755 scf_bootstrap

Then SCF_ The bootstrap file and the scf-springboot-java8-0.0.2-SNAPSHOT-all.jar file just compiled and processed are packaged into a zip file. See the figure below.

The packaged zip file is our deployment package.

Cloud function creation

In the function service, click new to start creating the function.

As shown below

  1. Select custom create
  2. Select Web function
  3. Enter a function name
  4. Select Java 8 as the running environment
  5. Select the local upload zip package as the submission method
  6. Upload there and select the previously compressed scf_spring_boot.zip package.

Then, in the following advanced configuration, write the startup command. The jar file in the command should be the name of the jar file you compiled.

Because the web function must ensure that the project listening port is 9000, specify the port in the command.

For more information on how to write the startup command, please refer to Startup file description.

As shown below:

Then configure the environment there and change the memory to 512MB. The execution timeout is set to 15 seconds.

Just use the default settings for other settings. Then click finish.

If there is no response after clicking finish, it is because you have to wait for the ZIP file to be uploaded before you start creating the function.

Because Web functions create API gateway triggers by default, we do not need to configure triggers separately.

Start test

Here we take the first GET method written in the Controller as an example. As shown in the figure below, we will GET all todo items.

In the function code of the function console, we can directly test our cloud functions.

According to the above code, we select GET as the request method, fill in / todos as path, and then click the test button. Then we can see our results in the lower right corner.

If you want to test elsewhere, you can copy the access path in the figure below for testing.

last

This tutorial does not cover the image function, because there is no difference between the image deployment and the original deployment. The project code does not need to be modified. Theoretically, this is the most suitable way for microservice projects.

In the next article, I will analyze the following topics in Serverless in detail.

  • Inter service call in Serverless
  • Database access in Serverless
  • Registration and discovery of services in Serverless
  • Service corruption and degradation in Serverless
  • Service splitting in Serverless

Tags: Spring Spring Boot Microservices function serverless

Posted on Wed, 24 Nov 2021 16:28:13 -0500 by saltedm8