Day 13: Dropwizard —— 非常棒的Java REST服务器栈

本文涉及的产品
云数据库 MongoDB,独享型 2核8GB
推荐场景:
构建全方位客户视图
简介: 我们发现了比较有趣的系列文章《30天学习30种新技术》,准备翻译,一天一篇更新,年终礼包。下面是第十三天的内容。我已经是一个使用了8年Java的软件开发人员了,我写过的大多数应用程序是用的Spring框架或Java EE。最近,我花了一些时间学习用Python进行web开发,其中印象非常深刻的是 Flask 框架——一个微型架构,这使得它很容易写REST后端。所以今天我决定找一个Java的Python Flask框架替代品,做一些研究后,我发现 Dropwizard 框架可以帮助达到Flask框架同样的生产力。在这篇博客中,我们将学习如何使用Dropwizard构建一个基于REST的Jav

编者注:我们发现了比较有趣的系列文章《30天学习30种新技术》,准备翻译,一天一篇更新,年终礼包。下面是第十三天的内容。


我已经是一个使用了8年Java的软件开发人员了,我写过的大多数应用程序是用的Spring框架或Java EE。最近,我花了一些时间学习用Python进行web开发,其中印象非常深刻的是 Flask 框架——一个微型架构,这使得它很容易写REST后端。所以今天我决定找一个Java的Python Flask框架替代品,做一些研究后,我发现 Dropwizard 框架可以帮助达到Flask框架同样的生产力。在这篇博客中,我们将学习如何使用Dropwizard构建一个基于REST的Java MongoDB应用程序。

image.png


什么是Dropwizard?

Dropwizard 是一个开源的Java框架,用于开发OPS友好、高性能的基于REST的后端。它是由Yammer开发的,来驱动基于JVM的后端。


Dropwizard提供同类最佳的Java库到一个嵌入式应用程序包。它由以下部分组成:

  1. 嵌入式Jetty:每一个应用程序被打包成一个jar(而不是war)文件,并开始自己的嵌入式Jetty容器。没有任何war文件和外部servlet容器。
  2. JAX-RS:Jersey(JAX-RS的参考实现)是用来写基于REST的Web服务的。
  3. JSON:REST服务用的是JSON,Jackson库用来做所有的JSON处理。
  4. 日志:使用Logback和SLF4J完成。
  5. Hibernate验证:Dropwizard使用Hibernate验证API进行声明性验证。
  6. 指标:Dropwizard支持监控使用标准库,它在监控代码方面有无与伦比的洞察力。

除了上面提到的这几个,Dropwizard还使用了一些其他的库,你可以在这里找到完整的列表


为什么是Dropwizard?

我决定学Dropwizard的原因有以下几点:

  1. 快速的项目引导:如果你已经在使用Spring和Java EE,你就会明白开发人员在引导项目时的痛苦。使用Dropwizard,你只需要在你的 pom.xml 文件中添加一个依赖就完成了。
  2. 应用指标:Dropwizard自带应用程序指标的支持。它提供了类似请求/响应时间这种非常有用的信息,只要把@ 定时注解来获取方法的执行时间。
  3. 生产力:每个Dropwizard应用程序有一个启动Jetty容器的主程序。这意味着,完全可以把应用程序作为一个主程序在IDE中运行和调试。所以就没有重新编译或部署war文件。

Github库

今天的演示应用程序的代码在GitHub上有:day13-dropwizard-mongodb-demo-app


必备条件

  1. 基础的Java知识是必须的;
  2. 下载并安装 MongoDB数据库
  3. 安装最新版本的Java Development Kit (JDK),OpenJDK 7 或是 Oracle JDK 7 都可以,这篇文章中使用JDK 7;
  4. Eclipse官网下载最新版本的Eclipse包,就目前而言eclipse最新版的代号是Kepler;

image.png

Eclipse的安装很容易,只需要解压下载下来的包即可。如果是在Linux或者Mac机器上,开个命令行窗口,输入如下命令:

$ tar -xzvf eclipse-jee-kepler-R-*.tar.gz

Windows下,你解压到哪里,那里就会有一个eclipse文件夹,这样就可以直接操作了,当然你也可以创建执行文件的快捷方式到桌面。

第1步:创建一个新的Maven项目

打开Eclipse IDE,然后到项目工作区(project workspace)。要创建一个新的项目,转到 文件>新建> Maven项目 (File > New > Maven Project) ,然后选择 Maven 原型 - 快速启动 (maven-archetype-quickstart),然后进入Ground IdArtifact Id,最后点击“完成”。

image.png

第2步:更新pom.xml

现在更新pom.xml文件以包括dropwizard核心maven依赖。同时也将更新Maven项目使用Java 1.7版本,更新pom.xml文件后,更新Maven项目(右键单击>Maven>更新项目)

<projectxmlns="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 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<groupId>com.shekhar</groupId>

<artifactId>blog</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>blog</name>

<url>http://maven.apache.org</url>

<properties>

   <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<dependencies>

   <dependency>

       <groupId>com.yammer.dropwizard</groupId>

       <artifactId>dropwizard-core</artifactId>

       <version>0.6.2</version>

   </dependency>

</dependencies>

<build>

   <plugins>

       <plugin>

           <groupId>org.apache.maven.plugins</groupId>

           <artifactId>maven-compiler-plugin</artifactId>

           <version>3.1</version>

           <configuration>

               <source>1.7</source>

               <target>1.7</target>

           </configuration>

       </plugin>

   </plugins>

</build>


第3步:创建配置类

每个Dropwizard应用程序都有一个配置类,它指定特定的环境参数。文章后面会将如主机、端口和数据库名之类的MongoDB的配置参数添加给它。这个类扩展了 com.yammer.dropwizard.config.Configuration类。

import com.yammer.dropwizard.config.Configuration;

public classBlogConfigurationextendsConfiguration{

}


第4步:创建服务类

该Dropwizard项目由一个服务类自举。这个类将各种提供基本功能的捆绑和命令集合在一块,它还启动嵌入式Jetty服务器并延伸com.yammer.dropwizard.Service

import com.yammer.dropwizard.Service;

import com.yammer.dropwizard.config.Bootstrap;

import com.yammer.dropwizard.config.Environment;

publicclassBlogServiceextendsService<BlogConfiguration> {

   publicstaticvoidmain(String[] args)throws Exception {

       new BlogService().run(new String[] { "server" });

   }

   @Override

   publicvoidinitialize(Bootstrap<BlogConfiguration> bootstrap) {

       bootstrap.setName("blog");

   }

   @Override

   publicvoidrun(BlogConfiguration configuration, Environment environment)throws Exception {

   }

}

上面的这些服务类可以:

  1. 有一个作为服务入口点的main方法。在main方法里面,创建BlogService的实例,并调用run方法。我们将服务器命令作为参数传递,服务器命令将启动嵌入式Jetty服务器。
  2. 初始化方法在服务运行方法之前被调用。
  3. 接下来,服务运行时将调用它的run方法,文章后面会将JAX-RS源加到这个方法里。


第5步:写IndexResource

写一个当GET请求指向“/” URL时会被调用的源,创建一个新的JAX-RS源(此资源将列出所有的博客),如下:

import java.util.Arrays;

import java.util.List;

import javax.ws.rs.GET;

import javax.ws.rs.Path;

import javax.ws.rs.Produces;

import javax.ws.rs.core.MediaType;

import com.yammer.metrics.annotation.Timed;

@Path("/")

publicclassIndexResource {

   @GET

   @Produces(value = MediaType.APPLICATION_JSON)

   @Timed

   public List<Blog> index() {

       return Arrays.asList(newBlog("Day 12: OpenCV--Face Detection for Java Developers",

               "https://www.openshift.com/blogs/day-12-opencv-face-detection-for-java-developers"));

   }

}

上面这段代码是一个标准的JAX-RS资源类。它添加@ Path注释和定义index()方法,这个index()会返回一个博客集合,这些博客将被转换为JSON文档。

上面提到IndexResource是用博客表示的。下面这段则表明该博客使用Hibernate验证器注解,以确保内容是有效的。例如,使用@URL注释,以确保只有合法的URL存储在MongoDB数据库。

import java.util.Date;

import java.util.UUID;

import org.hibernate.validator.constraints.NotBlank;

import org.hibernate.validator.constraints.URL;

publicclassBlog {

   privateString id = UUID.randomUUID().toString();

   @NotBlank

   privateString title;

   @URL

   @NotBlank

   privateString url;

   private final Date publishedOn = newDate();

   publicBlog() {

   }

   publicBlog(String title, String url) {

       super();

       this.title = title;

       this.url = url;

   }

   publicStringgetId() {

       return id;

   }

   publicStringgetTitle() {

       return title;

   }

   publicStringgetUrl() {

       return url;

   }

   publicDategetPublishedOn() {

       return publishedOn;

   }

}

接下来,在服务类的run方法注册IndexResource。用下面的方式更新BlogService run方法。

@Override

publicvoidrun(BlogConfiguration configuration, Environment environment)throws Exception {

  environment.addResource(new IndexResource());

}

现在,可以将BlogService类作为一个主程序来运行(右键点击>运行方式> Java应用程序),这将启动嵌入式Jetty容器,我们可以看到程序在 http://localhost:8080/ 里运行。

$ curl http://localhost:8080

[{"id":"9bb43d53-5436-4dac-abaa-ac530c833df1","title":"Day 12: OpenCV--Face Detection for Java Developers","url":"https://www.openshift.com/blogs/day-12-opencv-face-detection-for-java-developers","publishedOn":1384090975372}]

现在可以通过点击“指标(Metrics)”检查IndexResource的指标,该数据是可用的JSON格式。

"com.shekhar.blog.IndexResource":{

   "index":{

     "type":"timer",

     "duration":{

       "unit":"milliseconds",

       "min":17.764,

       "max":17.764,

       "mean":17.764,

       "std_dev":0.0,

       "median":17.764,

       "p75":17.764,

       "p95":17.764,

       "p98":17.764,

       "p99":17.764,

       "p999":17.764

     },

     "rate":{

       "unit":"seconds",

       "count":1,

       "mean":7.246537731991882E-4,

       "m1":2.290184897291144E-12,

       "m5":3.551918562683463E-5,

       "m15":2.445031498756583E-4

     }

   }

 },


第6步:配置MongoDB

pom.xml 里加入 mongo-jackson-mapper 的依赖。

<dependency>

   <groupId>net.vz.mongodb.jackson</groupId>

   <artifactId>mongo-jackson-mapper</artifactId>

   <version>1.4.2</version>

</dependency>

用MongoDB数据库的详细信息(如主机、端口和数据库名等)更新BlogConfiguration类。

importjavax.validation.constraints.Max;

importjavax.validation.constraints.Min;

importorg.codehaus.jackson.annotate.JsonProperty;

importorg.hibernate.validator.constraints.NotEmpty;

importcom.yammer.dropwizard.config.Configuration;

publicclassBlogConfigurationextendsConfiguration {

   @JsonProperty

   @NotEmpty

   public String mongohost = "localhost";

   @JsonProperty

   @Min(1)

   @Max(65535)

   public int mongoport = 27017;

   @JsonProperty

   @NotEmpty

   public String mongodb = "mydb";

}

接下来,创建一个名为MongoManaged的新类,它将允许你在应用程序启动和停止时管理程序资源。这样就实现了com.yammer.dropwizard.lifecycle.Managed

import com.mongodb.Mongo;

import com.yammer.dropwizard.lifecycle.Managed;

publicclassMongoManagedimplementsManaged {

   private Mongo mongo;

   publicMongoManaged(Mongo mongo) {

       this.mongo = mongo;

   }

   @Override

   publicvoidstart()throws Exception {

   }

   @Override

   publicvoidstop()throws Exception {

       mongo.close();

   }

}

在上面的代码中,关闭了stop方法中的MongoDB连接。

下一步,写一个MongoHealthCheck来检查MongoDB的连接与否。

import com.mongodb.Mongo;

import com.yammer.metrics.core.HealthCheck;

public classMongoHealthCheckextendsHealthCheck {

   privateMongo mongo;

   protectedMongoHealthCheck(Mongo mongo) {

       super("MongoDBHealthCheck");

       this.mongo = mongo;

   }

   @Override

   protectedResult check() throwsException {

       mongo.getDatabaseNames();

       returnResult.healthy();

   }

}

现在,更新BlogService类,将MongoDB的配置包含进来。

package com.shekhar.blog;

import com.mongodb.Mongo;

import com.yammer.dropwizard.Service;

import com.yammer.dropwizard.config.Bootstrap;

import com.yammer.dropwizard.config.Environment;

publicclassBlogServiceextendsService<BlogConfiguration> {

   publicstaticvoidmain(String[] args)throws Exception {

       newBlogService().run(newString[] { "server" });

   }

   @Override

   publicvoidinitialize(Bootstrap<BlogConfiguration> bootstrap) {

       bootstrap.setName("blog");

   }

   @Override

   publicvoidrun(BlogConfiguration configuration, Environment environment)throws Exception {

       Mongomongo=newMongo(configuration.mongohost, configuration.mongoport);

       MongoManagedmongoManaged=newMongoManaged(mongo);

       environment.manage(mongoManaged);

       environment.addHealthCheck(newMongoHealthCheck(mongo));

       environment.addResource(newIndexResource());

   }

}

上面这段代码:

  1. 使用BlogConfiguration对象创建了一个新的Mongo实例。
  2. 一个新的MongoManaged实例被创建并添加到环境中。
  3. 健康检查被添加。

运行该应用程序作为主程序。你可以到本地的 http://localhost:8081/healthcheck 健康检查页面去检验MongoDB是否在运行,如果MongoDB没有运行,会看到一个异常堆栈跟踪。

! MongoDBHealthCheck: ERROR

!  can't call something : Shekhars-MacBook-Pro.local/192.168.1.101:27017/admin

com.mongodb.MongoException$Network: can't call something : Shekhars-MacBook-Pro.local/192.168.1.101:27017/admin

   at com.mongodb.DBTCPConnector.call(DBTCPConnector.java:227)

   at com.mongodb.DBApiLayer$MyCollection.__find(DBApiLayer.java:305)

   at com.mongodb.DB.command(DB.java:160)

   at com.mongodb.DB.command(DB.java:183)

   at com.mongodb.Mongo.getDatabaseNames(Mongo.java:327)

   at com.shekhar.blog.MongoHealthCheck.check(MongoHealthCheck.java:17)

   at com.yammer.metrics.core.HealthCheck.execute(HealthCheck.java:195)

   at

Caused by: java.io.IOException: couldn't connect to[Shekhars-MacBook-Pro.local/192.168.1.101:27017] bc:java.net.ConnectException: Connection refused

   at com.mongodb.DBPort._open(DBPort.java:228)

   at com.mongodb.DBPort.go(DBPort.java:112)

   at com.mongodb.DBPort.call(DBPort.java:79)

   at com.mongodb.DBTCPConnector.call(DBTCPConnector.java:218)

   ... 33 more

* deadlocks: OK

现在启动MongoDB,可以看到:

* MongoDBHealthCheck: OK

* deadlocks: OK

第7步:创建BlogResource

现在写BlogResource类,它负责创建博客条目。

import java.util.ArrayList;

import java.util.List;

import javax.validation.Valid;

import javax.ws.rs.Consumes;

import javax.ws.rs.GET;

import javax.ws.rs.POST;

import javax.ws.rs.Path;

import javax.ws.rs.Produces;

import javax.ws.rs.core.MediaType;

import javax.ws.rs.core.Response;

import net.vz.mongodb.jackson.DBCursor;

import net.vz.mongodb.jackson.JacksonDBCollection;

import com.yammer.metrics.annotation.Timed;

@Path("/blogs")

@Produces(value = MediaType.APPLICATION_JSON)

@Consumes(value = MediaType.APPLICATION_JSON)

publicclassBlogResource {

   private JacksonDBCollection<Blog, String> collection;

   publicBlogResource(JacksonDBCollection<Blog, String> blogs) {

       this.collection = blogs;

   }

   @POST

   @Timed

   public Response publishNewBlog(@Valid Blog blog) {

       collection.insert(blog);

       return Response.noContent().build();

   }

}

下一步,更新BlogService run方法,将BlogResource也加进来。

@Override

   public void run(BlogConfiguration configuration, Environment environment) throws Exception {

       Mongo mongo = newMongo(configuration.mongohost, configuration.mongoport);

       MongoManaged mongoManaged = newMongoManaged(mongo);

       environment.manage(mongoManaged);

       environment.addHealthCheck(new MongoHealthCheck(mongo));

       DB db = mongo.getDB(configuration.mongodb);

       JacksonDBCollection<Blog, String> blogs = JacksonDBCollection.wrap(db.getCollection("blogs"), Blog.class, String.class);

       environment.addResource(new IndexResource());

       environment.addResource(new BlogResource(blogs));

   }

将BlogService类作为一个Java应用程序运行。为了测试BlogResource,做一个curl请求:

$ curl -i -X POST -H"Content-Type: application/json" -d '{"title":"Day 12: OpenCV--Face Detection for Java Developers","url":"https://www.openshift.com/blogs/day-12-opencv-face-detection-for-java-developers"}' http://localhost:8080/blogs

HTTP/1.1 204 No Content

Date: Sun, 10 Nov 2013 14:08:03 GMT

Content-Type: application/json

第8步:更新IndexResource

现在,更新IndexResource index()方法来从MongoDB获取所有的博客文件。

import java.util.ArrayList;

import java.util.List;

import javax.ws.rs.GET;

import javax.ws.rs.Path;

import javax.ws.rs.Produces;

import javax.ws.rs.core.MediaType;

import net.vz.mongodb.jackson.DBCursor;

import net.vz.mongodb.jackson.JacksonDBCollection;

import com.yammer.metrics.annotation.Timed;

@Path("/")

publicclassIndexResource {

   private JacksonDBCollection<Blog, String> collection;

   publicIndexResource(JacksonDBCollection<Blog, String> blogs) {

       this.collection = blogs;

   }

   @GET

   @Produces(value = MediaType.APPLICATION_JSON)

   @Timed

   public List<Blog> index() {

       DBCursor<Blog> dbCursor = collection.find();

       List<Blog> blogs = newArrayList<>();

       while (dbCursor.hasNext()) {

           Blogblog= dbCursor.next();

           blogs.add(blog);

       }

       return blogs;

   }

}

更新BlogService run方法将博客集合传递给IndexResource。

@Override

   public void run(BlogConfiguration configuration, Environment environment) throws Exception {

       Mongo mongo = newMongo(configuration.mongohost, configuration.mongoport);

       MongoManaged mongoManaged = newMongoManaged(mongo);

       environment.manage(mongoManaged);

       environment.addHealthCheck(new MongoHealthCheck(mongo));

       DB db = mongo.getDB(configuration.mongodb);

       JacksonDBCollection<Blog, String> blogs = JacksonDBCollection.wrap(db.getCollection("blogs"), Blog.class, String.class);

       environment.addResource(new IndexResource(blogs));

       environment.addResource(new BlogResource(blogs));

   }

将BlogService类作为一个Java应用程序运行。为了测试BlogResource,做一个curl请求:

$ curl http://localhost:8080

[{"id":"527f9806300462bbd300687e","title":"Day 12: OpenCV--Face Detection for Java Developers","url":"https://www.openshift.com/blogs/day-12-opencv-face-detection-for-java-developers","publishedOn":1384093702592}]

第9步:部署到云端

这里有一篇文章,教你如何在OpenShift部署Dropwizard应用,点击这里


相关实践学习
MongoDB数据库入门
MongoDB数据库入门实验。
快速掌握 MongoDB 数据库
本课程主要讲解MongoDB数据库的基本知识,包括MongoDB数据库的安装、配置、服务的启动、数据的CRUD操作函数使用、MongoDB索引的使用(唯一索引、地理索引、过期索引、全文索引等)、MapReduce操作实现、用户管理、Java对MongoDB的操作支持(基于2.x驱动与3.x驱动的完全讲解)。 通过学习此课程,读者将具备MongoDB数据库的开发能力,并且能够使用MongoDB进行项目开发。 &nbsp; 相关的阿里云产品:云数据库 MongoDB版 云数据库MongoDB版支持ReplicaSet和Sharding两种部署架构,具备安全审计,时间点备份等多项企业能力。在互联网、物联网、游戏、金融等领域被广泛采用。 云数据库MongoDB版(ApsaraDB for MongoDB)完全兼容MongoDB协议,基于飞天分布式系统和高可靠存储引擎,提供多节点高可用架构、弹性扩容、容灾、备份回滚、性能优化等解决方案。 产品详情: https://www.aliyun.com/product/mongodb
相关文章
|
3月前
|
Java
java小工具util系列5:java文件相关操作工具,包括读取服务器路径下文件,删除文件及子文件,删除文件夹等方法
java小工具util系列5:java文件相关操作工具,包括读取服务器路径下文件,删除文件及子文件,删除文件夹等方法
113 9
|
2月前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
93 5
|
3月前
|
存储 算法 Java
🧠Java零基础 - Java栈(Stack)详解
【10月更文挑战第17天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
96 2
|
4月前
|
Java Linux
java读取linux服务器下某文档的内容
java读取linux服务器下某文档的内容
55 3
java读取linux服务器下某文档的内容
|
4月前
|
运维 Java Linux
【运维基础知识】Linux服务器下手写启停Java程序脚本start.sh stop.sh及详细说明
### 启动Java程序脚本 `start.sh` 此脚本用于启动一个Java程序,设置JVM字符集为GBK,最大堆内存为3000M,并将程序的日志输出到`output.log`文件中,同时在后台运行。 ### 停止Java程序脚本 `stop.sh` 此脚本用于停止指定名称的服务(如`QuoteServer`),通过查找并终止该服务的Java进程,输出操作结果以确认是否成功。
158 1
|
4月前
|
分布式计算 资源调度 Hadoop
大数据-01-基础环境搭建 超详细 Hadoop Java 环境变量 3节点云服务器 2C4G XML 集群配置 HDFS Yarn MapRedece
大数据-01-基础环境搭建 超详细 Hadoop Java 环境变量 3节点云服务器 2C4G XML 集群配置 HDFS Yarn MapRedece
150 4
|
4月前
|
Java Shell Maven
Flink-11 Flink Java 3分钟上手 打包Flink 提交任务至服务器执行 JobSubmit Maven打包Ja配置 maven-shade-plugin
Flink-11 Flink Java 3分钟上手 打包Flink 提交任务至服务器执行 JobSubmit Maven打包Ja配置 maven-shade-plugin
207 4
|
4月前
|
存储 安全 Java
【用Java学习数据结构系列】探索栈和队列的无尽秘密
【用Java学习数据结构系列】探索栈和队列的无尽秘密
48 2
|
3天前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
39 14
|
6天前
|
安全 Java 程序员
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
36 13

热门文章

最新文章