前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >jdk下httpserver源码解析

jdk下httpserver源码解析

原创
作者头像
不会飞的小鸟
修改2020-04-07 11:22:33
5240
修改2020-04-07 11:22:33
举报
文章被收录于专栏:只为你下

  在写这篇博客之前我查了很久发现全网都没有一篇写httpserver源码解析的

  

  所以今天就由我来为大家解析一下httpserver的源码。(这里我会去掉其中的https部分的源码,只讲http部分,对httpserver中https的实现感兴趣的读者可以尝试自己去阅读,这部分并不复杂)

  

  第一次在没有参考资料的情况下写这么长一篇源码解析,可能会有很多错误和讲不清楚的地方,希望大家尽量指出来。

  

  大家最好先跟着我构建这样一个小demo,跑起来之后再一步一步去看源码

  

  /**

  

  * @author 肥宅快乐码

  

  */

  

  public class HttpServerSample {

  

  private static void serverStart() throws IOException {

  

  HttpServerProvider provider = HttpServerProvider.provider();

  

  // 监听端口8080,连接排队队列,如果队列中的连接超过这个数的话就会拒绝连接

  

  HttpServer httpserver =provider.createHttpServer(new InetSocketAddress(8080), 100);

  

  // 监听路径为RestSample,请求处理后回调RestGetHandler里的handle方法

  

  httpserver.createContext("/RestSample", new RestGetHandler());

  

  // 管理工作线程池

  

  ExecutorService executor = new ThreadPoolExecutor(10,200,60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new ThreadPoolExecutor.AbortPolicy());

  

  httpserver.setExecutor(executor);

  

  httpserver.start();

  

  System.out.println("server started");

  

  }

  

  public static void main(String[] args) throws IOException {

  

  serverStart();

  

  }

  

  }

  

  /**

  

  * 回调类,里面的handle方法主要完成将包装好的请求头返回给客户端的功能

  

  */

  

  class RestGetHandler implements HttpHandler {

  

  @Override

  

  public void handle(HttpExchange he) throws IOException {

  

  String requestMethod = he.getRequestMethod();

  

  // 如果是get方法

  

  if ("GET".equalsIgnoreCase(requestMethod)) {

  

  // 获取响应头,接下来我们来设置响应头信息

  

  Headers responseHeaders = he.getResponseHeaders();

  

  // 以json形式返回,其他还有text/html等等

  

  responseHeaders.set("Content-Type", "application/json");

  

  // 设置响应码200和响应body长度,这里我们设0,没有响应体

  

  he.sendResponseHeaders(200, 0);

  

  // 获取响应体

  

  OutputStream responseBody = he.getResponseBody();

  

  // 获取请求头并打印

  

  Headers requestHeaders = he.getRequestHeaders();

  

  Set<String> keySet = requestHeaders.keySet();

  

  Iterator<String> iter = keySet.iterator();

  

  while (iter.hasNext()) {

  

  String key = iter.next();

  

  List values = requestHeaders.get(key);

  

  String s = key + " = " + values.toString() + "\r\n";

  

  responseBody.write(s.getBytes());

  

  }

  

  // 关闭输出流

  

  responseBody.close();

  

  }

  

  }

  

  }

  

  ① 最开始我们通过 HttpServerProvider provider = HttpServerProvider.provider(); 创建了一个HttpServerProvider,也就是这里的DefaultHttpServerProvide

  

  // HttpServerProvider.java

  

  public static HttpServerProvider provider () {

  

  // 这里我们删掉了其他部分,只留下172、173两行

  

  // 这里创建了一个DefaultHttpServerProvide

  

  provider = new sun.net.httpserver.DefaultHttpServerProvider();

  

  return provider;

  

  }

  

  ② 之后我们调用 HttpServer httpserver =provider.createHttpServer(new InetSocketAddress(8080), 100); ,

  

  也就是调用了DefaultHttpServerProvider的createHttpServer创建一个HttpServerImpl,当然这里也可以用createHttpsServer创建一个HttpsServerImpl,但是前面说了我们这篇不分析https,所以这里忽略了createHttpsServer方法

  

  还有这里创建ServerImpl的构造方法我们暂时不讲,留到后面再讲

  

  // DefaultHttpServerProvider.java

  

  public HttpServer createHttpServer (InetSocketAddress addr, int backlog) throws IOException {

  

  return new HttpServerImpl (addr, backlog);

  

  }

  

  // HttpServerImpl.java

  

  HttpServerImpl (

  

  InetSocketAddress addr, int backlog

  

  ) throws IOException {

  

  server = new ServerImpl (this, "http", addr, backlog);

  

  }

  

  ③ 接下来我们创建了一个监听路径 httpserver.createContext("/RestSample", new RestGetHandler());

  

  // HttpServer.java

  

  public abstract HttpContext createContext (String path, HttpHandler handler) ;

  

  // HttpContextImpl.java

  

  public HttpContextImpl createContext (String path, HttpHandler handler) {

  

  // 这里调用的server是ServerImpl类的对象

  

  return server.createContext (path, handler);

  

  }

  

  这里成功返回了一个HttpContextImpl对象,这个我们后面会说,这里我们要知道的是,HttpServerImpl调用的是ServerImpl的实现

  

  到这里我们差不多可以聊一下httpserver的主要结构了:

  

  HttpServer是这里的祖先类,它是一个抽象类,抽象了一个HttpServer应该有的方法

  

  而HttpsServer和我们想象的不一样,它和HttpServer不是平行关系,而是HttpServer的子类,它在HttpServer的基础上加了setHttpsConfigurator和getHttpsConfigurator这两个方法而已

  

  HttpServerImpl和HttpsServerImpl虽然都是实现类,但是它们的方法都是调用ServerImpl的方法,都是围绕ServerImpl的

  

  所以我们也可以把ServerImpl看做这个项目的核心类

  

  ④ 之后设置一下工作线程池,初始化任务就完成了

  

  ExecutorService executor = new ThreadPoolExecutor(10,200,60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new ThreadPoolExecutor.AbortPolicy());

  

  httpserver.setExecutor(executor);

  

  httpserver.start();

  

  启动自然和我们刚刚聊的结构一样都是从HttpServer开始一层调一层调用到ServerImpl的方法的:

  

  // HttpServer.java

  

  public abstract void start () ;

  

  // HttpServerImpl.java

  

  public void start () {

  

  server.start();

  

  }

  

  // ServerImpl.java

  

  public void start () {

  

  // server未绑定端口或处于已启动或已关闭状态

  

  // 顺便先提一下,这里就可以留意到,ServerImpl作为一个核心类,管理了各种各样的状态(state)等

  

  if (!bound || started || finished) {

  

  throw new IllegalStateException ("server in wrong state");

  

  }

  

  // 如果没有设置线程池,那就用默认的,默认的话等于没有用线程池,是直接execute的,所以尽可能直接创建线程池

  

  if (executor == null) {

  

  executor = new DefaultExecutor();

  

  }

  

  // 创建了一个Dispatcher线程,用来分发任务,如Accept或者Readable

  

  Thread t = new Thread (dispatcher);

  

  // 设置一下状态

  

  started = true;

  

  // 运行线程

  

  t.start();

  

  }

  

  前面我们说过,ServerImpl是这整个项目的核心部分,它管理了httpserver的状态,提供了各种接口以及通用的方法,它也负责了几个内部类线程的启动

  

  所以,接下来我们会分为ServerImpl、Dispatcher、Exchange、ServerTimerTask与ServerTimerTask1四个部分来讲解

  

  (https相关的我去掉了)

  

  比较长,大家稍微过一眼有个印象,之后遇到的时候再回来看就行

  

  // http或https

  

  private String protocol;

  

  private Executor executor;

  

  // 负责接收连接用的类(这个本来在209行附近,我把它提上来了)

  

  private Dispatcher dispatcher;

  

  // ContextList这个类只是封装了一个List<HttpContextImpl>及一些方法,如限制监听的context(路径)的数目和查找context的方法

  

  private ContextList contexts;

  

  private InetSocketAddress address;

  

  // nio相关的那些类

  

  private ServerSocketChannel schan;

  

  private Selector selector;

  

  private SelectionKey listenerKey;

  

  // 负责管理之前提到的idle连接,也就是长连接的set

  

  // 长连接时,连接如果没有任务,就加进去. 如果超过一定时间没有任务,则主动断开长连接

  

  private Set<HttpConnection> idleConnections;

  

  // 管理所有的连接,方便在stop等情况下直接断开所有连接

  

  private Set<HttpConnection> allConnections;

  

  // 管理req连接和rsp连接,防止请求或响应超时,超时时由定时线程断开连接

  

  private Set<HttpConnection> reqConnections;

  

  private Set<HttpConnection> rspConnections;

  

  // 这两个之后6.4的Exchange的addEvent方法部分我们再说

  

  private List<Event> events;

  

  private final Object loLock 在写这篇博客之前我查了很久发现全网都没有一篇写httpserver源码解析的

所以今天就由我来为大家解析一下httpserver的源码。(这里我会去掉其中的https部分的源码,只讲http部分,对httpserver中https的实现感兴趣的读者可以尝试自己去阅读,这部分并不复杂)

第一次在没有参考资料的情况下写这么长一篇源码解析,可能会有很多错误和讲不清楚的地方,希望大家尽量指出来。

本文链接 https://www.cnblogs.com/fatmanhappycode/p/12614428.html

大家最好先跟着我构建这样一个小demo,跑起来之后再一步一步去看源码

/**

* @author 肥宅快乐码

*/

public class HttpServerSample {

private static void serverStart() throws IOException {

HttpServerProvider provider = HttpServerProvider.provider();

// 监听端口8080,连接排队队列,如果队列中的连接超过这个数的话就会拒绝连接

HttpServer httpserver =provider.createHttpServer(new InetSocketAddress(8080), 100);

// 监听路径为RestSample,请求处理后回调RestGetHandler里的handle方法

httpserver.createContext("/RestSample", new RestGetHandler());

// 管理工作线程池

ExecutorService executor = new ThreadPoolExecutor(10,200,60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new ThreadPoolExecutor.AbortPolicy());

httpserver.setExecutor(executor);

httpserver.start();

System.out.println("server started");

}

public static void main(String[] args) throws IOException {

serverStart();

}

}

/**

* 回调类,里面的handle方法主要完成将包装好的请求头返回给客户端的功能

*/

class RestGetHandler implements HttpHandler {

@Override

public void handle(HttpExchange he) throws IOException {

String requestMethod = he.getRequestMethod();

// 如果是get方法

if ("GET".equalsIgnoreCase(requestMethod)) {

// 获取响应头,接下来我们来设置响应头信息

Headers responseHeaders = he.getResponseHeaders();

// 以json形式返回,其他还有text/html等等

responseHeaders.set("Content-Type", "application/json");

// 设置响应码200和响应body长度,这里我们设0,没有响应体

he.sendResponseHeaders(200, 0);

// 获取响应体

OutputStream responseBody = he.getResponseBody();

// 获取请求头并打印

Headers requestHeaders = he.getRequestHeaders();

Set<String> keySet = requestHeaders.keySet();

Iterator<String> iter = keySet.iterator();

while (iter.hasNext()) {

String key = iter.next();

List values = requestHeaders.get(key);

String s = key + " = " + values.toString() + "\r\n";

responseBody.write(s.getBytes());

}

// 关闭输出流

responseBody.close();

}

}

}

① 最开始我们通过 HttpServerProvider provider = HttpServerProvider.provider(); 创建了一个HttpServerProvider,也就是这里的DefaultHttpServerProvide

// HttpServerProvider.java

public static HttpServerProvider provider () {

// 这里我们删掉了其他部分,只留下172、173两行

// 这里创建了一个DefaultHttpServerProvide

provider = new sun.net.httpserver.DefaultHttpServerProvider();

return provider;

}

② 之后我们调用 HttpServer httpserver =provider.createHttpServer(new InetSocketAddress(8080), 100); ,

也就是调用了DefaultHttpServerProvider的createHttpServer创建一个HttpServerImpl,当然这里也可以用createHttpsServer创建一个HttpsServerImpl,但是前面说了我们这篇不分析https,所以这里忽略了createHttpsServer方法

还有这里创建ServerImpl的构造方法我们暂时不讲,留到后面再讲

// DefaultHttpServerProvider.java

public HttpServer createHttpServer (InetSocketAddress addr, int backlog) throws IOException {

return new HttpServerImpl (addr, backlog);

}

// HttpServerImpl.java

HttpServerImpl (

InetSocketAddress addr, int backlog

) throws IOException {

server = new ServerImpl (this, "http", addr, backlog);

}

③ 接下来我们创建了一个监听路径 httpserver.createContext("/RestSample", new RestGetHandler());

// HttpServer.java

public abstract HttpContext createContext (String path, HttpHandler handler) ;

// HttpContextImpl.java

public HttpContextImpl createContext (String path, HttpHandler handler) {

// 这里调用的server是ServerImpl类的对象

return server.createContext (path, handler);

}

这里成功返回了一个HttpContextImpl对象,这个我们后面会说,这里我们要知道的是,HttpServerImpl调用的是ServerImpl的实现

到这里我们差不多可以聊一下httpserver的主要结构了:

HttpServer是这里的祖先类,它是一个抽象类,抽象了一个HttpServer应该有的方法

而HttpsServer和我们想象的不一样,它和HttpServer不是平行关系,而是HttpServer的子类,它在HttpServer的基础上加了setHttpsConfigurator和getHttpsConfigurator这两个方法而已

HttpServerImpl和HttpsServerImpl虽然都是实现类,但是它们的方法都是调用ServerImpl的方法,都是围绕ServerImpl的

所以我们也可以把ServerImpl看做这个项目的核心类

④ 之后设置一下工作线程池,初始化任务就完成了

ExecutorService executor = new ThreadPoolExecutor(10,200,60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new ThreadPoolExecutor.AbortPolicy());

httpserver.setExecutor(executor);

httpserver.start();

启动自然和我们刚刚聊的结构一样都是从HttpServer开始一层调一层调用到ServerImpl的方法的:

// HttpServer.java

public abstract void start () ;

// HttpServerImpl.java

public void start () {

server.start();

}

// ServerImpl.java

public void start () {

// server未绑定端口或处于已启动或已关闭状态

// 顺便先提一下,这里就可以留意到,ServerImpl作为一个核心类,管理了各种各样的状态(state)等

if (!bound || started || finished) {

throw new IllegalStateException ("server in wrong state");

}

// 如果没有设置线程池,那就用默认的,默认的话等于没有用线程池,是直接execute的,所以尽可能直接创建线程池

if (executor == null) {

executor = new DefaultExecutor();

}

// 创建了一个Dispatcher线程,用来分发任务,如Accept或者Readable

Thread t = new Thread (dispatcher);

// 设置一下状态

started = true;

// 运行线程

t.start();

}

前面我们说过,ServerImpl是这整个项目的核心部分,它管理了httpserver的状态,提供了各种接口以及通用的方法,它也负责了几个内部类线程的启动

所以,接下来我们会分为ServerImpl、Dispatcher、Exchange、ServerTimerTask与ServerTimerTask1四个部分来讲解

(https相关的我去掉了)

比较长,大家稍微过一眼有个印象,之后遇到的时候再回来看就行

// http或https

private String protocol;

private Executor executor;

// 负责接收连接用的类(这个本来在209行附近,我把它提上来了)

private Dispatcher dispatcher;

// ContextList这个类只是封装了一个List<HttpContextImpl>及一些方法,如限制监听的context(路径)的数目和查找context的方法

private ContextList contexts;

private InetSocketAddress address;

// nio相关的那些类

private ServerSocketChannel schan;

private Selector selector;

private SelectionKey listenerKey;

// 负责管理之前提到的idle连接,也就是长连接的set

// 长连接时,连接如果没有任务,就加进去. 如果超过一定时间没有任务,则主动断开长连接

private Set<HttpConnection> idleConnections;

// 管理所有的连接,方便在stop等情况下直接断开所有连接

private Set<HttpConnection> allConnections;

// 管理req连接和rsp连接,防止请求或响应超时,超时时由定时线程断开连接

private Set<HttpConnection> reqConnections;

private Set<HttpConnection> rspConnections;

// 这两个之后6.4的Exchange的addEvent方法部分我们再说

private List<Event> events;

private final Object loLock = new Object();

// 各种状态,相信大家看得懂是什么意思

private volatile boolean finished = false;

private volatile boolean terminating = false;

private boolean bound = false;

private boolean started = false;

// 系统时间,会由ServerTimerTask进行更新

private volatile long time;

// 这个似乎并没有任何用

private volatile long subticks = 0;

// 这个是用来记录一共更新了多少次time的,相当于时间戳一样的东西

private volatile long ticks;

// 把HttpServer包装进来,方便调用

private HttpServer wrapper;

// 这个的意思是ServerTimerTask每隔多长时间定期run一下,因为ServerTimerTask是一个定时任务线程

// 默认是10000ms也就是10秒一次

private final static int CLOCK_TICK = ServerConfig.getClockTick();

// 这个是允许长连接驻留的时间,默认是30秒

private final static long IDLE_INTERVAL = ServerConfig.getIdleInterval();

// 允许最大长连接数,默认200

private final static int MAX_IDLE_CONNECTIONS = ServerConfig.getMaxIdleConnections();

// ServerTimerTask1的定期时间,默认是1秒

private final static long TIMER_MILLIS = ServerConfig.getTimerMillis ();

// 最后这两个默认为-1,至于为什么是-1后面ServerTimerTask部分我们会说

private final static long MAX_REQ_TIME = getTimeMillis(ServerConfig.getMaxReqTime());

private final static long MAX_RSP_TIME=getTimeMillis(ServerConfig.getMaxRspTime());

private final static boolean REQ_RSP_CLEAN_ENABLED = MAX_REQ_TIME != -1 || MAX_RSP_TIME != -1;

// ServerTimerTask和ServerTimerTask1的对象,跑起来就是ServerTimerTask和ServerTimerTask1线程了

private Timer timer, timer1;

private Logger logger;

这就是刚刚2.1小节中提到的ServerImpl的构造方法,没什么要讲的,无非就是初始化了变量并启动了ServerTimerTask和ServerTimerTask1线程

ServerImpl (

HttpServer wrapper, String protocol, InetSocketAddress addr, int backlog

) throws IOException {

this.protocol = protocol;

this.wrapper = wrapper;

this.logger = Logger.getLogger ("com.sun.net.httpserver");

ServerConfig.checkLegacyProperties (logger);

this.address = addr;

contexts = new ContextList();

schan = ServerSocketChannel.open();

if (addr != null) {

ServerSocket socket = schan.socket();

socket.bind (addr, backlog);

bound = true;

}

selector = Selector.open ();

schan.configureBlocking (false);

listenerKey = schan.register (selector, SelectionKey.OP_ACCEPT);

dispatcher = new Dispatcher();

idleConnections = Collections.synchronizedSet (new HashSet<HttpConnection>());

allConnections = Collections.synchronizedSet (new HashSet<HttpConnection>());

reqConnections = Collections.synchronizedSet (new HashSet<HttpConnection>());

rspConnections = Collections.synchronizedSet (new HashSet<HttpConnection>());

time = System.currentTimeMillis();

timer = new Timer ("server-timer", true);

// 可以看到,在初始化阶段两个定时任务就已经启动了

timer.schedule (new ServerTimerTask(), CLOCK_TICK, CLOCK_TICK);

if (timer1Enabled) {

timer1 = new Timer ("server-timer1", true);

timer1.schedule (new ServerTimerTask1(),TIMER_MILLIS,TIMER_MILLIS);

logger.config ("HttpServer timer1 enabled period in ms: "+TIMER_MILLIS);

logger.config ("MAX_REQ_TIME: "+MAX_REQ_TIME);

logger.config ("MAX_RSP_TIME: "+MAX_RSP_TIME);

}

events = new LinkedList<Event>(www.tengyueylzc.cn );

logger.config ("HttpServer created "+protocol+" "+ addr);

}

当然ServerImpl有很多通用的方法,但是这里我们不讲,等到用到它们的时候我们再讲,这样比较方便了解这些通用方法的具体用途

= new Object();

  

  // 各种状态,相信大家看得懂是什么意思

  

  private volatile boolean finished shentuylzc.cn= false;

  

  private volatile boolean terminating www.shentuylgw.cn= false;

  

  private boolean bound = false;

  

  private boolean started = false;

  

  // 系统时间,会由ServerTimerTask进行更新

  

  private volatile long time;

  

  // 这个似乎并没有任何用

  

  private volatile long subticks = 0;

  

  // 这个是用来记录一共更新了多少次time的,相当于时间戳一样的东西

  

  private volatile long ticks;

  

  // 把HttpServer包装进来,方便调用

  

  private HttpServer wrapper;

  

  // 这个的意思是ServerTimerTask每隔多长时间定期run一下,因为ServerTimerTask是一个定时任务线程

  

  // 默认是10000ms也就是10秒一次

  

  private final static int CLOCK_TICK www.laiyuefeng.com= ServerConfig.getClockTick();

  

  // 这个是允许长连接驻留的时间,默认是30秒

  

  private final static long IDLE_INTERVAL = ServerConfig.getIdleInterval();

  

  // 允许最大长连接数,默认200

  

  private final static int MAX_IDLE_CONNECTIONS = ServerConfig.getMaxIdleConnections();

  

  // ServerTimerTask1的定期时间,默认是1秒

  

  private final static long TIMER_MILLIS www.51kunlunyule.com= ServerConfig.getTimerMillis ();

  

  // 最后这两个默认为-1,至于为什么是-1后面ServerTimerTask部分我们会说

  

  private final static long MAX_REQ_TIME = getTimeMillis(ServerConfig.getMaxReqTime());

  

  private final static long MAX_RSP_TIME=getTimeMillis(ServerConfig.getMaxRspTime());

  

  private final static boolean REQ_RSP_CLEAN_ENABLED = MAX_REQ_TIME != -1 || MAX_RSP_TIME != -1;

  

  // ServerTimerTask和ServerTimerTask1的对象,跑起来就是ServerTimerTask和ServerTimerTask1线程了

  

  private Timer timer, timer1;

  

  private Logger logger;

  

  这就是刚刚2.1小节中提到的ServerImpl的构造方法,没什么要讲的,无非就是初始化了变量并启动了ServerTimerTask和ServerTimerTask1线程

  

  ServerImpl (

  

  HttpServer wrapper, String protocol, InetSocketAddress addr, int backlog

  

  ) throws IOException {

  

  this.protocol = protocol;

  

  this.wrapper = wrapper;

  

  this.logger = Logger.getLogger ("com.sun.net.httpserver");

  

  ServerConfig.checkLegacyProperties (logger);

  

  this.address = addr;

  

  contexts = new ContextList();

  

  schan = ServerSocketChannel.open(www.leyouzaixan.cn);

  

  if (addr != null) {

  

  ServerSocket socket = schan.socket(www.javachenglei.com);

  

  socket.bind (addr, backlog);

  

  bound = true;

  

  }

  

  selector = Selector.open (www.51kunlunyule.com);

  

  schan.configureBlocking (false);

  

  listenerKey = schan.register (selector, SelectionKey.OP_ACCEPT);

  

  dispatcher = new Dispatcher();

  

  idleConnections = Collections.synchronizedSet (new HashSet<HttpConnection>());

  

  allConnections www.yixingylzc.cn= Collections.synchronizedSet (new HashSet<HttpConnection>());

  

  reqConnections = www.baihua178.cn Collections.synchronizedSet (new HashSet<HttpConnection>());

  

  rspConnections = Collections.synchronizedSet (new HashSet<HttpConnection>());

  

  time = System.currentTimeMillis(www.51feiyuzc.cn);

  

  timer = new Timer ("server-timer", true);

  

  // 可以看到,在初始化阶段两个定时任务就已经启动了

  

  timer.schedule (new ServerTimerTask(), CLOCK_TICK, CLOCK_TICK);

  

  if (timer1Enabled) {

  

  timer1 = new Timer ("server-timer1", true);

  

  timer1.schedule (new ServerTimerTask1(),TIMER_MILLIS,TIMER_MILLIS);

  

  logger.config ("HttpServer timer1 enabled period in ms: "+TIMER_MILLIS);

  

  logger.config ("MAX_REQ_TIME: "+MAX_REQ_TIME);

  

  logger.config ("MAX_RSP_TIME: "+MAX_RSP_TIME);

  

  }

  

  events = new LinkedList<Event>(www.51baishizc.cn );

  

  logger.config ("HttpServer created "+protocol+" "+ addr);

  

  }

  

  当然ServerImpl有很多通用的方法,但是这里我们不讲,等到用到它们的时候我们再讲,这样比较方便了解这些通用方法的具体用途

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档