淺談Spring Cloud Ribbon的原理
Ribbon是Netflix發(fā)布的開源項(xiàng)目,主要功能是提供客戶端的軟件負(fù)載均衡算法,將Netflix的中間層服務(wù)連接在一起。Ribbon客戶端組件提供一系列完善的配置項(xiàng)如連接超時(shí),重試等。簡(jiǎn)單的說(shuō),就是在配置文件中列出Load Balancer(簡(jiǎn)稱LB)后面所有的機(jī)器,Ribbon會(huì)自動(dòng)的幫助你基于某種規(guī)則(如簡(jiǎn)單輪詢,隨即連接等)去連接這些機(jī)器。我們也很容易使用Ribbon實(shí)現(xiàn)自定義的負(fù)載均衡算法。
說(shuō)起負(fù)載均衡一般都會(huì)想到服務(wù)端的負(fù)載均衡,常用產(chǎn)品包括LBS硬件或云服務(wù)、Nginx等,都是耳熟能詳?shù)漠a(chǎn)品。
而Spring Cloud提供了讓服務(wù)調(diào)用端具備負(fù)載均衡能力的Ribbon,通過和Eureka的緊密結(jié)合,不用在服務(wù)集群內(nèi)再架設(shè)負(fù)載均衡服務(wù),很大程度簡(jiǎn)化了服務(wù)集群內(nèi)的架構(gòu)。
具體也不想多寫虛的介紹,反正哪里都能看得到相關(guān)的介紹。
直接開擼代碼,通過代碼來(lái)看Ribbon是如何實(shí)現(xiàn)的。
配置

詳解:
1.RibbonAutoConfiguration配置生成RibbonLoadBalancerClient實(shí)例。
代碼位置:
spring-cloud-netflix-core-1.3.5.RELEASE.jar
org.springframework.cloud.netflix.ribbon
RibbonAutoConfiguration.class
@Configuration
@ConditionalOnClass({ IClient.class, RestTemplate.class, AsyncRestTemplate.class, Ribbon.class})
@RibbonClients
@AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
@AutoConfigureBefore({LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class})
@EnableConfigurationProperties(RibbonEagerLoadProperties.class)
public class RibbonAutoConfiguration {
// 略
@Bean
@ConditionalOnMissingBean(LoadBalancerClient.class)
public LoadBalancerClient loadBalancerClient() {
return new RibbonLoadBalancerClient(springClientFactory());
}
// 略
}
先看配置條件項(xiàng),RibbonAutoConfiguration配置必須在LoadBalancerAutoConfiguration配置前執(zhí)行,因?yàn)樵贚oadBalancerAutoConfiguration配置中會(huì)使用RibbonLoadBalancerClient實(shí)例。
RibbonLoadBalancerClient繼承自LoadBalancerClient接口,是負(fù)載均衡客戶端,也是負(fù)載均衡策略的調(diào)用方。
2.LoadBalancerInterceptorConfig配置生成:
1).負(fù)載均衡攔截器LoadBalancerInterceptor實(shí)例
包含:
LoadBalancerClient實(shí)現(xiàn)類的RibbonLoadBalancerClient實(shí)例
負(fù)載均衡的請(qǐng)求創(chuàng)建工廠LoadBalancerRequestFactory:實(shí)例
2).RestTemplate自定義的RestTemplateCustomizer實(shí)例
代碼位置:
spring-cloud-commons-1.2.4.RELEASE.jar
org.springframework.cloud.client.loadbalancer
LoadBalancerAutoConfiguration.class
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
// 略
@Bean
@ConditionalOnMissingBean
public LoadBalancerRequestFactory loadBalancerRequestFactory(
LoadBalancerClient loadBalancerClient) {
return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
}
@Configuration
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return new RestTemplateCustomizer() {
@Override
public void customize(RestTemplate restTemplate) {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
}
};
}
}
// 略
}
先看配置條件項(xiàng):
要求在項(xiàng)目環(huán)境中必須要有RestTemplate類。
要求必須要有LoadBalancerClient接口的實(shí)現(xiàn)類的實(shí)例,也就是上一步生成的RibbonLoadBalancerClient。
3.通過上面一步創(chuàng)建的RestTemplateCustomizer配置所有RestTemplate實(shí)例,就是將負(fù)載均衡攔截器設(shè)置給RestTemplate實(shí)例。
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
// 略
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
final List<RestTemplateCustomizer> customizers) {
return new SmartInitializingSingleton() {
@Override
public void afterSingletonsInstantiated() {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
}
};
}
// 略
@Configuration
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return new RestTemplateCustomizer() {
@Override
public void customize(RestTemplate restTemplate) {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
}
};
}
}
// 略
}
restTemplate.setInterceptors(list)這個(gè)地方就是注入負(fù)載均衡攔截器的地方LoadBalancerInterceptor。
從這個(gè)地方實(shí)際上也可以猜出來(lái),RestTemplate可以通過注入的攔截器來(lái)構(gòu)建相應(yīng)的請(qǐng)求實(shí)現(xiàn)負(fù)載均衡。
也能看出來(lái)可以自定義攔截器實(shí)現(xiàn)其他目的。
4.RibbonClientConfiguration配置生成ZoneAwareLoadBalancer實(shí)例
代碼位置:
spring-cloud-netflix-core-1.3.5.RELEASE.jar
org.springframework.cloud.netflix.ribbon
RibbonClientConfiguration.class
@SuppressWarnings("deprecation")
@Configuration
@EnableConfigurationProperties
//Order is important here, last should be the default, first should be optional
// see https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
@Import({OkHttpRibbonConfiguration.class, RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class})
public class RibbonClientConfiguration {
// 略
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
return this.propertiesFactory.get(ILoadBalancer.class, config, name);
}
return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
serverListFilter, serverListUpdater);
}
// 略
}
ZoneAwareLoadBalancer繼承自ILoadBalancer接口,該接口有一個(gè)方法:
/** * Choose a server from load balancer. * * @param key An object that the load balancer may use to determine which server to return. null if * the load balancer does not use this parameter. * @return server chosen */ public Server chooseServer(Object key);
ZoneAwareLoadBalancer就是一個(gè)具體的負(fù)載均衡實(shí)現(xiàn)類,也是默認(rèn)的負(fù)載均衡類,通過對(duì)chooseServer方法的實(shí)現(xiàn)選取某個(gè)服務(wù)實(shí)例。
攔截&請(qǐng)求

1.使用RestTemplate進(jìn)行Get、Post等各種請(qǐng)求,都是通過doExecute方法實(shí)現(xiàn)
代碼位置:
spring-web-4.3.12.RELEASE.jar
org.springframework.web.client
RestTemplate.class
public class RestTemplate extends InterceptingHttpAccessor implements RestOperations {
// 略
protected <T> T doExecute(URI url, HttpMethod method, RequestCallback requestCallback,
ResponseExtractor<T> responseExtractor) throws RestClientException {
Assert.notNull(url, "'url' must not be null");
Assert.notNull(method, "'method' must not be null");
ClientHttpResponse response = null;
try {
ClientHttpRequest request = createRequest(url, method);
if (requestCallback != null) {
requestCallback.doWithRequest(request);
}
response = request.execute();
handleResponse(url, method, response);
if (responseExtractor != null) {
return responseExtractor.extractData(response);
}
else {
return null;
}
}
catch (IOException ex) {
String resource = url.toString();
String query = url.getRawQuery();
resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
throw new ResourceAccessException("I/O error on " + method.name() +
" request for \"" + resource + "\": " + ex.getMessage(), ex);
}
finally {
if (response != null) {
response.close();
}
}
}
// 略
}
支持的各種http請(qǐng)求方法最終都是調(diào)用doExecute方法,該方法內(nèi)調(diào)用創(chuàng)建方法創(chuàng)建請(qǐng)求實(shí)例,并執(zhí)行請(qǐng)求得到響應(yīng)對(duì)象。
2.生成請(qǐng)求實(shí)例創(chuàng)建工廠
上一步代碼中,調(diào)用createRequest方法創(chuàng)建請(qǐng)求實(shí)例,這個(gè)方法是定義在父類中。
先整理出主要的繼承關(guān)系:

createRequest方法實(shí)際是定義在HttpAccessor抽象類中。
public abstract class HttpAccessor {
private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
public void setRequestFactory(ClientHttpRequestFactory requestFactory) {
Assert.notNull(requestFactory, "ClientHttpRequestFactory must not be null");
this.requestFactory = requestFactory;
}
public ClientHttpRequestFactory getRequestFactory() {
return this.requestFactory;
}
protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {
ClientHttpRequest request = getRequestFactory().createRequest(url, method);
if (logger.isDebugEnabled()) {
logger.debug("Created " + method.name() + " request for \"" + url + "\"");
}
return request;
}
}
在createRequest方法中調(diào)用getRequestFactory方法獲得請(qǐng)求實(shí)例創(chuàng)建工廠,實(shí)際上getRequestFactory并不是當(dāng)前HttpAccessor類中定義的,而是在子類InterceptingHttpAccessor中定義的。
public abstract class InterceptingHttpAccessor extends HttpAccessor {
private List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
public void setInterceptors(List<ClientHttpRequestInterceptor> interceptors) {
this.interceptors = interceptors;
}
public List<ClientHttpRequestInterceptor> getInterceptors() {
return interceptors;
}
@Override
public ClientHttpRequestFactory getRequestFactory() {
ClientHttpRequestFactory delegate = super.getRequestFactory();
if (!CollectionUtils.isEmpty(getInterceptors())) {
return new InterceptingClientHttpRequestFactory(delegate, getInterceptors());
}
else {
return delegate;
}
}
}
在這里做了個(gè)小動(dòng)作,首先還是通過HttpAccessor類創(chuàng)建并獲得SimpleClientHttpRequestFactory工廠,這個(gè)工廠主要就是在沒有攔截器的時(shí)候創(chuàng)建基本請(qǐng)求實(shí)例。
其次,在有攔截器注入的情況下,創(chuàng)建InterceptingClientHttpRequestFactory工廠,該工廠就是創(chuàng)建帶攔截器的請(qǐng)求實(shí)例,因?yàn)樽⑷肓素?fù)載均衡攔截器,所以這里就從InterceptingClientHttpRequestFactory工廠創(chuàng)建。
3.通過工廠創(chuàng)建請(qǐng)求實(shí)例
創(chuàng)建實(shí)例就看工廠的createRequest方法。
public class InterceptingClientHttpRequestFactory extends AbstractClientHttpRequestFactoryWrapper {
private final List<ClientHttpRequestInterceptor> interceptors;
public InterceptingClientHttpRequestFactory(ClientHttpRequestFactory requestFactory,
List<ClientHttpRequestInterceptor> interceptors) {
super(requestFactory);
this.interceptors = (interceptors != null ? interceptors : Collections.<ClientHttpRequestInterceptor>emptyList());
}
@Override
protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory) {
return new InterceptingClientHttpRequest(requestFactory, this.interceptors, uri, httpMethod);
}
}
就是new了個(gè)InterceptingClientHttpRequest實(shí)例,并且把攔截器、基本請(qǐng)求實(shí)例創(chuàng)建工廠注進(jìn)去。
4.請(qǐng)求實(shí)例調(diào)用配置階段注入的負(fù)載均衡攔截器的攔截方法intercept
可從第1步看出,創(chuàng)建完請(qǐng)求實(shí)例后,通過執(zhí)行請(qǐng)求實(shí)例的execute方法執(zhí)行請(qǐng)求。
ClientHttpRequest request = createRequest(url, method);
if (requestCallback != null) {
requestCallback.doWithRequest(request);
}
response = request.execute();
實(shí)際請(qǐng)求實(shí)例是InterceptingClientHttpRequest,execute實(shí)際是在它的父類中。
類定義位置:
spring-web-4.3.12.RELEASE.jar
org.springframework.http.client
InterceptingClientHttpRequest.class
看一下它們的繼承關(guān)系。

在execute方法中實(shí)際調(diào)用了子類實(shí)現(xiàn)的executeInternal方法。
public abstract class AbstractClientHttpRequest implements ClientHttpRequest {
private final HttpHeaders headers = new HttpHeaders();
private boolean executed = false;
@Override
public final HttpHeaders getHeaders() {
return (this.executed ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers);
}
@Override
public final OutputStream getBody() throws IOException {
assertNotExecuted();
return getBodyInternal(this.headers);
}
@Override
public final ClientHttpResponse execute() throws IOException {
assertNotExecuted();
ClientHttpResponse result = executeInternal(this.headers);
this.executed = true;
return result;
}
protected void assertNotExecuted() {
Assert.state(!this.executed, "ClientHttpRequest already executed");
}
protected abstract OutputStream getBodyInternal(HttpHeaders headers) throws IOException;
protected abstract ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException;
}
其實(shí)就是InterceptingClientHttpRequest類的executeInternal方法,其中,又調(diào)用了一個(gè)執(zhí)行器InterceptingRequestExecution的execute,通關(guān)判斷如果有攔截器注入進(jìn)來(lái)過,就調(diào)用攔截器的intercept方法。
這里的攔截器實(shí)際上就是在配置階段注入進(jìn)RestTemplate實(shí)例的負(fù)載均衡攔截器LoadBalancerInterceptor實(shí)例,可參考上面配置階段的第2步。
class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest {
// 略
@Override
protected final ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
InterceptingRequestExecution requestExecution = new InterceptingRequestExecution();
return requestExecution.execute(this, bufferedOutput);
}
private class InterceptingRequestExecution implements ClientHttpRequestExecution {
private final Iterator<ClientHttpRequestInterceptor> iterator;
public InterceptingRequestExecution() {
this.iterator = interceptors.iterator();
}
@Override
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
if (this.iterator.hasNext()) {
ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
return nextInterceptor.intercept(request, body, this);
}
else {
ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), request.getMethod());
for (Map.Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
List<String> values = entry.getValue();
for (String value : values) {
delegate.getHeaders().add(entry.getKey(), value);
}
}
if (body.length > 0) {
StreamUtils.copy(body, delegate.getBody());
}
return delegate.execute();
}
}
}
}
5.負(fù)載均衡攔截器調(diào)用負(fù)載均衡客戶端
在負(fù)載均衡攔截器LoadBalancerInterceptor類的intercept方法中,又調(diào)用了負(fù)載均衡客戶端LoadBalancerClient實(shí)現(xiàn)類的execute方法。
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
private LoadBalancerClient loadBalancer;
private LoadBalancerRequestFactory requestFactory;
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
this.loadBalancer = loadBalancer;
this.requestFactory = requestFactory;
}
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
// for backwards compatibility
this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
}
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
}
在配置階段的第1步,可以看到實(shí)現(xiàn)類是RibbonLoadBalancerClient。
6.負(fù)載均衡客戶端調(diào)用負(fù)載均衡策略選取目標(biāo)服務(wù)實(shí)例并發(fā)起請(qǐng)求
在RibbonLoadBalancerClient的第一個(gè)execute方法以及getServer方法中可以看到,實(shí)際上是通過ILoadBalancer的負(fù)載均衡器實(shí)現(xiàn)類作的chooseServer方法選取一個(gè)服務(wù),交給接下來(lái)的請(qǐng)求對(duì)象發(fā)起一個(gè)請(qǐng)求。
這里的負(fù)載均衡實(shí)現(xiàn)類默認(rèn)是ZoneAwareLoadBalancer區(qū)域感知負(fù)載均衡器實(shí)例,其內(nèi)部通過均衡策略選擇一個(gè)服務(wù)。
ZoneAwareLoadBalancer的創(chuàng)建可以參考配置階段的第4步。
public class RibbonLoadBalancerClient implements LoadBalancerClient {
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
serviceId), serverIntrospector(serviceId).getMetadata(server));
return execute(serviceId, ribbonServer, request);
}
@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
Server server = null;
if(serviceInstance instanceof RibbonServer) {
server = ((RibbonServer)serviceInstance).getServer();
}
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonLoadBalancerContext context = this.clientFactory
.getLoadBalancerContext(serviceId);
RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);
try {
T returnVal = request.apply(serviceInstance);
statsRecorder.recordStats(returnVal);
return returnVal;
}
// catch IOException and rethrow so RestTemplate behaves correctly
catch (IOException ex) {
statsRecorder.recordStats(ex);
throw ex;
}
catch (Exception ex) {
statsRecorder.recordStats(ex);
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
// 略
protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
return loadBalancer.chooseServer("default"); // TODO: better handling of key
}
protected ILoadBalancer getLoadBalancer(String serviceId) {
return this.clientFactory.getLoadBalancer(serviceId);
}
public static class RibbonServer implements ServiceInstance {
private final String serviceId;
private final Server server;
private final boolean secure;
private Map<String, String> metadata;
public RibbonServer(String serviceId, Server server) {
this(serviceId, server, false, Collections.<String, String> emptyMap());
}
public RibbonServer(String serviceId, Server server, boolean secure,
Map<String, String> metadata) {
this.serviceId = serviceId;
this.server = server;
this.secure = secure;
this.metadata = metadata;
}
// 略
}
}
代碼擼完,總結(jié)下。
普通使用RestTemplate請(qǐng)求其他服務(wù)時(shí),內(nèi)部使用的就是常規(guī)的http請(qǐng)求實(shí)例發(fā)送請(qǐng)求。
為RestTemplate增加了@LoanBalanced 注解后,實(shí)際上通過配置,為RestTemplate注入負(fù)載均衡攔截器,讓負(fù)載均衡器選擇根據(jù)其對(duì)應(yīng)的策略選擇合適的服務(wù)后,再發(fā)送請(qǐng)求。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Java中構(gòu)造器內(nèi)部的多態(tài)方法的行為實(shí)例分析
這篇文章主要介紹了Java中構(gòu)造器內(nèi)部的多態(tài)方法的行為,結(jié)合實(shí)例形式分析了java構(gòu)造器內(nèi)部多態(tài)方法相關(guān)原理、功能及操作技巧,需要的朋友可以參考下2019-10-10
java中為何重寫equals時(shí)必須重寫hashCode方法詳解
這篇文章主要給大家介紹了關(guān)于java中為什么重寫equals時(shí)必須重寫hashCode方法的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11
java實(shí)現(xiàn)百度坐標(biāo)的摩卡托坐標(biāo)與火星坐標(biāo)轉(zhuǎn)換的示例
這篇文章主要介紹了java實(shí)現(xiàn)百度坐標(biāo)的摩卡托坐標(biāo)與火星坐標(biāo)轉(zhuǎn)換的示例,需要的朋友可以參考下2014-03-03
Spring中ApplicationEventPublisher發(fā)布訂閱模式的實(shí)現(xiàn)
本文主要介紹了Spring中ApplicationEventPublisher發(fā)布訂閱模式的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07

