再探Pinpoint Agent (一)

1、Agent作为Pinpoint采集部分,作为数据的来源,在系统中发挥着重要的作用,Pinpoint Agent启动Agent时添加的参数有

-javaagent:$path\pinpoint-agent-1.7.4-SNAPSHOT\pinpoint-bootstrap-1.7.4-SNAPSHOT.jar -Dpinpoint.agentId=111 -Dpinpoint.applicationName=abc

当启动监控程序后,直接进入到PinpointBootStrap类下的premain函数:

    public static void premain(String agentArgs, Instrumentation instrumentation) {
        if (agentArgs == null) {
            agentArgs = "";
        }
        logger.info(ProductInfo.NAME + " agentArgs:" + agentArgs);

        final boolean success = STATE.start(); //检查pinpoint是否已经启动
        if (!success) {
            logger.warn("pinpoint-bootstrap already started. skipping agent loading.");
            return;
        }
        Map<String, String> agentArgsMap = argsToMap(agentArgs);//Args转Map

        final ClassPathResolver classPathResolver = new AgentDirBaseClassPathResolver();
        if (!classPathResolver.verify()) {
            logger.warn("Agent Directory Verify fail. skipping agent loading.");
            logPinpointAgentLoadFail();
            return;
        }

        BootstrapJarFile bootstrapJarFile = classPathResolver.getBootstrapJarFile();
        appendToBootstrapClassLoader(instrumentation, bootstrapJarFile);


        PinpointStarter bootStrap = new PinpointStarter(agentArgsMap, bootstrapJarFile, classPathResolver, instrumentation);
        if (!bootStrap.start()) {
            logPinpointAgentLoadFail();
        }

    }

下面主要介绍关键的代码:

final boolean success = STATE.start(); //检查pinpoint是否已经启动
    ->public boolean start() {
        return state.compareAndSet(STATE_NONE, STATE_STARTED);
    }

主要通过CAS判断是否已经启。

final ClassPathResolver classPathResolver = new AgentDirBaseClassPathResolver();
    public AgentDirBaseClassPathResolver(String classPath) {
        this.classPath = classPath;
        this.agentPattern = DEFAULT_AGENT_PATTERN;
        this.agentCommonsPattern = DEFAULT_AGENT_COMMONS_PATTERN;
        this.agentCorePattern = DEFAULT_AGENT_CORE_PATTERN;
        this.agentCoreOptionalPattern = DEFAULT_AGENT_CORE_OPTIONAL_PATTERN;
        this.annotationsPattern = DEFAULT_ANNOTATIONS;
        this.fileExtensionList = getDefaultFileExtensionList();
    }

主要是指定相关依赖jar的名称,采用正则表达式,方便版本扩展

lassPathResolver.verify()
   ->public boolean verify() {

        final BootstrapJarFile bootstrapJarFile = new BootstrapJarFile();

        // 1st find boot-strap.jar
        final boolean agentJarNotFound = this.findAgentJar();
        if (!agentJarNotFound) {
            logger.warn("pinpoint-bootstrap-x.x.x(-SNAPSHOT).jar not found.");
            return false;
        }

        // 2nd find pinpoint-commons.jar
        final String pinpointCommonsJar = getPinpointCommonsJar();
        if (pinpointCommonsJar == null) {
            logger.warn("pinpoint-commons-x.x.x(-SNAPSHOT).jar not found");
            return false;
        }
        final JarFile pinpointCommonsJarFile = getJarFile(pinpointCommonsJar);
        if (pinpointCommonsJarFile == null) {
            logger.warn("pinpoint-commons-x.x.x(-SNAPSHOT).jar not found");
            return false;
        }
        bootstrapJarFile.append(pinpointCommonsJarFile);

        // 3rd find bootstrap-core.jar
        final String bootStrapCoreJar = getBootStrapCoreJar();
        if (bootStrapCoreJar == null) {
            logger.warn("pinpoint-bootstrap-core-x.x.x(-SNAPSHOT).jar not found");
            return false;
        }
        JarFile bootStrapCoreJarFile = getJarFile(bootStrapCoreJar);
        if (bootStrapCoreJarFile == null) {
            logger.warn("pinpoint-bootstrap-core-x.x.x(-SNAPSHOT).jar not found");
            return false;
        }
        bootstrapJarFile.append(bootStrapCoreJarFile);

        // 4th find bootstrap-core-optional.jar
        final String bootStrapCoreOptionalJar = getBootStrapCoreOptionalJar();
        if (bootStrapCoreOptionalJar == null) {
            logger.info("pinpoint-bootstrap-core-optional-x.x.x(-SNAPSHOT).jar not found");
        } else {
            JarFile bootStrapCoreOptionalJarFile = getJarFile(bootStrapCoreOptionalJar);
            if (bootStrapCoreOptionalJarFile == null) {
                logger.info("pinpoint-bootstrap-core-optional-x.x.x(-SNAPSHOT).jar not found");
            } else {
                bootstrapJarFile.append(bootStrapCoreOptionalJarFile);
            }
        }

        // 5th find annotations.jar : optional dependency
        final String annotationsJar = getAnnotationsJar();
        if (annotationsJar == null) {
            logger.info("pinpoint-annotations-x.x.x(-SNAPSHOT).jar not found");
        } else {
            JarFile jarFile = getJarFile(annotationsJar);
            bootstrapJarFile.append(jarFile);
        }

        this.bootstrapJarFile = bootstrapJarFile;
        return true;
    }

由上面的程序可以看出,主要是将boot-strap.jar、pinpoint-commons.jar、bootstrap-core.jar、bootstrap-core-optional.jar、annotations.jar添加到bootstrapJarFile文件中,append方法就是将以上文件添加到一个list集合中

4.添加到BootstrapClassLoader查找的类中

  appendToBootstrapClassLoader(instrumentation, bootstrapJarFile);
    ->instrumentation.appendToBootstrapClassLoaderSearch(jarFile);

5.调用start方法

PinpointStarter bootStrap = new PinpointStarter(agentArgsMap, bootstrapJarFile, classPathResolver, instrumentation);
bootStrap.start()

start方法如下:

    boolean start() {
        final IdValidator idValidator = new IdValidator();
        final String agentId = idValidator.getAgentId();
        if (agentId == null) {
            return false;
        }
        final String applicationName = idValidator.getApplicationName();
        if (applicationName == null) {
            return false;
        }

        URL[] pluginJars = classPathResolver.resolvePlugins();  //获取插件url

        // TODO using PLogger instead of CommonLogger
        CommonLoggerFactory loggerFactory = StdoutCommonLoggerFactory.INSTANCE;
        TraceMetadataLoaderService typeLoaderService = new DefaultTraceMetadataLoaderService(pluginJars, loggerFactory);
        ServiceTypeRegistryService serviceTypeRegistryService = new DefaultServiceTypeRegistryService(typeLoaderService, loggerFactory);
        AnnotationKeyRegistryService annotationKeyRegistryService = new DefaultAnnotationKeyRegistryService(typeLoaderService, loggerFactory);

        String configPath = getConfigPath(classPathResolver);
        if (configPath == null) {
            return false;
        }

        // set the path of log file as a system property
        saveLogFilePath(classPathResolver);

        savePinpointVersion();

        try {
            // Is it right to load the configuration in the bootstrap?
            ProfilerConfig profilerConfig = DefaultProfilerConfig.load(configPath);

            // this is the library list that must be loaded
            List<URL> libUrlList = resolveLib(classPathResolver);
            AgentClassLoader agentClassLoader = new AgentClassLoader(libUrlList.toArray(new URL[libUrlList.size()]));
            final String bootClass = getBootClass();
            agentClassLoader.setBootClass(bootClass);
            logger.info("pinpoint agent [" + bootClass + "] starting...");


            AgentOption option = createAgentOption(agentId, applicationName, profilerConfig, instrumentation, pluginJars, bootstrapJarFile, serviceTypeRegistryService, annotationKeyRegistryService);
            Agent pinpointAgent = agentClassLoader.boot(option);
            pinpointAgent.start();
            registerShutdownHook(pinpointAgent);
            logger.info("pinpoint agent started normally.");
        } catch (Exception e) {
            // unexpected exception that did not be checked above
            logger.warn(ProductInfo.NAME + " start failed.", e);
            return false;
        }
        return true;
    }

介绍关键代码:
1.调用provider的setup方法,设置插件的ServiceType

TraceMetadataLoaderService typeLoaderService = new DefaultTraceMetadataLoaderService(pluginJars, loggerFactory);
   ->loader.load(jarLists);
      ->load(providers);
         ->provider.setup(context); //依次执行provider的setup方法

以tomcat插件为例,

    @Override
    public void setup(TraceMetadataSetupContext context) {
        context.addServiceType(TomcatConstants.TOMCAT);
        context.addServiceType(TomcatConstants.TOMCAT_METHOD);
    }

其中TomcatConstants类下定义的TOMCAT和TOMCAT_METHOD如下:

    public static final ServiceType TOMCAT = ServiceTypeFactory.of(1010, "TOMCAT", RECORD_STATISTICS);
    public static final ServiceType TOMCAT_METHOD = ServiceTypeFactory.of(1011, "TOMCAT_METHOD");//工厂构建

    -> return new DefaultServiceType(code, name, desc, properties); 

通过代码可以看出主要是构建ServiceType对象实例

2.主要是查找出刚才插件中创建的ServiceType以及默认ServiceType合并后的一些映射关系

 ServiceTypeRegistryService serviceTypeRegistryService = new DefaultServiceTypeRegistryService(typeLoaderService, loggerFactory);

3.主要是查找出刚才插件中创建的AnnotationKey以及默认AnnotationKey合并后的一些映射关系

 AnnotationKeyRegistryService annotationKeyRegistryService = new DefaultAnnotationKeyRegistryService(typeLoaderService, loggerFactory);

    ->this.registry = buildServiceTypeRegistry();
    

4.声明启动类

 AgentClassLoader agentClassLoader = new AgentClassLoader(libUrlList.toArray(new URL[libUrlList.size()]));
 final String bootClass = getBootClass();
 >BOOT_CLASS = "com.navercorp.pinpoint.profiler.DefaultAgent"   //声明启动类

5.创建AgentOption

AgentOption option = createAgentOption(agentId, applicationName, profilerConfig, instrumentation, pluginJars, bootstrapJarFile, serviceTypeRegistryService, annotationKeyRegistryService);

6、使用agentClassLoader启动,其实就是初始化DefaultAgent

Agent pinpointAgent = agentClassLoader.boot(option);

下面是DefaultAgent的初始化方法

    static {
        // Preload classes related to pinpoint-rpc module.
        ClassPreLoader.preload();
    }

    ->  preload(65535);
        ->serverAcceptor = new PinpointServerAcceptor();  //NIO
    public DefaultAgent(AgentOption agentOption, final InterceptorRegistryBinder interceptorRegistryBinder) {
        if (agentOption == null) {
            throw new NullPointerException("agentOption must not be null");
        }
        if (agentOption.getInstrumentation() == null) {
            throw new NullPointerException("instrumentation must not be null");
        }
        if (agentOption.getProfilerConfig() == null) {
            throw new NullPointerException("profilerConfig must not be null");
        }
        if (agentOption.getServiceTypeRegistryService() == null) {
            throw new NullPointerException("serviceTypeRegistryService must not be null");
        }

        if (interceptorRegistryBinder == null) {
            throw new NullPointerException("interceptorRegistryBinder must not be null");
        }
        logger.info("AgentOption:{}", agentOption);

        this.binder = new Slf4jLoggerBinder();
        bindPLoggerFactory(this.binder);

        this.interceptorRegistryBinder = interceptorRegistryBinder;
        interceptorRegistryBinder.bind();
        this.serviceTypeRegistryService = agentOption.getServiceTypeRegistryService();

        dumpSystemProperties();   //导出系统变量
        dumpConfig(agentOption.getProfilerConfig());  

        changeStatus(AgentStatus.INITIALIZING);  //修改Agent状态

        this.profilerConfig = agentOption.getProfilerConfig();

        this.applicationContext = newApplicationContext(agentOption, interceptorRegistryBinder);

        
        InterceptorInvokerHelper.setPropagateException(profilerConfig.isPropagateInterceptorException());
    }

这段代码中最关键的一句是

   this.applicationContext = newApplicationContext(agentOption, interceptorRegistryBinder);
      ->return new DefaultApplicationContext(agentOption, interceptorRegistryBinder, moduleFactoryProvider);

DefaultApplicationContext构造方法如下:

    public DefaultApplicationContext(AgentOption agentOption, final InterceptorRegistryBinder interceptorRegistryBinder, ModuleFactory moduleFactory) {
        Assert.requireNonNull(agentOption, "agentOption must not be null");
        this.profilerConfig = Assert.requireNonNull(agentOption.getProfilerConfig(), "profilerConfig must not be null");
        Assert.requireNonNull(moduleFactory, "moduleFactory must not be null");

        this.instrumentation = agentOption.getInstrumentation();
        this.serviceTypeRegistryService = agentOption.getServiceTypeRegistryService();

        if (logger.isInfoEnabled()) {
            logger.info("DefaultAgent classLoader:{}", this.getClass().getClassLoader());
        }

         //关键的下面两句
        final Module applicationContextModule = moduleFactory.newModule(agentOption, interceptorRegistryBinder);
        this.injector = Guice.createInjector(Stage.PRODUCTION, applicationContextModule);
        //使用injector 获取实例
        this.instrumentEngine = injector.getInstance(InstrumentEngine.class);

        this.classFileDispatcher = injector.getInstance(ClassFileTransformerDispatcher.class);
        this.dynamicTransformTrigger = injector.getInstance(DynamicTransformTrigger.class);
//        ClassFileTransformer classFileTransformer = injector.getInstance(ClassFileTransformer.class);
        ClassFileTransformer classFileTransformer = wrap(classFileDispatcher);
        instrumentation.addTransformer(classFileTransformer, true);

        this.spanStatClientFactory = injector.getInstance(Key.get(PinpointClientFactory.class, SpanStatClientFactory.class));
        logger.info("spanStatClientFactory:{}", spanStatClientFactory);

        this.spanDataSender = newUdpSpanDataSender();
        logger.info("spanDataSender:{}", spanDataSender);

        this.statDataSender = newUdpStatDataSender();
        logger.info("statDataSender:{}", statDataSender);

        this.clientFactory = injector.getInstance(Key.get(PinpointClientFactory.class, DefaultClientFactory.class));
        logger.info("clientFactory:{}", clientFactory);

        this.tcpDataSender = injector.getInstance(EnhancedDataSender.class);
        logger.info("tcpDataSender:{}", tcpDataSender);

        this.traceContext = injector.getInstance(TraceContext.class);

        this.agentInformation = injector.getInstance(AgentInformation.class);
        logger.info("agentInformation:{}", agentInformation);
        this.serverMetaDataRegistryService = injector.getInstance(ServerMetaDataRegistryService.class);

        this.deadlockMonitor = injector.getInstance(DeadlockMonitor.class);
        this.agentInfoSender = injector.getInstance(AgentInfoSender.class);
        this.agentStatMonitor = injector.getInstance(AgentStatMonitor.class);
    }

上面代码关键的两句:

 final Module applicationContextModule = moduleFactory.newModule(agentOption, interceptorRegistryBinder);
 this.injector = Guice.createInjector(Stage.PRODUCTION, applicationContextModule);

类似Spring依赖注入,使用轻量级谷歌的Guice进行依赖注入,关于Guice的使用可以参考以下文章
https://blog.csdn.net/cnhome/article/details/80627123

期间会调用插件的Plugin类下的setup方法,调用过程如下:

-->PluginContextLoadResult pluginContextLoadResult = this.pluginContextLoadResultProvider.get();//ApplicationServerTypeProvider类下的get方法
--> return new DefaultPluginContextLoadResult(profilerConfig, dynamicTransformTrigger, instrumentEngine, pluginJars); //PluginContextLoadResultProvider类
-->this.setupResultList = load(); //DefaultPluginContextLoadResult类
-->  List<SetupResult> load = loader.load(pluginJars);

   //ProfilerPluginLoader类
    public List<SetupResult> load(URL[] pluginJars) {

        List<SetupResult> pluginContexts = new ArrayList<SetupResult>(pluginJars.length);

        for (URL pluginJar : pluginJars) {

            final JarFile pluginJarFile = createJarFile(pluginJar);
            final List<String> pluginPackageList = getPluginPackage(pluginJarFile);

            final ClassNameFilter pluginFilterChain = createPluginFilterChain(pluginPackageList);

            final List<ProfilerPlugin> original = PluginLoader.load(ProfilerPlugin.class, new URL[] { pluginJar });

            List<ProfilerPlugin> plugins = filterDisablePlugin(original);

            for (ProfilerPlugin plugin : plugins) {
                 if (logger.isInfoEnabled()) {
                    logger.info("{} Plugin {}:{}", plugin.getClass(), PluginConfig.PINPOINT_PLUGIN_PACKAGE, pluginPackageList);
                }
                
                logger.info("Loading plugin:{} pluginPackage:{}", plugin.getClass().getName(), plugin);

                PluginConfig pluginConfig = new PluginConfig(pluginJar, pluginFilterChain);
                final ClassInjector classInjector = new JarProfilerPluginClassInjector(pluginConfig, instrumentEngine);
                final SetupResult result = pluginSetup.setupPlugin(plugin, classInjector);
                pluginContexts.add(result);
            }
        }
        

        return pluginContexts;
    }
->final SetupResult result = pluginSetup.setupPlugin(plugin, classInjector);
->profilerPlugin.setup(guardSetupContext);   //调用setup方法

如tomcat则调用:

   @Override
    public void setup(ProfilerPluginSetupContext context) {

        final TomcatConfig config = new TomcatConfig(context.getConfig());
        if (logger.isInfoEnabled()) {
            logger.info("TomcatPlugin config:{}", config);
        }
        if (!config.isTomcatEnable()) {
            logger.info("TomcatPlugin disabled");
            return;
        }

        TomcatDetector tomcatDetector = new TomcatDetector(config.getTomcatBootstrapMains());
        context.addApplicationTypeDetector(tomcatDetector);

        if (shouldAddTransformers(config)) {    //判断是否需要添加transformers
            logger.info("Adding Tomcat transformers");
            addTransformers(config);   //添加transformers

        } else {
            logger.info("Not adding Tomcat transfomers");
        }
    }

    private void addTransformers(TomcatConfig config) {
        if (config.isTomcatHidePinpointHeader()) {
            addRequestFacadeEditor();
        }

        addRequestEditor();
        addStandardHostValveEditor();
        addStandardServiceEditor();
        addTomcatConnectorEditor();
        addWebappLoaderEditor();

        addAsyncContextImpl();
    }

    private void addRequestEditor() {
        transformTemplate.transform("org.apache.catalina.connector.Request", new TransformCallback() {

            @Override
            public byte[] doInTransform(Instrumentor instrumentor, ClassLoader classLoader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException {
                InstrumentClass target = instrumentor.getInstrumentClass(classLoader, className, classfileBuffer);
                target.addField(TomcatConstants.TRACE_ACCESSOR);
                target.addField(TomcatConstants.ASYNC_ACCESSOR);

                // clear request.
                InstrumentMethod recycleMethodEditorBuilder = target.getDeclaredMethod("recycle");
                if (recycleMethodEditorBuilder != null) {
                    recycleMethodEditorBuilder.addInterceptor("com.navercorp.pinpoint.plugin.tomcat.interceptor.RequestRecycleInterceptor");  //添加拦截器
                }

                // trace asynchronous process.
                InstrumentMethod startAsyncMethodEditor = target.getDeclaredMethod("startAsync", "javax.servlet.ServletRequest", "javax.servlet.ServletResponse");
                if (startAsyncMethodEditor != null) {
                    startAsyncMethodEditor.addInterceptor("com.navercorp.pinpoint.plugin.tomcat.interceptor.RequestStartAsyncInterceptor");  //添加拦截器
                }

                return target.toBytecode();
            }
        });
    }

主要通过transform添加拦截器修改类的字节码,已达到监控的目的。

7.DefaultAgent的start函数(Running),主要是定时发送相关信息。

  pinpointAgent.start();
     --->this.applicationContext.start();
       -> this.deadlockMonitor.start();
           this.agentInfoSender.start();
           this.agentStatMonitor.start();

            ->scheduler.start();
            ->schedule(successListener, Integer.MAX_VALUE, IMMEDIATE, sendIntervalMs);
           ->AgentInfoSendTask task = new AgentInfoSendTask(successListener, retryCount);
               timer.scheduleAtFixedRate(task, delay, period);
           ->boolean isSuccessful = sendAgentInfo();
              ->dataSender.request(agentInfo, new AgentInfoSenderListener(future));  //定时发送agentInfo

今天就写这么多,下次再接着研究。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,825评论 6 546
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,814评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,980评论 0 384
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 64,064评论 1 319
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,779评论 6 414
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 56,109评论 1 330
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 44,099评论 3 450
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,287评论 0 291
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,799评论 1 338
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,515评论 3 361
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,750评论 1 375
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,221评论 5 365
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,933评论 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,327评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,667评论 1 296
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,492评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,703评论 2 380

推荐阅读更多精彩内容