spring-boot 2.5.4,nacos 作為配置、服務發現中心,Cloud Native Buildpacks 打包鏡像,GitLab CI/CD

spring-boot 2.5.4,nacos 作為配置、服務發現中心,Cloud Native Buildpacks 打包鏡像,GitLab CI/CD

本文主要介紹 Java 通過 Cloud Native Buildpacks 打包鏡像,通過 Gitlab 配置 CI/CD。以及使用 nacos 作為配置中心,使用 grpc 作為 RPC 框架。

前置條件:

  • JDK 版本:1.8
  • gradle 版本:7.1
  • spring-boot 版本:2.5.4
  • nacos 版本:1.3.1
  • GitLab 配置

spring-boot gradle 插件

spring-boot gradle 插件在 gradle 中提供 spring-boot 支持。該插件可以打 jar 或者 war 包。

plugins {
	id 'org.springframework.boot' version '2.5.4'
}

新建一個 gradle 項目,該項目在只引用 id 'org.springframework.boot' version '2.5.4' 插件的情況下,gralde 任務分佈完全沒有變化,如下圖所示。

引入 java 插件

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.5.4'
}

但當引入 java 插件後,情況就大大不同了,可見,spring-boot 插件和 java 插件一起應用後,將產生如下反應:

  1. 創建bootJar任務,執行該任務會生成一個 fat jar。該 jar 包把所有的類文件打包進 BOOT-INF/classes 中,把項目依賴的所有 jar 包打包進 BOOT-INF/lib 中。

  2. 配置 assemble 任務,該任務依賴於 bootJar 任務,所以執行 assemble 任務的時候也會執行 bootJar

  3. 配置 jar 任務,該任務可以配置 jar 包的 classifier。配置方式如下,默認情況下 classifier 為空字符串:

    bootJar {
        classifier = 'boot'
    }
    
    jar {
        classifier = ''
    }
    
  4. 創建 bootBuildImage 任務,該任務可以使用 CNB 打包 OCI 鏡像。後面會詳細介紹如何使用 CNB。

  5. 創建 bootRun 任務用於運行應用程序。

  6. 創建 bootArchives 配置,注意這裡是配置,不是任務。當應用 maven 插件時會為 bootArchives 配置創建 uploadBootArchives 任務。bootArchives 默認情況下包含 bootJarbootWar 任務生成的文件。

    uploadBootArchives {
        repositories {
            mavenDeployer {
                repository url: '//repo.example.com'
            }
        }
    }
    
  7. 創建 developmentOnly 配置。該配置用於管理開發時的依賴,比如 org.springframework.boot:spring-boot-devtools,該依賴僅在開發時使用,無需打進 jar 包中。

    dependencies {
        developmentOnly 'org.springframework.boot:spring-boot-devtools'
    }
    
  8. 創建 productionRuntimeClasspath 配置。它等價於 runtimeClasspath 中的依賴減去 developmentOnly 配置中的依賴。

  9. 配置 JavaCompile 任務默認使用 UTF-8

  10. 配置 JavaCompile 任務使用 -parameters 配置編譯器參數。

引入 io.spring.dependency-management 插件

引入該插件後,將自動管理依賴版本。

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.5.4'
    id "io.spring.dependency-management" version "1.0.11.RELEASE"
}

group 'com.toy'
version '1.0.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
}

引入 grpc 框架

基於本示例使用 nacos 作為服務發現中心,本示例將使用 net.devh:grpc-spring-boot-starter 依賴作為框架。

工程結構

目前為止,我們介紹了 java 項目中引入 spring gradle 所需的插件,以及各個組件的作用。接下來我們介紹如何引入 grpc,以及引入 grpc 後,我們的工程結構。

改造後工程結構總體如下:

protobuf

用於保存 proto 文件,以及發佈 proto 文件,當客戶端引用時,保證 jar 包最小。build.gradle 文件內容如下:

plugins {
    id 'java'
    id 'idea'
    id 'com.google.protobuf' version '0.8.17' //google proto 插件
    id 'maven-publish'
}

group 'com.toy'
version '1.0.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    //用於生成 java 類
    compileOnly 'io.grpc:grpc-protobuf:1.39.0'
    compileOnly 'io.grpc:grpc-stub:1.39.0'
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.17.3"
    }
    plugins {
        grpc {
            artifact = 'io.grpc:protoc-gen-grpc-java:1.39.0'
        }
    }

    generateProtoTasks {
        all()*.plugins {
            grpc {
            }
        }
    }
}

publishing {
    publications {
        proto_package(MavenPublication) {
        }
    }
    repositories {
        maven {
            allowInsecureProtocol = true
            url '你的 Maven 倉庫地址'
            credentials {
                username = 'Maven 賬號'
                password = 'Maven 密碼'
            }
        }
    }
}

生成的 Java 類路徑為 $projectName/build/.. 如下所示,生成的所有 class 文件位於 proto 文件夾下:

rpc

  1. 在 rpc 項目中添加啟動類 ToyApplication,內容如下:

    package com.toy.rpc;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    /**
     * @author Zhang_Xiang
     * @since 2021/8/20 15:34:58
     */
    @SpringBootApplication(scanBasePackages = {"com.toy.*"})
    public class ToyApplication {
        public static void main(String[] args) {
            SpringApplication.run(ToyApplication.class, args);
        }
    }
    
  2. 在包 com.toy.rpc.impl 中添加 HelloImpl 文件,內容如下:

    package com.toy.rpc.impl;
    
    import com.toy.proto.GreeterGrpc;
    import com.toy.proto.HelloReply;
    import com.toy.proto.HelloRequest;
    import io.grpc.stub.StreamObserver;
    import net.devh.boot.grpc.server.service.GrpcService;
    
    /**
     * @author Zhang_Xiang
     * @since 2021/8/20 15:35:56
     */
    @GrpcService
    public class HelloImpl extends GreeterGrpc.GreeterImplBase {
    
        @Override
        public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
            HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + request.getName()).build();
            responseObserver.onNext(reply);
            responseObserver.onCompleted();
        }
    }
    
  3. 添加集成測試

    (1)添加集成測試配置

    package com.toy.config;
    
    import net.devh.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration;
    import net.devh.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration;
    import net.devh.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration;
    import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
    import org.springframework.boot.test.context.TestConfiguration;
    
    /**
    * @author Zhang_Xiang
    * @since 2021/8/12 16:26:25
    */
    @TestConfiguration
    @ImportAutoConfiguration({
            GrpcServerAutoConfiguration.class, // Create required server beans
            GrpcServerFactoryAutoConfiguration.class, // Select server implementation
            GrpcClientAutoConfiguration.class}) // Support @GrpcClient annotation
    public class IntegrationTestConfigurations {
    
    }
    

    (2)添加測試類

    package com.toy;
    
    import com.toy.config.IntegrationTestConfigurations;
    import com.toy.proto.GreeterGrpc;
    import com.toy.proto.HelloReply;
    import com.toy.proto.HelloRequest;
    import net.devh.boot.grpc.client.inject.GrpcClient;
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.annotation.DirtiesContext;
    import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
    
    import static org.junit.jupiter.api.Assertions.assertEquals;
    
    /**
    * @author Zhang_Xiang
    * @since 2021/8/20 16:02:41
    */
    @SpringBootTest(properties = {
            "grpc.server.inProcessName=test", // Enable inProcess server
            "grpc.server.port=-1", // Disable external server
            "grpc.client.inProcess.address=in-process:test" // Configure the client to connect to the inProcess server
    })
    @SpringJUnitConfig(classes = {IntegrationTestConfigurations.class})
    @DirtiesContext
    public class HelloServerTest {
    
        @GrpcClient("inProcess")
        private GreeterGrpc.GreeterBlockingStub blockingStub;
    
        @Test
        @DirtiesContext
        public void sayHello_replyMessage() {
            HelloReply reply = blockingStub.sayHello(HelloRequest.newBuilder().setName("Zhang").build());
            assertEquals("Hello Zhang", reply.getMessage());
        }
    }
    
    
  4. build.gradle

    plugins {
        id 'java'
        id 'idea'
        id 'org.springframework.boot' version '2.5.4'
        id "io.spring.dependency-management" version "1.0.11.RELEASE"
    }
    
    group 'com.toy'
    version '1.0.0-SNAPSHOT'
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        implementation platform('io.grpc:grpc-bom:1.39.0') //使所有 protobuf 插件的版本保持一致
        implementation 'net.devh:grpc-spring-boot-starter:2.12.0.RELEASE'
        developmentOnly 'org.springframework.boot:spring-boot-devtools'
    
        implementation project(':protobuf') //引入 protobuf 項目
    
        testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.2'
        testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2'
        testImplementation 'io.grpc:grpc-testing'
        testImplementation('org.springframework.boot:spring-boot-starter-test')
    }
    
    bootBuildImage {
        imageName = "harbor.xxx.com/rpc/${project.name}:${project.version}"
        publish = true
        docker {
        publishRegistry {
                username = "admin"
                password = "admin"
                url = "harbor.xxx.com"
            }
        }
    }
    
    test {
        useJUnitPlatform()
    }
    

至此,整個 grpc 項目基礎結構完成。

添加 nacos 配置中心、服務發現

  1. 在 rpc 項目 build.gradle 文件中引入讀取 nacos 配置的 jar 包和註冊服務到 nacos 中的 jar 包。

    dependencies{
        implementation 'org.springframework.boot:spring-boot-starter-web' //用於註冊服務
        //添加此引用的原因是為了解決 spring boot 2.5.4 無法讀取 nacos 配置的問題
        implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap:3.0.3'
        implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery:2021.1'
        implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config:2021.1'
    }
    
  2. 添加讀取服務配置,在 rpc 項目中添加 bootstrap.propertise,內容如下:

    spring.profiles.active=dev
    spring.application.name=toy
    

    添加 bootstrap-dev.properties,內容如下:

    spring.cloud.nacos.config.server-addr=127.0.0.1:8848
    spring.cloud.nacos.config.namespace=52f2f610-46f6-4c57-a089-44072099adde
    spring.cloud.nacos.config.file-extension=yaml
    spring.cloud.nacos.config.group=DEFAULT_GROUP
    spring.cloud.nacos.discovery.namespace=52f2f610-46f6-4c57-a089-44072099adde
    spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
    

至此,完成了服務端通過 nacos 讀取配置,並且把服務端註冊到 nacos 中。

gitlab CI/CD

在根項目目錄下添加 .gitlab-ci.yml 文件。當 gitlab 安裝了 runner 後,將自動觸發 CI/CD,內容如下:

variables:
  CONTAINER_NAME: toy
  IMAGE_VERSION: 1.0.0
  IMAGE_TAG: harbor.xxx.com/toy/rpc
  PORT: 10086

stages:
  - test
  - publishJar
  - bootBuildImage //spring-boot 從 2.3.0 版本以後引入了 BootBuildImage 任務。
  - deploy

test:
  stage: test
  script:
    - gradle clean
    - gradle rpc:test

publishProtoBuf:
  stage: publishJar
  script:
    - gradle protobuf:publish

bootBuildImage:
  stage: bootBuildImage
  script:
    - gradle rpc:bootBuildImage

deployDev:
  stage: deploy
  script:
    - ssh $SERVER_USER@$SERVER_IP "docker login --username=$REGISTERY_NAME --password=$REGISTRY_PWD harbor.xxx.com; docker pull $IMAGE_TAG:$IMAGE_VERSION;"
    - ssh $SERVER_USER@$SERVER_IP "docker container rm -f $CONTAINER_NAME || true"
    - ssh $SERVER_USER@$SERVER_IP "docker run -d -p $PORT:$PORT -e JAVA_OPTS='-Xms512m -Xmx512m -Xss256K'  --net=host --name $CONTAINER_NAME $IMAGE_TAG:$IMAGE_VERSION"
  when: manual

這幾個步驟什麼意思呢?

  • 定義項目級別的變量
  • 定義了 4 個步驟,其中每個步驟中的任務又是可以並行的
    • test:運行項目中的單元測試(項目中沒有寫單元測試)、集成測試
    • publishJar:發佈項目中 protobuf 項目到私有 maven 倉庫中
    • bootBuildImage:打包鏡像,並根據配置發佈到鏡像倉庫中,這裡打包過程需要詳細說明
    • deploy:部署鏡像到遠程服務器中,在此步驟中配置了 when:manual,意思是手動觸發此步驟

注意: 這裡 SERVER_USERSERVER_IP$REGISTERY_NAME$REGISTRY_PWD 在 Gitlab 中通過超級管理員做了全局配置,即在所有項目中都可以使用。

定義 gitlab CI/CD 變量

CI/CD 變量一共有 4 種定義方式,如下:

  1. .gitlab-ci.yml 文件中定義
  2. 在項目中定義
  3. 在組中定義
  4. gitlab 全局變量

變量優先級(從高到低)

  1. 觸發變量、流水線變量、手動流水線變量
  2. 項目變量
  3. 組變量
  4. 全局變量
  5. 繼承變量
  6. .gitlab-ci.yml 文件中,job 中定義的變量
  7. .gitlab-ci.yml 中定義的變量,job 外的變量
  8. 部署變量
  9. 預定義變量

源碼地址