国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

SpringBoot究竟是如何跑起來(lái)的?

DevWiki / 3505人閱讀

摘要:你可以試著沿著調(diào)用棧代碼一層一層的深入進(jìn)去,如果你不打斷點(diǎn),你根本不知道接下來(lái)程序會(huì)往哪里流動(dòng)。接下來(lái)再看看運(yùn)行時(shí)堆棧,看看一個(gè)請(qǐng)求的調(diào)用棧有多深。就是如此被自動(dòng)裝配進(jìn)的。

摘要: 神奇的SpringBoot。

原文:SpringBoot 究竟是如何跑起來(lái)的?

作者:老錢(qián)

Fundebug經(jīng)授權(quán)轉(zhuǎn)載,版權(quán)歸原作者所有。

不得不說(shuō) SpringBoot 太復(fù)雜了,我本來(lái)只想研究一下 SpringBoot 最簡(jiǎn)單的 HelloWorld 程序是如何從 main 方法一步一步跑起來(lái)的,但是這卻是一個(gè)相當(dāng)深的坑。你可以試著沿著調(diào)用棧代碼一層一層的深入進(jìn)去,如果你不打斷點(diǎn),你根本不知道接下來(lái)程序會(huì)往哪里流動(dòng)。這個(gè)不同于我研究過(guò)去的 Go 語(yǔ)言、Python 語(yǔ)言框架,它們通常都非常直接了當(dāng),設(shè)計(jì)上清晰易懂,代碼寫(xiě)起來(lái)簡(jiǎn)單,里面的實(shí)現(xiàn)同樣也很簡(jiǎn)單。但是 SpringBoot 不是,它的外表輕巧簡(jiǎn)單,但是它的里面就像一只巨大的怪獸,這只怪獸有千百只腳把自己纏繞在一起,把愛(ài)研究源碼的讀者繞的暈頭轉(zhuǎn)向。但是這 Java 編程的世界 SpringBoot 就是老大哥,你卻不得不服。即使你的心中有千萬(wàn)頭草泥馬在奔跑,但是它就是天下第一。如果你是一個(gè)學(xué)院派的程序員,看到這種現(xiàn)象你會(huì)懷疑人生,你不得不接受一個(gè)規(guī)則 —— 受市場(chǎng)最歡迎的未必就是設(shè)計(jì)的最好的,里面夾雜著太多其它的非理性因素。

經(jīng)過(guò)了一番痛苦的折磨,我還是把 SpringBoot 的運(yùn)行原理摸清楚了,這里分享給大家。

Hello World

首先我們看看 SpringBoot 簡(jiǎn)單的 Hello World 代碼,就兩個(gè)文件 HelloControll.java 和 Application.java,運(yùn)行 Application.java 就可以跑起來(lái)一個(gè)簡(jiǎn)單的 RESTFul Web 服務(wù)器了。

// HelloController.java
package hello;

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

@RestController
public class HelloController {

    @RequestMapping("/")
    public String index() {
        return "Greetings from Spring Boot!";
    }

}

// Application.java
package hello;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

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

}

當(dāng)我打開(kāi)瀏覽器看到服務(wù)器正常地將輸出呈現(xiàn)在瀏覽器的時(shí)候,我不禁大呼 —— SpringBoot 真他媽太簡(jiǎn)單了。

但是問(wèn)題來(lái)了,在 Application 的 main 方法里我壓根沒(méi)有任何地方引用 HelloController 類,那么它的代碼又是如何被服務(wù)器調(diào)用起來(lái)的呢?這就需要深入到 SpringApplication.run() 方法中看個(gè)究竟了。不過(guò)即使不看代碼,我們也很容易有這樣的猜想,SpringBoot 肯定是在某個(gè)地方掃描了當(dāng)前的 package,將帶有 RestController 注解的類作為 MVC 層的 Controller 自動(dòng)注冊(cè)進(jìn)了 Tomcat Server。

還有一個(gè)讓人不爽的地方是 SpringBoot 啟動(dòng)太慢了,一個(gè)簡(jiǎn)單的 Hello World 啟動(dòng)居然還需要長(zhǎng)達(dá) 5 秒,要是再?gòu)?fù)雜一些的項(xiàng)目這樣龜漫的啟動(dòng)速度那真是不好想象了。

再抱怨一下,這個(gè)簡(jiǎn)單的 HelloWorld 雖然 pom 里只配置了一個(gè) maven 依賴,但是傳遞下去,它一共依賴了 36 個(gè) jar 包,其中以 spring 開(kāi)頭的 jar 包有 15 個(gè)。說(shuō)這是依賴地獄真一點(diǎn)不為過(guò)。

批評(píng)到這里就差不多了,下面就要正是進(jìn)入主題了,看看 SpringBoot 的 main 方法到底是如何跑起來(lái)的。

SpringBoot 的堆棧

了解 SpringBoot 運(yùn)行的最簡(jiǎn)單的方法就是看它的調(diào)用堆棧,下面這個(gè)啟動(dòng)調(diào)用堆棧還不是太深,我沒(méi)什么可抱怨的。

public class TomcatServer {

  @Override
  public void start() throws WebServerException {
  ...
  }

}

接下來(lái)再看看運(yùn)行時(shí)堆棧,看看一個(gè) HTTP 請(qǐng)求的調(diào)用棧有多深。不看不知道一看嚇了一大跳!

我通過(guò)將 IDE 窗口全屏化,并將其它的控制臺(tái)窗口源碼窗口統(tǒng)統(tǒng)最小化,總算勉強(qiáng)一個(gè)屏幕裝下了整個(gè)調(diào)用堆棧。

不過(guò)轉(zhuǎn)念一想,這也不怪 SpringBoot,絕大多數(shù)都是 Tomcat 的調(diào)用堆棧,跟 SpringBoot 相關(guān)的只有不到 10 層。

探索 ClassLoader

SpringBoot 還有一個(gè)特色的地方在于打包時(shí)它使用了 FatJar 技術(shù)將所有的依賴 jar 包一起放進(jìn)了最終的 jar 包中的 BOOT-INF/lib 目錄中,當(dāng)前項(xiàng)目的 class 被統(tǒng)一放到了 BOOT-INF/classes 目錄中。


    
        
            org.springframework.boot
            spring-boot-maven-plugin
        
    

這不同于我們平時(shí)經(jīng)常使用的 maven shade 插件,將所有的依賴 jar 包中的 class 文件解包出來(lái)后再密密麻麻的塞進(jìn)統(tǒng)一的 jar 包中。下面我們將 springboot 打包的 jar 包解壓出來(lái)看看它的目錄結(jié)構(gòu)。

├── BOOT-INF
│   ├── classes
│   │   └── hello
│   └── lib
│       ├── classmate-1.3.4.jar
│       ├── hibernate-validator-6.0.12.Final.jar
│       ├── jackson-annotations-2.9.0.jar
│       ├── jackson-core-2.9.6.jar
│       ├── jackson-databind-2.9.6.jar
│       ├── jackson-datatype-jdk8-2.9.6.jar
│       ├── jackson-datatype-jsr310-2.9.6.jar
│       ├── jackson-module-parameter-names-2.9.6.jar
│       ├── javax.annotation-api-1.3.2.jar
│       ├── jboss-logging-3.3.2.Final.jar
│       ├── jul-to-slf4j-1.7.25.jar
│       ├── log4j-api-2.10.0.jar
│       ├── log4j-to-slf4j-2.10.0.jar
│       ├── logback-classic-1.2.3.jar
│       ├── logback-core-1.2.3.jar
│       ├── slf4j-api-1.7.25.jar
│       ├── snakeyaml-1.19.jar
│       ├── spring-aop-5.0.9.RELEASE.jar
│       ├── spring-beans-5.0.9.RELEASE.jar
│       ├── spring-boot-2.0.5.RELEASE.jar
│       ├── spring-boot-autoconfigure-2.0.5.RELEASE.jar
│       ├── spring-boot-starter-2.0.5.RELEASE.jar
│       ├── spring-boot-starter-json-2.0.5.RELEASE.jar
│       ├── spring-boot-starter-logging-2.0.5.RELEASE.jar
│       ├── spring-boot-starter-tomcat-2.0.5.RELEASE.jar
│       ├── spring-boot-starter-web-2.0.5.RELEASE.jar
│       ├── spring-context-5.0.9.RELEASE.jar
│       ├── spring-core-5.0.9.RELEASE.jar
│       ├── spring-expression-5.0.9.RELEASE.jar
│       ├── spring-jcl-5.0.9.RELEASE.jar
│       ├── spring-web-5.0.9.RELEASE.jar
│       ├── spring-webmvc-5.0.9.RELEASE.jar
│       ├── tomcat-embed-core-8.5.34.jar
│       ├── tomcat-embed-el-8.5.34.jar
│       ├── tomcat-embed-websocket-8.5.34.jar
│       └── validation-api-2.0.1.Final.jar
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── org.springframework
└── org
    └── springframework
        └── boot

這種打包方式的優(yōu)勢(shì)在于最終的 jar 包結(jié)構(gòu)很清晰,所有的依賴一目了然。如果使用 maven shade 會(huì)將所有的 class 文件混亂堆積在一起,是無(wú)法看清其中的依賴。而最終生成的 jar 包在體積上兩也者幾乎是相等的。

在運(yùn)行機(jī)制上,使用 FatJar 技術(shù)運(yùn)行程序是需要對(duì) jar 包進(jìn)行改造的,它還需要自定義自己的 ClassLoader 來(lái)加載 jar 包里面 lib 目錄中嵌套的 jar 包中的類。我們可以對(duì)比一下兩者的 MANIFEST 文件就可以看出明顯差異

// Generated by Maven Shade Plugin
Manifest-Version: 1.0
Implementation-Title: gs-spring-boot
Implementation-Version: 0.1.0
Built-By: qianwp
Implementation-Vendor-Id: org.springframework
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_191
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
 ot-starter-parent/gs-spring-boot
Main-Class: hello.Application

// Generated by SpringBootLoader Plugin
Manifest-Version: 1.0
Implementation-Title: gs-spring-boot
Implementation-Version: 0.1.0
Built-By: qianwp
Implementation-Vendor-Id: org.springframework
Spring-Boot-Version: 2.0.5.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: hello.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_191
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
 ot-starter-parent/gs-spring-boot

SpringBoot 將 jar 包中的 Main-Class 進(jìn)行了替換,換成了 JarLauncher。還增加了一個(gè) Start-Class 參數(shù),這個(gè)參數(shù)對(duì)應(yīng)的類才是真正的業(yè)務(wù) main 方法入口。我們?cè)倏纯催@個(gè) JarLaucher 具體干了什么

public class JarLauncher{
    ...
  static void main(String[] args) {
    new JarLauncher().launch(args);
  }

  protected void launch(String[] args) {
    try {
      JarFile.registerUrlProtocolHandler();
      ClassLoader cl = createClassLoader(getClassPathArchives());
      launch(args, getMainClass(), cl);
    }
    catch (Exception ex) {
        ex.printStackTrace();
        System.exit(1);
    }
  }

  protected void launch(String[] args, String mcls, ClassLoader cl) {
        Runnable runner = createMainMethodRunner(mcls, args, cl);
        Thread runnerThread = new Thread(runner);
        runnerThread.setContextClassLoader(classLoader);
        runnerThread.setName(Thread.currentThread().getName());
        runnerThread.start();
  }

}

class MainMethodRunner {
  @Override
  public void run() {
    try {
      Thread th = Thread.currentThread();
      ClassLoader cl = th.getContextClassLoader();
      Class mc = cl.loadClass(this.mainClassName);
      Method mm = mc.getDeclaredMethod("main", String[].class);
      if (mm == null) {
        throw new IllegalStateException(this.mainClassName
                        + " does not have a main method");
      }
      mm.invoke(null, new Object[] { this.args });
    } catch (Exception ex) {
      ex.printStackTrace();
      System.exit(1);
    }
  }
}

從源碼中可以看出 JarLaucher 創(chuàng)建了一個(gè)特殊的 ClassLoader,然后由這個(gè) ClassLoader 來(lái)另啟一個(gè)多帶帶的線程來(lái)加載 MainClass 并運(yùn)行。

又一個(gè)問(wèn)題來(lái)了,當(dāng) JVM 遇到一個(gè)不認(rèn)識(shí)的類,BOOT-INF/lib 目錄里又有那么多 jar 包,它是如何知道去哪個(gè) jar 包里加載呢?我們繼續(xù)看這個(gè)特別的 ClassLoader 的源碼

class LaunchedURLClassLoader extends URLClassLoader {
  ...
  private Class doLoadClass(String name) {
    if (this.rootClassLoader != null) {
      return this.rootClassLoader.loadClass(name);
    }

    findPackage(name);
    Class cls = findClass(name);
    return cls;
  }

}

這里的 rootClassLoader 就是雙親委派模型里的 ExtensionClassLoader ,JVM 內(nèi)置的類會(huì)優(yōu)先使用它來(lái)加載。如果不是內(nèi)置的就去查找這個(gè)類對(duì)應(yīng)的 Package。

private void findPackage(final String name) {
    int lastDot = name.lastIndexOf(".");
    if (lastDot != -1) {
        String packageName = name.substring(0, lastDot);
        if (getPackage(packageName) == null) {
            try {
                definePackage(name, packageName);
            } catch (Exception ex) {
                // Swallow and continue
            }
        }
    }
}

private final HashMap packages = new HashMap<>();

protected Package getPackage(String name) {
    Package pkg;
    synchronized (packages) {
        pkg = packages.get(name);
    }
    if (pkg == null) {
        if (parent != null) {
            pkg = parent.getPackage(name);
        } else {
            pkg = Package.getSystemPackage(name);
        }
        if (pkg != null) {
            synchronized (packages) {
                Package pkg2 = packages.get(name);
                if (pkg2 == null) {
                    packages.put(name, pkg);
                } else {
                    pkg = pkg2;
                }
            }
        }
    }
    return pkg;
}

private void definePackage(String name, String packageName) {
  String path = name.replace(".", "/").concat(".class");
  for (URL url : getURLs()) {
    try {
      if (url.getContent() instanceof JarFile) {
        JarFile jf= (JarFile) url.getContent();
        if (jf.getJarEntryData(path) != null && jf.getManifest() != null) {
          definePackage(packageName, jf.getManifest(), url);
          return null;
        }
      }
    } catch (IOException ex) {
        // Ignore
    }
  }
  return null;
}

ClassLoader 會(huì)在本地緩存包名和 jar包路徑的映射關(guān)系,如果緩存中找不到對(duì)應(yīng)的包名,就必須去 jar 包中挨個(gè)遍歷搜尋,這個(gè)就比較緩慢了。不過(guò)同一個(gè)包名只會(huì)搜尋一次,下一次就可以直接從緩存中得到對(duì)應(yīng)的內(nèi)嵌 jar 包路徑。

深層 jar 包的內(nèi)嵌 class 的 URL 路徑長(zhǎng)下面這樣,使用感嘆號(hào) ! 分割

jar:file:/workspace/springboot-demo/target/application.jar!/BOOT-INF/lib/snakeyaml-1.19.jar!/org/yaml/snakeyaml/Yaml.class

不過(guò)這個(gè)定制的 ClassLoader 只會(huì)用于打包運(yùn)行時(shí),在 IDE 開(kāi)發(fā)環(huán)境中 main 方法還是直接使用系統(tǒng)類加載器加載運(yùn)行的。

不得不說(shuō),SpringbootLoader 的設(shè)計(jì)還是很有意思的,它本身很輕量級(jí),代碼邏輯很獨(dú)立沒(méi)有其它依賴,它也是 SpringBoot 值得欣賞的點(diǎn)之一。

HelloController 自動(dòng)注冊(cè)

還剩下最后一個(gè)問(wèn)題,那就是 HelloController 沒(méi)有被代碼引用,它是如何注冊(cè)到 Tomcat 服務(wù)中去的?它靠的是注解傳遞機(jī)制。

SpringBoot 深度依賴注解來(lái)完成配置的自動(dòng)裝配工作,它自己發(fā)明了幾十個(gè)注解,確實(shí)嚴(yán)重增加了開(kāi)發(fā)者的心智負(fù)擔(dān),你需要仔細(xì)閱讀文檔才能知道它是用來(lái)干嘛的。Java 注解的形式和功能是分離的,它不同于 Python 的裝飾器是功能性的,Java 的注解就好比代碼注釋,本身只有屬性,沒(méi)有邏輯,注解相應(yīng)的功能由散落在其它地方的代碼來(lái)完成,需要分析被注解的類結(jié)構(gòu)才可以得到相應(yīng)注解的屬性。

那注解是又是如何傳遞的呢?

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

@ComponentScan
public @interface SpringBootApplication {
...
}

public @interface ComponentScan {
    String[] basePackages() default {};
}

首先 main 方法可以看到的注解是 SpringBootApplication,這個(gè)注解又是由ComponentScan 注解來(lái)定義的,ComponentScan 注解會(huì)定義一個(gè)被掃描的包名稱,如果沒(méi)有顯示定義那就是當(dāng)前的包路徑。SpringBoot 在遇到 ComponentScan 注解時(shí)會(huì)掃描對(duì)應(yīng)包路徑下面的所有 Class,根據(jù)這些 Class 上標(biāo)注的其它注解繼續(xù)進(jìn)行后續(xù)處理。當(dāng)它掃到 HelloController 類時(shí)發(fā)現(xiàn)它標(biāo)注了 RestController 注解。

@RestController
public class HelloController {
...
}

@Controller
public @interface RestController {
}

而 RestController 注解又標(biāo)注了 Controller 注解。SpringBoot 對(duì) Controller 注解進(jìn)行了特殊處理,它會(huì)將 Controller 注解的類當(dāng)成 URL 處理器注冊(cè)到 Servlet 的請(qǐng)求處理器中,在創(chuàng)建 Tomcat Server 時(shí),會(huì)將請(qǐng)求處理器傳遞進(jìn)去。HelloController 就是如此被自動(dòng)裝配進(jìn) Tomcat 的。

掃描處理注解是一個(gè)非常繁瑣骯臟的活計(jì),特別是這種用注解來(lái)注解注解(繞口)的高級(jí)使用方法,這種方法要少用慎用。SpringBoot 中有大量的注解相關(guān)代碼,企圖理解這些代碼是乏味無(wú)趣的沒(méi)有必要的,它只會(huì)把你的本來(lái)清醒的腦袋搞暈。SpringBoot 對(duì)于習(xí)慣使用的同學(xué)來(lái)說(shuō)它是非常方便的,但是其內(nèi)部實(shí)現(xiàn)代碼不要輕易模仿,那絕對(duì)算不上模范 Java 代碼。

最后老錢(qián)表示自己真的很討厭 SpringBoot 這只怪獸,但是很無(wú)奈,這個(gè)世界人人都在使用它。這就好比老人們常常告誡年輕人的那句話:如果你改變不了世界,那就先適應(yīng)這個(gè)世界吧!

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/72897.html

相關(guān)文章

  • 項(xiàng)目在啟動(dòng)時(shí)竟是怎么運(yùn)行(項(xiàng)目運(yùn)行步驟)

    摘要:自己在前端的開(kāi)發(fā)中主要使用的框架,今天的這篇文章比較基礎(chǔ),我之前在剛剛接觸項(xiàng)目的時(shí)候并沒(méi)有思考過(guò)關(guān)于項(xiàng)目是究竟怎么運(yùn)行起來(lái)的,只知道項(xiàng)目就跑起來(lái)了,究竟我在輸入這行命令之后項(xiàng)目是怎么運(yùn)行的,分別走了哪幾步,怎么樣才走到生產(chǎn)環(huán)境,什么情況下又 自己在前端的開(kāi)發(fā)中主要使用vue.js的框架,今天的這篇文章比較基礎(chǔ),我之前在剛剛接觸vue項(xiàng)目的時(shí)候并沒(méi)有思考過(guò)關(guān)于項(xiàng)目是究竟怎么運(yùn)行起來(lái)的,只...

    Godtoy 評(píng)論0 收藏0
  • Node.js 竟是什么?

    摘要:在回調(diào)隊(duì)列中,函數(shù)等待調(diào)用棧為空,因?yàn)槊總€(gè)語(yǔ)句都執(zhí)行一次。最后一個(gè)運(yùn)行,并且從調(diào)用棧中彈出。它將回調(diào)以先進(jìn)先出順序移動(dòng)到調(diào)用棧并執(zhí)行。 翻譯:瘋狂的技術(shù)宅原文: https://medium.freecodecamp.o... 本文首發(fā)微信公眾號(hào):前端先鋒歡迎關(guān)注,每天都給你推送新鮮的前端技術(shù)文章 Node.js 是一個(gè) JavaScript 運(yùn)行時(shí)環(huán)境。聽(tīng)起來(lái)還不錯(cuò),不過(guò)這究竟...

    yeyan1996 評(píng)論0 收藏0
  • 「譯」JavaScript 竟是如何工作?(第一部分)

    摘要:文章的第二部分涵蓋了內(nèi)存管理的概念,不久后將發(fā)布。的標(biāo)準(zhǔn)化工作是由國(guó)際組織負(fù)責(zé)的,相關(guān)規(guī)范被稱為或者。隨著分析器和編譯器不斷地更改字節(jié)碼,的執(zhí)行性能逐漸提高。 原文地址:How Does JavaScript Really Work? (Part 1) 原文作者:Priyesh Patel 譯者:Chor showImg(https://segmentfault.com/img...

    Youngdze 評(píng)論0 收藏0
  • Node.js竟是什么?

    摘要:究竟是什么是一個(gè)運(yùn)行時(shí)環(huán)境。對(duì)此請(qǐng)求的響應(yīng)需要時(shí)間,但兩個(gè)用戶數(shù)據(jù)請(qǐng)求可以獨(dú)立并同時(shí)執(zhí)行。所以這會(huì)使不太適合多線程任務(wù)。這種非阻塞消除了多線程的需要,因?yàn)榉?wù)器可以同時(shí)處理多個(gè)請(qǐng)求。該事件將等待毫秒,然后回調(diào)函數(shù)。系統(tǒng)事件來(lái)自庫(kù)的核心。 Node.js究竟是什么? Node.js是一個(gè)JavaScript運(yùn)行時(shí)環(huán)境。聽(tīng)起來(lái)不錯(cuò),但這是什么意思?這是如何運(yùn)作的? Node運(yùn)行時(shí)環(huán)境包含執(zhí)...

    miracledan 評(píng)論0 收藏0
  • Node.js竟是什么?

    摘要:究竟是什么是一個(gè)運(yùn)行時(shí)環(huán)境。對(duì)此請(qǐng)求的響應(yīng)需要時(shí)間,但兩個(gè)用戶數(shù)據(jù)請(qǐng)求可以獨(dú)立并同時(shí)執(zhí)行。所以這會(huì)使不太適合多線程任務(wù)。這種非阻塞消除了多線程的需要,因?yàn)榉?wù)器可以同時(shí)處理多個(gè)請(qǐng)求。該事件將等待毫秒,然后回調(diào)函數(shù)。系統(tǒng)事件來(lái)自庫(kù)的核心。 Node.js究竟是什么? Node.js是一個(gè)JavaScript運(yùn)行時(shí)環(huán)境。聽(tīng)起來(lái)不錯(cuò),但這是什么意思?這是如何運(yùn)作的? Node運(yùn)行時(shí)環(huán)境包含執(zhí)...

    dingding199389 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<