05.Dubbo 源码解析之服务暴露

1. 环境搭建

  • 代码已经上传至 https://github.com/masteryourself/dubbo ,分支名称是 masteryourself-2.7.3-release

  • provider 是 dubbo-demo-xml-provider 工程,启动类是 Application

  • consumer 是 dubbo-demo-xml-consumer 工程,启动类是 Application

2. 源码解析

2.1 关于 ServiceBean

image

  • 由于 ServiceBean 实现了 ApplicationListener<ContextRefreshedEvent> 接口,所以会在 onApplicationEvent 方法中监听 ContextRefreshedEvent 事件,当容器刷新完毕后,会调用 export() 方法完成服务暴露

2.2 流程预览

image

// 1. 监听 spring 的 【ContextRefreshedEvent】 事件
org.apache.dubbo.config.spring.ServiceBean#onApplicationEvent ->
	// 调用 export() 方法完成服务暴露
	org.apache.dubbo.config.spring.ServiceBean#export ->
		// 调用父类 【ServiceConfig】 完成服务暴露
		org.apache.dubbo.config.ServiceConfig#export ->
			
			// 1.1 检查和更新属性
			org.apache.dubbo.config.ServiceConfig#checkAndUpdateSubConfigs  ->
				// 设置全局的默认属性,因为 serviceBean 和 application、registries 等配置类都有关联关系
				org.apache.dubbo.config.ServiceConfig#completeCompoundConfigs
				// 启动全局配置中心
				org.apache.dubbo.config.AbstractInterfaceConfig#startConfigCenter ->
					// 刷新 ApplicationConfig、MonitorConfig、ModuleConfig、ProtocolConfig、RegistryConfig、ProviderConfig、ConsumerConfig 配置
					org.apache.dubbo.config.context.ConfigManager#refreshAll

						// 1.1.1(*) 获取混合配置,给 set 开头的方法赋值,即完成属性赋值
						// 配置优先级默认是:系统环境变量 -> 配置中心某个应用的配置 -> 配置中心的全局配置 -> AbstractConfig 类的属性值 -> 
							// dubbo.properties.file 或 dubbo.properties 文件配置
						org.apache.dubbo.config.AbstractConfig#refresh

							// 1.1.1.1(*) 初始化 compositeConfiguration 混合配置 list
							org.apache.dubbo.common.config.Environment#getConfiguration

			// 1.2 导出服务
			org.apache.dubbo.config.ServiceConfig#doExport
				
				// 1.2.1(*) 循环所有的 protocols,依次进行服务暴露,这里的 protocols 有 2 个,因此一共会暴露 4 个地址
				// 0 = {ProtocolConfig@2823} "<dubbo:protocol name="dubbo" port="20880" valid="true" id="dubbo" prefix="dubbo.protocols." />"
	        	// 1 = {ProtocolConfig@2840} "<dubbo:protocol name="dubbo" port="20881" valid="true" id="dubbo2" prefix="dubbo.protocols." />"
				org.apache.dubbo.config.ServiceConfig#doExportUrls

					// 1.2.1.1 获取要导出的服务 url,格式如下:
				    // 0 = {URL@2835} "registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=demo-provider&
						// dubbo=2.0.2&pid=7208&qos-port=22222&registry=zookeeper&timestamp=1577007384243"
	        		// 1 = {URL@2836} "registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=demo-provider&
						// dubbo=2.0.2&pid=7208&qos-port=22222&registry=zookeeper&timestamp=1577007384245&version=1.1.0"
					org.apache.dubbo.config.AbstractInterfaceConfig#loadRegistries

					// 1.2.1.2(*) 调用此方法进行服务暴露
					org.apache.dubbo.config.ServiceConfig#doExportUrlsFor1Protocol

						// 1.2.1.2.1(*) 改写 url,把协议改成 injvm 协议,host 设置为 127.0.0.1,端口号设置为 0
						// 如果配置了 remote,则表示禁止使用 injvm 协议,就不会进行本地暴露
						org.apache.dubbo.config.ServiceConfig#exportLocal ->
							// 调用 Protocol 的动态代理类 【Protocol$Adaptive】 的 export 方法,先经过 wrapper 包装类,再到 【InjvmProtocol】,因为协议被改成了 injvm
							org.apache.dubbo.rpc.Protocol$Adaptive#export ->
								// ProtocolListener 包装类,当 protocol 不为 registry 时才会起作用,这里是 injvm,所以会起作用
								// 服务导出之后,可以用监听器做一些操作
								org.apache.dubbo.rpc.protocol.ProtocolListenerWrapper#export ->
									// ProtocolFilter 包装类,当 protocol 不为 registry 时才会起作用,这里是 injvm,所以会起作用
									org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper#export

										// 1.2.1.2.1.1(*) 构建 filterChain,实现类最终会被包装成 
										// EchoFilter -> ClassLoaderFilter -> GenericFilter -> ContextFilter -> TraceFilter -> TimeoutFilter -> 
											// MonitorFilter -> ExceptionFilter -> DemoServiceImpl
										org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper#buildInvokerChain

									// InjvmProtocol,真正处理 injvm 协议的 protocol 类
									org.apache.dubbo.rpc.protocol.injvm.InjvmProtocol#export ->
										// 在构造方法里把 invoker 添加到了 【exporterMap】 属性中,在 injvm 协议调用时会从此 map 取值
										// "org.apache.dubbo.demo.DemoService" -> "org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$CallbackRegistrationInvoker@5827af16"
										org.apache.dubbo.rpc.protocol.injvm.InjvmExporter#<init>

						// 1.2.1.2.2 循环所有的 registryURLs,作远程服务导出,形如:
						// registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=demo-provider&dubbo=2.0.2&
							// export=dubbo%3A%2F%2F192.168.89.1%3A20880%2Forg.apache.dubbo.demo.DemoService%3Fanyhost%3Dtrue%26application%3D
							// demo-provider%26bean.name%3Dorg.apache.dubbo.demo.DemoService%26bind.ip%3D192.168.89.1%26bind.port%3D20880%26
							// deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Dorg.apache.dubbo.demo.DemoService%26
							// methods%3DsayHello%26pid%3D18816%26qos-port%3D22222%26register%3Dtrue%26release%3D%26side%3Dprovider%26
							// timestamp%3D1577009068166&pid=18816&qos-port=22222&registry=zookeeper&timestamp=1577009067710
						// 调用 Protocol 的动态代理类 【Protocol$Adaptive】 的 export 方法,先经过 wrapper 包装类,再到 【RegistryProtocol】,因为协议是 registry
						org.apache.dubbo.rpc.Protocol$Adaptive#export ->
							// ProtocolListener 包装类,当 protocol 不为 registry 时才会起作用,这里是 registry,所以不会起作用
							// 服务导出之后,可以用监听器做一些操作
							org.apache.dubbo.rpc.protocol.ProtocolListenerWrapper#export ->
								// ProtocolFilter 包装类,当 protocol 不为 registry 时才会起作用,这里是 registry,所以不会起作用
								org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper#export

									// 1.2.1.2.2.1(*) 添加监听器,暴露远程服务,连接 zk 创建节点
									org.apache.dubbo.registry.integration.RegistryProtocol#export

										// 1.2.1.2.2.1.1 暴露远程服务,形如:
										// dubbo://192.168.89.1:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=demo-provider&
											// bean.name=org.apache.dubbo.demo.DemoService&bind.ip=192.168.89.1&bind.port=20880&deprecated=false&
											// dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&
											// pid=8724&qos-port=22222&register=true&release=&side=provider&timestamp=1577011539455
										org.apache.dubbo.registry.integration.RegistryProtocol#doLocalExport ->
											// 调用 Protocol 的动态代理类 【Protocol$Adaptive】 的 export 方法,先经过 wrapper 包装类,再到 【DubboProtocol】
											org.apache.dubbo.rpc.Protocol$Adaptive#export ->
												// ProtocolListener 包装类,当 protocol 不为 registry 时才会起作用,这里是 dubbo,所以会起作用
												// 服务导出之后,可以用监听器做一些操作
												org.apache.dubbo.rpc.protocol.ProtocolListenerWrapper#export ->
													// ProtocolFilter 包装类,当 protocol 不为 registry 时才会起作用,这里是 dubbo,所以会起作用同 【1.2.1.2.1.1】
													org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper#export ->
														// 获取 url,暴露 netty 远程服务
														org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol#export ->
															// 获取 netty server,绑定服务名称和端口号
															org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol#openServer

										// 1.2.1.2.2.1.2 把服务注册到 zk 上
										org.apache.dubbo.registry.integration.RegistryProtocol#register ->
											// 调用 zookeeper 的 doRegister 方法,把服务路径写到 zk 节点上
											org.apache.dubbo.registry.support.FailbackRegistry#doRegister

2.3 流程详解

2.3.1 AbstractConfig#refresh(1.1.1)
  • org.apache.dubbo.config.AbstractConfig
public void refresh() {
    try {

        // 获取混合配置
        // 1. 系统环境变量
        // 2. 配置中心某个应用的配置
        // 3. 配置中心的全局配置
        // 4. dubbo.properties.file 或 dubbo.properties 文件配置
        CompositeConfiguration compositeConfiguration = Environment.getInstance().getConfiguration(getPrefix(), getId());

        // 获取 AbstractConfig 类中属性的值,即调用 getXxx 或 isXxx 方法返回的所有 metaData 值
        InmemoryConfiguration config = new InmemoryConfiguration(getPrefix(), getId());
        config.addProperties(getMetaData());

        // 判断是否是配置中心的配置优先
        // 配置优先级默认是:系统环境变量 -> 配置中心某个应用的配置 -> 配置中心的全局配置 -> AbstractConfig 类的属性值 ->
        // dubbo.properties.file 或 dubbo.properties 文件配置
        if (Environment.getInstance().isConfigCenterFirst()) {
            // The sequence would be: SystemConfiguration -> AppExternalConfiguration -> ExternalConfiguration -> AbstractConfig -> PropertiesConfiguration
            compositeConfiguration.addConfiguration(3, config);
        } else {
            // The sequence would be: SystemConfiguration -> AbstractConfig -> AppExternalConfiguration -> ExternalConfiguration -> PropertiesConfiguration
            compositeConfiguration.addConfiguration(1, config);
        }

        // loop methods, get override value and set the new value back to method
        Method[] methods = getClass().getMethods();
        for (Method method : methods) {
            // 获取 set 开头的方法
            if (MethodUtils.isSetter(method)) {
                try {

                    // 根据 setXxx 的 xxx 属性,从 compositeConfiguration 混合配置中获取属性对应的值
                    String value = StringUtils.trim(compositeConfiguration.getString(extractPropertyName(getClass(), method)));
                    // isTypeMatch() is called to avoid duplicate and incorrect update, for example, we have two 'setGeneric' methods in ReferenceConfig.
                    if (StringUtils.isNotEmpty(value) && ClassUtils.isTypeMatch(method.getParameterTypes()[0], value)) {

                        // 反射调用 set 方法赋值
                        method.invoke(this, ClassUtils.convertPrimitive(method.getParameterTypes()[0], value));
                    }
                } catch (NoSuchMethodException e) {
                    logger.info("Failed to override the property " + method.getName() + " in " +
                            this.getClass().getSimpleName() +
                            ", please make sure every property has getter/setter method provided.");
                }
            }
        }
    } catch (Exception e) {
        logger.error("Failed to override ", e);
    }
}
2.3.2 Environment#getConfiguration(1.1.1.1)
  • org.apache.dubbo.common.config.Environment
public CompositeConfiguration getConfiguration(String prefix, String id) {
    CompositeConfiguration compositeConfiguration = new CompositeConfiguration();
    // Config center has the highest priority

    // 从系统环境变量中获取值
    compositeConfiguration.addConfiguration(this.getSystemConfig(prefix, id));

    // 从配置中心的 dubbo.config.xxx.dubbo.properties 节点获取值(xxx 应用的配置)
    compositeConfiguration.addConfiguration(this.getAppExternalConfig(prefix, id));

    // 从配置中心的 dubbo.config.dubbo.dubbo.properties 节点获取值(全局配置)
    compositeConfiguration.addConfiguration(this.getExternalConfig(prefix, id));

    // 从 dubbo.properties.file 或者 dubbo.properties 文件中获取值
    compositeConfiguration.addConfiguration(this.getPropertiesConfig(prefix, id));
    return compositeConfiguration;
}
2.3.3 ServiceConfig#doExportUrls(1.2.1)
  • org.apache.dubbo.config.ServiceConfig
private void doExportUrls() {

    // 获取要导出的服务 url,格式如下
    // 0 = {URL@2835} "registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=demo-provider&
    // dubbo=2.0.2&pid=7208&qos-port=22222&registry=zookeeper&timestamp=1577007384243"
    // 1 = {URL@2836} "registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=demo-provider&
    // dubbo=2.0.2&pid=7208&qos-port=22222&registry=zookeeper&timestamp=1577007384245&version=1.1.0"
    List<URL> registryURLs = loadRegistries(true);

    // 循环所有的 protocols,挨个导出服务,protocols 有如下两个,所以一共会暴露 4 个服务
    // 0 = {ProtocolConfig@2823} "<dubbo:protocol name="dubbo" port="20880" valid="true" id="dubbo" prefix="dubbo.protocols." />"
    // 1 = {ProtocolConfig@2840} "<dubbo:protocol name="dubbo" port="20881" valid="true" id="dubbo2" prefix="dubbo.protocols." />"
    for (ProtocolConfig protocolConfig : protocols) {
        String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);
        ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass);
        ApplicationModel.initProviderModel(pathKey, providerModel);

        // 调用此方法进行服务暴露
        doExportUrlsFor1Protocol(protocolConfig, registryURLs);
    }
}
2.3.4 ServiceConfig#doExportUrlsFor1Protocol(1.2.1.2)
  • org.apache.dubbo.config.ServiceConfig
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
    // 获取协议名称,如果是空,默认为 dubbo 协议
        String name = protocolConfig.getName();
    if (StringUtils.isEmpty(name)) {
        name = DUBBO;
    }

    Map<String, String> map = new HashMap<String, String>();
    map.put(SIDE_KEY, PROVIDER_SIDE);

    // 属性覆盖
    appendRuntimeParameters(map);
    appendParameters(map, metrics);
    appendParameters(map, application);
    appendParameters(map, module);
    // remove 'default.' prefix for configs from ProviderConfig
    // appendParameters(map, provider, Constants.DEFAULT_KEY);
    appendParameters(map, provider);
    appendParameters(map, protocolConfig);
    appendParameters(map, this);
    
    ...

    // export service
    String host = this.findConfigedHosts(protocolConfig, registryURLs, map);
    Integer port = this.findConfigedPorts(protocolConfig, name, map);

    // 构造一个要导出的 url
    URL url = new URL(name, host, port, getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), map);

    ...
    
    if (!SCOPE_NONE.equalsIgnoreCase(scope)) {

        // export to local if the config is not remote (export to remote only when config is remote)
        // 如果配置了 remote,则表示禁止使用 injvm 协议,就不会进行本地暴露
        if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) {
            exportLocal(url);
        }
        // export to remote if the config is not local (export to local only when config is local)
        if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) {
            if (!isOnlyInJvm() && logger.isInfoEnabled()) {
                logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
            }
            if (CollectionUtils.isNotEmpty(registryURLs)) {

                // 循环所有的 registryURLs
                for (URL registryURL : registryURLs) {
                
                    ...

                    // 暴露远程服务
                    // registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=demo-provider&dubbo=2.0.2&export=dubbo%3A%2F%2F192.168.89.1%3A20880%2Forg.apache.dubbo.demo.DemoService%3Fanyhost%3Dtrue%26application%3Ddemo-provider%26bean.name%3Dorg.apache.dubbo.demo.DemoService%26bind.ip%3D192.168.89.1%26bind.port%3D20880%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Dorg.apache.dubbo.demo.DemoService%26methods%3DsayHello%26pid%3D21564%26qos-port%3D22222%26register%3Dtrue%26release%3D%26side%3Dprovider%26timestamp%3D1577013554161&pid=21564&qos-port=22222&registry=zookeeper&timestamp=1577013552772
                    Exporter<?> exporter = protocol.export(wrapperInvoker);
                    exporters.add(exporter);
                }
            } else {
                
                ...
                
            }
            
            ...
            
        }
    }
    this.urls.add(url);
}
2.3.5 ServiceConfig#exportLocal(1.2.1.2.1)
  • org.apache.dubbo.config.ServiceConfig
private void exportLocal(URL url) {
    // 改写 url,把协议改成 injvm 协议,host 设置为 127.0.0.1,端口号设置为 0
    URL local = URLBuilder.from(url)
            .setProtocol(LOCAL_PROTOCOL)
            .setHost(LOCALHOST_VALUE)
            .setPort(0)
            .build();

    // 本地服务导出
    Exporter<?> exporter = protocol.export(
            PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, local));
    exporters.add(exporter);
    logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry url : " + local);
}
2.3.6 ProtocolFilterWrapper#buildInvokerChain(1.2.1.2.1.1)
  • org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper
private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
    Invoker<T> last = invoker;
    // 获取所有激活的 filter Extension
    List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);

    if (!filters.isEmpty()) {

        // 循环包装
        for (int i = filters.size() - 1; i >= 0; i--) {
            final Filter filter = filters.get(i);
            final Invoker<T> next = last;
            last = new Invoker<T>() {

                @Override
                public Class<T> getInterface() {
                    return invoker.getInterface();
                }

                @Override
                public URL getUrl() {
                    return invoker.getUrl();
                }

                @Override
                public boolean isAvailable() {
                    return invoker.isAvailable();
                }

                @Override
                public Result invoke(Invocation invocation) throws RpcException {
                    Result asyncResult;
                    try {
                        asyncResult = filter.invoke(next, invocation);
                    } catch (Exception e) {
                        // onError callback
                        if (filter instanceof ListenableFilter) {
                            Filter.Listener listener = ((ListenableFilter) filter).listener();
                            if (listener != null) {
                                listener.onError(e, invoker, invocation);
                            }
                        }
                        throw e;
                    }
                    return asyncResult;
                }

                @Override
                public void destroy() {
                    invoker.destroy();
                }

                @Override
                public String toString() {
                    return invoker.toString();
                }
            };
        }
    }

    return new CallbackRegistrationInvoker<>(last, filters);
}
2.3.7 RegistryProtocol#export(1.2.1.2.2.1)
  • org.apache.dubbo.registry.integration.RegistryProtocol#export
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
    // 获取注册中心地址
    // zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=demo-provider&dubbo=2.0.2&export=dubbo%3A%2F%2F192.168.89.1%3A20880%2Forg.apache.dubbo.demo.DemoService%3Fanyhost%3Dtrue%26application%3Ddemo-provider%26bean.name%3Dorg.apache.dubbo.demo.DemoService%26bind.ip%3D192.168.89.1%26bind.port%3D20880%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Dorg.apache.dubbo.demo.DemoService%26methods%3DsayHello%26pid%3D18816%26qos-port%3D22222%26register%3Dtrue%26release%3D%26side%3Dprovider%26timestamp%3D1577009068166&pid=18816&qos-port=22222&timestamp=1577009067710
    URL registryUrl = getRegistryUrl(originInvoker);

    // url to export locally
    // 获取导出地址
    // dubbo://192.168.89.1:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=demo-provider&bean.name=org.apache.dubbo.demo.DemoService&bind.ip=192.168.89.1&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=18816&qos-port=22222&register=true&release=&side=provider&timestamp=1577009068166
    URL providerUrl = getProviderUrl(originInvoker);

    // Subscribe the override data
    // FIXME When the provider subscribes, it will affect the scene : a certain JVM exposes the service and call
    //  the same service. Because the subscribed is cached key with the name of the service, it causes the
    //  subscription information to cover.

    // 获取能覆盖配置的订阅地址
    // provider://192.168.89.1:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=demo-provider&bean.name=org.apache.dubbo.demo.DemoService&bind.ip=192.168.89.1&bind.port=20880&category=configurators&check=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=1108&qos-port=22222&register=true&release=&side=provider&timestamp=1577010446361
    final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
    final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);

    // 添加监听器
    overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);

    providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
    //export invoker
    // 调用 originInvoker 的 protocol 实现,进行对应协议的服务暴露,这里是【DubboProtocol】
    final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);

    // url to registry
    // 获取 registry,这里是 【ZookeeperRegistry】
    final Registry registry = getRegistry(originInvoker);

    // 简化 url,因为新版本多了配置中心
    final URL registeredProviderUrl = getRegisteredProviderUrl(providerUrl, registryUrl);
    ProviderInvokerWrapper<T> providerInvokerWrapper = ProviderConsumerRegTable.registerProvider(originInvoker,
            registryUrl, registeredProviderUrl);

    //to judge if we need to delay publish
    // 判断是否需要注册
    boolean register = registeredProviderUrl.getParameter("register", true);
    if (register) {

        // 服务注册
        register(registryUrl, registeredProviderUrl);
        providerInvokerWrapper.setReg(true);
    }

    // Deprecated! Subscribe to override rules in 2.6.x or before.
    registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);

    exporter.setRegisterUrl(registeredProviderUrl);
    exporter.setSubscribeUrl(overrideSubscribeUrl);
    //Ensure that a new exporter instance is returned every time export
    return new DestroyableExporter<>(exporter);
}
发布了37 篇原创文章 · 获赞 3 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/masteryourself/article/details/103759037