概述

Servlet是Server Applet的缩写,即在服务器端运行的小程序,而Servlet框架则是对HTTP服务器(Servlet Container)和用户小程序中间层的标准化和抽象。这一层抽象隔离了HTTP服务器的实现细节,而Servlet规范定义了各个类的行为,从而保证了这些“服务器端运行的小程序”对服务器实现的无关性(即提升了其可移植性)。
在Servlet规范有以下几个核心类(接口):
ServletContext:定义了一些可以和Servlet Container交互的方法。
Registration:实现Filter和Servlet的动态注册。
ServletRequest(HttpServletRequest):对HTTP请求消息的封装。
ServletResponse(HttpServletResponse):对HTTP响应消息的封装。
RequestDispatcher:将当前请求分发给另一个URL,甚至ServletContext以实现进一步的处理。
Servlet(HttpServlet):所有“服务器小程序”要实现了接口,这些“服务器小程序”重写doGet、doPost、doPut、doHead、doDelete、doOption、doTrace等方法(HttpServlet)以实现响应请求的相关逻辑。
Filter(FilterChain):在进入Servlet前以及出Servlet以后添加一些用户自定义的逻辑,以实现一些横切面相关的功能,如用户验证、日志打印等功能。
AsyncContext:实现异步请求处理。

AsyncContext

在Servlet 3.0中引入了AsyncContext,用于实现一个请求可以暂停处理,然后在将来的某个时候重新处理该请求,以释放当前请求处理过程中占用的线程。在使用时,当发现请求需要等待一段时间后才能做进一步处理时,可以调用ServletRequest.startAsync()方法,返回AsyncContext实例,使用自己的线程池启动一个线程来做接下来的处理或者将其放入一个任务队列中,以由一个线程不断的检查它的可用状态,以实现最后的返回处理,或调用dispatch方法将其分发给其他URL做进一步响应处理。这项功能对SocketConnector没有多大意义,因为即使Servlet的servic俄方发退出了,其所占用的线程会继续等待,并不会被回收,只有对SelectChannelConnector来说才有效果,因为它的等待不在HttpConnection的handleRequest方法中(AsyncContinuation的scheduleTimeout方法中),而是将timeout的信息提交给SelectSet,它内部会有Acceptors个线程对timeout进行检查。AsyncContext接口定义如下:

public interface AsyncContext {
    // 原始请求相关信息的属性名,即dispatch方法所基于的计算信息。
    static final String ASYNC_REQUEST_URI = "javax.servlet.async.request_uri";
    static final String ASYNC_CONTEXT_PATH = "javax.servlet.async.context_path";
    static final String ASYNC_PATH_INFO = "javax.servlet.async.path_info";
    static final String ASYNC_SERVLET_PATH = "javax.servlet.async.servlet_path";
    static final String ASYNC_QUERY_STRING = "javax.servlet.async.query_string";

// AsyncContinuation是Jetty对AsyncContext的实现,它是一个有限状态机。
    // 1. 在初始化时,它处于IDLE状态,initial状态为true。
    // 2. 调用其handling()方法使其进入处理模式:若它处于IDLE状态,则设置initial状态为false,将状态转移到DISPATCHED,清除AsyncListener,返回true;若它处于REDISPATCH状态,将状态转移到REDISPATCHED,返回true;若它处于COMPLETING状态,状态转移到UNCOMPLETED,返回false;若它处于ASYNCWAIT状态,返回false。对其他状态,抛出IllegalStateException。
    // 3. 如果当前请求因为某些原因无法进一步处理时,可以调用ServletRequest.startAsync方法让当前请求进入ASYNCSTARTED状态,即调用AsyncContinuation.suspend方法,只有它处于DISPATCHED、REDISPATCHED状态下才能调用suspend方法,即在调用handling()方法之后。此方法还会更新AsyncEventState字段的信息,以及调用已注册的AsyncListener的onStartAsync方法,并清除已注册的AsyncListener。
    // 4. 调用unhandle()方法判断这个当前请求是否不需要做进一步处理而可以退出handleRequest中的循环:对ASYNCSTARTED状态,将其状态设置为ASYNCWAIT,并向SelectChannelHttpConnection中schedle一个timeout时间,如果此时它还是处于ASYNCWAIT状态(因为对非SelectChannelConnector,它会一直等待下一个dispatch/complete/timeout事件的到来,更新当前状态,并取消等待),则返回true,否则如果它变为COMPLETING状态,则设置状态为UNCOMPLETED,返回true,否则设置其状态为REDISPATCHED,并返回false;对DISPATCHED、REDISPATCHED状态,设置状态为UNCOMPLETED,返回true;对REDISPATCHING状态,设置为REDISPATCHED状态,返回false;对COMPLETING状态,设置为UNCOMPLETED,返回true;对其他状态,抛出异常。
    // 5. 当进入异步状态的请求完成后,需要将当前处理交由Container做进一步处理,如由另一个path完成进一步处理等,调用AsyncContext的dispatch方法,将当前请求分发回Container:如果当前AsyncContinuation处于ASYNCWAIT状态并且没有超时,设置状态为REDISPATCH,并cancelTimeout()、scheduleDispatch();对已经处于REDISPATCH状态,直接返回;对处于ASYNCSTARTED状态,设置为REDISPATCHING,并返回。
    // 6. 如果当前AsyncContinuation超时,调用其expired方法:对于处于ASYNCSARTED、ASYNCWAIT状态,触发AsyncListener的onTimeout事件,调用complete方法,并scheduleDispatch
    // 7. 当完成异步请求处理时,调用其complete方法:如果处于ASYNCWAIT状态,设置状态为COMPLETING,如果没有超时,scheduleTimeout、scheduleDispatch;当前状态为ASYNCSTARTED,设置状态为COMPLETING;对其他状态,抛出异常。
    // 8. 当退出handleRequest方法时,如果当前AsyncContinuation处于UNCOMPLETE状态,调用其doComplete方法,将其状态设置为COMPLETE,如果出现异常,注册javax.servlet.error.exception, javax.servlet.error.message属性,并触发AsyncListener的onError事件,否则触发onComplete事件。
    // 9. 对状态为ASYNCSTARTED、REDISPATCHING、COMPLETING、ASYNCWAIT,表示处于suspend状态。
    // 10. 对状态为ASYNCSTARTED、REDISPATCHING、REDISPATCH、ASYNCWAIT,表示其处于异步请求开启的状态。
    // 11. 对状态不是IDLE、DISPATCHED、UNCOMPLETED、COMPLETED,表示当前正处于异步请求状态。

// 在调用ServletRequest.startAsync方法中使用的ServletRequest、ServletResponse实例。在调用ServletRequest.startAsync方法时,内部调用AsyncContinuation的suspend方法,
    // 传入ServletContext、ServletRequest、ServletResponse实例,在有在AsyncContinuation实例处于DISPATCHED、REDISPATCHED状态下才能调用suspend方法。此时将_expired、_resumed状态设置为false,更新AsyncEventState中的AsyncContext、ServletContext(_suspendedContext, _dispatchedContext)、ServletRequest、ServletResponse、Path等信息(即如果传入的Request、Response、_suspendContext和当前已保存的实例不同或_event实例为null,则重新创建AsyncEventState实例,否则清除_event中的_dispatchedContext和_path字段)。将当前AsyncContinuation的状态设置为ASYNCSTARTED,保存已注册的AsyncListener列表(_asyncListeners)到_lastAsyncListeners,清除_asyncListeners列表,并触发_lastAsyncListeners中的onStartAsync事件(该事件中可以决定是否需要将自己注册回去)。
    // 对于AsyncContext实例,如果已经调用suspend方法,则返回_event中的ServletRequest、ServletResponse,否则返回HttpConnection中的ServletRequest、ServletResponse。 
    public ServletRequest getRequest();
    public ServletResponse getResponse();

// 当前AsyncContext是否使用原始的request、response实例进行初始化。
    public boolean hasOriginalRequestAndResponse();

public void dispatch();
    public void dispatch(String path);
    public void dispatch(ServletContext context, String path);
    public void complete();
    public void start(Runnable run);

public void addListener(AsyncListener listener);
    public void addListener(AsyncListener listener, ServletRequest servletRequest, ServletResponse servletResponse);

public <T extends AsyncListener> T createListener(Class<T> clazz) throws ServletException;

public void setTimeout(long timeout);
    public long getTimeout();
}

在Server的handleAsync()方法中,他使用HttpConnection的Request字段的AsyncEventState中的ServletRequest、ServletResponse作为Handler调用handle方法的参数,如果AsyncEventState中有path值,则会用该值来更新baseRequest中的URI相关信息。

RequestDispatcher

在Servlet中RequestDispatcher用于将请求分发到另一个URL中,或向响应中包含更多的信息。一般用于对当前请求做一些前期处理,然后需要后期其他Servlet、JSP来做进一步处理。在Jetty中使用Dispatcher类实现该接口,其接口定义如下:

public interface RequestDispatcher {
    // 在Dispatcher类中包含了ContextHandler、uri、path、dQuery、named字段,其中ContextHandler是当前Web Application配置的Handler链用于将请求分发给当前Container(调用handle()方法)做进一步处理、dipatch后请求的全URI、path表示uriInContext、dQuery表示新传入的parameter、named表示可以使用Servlet名称创建Dispatcher,即将当前请求分发到一个命名的Servlet中。
    // 在Dispatcher类中有三个方法:forward、error、include。对forward、error来说,如果Response已经commit,会抛出IllegalStateException。
    // 其中forward和error只是DispatcherType不一样(FORWARD、ERROR),其他逻辑一样:清除ServletResponse中所有响应相关的字段,如Content Buffer、locale、ContentType、CharacterEncoding、MimeType等信息,设置ServletRequest的DispatchType;对named方式的Dispatcher,直接调用ContextHandler的handle方法,其target参数即为传入的named;如果dQuery字段不为null,将该dQuery中的包含的参数合并到当前请求中;更新Request的URI、ContextPath,并在其Request属性中添加原始请求的pathInfo、queryString、RequestURI、contextPath、servletPath信息,分别对应该接口中定义的字段,如果这是第二次forward,则保留最原始的请求相关的信息;最后调用ContextHandler的handle方法,target为path属性;在调用结束后,将RequestURI、ContextPath、ServletPath、PathInfo、Attributes、Parameters、QueryString、DispatcherType属性设置为原来的值。
    // 对include方法,它不会清除Response中的Buffer等信息:首先设置DispatcherType为INCLUDE,HttpConnection中的include字段加1,表示正处于INCLUDE的dispatch状态,从而阻止对ServletResponse响应头的设置、发送重定向响应、发送Error响应等操作,该include字段会在该方法结束是调用HttpConnection的included方法将其减1;同样对于named设置的Dispatcher实例,直接调用ContextHandler的handle方法,target为named值;对以path方式的include,首先合并传入的dQuery参数到Request中,更新Request中属性的requestURI、contextPath、pathInfo、query等,后调用ContextHandler的handle方法,target为path,在handle方法完成后,将请求Attributes、Parameters、DispatcherType设置会原有值。

// 在forward中,原始请求对应信息使用的属性名。
    static final String FORWARD_REQUEST_URI = "javax.servlet.forward.request_uri";
    static final String FORWARD_CONTEXT_PATH = "javax.servlet.forward.context_path";
    static final String FORWARD_PATH_INFO = "javax.servlet.forward.path_info";
    static final String FORWARD_SERVLET_PATH = "javax.servlet.forward.servlet_path";
    static final String FORWARD_QUERY_STRING = "javax.servlet.forward.query_string";

// 在include中,原始请求对应信息使用的属性名。
    static final String INCLUDE_REQUEST_URI = "javax.servlet.include.request_uri";
    static final String INCLUDE_CONTEXT_PATH = "javax.servlet.include.context_path";
    static final String INCLUDE_PATH_INFO = "javax.servlet.include.path_info";
    static final String INCLUDE_SERVLET_PATH = "javax.servlet.include.servlet_path";
    static final String INCLUDE_QUERY_STRING = "javax.servlet.include.query_string";

// 在error中,原始请求对应信息使用的属性名。
    public static final String ERROR_EXCEPTION = "javax.servlet.error.exception";
    public static final String ERROR_EXCEPTION_TYPE = "javax.servlet.error.exception_type";
    public static final String ERROR_MESSAGE = "javax.servlet.error.message";
    public static final String ERROR_REQUEST_URI = "javax.servlet.error.request_uri";
    public static final String ERROR_SERVLET_NAME = "javax.servlet.error.servlet_name";
    public static final String ERROR_STATUS_CODE = "javax.servlet.error.status_code";

public void forward(ServletRequest request, ServletResponse response) throws ServletException, IOException;
    public void include(ServletRequest request, ServletResponse response) throws ServletException, IOException;
}

HttpSession

Http的请求是无状态的,这种方式的好处是逻辑简单,因为服务器不需要根据当前服务器的状态做一些特殊处理,然而在实际应用中,有些时候希望一系列的请求共享一些数据和信息,在HTTP中可以有两种方式实现这种需求:一种是cookie,所有这些共享的数据和信息都使用cookie在发送请求时发送给服务器,在响应请求时将最新的信息和状态通过set-cookie的方式重新发送回客户端,这种方式可以使服务器依然保持简单无状态的处理逻辑,然而它每次都要来回传送这种状态信息,会占用带宽,而且cookie本身有大小限制,有些客户端处于安全的因素会禁止cookie使用,另外cookie采用明文方式,对有些数据来说是不适合的;另一种方式则是采用服务器端Session的方法,即使用SessionId将一系列的请求关联在一起,可以向Session存储这些请求共享的信息和数据,Session方式的好处是这些数据保存在服务器端,因而它是安全的,而且不需要每次在客户端和服务器端传输,可以减少带宽,而它不好的地方是会增加服务器负担,因为如果Session过多会占用服务器内存,另外它也会增加服务器端的逻辑,服务器要有一种机制保证相同的SessionId确实属于同一个系列的请求。
在Servlet种使用HttpSession抽象这种服务器端Session的信息。它包含了SessionId、CreationTime、LastAccessedTime、MaxInactiveInterval、Attributes等信息,在Servlet中的可见范伟是ServletContext,即跨Web Application的Session是不可见的。在Jetty中使用SessionManager来管理Session,Session可以存储在数据库中(JDBCSessionManager),也可以存在内存中(HashSessionManager)。在Jetty中使用AbstractSessionManager的内部类Session来实现HttpSession接口,并且该实现是线程安全的。HttpSession的接口定义如下:

public interface HttpSession {
    // HttpSession创建的时间戳,从1970-01-01 00:00:00.000开始算到现在的毫秒数。
    public long getCreationTime();
    
    // 当前Session的ID号,它用来唯一标识Web Application中的一个Session实例。在Jetty的实现中,有两种ID:NodeId和ClusterId,在SessionIdManager创建一个ClusterId时,可以使用一个SecureRandom的两次nextLong的36进制的字符串相加或者两次当前SessionIdManager的hashCode、当前可用内存数、random.nextInt()、Request的hashCode左移32位的异或操作的36进制字符串相加,并添加workName前缀,如果该ID已经存在,则继续使用以上逻辑,直到找到一个没有被使用的唯一的ID号。如果请求中的RequestedSessionId存在并在使用,则使用该值作为SessionID;如果当前请求已经存在一个正在使用的SessionId(在org.eclipse.jetty.server.newSessionId请求熟悉中),则使用该ID。而对与NodeId,它会在ClusterId之后加一个".workerName",可以通过SessionIdManager设置workName或在HashSessionIdManager中使用org.eclipse.jetty.ajp.JVMRoute请求属性设置。在AbstractSessionManager中设置NodeIdInSessionId为true来配置使用NodeId作为SessionId,默认使用ClusterId作为SessionId。
    public String getId();
    
    // 返回最后一次访问时间戳。在每一次属于同一个Session的新的Request到来时都会更新该值。
    public long getLastAccessedTime();
    
    // 返回该Session对应的ServletContext。
    public ServletContext getServletContext();

// 设置Session的Idle时间,以秒为单位。
    public void setMaxInactiveInterval(int interval);
    public int getMaxInactiveInterval();

// Attribute相关操作。在设置属性时,如果传入value为null,则移除该属性;如果该属性已存在,则替换该属性;如果属性值实现了HttpSessionBindingListener,则它在替换时会触发其valueUnbound事件,属性设置时会触发valueBound事件;如果HttpSession中注册了HttpSessionAttributeListener,则会触发响应的attributeAdded、attributeReplaced事件。而removeAttribute时,也会触发相应的valueUnbound事件以及attributeRemoved事件。
    public Object getAttribute(String name);
    public Enumeration<String> getAttributeNames();    
    public void setAttribute(String name, Object value);
    public void removeAttribute(String name);

// Session失效,它移除所有和其绑定的属性,并且将Session实例从SessionManager中移除。
    public void invalidate();
    
    // true表识客户端没有使用session。此时客户端请求可能不需要使用session信息或者它使用cookie作为session的信息交互。
    public boolean isNew();
}

在Jetty中使用SessionIdManager来创建管理SessionId信息,默认实现有HashSessionIdManager和JDBCSessionIdManager:

public interface SessionIdManager extends LifeCycle {
    public boolean idInUse(String id);
    public void addSession(HttpSession session);
    public void removeSession(HttpSession session);
    public void invalidateAll(String id);
    public String newSessionId(HttpServletRequest request,long created);
    public String getWorkerName();
    public String getClusterId(String nodeId);
    public String getNodeId(String clusterId,HttpServletRequest request);  
}

而HttpSession的创建和管理则使用SessionManager,默认有HashSessionManager和JDBCSessionManager两个实现:

public interface SessionManager extends LifeCycle {
    // 在AbstractSessionManager定义了Session内部类实现了HttpSession接口,使用SessionIdManager来生成并管理SessionId,可以注册HttpSessionAttributeListener和HttpSessionListener(在HttpSession创建和销毁时分别触发sessionCreated、sessionDestroyed事件)。另外它还实现了SessionCookieConfig内部类,用于使用Cookie配置Session的信息,如Name(默认JSESSIONID)、domain、path、comment、httpOnly、secure、maxAge等。在HashSessionManager和JDBCSessionManager中还各自有一个线程会检查Session的expire状态,并Invalidate已经expired的Session。最后,AbstractSessionManager还包含了Session相关的统计信息。

// 在SessionManager中定义的一些属性,可以使用该方法定义的一些属性在ServletContext的initParam中设置,即web.xml文件中的init-param中设置。

// 创建并添加HttpSession实例。
    public HttpSession newHttpSession(HttpServletRequest request);

// 根据SessionId获取HttpSession实例。
    public HttpSession getHttpSession(String id);

// 获取Cookie作为SessionTrackingMode时,该Cookie是否属于httpOnly(用来阻止某些cross-script攻击)
    public boolean getHttpOnly();

// Session的最大Idle时间,秒为单位
    public int getMaxInactiveInterval();
    public void setMaxInactiveInterval(int seconds);

public void setSessionHandler(SessionHandler handler);

// 事件相关操作
    public void addEventListener(EventListener listener);
    public void removeEventListener(EventListener listener);
    public void clearEventListeners();

// 在使用Cookie作为SessionTrackingMode时,获取作为Session Tracking的Cookie
    public HttpCookie getSessionCookie(HttpSession session, String contextPath, boolean requestIsSecure);

public SessionIdManager getIdManager();
    public void setIdManager(SessionIdManager idManager);
    public boolean isValid(HttpSession session);
    public String getNodeId(HttpSession session);
    public String getClusterId(HttpSession session);

// 更新Session的AccessTime。
    public HttpCookie access(HttpSession session, boolean secure);

public void complete(HttpSession session);

// 使用URL作为SessionTrackingMode时,在URL中作为SessionId的parameter name。
    public void setSessionIdPathParameterName(String parameterName);
    public String getSessionIdPathParameterName();

// 使用URL作为SessionTrackingMode时,在URL中SessionId信息的前缀,默认为:;<sessionIdParameterName>=
    public String getSessionIdPathParameterNamePrefix();

public boolean isUsingCookies();
    public boolean isUsingURLs();
    public Set<SessionTrackingMode> getDefaultSessionTrackingModes();
    public Set<SessionTrackingMode> getEffectiveSessionTrackingModes();
    public void setSessionTrackingModes(Set<SessionTrackingMode> sessionTrackingModes);
    public SessionCookieConfig getSessionCookieConfig();

public boolean isCheckingRemoteSessionIdEncoding();
    public void setCheckingRemoteSessionIdEncoding(boolean remote);
}

SessionHandler

SessionHandler继承子ScopedHandler,它主要使用SessionManager在doScope方法中为当前Scope设置Session信息。
1. 如果使用Cookie作为SessionId的通信,则首先从Cookie中向Request设置RequestedSessionId。
2. 否则,从URL中计算出RequestedSessionId,并设置到Request中。
3. 如果SessionManager发生变化,则更新Request中SessionManager实例以及Session实例。
4. 如果Session发生变化,则更新Session的AccessTime,并将返回的cookie写入Response中。
5. 在退出时设置回Request原有的SessionManager和Session实例,如果需要的话。

深入Jetty源码之Servlet框架及实现(AsyncContext、RequestDispatcher、HttpSession)相关推荐

  1. 深入理解Tomcat和Jetty源码之第二篇servlet规范和servlet容器

    深入理解Tomcat和Jetty源码之第二篇servlet规范和servlet容器 思维导图总览 这篇推送主要讲servlet的规范和什么是servlet容器? 1.先来讲讲servlet规范: 2. ...

  2. 深入理解Tomcat和Jetty源码之第一篇前奏知识

    深入理解Tomcat和Jetty源码之第一篇前奏知识 这周开始对Tomcat和Jetty的源码和框架产生了浓厚的兴趣,接下来想记录一下学习Tomcat和Jetty源码的心得和体会,有兴趣的朋友可以关注 ...

  3. 基于Java的环保企业网站管理系统+项目源码(SSM框架)

    活动地址:毕业季·进击的技术er 博主介绍:✌在职Java研发工程师.专注于程序设计.源码分享.技术交流.专注于Java技术领域和毕业设计✌ 项目名称 基于Java的环保企业网站管理系统+项目源码(S ...

  4. Jetty源码学习3-启动服务器

    2019独角兽企业重金招聘Python工程师标准>>> 引言 本文主要介绍下环境准备.启动加载的配置文件的涵义和start.jar的加载原理. 环境准备 1.下载jetty-dist ...

  5. 【OkHttp】OkHttp 源码分析 ( 网络框架封装 | OkHttp 4 迁移 | OkHttp 建造者模式 )

    OkHttp 系列文章目录 [OkHttp]OkHttp 简介 ( OkHttp 框架特性 | Http 版本简介 ) [OkHttp]Android 项目导入 OkHttp ( 配置依赖 | 配置 ...

  6. 源码解析Servlet和HttpServlet

    在编写 Servlet 时需要用到两个用于所有 Servlet 的基本软件包:javax.servlet 和 javax.servlet.http. 下面主要介绍 javax.servlet 提供的 ...

  7. mui打包ios_优品视界,聚合影视APP源码。mui框架,hbuilder即可编译,云打包生成ios的Android的app...

    [实例简介] 影视APP源码,mui框架的.hbuilderx或hbuilder打包即可,只包含前端web app源码文件.基本功能都齐全,有需要的可以拿去.卖好几百的东西.聚合影视源码 Movie ...

  8. dubbo源码解析之框架粗谈

    dubbo框架设计 一.dubbo框架整体设计 二.各层说明 三.dubbo工程模块分包 四.依赖关系 五.调用链 文章系列 [一.dubbo源码解析之框架粗谈] [二.dubbo源码解析之dubbo ...

  9. SpringMVC源码分析_框架原理图

                                                                                 SpringMVC源码分析_框架原理图     ...

最新文章

  1. ftp 信息服务器日常维护,Web Ftp Mail服务器的日常管理与维护
  2. CentOS系统快捷方式设置
  3. Ubuntu安装HBase2.2.4并进行单机/伪分布式配置
  4. Linux的网络类型
  5. 【离散化】【DP】命运石之门的选择
  6. 【渝粤教育】广东开放大学 动画原画设计 形成性考核 (22)
  7. FFMPEG中H.264的算法文档--整理自ffmpeg论坛等
  8. 英国电信前掌门韦华恩出任阿尔卡特朗讯CEO
  9. jquery定时滑出可最小化的底部提示层
  10. python函数调用时所提供的参数可以是常量_如何使用mock作为函数参数在Python中修补常量...
  11. 昆仑通态如何连接sqlserver数据库_[其他]:MCGS与其他组态软件及ERP软件协同应用...
  12. MTK手机 获取 Serial Number(barcode)
  13. mac brew命令汇总
  14. day17--途牛旅游项目-激活功能
  15. Android8.0 USB系统框架
  16. 哥德尔定理的背景知识1:戴德金的数观念——哥德尔逻辑与哲学之3
  17. 《图书管理系统——“借书还书”模块》
  18. micropython ws2812b 渐变灯
  19. react中的双向绑定
  20. __attribute_((naked))

热门文章

  1. 在别的电脑上运行cg程序出现错误的解决办法
  2. 前两天去A公司面试,面试管问的题目一下子闷了。很郁闷。重新答题。在这里分享一下...
  3. 基础数据结构【一】————数组
  4. CMake学习(一)
  5. Redis以及Redis的php扩展安装无错版
  6. asp.net的Ajax学习进阶
  7. C#实现光盘做启动盘
  8. 实例解说.Net构架下的加密编程
  9. 结合ashx来在DataGrid中显示从数据库中读出的图片
  10. android怎么监听多点触摸_什么是多点触控技术,有哪些用途