Spring Cloud Alibaba(二、Nacos服務注冊及 RestTemplate、FeignClient方式發(fā)現(xiàn))

SpringCloud標準版的服務注冊可以用 eureka、consul或zookeeper,配置中心用config或bus,現(xiàn)在Spring Cloud AlibabaNacos搞定這些,簡言之就是注冊中心加配置中心。Nacos可以代替eureka做服務注冊中心。
Gitee上的Spring-Cloud-Alibaba/nacos-discovery-example

Nacos

Nacos是什么

Nacos 致力于幫助您發(fā)現(xiàn)、配置和管理微服務。Nacos 提供了一組簡單易用的特性集,幫助您快速實現(xiàn)動態(tài)服務發(fā)現(xiàn)、服務配置、服務元數(shù)據(jù)及流量管理。

Nacos 的關鍵特性包括:

  • 服務發(fā)現(xiàn)和服務健康監(jiān)測

    Nacos 支持基于 DNS 和基于 RPC 的服務發(fā)現(xiàn)。服務提供者使用 原生SDK、OpenAPI、或一個獨立的Agent TODO注冊 Service 后,服務消費者可以使用DNS TODOHTTP&API查找和發(fā)現(xiàn)服務。

    Nacos 提供對服務的實時的健康檢查,阻止向不健康的主機或服務實例發(fā)送請求。Nacos 支持傳輸層 (PING 或 TCP)和應用層 (如 HTTP、MySQL、用戶自定義)的健康檢查。 對于復雜的云環(huán)境和網(wǎng)絡拓撲環(huán)境中(如 VPC、邊緣網(wǎng)絡等)服務的健康檢查,Nacos 提供了 agent 上報模式和服務端主動檢測2種健康檢查模式。Nacos 還提供了統(tǒng)一的健康檢查儀表盤,幫助您根據(jù)健康狀態(tài)管理服務的可用性及流量。

  • 動態(tài)配置服務

    動態(tài)配置服務可以讓您以中心化、外部化和動態(tài)化的方式管理所有環(huán)境的應用配置和服務配置。

    動態(tài)配置消除了配置變更時重新部署應用和服務的需要,讓配置管理變得更加高效和敏捷。

    配置中心化管理讓實現(xiàn)無狀態(tài)服務變得更簡單,讓服務按需彈性擴展變得更容易。

    Nacos 提供了一個簡潔易用的UI (控制臺樣例 Demo) 幫助您管理所有的服務和應用的配置。Nacos 還提供包括配置版本跟蹤、金絲雀發(fā)布、一鍵回滾配置以及客戶端配置更新狀態(tài)跟蹤在內(nèi)的一系列開箱即用的配置管理特性,幫助您更安全地在生產(chǎn)環(huán)境中管理配置變更和降低配置變更帶來的風險。

  • 動態(tài) DNS 服務

    動態(tài) DNS 服務支持權(quán)重路由,讓您更容易地實現(xiàn)中間層負載均衡、更靈活的路由策略、流量控制以及數(shù)據(jù)中心內(nèi)網(wǎng)的簡單DNS解析服務。動態(tài)DNS服務還能讓您更容易地實現(xiàn)以 DNS 協(xié)議為基礎的服務發(fā)現(xiàn),以幫助您消除耦合到廠商私有服務發(fā)現(xiàn) API 上的風險。

    Nacos 提供了一些簡單的 DNS APIs (暫未實現(xiàn)) 幫助您管理服務的關聯(lián)域名和可用的 IP:PORT 列表.

  • 服務及其元數(shù)據(jù)管理

    Nacos 能讓您從微服務平臺建設的視角管理數(shù)據(jù)中心的所有服務及元數(shù)據(jù),包括管理服務的描述、生命周期、服務的靜態(tài)依賴分析、服務的健康狀態(tài)、服務的流量管理、路由及安全策略、服務的 SLA 以及最首要的 metrics 統(tǒng)計數(shù)據(jù)。

前提

您需要先下載 Nacos 并啟動 Nacos server。操作步驟參見 Nacos 快速入門。我使用的是windows單機版,進入bin目錄下執(zhí)行以下命令啟動nacos-startup.bat

啟動Nacos

管理頁面:訪問ip:8848/nacos,看到如下畫面就啟動成功了,默認賬號密碼都是nacos。
Nacos控制臺管理頁面

服務注冊與發(fā)現(xiàn)

本項目演示如何使用 Nacos Discovery Starter 完成 Spring Cloud 應用的服務注冊與發(fā)現(xiàn)。

服務提供者
  1. 新建基于Nacos的服務提供者
    新建module,Spring boot的腳手架starter使用阿里云https://start.aliyun.com/。
    Spring Initializr
  2. Dependiencies選擇如下圖所示:
    Nacos Service Discovery依賴

    1.我的IDEA 2020.1Spring Boot 2.2.0版本,如果與我不同,可以參考我的pom文件修改版本:
<?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>2.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.pay.cloud.alibaba</groupId>
    <artifactId>nacos-discovery-provider</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>nacos-discovery-provider</name>
    <description>Spring cloud Alibaba nacos-discovery-provider project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

核心是引入如下依賴:

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

踩坑:
由于Spring Boot 2.3.0版本太新,使用<spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>,會報錯Endpoint ID 'nacos-discovery' contains invalid characters, please migrate to a valid format.。查看Spring-Cloud-Alibaba開源代碼pom.xml,發(fā)現(xiàn)他使用的spring-cloud版本是<spring-cloud-commons.version>2.2.2.RELEASE</spring-cloud-commons.version>,二者的版本對應關系見上篇文章末尾部分。

  1. application.properties
server.port=18081
spring.application.name=service-provider
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
#spring.cloud.nacos.discovery.instance-enabled=true

spring.cloud.nacos.username=nacos
spring.cloud.nacos.password=nacos

management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always

配置文件中配置 Nacos Server 地址:spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848。
Nacos 服務的名稱會使用:spring.application.name=service-provider配置值。

  1. 啟動類
package com.pay.cloud.alibaba.nacosdiscoveryprovider;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @ClassName: NacosDiscoveryProviderApplication
 * @Description: nacos服務提供者
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/6/11 16:51
 * @Copyright:
 */
@EnableDiscoveryClient
@SpringBootApplication
public class NacosDiscoveryProviderApplication {

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

    @RestController
    class EchoController {

        @GetMapping("/")
        public ResponseEntity index() {
            return new ResponseEntity("index error", HttpStatus.INTERNAL_SERVER_ERROR);
        }

        @GetMapping("/test")
        public ResponseEntity test() {
            return new ResponseEntity("error", HttpStatus.INTERNAL_SERVER_ERROR);
        }

        @GetMapping("/sleep")
        public String sleep() {
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "ok";
        }

        @GetMapping("/echo/{string}")
        public String echo(@PathVariable String string) {
            return "Guoxiuzhi's Nacos Discovery output:" + string;
        }

        @GetMapping("/divide")
        public String divide(@RequestParam Integer a, @RequestParam Integer b) {
            return String.valueOf(a / b);
        }

    }
}

使用 @EnableDiscoveryClient注解開啟服務注冊與發(fā)現(xiàn)功能。

  1. 啟動 Nacos Server
  • 首先需要獲取 Nacos Server,支持直接下載和源碼構(gòu)建兩種方式。

    1. 直接下載:Nacos Server 下載頁
    2. 源碼構(gòu)建:進入 Nacos Github 項目頁面,將代碼 git clone 到本地自行編譯打包,參考此文檔。推薦使用源碼構(gòu)建方式以獲取最新版本
  • 啟動 Server,進入解壓后文件夾或編譯打包好的文件夾,找到如下相對文件夾 nacos/bin,并對照操作系統(tǒng)實際情況之下如下命令。

    1. Linux/Unix/Mac 操作系統(tǒng),執(zhí)行命令 sh startup.sh -m standalone
    2. Windows 操作系統(tǒng),執(zhí)行命令 cmd startup.cmd
  1. 啟動應用
    支持 IDE 直接啟動和編譯打包后啟動。
    然后看看nacos控制臺,可以看到服務名稱為service-provider已經(jīng)注冊到了Nacos:
    服務注冊截圖
服務消費者

分別使用 RestTemplate 和 FeignClient來消費服務。

  1. pom文件
<?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>2.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.pay.alibaba.nacos</groupId>
    <artifactId>nacos-discovery-consumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>nacos-discovery-consumer</name>
    <description>nacos-discovery-consumer project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.1.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  1. 代碼
    啟動類:
package com.pay.alibaba.nacos.nacosdiscovery.consumer;


import com.alibaba.cloud.sentinel.annotation.SentinelRestTemplate;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.UrlCleaner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

/**
 * @ClassName: ConsumerApplication
 * @Description: 啟動類
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/6/11 20:15
 * @Copyright:
 */
@SpringBootApplication
@EnableDiscoveryClient(autoRegister = true)
@EnableFeignClients
public class ConsumerApplication {

    @LoadBalanced
    @Bean
    @SentinelRestTemplate(urlCleanerClass = UrlCleaner.class, urlCleaner = "clean")
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @LoadBalanced
    @Bean
    @SentinelRestTemplate
    public RestTemplate restTemplate1() {
        return new RestTemplate();
    }

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

}

說明:

添加 @LoadBlanced 注解,使得 RestTemplate 接入 Ribbon

 @Bean
 @LoadBalanced
 public RestTemplate restTemplate() {
     return new RestTemplate();
 }

EchoService

package com.pay.alibaba.nacos.nacosdiscovery.consumer.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
/**   
 * @ClassName: EchoService
 * @Description: 
 * @author: 郭秀志 jbcode@126.com
 * @date:   2020/6/11 21:11    
 * @Copyright:  
 */
@FeignClient(name = "service-provider", fallback = EchoServiceFallback.class,
        configuration = FeignConfiguration.class)
public interface EchoService {

    @GetMapping("/echo/{str}")
    String echo(@PathVariable("str") String str);

    @GetMapping("/divide")
    String divide(@RequestParam("a") Integer a, @RequestParam("b") Integer b);

    default String divide(Integer a) {
        return divide(a, 0);
    }

    @GetMapping("/notFound")
    String notFound();

}

class FeignConfiguration {

    @Bean
    public EchoServiceFallback echoServiceFallback() {
        return new EchoServiceFallback();
    }

}

class EchoServiceFallback implements EchoService {

    @Override
    public String echo(@PathVariable("str") String str) {
        return "echo fallback";
    }

    @Override
    public String divide(@RequestParam Integer a, @RequestParam Integer b) {
        return "divide fallback";
    }

    @Override
    public String notFound() {
        return "notFound fallback";
    }

}

說明:
FeignClient 已經(jīng)默認集成了 Ribbon ,此處演示如何配置一個 FeignClient。使用 @FeignClient 注解將 EchoService 這個接口包裝成一個 FeignClient,屬性 name 對應服務名 service-provider。

 @FeignClient(name = "service-provider")
 public interface EchoService {
     @GetMapping(value = "/echo/{str}")
     String echo(@PathVariable("str") String str);
 }

UrlCleaner

package com.pay.alibaba.nacos.nacosdiscovery.consumer.uitil;
/**   
 * @ClassName: UrlCleaner
 * @Description: 
 * @author: 郭秀志 jbcode@126.com
 * @date:   2020/6/11 20:18    
 * @Copyright:  
 */
public class UrlCleaner {

    public static String clean(String url) {
        System.out.println("enter urlCleaner");
        if (url.matches(".*/echo/.*")) {
            System.out.println("change url");
            url = url.replaceAll("/echo/.*", "/echo/{str}");
        }
        return url;
    }

}

TestController

package com.pay.alibaba.nacos.nacosdiscovery.consumer.controller;


import com.pay.alibaba.nacos.nacosdiscovery.consumer.service.EchoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**   
 * @ClassName:  TestController
 * @Description: 
 * @author: 郭秀志 jbcode@126.com
 * @date:   2020/6/11 21:00
 * @Copyright:  
 */
@RestController
public class TestController {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private RestTemplate restTemplate1;

    @Autowired
    private EchoService echoService;

    @Autowired
    private DiscoveryClient discoveryClient;

    // @PostConstruct
    // public void init() {
    // restTemplate1.setErrorHandler(new ResponseErrorHandler() {
    // @Override
    // public boolean hasError(ClientHttpResponse response) throws IOException {
    // return false;
    // }
    //
    // @Override
    // public void handleError(ClientHttpResponse response) throws IOException {
    // System.err.println("handle error");
    // }
    // });
    // }

    @GetMapping("/echo-rest/{str}")
    public String rest(@PathVariable String str) {
        return restTemplate.getForObject("http://service-provider/echo/" + str,
                String.class);
    }

    @GetMapping("/index")
    public String index() {
        return restTemplate1.getForObject("http://service-provider", String.class);
    }

    @GetMapping("/test")
    public String test() {
        return restTemplate1.getForObject("http://service-provider/test", String.class);
    }

    @GetMapping("/sleep")
    public String sleep() {
        return restTemplate1.getForObject("http://service-provider/sleep", String.class);
    }

    @GetMapping("/notFound-feign")
    public String notFound() {
        return echoService.notFound();
    }

    @GetMapping("/divide-feign")
    public String divide(@RequestParam Integer a, @RequestParam Integer b) {
        return echoService.divide(a, b);
    }

    @GetMapping("/divide-feign2")
    public String divide(@RequestParam Integer a) {
        return echoService.divide(a);
    }

    @GetMapping("/echo-feign/{str}")
    public String feign(@PathVariable String str) {
        return echoService.echo(str);
    }

    @GetMapping("/services/{service}")
    public Object client(@PathVariable String service) {
        return discoveryClient.getInstances(service);
    }

    @GetMapping("/services")
    public Object services() {
        return discoveryClient.getServices();
    }

}

echo 方法上的 @RequestMapping 注解將 echo 方法與 URL "/echo/{str}" 相對應,@PathVariable 注解將 URL 路徑中的 {str} 對應成 echo 方法的參數(shù) str。

完成以上配置后,將兩者自動注入到 TestController 中。

@RestController
public class TestController {

    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private EchoService echoService;

    @GetMapping(value = "/echo-rest/{str}")
    public String rest(@PathVariable String str) {
        return restTemplate.getForObject("http://service-provider/echo/" + str, String.class);
    }
    @GetMapping(value = "/echo-feign/{str}")
    public String feign(@PathVariable String str) {
        return echoService.echo(str);
    }
}

配置必要的配置,在 nacos-discovery-consumer 項目的 /src/main/resources/application.properties 中添加基本配置信息

#################################### common config : ####################################
spring.application.name=nacos-discovery-consumer
# 應用服務web訪問端口
server.port=18080
# ActuatorWeb訪問端口
management.server.port=18081
management.endpoints.jmx.exposure.include=*
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
# spring cloud access&secret config
# 可以訪問如下地址查看: https://usercenter.console.aliyun.com/#/manage/ak
alibaba.cloud.access-key=****
alibaba.cloud.secret-key=****
#################################### nacosdiscovery config : ####################################
# 微服務引擎控制臺: https://mse.console.aliyun.com
# Nacos幫助文檔: https://nacos.io/zh-cn/docs/concepts.html
# Nacos認證信息
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
# Nacos 服務發(fā)現(xiàn)與注冊配置,其中子屬性 server-addr 指定 Nacos 服務器主機和端口
spring.cloud.nacos.discovery.server-addr=localhost:8848

測試運行

啟動應用,支持 IDE 直接啟動和編譯打包后啟動。

  1. 啟動provider項目
    采用打包方式啟動2個服務,來驗證consumer請求provider的負載均衡。
    java -jar nacos-discovery-provider-0.0.1-SNAPSHOT.jar --server.port=8080 --management.server.port=8081
    java -jar nacos-discovery-provider-0.0.1-SNAPSHOT.jar --server.port=8090 --management.server.port=8091
    查看Nacos控制臺,可以看到2個provider實例。
    服務列表
  2. 啟動consumer項目
    采用IDEA直接啟動方式,啟動后如上圖同樣看到consumer被注冊進服務列表。
    restTemplate方式訪問:http://localhost:18080/echo-rest/郭秀志,可以看到應用的第一個控制臺輸出:

restTemplate方式訪問:http://localhost:18080/echo-rest/loadbalance,可以看到應用的第二個控制臺輸出:


FeignClient方式瀏覽器訪問:http://localhost:18080/echo-feign/loadbalance,可以看到返回字符串:
FeignClient

繞口令式總結(jié):可見獨立的客戶端消費者應用通過負載均衡根據(jù)Nacos注冊的服務名稱(而不是ip+端口號)調(diào)用服務生效。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容