spring boot 源码解析46-MetricReader

扫码关注公众号:Java 技术驿站

发送:vip
将链接复制到本浏览器,永久解锁本站全部文章

【公众号:Java 技术驿站】 【加作者微信交流技术,拉技术群】

前言

本文来介绍MetricReader的实现,其类图如下:

20191017100445\_1.png

其中如下实现在之前的文章中有介绍,本文不在对其进行说明:

  1. BufferMetricReader–> 在 spring boot 源码解析44-PrefixMetricReader,PrefixMetricWriter,MultiMetricRepository 中有说明
  2. MetricRepository,InMemoryMetricRepository–>在 spring boot 源码解析37-CounterService详解中有说明
  3. RedisMetricRepository–>在spring boot 源码解析41-CounterWriter,GaugeWriter解析中介绍

解析

MetricReader

MetricReader–> 个简单的读取数据的接口–>用来审查Metric.代码如下:

    public interface MetricReader {

        // 获得1个是指定name的Metric的实例(通常会返回最后1个记录的)Metric,如果没有对应的,则返回null
        Metric<?> findOne(String metricName);

        // 获得所有MetricReader持有的Metric
        Iterable<Metric<?>> findAll();

        // 获得MetricReader持有的Metric得到数量
        long count();

    }

CompositeMetricReader

CompositeMetricReader–> 组合模式,所有的方法都是调用其内部持有的MetricReader依次去处理.代码如下:

    public class CompositeMetricReader implements MetricReader {

        private final List<MetricReader> readers = new ArrayList<MetricReader>();

        public CompositeMetricReader(MetricReader... readers) {
            Collections.addAll(this.readers, readers);
        }

        @Override
        public Metric<?> findOne(String metricName) {
            for (MetricReader delegate : this.readers) {
                Metric<?> value = delegate.findOne(metricName);
                if (value != null) {
                    return value;
                }
            }
            return null;
        }

        @Override
        public Iterable<Metric<?>> findAll() {
            List<Metric<?>> values = new ArrayList<Metric<?>>((int) count());
            for (MetricReader delegate : this.readers) {
                Iterable<Metric<?>> all = delegate.findAll();
                for (Metric<?> value : all) {
                    values.add(value);
                }
            }
            return values;
        }

        @Override
        public long count() {
            long count = 0;
            for (MetricReader delegate : this.readers) {
                count += delegate.count();

            }
            return count;
        }

    }

实例化地方如下:

20191017100445\_2.png

关于这两处的调用点会在后续的文章中有介绍

MetricRegistryMetricReader

MetricRegistryMetricReader–> 实现了MetricReader, MetricRegistryListener,从MetricRegistry中读取metrics的MetricReader,Gauges 和 Counters 被反射成1个单一值,Timers,Meters and Histograms 被扩展到1个metrics的(包含所有Number的属性的)集合中.

  1. 字段,构造器如下:

        private static final Log logger = LogFactory.getLog(MetricRegistryMetricReader.class);
    
        // key--> Histograms,Meter,Timer,Snapshot 对应的class, value --> 该class中属性是number类型的属性名
        private static final Map, Set> numberKeys = new ConcurrentHashMap, Set>();
    
        private final Object monitor = new Object();
    
        // key-->metricName,value-->MetricRegistry 中对应的Metric 名
        private final Map names = new ConcurrentHashMap();
    
        // key--> MetricRegistry 中对应的Metric 名 value --> metricName
        private final MultiValueMap reverse = new LinkedMultiValueMap();
    
        private final MetricRegistry registry;
    
        public MetricRegistryMetricReader(MetricRegistry registry) {
            this.registry = registry;
            registry.addListener(this);
        }
  2. 方法实现如下:

    1. onGaugeAdded–>当Gauge添加到MetricRegistry时触发.代码如下:

          public void onGaugeAdded(String name, Gauge gauge) {
              this.names.put(name, name);
              synchronized (this.monitor) {
                  this.reverse.add(name, name);
              }
          }
    2. onCounterAdded–> 当Counter添加到MetricRegistry时触发.代码如下:

          public void onCounterAdded(String name, Counter counter) {
              this.names.put(name, name);
              synchronized (this.monitor) {
                  this.reverse.add(name, name);
              }
          }
    3. onHistogramAdded–> 当Histogram添加到MetricRegistry时触发.代码如下:

          public void onHistogramAdded(String name, Histogram histogram) {
              for (String key : getNumberKeys(histogram)) {
                  String metricName = name + "." + key;
                  this.names.put(metricName, name);
                  synchronized (this.monitor) {
                      this.reverse.add(name, metricName);
                  }
              }
              for (String key : getNumberKeys(histogram.getSnapshot())) {
                  String metricName = name + ".snapshot." + key;
                  this.names.put(metricName, name);
                  synchronized (this.monitor) {
                      this.reverse.add(name, metricName);
                  }
              }
          }
      1. 获得Histogram中属性是Number类型的属性名,此时获得的是count 依次处理之

        1. 将Number类型的属性名依次加上指定的name前缀生成metricName
        2. 添加到names,reverse中
      2. 获得Snapshot中属性是Number类型的属性名,依次处理之,将Number类型的属性名依次加上指定的name前缀生成metricName,添加到names,reverse中.此时获得的是75thPercentile, 98thPercentile, min, 95thPercentile, 99thPercentile, median, max, mean, 999thPercentile, stdDev.

      getNumberKeys方法如下:

          private static Set getNumberKeys(Object metric) {
              // 1. 尝试从缓存中获取,如果获取不到的话,则进行初始化
              Set result = numberKeys.get(metric.getClass());
              if (result == null) {
                  result = new HashSet();
              }
              if (result.isEmpty()) {
                  // 2. 如果result 等于null,意味着是第1次创建,则获得metric所有的PropertyDescriptor,遍历之
                  for (PropertyDescriptor descriptor : BeanUtils
                          .getPropertyDescriptors(metric.getClass())) {
                      // 2.1 如果属性的类型是Number类型的话,则加入到result中
                      if (ClassUtils.isAssignable(Number.class, descriptor.getPropertyType())) {
                          result.add(descriptor.getName());
                      }
                  }
                  // 3. 加入到numberKeys中
                  numberKeys.put(metric.getClass(), result);
              }
              return result;
          }
    4. onMeterAdded–>当Meter添加到MetricRegistry时触发.代码如下:

          public void onMeterAdded(String name, Meter meter) {
              for (String key : getNumberKeys(meter)) {
                  String metricName = name + "." + key;
                  this.names.put(metricName, name);
                  synchronized (this.monitor) {
                      this.reverse.add(name, metricName);
                  }
              }
          }

      获得Meter中属性是Number类型的属性名,依次处理之,将Number类型的属性名依次加上指定的name前缀生成metricName,添加到names,reverse中.此时获得的是count,fifteenMinuteRate,fiveMinuteRate,meanRate,oneMinuteRate

    5. onTimerAdded–> 当Timer添加到MetricRegistry时触发.代码如下:

          public void onTimerAdded(String name, Timer timer) {
                  for (String key : getNumberKeys(timer)) {
                  String metricName = name + "." + key;
                  this.names.put(metricName, name);
                  synchronized (this.monitor) {
                      this.reverse.add(name, metricName);
                  }
              }
              for (String key : getNumberKeys(timer.getSnapshot())) {
                  String metricName = name + ".snapshot." + key;
                  this.names.put(metricName, name);
                  synchronized (this.monitor) {
                      this.reverse.add(name, metricName);
                  }
              }
          }
      1. 获得Timer中属性是Number类型的属性名,依次处理之,将Number类型的属性名依次加上指定的name前缀生成metricName,添加到names,reverse中.此时获得的是count,fifteenMinuteRate,fiveMinuteRate,meanRate,oneMinuteRate
      2. 获得Snapshot中属性是Number类型的属性名,依次处理之,将Number类型的属性名依次加上指定的name前缀生成metricName,添加到names,reverse中.此时获得的是75thPercentile, 98thPercentile, min, 95thPercentile, 99thPercentile, median, max, mean, 999thPercentile, stdDev.
    6. onGaugeRemoved,onCounterRemoved,onHistogramRemoved,onMeterRemoved,onTimerRemoved最终都会调用remove方法来处理.代码如下:

          private void remove(String name) {
              List keys;
              synchronized (this.monitor) {
                  keys = this.reverse.remove(name);
              }
              if (keys != null) {
                  for (String key : keys) {
                      this.names.remove(name + "." + key);
                  }
              }
          }

      从reverse,names中删除

    7. findOne,代码如下:

          public Metric findOne(String metricName) {
              // 1. 从names中获得指定metricName在MetricRegistry中注册的名字,如果不存在则返回null
              String name = this.names.get(metricName);
              if (name == null) {
                  return null;
              }
              // 2. 从MetricRegistry中获得对应的Metric,如果获取不到,则返回null
              com.codahale.metrics.Metric metric = this.registry.getMetrics().get(name);
              if (metric == null) {
                  return null;
              }
              // 3. 如果是Counter
              if (metric instanceof Counter) {
                  Counter counter = (Counter) metric;
                  return new Metric(metricName, counter.getCount());
              }
              // 4. 如果是Gauge
              if (metric instanceof Gauge) {
                  Object value = ((Gauge) metric).getValue();
                  // 4.1 如果统计值是number类型的,则实例化Metric 返回,否则返回null
                  if (value instanceof Number) {
                      return new Metric(metricName, (Number) value);
                  }
                  if (logger.isDebugEnabled()) {
                      logger.debug("Ignoring gauge '" + name + "' (" + metric
                              + ") as its value is not a Number");
                  }
                  return null;
              }
              // 5. 如果是抽样
              if (metric instanceof Sampling) {
                  // 5.1 如果metricName 含有.snapshot.
                  if (metricName.contains(".snapshot.")) {
      
                      // 5.2 获得对应的测量值
                      Number value = getMetric(((Sampling) metric).getSnapshot(), metricName);
                      // 5.3 如果该metricName对应的metric为Timer,则将其值转换为毫秒
                      if (metric instanceof Timer) {
                          // convert back to MILLISEC
                          value = TimeUnit.MILLISECONDS.convert(value.longValue(),
                                  TimeUnit.NANOSECONDS);
                      }
                      // 5.4 返回
                      return new Metric(metricName, value);
                  }
              }
              // 6. 获得对应的测量值 封装为Metric 后返回
              return new Metric(metricName, getMetric(metric, metricName));
          }
      1. 从names中获得指定metricName在MetricRegistry中注册的名字,如果不存在则返回null
      2. 从MetricRegistry中获得对应的Metric,如果获取不到,则返回null
      3. 如果是Counter,则直接返回对应的Metric
      4. 如果是Gauge

        1. 如果统计值是number类型的,则实例化Metric 返回,否则返回null
      5. 如果是Sampling的实例.

        1. 如果metricName 含有.snapshot.

          1. 获得对应的测量值
          2. 如果该metricName对应的metric为Timer,则将其值转换为毫秒
          3. 返回Metric
      6. 获得对应的测量值 封装为Metric 后返回.代码如下:

            private static Number getMetric(Object metric, String metricName) {
                // 1. 获取metricName 最后1个点的后的字符串
                String key = StringUtils.getFilenameExtension(metricName);
                // 2. 将metric 包装为BeanWrapperImpl,直接读取该key所对应的值
                return (Number) new BeanWrapperImpl(metric).getPropertyValue(key);
            }
      7. findAll,代码如下:

            public Iterable> findAll() {
                return new Iterable>() {
                    @Override
                    public Iterator> iterator() {
                        Set> metrics = new HashSet>();
                        // 1. 遍历names
                        for (String name : MetricRegistryMetricReader.this.names.keySet()) {
                            // 2. 根据name 获得对应的Metric,如果不等于null,则添加到metrics中
                            Metric metric = findOne(name);
                            if (metric != null) {
                                metrics.add(metric);
                            }
                        }
                        // 3. 返回
                        return metrics.iterator();
                    }
                };
            }
      8. count.代码如下:

            public long count() {
                return this.names.size();
            }
  3. 自动装配:

    在MetricsDropwizardAutoConfiguration中声明,如下:

        @Configuration
        @ConditionalOnClass(MetricRegistry.class)
        @AutoConfigureBefore(MetricRepositoryAutoConfiguration.class)
        public class MetricsDropwizardAutoConfiguration {
            ....
    
            @Bean
            public MetricReaderPublicMetrics dropwizardPublicMetrics(
                    MetricRegistry metricRegistry) {
                MetricRegistryMetricReader reader = new MetricRegistryMetricReader(
                        metricRegistry);
                return new MetricReaderPublicMetrics(reader);
            }
        }

    当满足如下条件时生效:

    1. ConditionalOnClass(MetricRegistry.class)–> 在类路径下存在MetricRegistry.class

MetricsEndpointMetricReader

后续文章分析

AggregateMetricReader

注意,该类没有自动装配

  1. 该类的字段,构造器如下:

        private MetricReader source;
    
        private String keyPattern = "d.d";
    
        // 应用到所有的输出metrics中.
        private String prefix = "aggregate.";
    
        public AggregateMetricReader(MetricReader source) {
            this.source = source;
        }

    keyPattern 说明如下:

    指定该类对于repository中的key,如何操作.这些key在repository中被假定为是.分隔的,且给定的格式也必须是同样的格式,比如:”d.d.k.d”.keyPattern 需要与repository中的keys进行匹配,并将应用如下规则:

    1. d–>丢弃这些key段(对全局前缀比如系统标识或者聚合keys a.k.a的物理标识 )
    2. k–> 保持原样不需要进行改变(对逻辑标识有用比如app的名字)

    默认是d.d(假设有全局前缀其长度为2)

  2. 方法实现如下:

    1. findOne,代码如下:

          public Metric findOne(String metricName) {
              // 1. 如果不是指定前缀开头的,返回null
              if (!metricName.startsWith(this.prefix)) {
                  return null;
              }
              // 2 . 实例化InMemoryMetricRepository
              InMemoryMetricRepository result = new InMemoryMetricRepository();
              // 3. 去除前缀
              String baseName = metricName.substring(this.prefix.length());
              // 4. 获取source中的所有Metric,遍历
              for (Metric metric : this.source.findAll()) {
                  // 4.1 获得SourceKey
                  String name = getSourceKey(metric.getName());
                  if (baseName.equals(name)) {
                      update(result, name, metric);
                  }
              }
              // 5. 查找
              return result.findOne(metricName);
          }
      1. 如果不是指定前缀开头的,返回nul
      2. 实例化InMemoryMetricRepository
      3. 去除前缀
      4. 获取source中的所有Metric,遍历

        1. 获得SourceKey.代码如下:

              private String getSourceKey(String name) {
                  // 1. 将给定的name,本类中的keyPattern 通过.进行分割
                  String[] keys = StringUtils.delimitedListToStringArray(name, ".");
                  String[] patterns = StringUtils.delimitedListToStringArray(this.keyPattern, ".");
                  StringBuilder builder = new StringBuilder();
                  // 2. 依次遍历patterns,如果其值是k,则加入到builder中
                  for (int i = 0; i < patterns.length; i++) {
                      if ("k".equals(patterns[i])) {
                          builder.append(builder.length() > 0 ? "." : "");
                          builder.append(keys[i]);
                      }
                  }
                  // 3.  此时发生在 keys.length > patterns.length 的情况,那么就需要依次的将keys中的数据加入到builder中
                  for (int i = patterns.length; i < keys.length; i++) {
                      builder.append(builder.length() > 0 ? "." : "");
                      builder.append(keys[i]);
                  }
                  // 4. 返回
                  return builder.toString();
              }
          1. 将给定的name,本类中的keyPattern 通过.进行分割
          2. 依次遍历patterns,如果其值是k,则加入到builder中
          3. 如果发生 keys.length > patterns.length 的情况,那么就需要依次的将keys中的数据加入到builder中
          4. 返回
        2. 如果SourceKey和baseName相同的话,则添加到result中.代码如下:

              private void update(InMemoryMetricRepository result, String key, Metric metric) {
                  // 1. 将key加上前缀
                  String name = this.prefix + key;
                  // 2. 从InMemoryMetricRepository 进行查找,如果找不到的话,则实例化1个,此时一般情况下都会实例化的
                  Metric aggregate = result.findOne(name);
                  if (aggregate == null) {
                      aggregate = new Metric(name, metric.getValue(),
                              metric.getTimestamp());
                  }
                  else if (key.contains("counter.")) {
                      // accumulate all values
                      aggregate = new Metric(name,
                              metric.increment(aggregate.getValue().intValue()).getValue(),
                              metric.getTimestamp());
                  }
                  else if (aggregate.getTimestamp().before(metric.getTimestamp())) {
                      // sort by timestamp and only take the latest
                      aggregate = new Metric(name, metric.getValue(),
                              metric.getTimestamp());
                  }
                  // 3. 添加
                  result.set(aggregate);
              }
          1. 将key加上前缀
          2. 从InMemoryMetricRepository 进行查找,如果找不到的话,则实例化1个,此时一般情况下都会实例化的
          3. 添加
      5. 查找
    2. findAll,count的实现和findOne类似,如下:

          @Override
          public Iterable> findAll() {
              InMemoryMetricRepository result = new InMemoryMetricRepository();
              for (Metric metric : this.source.findAll()) {
                  String key = getSourceKey(metric.getName());
                  if (key != null) {
                      update(result, key, metric);
                  }
              }
              return result.findAll();
          }
          @Override
          public long count() {
              Set names = new HashSet();
              for (Metric metric : this.source.findAll()) {
                  String name = getSourceKey(metric.getName());
                  if (name != null) {
                      names.add(name);
                  }
              }
              return names.size();
          }

SpringIntegrationMetricReader

SpringIntegrationMetricReader–>基于spring-integration来实现的.

  1. 字段,构造器如下:

        private final IntegrationManagementConfigurer configurer;
    
        public SpringIntegrationMetricReader(IntegrationManagementConfigurer configurer) {
            this.configurer = configurer;
        }
  2. 方法实现如下:

    1. findOne–> 默认返回null.代码如下:

          public Metric findOne(String metricName) {
              return null;
          }
    2. findAll,代码如下:

          public Iterable> findAll() {
              List> result = new ArrayList>();
              // 1. 获得配置的Channel,Handler,Source的名字
              String[] channelNames = this.configurer.getChannelNames();
              String[] handlerNames = this.configurer.getHandlerNames();
              String[] sourceNames = this.configurer.getSourceNames();
              // 2.添加Channel的统计
              addChannelMetrics(result, channelNames);
              // 3. 添加Handler的统计
              addHandlerMetrics(result, handlerNames);
              // 3. 添加source的统计
              addSourceMetrics(result, sourceNames);
              // 4. 添加Channel,Handler,Source的数量统计
              result.add(new Metric("integration.handlerCount", handlerNames.length));
              result.add(new Metric("integration.channelCount", channelNames.length));
              result.add(new Metric("integration.sourceCount", sourceNames.length));
              return result;
          }
      1. 获得配置的Channel,Handler,Source的名字
      2. 添加Channel的统计.代码如下:

            private void addChannelMetrics(List> result, String[] names) {
                // 1. 遍历Channel的名字,依次调用addChannelMetrics添加
                for (String name : names) {
                    addChannelMetrics(result, name, this.configurer.getChannelMetrics(name));
                }
            }

        遍历Channel的名字,依次调用addChannelMetrics添加.代码如下:

            private void addChannelMetrics(List> result, String name,
                    MessageChannelMetrics metrics) {
                // 1. 在Channel 名字前加上前缀
                String prefix = "integration.channel." + name;
                // 2. 添加该Channel所对应的错误率(每秒)
                result.addAll(getStatistics(prefix + ".errorRate", metrics.getErrorRate()));
                // 3. 添加成功发送数量的统计
                result.add(new Metric(prefix + ".sendCount", metrics.getSendCountLong()));
                // 4. 添加发送消息的速率统计(每秒)
                result.addAll(getStatistics(prefix + ".sendRate", metrics.getSendRate()));
                if (metrics instanceof PollableChannelManagement) {
                    // 5. 如果传入的PollableChannelManagement的实例,则添加拉取数据的统计
                    result.add(new Metric(prefix + ".receiveCount",
                            ((PollableChannelManagement) metrics).getReceiveCountLong()));
                }
            }
        1. 在Channel 名字前加上前缀
        2. 添加该Channel所对应的错误率(每秒)
        3. 添加成功发送数量的统计
        4. 添加发送消息的速率统计(每秒)
        5. 如果传入的PollableChannelManagement的实例,则添加拉取数据的统计

        getStatistics代码如下:

            private Collection> getStatistics(String name, Statistics stats) {
                List> metrics = new ArrayList>();
                metrics.add(new Metric(name + ".mean", stats.getMean()));
                metrics.add(new Metric(name + ".max", stats.getMax()));
                metrics.add(new Metric(name + ".min", stats.getMin()));
                metrics.add(new Metric(name + ".stdev", stats.getStandardDeviation()));// 标准差
                metrics.add(new Metric(name + ".count", stats.getCountLong()));
                return metrics;
            }
      3. 添加Handler的统计.代码如下:

            private void addHandlerMetrics(List> result, String[] names) {
                // 2. 遍历andler的名字,依次调用addHandlerMetrics添加
                for (String name : names) {
                    addHandlerMetrics(result, name, this.configurer.getHandlerMetrics(name));
                }
            }

        遍历andler的名字,依次调用addHandlerMetrics添加.代码如下:

            private void addHandlerMetrics(List> result, String name,
                    MessageHandlerMetrics metrics) {
                String prefix = "integration.handler." + name;
                result.addAll(getStatistics(prefix + ".duration", metrics.getDuration()));// 持续时间
                long activeCount = metrics.getActiveCountLong(); // handler 的激活执行数量
                result.add(new Metric(prefix + ".activeCount", activeCount));
            }
      4. 添加source的统计.代码如下:

            private void addSourceMetrics(List> result, String[] names) {
                for (String name : names) {
                    addSourceMetrics(result, name, this.configurer.getSourceMetrics(name));
                }
            }

        遍历Sourcer的名字,依次调用addSourceMetrics添加.代码如下:

            private void addSourceMetrics(List> result, String name,
                    MessageSourceMetrics sourceMetrics) {
                String prefix = "integration.source." + name;
                result.add(new Metric(prefix + ".messageCount",
                        sourceMetrics.getMessageCountLong())); // 消息数量
            }
      5. 添加Channel,Handler,Source的数量统计
    3. count–>统计Channel,Handler,Source的次数.代码如下:

          public long count() {
              int totalChannelCount = this.configurer.getChannelNames().length;
              int totalHandlerCount = this.configurer.getHandlerNames().length;
              int totalSourceCount = this.configurer.getSourceNames().length;
              return totalChannelCount + totalHandlerCount + totalSourceCount;
          }
  3. 自动装配:

    在IntegrationMetricsConfiguration中声明,如下:

        @Configuration
        @ConditionalOnClass(EnableIntegrationManagement.class)
        @ConditionalOnJava(JavaVersion.SEVEN)
        @UsesJava7
        static class IntegrationMetricsConfiguration {
    
            @Bean(name = IntegrationManagementConfigurer.MANAGEMENT_CONFIGURER_NAME)
            @ConditionalOnMissingBean(value = IntegrationManagementConfigurer.class, name = IntegrationManagementConfigurer.MANAGEMENT_CONFIGURER_NAME, search = SearchStrategy.CURRENT)
            public IntegrationManagementConfigurer managementConfigurer() {
                IntegrationManagementConfigurer configurer = new IntegrationManagementConfigurer();
                configurer.setDefaultCountsEnabled(true);
                configurer.setDefaultStatsEnabled(true);
                return configurer;
            }
    
            @Bean
            @ConditionalOnMissingBean(name = "springIntegrationPublicMetrics")
            public MetricReaderPublicMetrics springIntegrationPublicMetrics(
                    IntegrationManagementConfigurer managementConfigurer) {
                return new MetricReaderPublicMetrics(
                        new SpringIntegrationMetricReader(managementConfigurer));
            }
    
        }

    IntegrationMetricsConfiguration在满足如何条件时生效:

    1. @ConditionalOnClass(EnableIntegrationManagement.class) –> 在类路径下存在EnableIntegrationManagement.class时生效
    2. @ConditionalOnJava(JavaVersion.SEVEN)–> 在jdk1.7及jdk1.7以上的环境中生效

    springIntegrationPublicMetrics:

    在满足BeanFactory中不存在id为springIntegrationPublicMetrics,类型为MetricReaderPublicMetrics的baan时生效

    managementConfigurer:

    在满足在当前BeanFactoty中不存在id为integrationManagementConfigurer,类型为IntegrationManagementConfigurer的bean时生效.


来源:[]()

赞(0) 打赏
版权归原创作者所有,任何形式的转载请联系博主:daming_90:Java 技术驿站 » spring boot 源码解析46-MetricReader

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

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

支付宝扫一扫打赏

微信扫一扫打赏