Spring Boot 打包与部署全面指南:从基础到高级实践

前言

作为Java开发者,掌握Spring Boot应用的打包与部署是必备技能。本文将全面系统地介绍Spring Boot应用的打包与部署方式,从基础到高级,涵盖各种场景和需求。

一、Spring Boot打包基础

1.1 打包格式对比

Spring Boot支持多种打包格式,以下是主要格式的对比:

打包格式文件扩展名特点适用场景
JAR.jar内嵌容器,可直接运行微服务、云原生应用
WAR.war需要外部容器部署传统企业应用,需部署到Tomcat等容器
ZIP.zip包含启动脚本和依赖需要脚本控制的部署
TAR.GZ.tar.gz压缩格式,节省空间Linux环境部署

1.2 打包配置

pom.xml中配置打包方式:

<packaging>jar</packaging>  <!-- 或 war -->

Spring Boot的Maven插件配置:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>2.7.0</version>
            <configuration>
                <executable>true</executable>  <!-- 设置为可执行 -->
            </configuration>
        </plugin>
    </plugins>
</build>

1.3 打包命令

# 普通打包
mvn package

# 跳过测试打包
mvn package -DskipTests

# 重新打包(清理后)
mvn clean package

打包后会在target目录下生成相应的文件,如myapp-0.0.1-SNAPSHOT.jar

二、JAR包部署详解

2.1 可执行JAR原理

Spring Boot的可执行JAR采用特殊结构:

myapp.jar
├── META-INF
│   └── MANIFEST.MF    # 包含Main-Class和Start-Class
├── BOOT-INF
│   ├── classes        # 应用类文件
│   └── lib            # 依赖库
└── org
    └── springframework
        └── boot
            └── loader # Spring Boot类加载器

2.2 运行JAR的多种方式

基本运行
java -jar myapp.jar
指定配置文件
java -jar myapp.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties
自定义JVM参数
java -Xms256m -Xmx1024m -jar myapp.jar
后台运行(Linux)
nohup java -jar myapp.jar > app.log 2>&1 &
服务化运行(Systemd)

创建服务文件/etc/systemd/system/myapp.service

[Unit]
Description=My Spring Boot Application
After=syslog.target

[Service]
User=appuser
ExecStart=/usr/bin/java -jar /opt/myapp/myapp.jar
SuccessExitStatus=143

[Install]
WantedBy=multi-user.target

然后启用服务:

sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp

三、WAR包部署详解

3.1 转换为WAR包

修改pom.xml
<packaging>war</packaging>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <scope>provided</scope>  <!-- 表示由容器提供 -->
    </dependency>
</dependencies>
修改启动类
@SpringBootApplication
public class MyApplication extends SpringBootServletInitializer {
    
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(MyApplication.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

3.2 部署到外部Tomcat

  1. 打包:mvn clean package
  2. 将生成的WAR文件复制到Tomcat的webapps目录
  3. 启动Tomcat:${TOMCAT_HOME}/bin/startup.sh

3.3 传统部署与云原生部署对比

特性传统WAR部署云原生JAR部署
容器依赖需要外部容器内嵌容器
部署方式文件复制直接运行
多实例复杂简单
启动速度较慢较快
资源占用较高较低
适用场景传统企业应用微服务、云环境

四、高级打包技巧

4.1 分类依赖打包

将依赖库分离,加快更新部署速度:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <layout>ZIP</layout>
        <includes>
            <include>
                <groupId>nothing</groupId>
                <artifactId>nothing</artifactId>
            </include>
        </includes>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>repackage</goal>
            </goals>
            <configuration>
                <classifier>exec</classifier>
            </configuration>
        </execution>
    </executions>
</plugin>

打包后会生成两个文件:

  • myapp.jar - 仅包含应用代码
  • myapp-exec.jar - 完整可执行JAR

4.2 自定义MANIFEST.MF

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <manifest>
            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
            <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
        </manifest>
        <manifestEntries>
            <Implementation-Vendor>My Company</Implementation-Vendor>
            <Built-By>${user.name}</Built-By>
        </manifestEntries>
    </configuration>
</plugin>

4.3 多环境打包

使用Profile
<profiles>
    <profile>
        <id>dev</id>
        <properties>
            <activatedProperties>dev</activatedProperties>
        </properties>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
    <profile>
        <id>prod</id>
        <properties>
            <activatedProperties>prod</activatedProperties>
        </properties>
    </profile>
</profiles>
指定Profile打包
mvn package -Pprod
多环境配置文件
resources/
├── application.yml
├── application-dev.yml
├── application-prod.yml
└── application-test.yml

五、Docker化部署

5.1 基础Dockerfile

# 使用OpenJDK官方镜像
FROM openjdk:11-jre-slim

# 维护者信息
LABEL maintainer="developer@company.com"

# 设置工作目录
WORKDIR /app

# 复制JAR文件到容器
COPY target/myapp.jar myapp.jar

# 暴露端口
EXPOSE 8080

# 启动命令
ENTRYPOINT ["java", "-jar", "myapp.jar"]

构建并运行:

docker build -t myapp .
docker run -p 8080:8080 -d myapp

5.2 多阶段构建(优化镜像大小)

# 第一阶段:构建
FROM maven:3.6.3-jdk-11 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src /app/src
RUN mvn package -DskipTests

# 第二阶段:运行
FROM openjdk:11-jre-slim
WORKDIR /app
COPY --from=build /app/target/myapp.jar myapp.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "myapp.jar"]

5.3 最佳实践Dockerfile

FROM eclipse-temurin:17-jdk-jammy as builder
WORKDIR /workspace/app

COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
COPY src src

RUN ./mvnw install -DskipTests
RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)

FROM eclipse-temurin:17-jre-jammy
VOLUME /tmp
ARG DEPENDENCY=/workspace/app/target/dependency
COPY --from=builder ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=builder ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=builder ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","com.example.MyApplication"]

5.4 Docker Compose部署

docker-compose.yml示例:

# 指定 Docker Compose 文件的版本,这里使用的是 3.8 版本,不同版本可能有不同的特性和语法支持
version: '3.8'

# services 部分定义了要部署的服务列表
services:
  # 定义名为 app 的服务
  app:
    # 指定服务使用的镜像,这里使用名为 myapp:latest 的镜像。
    # 如果本地不存在该镜像,Docker Compose 会尝试拉取。
    image: myapp:latest
    # 配置构建镜像的相关信息,. 表示在当前目录下查找 Dockerfile 来构建镜像。
    # 如果同时指定了 image 和 build,优先使用 build 构建镜像。
    build: .
    # 端口映射配置,将容器的 8080 端口映射到宿主机的 8080 端口,
    # 这样外部可以通过宿主机的 8080 端口访问容器内的服务。
    ports:
      - "8080:8080"
    # 环境变量配置,设置名为 SPRING_PROFILES_ACTIVE 的环境变量,值为 prod,
    # 通常用于指定 Spring 应用的运行环境。
    environment:
      - SPRING_PROFILES_ACTIVE=prod
    # 卷挂载配置,将宿主机当前目录下的 logs 目录挂载到容器内的 /app/logs 目录,
    # 实现数据持久化和共享,方便查看容器内生成的日志。
    volumes:
      - ./logs:/app/logs
    # 健康检查配置,用于定期检查服务是否正常运行
    healthcheck:
      # 执行的检查命令,使用 curl 命令检查 http://localhost:8080/actuator/health 地址是否可访问,
      # -f 选项表示如果请求失败不显示错误信息。
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      # 检查的时间间隔,每隔 30 秒执行一次健康检查。
      interval: 30s
      # 单次检查的超时时间,如果 10 秒内未收到响应则认为检查失败。
      timeout: 10s
      # 最大重试次数,如果连续 3 次检查失败,则认为服务不健康。
      retries: 3

  # 定义名为 redis 的服务
  redis:
    # 指定服务使用的镜像,这里使用 redis:alpine 镜像,alpine 是一个轻量级的 Linux 发行版。
    image: redis:alpine
    # 端口映射配置,将容器的 6379 端口映射到宿主机的 6379 端口,
    # 以便外部可以通过宿主机的 6379 端口访问 Redis 服务。
    ports:
      - "6379:6379"
    # 卷挂载配置,将名为 redis_data 的卷挂载到容器内的 /data 目录,
    # 用于持久化存储 Redis 的数据。
    volumes:
      - redis_data:/data

# volumes 部分定义了要使用的卷
volumes:
  # 定义名为 redis_data 的卷,Docker 会自动管理这个卷的创建和删除。
  redis_data:

六、云原生部署

6.1 Kubernetes部署

基础Deployment
# 定义 Kubernetes API 版本,apps/v1 表明使用应用相关的 API 组版本
apiVersion: apps/v1
# 定义资源类型,这里是 Deployment 资源,用于管理应用的副本数量和滚动更新等
kind: Deployment
# 元数据部分,包含资源的名称和标签等信息
metadata:
  # Deployment 的名称,用于在 Kubernetes 集群中唯一标识该 Deployment
  name: myapp-deployment
  # 标签,用于对资源进行分类和选择,这里定义了一个名为 app 的标签,值为 myapp
  labels:
    app: myapp
# 规范部分,定义了 Deployment 的具体配置
spec:
  # 指定应用的副本数量,这里设置为 3 个,意味着 Kubernetes 会确保始终有 3 个 Pod 运行该应用
  replicas: 3
  # 选择器,用于指定 Deployment 管理哪些 Pod,这里通过匹配标签 app=myapp 来选择 Pod
  selector:
    matchLabels:
      app: myapp
  # Pod 模板,定义了 Deployment 创建的 Pod 的配置
  template:
    # Pod 的元数据,包含标签等信息
    metadata:
      # 标签,用于对 Pod 进行分类和选择,这里的标签要与 selector 中的匹配标签一致
      labels:
        app: myapp
    # Pod 的规范部分,定义了 Pod 中容器的配置
    spec:
      # 容器列表,这里只定义了一个容器
      containers:
      # 容器的名称,用于在 Pod 中唯一标识该容器
      - name: myapp
        # 容器使用的镜像,这里指定了镜像的仓库地址和版本号
        image: myregistry/myapp:1.0.0
        # 镜像拉取策略,Always 表示每次创建或重启 Pod 时都会尝试从镜像仓库拉取最新的镜像
        imagePullPolicy: Always
        # 容器暴露的端口,这里指定容器内部监听的端口为 8080
        ports:
        - containerPort: 8080
        # 环境变量列表,用于向容器内传递配置信息
        env:
        # 环境变量的名称
        - name: SPRING_PROFILES_ACTIVE
          # 环境变量的值,这里指定 Spring 应用使用生产环境配置
          value: "prod"
        # 资源请求和限制配置
        resources:
          # 资源请求,告诉 Kubernetes 为该容器分配的最小资源量
          requests:
            # CPU 请求,500m 表示 0.5 个 CPU 核心
            cpu: "500m"
            # 内存请求,512Mi 表示 512 兆字节的内存
            memory: "512Mi"
          # 资源限制,限制容器使用的最大资源量
          limits:
            # CPU 限制,1 表示 1 个 CPU 核心
            cpu: "1"
            # 内存限制,1Gi 表示 1 吉字节的内存
            memory: "1Gi"
        # 存活探针配置,用于检测容器是否正常运行
        livenessProbe:
          # 使用 HTTP GET 请求进行检测
          httpGet:
            # 请求的路径,这里通过访问 /actuator/health 端点来检测应用的健康状态
            path: /actuator/health
            # 请求的端口,与容器暴露的端口一致
            port: 8080
          # 初始延迟时间,容器启动后等待 30 秒再开始进行存活检测
          initialDelaySeconds: 30
          # 检测周期,每隔 10 秒进行一次存活检测
          periodSeconds: 10
        # 就绪探针配置,用于检测容器是否准备好接收请求
        readinessProbe:
          # 使用 HTTP GET 请求进行检测
          httpGet:
            # 请求的路径,这里通过访问 /actuator/health 端点来检测应用的健康状态
            path: /actuator/health
            # 请求的端口,与容器暴露的端口一致
            port: 8080
          # 初始延迟时间,容器启动后等待 20 秒再开始进行就绪检测
          initialDelaySeconds: 20
          # 检测周期,每隔 5 秒进行一次就绪检测
          periodSeconds: 5
Service配置
# 定义 Kubernetes API 版本,v1 是核心 API 组的版本
apiVersion: v1
# 定义资源类型,这里是 Service 资源,用于为一组 Pod 提供统一的网络访问入口
kind: Service
# 元数据部分,包含资源的名称等信息
metadata:
  # Service 的名称,用于在 Kubernetes 集群中唯一标识该 Service
  name: myapp-service
# 规范部分,定义了 Service 的具体配置
spec:
  # 选择器,用于指定 Service 要代理的 Pod。这里通过匹配标签 app=myapp 来选择 Pod,
  # 意味着这个 Service 会将请求转发到带有 app=myapp 标签的 Pod 上
  selector:
    app: myapp
  # 端口配置,定义了 Service 如何接收和转发流量
  ports:
    # 端口配置项,这里定义了一个端口规则
    - protocol: TCP
      # Service 对外暴露的端口,外部客户端可以通过这个端口访问 Service
      port: 80
      # 目标端口,Service 接收到的流量会被转发到后端 Pod 的这个端口上
      targetPort: 8080
  # Service 的类型,LoadBalancer 表示使用云提供商的负载均衡器来将流量分发到后端 Pod。
  # 当创建这种类型的 Service 时,云提供商会自动创建一个外部负载均衡器,并将其 IP 地址分配给该 Service
  type: LoadBalancer

6.2 Helm Chart部署

目录结构:

myapp-chart/
├── Chart.yaml
├── values.yaml
├── templates/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   └── configmap.yaml
└── charts/

示例values.yaml

replicaCount: 3

image:
  repository: myregistry/myapp
  tag: 1.0.0
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

resources:
  limits:
    cpu: 1
    memory: 1Gi
  requests:
    cpu: 500m
    memory: 512Mi

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80

七、性能优化与监控

7.1 JVM调优参数

常用JVM参数表:

参数说明示例
-Xms初始堆大小-Xms512m
-Xmx最大堆大小-Xmx1024m
-XX:MetaspaceSize元空间初始大小-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize元空间最大大小-XX:MaxMetaspaceSize=256m
-XX:+UseG1GC使用G1垃圾收集器-XX:+UseG1GC
-XX:MaxGCPauseMillis最大GC停顿时间目标-XX:MaxGCPauseMillis=200
-XX:ParallelGCThreads并行GC线程数-XX:ParallelGCThreads=4
-XX:ConcGCThreads并发GC线程数-XX:ConcGCThreads=2

7.2 Spring Boot Actuator监控

配置application.yml

management:
  endpoint:
    health:
      show-details: always
    metrics:
      enabled: true
    prometheus:
      enabled: true
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  metrics:
    export:
      prometheus:
        enabled: true
    tags:
      application: ${spring.application.name}

7.3 性能监控指标

关键监控指标表:

指标路径说明
JVM内存/actuator/metrics/jvm.memory.usedJVM内存使用情况
HTTP请求/actuator/metrics/http.server.requestsHTTP请求统计
系统CPU/actuator/metrics/system.cpu.usage系统CPU使用率
线程信息/actuator/metrics/jvm.threads.live活动线程数
垃圾回收/actuator/metrics/jvm.gc.pauseGC暂停时间

八、安全部署实践

8.1 安全加固措施

安全措施对照表:

措施实现方式说明
禁用敏感端点management.endpoints.web.exposure.exclude=env,beans限制暴露的端点
启用HTTPSserver.ssl.enabled=true加密通信
认证保护spring.security.user.name/password基本认证
内容安全策略添加安全头防止XSS等攻击
最小权限原则使用非root用户运行降低风险

8.2 使用非root用户运行

Dockerfile示例:

FROM openjdk:11-jre-slim

RUN addgroup --system appuser && adduser --system --no-create-home --ingroup appuser appuser

WORKDIR /app
COPY --chown=appuser:appuser target/myapp.jar myapp.jar

USER appuser

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "myapp.jar"]

8.3 密钥管理

使用环境变量或密钥管理服务:

# application.yml
spring:
  datasource:
    password: ${DB_PASSWORD}

Kubernetes中使用Secret:

apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
data:
  username: YWRtaW4=
  password: cGFzc3dvcmQxMjM=

然后在Deployment中引用:

env:
- name: DB_PASSWORD
  valueFrom:
    secretKeyRef:
      name: db-secret
      key: password

九、持续集成与持续部署(CI/CD)

9.1 GitHub Actions示例

.github/workflows/build-deploy.yml:

name: Build and Deploy

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    
    - name: Set up JDK 11
      uses: actions/setup-java@v2
      with:
        java-version: '11'
        distribution: 'adopt'
    
    - name: Build with Maven
      run: mvn -B package --file pom.xml -DskipTests
      
    - name: Login to Docker Hub
      uses: docker/login-action@v1
      with:
        username: ${{ secrets.DOCKER_HUB_USERNAME }}
        password: ${{ secrets.DOCKER_HUB_TOKEN }}
    
    - name: Build and push Docker image
      run: |
        docker build -t ${{ secrets.DOCKER_HUB_USERNAME }}/myapp:${{ github.sha }} .
        docker push ${{ secrets.DOCKER_HUB_USERNAME }}/myapp:${{ github.sha }}
  
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
    - name: Install kubectl
      uses: azure/setup-kubectl@v1
      
    - name: Deploy to Kubernetes
      run: |
        echo "${{ secrets.KUBE_CONFIG }}" > kubeconfig.yaml
        export KUBECONFIG=kubeconfig.yaml
        kubectl set image deployment/myapp myapp=${{ secrets.DOCKER_HUB_USERNAME }}/myapp:${{ github.sha }}

9.2 Jenkins Pipeline示例

Jenkinsfile:

pipeline {
    agent any
    
    environment {
        DOCKER_IMAGE = 'myregistry/myapp'
        KUBECONFIG = credentials('kubeconfig')
    }
    
    stages {
        stage('Build') {
            steps {
                sh 'mvn clean package -DskipTests'
            }
        }
        
        stage('Test') {
            steps {
                sh 'mvn test'
            }
            post {
                always {
                    junit '**/target/surefire-reports/*.xml'
                }
            }
        }
        
        stage('Build Docker Image') {
            steps {
                script {
                    docker.build("${DOCKER_IMAGE}:${env.BUILD_ID}")
                }
            }
        }
        
        stage('Push Docker Image') {
            steps {
                script {
                    docker.withRegistry('https://myregistry.com', 'dockerhub') {
                        docker.image("${DOCKER_IMAGE}:${env.BUILD_ID}").push()
                    }
                }
            }
        }
        
        stage('Deploy to Kubernetes') {
            steps {
                sh """
                    kubectl apply -f k8s/deployment.yaml
                    kubectl set image deployment/myapp myapp=${DOCKER_IMAGE}:${env.BUILD_ID}
                """
            }
        }
    }
    
    post {
        success {
            slackSend channel: '#deployments', 
                      color: 'good', 
                      message: "Deployment succeeded: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
        }
        failure {
            slackSend channel: '#deployments', 
                      color: 'danger', 
                      message: "Deployment failed: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
        }
    }
}

十、故障排查与日志管理

10.1 常见问题排查表

问题现象可能原因解决方案
应用启动失败端口冲突检查端口使用netstat -tulnp,修改server.port
内存溢出内存不足或内存泄漏增加JVM内存,分析内存dump
响应缓慢数据库查询慢或GC频繁优化SQL,调整JVM参数
连接池耗尽连接泄漏或配置不当检查连接关闭,调整连接池大小
健康检查失败依赖服务不可用检查依赖服务,设置合理的超时

10.2 日志配置最佳实践

application.yml示例:

logging:
  level:
    root: INFO
    org.springframework.web: DEBUG
    com.myapp: DEBUG
  file:
    name: logs/app.log
    max-history: 30
    max-size: 100MB
  pattern:
    file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
    console: "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID}){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx"

10.3 日志收集架构

推荐使用ELK(Elasticsearch+Logstash+Kibana)或EFK(Elasticsearch+Fluentd+Kibana)栈:

  1. 应用输出结构化日志(JSON格式)
  2. Filebeat或Fluentd收集日志
  3. 发送到Logstash进行过滤和处理
  4. 存储到Elasticsearch
  5. 通过Kibana可视化

Docker Compose日志配置示例:

services:
  app:
    image: myapp
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "3"

总结

本文全面介绍了Spring Boot应用的打包与部署,从基础的JAR/WAR打包到高级的云原生部署,涵盖了各种场景和最佳实践。关键点总结:

  1. 打包选择:根据场景选择JAR(云原生)或WAR(传统部署)
  2. 部署方式:从简单命令行到容器化、Kubernetes集群部署
  3. 性能优化:合理配置JVM参数和Spring Boot特性
  4. 安全实践:最小权限原则、密钥管理、安全加固
  5. 自动化:通过CI/CD实现高效可靠的部署流程
  6. 可观测性:完善的日志和监控是生产环境必备

转发就算了,毕竟这么好笑的东西不能独享。


想了解更多的可以关注微信公众号:“Eric的技术杂货库”,后期会有更多的干货以及资料下载。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Clf丶忆笙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值