Eureka注册中心 与 OpenFeign调用接口

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

需求

一个应用通过接口,调用另一个应用的接口。使用OpenFeign来实现接口调用。

说明

通过OpenFeign(本文接下来简称Feign)调用远程接口,需要Eureka注册中心的支持。

OpenFeign调用接口的逻辑如下:

  1. 提供接口的应用(A),将自身注册到Eureka服务器(注册中心);应用A需要给自己起一个应用名称;
  2. 调用接口的应用(B),从Eureka读取所有已注册服务的信息;
  3. B应用的Feign客户端,通过服务的应用名称,从已注册服务的信息中,找到应用A(对应的IP地址和端口号),从而调用A的接口。

本文主要内容

本文主要讲述,如何配置一个注册中心(Eureka),Feign的配置,以及使用Feign来调用接口。
主要包含三个部分:

  1. 配置Eureka注册中心(单体,非集群);
  2. 配置提供接口的应用,注册到Eureka:提供被调用的接口;
  3. 配置调用接口的应用,从Eureka获取到被调用方地址:调用接口。

Eureka服务器

1. 依赖

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

2. 配置(application.properties)

此配置为单体服务器配置,非集群配置。

server.port=8761

# 主机名,不配置的时候将根据操作系统的主机名获取。
eureka.instance.hostname=localhost

# 不将自身注册到注册中心。是否将自己注册到注册中心,默认为true。单个Eureka服务器,不需要注册自身,配置为false;如果是Eureka集群,则需要注册自身,即配置为true。
eureka.client.registerWithEureka=false
# 是否从注册中心获取服务注册信息,默认为true。
eureka.client.fetchRegistry=false
# 注册中心对外暴露的注册地址
eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/


3. 开启Eureka服务器

在 Application 启动类上,添加注解 @EnableEurekaServer.

示例代码:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class EurekaServerDemoApplication {

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

}

FeignServer

提供接口的应用,可以通过Feign来调用接口。

1. 依赖

  • Eureka Discovery Client
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

2. 配置(application.properties)

server.port=8081

# 应用名称
spring.application.name=feign-server
# 使用 ip地址:端口号 注册
eureka.instance.prefer-ip-address=true
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}
# 注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/

3. 提供接口

package com.example.feign.server.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("feign_server_path")
public class DataController {

	@GetMapping("hello")
	public String hello() {
		return "hello feign server!";
	}

	@GetMapping("data")
	public String getData() {
		return "来自FeignServer的数据!";
	}

	@GetMapping("result")
	public String getData(String account) {
		return "从FeignServer查询的数据!入参为:" + account;
	}

	@GetMapping("two_params")
	public String getDataByTwoParam(String account, String name) {
		return "从FeignServer查询的数据!account=" + account + ",name=" + name;
	}

}


Feign客户端

通过Feign,调用FeignServer应用的接口。

1. 依赖

需要引入两个依赖:

  • Eureka Discovery Client
  • OpenFeign
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

注意:需要通过 <dependencyManagement><properties>,管理 spring cloud 版本。如果项目中已经添加,则无需再额外修改。

<dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-dependencies</artifactId>
			<version>${spring-cloud.version}</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
	</dependencies>
</dependencyManagement>
<properties>
	<spring-cloud.version>2021.0.8</spring-cloud.version>
</properties>

2. 配置(application.properties)

server.port=8082

# 不将自身注册到Eureka注册中心。本配置为是否将自己注册到注册中心,默认为true。
eureka.client.registerWithEureka=false
# 注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/

3. 开启Feign客户端

在 Application 启动类上,添加注解 @EnableFeignClients.

示例代码:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients
@SpringBootApplication
public class FeignClientDemoApplication {

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

}

4. 定义接口(与FeignServer对应)

注解 @FeignClient:表示Feign接口。

name:Feign所调用的应用的应用名称。

path:FeignClient中所有接口的公共路径。一般对应到Feign所调用的应用的Controller的接口公共路径,即 Controller 上 @RequestMapping 中的接口路径。

注意:FeignClient 中,namevalue,互为别名。官网示例中使用的是 name,本示例中也采用了name字段。但是经过测试,value字段的效果与name完全一样。

package com.example.feign.client.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "feign-server", path = "feign_server_path")
public interface DataClient {

	@GetMapping("data")
	String getData();

	@GetMapping("result")
	String getDataByOneParam(@RequestParam("account") String account);

	@GetMapping("two_params")
	public String getDataByTwoParam(@RequestParam("account") String account, @RequestParam("name") String name);

}

5. 调用Feign接口

像调用本地方法一样,调用Feign接口。

package com.example.feign.client.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.feign.client.feign.DataClient;

@RestController
@RequestMapping("feign_client")
public class FeignClientDataController {

	@GetMapping("hello")
	public String hello() {
		return "hello feign client!";
	}

	@Autowired
	private DataClient client;

	@GetMapping("data")
	public String getData() {
		return "通过FeignClient调用:" + client.getData();
	}

	@GetMapping("one_param")
	public String getDataByOneParam(String account) {
		return "通过FeignClient调用:" + client.getDataByOneParam(account);
	}

	@GetMapping("two_params")
	public String getDataByTwoParam(String account, String name) {
		return "通过FeignClient调用:" + client.getDataByTwoParam(account, name);
	}

}


调用示例

Eureka

Eureka注册中心 与 OpenFeign调用接口,Spring,eureka,feign,OpenFeign

FeignServer的接口直接调用

Eureka注册中心 与 OpenFeign调用接口,Spring,eureka,feign,OpenFeign

FeignClient通过Feign,调用FeignServer的接口

Eureka注册中心 与 OpenFeign调用接口,Spring,eureka,feign,OpenFeign

创建项目时添加Eureka和Feign依赖

在新创建SpringBoot项目时,可以通过SpringBoot创建器,添加依赖。此时,在左下侧的依赖搜索框内,可以直接搜索到Eureka和OpenFeign的相关依赖。勾选需要的依赖,则创建时对应依赖直接添加到项目中。

Eureka和OpenFeign的三个依赖,以及对应的含义如下:

Eureka Server :Eureka服务器;
Eureka Discovery Client :Eureka客户端;
OpenFeign :Feign客户端;

Eureka注册中心 与 OpenFeign调用接口,Spring,eureka,feign,OpenFeign

application中配置被调用方的应用名

Feign客户端使用配置(占位符),设置被调用方的应用名。
Feign中,name 和 url 属性支持占位符。

官网示例

Eureka注册中心 与 OpenFeign调用接口,Spring,eureka,feign,OpenFeign

代码实例

Eureka注册中心 与 OpenFeign调用接口,Spring,eureka,feign,OpenFeign

FeignClient配置

package com.example.feign.client.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "${feign.name}", path = "feign_server_path")
public interface DataClient {

	@GetMapping("data")
	String getData();

	@GetMapping("one_param")
	String getDataByOneParam(@RequestParam("account") String account);

	@GetMapping("two_params")
	public String getDataByTwoParam(@RequestParam("account") String account, @RequestParam("name") String name);

}

application配置

# 被调用的Feign服务的应用名
feign.name=feign-server

contextId:区分同一个应用对应多个FeignClient

被调用的应用所提供的接口,根据业务逻辑,可能会划分为多个不同的模块。在Feign客户端中,每一个模块,对应一个独立的FeignClient。
因为调用是同一个应用,所以多个FeignClient的应用名(name字段)是相同的。需要通过contextId字段,对不同的FeignClient进行处分;否则会出现冲突导致报错。

核心代码

两个FeignClient,分别使用不同的contextId

package com.example.feign.client.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "${feign.name}", path = "feign_server_path", contextId = "DataClient")
public interface DataClient {
	// 接口定义代码,省略...
}

package com.example.feign.client.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "${feign.name}", path = "files", contextId = "FileClient")
public interface FileClient {
	// 接口定义代码,省略...
}

不加contextId的报错

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
[2m2023-08-06 23:27:20.184[0;39m [31mERROR[0;39m [35m14592[0;39m [2m---[0;39m [2m[           main][0;39m [36mo.s.b.d.LoggingFailureAnalysisReporter  [0;39m [2m:[0;39m 

***************************
APPLICATION FAILED TO START
***************************

Description:

The bean '${feign.name}.FeignClientSpecification' could not be registered. A bean with that name has already been defined and overriding is disabled.

Action:

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

参考

  1. 官方文档
    Eureka注册中心 与 OpenFeign调用接口,Spring,eureka,feign,OpenFeign

  2. 博客
    两个FeignClient接口使用相同服务名报错问题

@RequestParam:Get方法参数注解

Feign的Get方法,请求参数需要加 @RequestParam 注解。
如果不加注解,根据参数的个数,会报如下两种错误。

两种报错

Body parameter 0 was null

Feign客户端,调用Get方法时,接口包含一个参数,报错:

java.lang.IllegalArgumentException: Body parameter 0 was null

Method has too many Body parameters

Feign客户端,调用Get方法时,接口包含多个参数,报错:

Method has too many Body parameters

报错接口的原始代码

Body parameter 0 was null

  • Feign服务器端接口
	@GetMapping("one_param")
	public String getData(String account) {
		return "从FeignServer查询的数据!入参为:" + account;
	}
  • Feign客户端
	@GetMapping("one_param")
	String getData(String account);

Method has too many Body parameters

  • Feign服务器端接口
	@GetMapping("two_params")
	public String getDataByTwoParam(String account, String name) {
		return "从FeignServer查询的数据!account=" + account + ",name=" + name;
	}
  • Feign客户端
	@GetMapping("two_params")
	public String getDataByTwoParam(String account, String name);

解决方法:@RequestParam

Feign接口参数添加@RequestParam注解。

Feign客户端,修改后的代码如下:

import org.springframework.web.bind.annotation.RequestParam;
	@GetMapping("result")
	String getData(@RequestParam("account") String account);

	@GetMapping("two_params")
	public String getDataByTwoParam(@RequestParam("account") String account, @RequestParam("name") String name);

完整的Feign客户端代码示例

package com.example.feign.client.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "feign-server", path = "feign_server_path")
public interface FeignInvocationService {

	@GetMapping("data")
	String getFeignServerData();

	@GetMapping("result")
	String getData(@RequestParam("account") String account);

	@GetMapping("two_params")
	public String getDataByTwoParam(@RequestParam("account") String account, @RequestParam("name") String name);

}

成功调用的接口示例

Eureka注册中心 与 OpenFeign调用接口,Spring,eureka,feign,OpenFeign

Eureka注册中心 与 OpenFeign调用接口,Spring,eureka,feign,OpenFeign

@SpringQueryMap

Feign 的 GET接口,数据类(即:POJO)作为参数,使用 @SpringQueryMap 注解标注参数。
不在参数前加上 @SpringQueryMap 注解,Feign会报错。

代码示例

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.SpringQueryMap;
import org.springframework.web.bind.annotation.GetMapping;

import com.example.feign.client.feign.query.InputQuery;

@FeignClient(name = "${feign.name}", path = "feign_server_path", contextId = "DataClient")
public interface DataClient {

	// 其他接口...

	@GetMapping("query_object")
	public String getDataByQueryObject(@SpringQueryMap InputQuery query);

}

报错

2023-08-07 23:06:25.570[0;39m [31mERROR[0;39m [35m9368[0;39m [2m---[0;39m [2m[nio-8082-exec-3][0;39m [36mo.a.c.c.C.[.[.[/].[dispatcherServlet]   [0;39m [2m:[0;39m Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is feign.FeignException$MethodNotAllowed: [405] during [GET] to [http://feign-server/feign_server_path/query_object] [DataClient#getDataByQueryObject(InputQuery)]: [{"timestamp":"2023-08-07T15:06:25.532+00:00","status":405,"error":"Method Not Allowed","path":"/feign_server_path/query_object"}]] with root cause

feign.FeignException$MethodNotAllowed: [405] during [GET] to [http://feign-server/feign_server_path/query_object] [DataClient#getDataByQueryObject(InputQuery)]: [{"timestamp":"2023-08-07T15:06:25.532+00:00","status":405,"error":"Method Not Allowed","path":"/feign_server_path/query_object"}]
        at feign.FeignException.clientErrorStatus(FeignException.java:221) ~[feign-core-11.10.jar:na]
        at feign.FeignException.errorStatus(FeignException.java:194) ~[feign-core-11.10.jar:na]
        at feign.FeignException.errorStatus(FeignException.java:185) ~[feign-core-11.10.jar:na]

官方文档

Feign @QueryMap support

Eureka注册中心 与 OpenFeign调用接口,Spring,eureka,feign,OpenFeign

@SpringQueryMap 参数丢失

使用 @SpringQueryMap 的接口,只能含有一个参数对象。
如果接口有两个参数对象,并且都用 @SpringQueryMap 注解,则第二个参数对象会被丢弃,根本不会解析到接口请求的参数里。

打印Feign日志

Feign发送请求的日志信息

配置文件

# 打印Feign接口调用日志(仅开发测试环境使用)
logging.level.com.kiiik.web=debug
feign.client.config.default.loggerLevel=FULL

参考文章:
打印Feign日志

url:通过IP地址和端口号访问被调用应用

@FeignClient 可以通过 url 字段,指定房屋服务器的IP地址和端口号。当被调用的应用,没有注册到Eureka注册中心时,直接通过 url 配置实际地址就好了。

代码示例

FeignClient:url字段

package com.example.feign.client.feign;

import org.springframework.cloud.openfeign.FeignClient;

@FeignClient(name = "${feign.name}", url = "${feign.url}", path = "feign_server_path", contextId = "DataClient")
public interface DataClient {
    // 接口,省略...
}

配置

# 被调用的Feign服务的IP地址和端口号(用于调用没有注册到Eureka的服务)
feign.url=http://localhost:8081

官网文档

Eureka注册中心 与 OpenFeign调用接口,Spring,eureka,feign,OpenFeign

通过Feign调用接口下载文件

实现方法

需要通过Feign调用接口下载文件,直接让Feign接口返回值为 Response,全称 feign.Response;然后通过Response获取到输入流。之后就可以对输入流进行处理,放入输出流中,保存到本地或传递文件给用户。

代码示例

package com.example.feign.client.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import feign.Response;

@FeignClient(name = "${feign.name}", url = "${feign.url}", path = "files", contextId = "FileClient")
public interface FileClient {

	@GetMapping("download")
	Response download();

	@GetMapping("/download/{filename}")
	Response download(@PathVariable("filename") String filename);

}

package com.example.feign.client.controller;

import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;

import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.feign.client.feign.FileClient;

@RestController
@RequestMapping("files")
public class FileController {

	@Autowired
	private FileClient client;

	@GetMapping("download")
	public void download(HttpServletResponse response) throws IOException {
		InputStream inputStream = client.download().body().asInputStream();

		String fileName = URLEncoder.encode("测试文件.txt", "UTF-8");
		response.setHeader("content-disposition", "attachment;fileName=" + fileName);
		ServletOutputStream outputStream = response.getOutputStream();

		IOUtils.copy(inputStream, outputStream);
		IOUtils.closeQuietly(inputStream);
		IOUtils.closeQuietly(outputStream);
	}

	@GetMapping("/download/{filename}")
	public void downloadByPathname(@PathVariable("filename") String filename, HttpServletResponse response)
			throws IOException {
		InputStream inputStream = client.download(filename).body().asInputStream();

		String fileName = URLEncoder.encode(filename, "UTF-8");
		response.setHeader("content-disposition", "attachment;fileName=" + fileName);
		ServletOutputStream outputStream = response.getOutputStream();

		IOUtils.copy(inputStream, outputStream);
		IOUtils.closeQuietly(inputStream);
		IOUtils.closeQuietly(outputStream);
	}

}

Spring Cloud OpenFeign 官方文档

Spring Cloud OpenFeign官方文档

Spring Cloud OpenFeign 官网

Spring Cloud OpenFeign 官网文章来源地址https://www.toymoban.com/news/detail-619111.html

到了这里,关于Eureka注册中心 与 OpenFeign调用接口的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • 【spring cloud学习】3、Eureka Server注册中心

    Eureka本身是Netflix开源的一款注册中心产品,并且Spring Cloud提供了相应的集成封装。选择Eureka作为注册中心实例来讲解是出于以下原因: (1)Eureka在业界的应用十分广泛,整个框架经受住了Netflix严酷生产环境的考验。 (2)除了Eureka注册中心外,Netflix的其他服务治理功能也十

    2024年02月11日
    浏览(56)
  • 【Spring Cloud】深入理解 Eureka 注册中心的原理、服务的注册与发现

    在微服务架构中,服务的注册与发现是至关重要的一环。为了实现这一目标,Eureka 注册中心应运而生。在本篇文章中,我们将深入理解 Eureka 注册中心的原理,以及探讨服务的注册与发现机制。 在微服务的协作中,服务之间的远程调用是常见的需求。然而,使用传统的 Rest

    2024年02月08日
    浏览(62)
  • Spring Cloud Eureka Service Registry 服务注册中心实践

    作者:禅与计算机程序设计艺术 在分布式微服务架构下,服务发现是保证应用可用的关键组件之一。在Spring Cloud体系中,服务发现中心通过Netflix Eureka实现。 本文将介绍Spring Cloud Eureka服务注册中心的机制、配置及使用方法,并通过实例对Eureka的功能及其局限性进行详细阐述

    2024年02月11日
    浏览(89)
  • 特别详细的Spring Cloud 系列教程1:服务注册中心Eureka的启动

    Eureka已经被Spring Cloud继承在其子项目spring-cloud-netflix中,搭建Eureka Server的方式还是非常简单的。只需要通过一个独立的maven工程即可搭建Eureka Server。  我们引入spring cloud的依赖和eureka的依赖。 注意spring cloud和springboot的版本要对应,不然容易出现各种奇怪的错误。 不知道spr

    2024年04月08日
    浏览(76)
  • 客快物流大数据项目(一百一十三):注册中心 Spring Cloud Eureka

    文章目录 注册中心 Spring Cloud Eureka 一、Eureka 简介 二、架构图

    2023年04月25日
    浏览(67)
  • 【微服务】Eureka注册中心

    我们在前文的案例中,我们采取如下的方式发送http请求: 我们将user-service的ip地址和端口硬编码在了代码当中,这样的写法是有一定问题的。我们在公司开发中,可能会面临多个环境,开发环境、测试环境等等,每一次环境的变更可能服务的地址也会发生变化,使用硬编码显

    2024年01月15日
    浏览(38)
  • 服务注册中心 Eureka

    服务注册中心 Eureka Spring Cloud Eureka 是 Netflix 公司开发的注册发现组件,本身是一个基于 REST 的服务。提供 注册与发现 ,同时还提供了负载均衡、故障转移等能力。 Eureka 有 3 个角色 服务中心(Eureka Server):服务器端。它提供服务的注册和发现功能,即实现服务的治理。 服

    2024年02月11日
    浏览(55)
  • 微服务Eureka注册中心

    目录 一、Eureka的结构和作用 二、搭建eureka-server 三、服务注册 四、服务发现 假如我们的服务提供者user-service部署了多个实例,如图: 存在的问题: order-service在发起远程调用的时候,该如何得知user-service实例的ip地址和端口? 有多个user-service实例地址,order-service调用时该如

    2024年02月13日
    浏览(53)
  • SpringCloud--Eureka注册中心

              Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务注册和发现。Eureka 采用了 C-S 的设计架构。Eureka Server 作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用 Eureka 的客户端连接到 Eureka Server并维持心跳连接。这样系统的维护人员就

    2024年01月17日
    浏览(39)
  • SpringCloud --- Eureka注册中心

    假如我们的服务提供者user-service部署了多个实例,如图 思考几个问题: order-service在发起远程调用的时候,该如何得知user-service实例的ip地址和端口? 有多个user-service实例地址,order-service调用时该如何选择? order-service如何得知某个user-service实例是否依然健康,是不是已经宕

    2023年04月24日
    浏览(41)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包