19、使用 Spring Boot 2 构建微服务

使用 Spring Boot 2 构建微服务

1. Spring Boot 执行方法

Spring Boot 可以通过以下方法执行:
- 使用可执行 JAR 文件,通过 $ java -jar 命令,搭配以下嵌入式 Servlet 容器:
- Tomcat 8.5
- Jetty 9.4
- Undertow 1.4
- 通过传统 WAR 部署到任何实现 Servlet 3.1+ 规范的应用服务器或 Servlet 容器中。

最新版本(撰写本文时为 v2.0.5)支持使用 Maven 或 Gradle 进行构建,运行需要 Java 8 或更高版本,并且基于 Spring 5.0.9 版本作为核心。

2. Maven 设置

Apache Maven 是最常见的构建管理系统,可作为 Java 应用程序的事实上的构建和打包操作工具。Spring Boot 与 Apache Maven 3.2 或更高版本兼容,为了轻松管理所有 Spring Boot 依赖项(特别是正确的版本),可以将 Maven POM 文件设置为继承自 spring-boot-starter-parent 项目。

以下是一个 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>myproject</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <!-- Inherit defaults from Spring Boot -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
    </parent>
    <!-- Add typical dependencies for a web application -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    <!-- Package as an executable jar -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

使用以下命令构建和打包应用程序:

$ mvn clean package
3. Gradle 设置

Gradle 是一个开源的构建自动化工具,使用 Groovy 或 Kotlin DSL 编写的脚本。它得到了主要 IDE 的支持,可以使用命令行界面或通过持续集成服务器运行。

安装后,在项目根目录下运行以下命令创建新项目(或自动将现有 Maven 项目转换为 Gradle 项目):

$ gradle init

为了在 Gradle 项目中使用 Spring Boot,可以创建如下 Gradle 文件:

plugins {
    id 'org.springframework.boot' version '2.0.5.RELEASE'
    id 'java'
}
jar {
    baseName = 'myproject'
    version = '0.0.1-SNAPSHOT'
}
repositories {
    jcenter()
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-dependencies:2.0.5.RELEASE'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

构建可执行 JAR 文件:

$ ./gradlew mySpringBootJar

运行可执行 JAR 文件:

$ java -jar build/libs/gradle-my-spring-boot-project.jar

或者使用以下 Gradle 命令运行:

$ ./gradlew bootRun
4. 从早期版本升级 Spring Boot

Spring Boot 提供了一项功能,使开发人员能够在应用程序启动时分析应用程序环境并打印结果。还可以使用属性迁移器启动器自动迁移应用程序属性。

要激活此环境,需在项目中添加以下 Maven 依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-properties-migrator</artifactId>
    <scope>runtime</scope>
</dependency>

完成分析和迁移后,确保从项目依赖项中移除该模块。

5. 构建 Spring Boot 微服务

这里将实现一个足球运动员微服务,它将处理足球运动员领域,暴露 CRUD API,并使用 PostgreSQL 数据库存储和检索信息。

构建应用程序需要以下工具及安装信息:
| 工具名称 | 安装信息 |
| ---- | ---- |
| Apache Maven 3.5.4 | 安装链接 |
| JDK 1.8.0_171 | 可使用 Oracle JDK 或 OpenJDK,推荐 OpenJDK 安装链接 |
| Spring Boot 2.0.5 | 后续介绍安装说明 |
| Docker Community Edition 18.06.1 | 安装链接 |

6. 项目详情

构建微服务源代码需要在系统上安装 PostgreSQL,这里使用 Docker 来安装和管理 PostgreSQL。

以下是安装和配置 PostgreSQL 的步骤:
1. 安装 Docker 后,打开新的终端窗口,运行以下命令启动 PostgreSQL 容器:

$ docker run --name postgres_springboot -e POSTGRES_PASSWORD=postgresPwd -e POSTGRES_DB=football_players_registry -d -p 5532:5432 postgres
  1. 验证容器是否启动,运行 $ docker ps -a 命令,查看创建的容器及其相对状态。
  2. 查看容器日志,运行 $ docker logs -f <容器 ID> 命令,获取 PostgreSQL 状态信息。
  3. 连接到容器,运行 $ docker exec -it <容器 ID> bash 命令。
  4. 登录 PostgreSQL,运行 $ psql -U postgres 命令。
  5. 验证数据库列表,运行 $ \l 命令。
  6. 连接到 football_players_registry 数据库,运行 $ \connect football_players_registry 命令。
  7. 创建 FOOTBALL_PLAYER 表,运行以下命令:
CREATE TABLE FOOTBALL_PLAYER(
    ID SERIAL PRIMARY KEY NOT NULL,
    NAME VARCHAR(50) NOT NULL,
    SURNAME VARCHAR(50) NOT NULL,
    AGE INT NOT NULL,
    TEAM VARCHAR(50) NOT NULL,
    POSITION VARCHAR(50) NOT NULL,
    PRICE NUMERIC
);
  1. 检查表格结构,运行 $ \d+ football_player 命令。
7. 创建源代码

使用 Spring Initializr 项目生成器工具( https://start.spring.io/ )获取项目骨架。实现微服务将使用以下组件:
- Web:包含使用 Tomcat Servlet 容器和 Spring MVC 进行完整 Web 开发所需的所有模块。
- Actuator:提供生产就绪功能,帮助监控和管理应用程序。
- DevTools:Spring Boot 开发工具。
- JPA:Java 持久化 API,包括 spring-data-jpa spring-orm 和 Hibernate。
- PostgreSQL:PostgreSQL JDBC 驱动程序。

设置项目的 Maven Group 名称为 com.packtpub.springboot ,Artifact 为 football-player-microservice 。点击“Generate Project”创建并下载项目骨架的 ZIP 文件,解压到指定目录,使用喜欢的 IDE 打开 Maven 项目。

核心的 pom.xml 文件部分内容如下:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.5.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.packtpub.springboot</groupId>
<artifactId>football-player-microservice</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Spring Boot Football player microservice</name>
<description>Demo project for Spring Boot</description>

src/main/resources 目录下的 application.properties 文件中设置数据库配置:

## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5532/football_players_registry
spring.datasource.username= postgres
spring.datasource.password=postgresPwd
# This property always initialize the database using sql scripts set under resources directory
spring.datasource.initialization-mode=always
# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
spring.jpa.hibernate.ddl-auto=none

运行以下命令进行首次构建:

$ mvn clean package

构建完成后,运行项目:

$ java -jar target/football-player-microservice-0.0.1-SNAPSHOT.jar

使用执行器健康检查验证应用程序是否正确启动,访问 http://localhost:8080/actuator/health ,确保结果为 STATUS: "UP"

8. 实体类 - JPA

需要一个领域模型对象来映射插入数据库的记录,使用 JPA 规范创建实体类。

创建 com.packtpub.springboot.footballplayermicroservice.model 包,在其中创建 FootballPlayer 类:

import java.io.Serializable;
import java.math.BigInteger;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import javax.xml.bind.annotation.XmlRootElement;

/**
 * Domain model class that maps the data stored into football_player table
 * inside database.
 *
 * @author Mauro Vocale
 * @version 1.0.0 29/09/2018
 */
@Entity
@Table(name = "football_player")
@XmlRootElement
@NamedQueries({
    @NamedQuery(name = "FootballPlayer.findAll", query = "SELECT f FROM FootballPlayer f")
})
public class FootballPlayer implements Serializable {
    private static final long serialVersionUID = -92346781936044228L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Basic(optional = false)
    @Column(name = "id")
    private Integer id;
    @Basic(optional = false)
    @NotNull
    @Size(min = 1, max = 50)
    @Column(name = "name")
    private String name;
    @Basic(optional = false)
    @NotNull
    @Size(min = 1, max = 50)
    @Column(name = "surname")
    private String surname;
    @Basic(optional = false)
    @NotNull
    @Column(name = "age")
    private int age;
    @Basic(optional = false)
    @NotNull
    @Size(min = 1, max = 50)
    @Column(name = "team")
    private String team;
    @Basic(optional = false)
    @NotNull
    @Size(min = 1, max = 50)
    @Column(name = "position")
    private String position;
    @Column(name = "price")
    private BigInteger price;

    public FootballPlayer() {
    }

    public FootballPlayer(String name, String surname, int age,
                          String team, String position, BigInteger price) {
        this.name = name;
        this.surname = surname;
        this.age = age;
        this.team = team;
        this.position = position;
        this.price = price;
    }
}
9. 仓库 - JPA

Spring Data JPA 具有在运行时自动从仓库接口创建仓库实现的功能。

创建 com.packtpub.springboot.footballplayermicroservice.repository 包,在其中创建 FootballPlayerRepository 接口:

import org.springframework.data.repository.CrudRepository;

/**
 * FootballPlayerRepository extends the CrudRepository interface.
 * The type of entity and ID that it works with, FootballPlayer and Integer, are
 * specified in the generic parameters on CrudRepository.
 * By extending CrudRepository, FootballPlayerRepository inherits several
 * methods for working with FootballPlayer persistence, including methods
 * for saving, deleting, and finding FootballPlayer entities.
 *
 * @author Mauro Vocale
 * @version 30/09/2018
 */
public interface FootballPlayerRepository extends CrudRepository<FootballPlayer, Integer> {
}

为了完成数据库访问操作管理,在 src/main/resources 目录下创建以下文件:
- schema.sql :包含表创建 SQL 命令的文件。
- data.sql :包含 SQL 插入数据命令的文件。

运行以下命令再次启动应用程序:

$ mvn spring-boot:run

可以看到嵌入式 Tomcat 执行了 SQL 创建模式和加载数据脚本。

10. RESTful 网络服务

Spring 框架提供了一种直观且易于使用的模型,通过 RESTful 网络服务暴露数据,这是微服务之间 API 通信的事实上的标准。2.0.5 版本会自动将 CRUD 操作暴露为 RESTful API。

FootballPlayerRepository 接口继承了 org.springframework.data.repository.CrudRepository 的以下方法:

public <S extends T> S save(S s);
public <S extends T> Iterable<S> saveAll(Iterable<S> itrbl);
public Optional<T> findById(ID id);
public boolean existsById(ID id);
public Iterable<T> findAll();
public Iterable<T> findAllById(Iterable<ID> itrbl);
public long count();
public void deleteById(ID id);
public void delete(T t);
public void deleteAll(Iterable<? extends T> itrbl);
public void deleteAll();

要自动暴露这些方法,需在 Maven pom.xml 文件中插入 spring-boot-starter-data-rest 依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>

编译并运行项目:

$ mvn clean package && mvn spring-boot:run

打开浏览器访问 http://localhost:8080/ ,可以获取可用 API 的链接。访问 http://localhost:8080/footballPlayers 可以获取应用程序中存储的足球运动员列表。

也可以采用旧风格的方法,创建服务类和控制器类来调用 JPA 层并暴露 API。

创建 FootballPlayerService 类:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.Iterable;

@Service
public class FootballPlayerService {
    @Autowired
    private FootballPlayerRepository repository;

    public Iterable<FootballPlayer> findAll() {
        return repository.findAll();
    }

    public FootballPlayer save(FootballPlayer entity) {
        return repository.save(entity);
    }

    public void deleteById(Integer id) {
        repository.deleteById(id);
    }

    public Optional<FootballPlayer> findById(Integer id) {
        return repository.findById(id);
    }
}

创建 FootballPlayerRESTController 类:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.Optional;
import java.util.Iterable;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;

@RestController
@RequestMapping("/footballplayer")
public class FootballPlayerRESTController {
    @Autowired
    private FootballPlayerService service;

    @RequestMapping(method = RequestMethod.GET, produces = "application/json")
    public Iterable<FootballPlayer> findAll() {
        return service.findAll();
    }

    @RequestMapping(value = "/save", method = RequestMethod.POST, produces = "application/json")
    public FootballPlayer save(@RequestBody FootballPlayer entity) {
        return service.save(entity);
    }

    @RequestMapping(value = "/update/{id}", method = RequestMethod.PUT, produces = "application/json")
    public FootballPlayer edit(@PathVariable Integer id, @RequestBody FootballPlayer entity) {
        return service.save(entity);
    }

    @RequestMapping(value = "/delete/{id}", method = RequestMethod.DELETE, produces = "application/json")
    public void delete(@PathVariable Integer id) {
        service.deleteById(id);
    }

    @RequestMapping(value = "/show/{id}", method = RequestMethod.GET, produces = "application/json")
    public Optional<FootballPlayer> findById(@PathVariable Integer id) {
        return service.findById(id);
    }
}

调用 API 获取足球运动员列表:

$ curl http://localhost:8080/footballplayer | json_pp
11. JUnit 测试

为确保 API 正常工作,添加以下 Maven 依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <scope>test</scope>
</dependency>

com.packtpub.springboot.footballplayermicroservice.FootballPlayerMicroserviceApplicationTests 类中编写测试方法:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import java.io.IOException;
import java.math.BigInteger;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import com.jayway.jsonpath.JsonPath;
import net.minidev.json.JSONArray;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = FootballPlayerMicroserviceApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class FootballPlayerMicroserviceApplicationTests {
    private final HttpHeaders headers = new HttpHeaders();
    private final TestRestTemplate restTemplate = new TestRestTemplate();
    @Autowired
    private FootballPlayerRepository repository;
    @org.springframework.boot.web.server.LocalServerPort
    private int port;

    @Test
    public void test_1_FindAll() throws IOException {
        System.out.println("findAll");
        HttpEntity<String> entity = new HttpEntity<>(null, headers);
        ResponseEntity<String> response =
                restTemplate.exchange(createURLWithPort("/footballplayer"),
                        HttpMethod.GET, entity, String.class);
        assertEquals(HttpStatus.OK, response.getStatusCode());
        JSONArray jsonArray = JsonPath.read(response.getBody(), "$.[*]");
        assertEquals(23, jsonArray.size());
    }

    @Test
    public void test_2_Create() {
        System.out.println("create");
        FootballPlayer player = new FootballPlayer("Mauro", "Vocale", 38,
                "Juventus", "central midfielder", new BigInteger("100"));
        HttpEntity<FootballPlayer> entity = new HttpEntity<>(player, headers);
        ResponseEntity<String> response = restTemplate.exchange(
                createURLWithPort("/footballplayer/save"),
                HttpMethod.POST, entity, String.class);
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertEquals("{\"id\":24,\"name\":\"Mauro\",\"surname\":\"Vocale\",\"age\":38,\"team\":\"Juventus\",\"position\":\"central midfielder\",\"price\":100}", response.getBody());
    }
}

以下是整个流程的 mermaid 流程图:

graph LR
    A[开始] --> B[选择构建工具]
    B --> C{Maven 或 Gradle}
    C -- Maven --> D[配置 pom.xml]
    C -- Gradle --> E[配置 build.gradle]
    D --> F[构建项目 mvn clean package]
    E --> F
    F --> G[安装和配置 PostgreSQL]
    G --> H[创建实体类和仓库接口]
    H --> I[创建数据库脚本 schema.sql 和 data.sql]
    I --> J[运行项目 mvn spring-boot:run]
    J --> K[添加 RESTful 服务依赖]
    K --> L[编译运行项目 mvn clean package && mvn spring-boot:run]
    L --> M[测试 API]
    M --> N[结束]

使用 Spring Boot 2 构建微服务

12. 测试类分析

FootballPlayerMicroserviceApplicationTests 测试类中,主要进行了两个关键测试:
- test_1_FindAll 方法
- 该方法的主要目的是测试获取所有足球运动员信息的 API 是否正常工作。
- 首先,创建了一个 HttpEntity 对象,用于封装请求的头部信息。
- 然后,使用 TestRestTemplate 发送一个 GET 请求到 /footballplayer 接口。
- 通过 assertThat 断言,验证响应的状态码是否为 HttpStatus.OK
- 接着,使用 JsonPath 解析响应体,将其转换为 JSONArray ,并验证数组的大小是否符合预期。
- test_2_Create 方法
- 此方法用于测试创建足球运动员信息的 API。
- 先创建一个 FootballPlayer 对象,包含球员的详细信息。
- 同样创建一个 HttpEntity 对象,将球员信息和头部信息封装其中。
- 使用 TestRestTemplate 发送一个 POST 请求到 /footballplayer/save 接口。
- 通过 assertThat 断言,验证响应的状态码是否为 HttpStatus.OK ,以及响应体是否包含正确的球员信息。

13. 总结与注意事项
  • 依赖管理 :使用 spring-boot-starter-parent 可以方便地管理 Spring Boot 项目的依赖版本,避免版本冲突。
  • 数据库配置 :在 application.properties 中正确配置数据库连接信息,确保应用程序能够正常连接到 PostgreSQL 数据库。
  • RESTful API 暴露 :通过 spring-boot-starter-data-rest 可以自动将 CrudRepository 中的方法暴露为 RESTful API,但在实际开发中,也可以根据需求手动创建服务类和控制器类。
  • 测试 :编写 JUnit 测试用例可以确保 API 的正确性和稳定性,在测试时要注意模拟真实的请求和响应。
14. 常见问题及解决方法
问题描述 可能原因 解决方法
应用程序无法启动 数据库连接失败、依赖冲突等 检查 application.properties 中的数据库配置,确保数据库服务正常运行;检查 pom.xml build.gradle 中的依赖版本是否兼容。
RESTful API 无法访问 配置错误、端口被占用等 检查 pom.xml 中是否添加了 spring-boot-starter-data-rest 依赖;检查端口是否被其他应用程序占用。
测试用例失败 数据不一致、API 实现错误等 检查数据库中的数据是否符合测试预期;检查服务类和控制器类的实现是否正确。
15. 扩展功能建议
  • 添加更多的业务逻辑 :在 FootballPlayerService 类中添加更多的业务逻辑,例如数据验证、权限控制等。
  • 集成其他服务 :可以将该微服务与其他服务集成,例如缓存服务、消息队列服务等,以提高系统的性能和可靠性。
  • 使用 Swagger 进行 API 文档管理 :虽然由于 SpringFox 框架的 bug 暂时无法使用 Swagger 进行 API 文档管理,但可以关注该框架的更新,待 bug 修复后使用 Swagger 自动生成 API 文档。
16. 后续开发流程

以下是后续开发的 mermaid 流程图:

graph LR
    A[现有项目] --> B[添加业务逻辑]
    B --> C[集成其他服务]
    C --> D[使用 Swagger 管理 API 文档]
    D --> E[持续测试和优化]
    E --> F[部署到生产环境]
    F --> G[监控和维护]

在后续开发中,可以按照上述流程逐步完善和优化微服务。先在现有项目基础上添加更多的业务逻辑,增强系统的功能。然后集成其他服务,提升系统的性能和可靠性。接着使用 Swagger 进行 API 文档管理,方便开发人员和测试人员使用。在开发过程中持续进行测试和优化,确保系统的稳定性和正确性。最后将系统部署到生产环境,并进行监控和维护,及时处理出现的问题。

通过以上步骤,我们可以构建一个完整、稳定且功能强大的 Spring Boot 微服务,满足实际业务的需求。

基于可靠性评估序贯蒙特卡洛模拟法的配电网可靠性评估研究(Matlab代码实现)内容概要:本文围绕“基于可靠性评估序贯蒙特卡洛模拟法的配电网可靠性评估研究”,介绍了利用Matlab代码实现配电网可靠性的仿真分析方法。重点采用序贯蒙特卡洛模拟法对配电网进行长时间段的状态抽样与统计,通过模拟系统元件的故障与修复过程,评估配电网的关键可靠性指标,如系统停电频率、停电持续时间、负荷点可靠性等。该方法能够有效处理复杂网络结构与设备时序特性,提升评估精度,适用于含分布式电源、电动汽车等新型负荷接入的现代配电网。文中提供了完整的Matlab实现代码与案例分析,便于复现和扩展应用。; 适合人群:具备电力系统基础知识和Matlab编程能力的高校研究生、科研人员及电力行业技术人员,尤其适合从事配电网规划、运行与可靠性分析相关工作的人员; 使用场景及目标:①掌握序贯蒙特卡洛模拟法在电力系统可靠性评估中的基本原理与实现流程;②学习如何通过Matlab构建配电网仿真模型并进行状态转移模拟;③应用于含新能源接入的复杂配电网可靠性定量评估与优化设计; 阅读建议:建议结合文中提供的Matlab代码逐段调试运行,理解状态抽样、故障判断、修复逻辑及指标统计的具体实现方式,同时可扩展至不同网络结构或加入更多不确定性因素进行深化研究。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值