chapter12:SpringBoot与检索

这篇具有很好参考价值的文章主要介绍了chapter12:SpringBoot与检索。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Spring Boot与检索视频

1. 简介

我们的应用经常需要添加检索功能,开源的ElasticSearch是目前全文搜索引擎的首选。 他可以快速的存储、搜索和分析海量数据。SpringBoot通过整合Spring Data ElasticSearch为我们提供了非常便捷的检索功能支持。

ElasticSearch是一个分布式搜索服务,提供Restful API, 底层基于Lucene,采用多shard(分片)的方式保证数据安全,并且提供自动resharding的功能,github等大型站点也是采用了ElasticSearch作为其搜索服务。

2. 安装elasticsearch

使用docker安装elasticsearch镜像,docker安装可以参考 : docker安装

下载镜像

docker search elasticsearch
docker pull elasticsearch

运行镜像,因为elasticsearch是java写的,产品默认内存配置是2GB,我使用虚拟机安装的CentOS7系统内存不够,可以在运行镜像时指定运行elasticsearch的最大,最小内存配置为256m。9200是对外访问的http端口,9300是集群节点之间的通信端口。

docker run -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9200:9200 -p 9300:9300 --name elasticsearch01 elasticsearch:latest

tips:docker hub经常访问不到,下载镜像慢。

可以使用国内的镜像,修改镜像仓库参考:Linux docker设置国内镜像

运行elasticsearch成功后, 访问http://192.168.111.129:9200/, 返回如下json串信息表示启动成功。

{
  "name" : "ScvrTuB",
  "cluster_name" : "elasticsearch", // 节点名称
  "cluster_uuid" : "h2HwBRL3Q9qB44fKYUYVOQ",
  "version" : {
    "number" : "5.6.12",
    "build_hash" : "cfe3d9f",
    "build_date" : "2018-09-10T20:12:43.732Z",
    "build_snapshot" : false,
    "lucene_version" : "6.6.1"
  },
  "tagline" : "You Know, for Search"
}

3. 快速入门

使用手册文档

员工文档的形式存储为例: 一个文档代表一个员工数据。存储数据到ElasticSearch的行为叫做索引, 但在索引一个文档之前,需要确定将文档存储在哪里。

一个ElasticSearch集群可以包含多个索引,相应的每个索引可以包含多个类型。这些不同的类型存储着多个文档, 每个文档又有多个属性

用关系型数据库来类比,索引-数据库,类型-表,文档-表中的记录行,属性-列。
chapter12:SpringBoot与检索,spring boot
将 HTTP 命令由 PUT 改为 GET 可以用来检索文档,同样的,可以使用 DELETE 命令来删除文档,以及使用 HEAD 指令来检查文档是否存在。如果想更新已存在的文档,只需再次 PUT

3.1 索引员工文档

对于员工目录,我们将做如下操作:

  • 每个员工索引一个文档,文档包含该员工的所有信息。
  • 每个文档都将是 employee 类型
  • 该类型位于 索引 megacorp 内。
  • 该索引保存在我们的 Elasticsearch 集群中。

实践中这非常简单(尽管看起来有很多步骤),我们可以通过一条命令完成所有这些动作:

# put请求
http://192.168.111.129:9200/megacorp/employee/1
{
    "first_name" : "John",
    "last_name" :  "Smith",
    "age" :        25,
    "about" :      "I love to go rock climbing",
    "interests": [ "sports", "music" ]
}

注意,路径 /megacorp/employee/1 包含了三部分的信息:

  • megacorp 索引名称
  • employee 类型名称
  • 1 特定雇员的ID

响应结果

{
    "_index": "megacorp",
    "_type": "employee",
    "_id": "1",
    "_version": 1,
    "result": "created",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "created": true
}

同样的方式添加id=2,id=3的员工对象信息
chapter12:SpringBoot与检索,spring boot

3.2 检索文档

检索员工信息

# get请求
http://192.168.111.129:9200/megacorp/employee/1
curl -X GET http://192.168.111.129:9200/megacorp/employee/1

响应结果

{
    "_index": "megacorp",
    "_type": "employee",
    "_id": "1",
    "_version": 1,
    "found": true,
    "_source": {
        "first_name": "John",
        "last_name": "Smith",
        "age": 25,
        "about": "I love to go rock climbing",
        "interests": [
            "sports",
            "music"
        ]
    }
}

3.3 删除文档

删除员工信息

# DELETE请求
http://192.168.111.129:9200/megacorp/employee/1
curl -X DELETE http://192.168.111.129:9200/megacorp/employee/1

响应结果

{
  "found": true,
  "_index": "megacorp",
  "_type": "employee",
  "_id": "1",
  "_version": 2,
  "result": "deleted",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  }
}

删除后再次查询 http://192.168.111.129:9200/megacorp/employee/1的结果, 已经无法查询到了。

{
    "_index": "megacorp",
    "_type": "employee",
    "_id": "1",
    "found": false
}

也可以使用HEAD请求方式来检查文档是否存在,如果没有索引到文档,会报404;

C:\Users\18482>curl --head HEAD http://192.168.111.129:9200/megacorp/employee/1
curl: (6) Could not resolve host: HEAD
HTTP/1.1 404 Not Found
content-type: application/json; charset=UTF-8
content-length: 64

如果索引到文档返回1。

C:\Users\18482>curl --head HEAD http://192.168.111.129:9200/megacorp/employee/2
curl: (6) Could not resolve host: HEAD
HTTP/1.1 200 OK
content-type: application/json; charset=UTF-8
content-length: 260

3.4 轻量搜索

查询所有员工信息。

C:\Users\18482>curl http://192.168.111.129:9200/megacorp/employee/_search

响应结果,查询到存在的2条文档。

{
  "took": 4,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 2,  
    "max_score": 1,
    "hits": [
      {
        "_index": "megacorp",
        "_type": "employee",
        "_id": "2",
        "_score": 1,
        "_source": {
          "first_name": "Jane",
          "last_name": "Smith",
          "age": 32,
          "about": "I like to collect rock albums",
          "interests": [
            "music"
          ]
        }
      },
      {
        "_index": "megacorp",
        "_type": "employee",
        "_id": "3",
        "_score": 1,
        "_source": {
          "first_name": "Douglas",
          "last_name": "Fir",
          "age": 35,
          "about": "I like to build cabinets",
          "interests": [
            "forestry"
          ]
        }
      }
    ]
  }
}

3.5 指定查询参数搜索

搜索姓氏为 Smith 的雇员

curl -X GET http://192.168.111.129:9200/megacorp/employee/_search?q=last_name:Smith

返回结果给出了所有的 Smith

{
    "took": 6,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 1,
        "max_score": 0.2876821,
        "hits": [
            {
                "_index": "megacorp",
                "_type": "employee",
                "_id": "2",
                "_score": 0.2876821,
                "_source": {
                    "first_name": "Jane",
                    "last_name": "Smith",
                    "age": 32,
                    "about": "I like to collect rock albums",
                    "interests": [
                        "music"
                    ]
                }
            }
        ]
    }
}

3.6 查询表达式搜索

使用 JSON 构造了一个请求。我们可以像这样重写之前的查询所有名为 Smith 的搜索 .

# get请求
http://192.168.111.129:9200/megacorp/employee/_search
{
    "query" : {
        "match" : {
            "last_name" : "Smith"
        }
    }
}

返回结果与之前的查询一样 。

3.7 过滤器filter

搜索姓氏为 Smith 的员工,但这次我们只需要年龄大于 30 的。查询需要稍作调整,使用过滤器 filter ,它支持高效地执行一个结构化查询。

# get请求
http://192.168.111.129:9200/megacorp/employee/_search
{
    "query" : {
        "bool": {
            "must": {
                "match" : {
                    "last_name" : "smith" 
                }
            },
            "filter": {
                "range" : {
                    "age" : { "gt" : 30 } 
                }
            }
        }
    }
}

3.8 全文搜索

搜索下所有喜欢攀岩(rock climbing)的员工:

# get请求
http://192.168.111.129:9200/megacorp/employee/_search
{
    "query" : {
        "match" : {
            "about" : "rock climbing"
        }
    }
}

3.9 短语搜索

找出一个属性中的独立单词是没有问题的,但有时候想要精确匹配一系列单词或者_短语_ 。 比如, 我们想执行这样一个查询,仅匹配同时包含 “rock” “climbing” ,并且 二者以短语 “rock climbing” 的形式紧挨着的雇员记录。

为此对 match 查询稍作调整,使用一个叫做 match_phrase 的查询:

# get请求
http://192.168.111.129:9200/megacorp/employee/_search
{
    "query" : {
        "match_phrase" : {
            "about" : "rock climbing"
        }
    }
}

3.10 高亮搜索

许多应用都倾向于在每个搜索结果中 高亮 部分文本片段,以便让用户知道为何该文档符合查询条件。在 Elasticsearch 中检索出高亮片段也很容易。

再次执行前面的查询,并增加一个新的 highlight 参数:

# get请求
http://192.168.111.129:9200/megacorp/employee/_search
{
    "query" : {
        "match_phrase" : {
            "about" : "rock climbing"
        }
    },
    "highlight": {
        "fields" : {
            "about" : {}
        }
    }
}

当执行该查询时,返回结果与之前一样,与此同时结果中还多了一个叫做 highlight 的部分。这个部分包含了 about 属性匹配的文本片段,并以 HTML 标签 <em> </em>封装:

{
    "took": 142,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 1,
        "max_score": 0.53484553,
        "hits": [
            {
                "_index": "megacorp",
                "_type": "employee",
                "_id": "1",
                "_score": 0.53484553,
                "_source": {
                    "first_name": "John",
                    "last_name": "Smith",
                    "age": 25,
                    "about": "I love to go rock climbing",
                    "interests": [
                        "sports",
                        "music"
                    ]
                },
                "highlight": {
                    "about": [
                        "I love to go <em>rock</em> <em>climbing</em>"
                    ]
                }
            }
        ]
    }
}

4. 项目使用ElasticSearch

4.1 创建项目及配置

创建Springboot项目,导入相关依赖

<?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>1.5.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.crysw</groupId>
    <artifactId>springboot03-elasticsearch</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot03-elasticsearch</name>
    <description>springboot03-elasticsearch</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>

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

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

springboot默认支持两种技术来和elasticsearch交互。

  • Jest(默认不生效),需要导入jest的工具包
  • SpringData ElasticSearch,如果启动项目报连接超时,可能是ES版本和SpringBoot版本不适配。
    • ElasticsearchAutoConfiguration, 配置了Client来交互ES;
    • ElasticsearchDataAutoConfiguration 配置了ElasticsearchTemplate;
    • ElasticsearchRepository接口提供了类似JPA操作数据库的api一样操作ES的api;

如果使用jest,需要导入Jtest的依赖才会生效。

<dependency>
    <groupId>io.searchbox</groupId>
    <artifactId>jest</artifactId>
    <version>5.3.3</version>
</dependency>

添加Jtest配置,指定elasticSearch服务地址

#jedis
spring.elasticsearch.jest.uris=http://192.168.111.129:9200

如果是使用SpringData ElasticSearch,配置如下:

#spring data elasticsearch
spring.data.elasticsearch.cluster-name=elasticsearch
spring.data.elasticsearch.cluster-nodes=192.168.111.129:9300

4.2 测试索引文档

@SpringBootTest
@RunWith(SpringRunner.class)
public class Springboot03ElasticsearchApplicationTests {
    
    @Autowired
    private JestClient jestClient;
    @Test
    public void createIndex() throws IOException {
        // 给ElasticSearch中索引一个文档
        Article article = Article.builder().id(1).title("好消息").author("张三").content("hello world").build();
        // 构建一个索引
        Index index = new Index.Builder(article).index("atguigu").type("article").build();
        // 执行
        jestClient.execute(index);
    }
    
    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
	class Article {
        @JestId
        private Integer id;
        private String author;
        private String title;
        private String content;
	}
}

4.3 测试搜索文档

@SpringBootTest
@RunWith(SpringRunner.class)
public class Springboot03ElasticsearchApplicationTests {
    
    @Autowired
    private JestClient jestClient;
    @Test
    public void search() throws IOException {
        String jsonStr = "{\n" +
                "  \"query\": {\n" +
                "    \"match\": {\n" +
                "      \"content\": \"hello\"\n" +
                "    }\n" +
                "  }\n" +
                "}";

        Search search = new Search.Builder(jsonStr).addIndex("atguigu").addType("article").build();
        SearchResult searchResult = jestClient.execute(search);
        System.out.println("查询结果:" + searchResult.getJsonString());
    }
}

5. ElasticsearchRepository

ElasticsearchRepository用到的是SpringData elasticsearch, 所以需要加上相关的配置。

#spring data elasticsearch
spring.data.elasticsearch.cluster-name=elasticsearch
spring.data.elasticsearch.cluster-nodes=192.168.111.129:9300

5.1 自定义接口

ElasticsearchRepository类似Jpa的使用,提供了常用增删查改的api方法给我们使用,只需要自定义接口实现ElasticsearchRepository即可。

public interface BookRepository extends ElasticsearchRepository<Book, Integer> {

    List<Book> findByBookName(String bookName);
}

5.1 测试公共api

@SpringBootTest
@RunWith(SpringRunner.class)
public class Springboot03ElasticsearchApplicationTests {
    @Autowired
    private BookRepository bookRepository;
    // 索引文档
    @Test
    public void createIndexOfBook() {
        Book book = Book.builder().id(1).author("李四").bookName("java核心技术").build();
        bookRepository.index(book);
    }
    
    @AllArgsConstructor
    @NoArgsConstructor
    @Data
    @Builder
    @Document(indexName = "atguigu", type = "book")
    public class Book {
        private Integer id;
        private String bookName;
        private String author;
    }
}

5.2 测试自定义api

@SpringBootTest
@RunWith(SpringRunner.class)
public class Springboot03ElasticsearchApplicationTests {
    @Autowired
    private BookRepository bookRepository;
    
    @Test
    public void findByBookName() {
        List<Book> books = bookRepository.findByBookName("java");
        System.out.println("打印查询结果:");
        books.forEach(System.out::println);
    }
}    

更多查看 spring-data-elasticsearch

6. 自动配置

ElasticsearchAutoConfiguration配置类提供了Client, ElasticsearchProperties封装了配置属性,可以通过spring.data.elasticsearch.xxx修改配置属性的值。

@Configuration
@ConditionalOnClass({ Client.class, TransportClientFactoryBean.class,
		NodeClientFactoryBean.class })
@EnableConfigurationProperties(ElasticsearchProperties.class)
public class ElasticsearchAutoConfiguration implements DisposableBean {
    // 创建Client客户端来操作elasticsearch
    @Bean
	@ConditionalOnMissingBean
	public Client elasticsearchClient() {
		try {
			return createClient();
		}
		catch (Exception ex) {
			throw new IllegalStateException(ex);
		}
	}

	private Client createClient() throws Exception {
		if (StringUtils.hasLength(this.properties.getClusterNodes())) {
			return createTransportClient();
		}
		return createNodeClient();
	}

	private Client createNodeClient() throws Exception {
		Settings.Builder settings = Settings.settingsBuilder();
		for (Map.Entry<String, String> entry : DEFAULTS.entrySet()) {
			if (!this.properties.getProperties().containsKey(entry.getKey())) {
				settings.put(entry.getKey(), entry.getValue());
			}
		}
		settings.put(this.properties.getProperties());
		Node node = new NodeBuilder().settings(settings)
				.clusterName(this.properties.getClusterName()).node();
		this.releasable = node;
		return node.client();
	}
}

ElasticsearchDataAutoConfiguration提供了ElasticsearchTemplate模板。

@Configuration
@ConditionalOnClass({ Client.class, ElasticsearchTemplate.class })
@AutoConfigureAfter(ElasticsearchAutoConfiguration.class)
public class ElasticsearchDataAutoConfiguration {
    @Bean
	@ConditionalOnMissingBean
	@ConditionalOnBean(Client.class)
	public ElasticsearchTemplate elasticsearchTemplate(Client client,
			ElasticsearchConverter converter) {
		try {
            // 实际使用Client交互
            // org.elasticsearch.client.Client
			return new ElasticsearchTemplate(client, converter);
		}
		catch (Exception ex) {
			throw new IllegalStateException(ex);
		}
	}
}

ElasticsearchRepository提供了常用的增删查改的api文章来源地址https://www.toymoban.com/news/detail-603004.html

@NoRepositoryBean
public interface ElasticsearchRepository<T, ID extends Serializable> extends ElasticsearchCrudRepository<T, ID> {

	<S extends T> S index(S entity);

	Iterable<T> search(QueryBuilder query);

	Page<T> search(QueryBuilder query, Pageable pageable);

	Page<T> search(SearchQuery searchQuery);

	Page<T> searchSimilar(T entity, String[] fields, Pageable pageable);

	void refresh();

	Class<T> getEntityClass();
}

到了这里,关于chapter12:SpringBoot与检索的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Spring Boot】Spring Boot项目中如何查看springBoot版本和Spring的版本

    在项目中查看默认版本有两种方式如下 Spring Boot 的最新版本支持情况: 版本 发布时间 停止维护时间 停止商业支持 3.0.x 2022-11-24 2023-11-24 2025-02-24 2.7.x 2022-05-19 2023-11-18 2025-02-18 2.6.x 2021-12-17 2022-11-24 2024-02-24 2.5.x 2021-05-20 已停止 2023-08-24 2.4.x 2020-11-12 已停止 2023-02-23 2.3.x 2020-05-

    2024年02月11日
    浏览(99)
  • SpringBoot整理-Spring Boot配置

    Spring Boot 的配置系统是其核心功能之一,旨在简化 Spring 应用的配置过程。Spring Boot 提供了一种灵活的方式来配置你的应用,无论是通过外部配置文件,环境变量,命令行参数还是在代码中直接配置。以下是关于 Spring Boot 配置的几个重要方面: 配置文件 application.prop

    2024年01月25日
    浏览(55)
  • 【Spring Boot】SpringBoot 单元测试

    单元测试(unit testing),是指对软件中的最⼩可测试单元进⾏检查和验证的过程就叫单元测试。 1、可以⾮常简单、直观、快速的测试某⼀个功能是否正确。 2、使⽤单元测试可以帮我们在打包的时候,发现⼀些问题,因为在打包之前,所以的单元测试必须通过,否则不能打包

    2024年02月07日
    浏览(57)
  • SpringBoot教程(一)|认识Spring Boot

    安得广厦千万间,大庇天下寒士俱欢颜,风雨不动安如山,呜呼,何时眼前突兀见此屋,吾庐独破受冻死亦足! Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需

    2024年01月16日
    浏览(51)
  • SpringBoot整理-Spring Boot与Spring MVC的区别

    Spring Boot 和 Spring MVC 是 Spring 框架的两个不同部分,它们在 Java Web 开发中扮演着各自独特的角色。理解它们之间的区别有助于更好地利用 Spring 生态系统进行有效的应用开发。 Spring MVC 定义:  Spring MVC 是基于 Model-View-Controller(模型-视图-控制器)设计模式的一个 

    2024年01月22日
    浏览(45)
  • SpringBoot教程(三) | Spring Boot初体验

    上篇文章我们创建了SpringBoot 项目,并且进行了简单的启动。整个项目了里其实我们就动了两个文件,一个是pom.xml负责管理springboot的相关依赖,一个是springBoot的启动类。 pom文件中通过starter的形式大大简化了配置,不像以前一样需要引入大量的依赖配置,搞不好还得解决冲突

    2024年01月16日
    浏览(49)
  • 18、全文检索--Elasticsearch-- SpringBoot 整合 Spring Data Elasticsearch(异步方式(Reactive)和 传统同步方式 分别操作ES的代码演示)

    启动命令行窗口,执行:elasticsearch 命令即可启动 Elasticsearch 服务器 三种查询方式解释: 方法名查询: 就是全自动查询,只要按照规则来定义查询方法 ,Spring Data Elasticsearch 就会帮我们生成对应的查询语句,并且生成方法体。 @Query 查询 : 就是半自动查询, 按照 S

    2024年03月12日
    浏览(67)
  • 【SpringBoot】Spring Boot 单体应用升级 Spring Cloud 微服务

    Spring Cloud 是在 Spring Boot 之上构建的一套微服务生态体系,包括服务发现、配置中心、限流降级、分布式事务、异步消息等,因此通过增加依赖、注解等简单的四步即可完成 Spring Boot 应用到 Spring Cloud 升级。 Spring Boot 应用升级为 Spring Cloud Cloud Native 以下是应用升级 Spring Clou

    2024年02月02日
    浏览(45)
  • 【Spring Boot】SpringBoot参数验证以及实现原理

    参数验证很重要,是平时开发环节中不可少的一部分,但是我想很多后端同事会偷懒,干脆不做,这样很可能给系统的稳定性和安全性带来严重的危害。 那么在Spring Boot应用中如何做好参数校验工作呢,本文提供了小技巧以及验证实现原理,你知道几个呢? Spring Boot 提供了内

    2023年04月16日
    浏览(53)
  • Springboot 实践(13)spring boot 整合RabbitMq

    前文讲解了RabbitMQ的下载和安装,此文讲解springboot整合RabbitMq实现消息的发送和消费。 1、创建web project项目,名称为“SpringbootAction-RabbitMQ” 2、修改pom.xml文件,添加amqp使用jar包    !--  RabbitMQ --         dependency             groupIdorg.springframework.boot/groupId         

    2024年02月09日
    浏览(60)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包