用Visual Studio Code学习Spring Boot的入门

1. 这篇文章的内容

由于可能要在开发业务中使用Spring Framework,所以尝试使用VS Code而不是Eclipse作为开发工具。我认为在Java开发中,目前主流是使用Eclipse,但将来可能会增加使用VS Code的情况。

2. 关于 Spring 框架的复习

在开始这个问题之前,我会简单地回顾一下Spring Framework。

(1) Spring Boot 是什么来着?和 Spring Framework 有什么不同?

根据《Spring 徹底入門》一书的介绍,Spring Boot 是一个用于简化开发生产级别的Spring应用程序的Spring项目,只需最少的配置即可。我们可以将其视为使用Spring Framework的“即插即用套件”。因此,可以说通过Spring Boot创建的应用程序是由Spring Framework构建的。它是为那些想尝试使用Spring Framework,而不想被繁琐的细节或麻烦事所困扰的人们准备的一种降低入门门槛的简化机制。

    Spring Boot の概要とサポート期間 – リファレンスドキュメント (pleiades.io)

 

    Spring Boot リファレンスドキュメント (pleiades.io)

 

(2) Spring Framework是Java EE的一种吗?

最初开发Spring时(大约2004年),JavaEE还不存在,而是存在其前身称为”J2EE”的标准规范。从J2EE的第5个版本开始,它改名为Java EE(进一步在第9个版本改名为Jakarta EE)。J2EE/Java EE/Jakarta EE是Java企业级功能的标准规范,它本身不是一个框架,而是一种规范。所以Spring Framework不是JavaEE的一种,它是与JavaEE相符合或不符合的关系。目前来看,可以说Spring Framework”部分符合”JavaEE,但并不完全符合。最初Spring Framework的设计初衷是反对J2EE/JavaEE的繁杂难用,因此强调轻便易用。由于两者取长补短,所以一些功能有相似或重叠之处,逐渐趋向相似的方向。

现在我们就结束复习,开始正题。

2. 使用过的版本 guò de

(1) Windows 11 专业版

    • バージョン: 22H2

 

    OSビルド: 22621.1105

我认为,只是偶尔使用这个问题,对于本文内容而言,在Windows10上没有问题。

(2) 开放JDK

>java -version
openjdk version "17.0.6" 2023-01-17 LTS
OpenJDK Runtime Environment (Red_Hat-17.0.6.0+10-1) (build 17.0.6+10-LTS)
OpenJDK 64-Bit Server VM (Red_Hat-17.0.6.0+10-1) (build 17.0.6+10-LTS, mixed mode, sharing)

这次我们使用了Red Hat,但我觉得JDK在Oracle OpenJDK上也没有问题。
为什么选择Red Hat,是因为在VS Code上显示了这个链接要使用它。我不清楚为什么会推荐Red Hat版,但考虑到VSCode的Java语言支持扩展是Red Hat的产品,所以JDK也最好配套使用,相容性应该会更好。

(3) 春季框架

从pom.xml中

<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.2</version>

这不太是前提,更像是在创建Spring Boot项目时选择该版本,计划使用3.0.2版本。

(4) Visual Studio Code
(4) 可视化工作室代码

バージョン: 1.75.1 (user setup)
コミット: 441438abd1ac652551dbe4d408dfcec8a499b8bf
 日付: 2023-02-08T21:32:34.589Z
Electron: 19.1.9
Chromium: 102.0.5005.194
Node.js: 16.14.2
V8: 10.2.154.23-electron.0
OS: Windows_NT x64 10.0.22621
Sandboxed: Yes

3. 环境设置步骤

假设JDK和VS Code已经安装好了,作为任务的前提。

安装 Visual Studio Code 的 Java 扩展包 Extension Pack。

Extension Pack for Java1.png

这是一个推荐用于在VS Code上进行Java开发的扩展插件合集。包括以下扩展插件。

? Language Support for Java™ by Red Hat
補完、リファクタリング、スニペットのサポート

? Debugger for Java
デバッグ機能

? Test Runner for Java
JUnit/TestNG 対応

? Maven for Java
ビルドツールのMaven を使う拡張機能です。

? Project Manager for Java
Javaプロジェクト管理機能。ここでいう「プロジェクト」はソースやリソース類を束ねたもの、という意味で、Eclipse 上のプロジェクトと同じように考えればよいです。

? Visual Studio IntelliCode
AIによるコーディング支援機能。コード補完のときにいろいろサジェストしてくれます。

Extension Pack for Java2.png

安装VS Code的Spring Boot Extension Pack扩展功能。

Spring Boot Extension Pack.png

这个扩展功能包包括以下内容。

Spring Boot Extension Pack2.png

使用此方法安装所需工具已完成,作为构建HelloWorld所需的环境搭建已经结束。尝试后发现比预想中简单,但我认为在开发实用的应用程序时,还需要准备其他的DBMS环境。

4. 创建Spring Boot项目

我想立刻创建一个类似HelloWorld的最小项目。

创建一个Java项目

打开侧边栏的“资源管理器”而不打开其他任何东西,屏幕左侧会显示如下图所示的三个按钮。接下来,点击“创建Java项目”按钮,会在主菜单中部打开一个下拉菜单。因为是“选择项目类型”,所以需要选择项目的类型。我们知道Maven、Gradle、JavaFX都是项目类型,但是Quarkus是什么呢?看起来它是一个特定于Kubernetes的Java框架。而MicroProfile则是一种专注于微服务架构的框架,因此可能会创建与此相应的项目。在这里,我们大概会选择最简单的“Spring Boot”。

Select project type.png
Spring boot version.png
Project Language.png
Group ID.png
artifact id.png
Packaging TYpe.png

如果将打包类型设置为Jar,则Spring Boot应用程序将被打包为一个单独的Jar文件。该Jar文件包含了应用程序的所有依赖关系。如果将该Jar作为独立应用程序启动,所有必需的组件将自动加载。在本地环境执行时,还包括Spring Boot Tomcat作为应用程序容器。如果选择将其打包为War,则Tomcat将不包含在内,需要另外准备。也就是说,在本地环境进行开发和执行时,请选择Jar文件,而在部署到生产服务器上时,请选择War文件。

关于选择将其放入 Jar 文件或者 War 文件之间的差异在这一部分有所描述。

 

JDK version.png
Dependency.png
Sucessfully.png
    Spring Boot プロジェクトの作成 – Spring Initializr (pleiades.io)

 

以下的消息將在屏幕右下方顯示。

View Project.png

打开已生成的Spring Boot项目

Open project.png
HelloApplication.png

(1) 先试试运行一下

在 main 方法上方显示了 “运行 | 调试”。一按运行按钮,看起来就像是开始跑了。只用这样会运行吗?
我试一试按下去。然后它开始动了起来…

Run.png
Error Page.png

写一个最简单的Controller类。

看来至少需要掌握基本的编程技能,似乎至少需要自己准备MVC模式中的C(控制器)类。

【参考报道】

 

Controller 类是一个添加了 @Controller 或 @RestController 注解的类。

【Javadoc】中文翻译:文档注释

    • Controller (Spring Framework API) – Javadoc (pleiades.io)

 

    RestController (Spring Framework API) – Javadoc (pleiades.io)

两者的区别似乎在于,@Controller通常用于普通的Web应用控制器,而@RestController用于创建REST API的控制器。这里的“似乎”是因为在文档中找不到明确的描述,可能是因为太过明显而没有写,或者在阅读Spring MVC文档时有相关说明。根据阅读上面的Qiita文章,@RestController可以在不写@ResponseBody的情况下使用,所以如果只是简单使用的话,可以考虑使用@RestController。我马上就写一下看看。

package com.anestec.hello;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloApplicationController {
}

当我再次运行,并没有发生任何变化(404错误)。似乎还需要一些其他的东西。根据上述的博文,在方法上添加 @RequestMapping 注解是必需的。根据官方文档,下面有相关说明。

【参考文献】

    • リクエストマッピング(Spring MVCドキュメント)

 

    Spring Boot 3.0 入門 – リファレンスドキュメント (pleiades.io)

使用@RequestMapping注解可以将请求映射到控制器方法。

@RequestMapping注解提供了“路由”信息。具有特定路径的HTTP请求需要映射到home方法,以便将信息传达给Spring。@RestController注解指示Spring直接将结果字符串返回给调用者。

由于是REST API,因此需要将请求的URL路由映射到类的方法中,我认为这应该是为此目的而存在的注解。
只需创建一个返回”Hello World”的方法来响应根路径(“/”)即可。

package com.anestec.hello;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloApplicationController {

    @RequestMapping("/")  // ルートへこのメソッドをマップする
    public String test() {
        return "Hello World";
    }
}

(3) 再次运行→试试看展示

我将以此状态再次运行一次。

Hello Success.png
HTML source.png
C:\>curl -v localhost:8080
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.83.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 11
< Date: Thu, 16 Feb 2023 06:15:45 GMT
<
Hello World* Connection #0 to host localhost left intact

由于它作为 REST API 运行,所以响应似乎只返回了方法返回的字符串。从 Content-Length:11 可以看出这一点。本来应该在这里返回 JSON 字符串。

进行调试执行试试看

这次让我们从HelloApplication类中按下“Debug”按钮并进行调试运行。在事先设定的返回“Hello World”的地方设立一个断点,并在从浏览器发送请求并在返回响应之前暂停执行。

debug.png
HelloApplicationController.test() (c:\work\hello\src\main\java\com\anestec\hello\HelloApplicationController.java:11)
NativeMethodAccessorImpl.invoke0(Method,Object,Object[])[native method] (不明なソース:-1)
NativeMethodAccessorImpl.invoke(Object,Object[]) (不明なソース:-1)
DelegatingMethodAccessorImpl.invoke(Object,Object[]) (不明なソース:-1)
Method.invoke(Object,Object[]) (不明なソース:-1)
InvocableHandlerMethod.doInvoke(Object[]) (\spring-web-6.0.4.jar\org.springframework.web.method.support\InvocableHandlerMethod.class:207)
InvocableHandlerMethod.invokeForRequest(NativeWebRequest,ModelAndViewContainer,Object[]) (\spring-web-6.0.4.jar\org.springframework.web.method.support\InvocableHandlerMethod.class:152)
ServletInvocableHandlerMethod.invokeAndHandle(ServletWebRequest,ModelAndViewContainer,Object[]) (\spring-webmvc-6.0.4.jar\org.springframework.web.servlet.mvc.method.annotation\ServletInvocableHandlerMethod.class:117)
RequestMappingHandlerAdapter.invokeHandlerMethod(HttpServletRequest,HttpServletResponse,HandlerMethod) (\spring-webmvc-6.0.4.jar\org.springframework.web.servlet.mvc.method.annotation\RequestMappingHandlerAdapter.class:884)
RequestMappingHandlerAdapter.handleInternal(HttpServletRequest,HttpServletResponse,HandlerMethod) (\spring-webmvc-6.0.4.jar\org.springframework.web.servlet.mvc.method.annotation\RequestMappingHandlerAdapter.class:797)
AbstractHandlerMethodAdapter.handle(HttpServletRequest,HttpServletResponse,Object) (\spring-webmvc-6.0.4.jar\org.springframework.web.servlet.mvc.method\AbstractHandlerMethodAdapter.class:87)
DispatcherServlet.doDispatch(HttpServletRequest,HttpServletResponse) (\spring-webmvc-6.0.4.jar\org.springframework.web.servlet\DispatcherServlet.class:1080)
DispatcherServlet.doService(HttpServletRequest,HttpServletResponse) (\spring-webmvc-6.0.4.jar\org.springframework.web.servlet\DispatcherServlet.class:973)
FrameworkServlet.processRequest(HttpServletRequest,HttpServletResponse) (\spring-webmvc-6.0.4.jar\org.springframework.web.servlet\FrameworkServlet.class:1011)
FrameworkServlet.doGet(HttpServletRequest,HttpServletResponse) (\spring-webmvc-6.0.4.jar\org.springframework.web.servlet\FrameworkServlet.class:903)
HttpServlet.service(HttpServletRequest,HttpServletResponse) (\tomcat-embed-core-10.1.5.jar\jakarta.servlet.http\HttpServlet.class:705)
FrameworkServlet.service(HttpServletRequest,HttpServletResponse) (\spring-webmvc-6.0.4.jar\org.springframework.web.servlet\FrameworkServlet.class:885)
HttpServlet.service(ServletRequest,ServletResponse) (\tomcat-embed-core-10.1.5.jar\jakarta.servlet.http\HttpServlet.class:814)
ApplicationFilterChain.internalDoFilter(ServletRequest,ServletResponse) (\tomcat-embed-core-10.1.5.jar\org.apache.catalina.core\ApplicationFilterChain.class:223)
ApplicationFilterChain.doFilter(ServletRequest,ServletResponse) (\tomcat-embed-core-10.1.5.jar\org.apache.catalina.core\ApplicationFilterChain.class:158)
WsFilter.doFilter(ServletRequest,ServletResponse,FilterChain) (\tomcat-embed-websocket-10.1.5.jar\org.apache.tomcat.websocket.server\WsFilter.class:53)
ApplicationFilterChain.internalDoFilter(ServletRequest,ServletResponse) (\tomcat-embed-core-10.1.5.jar\org.apache.catalina.core\ApplicationFilterChain.class:185)
ApplicationFilterChain.doFilter(ServletRequest,ServletResponse) (\tomcat-embed-core-10.1.5.jar\org.apache.catalina.core\ApplicationFilterChain.class:158)
RequestContextFilter.doFilterInternal(HttpServletRequest,HttpServletResponse,FilterChain) (\spring-web-6.0.4.jar\org.springframework.web.filter\RequestContextFilter.class:100)
OncePerRequestFilter.doFilter(ServletRequest,ServletResponse,FilterChain) (\spring-web-6.0.4.jar\org.springframework.web.filter\OncePerRequestFilter.class:116)
ApplicationFilterChain.internalDoFilter(ServletRequest,ServletResponse) (\tomcat-embed-core-10.1.5.jar\org.apache.catalina.core\ApplicationFilterChain.class:185)
ApplicationFilterChain.doFilter(ServletRequest,ServletResponse) (\tomcat-embed-core-10.1.5.jar\org.apache.catalina.core\ApplicationFilterChain.class:158)
FormContentFilter.doFilterInternal(HttpServletRequest,HttpServletResponse,FilterChain) (\spring-web-6.0.4.jar\org.springframework.web.filter\FormContentFilter.class:93)
OncePerRequestFilter.doFilter(ServletRequest,ServletResponse,FilterChain) (\spring-web-6.0.4.jar\org.springframework.web.filter\OncePerRequestFilter.class:116)
ApplicationFilterChain.internalDoFilter(ServletRequest,ServletResponse) (\tomcat-embed-core-10.1.5.jar\org.apache.catalina.core\ApplicationFilterChain.class:185)
ApplicationFilterChain.doFilter(ServletRequest,ServletResponse) (\tomcat-embed-core-10.1.5.jar\org.apache.catalina.core\ApplicationFilterChain.class:158)
CharacterEncodingFilter.doFilterInternal(HttpServletRequest,HttpServletResponse,FilterChain) (\spring-web-6.0.4.jar\org.springframework.web.filter\CharacterEncodingFilter.class:201)
OncePerRequestFilter.doFilter(ServletRequest,ServletResponse,FilterChain) (\spring-web-6.0.4.jar\org.springframework.web.filter\OncePerRequestFilter.class:116)
ApplicationFilterChain.internalDoFilter(ServletRequest,ServletResponse) (\tomcat-embed-core-10.1.5.jar\org.apache.catalina.core\ApplicationFilterChain.class:185)
ApplicationFilterChain.doFilter(ServletRequest,ServletResponse) (\tomcat-embed-core-10.1.5.jar\org.apache.catalina.core\ApplicationFilterChain.class:158)
StandardWrapperValve.invoke(Request,Response) (\tomcat-embed-core-10.1.5.jar\org.apache.catalina.core\StandardWrapperValve.class:177)
StandardContextValve.invoke(Request,Response) (\tomcat-embed-core-10.1.5.jar\org.apache.catalina.core\StandardContextValve.class:97)
AuthenticatorBase.invoke(Request,Response) (\tomcat-embed-core-10.1.5.jar\org.apache.catalina.authenticator\AuthenticatorBase.class:542)
StandardHostValve.invoke(Request,Response) (\tomcat-embed-core-10.1.5.jar\org.apache.catalina.core\StandardHostValve.class:119)
ErrorReportValve.invoke(Request,Response) (\tomcat-embed-core-10.1.5.jar\org.apache.catalina.valves\ErrorReportValve.class:92)
StandardEngineValve.invoke(Request,Response) (\tomcat-embed-core-10.1.5.jar\org.apache.catalina.core\StandardEngineValve.class:78)
CoyoteAdapter.service(Request,Response) (\tomcat-embed-core-10.1.5.jar\org.apache.catalina.connector\CoyoteAdapter.class:357)
Http11Processor.service(SocketWrapperBase) (\tomcat-embed-core-10.1.5.jar\org.apache.coyote.http11\Http11Processor.class:400)
AbstractProcessorLight.process(SocketWrapperBase,SocketEvent) (\tomcat-embed-core-10.1.5.jar\org.apache.coyote\AbstractProcessorLight.class:65)
AbstractProtocol$ConnectionHandler.process(SocketWrapperBase,SocketEvent) (\tomcat-embed-core-10.1.5.jar\org.apache.coyote\AbstractProtocol.class:859)
NioEndpoint$SocketProcessor.doRun() (\tomcat-embed-core-10.1.5.jar\org.apache.tomcat.util.net\NioEndpoint.class:1734)
SocketProcessorBase.run() (\tomcat-embed-core-10.1.5.jar\org.apache.tomcat.util.net\SocketProcessorBase.class:52)
ThreadPoolExecutor.runWorker(ThreadPoolExecutor$Worker) (\tomcat-embed-core-10.1.5.jar\org.apache.tomcat.util.threads\ThreadPoolExecutor.class:1191)
ThreadPoolExecutor$Worker.run() (\tomcat-embed-core-10.1.5.jar\org.apache.tomcat.util.threads\ThreadPoolExecutor.class:659)
TaskThread$WrappingRunnable.run() (\tomcat-embed-core-10.1.5.jar\org.apache.tomcat.util.threads\TaskThread.class:61)
Thread.run() (不明なソース:-1)

我不是非常追求,但只需看一下最后一次调用 InvocableHandlerMethod#doInvoke(Object[])。

public class InvocableHandlerMethod extends HandlerMethod {
    
    // (中略)
    
    /**
     * Invoke the handler method with the given argument values.
     */
    @Nullable
    protected Object doInvoke(Object... args) throws Exception {
        Method method = getBridgedMethod();
        try {
            if (KotlinDetector.isSuspendingFunction(method)) {
                return invokeSuspendingFunction(method, getBean(), args);
            }
            return method.invoke(getBean(), args); // ←ここから呼ばれている
        }
        catch (IllegalArgumentException ex) {
            assertTargetBean(method, getBean(), args);
            String text = (ex.getMessage() == null || ex.getCause() instanceof NullPointerException) ?
                    "Illegal argument" : ex.getMessage();
            throw new IllegalStateException(formatInvokeError(text, args), ex);
        }
        catch (InvocationTargetException ex) {
            // Unwrap for HandlerExceptionResolvers ...
            Throwable targetException = ex.getCause();
            if (targetException instanceof RuntimeException runtimeException) {
                throw runtimeException;
            }
            else if (targetException instanceof Error error) {
                throw error;
            }
            else if (targetException instanceof Exception exception) {
                throw exception;
            }
            else {
                throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
            }
        }
    }

使用反射可以看到调用了这个方法。似乎也可以看到传入了参数。我想,如果在URL上添加GET参数,它们应该会传递到这里。让我们进一步追溯栈。

public class InvocableHandlerMethod extends HandlerMethod {
   
    // (中略)
    
    @Nullable
    public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
            Object... providedArgs) throws Exception {
    
        Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); // ←引数はここで取得しているらしい
        if (logger.isTraceEnabled()) {
            logger.trace("Arguments: " + Arrays.toString(args));
        }
        return doInvoke(args); // ←ここで呼出し
    }

(5) 尝试传递 GET 参数

要是这样的话,我们试试看吧…使用 @RequestParam 注解应该能够获取参数。

package com.anestec.hello;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloApplicationController {

	@RequestMapping("/")
    public String test(@RequestParam("id") String id) { // 引数 id を貰ってみる。
        return "Hello World id=[" + id + "]";
    }
}
Get Parameter.png
C:\>curl -v localhost:8080?id=12345
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /?id=12345 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.83.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 22
< Date: Thu, 16 Feb 2023 07:20:16 GMT
<
Hello World id=[12345]* Connection #0 to host localhost left intact

不需要设置POM吗?

这次完全没有手动修改pom.xml的必要。似乎已经反映了在项目最初生成时设定的内容。为了确认一下内容,出现了以下这样的情况。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.0.2</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.anestec</groupId>
	<artifactId>hello</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>hello</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

7. 其他要注意的要点。

(1) 春季启动仪表盘

image.png

这个信息整理得很清晰,感觉很好。

(2) 调试期间显示变量

image.png

8. 总结

所以,我們總結到這裡就好了。

JDK17 以上をあらかじめ入れて置けば、VS Code + 拡張機能で Spring Boot の最小限のアプリケーション(今回はREST API)はほぼあっという間に作れる。

具体的には、以下をやるだけで良い。

拡張機能 Extension Pack for Java をインストール
拡張機能 Spring Boot Extension Pack をインストール
Explorer の「Create Java Project」からプロンプトに従って必要事項を入力する。
Controller クラスを作成する。
Application クラスのmainメソッドから Run、または Debug で実効する。

VSCode のSpring開発体験は全体的によく洗練されていて、Eclipse みたいにゴチャゴチャしていない(個人の感想です)

所以,我得到的印象是,使用VSCode进行Spring开发完全没有问题。我认为未来使用Docker容器和WSL进行Java开发的趋势会增加,从这个角度来看,使用VSCode进行开发将比Eclipse更有优势。我认为未来它将成为主流。

如果有时间的话,我也想尝试使用Thymeleaf进行界面开发。非常感谢您阅读到这里。

广告
将在 10 秒后关闭
bannerAds