• ElasticSearch系列——Elasticsearch Java API Client


    Elasticsearch Java API Client

    这是用于Elasticsearch的官方Java API客户端的文档。客户端为所有Elasticsearch API提供强类型请求和响应。我们要注意原来的HighRestAPIClient以及停用了,这是趋势,包括SpringData-ElasticSearch4.4.5之后配合ES8的推出也会更换

    官方地址

    https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/7.17/indexing.html

    特点

    1. 所有Elasticsearch API的强类型请求和响应。
    2. 所有API的阻塞和异步版本。
    3. 使用流畅的构建器和函数模式,在创建复杂的嵌套结构时,允许编写简洁但可读的代码
    4. 通过使用对象映射器(如杰克逊或任何JSON-B实现)无缝集成应用程序类。
    5. 将协议处理委托给http客户端(如Java低级REST客户端),该客户端负责处理所有传输级问题:HTTP连接池、重试、节点发现等。

    需求

    1. Java11版本
    2. 一 个 JSON 对象映射库,允许您的应用程序类与Elasticsearch API无缝集成 ,Java 客户端支持 Jackson 或 JSON-B 库 ( 如 Eclipse Yasson )

    核心三大组成

    Java API客户端由三个主要组件构成:

    1. API client classes(API客户端类)。它们为Elasticsearch API提供了强类型数据结构和方法。由于Elasticsearch API很大,它以特性组(也称为“命名空间”)的形式进行结构化,每个特性组都有自己的客户端类。Elasticsearch核心功能在ElasticsearchClient类中实现。
    2. A JSON object mapper(JSON对象映射器):这将应用程序类映射到JSON,并将它们与API客户机无缝集成。
    3. A transport layer implementation(一种传输层实现):这是所有HTTP请求处理发生的地方。

    包和命名空间

    Elasticsearch API很大,并被组织成功能组,这可以在Elasticsearch API文档中看到。

    Java API客户端遵循以下结构:特征组被称为“命名空间”,并且每个命名空间位于co.elastic.clients.elasticsearch的子包中。

    每个命名空间客户端都可以从顶级Elasticsearch客户端访问。唯一的例外是“search”和“document”API,它们位于核心子包中,可以在主Elasticsearch客户端对象上访问。

    QuickStart

    1.导入依赖

    这里记住你的elasticsearch-java必须对应你电脑上装的ES版本

    	<dependency>
          <groupId>co.elastic.clients</groupId>
          <artifactId>elasticsearch-java</artifactId>
          <version>7.17.6</version>
        </dependency>
    
        <dependency>
          <groupId>com.fasterxml.jackson.core</groupId>
          <artifactId>jackson-databind</artifactId>
          <version>2.12.3</version>
        </dependency>
        <dependency>
          <groupId>jakarta.json</groupId>
          <artifactId>jakarta.json-api</artifactId>
          <version>2.0.1</version>
        </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2.开启连接

            //创建一个低级的客户端
            final RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200)).build();
            //创建JSON对象映射器
            final RestClientTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
            //创建API客户端
            final ElasticsearchClient client = new ElasticsearchClient(transport);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3.关闭连接

            client.shutdown();
            transport.close();
            restClient.close();
    
    • 1
    • 2
    • 3

    完整代码

    public class Client {
        public static void main(String[] args) throws IOException {
            //创建一个低级的客户端
            final RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200)).build();
            //创建JSON对象映射器
            final RestClientTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
            //创建API客户端
            final ElasticsearchClient client = new ElasticsearchClient(transport);
            //查询所有索引-------------------------------------------------------------------------------------
            final GetIndexResponse response = client.indices().get(query -> query.index("_all"));
            final IndexState products = response.result().get("products");
            System.out.println(products.toString());
            //关闭
            client.shutdown();
            transport.close();
            restClient.close();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    @JsonIgnore注解

    标记注释,指示访问器(字段、getter/setter方法或[JsonCreator带注释的构造函数或工厂方法]的Creator参数)的逻辑属性将被基于内省的序列化和反序列化功能忽略。

    上面是官方的解释,在我们的ES API中这个注解用于在实体类上的某个属性进行添加,用于忽略这个属性,即使用该注解标注的属性在查询时会被输出为null(前提是你设置了映射含有,若本来就没有映射,则不会看到这个属性),而在创建时则会直接忽略
    例如:

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    public class Produce {
        @JsonIgnore
        private String sku;
        private String name;
        private String des;
        private double price;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    我们在sku上添加了这个注解,那么创建文档时就不会有这个属性(请大家运行以下代码进行验证即可),一般我们在真实业务中也会这样做,因为文档的_id字段会与之重合导致不必要的数据冗余

    public class CreateDocClass {
        public static void main(String[] args) throws IOException {
            final RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200)).build();
            final RestClientTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
            final ElasticsearchClient client = new ElasticsearchClient(transport);
            //创建文档
            final Produce produce = new Produce("aabbcc555", "旺仔牛比糖", "旺仔牛比糖吃了炒鸡牛笔", 6.66D);
            final IndexResponse response = client.index(builder -> builder.index("produces").id(produce.getSku()).document(produce));
            System.err.println(response.version());
            client.shutdown();
            transport.close();
            restClient.close();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述

    JsonData类

    原始JSON值。可以使用JsonpMapper将其转换为JSON节点树或任意对象。 此类型在API类型中用于没有静态定义类型或无法表示为封闭数据结构的泛型参数的值。 API客户端返回的此类实例保留对客户端的JsonpMapper的引用,并且可以使用to(class)转换为任意类型,而不需要显式映射器

    我们一般在ES的DSL范围查询中会使用到!
    核心方法:

    1. to:将此对象转换为目标类。必须在创建时提供映射器
    2. from:从读取器创建原始JSON值
    3. of:从现有对象创建原始JSON值,以及用于进一步转换的映射器
    4. deserialize:使用反序列化程序转换此对象。必须在创建时提供映射器

    这个类不多大家自己看一下应该就知道怎么用了,位于package co.elastic.clients.json;

    API使用

    以下所有API和我之前的文章对应请查看:ElasticSearch系列——Kibana,核心概念的使用Kibana对ES进行操作部分

    开启连接

    1. 构建RestClient低级客户端
    2. 构建对象映射器
    3. 构建ES的API客户端
            final RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200)).build();
            final RestClientTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
            final ElasticsearchClient client = new ElasticsearchClient(transport);
    
    • 1
    • 2
    • 3

    关闭连接

    对应开启

    1. 关闭ES的API客户端
    2. 关闭对象映射器
    3. 关闭低级客户端
            client.shutdown();
            transport.close();
            restClient.close();
    
    • 1
    • 2
    • 3

    查询所有索引

    indices是index的复数
    其实看起来很好理解
    使用GET方法进行查询index
    查询所有的就是GET http://localhost:9200/_all

    //省略连接...
            final GetIndexResponse all = client.indices().get(query -> query.index("_all"));
            System.out.println(all.toString());
    //省略关闭...
    
    • 1
    • 2
    • 3
    • 4

    查询某个索引

    原来我们使用GET http://localhost:9200/索引名称这里也是一样设置索引名称即可

            //查询某个索引
            final GetIndexResponse products = client.indices().get(query -> query.index("products"));
            System.err.println(products.toString());
    
    • 1
    • 2
    • 3

    创建索引

    //创建指定索引
            boolean exists = client.indices().exists(query -> query.index("products")).value();
            System.out.println(exists);
            if (exists) {
                System.err.println("索引已存在");
    
            } else {
                final CreateIndexResponse products = client.indices().create(builder -> builder.index("products"));
                System.err.println(products.acknowledged());
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    删除指定索引

            //删除指定索引
            boolean exists = client.indices().exists(query -> query.index("products")).value();
            System.out.println(exists);
            if (exists) {
                DeleteIndexResponse response = client.indices().delete(query -> query.index("products"));
                System.err.println(response.acknowledged());
            } else {
                System.err.println("索引不存在");
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    查询索引的映射

    没有直接去查映射的是根据索引向下找到映射信息,主要是一下代码

    response.result().get(索引名称).mappings(
    
    • 1
            //查询映射信息
            final GetIndexResponse response = client.indices().get(builder -> builder.index("produces"));
            System.err.println(response.result().get("produces").mappings());
    
    • 1
    • 2
    • 3

    创建索引指定映射

    1. numberOfReplicas(“1”):设置副本
    2. numberOfShards(“1”):设置分片
            //创建索引指定映射,分片和副本信息
            final CreateIndexResponse response = client.indices().create(builder ->
                    builder.settings(indexSetting -> indexSetting.numberOfReplicas("1").numberOfShards("1")).mappings(
                            map -> map
                                    .properties("name", propertyBuilder -> propertyBuilder.keyword(keywordProperty -> keywordProperty))
                                    .properties("price", propertyBuilder -> propertyBuilder.double_(doubleNumProperty -> doubleNumProperty))
                                    .properties("des", propertyBuilder -> propertyBuilder.text(textProperty -> textProperty.analyzer("ik_smart").searchAnalyzer("ik_smart")))
                    ).index("produces")
    
            );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    创建文档

    使用HashMap作为数据存储容器
            //创建文档
            //1.创建HashMap进行存储数据,文档要对应映射
            final HashMap<String, Object> doc = new HashMap<>();
            doc.put("name","辣条");
            doc.put("price",5.62D);
            doc.put("des","卫龙辣条,草鸡好吃");
            //2.将文档存入索引中
            final IndexResponse response = client.index(builder -> builder.index("produces").id("116688").document(doc));
            System.err.println(response.version());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在我们使用HashMap的时候我们可以将文档的ID直接放在Map里,如下:
    但是我们要知道的是如果你这样设置了,你的索引的映射就会变化增加一个id的映射

            //创建文档
            //1.创建HashMap进行存储数据,文档要对应映射
            final HashMap<String, Object> doc = new HashMap<>();
            doc.put("id","116677");
            doc.put("name","辣条");
            doc.put("price",5.62D);
            doc.put("des","卫龙辣条,草鸡好吃");
            //2.将文档存入索引中
            final IndexResponse response = client.index(builder -> builder.index("produces").id(doc.get("id").toString()).document(doc));
            System.err.println(response.version());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    使用自定义类作为数据存储容器

    实体类

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    public class Produce {
        private String sku;
        private String name;
        private String des;
        private double price;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
            //创建文档
            final Produce produce = new Produce("aabbcc123", "旺仔牛比糖", "旺仔牛比糖吃了炒鸡牛笔", 6.66D);
            final IndexResponse response = client.index(builder -> builder.index("produces").id(produce.getSku()).document(produce));
            System.err.println(response.version());
    
    • 1
    • 2
    • 3
    • 4
    使用外部JSON数据创建

    这里要注意我们需要使用StringReader进行读取时使用replace函数将设置的'改为",当然这在真实的业务中肯定不会有,因为真实业务中一定是标准的JSON数据,无需使用replace进行替换了

            //创建文档
            final StringReader input = new StringReader(
                    "{'name':'农夫三拳','price':3.00,'des':'农夫三拳有点甜'}".replace('\'', '"')
            );
            final IndexResponse response = client.index(builder -> builder.index("produces").id("44514").withJson(input));
            System.err.println(response.version());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    查询所有文档

            //查询所有文档
            final SearchResponse<Object> response = client.search(builder -> builder.index("produces"), Object.class);
            final List<Hit<Object>> hits = response.hits().hits();
            hits.forEach(
                    x-> System.err.println(x)
            );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    根据ID查询文档

    使用HashMap对应查询
            //查询文档
            final GetResponse<Map> response = client.get(builder -> builder.index("produces").id("116677"), Map.class);
            final Map source = response.source();
            source.forEach((x,y)->{
                System.err.println(x+":"+y);
            });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    使用自定义类对应查询
            final GetResponse<Produce> response1 = client.get(builder -> builder.index("produces").id("aabbcc123"), Produce.class);
            final Produce source1 = response1.source();
            System.err.println(source1.toString());
    
    • 1
    • 2
    • 3

    删除指定文档

            //根据ID删除文档
            final DeleteResponse response = client.delete(builder -> builder.index("produces").id("44514"));
            System.err.println(response.shards().successful());
    
    • 1
    • 2
    • 3

    修改文档

    覆盖写
            //修改文档(覆盖)
            final Produce produce = new Produce("ccaabb123", "旺仔摇滚洞", "旺仔摇滚洞乱摇乱滚", 10.23D);
            final UpdateResponse<Produce> response = client.update(builder -> builder.index("produces").id("aabbcc123").doc(produce), Produce.class);
            System.err.println(response.shards().successful());
    
    • 1
    • 2
    • 3
    • 4
    修改部分文档

    区别在于我们需要设置.docAsUpsert(true)表明是修改部分而不是覆盖

            //修改文档(部分修改)
    //        final Produce produce = new Produce("ccaabb123", "旺仔摇滚洞", "旺仔摇滚洞乱摇乱滚", 10.23D);
            final Produce produce = new Produce();
            produce.setName("旺仔摇不动");
            final UpdateResponse<Produce> response = client.update(builder -> builder.index("produces").id("aabbcc123").doc(produce).docAsUpsert(true), Produce.class);
            System.err.println(response.shards().successful());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    批量操作

    对应批量操作大家应该知道概念:看起来是批量,实际是逐条处理
    所以我一开始想要写成for循环的单个文档创建,后来想了一下,如果这样写,其实没有什么必要了,尝试了一下确实有bulk函数,写成lambda方式后发现直接使用lambda构建实际是不行的,会报方法只能调用一次的错误,那么我们就需要在外部先构建出批量的条件然后再直接调用build方法即可final BulkResponse response = client.bulk(br.build());

    public class BulkDoc {
        public static void main(String[] args) throws IOException {
            final RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200)).build();
            final RestClientTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
            final ElasticsearchClient client = new ElasticsearchClient(transport);
            //批量操作
            //准备数据
            final Produce produce1 = new Produce("1000", "风油精", "风油精辣眼睛", 20.23D);
            final Produce produce2 = new Produce("1001", "六神花露水", "蚊虫叮咬就用Six God", 8.28D);
            final Produce produce3 = new Produce("1002", "龙虎万精油", "龙虎牌万精油赶走疲劳做回自己", 13.3D);
            final ArrayList<Produce> produceList = new ArrayList<>();
            produceList.add(produce1);
            produceList.add(produce2);
            produceList.add(produce3);
            //构建BulkRequest
            final BulkRequest.Builder br = new BulkRequest.Builder();
            for (Produce produce : produceList) {
                br.operations(op->op.index(idx->idx.index("produces").id(produce.getSku()).document(produce)));
            }
            final BulkResponse response = client.bulk(br.build());
            System.err.println(response.toString());
            client.shutdown();
            transport.close();
            restClient.close();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    DSL条件查询

    根据具体的条件对文档进行查询,这里的条件就是name为龙虎万精油
    注意的是若使用条件查询必须传入一个你要转化得到的实体对象在这里是Produce.class
    若你的索引中存了和其不符合的文档则会报错!

            //简单query方式查询
            final SearchResponse<Produce> response = client.search(builder ->
                    builder.index("produces")
                            .query(q ->
                                    q.match(t ->
                                            t.field("name")
                                                    .query("龙虎万精油"))), Produce.class);
            System.err.println(response.hits().hits());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    DSL matchAll查询所有文档

            //matchAll
            final SearchResponse<Produce> response = client.search(builder ->
                    builder.index("produces")
                            .query(q ->
                                    q.matchAll(
                                            v->v
                                    )), Produce.class);
    
            System.err.println(response.hits().hits());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    多ID查询(ids)

    在values中我们可以传入一个List也可以像我这样直接写多个ID,因为其使用了不定长参数

            //多ID查询
            final SearchResponse<Produce> response = client.search(builder ->
                    builder.index("produces")
                            .query(q ->
                                    q.ids(sid->sid.values("1000","1001"))), Produce.class);
            System.err.println(response.hits().hits());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    term不分词查询

            //term不分词条件查询
            final SearchResponse<Produce> response = client.search(builder -> builder.index("produces")
            .query(q -> q.term(t -> t.field("name").value("风油精"))), Produce.class);
            System.err.println(response.hits().hits());
    
    • 1
    • 2
    • 3
    • 4

    范围查询

            //范围查询
            final SearchResponse<Produce> response = client.search(builder ->
                            builder.index("produces").query(q ->
                                    q.range(r ->
                                            r.field("price").gt(JsonData.of(5D)).lt(JsonData.of(15D)))),
                    Produce.class);
            System.err.println(response.hits().hits());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    前缀查询

            //前缀查询
            final SearchResponse<Produce> response = client.search(builder ->
                            builder.index("produces").query(q ->q.prefix(p->p.field("name").value("六"))),
                    Produce.class);
            System.err.println(response.hits().hits());
    
    • 1
    • 2
    • 3
    • 4
    • 5

    匹配查询

    *全字符匹配
            //匹配查询
                    final SearchResponse<Produce> response = client.search(builder ->
                            builder.index("produces").query(q ->q.wildcard(w->w.field("name").value("风*"))),
                    Produce.class);
            System.err.println(response.hits().hits());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    ?单字符匹配
            //匹配查询
                    final SearchResponse<Produce> response = client.search(builder ->
                            builder.index("produces").query(q ->q.wildcard(w->w.field("name").value("风?精"))),
                    Produce.class);
            System.err.println(response.hits().hits());
    
    • 1
    • 2
    • 3
    • 4
    • 5

    模糊查询

            //模糊查询
            final SearchResponse<Produce> response = client.search(builder ->
                            builder.index("produces").query(q ->q.fuzzy(f->f.field("name").value("六仙花露水"))),
                    Produce.class);
            System.err.println(response.hits().hits());
    
    • 1
    • 2
    • 3
    • 4
    • 5

    多条件查询

    使用bool关键字配合must,should,must_not

    1. must:所有条件必须同时成立
    2. must_not:所有条件必须同时不成立
    3. should:所有条件中成立一个即可
            //多条件
            final SearchResponse<Produce> response = client.search(builder ->
                            builder.index("produces").query(q ->
                                    q.bool(b ->
                                            b.must(t ->
                                                    t.term(v ->
                                                            v.field("name")
                                                                    .value("旺仔摇不动")))
                                             .must(t2 ->
                                                     t2.term(v2 ->
                                                            v2.field("price")
                                                                     .value(0.0D))))),
                    Produce.class);
            System.err.println(response.hits().hits());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    多字段查询

            //多字段查询
            final SearchResponse<Produce> response = client.search(builder ->
                            builder.index("produces").query(q->q.multiMatch(qs->qs.query("蚊虫叮咬 辣眼睛").fields("name","des"))),
                    Produce.class);
            System.err.println(response.hits().hits());
    
    • 1
    • 2
    • 3
    • 4
    • 5

    字段分词查询

            //字段分词查询
            final SearchResponse<Produce> response = client.search(builder ->
                            builder.index("produces").query(q->q.queryString(qs->qs.defaultField("des").query("摇滚"))),
                    Produce.class);
            System.err.println(response.hits().hits());
    
    • 1
    • 2
    • 3
    • 4
    • 5

    高亮显示

    我们注意要设置前缀和后缀

            //高亮显示
            final SearchResponse<Produce> response = client.search(builder ->
                            builder.index("produces")
                                    .query(q -> q.match(v -> v.field("name").query("风油精")))
                                    .highlight(h -> h.preTags("").postTags("").fields("name", hf -> hf)),
                    Produce.class);
            System.err.println(response.toString());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    分页查询

    我们使用match_all进行全部搜索的时候使用size关键字设置每一页的大小,使用from关键字设置页码
    from的计算公式:(页码-1)*size

            //分页查询
                    final SearchResponse<Produce> response = client.search(builder ->
                            builder.index("produces")
                                    .query(q->q.matchAll(v->v)).size(2).from(0),
                    Produce.class);
            System.err.println(response.hits().hits());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    排序

    使用sort关键字指定需要进行排序的字段设置排序类型即可,我们这里会使用到SortOrder枚举类来进行指定排序方式

    1. desc:降序
    2. asc:升序
            //排序
            final SearchResponse<Produce> response = client.search(builder ->
                            builder.index("produces")
                                    .query(q->q.matchAll(v->v))
                                    .sort(builder1 -> builder1.field(f->f.field("price").order(SortOrder.Asc))),
                    Produce.class);
            System.err.println(response.hits().hits());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    指定字段查询

    使用_source关键字在数组中设置需要展示的字段
    值得注意的是在source方法中需要我们写filter去指定是include包含或是exclude去除xx字段

            //指定字段查询
            final SearchResponse<Produce> response = client.search(builder ->
                            builder.index("produces")
                                    .query(q->q.matchAll(v->v))
                                    .source(s->s.filter(v->v.includes("price","des"))),
                    Produce.class);
            System.err.println(response.hits().hits());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  • 相关阅读:
    物联网浏览器(IoTBrowser)-电子秤模块及二次驱动开发
    /etc/sudoers文件未配置nopasswd:但sudo su没有输密码就直接进root了
    【R语言】动画图:散点图
    湖仓一体电商项目(三):3万字带你从头开始搭建12个大数据项目基础组件
    【Linux】SSH登陆时终端报错shell request failed on channel 0
    2.4G无线麦克风领夹麦一拖二_全双工_杰理JL6976M单芯片方案
    架构师系列-Nginx、OpenResty(一)- 基本使用配置
    恋爱脑学Rust之dyn关键字的作用
    计算机毕业设计(附源码)python舟影短视频平台
    Deque容器的系列操作( 详解 )
  • 原文地址:https://blog.csdn.net/qq_51553982/article/details/127738852