
暂无个人介绍
最近一直在捣鼓HBase的项目,之前写了一些代码从数据库加载数据到HBase,所有的代码都跑得好好地,然而今天尝试着换了一个数据库,就跑不通了。通过数据工具,可以发现连接没有问题,而且有部分逻辑很顺利通过了,然而有一些就是卡主了,通过jstack打印出来的信息可以找到这样的堆栈: "runner{object-loader#292}-objecthandler" #323 prio=5 os_prio=0 tid=0x00002aaadc5ec800 nid=0x7f62 in Object.wait() [0x0000000056ce4000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) at java.lang.Object.wait(Object.java:502) at org.apache.commons.pool.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:1104) - locked <0x00000007736013e8> (a org.apache.commons.pool.impl.GenericObjectPool$Latch) at org.apache.commons.dbcp.PoolingDataSource.getConnection(PoolingDataSource.java:106) at org.apache.commons.dbcp.BasicDataSource.getConnection(BasicDataSource.java:1044) at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:111) at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:77) 所以开始我怀疑是连接的问题,从网上也找到了一个类型的现象,有人怀疑是DBPC的一个bug导致死锁:http://stackoverflow.com/questions/5714511/deadlock-issue-in-dbcp-deployed-on-tomcat,所以我升级了DBCP版本1.4,然而和这人一样的结果,升级DBCP版本并没有解决问题。简单的看DBCP的代码,都开始怀疑是不是因为没有Spring JdbcTemplate没有正确的把Connection返回回去引起泄漏了,然而也有点感觉不太可能,因为这段代码在其他数据库都跑得好好地,但是我们的数据库版本都是一致的,然而其他配置上也被假设一致了(被忽略的一个重要的点)。后来开始调配置,减少连接数,减少线程数,经过各种组合,发现当把DB读的batch降到1的时候就可以work了,非常诡异的一个问题。从数据工具中查到,如果用batch,得到的SQL是:SELECT <column>, <column> FROM <table> where iid in (@p0, @p1) 如果是batch是1的话:SELECT <column>, <column> FROM <table> where iid in (@p0) 这段SQL语句是这么产生的: DataSource dataSource = ....this.jdbc = new NamedParameterJdbcTemplate(dataSource); ...MapSqlParameterSource parameters = new MapSqlParameterSource(); parameters.addValue("params", paramsMap.keySet()); jdbc.query("SELECT <columns> FROM <table> where <column> in (:params)";, parameters, new ResultSetExtractor<Void>() {....}) 如果是一个batch的话,在jstack堆栈中可以看到它一直在等数据库的返回结果:"runner{object-loader#16}-objecthandler" #47 prio=5 os_prio=0 tid=0x0000000006ddd800 nid=0x2694 runnable [0x0000000045434000] java.lang.Thread.State: RUNNABLE at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) at java.net.SocketInputStream.read(SocketInputStream.java:170) at java.net.SocketInputStream.read(SocketInputStream.java:141) at com.sybase.jdbc3.timedio.RawDbio.reallyRead(Unknown Source) at com.sybase.jdbc3.timedio.Dbio.doRead(Unknown Source) at com.sybase.jdbc3.timedio.InStreamMgr.a(Unknown Source) at com.sybase.jdbc3.timedio.InStreamMgr.doRead(Unknown Source) at com.sybase.jdbc3.tds.TdsProtocolContext.getChunk(Unknown Source) 这也解释了第一个堆栈一直停在borrowObject(getConnection)的阶段,因为之前所有的Connection都在数据库堵住没有返回,所以这个线程再拿Connection的时候超过了我设置的最大Connection数,所以就等着拿不到Connection。在后来查了一下不同数据库的JDBC Driver信息(sp_version):jConnect (TM) for JDBC(TM)/7.07 ESD #4 (Build 26793)/P/EBF20302/JDK 1.6.0/jdbcmain/OPT/Thu Jul 5 22:08:44 PDT 2012 jConnect (TM) for JDBC(TM)/1000/Wed Mar 11 05:01:24 2015 PDT 也就是说这种用法是因为旧的JDBC Driver对NamedParameterJdbcTemplate不完善引起的,这个坑花了我一整天的时间。。。。
前记 几年前在读Google的BigTable论文的时候,当时并没有理解论文里面表达的思想,因而囫囵吞枣,并没有注意到SSTable的概念。再后来开始关注HBase的设计和源码后,开始对BigTable传递的思想慢慢的清晰起来,但是因为事情太多,没有安排出时间重读BigTable的论文。在项目里,我因为自己在学HBase,开始主推HBase,而另一个同事则因为对Cassandra比较感冒,因而他主要关注Cassandra的设计,不过我们两个人偶尔都会讨论一下技术、设计的各种观点和心得,然后他偶然的说了一句:Cassandra和HBase都采用SSTable格式存储,然后我本能的问了一句:什么是SSTable?他并没有回答,可能也不是那么几句能说清楚的,或者他自己也没有尝试的去问过自己这个问题。然而这个问题本身却一直困扰着我,因而趁着现在有一些时间深入学习HBase和Cassandra相关设计的时候先把这个问题弄清楚了。SSTable的定义 要解释这个术语的真正含义,最好的方法就是从它的出处找答案,所以重新翻开BigTable的论文。在这篇论文中,最初对SSTable是这么描述的(第三页末和第四页初):SSTable The Google SSTable file format is used internally to store Bigtable data. An SSTable provides a persistent, ordered immutable map from keys to values, where both keys and values are arbitrary byte strings. Operations are provided to look up the value associated with a specified key, and to iterate over all key/value pairs in a specified key range. Internally, each SSTable contains a sequence of blocks (typically each block is 64KB in size, but this is configurable). A block index (stored at the end of the SSTable) is used to locate blocks; the index is loaded into memory when the SSTable is opened. A lookup can be performed with a single disk seek: we first find the appropriate block by performing a binary search in the in-memory index, and then reading the appropriate block from disk. Optionally, an SSTable can be completely mapped into memory, which allows us to perform lookups and scans without touching disk. 简单的非直译:SSTable是Bigtable内部用于数据的文件格式,它的格式为文件本身就是一个排序的、不可变的、持久的Key/Value对Map,其中Key和value都可以是任意的byte字符串。使用Key来查找Value,或通过给定Key范围遍历所有的Key/Value对。每个SSTable包含一系列的Block(一般Block大小为64KB,但是它是可配置的),在SSTable的末尾是Block索引,用于定位Block,这些索引在SSTable打开时被加载到内存中,在查找时首先从内存中的索引二分查找找到Block,然后一次磁盘寻道即可读取到相应的Block。还有一种方案是将这个SSTable加载到内存中,从而在查找和扫描中不需要读取磁盘。这个貌似就是HFile第一个版本的格式么,贴张图感受一下:在HBase使用过程中,对这个版本的HFile遇到以下一些问题(参考这里):1. 解析时内存使用量比较高。2. Bloom Filter和Block索引会变的很大,而影响启动性能。具体的,Bloom Filter可以增长到100MB每个HFile,而Block索引可以增长到300MB,如果一个HRegionServer中有20个HRegion,则他们分别能增长到2GB和6GB的大小。HRegion需要在打开时,需要加载所有的Block索引到内存中,因而影响启动性能;而在第一次Request时,需要将整个Bloom Filter加载到内存中,再开始查找,因而Bloom Filter太大会影响第一次请求的延迟。而HFile在版本2中对这些问题做了一些优化,具体会在HFile解析时详细说明。SSTable作为存储使用 继续BigTable的论文往下走,在5.3 Tablet Serving小节中这样写道(第6页):Tablet Serving Updates are committed to a commit log that stores redo records. Of these updates, the recently committed ones are stored in memory in a sorted buffer called a memtable; the older updates are stored in a sequence of SSTables. To recover a tablet, a tablet server reads its metadata from the METADATA table. This metadata contains the list of SSTables that comprise a tablet and a set of a redo points, which are pointers into any commit logs that may contain data for the tablet. The server reads the indices of the SSTables into memory and reconstructs the memtable by applying all of the updates that have committed since the redo points. When a write operation arrives at a tablet server, the server checks that it is well-formed, and that the sender is authorized to perform the mutation. Authorization is performed by reading the list of permitted writers from a Chubby file (which is almost always a hit in the Chubby client cache). A valid mutation is written to the commit log. Group commit is used to improve the throughput of lots of small mutations [13, 16]. After the write has been committed, its contents are inserted into the memtable. When a read operation arrives at a tablet server, it is similarly checked for well-formedness and proper authorization. A valid read operation is executed on a merged view of the sequence of SSTables and the memtable. Since the SSTables and the memtable are lexicographically sorted data structures, the merged view can be formed efficiently. Incoming read and write operations can continue while tablets are split and merged. 第一段和第三段简单描述,非翻译: 在新数据写入时,这个操作首先提交到日志中作为redo纪录,最近的数据存储在内存的排序缓存memtable中;旧的数据存储在一系列的SSTable 中。在recover中,tablet server从METADATA表中读取metadata,metadata包含了组成Tablet的所有SSTable(纪录了这些SSTable的元 数据信息,如SSTable的位置、StartKey、EndKey等)以及一系列日志中的redo点。Tablet Server读取SSTable的索引到内存,并replay这些redo点之后的更新来重构memtable。 在读时,完成格式、授权等检查后,读会同时读取SSTable、memtable(HBase中还包含了BlockCache中的数据)并合并他们的结果,由于SSTable和memtable都是字典序排列,因而合并操作可以很高效完成。 SSTable在Compaction过程中的使用 在BigTable论文5.4 Compaction小节中是这样说的:Compaction As write operations execute, the size of the memtable increases. When the memtable size reaches a threshold, the memtable is frozen, a new memtable is created, and the frozen memtable is converted to an SSTable and written to GFS. This minor compaction process has two goals: it shrinks the memory usage of the tablet server, and it reduces the amount of data that has to be read from the commit log during recovery if this server dies. Incoming read and write operations can continue while compactions occur. Every minor compaction creates a new SSTable. If this behavior continued unchecked, read operations might need to merge updates from an arbitrary number of SSTables. Instead, we bound the number of such files by periodically executing a merging compaction in the background. A merging compaction reads the contents of a few SSTables and the memtable, and writes out a new SSTable. The input SSTables and memtable can be discarded as soon as the compaction has finished. A merging compaction that rewrites all SSTables into exactly one SSTable is called a major compaction. SSTables produced by non-major compactions can contain special deletion entries that suppress deleted data in older SSTables that are still live. A major compaction, on the other hand, produces an SSTable that contains no deletion information or deleted data. Bigtable cycles through all of its tablets and regularly applies major compactions to them. These major compactions allow Bigtable to reclaim resources used by deleted data, and also allow it to ensure that deleted data disappears from the system in a timely fashion, which is important for services that store sensitive data. 随着memtable大小增加到一个阀值,这个memtable会被冻住而创建一个新的memtable以供使用,而旧的memtable会转换成一个SSTable而写道GFS中,这个过程叫做minor compaction。这个minor compaction可以减少内存使用量,并可以减少日志大小,因为持久化后的数据可以从日志中删除。在minor compaction过程中,可以继续处理读写请求。每次minor compaction会生成新的SSTable文件,如果SSTable文件数量增加,则会影响读的性能,因而每次读都需要读取所有SSTable文件,然后合并结果,因而对SSTable文件个数需要有上限,并且时不时的需要在后台做merging compaction,这个merging compaction读取一些SSTable文件和memtable的内容,并将他们合并写入一个新的SSTable中。当这个过程完成后,这些源SSTable和memtable就可以被删除了。 如果一个merging compaction是合并所有SSTable到一个SSTable,则这个过程称做major compaction。一次major compaction会将mark成删除的信息、数据删除,而其他两次compaction则会保留这些信息、数据(mark的形式)。Bigtable会时不时的扫描所有的Tablet,并对它们做major compaction。这个major compaction可以将需要删除的数据真正的删除从而节省空间,并保持系统一致性。SSTable的locality和In Memory 在Bigtable中,它的本地性是由Locality group来定义的,即多个column family可以组合到一个locality group中,在同一个Tablet中,使用单独的SSTable存储这些在同一个locality group的column family。HBase把这个模型简化了,即每个column family在每个HRegion都使用单独的HFile存储,HFile没有locality group的概念,或者一个column family就是一个locality group。在Bigtable中,还可以支持在locality group级别设置是否将所有这个locality group的数据加载到内存中,在HBase中通过column family定义时设置。这个内存加载采用延时加载,主要应用于一些小的column family,并且经常被用到的,从而提升读的性能,因而这样就不需要再从磁盘中读取了。SSTable压缩 Bigtable的压缩是基于locality group级别:Compression Clients can control whether or not the SSTables for a locality group are compressed, and if so, which compression format is used. The user-specified compression format is applied to each SSTable block (whose size is controllable via a locality group specific tuning parameter). Although we lose some space by compressing each block separately, we benefit in that small portions of an SSTable can be read without decompressing the entire file. Many clients use a two-pass custom compression scheme. The first pass uses Bentley and McIlroy’s scheme [6], which compresses long common strings across a large window. The second pass uses a fast compression algorithm that looks for repetitions in a small 16 KB window of the data. Both compression passes are very fast—they encode at 100–200 MB/s, and decode at 400–1000 MB/s on modern machines. Bigtable的压缩以SSTable中的一个Block为单位,虽然每个Block为压缩单位损失一些空间,但是采用这种方式,我们可以以Block为单位读取、解压、分析,而不是每次以一个“大”的SSTable为单位读取、解压、分析。SSTable的读缓存 为了提升读的性能,Bigtable采用两层缓存机制:Caching for read performance To improve read performance, tablet servers use two levels of caching. The Scan Cache is a higher-level cache that caches the key-value pairs returned by the SSTable interface to the tablet server code. The Block Cache is a lower-level cache that caches SSTables blocks that were read from GFS. The Scan Cache is most useful for applications that tend to read the same data repeatedly. The Block Cache is useful for applications that tend to read data that is close to the data they recently read (e.g., sequential reads, or random reads of different columns in the same locality group within a hot row). 两层缓存分别是:1. High Level,缓存从SSTable读取的Key/Value对。提升那些倾向重复的读取相同的数据的操作(引用局部性原理)。2. Low Level,BlockCache,缓存SSTable中的Block。提升那些倾向于读取相近数据的操作。Bloom Filter 前文有提到Bigtable采用合并读,即需要读取每个SSTable中的相关数据,并合并成一个结果返回,然而每次读都需要读取所有SSTable,自然会耗费性能,因而引入了Bloom Filter,它可以很快速的找到一个RowKey不在某个SSTable中的事实(注:反过来则不成立)。Bloom Filter As described in Section 5.3, a read operation has to read from all SSTables that make up the state of a tablet. If these SSTables are not in memory, we may end up doing many disk accesses. We reduce the number of accesses by allowing clients to specify that Bloom fil- ters [7] should be created for SSTables in a particu- lar locality group. A Bloom filter allows us to ask whether an SSTable might contain any data for a spec- ified row/column pair. For certain applications, a small amount of tablet server memory used for storing Bloom filters drastically reduces the number of disk seeks re- quired for read operations. Our use of Bloom filters also implies that most lookups for non-existent rows or columns do not need to touch disk. SSTable设计成Immutable的好处 在SSTable定义中就有提到SSTable是一个Immutable的order map,这个Immutable的设计可以让系统简单很多:Exploiting Immutability Besides the SSTable caches, various other parts of the Bigtable system have been simplified by the fact that all of the SSTables that we generate are immutable. For example, we do not need any synchronization of accesses to the file system when reading from SSTables. As a result, concurrency control over rows can be implemented very efficiently. The only mutable data structure that is accessed by both reads and writes is the memtable. To reduce contention during reads of the memtable, we make each memtable row copy-on-write and allow reads and writes to proceed in parallel. Since SSTables are immutable, the problem of permanently removing deleted data is transformed to garbage collecting obsolete SSTables. Each tablet’s SSTables are registered in the METADATA table. The master removes obsolete SSTables as a mark-and-sweep garbage collection [25] over the set of SSTables, where the METADATA table contains the set of roots. Finally, the immutability of SSTables enables us to split tablets quickly. Instead of generating a new set of SSTables for each child tablet, we let the child tablets share the SSTables of the parent tablet. 关于Immutable的优点有以下几点:1. 在读SSTable是不需要同步。读写同步只需要在memtable中处理,为了减少memtable的读写竞争,Bigtable将memtable的row设计成copy-on-write,从而读写可以同时进行。2. 永久的移除数据转变为SSTable的Garbage Collect。每个Tablet中的SSTable在METADATA表中有注册,master使用mark-and-sweep算法将SSTable在GC过程中移除。3. 可以让Tablet Split过程变的高效,我们不需要为每个子Tablet创建新的SSTable,而是可以共享父Tablet的SSTable。
高性能IO模型浅析 转自:http://www.cnblogs.com/fanzhidongyzby/p/4098546.html 服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种: (1)同步阻塞IO(Blocking IO):即传统的IO模型。 (2)同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。注意这里所说的NIO并非Java的NIO(New IO)库。 (3)IO多路复用(IO Multiplexing):即经典的Reactor设计模式,有时也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型。 (4)异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO。 同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。 阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:阻塞是指IO操作需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。 另外,Richard Stevens 在《Unix 网络编程》卷1中提到的基于信号驱动的IO(Signal Driven IO)模型,由于该模型并不常用,本文不作涉及。接下来,我们详细分析四种常见的IO模型的实现原理。为了方便描述,我们统一使用IO的读操作作为示例。 一、同步阻塞IO 同步阻塞IO模型是最简单的IO模型,用户线程在内核进行IO操作时被阻塞。 图1 同步阻塞IO 如图1所示,用户线程通过系统调用read发起IO读操作,由用户空间转到内核空间。内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作。 用户线程使用同步阻塞IO模型的伪代码描述为: { read(socket, buffer); process(buffer); } 即用户需要等待read将socket中的数据读取到buffer后,才继续处理接收的数据。整个IO请求的过程中,用户线程是被阻塞的,这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够。 二、同步非阻塞IO 同步非阻塞IO是在同步阻塞IO的基础上,将socket设置为NONBLOCK。这样做用户线程可以在发起IO请求后可以立即返回。 图2 同步非阻塞IO 如图2所示,由于socket是非阻塞的方式,因此用户线程发起IO请求时立即返回。但并未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。 用户线程使用同步非阻塞IO模型的伪代码描述为: { while(read(socket, buffer) != SUCCESS) { } process(buffer); } 即 用户需要不断地调用read,尝试读取socket中的数据,直到读取成功后,才继续处理接收的数据。整个IO请求的过程中,虽然用户线程每次发起IO请 求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。一般很少直接使用这种模型,而是在其他IO模型中使用非阻 塞IO这一特性。 三、IO多路复用 IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。 图3 多路分离函数select 如图3所示,用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。 从 流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效 率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调 用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。 用户线程使用select函数的伪代码描述为: { select(socket); while(1) { sockets = select(); for(socket in sockets) { if(can_read(socket)) { read(socket, buffer); process(buffer); } } } } 其中while循环前将socket添加到select监视中,然后在while内一直调用select获取被激活的socket,一旦socket可读,便调用read函数将socket中的数据读取出来。 然 而,使用select函数的优点并不仅限于此。虽然上述方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻 塞),平均时间甚至比同步阻塞IO模型还要长。如果用户线程只注册自己感兴趣的socket或者IO请求,然后去做自己的事情,等到数据到来时再进行处 理,则可以提高CPU的利用率。 IO多路复用模型使用了Reactor设计模式实现了这一机制。 图4 Reactor设计模式 如 图4所示,EventHandler抽象类表示IO事件处理器,它拥有IO文件句柄Handle(通过get_handle获取),以及对Handle的 操作handle_event(读/写等)。继承于EventHandler的子类可以对事件处理器的行为进行定制。Reactor类用于管理 EventHandler(注册、删除等),并使用handle_events实现事件循环,不断调用同步事件多路分离器(一般是内核)的多路分离函数 select,只要某个文件句柄被激活(可读/写等),select就返回(阻塞),handle_events就会调用与文件句柄关联的事件处理器的 handle_event进行相关操作。 图5 IO多路复用 如 图5所示,通过Reactor的方式,可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理 器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,则通知 相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。由于select函数是阻塞的,因此多路IO复用 模型也被称为异步阻塞IO模型。注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。一般在使用IO多路复用模型 时,socket都是设置为NONBLOCK的,不过这并不会产生影响,因为用户发起IO请求时,数据已经到达了,用户线程一定不会被阻塞。 用户线程使用IO多路复用模型的伪代码描述为: void UserEventHandler::handle_event() { if(can_read(socket)) { read(socket, buffer); process(buffer); } } { Reactor.register(new UserEventHandler(socket)); } 用户需要重写EventHandler的handle_event函数进行读取数据、处理数据的工作,用户线程只需要将自己的EventHandler注册到Reactor即可。Reactor中handle_events事件循环的伪代码大致如下。 Reactor::handle_events() { while(1) { sockets = select(); for(socket in sockets) { get_event_handler(socket).handle_event(); } } } 事件循环不断地调用select获取被激活的socket,然后根据获取socket对应的EventHandler,执行器handle_event函数即可。 IO多路复用是最常使用的IO模型,但是其异步程度还不够“彻底”,因为它使用了会阻塞线程的select系统调用。因此IO多路复用只能称为异步阻塞IO,而非真正的异步IO。 四、异步IO “真 正”的异步IO需要操作系统更强的支持。在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。而在异 步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。 异步IO模型使用了Proactor设计模式实现了这一机制。 图6 Proactor设计模式 如 图6,Proactor模式和Reactor模式在结构上比较相似,不过在用户(Client)使用方式上差别较大。Reactor模式中,用户线程通过 向Reactor对象注册感兴趣的事件监听,然后事件触发时调用事件处理函数。而Proactor模式中,用户线程将 AsynchronousOperation(读/写等)、Proactor以及操作完成时的CompletionHandler注册到 AsynchronousOperationProcessor。AsynchronousOperationProcessor使用Facade模式提 供了一组异步操作API(读/写等)供用户使用,当用户线程调用异步API后,便继续执行自己的任务。 AsynchronousOperationProcessor 会开启独立的内核线程执行异步操作,实现真正的异步。当异步IO操作完成 时,AsynchronousOperationProcessor将用户线程与AsynchronousOperation一起注册的Proactor 和CompletionHandler取出,然后将CompletionHandler与IO操作的结果数据一起转发给 Proactor,Proactor负责回调每一个异步操作的事件完成处理函数handle_event。虽然Proactor模式中每个异步操作都可以 绑定一个Proactor对象,但是一般在操作系统中,Proactor被实现为Singleton模式,以便于集中化分发操作完成事件。 图7 异步IO 如 图7所示,异步IO模型中,用户线程直接使用内核提供的异步IO API发起read请求,且发起后立即返回,继续执行用户线程代码。不过此时用户线程已 经将调用的AsynchronousOperation和CompletionHandler注册到内核,然后操作系统开启独立的内核线程去处理IO操 作。当read请求的数据到达时,由内核负责读取socket中的数据,并写入用户指定的缓冲区中。最后内核将read的数据和用户线程注册的 CompletionHandler分发给内部Proactor,Proactor将IO完成的信息通知给用户线程(一般通过调用用户线程注册的完成事件 处理函数),完成异步IO。 用户线程使用异步IO模型的伪代码描述为: void UserCompletionHandler::handle_event(buffer) { process(buffer); } { aio_read(socket, new UserCompletionHandler); } 用户需要重写CompletionHandler的handle_event函数进行处理数据的工作,参数buffer表示Proactor已经准备好的数据,用户线程直接调用内核提供的异步IO API,并将重写的CompletionHandler注册即可。 相 比于IO多路复用模型,异步IO并不十分常用,不少高性能并发服务程序使用IO多路复用模型+多线程任务处理的架构基本可以满足需求。况且目前操作系统对 异步IO的支持并非特别完善,更多的是采用IO多路复用模型模拟异步IO的方式(IO事件触发时不直接通知用户线程,而是将数据读写完毕后放到用户指定的 缓冲区中)。Java7之后已经支持了异步IO,感兴趣的读者可以尝试使用。 本文从基本概念、工作流程和代码示 例三个层次简要描述了常见的四种高性能IO模型的结构和原理,理清了同步、异步、阻塞、非阻塞这些容易混淆的概念。通过对高性能IO模型的理解,可以在服 务端程序的开发中选择更符合实际业务特点的IO模型,提高服务质量。希望本文对你有所帮助。 相似的: http://www.cnblogs.com/nufangrensheng/p/3588690.html http://www.ibm.com/developerworks/cn/linux/l-async/
前记 很早以前就有读Netty源码的打算了,然而第一次尝试的时候从Netty4开始,一直抓不到核心的框架流程,后来因为其他事情忙着就放下了。这次趁着休假重新捡起这个硬骨头,因为Netty3现在还在被很多项目使用,因而这次决定先从Netty3入手,瞬间发现Netty3的代码比Netty4中规中矩的多,很多概念在代码本身中都有清晰的表达,所以半天就把整个框架的骨架搞清楚了。再读Netty4对Netty3的改进总结,回去读Netty4的源码,反而觉得轻松了,一种豁然开朗的感觉。记得去年读Jetty源码的时候,因为代码太庞大,并且自己的HTTP Server的了解太少,因而只能自底向上的一个一个模块的叠加,直到最后把所以的模块连接在一起而看清它的真正核心骨架。现在读源码,开始习惯先把骨架理清,然后延伸到不同的器官、血肉而看清整个人体。本文从Reactor模式在Netty3中的应用,引出Netty3的整体架构以及控制流程;然而除了Reactor模式,Netty3还在ChannelPipeline中使用了Intercepting Filter模式,这个模式也在Servlet的Filter中成功使用,因而本文还会从Intercepting Filter模式出发详细介绍ChannelPipeline的设计理念。本文假设读者已经对Netty有一定的了解,因而不会包含过多入门介绍,以及帮Netty做宣传的文字。Netty3中的Reactor模式 Reactor模式在Netty中应用非常成功,因而它也是在Netty中受大肆宣传的模式,关于Reactor模式可以详细参考本人的另一篇文章《Reactor模式详解》,对Reactor模式的实现是Netty3的基本骨架,因而本小节会详细介绍Reactor模式如何应用Netty3中。如果读《Reactor模式详解》,我们知道Reactor模式由Handle、Synchronous Event Demultiplexer、Initiation Dispatcher、Event Handler、Concrete Event Handler构成,在Java的实现版本中,Channel对应Handle,Selector对应Synchronous Event Demultiplexer,并且Netty3还使用了两层Reactor:Main Reactor用于处理Client的连接请求,Sub Reactor用于处理和Client连接后的读写请求(关于这个概念还可以参考Doug Lea的这篇PPT:Scalable IO In Java)。所以我们先要解决Netty3中使用什么类实现所有的上述模块并把他们联系在一起的,以NIO实现方式为例:模式是一种抽象,但是在实现中,经常会因为语言特性、框架和性能需要而做一些改变,因而Netty3对Reactor模式的实现有一套自己的设计:1. ChannelEvent:Reactor是基于事件编程的,因而在Netty3中使用ChannelEvent抽象的表达Netty3内部可以产生的各种事件,所有这些事件对象在Channels帮助类中产生,并且由它将事件推入到ChannelPipeline中,ChannelPipeline构建ChannelHandler管道,ChannelEvent流经这个管道实现所有的业务逻辑处理。ChannelEvent对应的事件有:ChannelStateEvent表示Channel状态的变化事件,而如果当前Channel存在Parent Channel,则该事件还会传递到Parent Channel的ChannelPipeline中,如OPEN、BOUND、CONNECTED、INTEREST_OPS等,该事件可以在各种不同实现的Channel、ChannelSink中产生;MessageEvent表示从Socket中读取数据完成、需要向Socket写数据或ChannelHandler对当前Message解析(如Decoder、Encoder)后触发的事件,它由NioWorker、需要对Message做进一步处理的ChannelHandler产生;WriteCompletionEvent表示写完成而触发的事件,它由NioWorker产生;ExceptionEvent表示在处理过程中出现的Exception,它可以发生在各个构件中,如Channel、ChannelSink、NioWorker、ChannelHandler中;IdleStateEvent由IdleStateHandler触发,这也是一个ChannelEvent可以无缝扩展的例子。注:在Netty4后,已经没有ChannelEvent类,所有不同事件都用对应方法表达,这也意味这ChannelEvent不可扩展,Netty4采用在ChannelInboundHandler中加入userEventTriggered()方法来实现这种扩展,具体可以参考这里。2. ChannelHandler:在Netty3中,ChannelHandler用于表示Reactor模式中的EventHandler。ChannelHandler只是一个标记接口,它有两个子接口:ChannelDownstreamHandler和ChannelUpstreamHandler,其中ChannelDownstreamHandler表示从用户应用程序流向Netty3内部直到向Socket写数据的管道,在Netty4中改名为ChannelOutboundHandler;ChannelUpstreamHandler表示数据从Socket进入Netty3内部向用户应用程序做数据处理的管道,在Netty4中改名为ChannelInboundHandler。3. ChannelPipeline:用于管理ChannelHandler的管道,每个Channel一个ChannelPipeline实例,可以运行过程中动态的向这个管道中添加、删除ChannelHandler(由于实现的限制,在最末端的ChannelHandler向后添加或删除ChannelHandler不一定在当前执行流程中起效,参考这里)。ChannelPipeline内部维护一个ChannelHandler的双向链表,它以Upstream(Inbound)方向为正向,Downstream(Outbound)方向为方向。ChannelPipeline采用Intercepting Filter模式实现,具体可以参考这里,这个模式的实现在后一节中还是详细介绍。4. NioSelector:Netty3使用NioSelector来存放Selector(Synchronous Event Demultiplexer),每个新产生的NIO Channel都向这个Selector注册自己以让这个Selector监听这个NIO Channel中发生的事件,当事件发生时,调用帮助类Channels中的方法生成ChannelEvent实例,将该事件发送到这个Netty Channel对应的ChannelPipeline中,而交给各级ChannelHandler处理。其中在向Selector注册NIO Channel时,Netty Channel实例以Attachment的形式传入,该Netty Channel在其内部的NIO Channel事件发生时,会以Attachment的形式存在于SelectionKey中,因而每个事件可以直接从这个Attachment中获取相关链的Netty Channel,并从Netty Channel中获取与之相关联的ChannelPipeline,这个实现和Doug Lea的Scalable IO In Java一模一样。另外Netty3还采用了Scalable IO In Java中相同的Main Reactor和Sub Reactor设计,其中NioSelector的两个实现:Boss即为Main Reactor,NioWorker为Sub Reactor。Boss用来处理新连接加入的事件,NioWorker用来处理各个连接对Socket的读写事件,其中Boss通过NioWorkerPool获取NioWorker实例,Netty3模式使用RoundRobin方式放回NioWorker实例。更形象一点的,可以通过Scalable IO In Java的这张图表达:若与Ractor模式对应,NioSelector中包含了Synchronous Event Demultiplexer,而ChannelPipeline中管理着所有EventHandler,因而NioSelector和ChannelPipeline共同构成了Initiation Dispatcher。5. ChannelSink:在ChannelHandler处理完成所有逻辑需要向客户端写响应数据时,一般会调用Netty Channel中的write方法,然而在这个write方法实现中,它不是直接向其内部的Socket写数据,而是交给Channels帮助类,内部创建DownstreamMessageEvent,反向从ChannelPipeline的管道中流过去,直到第一个ChannelHandler处理完毕,最后交给ChannelSink处理,以避免阻塞写而影响程序的吞吐量。ChannelSink将这个MessageEvent提交给Netty Channel中的writeBufferQueue,最后NioWorker会等到这个NIO Channel已经可以处理写事件时无阻塞的向这个NIO Channel写数据。这就是上图的send是从SubReactor直接出发的原因。6. Channel:Netty有自己的Channel抽象,它是一个资源的容器,包含了所有一个连接涉及到的所有资源的饮用,如封装NIO Channel、ChannelPipeline、Boss、NioWorkerPool等。另外它还提供了向内部NIO Channel写响应数据的接口write、连接/绑定到某个地址的connect/bind接口等,个人感觉虽然对Channel本身来说,因为它封装了NIO Channel,因而这些接口定义在这里是合理的,但是如果考虑到Netty的架构,它的Channel只是一个资源容器,有这个Channel实例就可以得到和它相关的基本所有资源,因而这种write、connect、bind动作不应该再由它负责,而是应该由其他类来负责,比如在Netty4中就在ChannelHandlerContext添加了write方法,虽然netty4并没有删除Channel中的write接口。Netty3中的Intercepting Filter模式 如果说Reactor模式是Netty3的骨架,那么Intercepting Filter模式则是Netty的中枢。Reactor模式主要应用在Netty3的内部实现,它是Netty3具有良好性能的基础,而Intercepting Filter模式则是ChannelHandler组合实现一个应用程序逻辑的基础,只有很好的理解了这个模式才能使用好Netty,甚至能得心应手。关于Intercepting Filter模式的详细介绍可以参考这里,本节主要介绍Netty3中对Intercepting Filter模式的实现,其实就是DefaultChannelPipeline对Intercepting Filter模式的实现。在上文有提到Netty3的ChannelPipeline是ChannelHandler的容器,用于存储与管理ChannelHandler,同时它在Netty3中也起到桥梁的作用,即它是连接Netty3内部到所有ChannelHandler的桥梁。作为ChannelPipeline的实现者DefaultChannelPipeline,它使用一个ChannelHandler的双向链表来存储,以DefaultChannelPipelineContext作为节点: public interface ChannelHandlerContext { Channel getChannel(); ChannelPipeline getPipeline(); String getName(); ChannelHandler getHandler(); boolean canHandleUpstream(); boolean canHandleDownstream(); void sendUpstream(ChannelEvent e); void sendDownstream(ChannelEvent e); Object getAttachment(); void setAttachment(Object attachment); }private final class DefaultChannelHandlerContext implements ChannelHandlerContext { volatile DefaultChannelHandlerContext next; volatile DefaultChannelHandlerContext prev; private final String name; private final ChannelHandler handler; private final boolean canHandleUpstream; private final boolean canHandleDownstream; private volatile Object attachment; ..... } 在DefaultChannelPipeline中,它存储了和当前ChannelPipeline相关联的Channel、ChannelSink以及ChannelHandler链表的head、tail,所有ChannelEvent通过sendUpstream、sendDownstream为入口流经整个链表: public class DefaultChannelPipeline implements ChannelPipeline { private volatile Channel channel; private volatile ChannelSink sink; private volatile DefaultChannelHandlerContext head; private volatile DefaultChannelHandlerContext tail; ...... public void sendUpstream(ChannelEvent e) { DefaultChannelHandlerContext head = getActualUpstreamContext(this.head); if (head == null) { return; } sendUpstream(head, e); } void sendUpstream(DefaultChannelHandlerContext ctx, ChannelEvent e) { try { ((ChannelUpstreamHandler) ctx.getHandler()).handleUpstream(ctx, e); } catch (Throwable t) { notifyHandlerException(e, t); } } public void sendDownstream(ChannelEvent e) { DefaultChannelHandlerContext tail = getActualDownstreamContext(this.tail); if (tail == null) { try { getSink().eventSunk(this, e); return; } catch (Throwable t) { notifyHandlerException(e, t); return; } } sendDownstream(tail, e); } void sendDownstream(DefaultChannelHandlerContext ctx, ChannelEvent e) { if (e instanceof UpstreamMessageEvent) { throw new IllegalArgumentException("cannot send an upstream event to downstream"); } try { ((ChannelDownstreamHandler) ctx.getHandler()).handleDownstream(ctx, e); } catch (Throwable t) { e.getFuture().setFailure(t); notifyHandlerException(e, t); } } 对Upstream事件,向后找到所有实现了ChannelUpstreamHandler接口的ChannelHandler组成链(getActualUpstreamContext()),而对Downstream事件,向前找到所有实现了ChannelDownstreamHandler接口的ChannelHandler组成链(getActualDownstreamContext()): private DefaultChannelHandlerContext getActualUpstreamContext(DefaultChannelHandlerContext ctx) { if (ctx == null) { return null; } DefaultChannelHandlerContext realCtx = ctx; while (!realCtx.canHandleUpstream()) { realCtx = realCtx.next; if (realCtx == null) { return null; } } return realCtx; } private DefaultChannelHandlerContext getActualDownstreamContext(DefaultChannelHandlerContext ctx) { if (ctx == null) { return null; } DefaultChannelHandlerContext realCtx = ctx; while (!realCtx.canHandleDownstream()) { realCtx = realCtx.prev; if (realCtx == null) { return null; } } return realCtx; } 在实际实现ChannelUpstreamHandler或ChannelDownstreamHandler时,调用 ChannelHandlerContext中的sendUpstream或sendDownstream方法将控制流程交给下一个 ChannelUpstreamHandler或下一个ChannelDownstreamHandler,或调用Channel中的write方法发送 响应消息。 public class MyChannelUpstreamHandler implements ChannelUpstreamHandler { public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception { // handle current logic, use Channel to write response if needed. // ctx.getChannel().write(message); ctx.sendUpstream(e); } }public class MyChannelDownstreamHandler implements ChannelDownstreamHandler { public void handleDownstream( ChannelHandlerContext ctx, ChannelEvent e) throws Exception { // handle current logic ctx.sendDownstream(e); } } 当ChannelHandler向ChannelPipelineContext发送事件时,其内部从当前ChannelPipelineContext节点出发找到下一个ChannelUpstreamHandler或ChannelDownstreamHandler实例,并向其发送ChannelEvent,对于Downstream链,如果到达链尾,则将ChannelEvent发送给ChannelSink: public void sendDownstream(ChannelEvent e) { DefaultChannelHandlerContext prev = getActualDownstreamContext(this.prev); if (prev == null) { try { getSink().eventSunk(DefaultChannelPipeline.this, e); } catch (Throwable t) { notifyHandlerException(e, t); } } else { DefaultChannelPipeline.this.sendDownstream(prev, e); } }public void sendUpstream(ChannelEvent e) { DefaultChannelHandlerContext next = getActualUpstreamContext(this.next); if (next != null) { DefaultChannelPipeline.this.sendUpstream(next, e); } } 正是因为这个实现,如果在一个末尾的ChannelUpstreamHandler中先移除自己,在向末尾添加一个新的ChannelUpstreamHandler,它是无效的,因为它的next已经在调用前就固定设置为null了。ChannelPipeline作为ChannelHandler的容器,它还提供了各种增、删、改ChannelHandler链表中的方法,而且如果某个ChannelHandler还实现了LifeCycleAwareChannelHandler,则该ChannelHandler在被添加进ChannelPipeline或从中删除时都会得到同志: public interface LifeCycleAwareChannelHandler extends ChannelHandler { void beforeAdd(ChannelHandlerContext ctx) throws Exception; void afterAdd(ChannelHandlerContext ctx) throws Exception; void beforeRemove(ChannelHandlerContext ctx) throws Exception; void afterRemove(ChannelHandlerContext ctx) throws Exception; }public interface ChannelPipeline { void addFirst(String name, ChannelHandler handler); void addLast(String name, ChannelHandler handler); void addBefore(String baseName, String name, ChannelHandler handler); void addAfter(String baseName, String name, ChannelHandler handler); void remove(ChannelHandler handler); ChannelHandler remove(String name); <T extends ChannelHandler> T remove(Class<T> handlerType); ChannelHandler removeFirst(); ChannelHandler removeLast(); void replace(ChannelHandler oldHandler, String newName, ChannelHandler newHandler); ChannelHandler replace(String oldName, String newName, ChannelHandler newHandler); <T extends ChannelHandler> T replace(Class<T> oldHandlerType, String newName, ChannelHandler newHandler); ChannelHandler getFirst(); ChannelHandler getLast(); ChannelHandler get(String name); <T extends ChannelHandler> T get(Class<T> handlerType); ChannelHandlerContext getContext(ChannelHandler handler); ChannelHandlerContext getContext(String name); ChannelHandlerContext getContext(Class<? extends ChannelHandler> handlerType); void sendUpstream(ChannelEvent e); void sendDownstream(ChannelEvent e); ChannelFuture execute(Runnable task); Channel getChannel(); ChannelSink getSink(); void attach(Channel channel, ChannelSink sink); boolean isAttached(); List<String> getNames(); Map<String, ChannelHandler> toMap(); } 在DefaultChannelPipeline的ChannelHandler链条的处理流程为:
问题描述 在服务器编程中,通常需要处理多种不同的请求,在正式处理请求之前,需要对请求做一些预处理,如: 纪录每个Client的每次访问信息。 对Client进行认证和授权检查(Authentication and Authorization)。 检查当前Session是否合法。 检查Client的IP地址是否可信赖或不可信赖(IP地址白名单、黑名单)。 请求数据是否先要解压或解码。 是否支持Client请求的类型、Browser版本等。 添加性能监控信息。 添加调试信息。 保证所有异常都被正确捕获到,对未预料到的异常做通用处理,防止给Client看到内部堆栈信息。 在响应返回给客户端之前,有时候也需要做一些预处理再返回: 对响应消息编码或压缩。 为所有响应添加公共头、尾等消息。 进一步Enrich响应消息,如添加公共字段、Session信息、Cookie信息,甚至完全改变响应消息等。 如何实现这样的需求,同时保持可扩展性、可重用性、可配置、移植性?问题解决 要实现这种需求,最直观的方法就是在每个请求处理过程中添加所有这些逻辑,为了减少代码重复,可以将所有这些检查提取成方法,这样在每个处理方法中调用即可: public Response service1(Request request) { validate(request); request = transform(request); Response response = process1(request); return transform(response); } 此时,如果出现service2方法,依然需要拷贝service1中的实现,然后将process1换成process2即可。这个时候我们发现很多重复代码,继续对它重构,比如提取公共逻辑到基类成模版方法,这种使用继承的方式会引起子类对父类的耦合,如果要让某些模块变的可配置需要有太多的判断逻辑,代码变的臃肿;因而可以更进一步,将所有处理逻辑抽象出一个Processor接口,然后使用Decorate模式(即引用优于继承): public interface Processor { Response process(Request request); }public class CoreProcessor implements Processor { public Response process(Request request) { // do process/calculation } }public class DecoratedProcessor implements Processor { private final Processor innerProcessor; public DecoratedProcessor(Processor processor) { this.innerProcessor = processor; } public Response process(Request request) { request = preProcess(request); Response response = innerProcessor.process(request); response = postProcess(response); return response; } protected Request preProcess(Request request) { return request; } protected Response postProcess(Response response) { return response; } }public void Transformer extends DecoratedProcessor { public Transformer(Processor processor) { super(processor); } protected Request preProcess(Request request) { return transformRequest(request); } protected Response postProcess(Response response) { return transformResponse(response); } } 此时,如果需要在真正的处理逻辑之前加入其他的预处理逻辑,只需要继承DecoratedProcessor,实现preProcess或postProcess方法,分别在请求处理之前和请求处理之后横向切入一些逻辑,也就是所谓的AOP编程:面向切面的编程,然后只需要根据需求构建这个链条: Processor processor = new MissingExceptionCatcher(new Debugger(new Transformer(new CoreProcessor()); Response response = processor.process(request); ...... 这已经是相对比较好的设计了,每个Processor只需要关注自己的实现逻辑即可,代码变的简洁;并且每个Processor各自独立,可重用性好,测试方便;整条链上能实现的功能只是取决于链的构造,因而只需要有一种方法配置链的构造即可,可配置性也变得灵活;然而很多时候引用是一种静态的依赖,而无法满足动态的需求。要构造这条链,每个前置Processor需要知道其后的Processor,这在某些情况下并不是在起初就知道的。此时,我们需要引入Intercepting Filter模式来实现动态的改变条链。Intercepting Filter模式 在前文已经构建了一条由引用而成的Processor链,然而这是一条静态链,并且需要一开始就能构造出这条链,为了解决这个限制,我们可以引入一个ProcessorChain来维护这条链,并且这条链可以动态的构建。有多种方式可以实现并控制这个链: 在存储上,可以使用数组来存储所有的Processor,Processor在数组中的位置表示这个Processor在链条中的位置;也可以用链表来存储所有的Processor,此时Processor在这个链表中的位置即是在链中的位置。 在抽象上,可以所有的逻辑都封装在Processor中,也可以将核心逻辑使用Processor抽象,而外围逻辑使用Filter抽象。 在流程控制上,一般通过在Processor实现方法中直接使用ProcessorChain实例(通过参数掺入)来控制流程,利用方法调用的进栈出栈的特性实现preProcess()和postProcess()处理。 在实际中使用这个模式的有:Servlet的Filter机制、Netty的ChannelPipeline中、Structs2中的Interceptor中都实现了这个模式。Intercepting Filter模式在Servlet的Filter中的实现(Jetty版本) 其中Servlet的Filter在Jetty的实现中使用数组存储Filter,Filter末尾可以使用Servlet实例处理真正的业务逻辑,在流程控制上,使用FilterChain的doFilter方法来实现。如FilterChain在Jetty中的实现: public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException // pass to next filter if (_filter < LazyList.size(_chain)) { FilterHolder holder= (FilterHolder)LazyList.get(_chain, _filter++); Filter filter= holder.getFilter(); filter.doFilter(request, response, this); return; } // Call servlet HttpServletRequest srequest = (HttpServletRequest)request; if (_servletHolder != null) { _servletHolder.handle(_baseRequest,request, response); } } 这里,_chain实际上是一个Filter的ArrayList,由FilterChain调用doFilter()启动调用第一个Filter的doFilter()方法,在实际的Filter实现中,需要手动的调用FilterChain.doFilter()方法来启动下一个Filter的调用,利用方法调用的进栈出栈的特性实现Request的pre-process和Response的post-process处理。如果不调用FilterChain.doFilter()方法,则表示不需要调用之后的Filter,流程从当前Filter返回,在它之前的Filter的FilterChain.doFilter()调用之后的逻辑反向处理直到第一个Filter处理完成而返回。 public class MyFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // pre-process ServletRequest chain.doFilter(request, response); // post-process Servlet Response } } 整个Filter链的处理流程如下:Intercepting Filter模式在Netty3中的实现 Netty3在DefaultChannelPipeline中实现了Intercepting Filter模式,其中ChannelHandler是它的Filter。在Netty3的DefaultChannelPipeline中,使用一个以ChannelHandlerContext为节点的双向链表来存储ChannelHandler,所有的横切面逻辑和实际业务逻辑都用ChannelHandler表达,在控制流程上使用ChannelHandlerContext的sendDownstream()和sendUpstream()方法来控制流程。不同于Servlet的Filter,ChannelHandler有两个子接口:ChannelUpstreamHandler和ChannelDownstreamHandler分别用来请求进入时的处理流程和响应出去时的处理流程。对于Client的请求,从DefaultChannelPipeline的sendUpstream()方法入口: public void sendDownstream(ChannelEvent e) { DefaultChannelHandlerContext tail = getActualDownstreamContext(this.tail); if (tail == null) { try { getSink().eventSunk(this, e); return; } catch (Throwable t) { notifyHandlerException(e, t); return; } } sendDownstream(tail, e); }void sendDownstream(DefaultChannelHandlerContext ctx, ChannelEvent e) { if (e instanceof UpstreamMessageEvent) { throw new IllegalArgumentException("cannot send an upstream event to downstream"); } try { ((ChannelDownstreamHandler) ctx.getHandler()).handleDownstream(ctx, e) } catch (Throwable t) { e.getFuture().setFailure(t); notifyHandlerException(e, t); } } 如果有响应消息,该消息从DefaultChannelPipeline的sendDownstream()方法为入口: public void sendUpstream(ChannelEvent e) { DefaultChannelHandlerContext head = getActualUpstreamContext(this.head); if (head == null) { return; } sendUpstream(head, e); }void sendUpstream(DefaultChannelHandlerContext ctx, ChannelEvent e) { try { ((ChannelUpstreamHandler) ctx.getHandler()).handleUpstream(ctx, e); } catch (Throwable t) { notifyHandlerException(e, t); } } 在实际实现ChannelUpstreamHandler或ChannelDownstreamHandler时,调用ChannelHandlerContext中的sendUpstream或sendDownstream方法将控制流程交给下一个ChannelUpstreamHandler或下一个ChannelDownstreamHandler,或调用Channel中的write方法发送响应消息。 public class MyChannelUpstreamHandler implements ChannelUpstreamHandler { public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception { // handle current logic, use Channel to write response if needed. // ctx.getChannel().write(message); ctx.sendUpstream(e); } }public class MyChannelDownstreamHandler implements ChannelDownstreamHandler { public void handleDownstream( ChannelHandlerContext ctx, ChannelEvent e) throws Exception { // handle current logic ctx.sendDownstream(e); } } 当ChannelHandler向ChannelPipelineContext发送事件时,其内部从当前ChannelPipelineContext 节点出发找到下一个ChannelUpstreamHandler或ChannelDownstreamHandler实例,并向其发送 ChannelEvent,对于Downstream链,如果到达链尾,则将ChannelEvent发送给ChannelSink: public void sendDownstream(ChannelEvent e) { DefaultChannelHandlerContext prev = getActualDownstreamContext(this.prev); if (prev == null) { try { getSink().eventSunk(DefaultChannelPipeline.this, e); } catch (Throwable t) { notifyHandlerException(e, t); } } else { DefaultChannelPipeline.this.sendDownstream(prev, e); } }public void sendUpstream(ChannelEvent e) { DefaultChannelHandlerContext next = getActualUpstreamContext(this.next); if (next != null) { DefaultChannelPipeline.this.sendUpstream(next, e); } } 正是因为这个实现,如果在一个末尾的ChannelUpstreamHandler中先移除自己,在向末尾添加一个新的ChannelUpstreamHandler,它是无效的,因为它的next已经在调用前就固定设置为null了。在DefaultChannelPipeline的ChannelHandler链条的处理流程为:在这个实现中,不像Servlet的Filter实现利用方法调用栈的进出栈来完成pre-process和post-process,而是在进去的链和出来的链各自调用handleUpstream()和handleDownstream()方法,这样会引起调用栈其实是两条链的总和,因而需要注意这条链的总长度。这样做的好处是这条ChannelHandler的链不依赖于方法调用栈,而是在DefaultChannelPipeline内部本身的链,因而在handleUpstream()或handleDownstream()可以随时将执行流程转发给其他线程或线程池,只需要保留ChannelPipelineContext引用,在处理完成后用这个ChannelPipelineContext重新向这条链的后一个节点发送ChannelEvent,然而由于Servlet的Filter依赖于方法的调用栈,因而方法返回意味着所有执行完成,这种限制在异步编程中会引起问题,因而Servlet在3.0后引入了Async的支持。Intercepting Filter模式的缺点 简单提一下这个模式的缺点:1. 相对传统的编程模型,这个模式有一定的学习曲线,需要很好的理解该模式后才能灵活的应用它来编程。2. 需要划分不同的逻辑到不同的Filter中,这有些时候并不是那么容易。3. 各个Filter之间共享数据将变得困难。在Netty3中可以自定义自己的ChannelEvent来实现自定义消息的传输,或者使用ChannelPipelineContext的Attachment字段来实现消息传输,而Servlet中的Filter则没有提供类似的机制,如果不是可以配置的数据在Config中传递,其他时候的数据共享需要其他机制配合完成。
前记 第一次听到Reactor模式是三年前的某个晚上,一个室友突然跑过来问我什么是Reactor模式?我上网查了一下,很多人都是给出NIO中的 Selector的例子,而且就是NIO里Selector多路复用模型,只是给它起了一个比较fancy的名字而已,虽然它引入了EventLoop概 念,这对我来说是新的概念,但是代码实现却是一样的,因而我并没有很在意这个模式。然而最近开始读Netty源码,而Reactor模式是很多介绍Netty的文章中被大肆宣传的模式,因而我再次问自己,什么是Reactor模式?本文就是对这个问题关于我的一些理解和尝试着来解答。什么是Reactor模式 要回答这个问题,首先当然是求助Google或Wikipedia,其中Wikipedia上说:“The reactor design pattern is an event handling pattern for handling service requests delivered concurrently by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to associated request handlers.”。从这个描述中,我们知道Reactor模式首先是事件驱动的,有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers;这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler。如果用图来表达:从结构上,这有点类似生产者消费者模式,即有一个或多个生产者将事件放入一个Queue中,而一个或多个消费者主动的从这个Queue中Poll事件来处理;而Reactor模式则并没有Queue来做缓冲,每当一个Event输入到Service Handler之后,该Service Handler会主动的根据不同的Event类型将其分发给对应的Request Handler来处理。更学术的,这篇文章(Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events)上说:“The Reactor design pattern handles service requests that are delivered concurrently to an application by one or more clients. Each service in an application may consistent of several methods and is represented by a separate event handler that is responsible for dispatching service-specific requests. Dispatching of event handlers is performed by an initiation dispatcher, which manages the registered event handlers. Demultiplexing of service requests is performed by a synchronous event demultiplexer. Also known asDispatcher, Notifier”。这段描述和Wikipedia上的描述类似,有多个输入源,有多个不同的EventHandler(RequestHandler)来处理不同的请求,Initiation Dispatcher用于管理EventHander,EventHandler首先要注册到Initiation Dispatcher中,然后Initiation Dispatcher根据输入的Event分发给注册的EventHandler;然而Initiation Dispatcher并不监听Event的到来,这个工作交给Synchronous Event Demultiplexer来处理。Reactor模式结构 在解决了什么是Reactor模式后,我们来看看Reactor模式是由什么模块构成。图是一种比较简洁形象的表现方式,因而先上一张图来表达各个模块的名称和他们之间的关系:Handle:即操作系统中的句柄,是对资源在操作系统层面上的一种抽象,它可以是打开的文件、一个连接(Socket)、Timer等。由于Reactor模式一般使用在网络编程中,因而这里一般指Socket Handle,即一个网络连接(Connection,在Java NIO中的Channel)。这个Channel注册到Synchronous Event Demultiplexer中,以监听Handle中发生的事件,对ServerSocketChannnel可以是CONNECT事件,对SocketChannel可以是READ、WRITE、CLOSE事件等。Synchronous Event Demultiplexer:阻塞等待一系列的Handle中的事件到来,如果阻塞等待返回,即表示在返回的Handle中可以不阻塞的执行返回的事件类型。这个模块一般使用操作系统的select来实现。在Java NIO中用Selector来封装,当Selector.select()返回时,可以调用Selector的selectedKeys()方法获取Set<SelectionKey>,一个SelectionKey表达一个有事件发生的Channel以及该Channel上的事件类型。上图的“Synchronous Event Demultiplexer ---notifies--> Handle”的流程如果是对的,那内部实现应该是select()方法在事件到来后会先设置Handle的状态,然后返回。不了解内部实现机制,因而保留原图。Initiation Dispatcher:用于管理Event Handler,即EventHandler的容器,用以注册、移除EventHandler等;另外,它还作为Reactor模式的入口调用Synchronous Event Demultiplexer的select方法以阻塞等待事件返回,当阻塞等待返回时,根据事件发生的Handle将其分发给对应的Event Handler处理,即回调EventHandler中的handle_event()方法。Event Handler:定义事件处理方法:handle_event(),以供InitiationDispatcher回调使用。Concrete Event Handler:事件EventHandler接口,实现特定事件处理逻辑。Reactor模式模块之间的交互 简单描述一下Reactor各个模块之间的交互流程,先从序列图开始:1. 初始化InitiationDispatcher,并初始化一个Handle到EventHandler的Map。2. 注册EventHandler到InitiationDispatcher中,每个EventHandler包含对相应Handle的引用,从而建立Handle到EventHandler的映射(Map)。3. 调用InitiationDispatcher的handle_events()方法以启动Event Loop。在Event Loop中,调用select()方法(Synchronous Event Demultiplexer)阻塞等待Event发生。4. 当某个或某些Handle的Event发生后,select()方法返回,InitiationDispatcher根据返回的Handle找到注册的EventHandler,并回调该EventHandler的handle_events()方法。5. 在EventHandler的handle_events()方法中还可以向InitiationDispatcher中注册新的Eventhandler,比如对AcceptorEventHandler来,当有新的client连接时,它会产生新的EventHandler以处理新的连接,并注册到InitiationDispatcher中。Reactor模式实现 在Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events中,一直以Logging Server来分析Reactor模式,这个Logging Server的实现完全遵循这里对Reactor描述,因而放在这里以做参考。Logging Server中的Reactor模式实现分两个部分:Client连接到Logging Server和Client向Logging Server写Log。因而对它的描述分成这两个步骤。Client连接到Logging Server1. Logging Server注册LoggingAcceptor到InitiationDispatcher。2. Logging Server调用InitiationDispatcher的handle_events()方法启动。3. InitiationDispatcher内部调用select()方法(Synchronous Event Demultiplexer),阻塞等待Client连接。4. Client连接到Logging Server。5. InitiationDisptcher中的select()方法返回,并通知LoggingAcceptor有新的连接到来。 6. LoggingAcceptor调用accept方法accept这个新连接。7. LoggingAcceptor创建新的LoggingHandler。8. 新的LoggingHandler注册到InitiationDispatcher中(同时也注册到Synchonous Event Demultiplexer中),等待Client发起写log请求。Client向Logging Server写Log1. Client发送log到Logging server。2. InitiationDispatcher监测到相应的Handle中有事件发生,返回阻塞等待,根据返回的Handle找到LoggingHandler,并回调LoggingHandler中的handle_event()方法。3. LoggingHandler中的handle_event()方法中读取Handle中的log信息。4. 将接收到的log写入到日志文件、数据库等设备中。3.4步骤循环直到当前日志处理完成。5. 返回到InitiationDispatcher等待下一次日志写请求。在Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events有对Reactor模式的C++的实现版本,多年不用C++,因而略过。 Java NIO对Reactor的实现 在Java的NIO中,对Reactor模式有无缝的支持,即使用Selector类封装了操作系统提供的Synchronous Event Demultiplexer功能。这个Doug Lea已经在Scalable IO In Java中有非常深入的解释了,因而不再赘述,另外这篇文章对Doug Lea的Scalable IO In Java有一些简单解释,至少它的代码格式比Doug Lea的PPT要整洁一些。需要指出的是,不同这里使用InitiationDispatcher来管理EventHandler,在Doug Lea的版本中使用SelectionKey中的Attachment来存储对应的EventHandler,因而不需要注册EventHandler这个步骤,或者设置Attachment就是这里的注册。而且在这篇文章中,Doug Lea从单线程的Reactor、Acceptor、Handler实现这个模式出发;演化为将Handler中的处理逻辑多线程化,实现类似Proactor模式,此时所有的IO操作还是单线程的,因而再演化出一个Main Reactor来处理CONNECT事件(Acceptor),而多个Sub Reactor来处理READ、WRITE等事件(Handler),这些Sub Reactor可以分别再自己的线程中执行,从而IO操作也多线程化。这个最后一个模型正是Netty中使用的模型。并且在Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events的9.5 Determine the Number of Initiation Dispatchers in an Application中也有相应的描述。EventHandler接口定义 对EventHandler的定义有两种设计思路:single-method设计和multi-method设计:A single-method interface:它将Event封装成一个Event Object,EventHandler只定义一个handle_event(Event event)方法。这种设计的好处是有利于扩展,可以后来方便的添加新的Event类型,然而在子类的实现中,需要判断不同的Event类型而再次扩展成 不同的处理方法,从这个角度上来说,它又不利于扩展。另外在Netty3的使用过程中,由于它不停的创建ChannelEvent类,因而会引起GC的不稳定。A multi-method interface:这种设计是将不同的Event类型在 EventHandler中定义相应的方法。这种设计就是Netty4中使用的策略,其中一个目的是避免ChannelEvent创建引起的GC不稳定, 另外一个好处是它可以避免在EventHandler实现时判断不同的Event类型而有不同的实现,然而这种设计会给扩展新的Event类型时带来非常 大的麻烦,因为它需要该接口。关于Netty4对Netty3的改进可以参考这里:ChannelHandler with no event objectIn 3.x, every I/O operation created a ChannelEvent object. For each read / write, it additionally created a new ChannelBuffer. It simplified the internals of Netty quite a lot because it delegates resource management and buffer pooling to the JVM. However, it often was the root cause of GC pressure and uncertainty which are sometimes observed in a Netty-based application under high load. 4.0 removes event object creation almost completely by replacing the event objects with strongly typed method invocations. 3.x had catch-all event handler methods such as handleUpstream() and handleDownstream(), but this is not the case anymore. Every event type has its own handler method now: 为什么使用Reactor模式 归功与Netty和Java NIO对Reactor的宣传,本文慕名而学习的Reactor模式,因而已经默认Reactor具有非常优秀的性能,然而慕名归慕名,到这里,我还是要不得不问自己Reactor模式的好处在哪里?即为什么要使用这个Reactor模式?在Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events中是这么说的:Reactor Pattern优点 Separation of concerns: The Reactor pattern decouples application-independent demultiplexing and dispatching mechanisms from application-specific hook method functionality. The application-independent mechanisms become reusable components that know how to demultiplex events and dispatch the appropriate hook methods defined by Event Handlers. In contrast, the application-specific functionality in a hook method knows how to perform a particular type of service. Improve modularity, reusability, and configurability of event-driven applications: The pattern decouples application functionality into separate classes. For instance, there are two separate classes in the logging server: one for establishing connections and another for receiving and processing logging records. This decoupling enables the reuse of the connection establishment class for different types of connection-oriented services (such as file transfer, remote login, and video-on-demand). Therefore, modifying or extending the functionality of the logging server only affects the implementation of the logging handler class. Improves application portability: The Initiation Dispatcher’s interface can be reused independently of the OS system calls that perform event demultiplexing. These system calls detect and report the occurrence of one or more events that may occur simultaneously on multiple sources of events. Common sources of events may in- clude I/O handles, timers, and synchronization objects. On UNIX platforms, the event demultiplexing system calls are called select and poll [1]. In the Win32 API [16], the WaitForMultipleObjects system call performs event demultiplexing. Provides coarse-grained concurrency control: The Reactor pattern serializes the invocation of event handlers at the level of event demultiplexing and dispatching within a process or thread. Serialization at the Initiation Dispatcher level often eliminates the need for more complicated synchronization or locking within an application process. 这些貌似是很多模式的共性:解耦、提升复用性、模块化、可移植性、事件驱动、细力度的并发控制等,因而并不能很好的说明什么,特别是它鼓吹的对性能的提升,这里并没有体现出来。当然在这篇文章的开头有描述过另一种直观的实现:Thread-Per-Connection,即传统的实现,提到了这个传统实现的以下问题:Thread Per Connection缺点 Efficiency: Threading may lead to poor performance due to context switching, synchronization, and data movement [2]; Programming simplicity: Threading may require complex concurrency control schemes; Portability: Threading is not available on all OS platforms.对于性能,它其实就是第一点关于Efficiency的描述,即线程的切换、同步、数据的移动会引起性能问题。也就是说从性能的角度上,它最大的提升就是减少了性能的使用,即不需要每个Client对应一个线程。我的理解,其他业务逻辑处理很多时候也会用到相同的线程,IO读写操作相对CPU的操作还是要慢很多,即使Reactor机制中每次读写已经能保证非阻塞读写,这里可以减少一些线程的使用,但是这减少的线程使用对性能有那么大的影响吗?答案貌似是肯定的,这篇论文(SEDA: Staged Event-Driven Architecture - An Architecture for Well-Conditioned, Scalable Internet Service)对随着线程的增长带来性能降低做了一个统计:在这个统计中,每个线程从磁盘中读8KB数据,每个线程读同一个文件,因而数据本身是缓存在操作系统内部的,即减少IO的影响;所有线程是事先分配的,不会有线程启动的影响;所有任务在测试内部产生,因而不会有网络的影响。该统计数据运行环境:Linux 2.2.14,2GB内存,4-way 500MHz Pentium III。从图中可以看出,随着线程的增长,吞吐量在线程数为8个左右的时候开始线性下降,并且到64个以后而迅速下降,其相应事件也在线程达到256个后指数上升。即1+1<2,因为线程切换、同步、数据移动会有性能损失,线程数增加到一定数量时,这种性能影响效果会更加明显。对于这点,还可以参考C10K Problem,用以描述同时有10K个Client发起连接的问题,到2010年的时候已经出现10M Problem了。当然也有人说:Threads are expensive are no longer valid.在不久的将来可能又会发生不同的变化,或者这个变化正在、已经发生着?没有做过比较仔细的测试,因而不敢随便断言什么,然而本人观点,即使线程变的影响并没有以前那么大,使用Reactor模式,甚至时SEDA模式来减少线程的使用,再加上其他解耦、模块化、提升复用性等优点,还是值得使用的。Reactor模式的缺点 Reactor模式的缺点貌似也是显而易见的:1. 相比传统的简单模型,Reactor增加了一定的复杂性,因而有一定的门槛,并且不易于调试。2. Reactor模式需要底层的Synchronous Event Demultiplexer支持,比如Java中的Selector支持,操作系统的select系统调用支持,如果要自己实现Synchronous Event Demultiplexer可能不会有那么高效。3. Reactor模式在IO读写数据时还是在同一个线程中实现的,即使使用多个Reactor机制的情况下,那些共享一个Reactor的Channel如果出现一个长时间的数据读写,会影响这个Reactor中其他Channel的相应时间,比如在大文件传输时,IO操作就会影响其他Client的相应时间,因而对这种操作,使用传统的Thread-Per-Connection或许是一个更好的选择,或则此时使用Proactor模式。参考 Reactor Pattern WikiPediaReactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous EventsScalable IO In JavaC10K Problem WikiPedia
前言 这是《深入HBase架构解析(一)》的续,不多废话,继续。。。。HBase读的实现 通过前文的描述,我们知道在HBase写时,相同Cell(RowKey/ColumnFamily/Column相同)并不保证在一起,甚至删除一个Cell也只是写入一个新的Cell,它含有Delete标记,而不一定将一个Cell真正删除了,因而这就引起了一个问题,如何实现读的问题?要解决这个问题,我们先来分析一下相同的Cell可能存在的位置:首先对新写入的Cell,它会存在于MemStore中;然后对之前已经Flush到HDFS中的Cell,它会存在于某个或某些StoreFile(HFile)中;最后,对刚读取过的Cell,它可能存在于BlockCache中。既然相同的Cell可能存储在三个地方,在读取的时候只需要扫瞄这三个地方,然后将结果合并即可(Merge Read),在HBase中扫瞄的顺序依次是:BlockCache、MemStore、StoreFile(HFile)。其中StoreFile的扫瞄先会使用Bloom Filter过滤那些不可能符合条件的HFile,然后使用Block Index快速定位Cell,并将其加载到BlockCache中,然后从BlockCache中读取。我们知道一个HStore可能存在多个StoreFile(HFile),此时需要扫瞄多个HFile,如果HFile过多又是会引起性能问题。Compaction MemStore每次Flush会创建新的HFile,而过多的HFile会引起读的性能问题,那么如何解决这个问题呢?HBase采用Compaction机制来解决这个问题,有点类似Java中的GC机制,起初Java不停的申请内存而不释放,增加性能,然而天下没有免费的午餐,最终我们还是要在某个条件下去收集垃圾,很多时候需要Stop-The-World,这种Stop-The-World有些时候也会引起很大的问题,比如参考本人写的这篇文章,因而设计是一种权衡,没有完美的。还是类似Java中的GC,在HBase中Compaction分为两种:Minor Compaction和Major Compaction。 Minor Compaction是指选取一些小的、相邻的StoreFile将他们合并成一个更大的StoreFile,在这个过程中不会处理已经Deleted或Expired的Cell。一次Minor Compaction的结果是更少并且更大的StoreFile。(这个是对的吗?BigTable中是这样描述Minor Compaction的:As write operations execute, the size of the memtable in- creases. When the memtable size reaches a threshold, the memtable is frozen, a new memtable is created, and the frozen memtable is converted to an SSTable and written to GFS. This minor compaction process has two goals: it shrinks the memory usage of the tablet server, and it reduces the amount of data that has to be read from the commit log during recovery if this server dies. Incom- ing read and write operations can continue while com- pactions occur. 也就是说它将memtable的数据flush的一个HFile/SSTable称为一次Minor Compaction) Major Compaction是指将所有的StoreFile合并成一个StoreFile,在这个过程中,标记为Deleted的Cell会被删除,而那些已经Expired的Cell会被丢弃,那些已经超过最多版本数的Cell会被丢弃。一次Major Compaction的结果是一个HStore只有一个StoreFile存在。Major Compaction可以手动或自动触发,然而由于它会引起很多的IO操作而引起性能问题,因而它一般会被安排在周末、凌晨等集群比较闲的时间。 更形象一点,如下面两张图分别表示Minor Compaction和Major Compaction。HRegion Split 最初,一个Table只有一个HRegion,随着数据写入增加,如果一个HRegion到达一定的大小,就需要Split成两个HRegion,这个大小由hbase.hregion.max.filesize指定,默认为10GB。当split时,两个新的HRegion会在同一个HRegionServer中创建,它们各自包含父HRegion一半的数据,当Split完成后,父HRegion会下线,而新的两个子HRegion会向HMaster注册上线,处于负载均衡的考虑,这两个新的HRegion可能会被HMaster分配到其他的HRegionServer中。关于Split的详细信息,可以参考这篇文章:《Apache HBase Region Splitting and Merging》。HRegion负载均衡 在HRegion Split后,两个新的HRegion最初会和之前的父HRegion在相同的HRegionServer上,出于负载均衡的考虑,HMaster可能会将其中的一个甚至两个重新分配的其他的HRegionServer中,此时会引起有些HRegionServer处理的数据在其他节点上,直到下一次Major Compaction将数据从远端的节点移动到本地节点。HRegionServer Recovery 当一台HRegionServer宕机时,由于它不再发送Heartbeat给ZooKeeper而被监测到,此时ZooKeeper会通知HMaster,HMaster会检测到哪台HRegionServer宕机,它将宕机的HRegionServer中的HRegion重新分配给其他的HRegionServer,同时HMaster会把宕机的HRegionServer相关的WAL拆分分配给相应的HRegionServer(将拆分出的WAL文件写入对应的目的HRegionServer的WAL目录中,并并写入对应的DataNode中),从而这些HRegionServer可以Replay分到的WAL来重建MemStore。HBase架构简单总结 在NoSQL中,存在著名的CAP理论,即Consistency、Availability、Partition Tolerance不可全得,目前市场上基本上的NoSQL都采用Partition Tolerance以实现数据得水平扩展,来处理Relational DataBase遇到的无法处理数据量太大的问题,或引起的性能问题。因而只有剩下C和A可以选择。HBase在两者之间选择了Consistency,然后使用多个HMaster以及支持HRegionServer的failure监控、ZooKeeper引入作为协调者等各种手段来解决Availability问题,然而当网络的Split-Brain(Network Partition)发生时,它还是无法完全解决Availability的问题。从这个角度上,Cassandra选择了A,即它在网络Split-Brain时还是能正常写,而使用其他技术来解决Consistency的问题,如读的时候触发Consistency判断和处理。这是设计上的限制。从实现上的优点: HBase采用强一致性模型,在一个写返回后,保证所有的读都读到相同的数据。 通过HRegion动态Split和Merge实现自动扩展,并使用HDFS提供的多个数据备份功能,实现高可用性。 采用HRegionServer和DataNode运行在相同的服务器上实现数据的本地化,提升读写性能,并减少网络压力。 内建HRegionServer的宕机自动恢复。采用WAL来Replay还未持久化到HDFS的数据。 可以无缝的和Hadoop/MapReduce集成。 实现上的缺点: WAL的Replay过程可能会很慢。 灾难恢复比较复杂,也会比较慢。 Major Compaction会引起IO Storm。 。。。。 参考: https://www.mapr.com/blog/in-depth-look-hbase-architecture#.VdNSN6Yp3qxhttp://jimbojw.com/wiki/index.php?title=Understanding_Hbase_and_BigTablehttp://hbase.apache.org/book.html http://www.searchtb.com/2011/01/understanding-hbase.html http://research.google.com/archive/bigtable-osdi06.pdf
问题起因 依然是在使用GemFire的集群中,我们发现偶尔会出现一些GemFire的Function执行特别慢,并且超过了两分钟(为了保证数据的一致性,我们在写之前需要先拿一个Lock,因为不能每个Key都对应一个Lock,因而我们使用了Guava的Stripe Lock(关于Stripe Lock可以参考这里),而且这个Lock本身我们指定了2分钟的超时时间,因而如果写超过两分钟,我们就会收到Exception)。这个问题其实已经困扰了我们好几年了,刚前段时间,我们发现长时间的Stop-The-World GC会引起这个问题,而且这种时候很多时候会引起那个节点从集群中退出,并不是所有的这种错误都有GC的问题,我特地查了GC的日志,有些这种写超过两分钟的情况下,GC一直处于非常健康的状态,而且查了GemFire的日志和我们自己的日志,也没有发现任何异常。由于我们每个数据保留两分份拷贝,也就是说每次数据写都要写两个节点,两分钟对CPU来说可以做太多的事情,因而只有IO才能在某些时候产生这种问题,在问题发生的时候也没有任何overflow数据,而且本地操作,即使对IO来说2分钟也是一个非常长的时间了,因而我们只能怀疑这是写另一个节点引起的,对另一个节点,它是在同一个Data Center中,而且基本是在同一个Chasis内部,因而它们之间小于1M的数据量通信也不太可能花去2分钟的时间,所以剩下的我们就只能怀疑网络的问题了,比如数据丢包、网络抖动、网络流量太大一起传输变慢等,但是我们没有找到任何相关的问题。所以我们很长一段时间素手无策,只能怪GemFire闭源,我们不知道这两分钟是不是GemFire自己内部在做一些不为人知的事情,因而太忙了而每来得及处理我们的写请求。虽然我一直觉得不管在处理什么炒作,两分钟都没有响应根本无法解释的通,更何况GemFire节点之间并没有报告有任何异常,或者像以前发现的一个节点向Locator举报另一个节点没有响应的问题,Locator自己也能很正常的向那个节点发送新的成员信息(View),因而看起来向是这个节点虽然花了两分钟多来写一个数据,但是它还是有响应的,有点“假死”的赶脚。问题发现 这个问题这么几年以来时不时的就会发生,而且因为以前花的时间太多了,而且也没有找到任何出错的地方,现在索性不去花太多时间在上面了,更何况这个它很长时间才发生一次,并且今年以来就一直没发生过,直到前几周出现一次,我有点不信邪的重新去看这个问题,依然没有找到任何可疑的地方,GC日志、应用程序日志、GemFire自己的日志、网络、CPU使用情况等所有的都是正常的,除了问题发生的那个时刻,应用程序没有任何日志,另外在问题发生之前出现过Log4J日志文件的Rolling(我们使用RollingFileAppender,并且只保留20个日志文件),但是Log4J日志文件Roll的日志出现了断结,在开始要Roll到真正完成Roll中间还有几行GemFire自身的日志,此时我并没有觉得这个是有很大问题的,因为我始终觉得Log4J除了它自己提到平均对性能有10%的影响以外,它就是一个简单的把日志写到文件的过程,不会影响的整个应用程序本身,因为它太简单了,直到今天这个问题再次出现,依然没有任何其他方面的收获,所有的地方都显示正常状态,甚至我们之前发现的网卡问题今天也没有发生,然而同样是出问题的两分钟没有出现应用程序日志,日志文件Roll的日志和上次类似,开始Roll到结束出现GemFire日志的交叉。 最近一次发生的日志 [info 2015/08/12 01:56:07.736 BST …] ClientHealthMonitor: Registering client with member id … log4j: rolling over count=20971801 log4j: maxBackupIndex=20 [info 2015/08/12 01:56:12.265 BST …] ClientHealthMonitor: Unregistering client with member id … …… [info 2015/08/12 01:56:23.773 BST …] ClientHealthMonitor: Registering client with member id … log4j: Renaming file logs/….log.19 to logs/….log.20 一周前发生的日志 [info 2015/08/04 01:43:45.761 BST …] ClientHealthMonitor: Registering client with member id … log4j: rolling over count=20971665 log4j: maxBackupIndex=20 …… [info 2015/08/04 01:45:25.506 BST …] ClientHealthMonitor: Registering client with member id … log4j: Renaming file logs/….log.19 to logs/….log.20 看似这个是一个规律(套用同事的一句话:一次发生时偶然,两次发生就是科学了)。然而此时我其实依然不太相信Log4J是“凶手”,因为我一直觉得Log4J是一个简单的日志输出框架,它要是出问题也只是它自己的问题,是局部的,而这个问题的出现明显是全局的,直到我突然脑子一闪而过,日志打印的操作是synchronized,也就是说在日志文件Roll的时候,所有其它需要打日志的线程都要等待直到Roll完成,如果这个Roll过程超过了2分钟,那么就会发生我们看到的Stripe Lock超时,也就是发生了程序“假死”的状态。重新查看Log4J打印日志的方法调用栈,它会在两个地方用synchronized,即同一个Category(Logger)类实例: public void callAppenders(LoggingEvent event) { int writes = 0; for(Category c = this; c != null; c=c.parent) { // Protected against simultaneous call to addAppender, removeAppender, synchronized(c) { if(c.aai != null) { writes += c.aai.appendLoopOnAppenders(event); } if(!c.additive) { break; } } } 。。。 } 以及同一个Appender在doApppend时: public synchronized void doAppend(LoggingEvent event) { 。。。 this.append(event); } 而Roll的过程就是在append方法中,进一步分析,在下面两句话之间,他们分别花费了超过100s和超过11s的时间: log4j: maxBackupIndex=20 。。。 log4j: Renaming file logs/….log.19 to logs/….log.20 而这两句之间只包含了两个File.exists(),一个File.delete(),一个File.rename()操作: public void rollOver() { 。。。 if(maxBackupIndex > 0) { // Delete the oldest file, to keep Windows happy. file = new File(fileName + '.' + maxBackupIndex); if (file.exists()) renameSucceeded = file.delete(); for (int i = maxBackupIndex - 1; i >= 1 && renameSucceeded; i--) { file = new File(fileName + "." + i); if (file.exists()) { target = new File(fileName + '.' + (i + 1)); LogLog.debug("Renaming file " + file + " to " + target); renameSucceeded = file.renameTo(target); } } 。。。 } } NFS简单性能测试和分析 因而我对NFS的性能作了一些简单测试: 只有一个线程时,在NFS下rename性能: 1 file: 3ms 10 files: 48ms 20 files: 114ms 相比较,在本地磁盘rename的性能: 1 file: 1ms 3 files: 1ms 10 files: 3ms 对NFS和本地磁盘写的性能(模拟日志,每行都会flush): NFS LOCAL 1 writer, 11M 443ms 238ms 1 writer, 101M 2793ms 992ms 10 writers, 11M ~4400ms ~950ms 10 writers, 101M ~30157ms ~5500ms 一些其他的统计: 100同时写: Create 20 files spend: 301ms Renaming 20 files spends: 333ms Delete 20 files spends: 329ms 1000同时写: Create 20 files spend: 40145ms Renaming 20 files spends: 39273ms 而在1000个同时写的过程中,重命名: Rename file: LogTest1.50 take: 36434ms Rename file: LogTest1.51 take: 39ms Rename file: LogTest1.52 take: 34ms 也就是说在这个模拟过程中,一个文件的rename超过36s,而向我们有十几台机器同时使用相同的NFS,并且每台机器上都跑二三十个程序,如果那段时间同时有上万个的日志写,可以预计达到100s情况是可能发生的。 关于NFS性能的问题,在《构建高性能WEB站点》的书(330页)中也有涉及。简单的介绍,NFS由Sun在1984年开发,是主流异构平台实现文件共享的首选方案。它并没有自己的传输协议,而是使用RPC(Remote Procedure Call)协议(应用层),RPC协议默认底层基于UDP传输,但是自己实现在丢包时的重传机制,而且NFS服务器采用多进程模型,默认进程为4,但是一般都会调优增加服务进程数,然而“不管怎么对NFS进行性能优化,NFS注定不适合作为I/O密集型文件共享方案,但可以作为一般用途,比如提供站点内部的资源共享,它的优势在于容易搭建,而且可以减少不必要的数据冗余。” 可以使用命令:“nfsstat -c”获取对NFS服务器的操作的简单统计,具体可以参考《构建高性能WEB站点》的相关章节,里面还有更详细的对NFS服务器性能的测试。 总结 从这个事件我总结了两件事情: 1. 日志的影响可能是全局性的,因而要非常小心,一个耗时的操作可能引起程序的“假死”,因而要非常小心。 2. 虽然把日志打印在NFS上,对大量的日志文件查找会方便很多,但是这是一个很耗性能的设计,特别是当大量的程序共享这个NFS的时候,因而要尽量避免。
一直想好好学习concurrent包中的各个类的实现,然而经常看了一点就因为其他事情干扰而放下了。发现这样太不利于自己的成长了,因而最近打算潜心一件一件的完成自己想学习的东西。 对concurrent包的学习打算先从Lock的实现开始,因而自然而然的就端起了AbstractQueuedSynchronizer,然而要读懂这个类的源码并不是那么容易,因而我就开始问自己一个问题:如果自己要去实现这个一个Lock对象,应该如何实现呢? 要实现Lock对象,首先理解什么是锁?我自己从编程角度简单的理解,所谓锁对象(互斥锁)就是它能保证一次只有一个线程能进入它保护的临界区,如果有一个线程已经拿到锁对象,那么其他对象必须让权等待,而在该线程退出这个临界区时需要唤醒等待列表中的其他线程。更学术一些,《计算机操作系统》中对同步机制准则的归纳(P50): 空闲让进。当无进程处于临界区时,表明临界资源处于空闲状态,应允许一个请求进入临界区的进程立即进入自己的临界区,以有效的利用临界资源。 忙则等待。当已有进程进入临界区时,表明临界资源正在被访问,因而其他试图进入临界区的进程必须等待,以保证对临界区资源的互斥访问。 有限等待。对要求访问临界资源的进程,应保证在有限时间内能进入自己的临界区,以免陷入“死等”状态。 让权等待。当进程不能进入自己的临界区时,应该释放处理机,以免进程陷入“忙等”状态。 说了那么多,其实对互斥锁很简单,只需要一个标记位,如果该标记位为0,表示没有被占用,因而直接获得锁,然后把该标记位置为1,此时其他线程发现该标记位已经是1,因而需要等待。这里对这个标记位的比较并设值必须是原子操作,而在JDK5以后提供的atomic包里的工具类可以很方便的提供这个原子操作。然而上面的四个准则应该漏了一点,即释放锁的线程(进程)和得到锁的线程(进程)应该是同一个,就像一把钥匙对应一把锁(理想的),所以一个非常简单的Lock类可以这么实现: public class SpinLockV1 { private final AtomicInteger state = new AtomicInteger(0); private volatile Thread owner; // 这里owner字段可能存在中间值,不可靠,因而其他线程不可以依赖这个字段的值 public void lock() { while (!state.compareAndSet(0, 1)) { } owner = Thread.currentThread(); } public void unlock() { Thread currentThread = Thread.currentThread(); if (owner != currentThread || !state.compareAndSet(1, 0)) { throw new IllegalStateException("The lock is not owned by thread: " + currentThread); } owner = null; } } 一个简单的测试方法: @Test public void testLockCorrectly() throws InterruptedException { final int COUNT = 100; Thread[] threads = new Thread[COUNT]; SpinLockV1 lock = new SpinLockV1(); AddRunner runner = new AddRunner(lock); for (int i = 0; i < COUNT; i++) { threads[i] = new Thread(runner, "thread-" + i); threads[i].start(); } for (int i = 0; i < COUNT; i++) { threads[i].join(); } assertEquals(COUNT, runner.getState()); } private static class AddRunner implements Runnable { private final SpinLockV1 lock; private int state = 0; public AddRunner(SpinLockV1 lock) { this.lock = lock; } public void run() { lock.lock(); try { quietSleep(10); state++; System.out.println(Thread.currentThread().getName() + ": " + state); } finally { lock.unlock(); } } public int getState() { return state; } } 然而这个SpinLock其实并不需要state这个字段,因为owner的赋值与否也是一种状态,因而可以用它作为一种互斥状态: public class SpinLockV2 { private final AtomicReference<Thread> owner = new AtomicReference<Thread>(null); public void lock() { final Thread currentThread = Thread.currentThread(); while (!owner.compareAndSet(null, currentThread)) { } } public void unlock() { Thread currentThread = Thread.currentThread(); if (!owner.compareAndSet(currentThread, null)) { throw new IllegalStateException("The lock is not owned by thread: " + currentThread); } } } 这在操作系统中被定义为整形信号量,然而整形信号量如果没拿到锁会一直处于“忙等”状态(没有遵循有限等待和让权等待的准则),因而这种锁也叫Spin Lock,在短暂的等待中它可以提升性能,因为可以减少线程的切换,concurrent包中的Atomic大部分都采用这种机制实现,然而如果需要长时间的等待,“忙等”会占用不必要的CPU时间,从而性能会变的很差,这个时候就需要将没有拿到锁的线程放到等待列表中,这种方式在操作系统中也叫记录型信号量,它遵循了让权等待准则(当前没有实现有限等待准则)。在JDK6以后提供了LockSupport.park()/LockSupport.unpark()操作,可以将当前线程放入一个等待列表或将一个线程从这个等待列表中唤醒。然而这个park/unpark的等待列表是一个全局的等待列表,在unpartk的时候还是需要提供需要唤醒的Thread对象,因而我们需要维护自己的等待列表,但是如果我们可以用JDK提供的工具类ConcurrentLinkedQueue,就非常容易实现,如LockSupport文档中给出来的代码事例: class FIFOMutex { private final AtomicBoolean locked = new AtomicBoolean(false); private final Queue<Thread> waiters = new ConcurrentLinkedQueue<Thread>(); public void lock() { boolean wasInterrupted = false; Thread current = Thread.currentThread(); waiters.add(current); // Block while not first in queue or cannot acquire lock while (waiters.peek() != current || !locked.compareAndSet(false, true)) { LockSupport.park(this); if (Thread.interrupted()) // ignore interrupts while waiting wasInterrupted = true; } waiters.remove(); if (wasInterrupted) // reassert interrupt status on exit current.interrupt(); } public void unlock() { locked.set(false); LockSupport.unpark(waiters.peek()); } } 在该代码事例中,有一个线程等待队列和锁标记字段,每次调用lock时先将当前线程放入这个等待队列中,然后拿出队列头线程对象,如果该线程对象正好是当前线程,并且成功 使用CAS方式设置locked字段(这里需要两个同时满足,因为可能出现一个线程已经从队列中移除了但还没有unlock,此时另一个线程调用lock方法,此时队列头的线程就是第二个线程,然而由于第一个线程还没有unlock或者正在unlock,因而需要使用CAS原子操作来判断是否要park),表示该线程竞争成功,获得锁,否则将当前线程park,这里之所以要放在 while循环中,因为park操作可能无理由返回(spuriously),如文档中给出的描述: LockSupport.park()public static void park(Object blocker) Disables the current thread for thread scheduling purposes unless the permit is available. If the permit is available then it is consumed and the call returns immediately; otherwise the current thread becomes disabled for thread scheduling purposes and lies dormant until one of three things happens: Some other thread invokes unpark with the current thread as the target; or Some other thread interrupts the current thread; or The call spuriously (that is, for no reason) returns. This method does not report which of these caused the method to return. Callers should re-check the conditions which caused the thread to park in the first place. Callers may also determine, for example, the interrupt status of the thread upon return. Parameters: blocker - the synchronization object responsible for this thread parking Since: 1.6 我在实现自己的类时就被这个“无理由返回”坑了好久。对于已经获得锁的线程,将该线程从等待队列中移除,这里由于ConcurrentLinkedQueue是线程安全的,因而能保证每次都是队列头的线程得到锁,因而在得到锁匙将队列头移除。unlock逻辑比较简单,只需要将locked字段打开(设置为false),唤醒(unpark)队列头的线程即可,然后该线程会继续在lock方法的while循环中继续竞争unlocked字段,并将它自己从线程队列中移除表示获得锁成功。当然安全起见,最好在unlock中加入一些验证逻辑,如解锁的线程和加锁的线程需要相同。然而本文的目的是自己实现一个Lock对象,即只使用一些基本的操作,而不使用JDK提供的Atomic类和ConcurrentLinkedQueue。类似的首先我们也需要一个队列存放等待线程队列(公平起见,使用先进先出队列),因而先定义一个Node对象用以构成这个队列: protected static class Node { volatile Thread owner; volatile Node prev; volatile Node next; public Node(Thread owner) { this.owner = owner; this.state = INIT; } public Node() { this(Thread.currentThread()); } } 简单起见,队列头是一个起点的placeholder,每个调用lock的线程都先将自己竞争放入这个队列尾,每个队列头后一个线程(Node)即是获得锁的线程,所以我们需要有head Node字段用以快速获取队列头的后一个Node,而tail Node字段用来快速插入新的Node,所以关键在于如何线程安全的构建这个队列,方法还是一样的,使用CAS操作,即CAS方法将自己设置成tail值,然后重新构建这个列表: protected boolean enqueue(Node node) { while (true) { final Node preTail = tail; node.prev = preTail; if (compareAndSetTail(preTail, node)) { preTail.next = node; return node.prev == head; } } } 在当前线程Node以线程安全的方式放入这个队列后,lock实现相对就比较简单了,如果当前Node是的前驱是head,该线程获得锁,否则park当前线程,处理park无理由返回的问题,因而将park放入while循环中(该实现是一个不可重入的实现): public void lock() { // Put the latest node to a queue first, then check if the it is the first node // this way, the list is the only shared resource to deal with Node node = new Node(); if (enqueue(node)) { current = node.owner; } else { while (node.prev != head) { LockSupport.park(this); // This may return "spuriously"!!, so put it to while } current = node.owner; } } unlock的实现需要考虑多种情况,如果当前Node(head.next)有后驱,那么直接unpark该后驱即可;如果没有,表示当前已经没有其他线程在等待队列中,然而在这个判断过程中可能会有其他线程进入,因而需要用CAS的方式设置tail,如果设置失败,表示此时有其他线程进入,因而需要将该新进入的线程unpark从而该新进入的线程在调用park后可以立即返回(这里的CAS和enqueue的CAS都是对tail操作,因而能保证状态一致): public void unlock() { Node curNode = unlockValidate(); Node next = curNode.next; if (next != null) { head.next = next; next.prev = head; LockSupport.unpark(next.owner); } else { if (!compareAndSetTail(curNode, head)) { while (curNode.next == null) { } // Wait until the next available // Another node queued during the time, so we have to unlock that, or else, this node can never unparked unlock(); } else { compareAndSetNext(head, curNode, null); // Still use CAS here as the head.next may already been changed } } } 具体的代码和测试类可以参考查看这里。 其实直到自己写完这个类后才直到者其实这是一个MCS锁的变种,因而这个实现每个线程park在自身对应的node上,而由前一个线程unpark它;而AbstractQueuedSynchronizer是CLH锁,因为它的park由前驱状态决定,虽然它也是由前一个线程unpark它。具体可以参考这里。
非常好的术语参考表,纪录下来以防以后忘了。转自:http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html HotSpot Glossary of Terms A work in progress, especially as the HotSpot VM evolves. But a place to put definitions of things so we only have to define them once. There are empty entries (marked TBD for "to be defined") because we think of things that we need to define faster than we think of good definitions. adaptive spinning An optimization technique whereby a thread spins waiting for a change-of-state to occur (typically a flag that represents some event has occurred - such as the release of a lock) rather than just blocking until notified that the change has occurred. The "adaptive" part comes from the policy decisions that control how long the thread will spin until eventually deciding to block. biased locking An optimization in the VM that leaves an object as logically locked by a given thread even after the thread has released the lock. The premise is that if the thread subsequently reacquires the lock (as often happens), then reacquisition can be achieved at very low cost. If a different thread tries to acquire a biased lock then the bias must be revoked from the current bias owner. block start table A table that shows, for a region of the heap, where the object starts that comes on to this region from lower addresees. Used, for example, with thecard table variant of the remembered set. bootstrap classloader The logical classloader that has responsibility for loading the classes (and resources) that are found in the boot-classpath - typically the core Java platform classes. Typically implemented as part of the VM, by historical convention the bootstrap classloader is represented by NULL at the Java API level. bytecode verification A step in the linking process of a class where the methods bytecodes are analyzed to ensure type-safety. C1 compiler Fast, lightly optimizing bytecode compiler. Performs some value numbering, inlining, and class analysis. Uses a simple CFG-oriented SSA "high" IR, a machine-oriented "low" IR, a linear scan register allocation, and a template-style code generator. C2 compiler Highly optimizing bytecode compiler, also known as 'opto'. Uses a "sea of nodes" SSA "ideal" IR, which lowers to a machine-specific IR of the same kind. Has a graph-coloring register allocator; colors all machine state, including local, global, and argument registers and stack. Optimizations include global value numbering, conditional constant type propagation, constant folding, global code motion, algebraic identities, method inlining (aggressive, optimistic, and/or multi-morphic), intrinsic replacement, loop transformations (unswitching, unrolling), array range check elimination. card table A kind of remembered set that records where oops have changed in a generation. class data sharing A startup optimization that records the in-memory form of some classes, so that that form can be mapped into memory by a subsequent run of the virtual machine, rather than loading those classes from their class files. class hierachy analysis Also known as 'CHA'. Analysis of the class tree used by a compiler to determine if the receiver at a virtual call site has a single implementor. If so, the callee can be inlined or the compiler can employ some other static call mechanism. code cache A special heap that holds compiled code. These objects are not relocated by the GC, but may contain oops, which serve as GC roots. compaction A garbage collection technique that results in live objects occupying a dense portion of the virtual address space, and available space in another portion of the address space. Cf. free list. concurrency Concurrency, or more specifically concurrent programming, is the logical simultaneous execution of multiple instruction streams. If multiple processors are available then the logical simultaneity can be physical simultaneity - this is known as 'parallelism' concurrent garbage collection A garbage collection algorithm that does most (if not all) of its work while the Java application threads are still running. copying garbage collection A garbage collection algorithm that moves objects during the collection. deoptimization The process of converting an compiled (or more optimized) stack frame into an interpreted (or less optimized) stack frame. Also describes the discarding of an nmethod whose dependencies (or other assumptions) have been broken. Deoptimized nmethods are typically recompiled to adapt to changing application behavior. Example: A compiler initially assumes a reference value is never null, and tests for it using a trapping memory access. Later on, the application uses null values, and the method is deoptimized and recompiled to use an explicit test-and-branch idiom to detect such nulls. dependency An optimistic assumption associated with an nmethod, which allowed the compiler to emit improved code into the nmethod. Example: A given class has no subclasses, which simplifies method dispatch and type testing. The loading of new classes (or replacement of old classes) can cause dependencies to become false, which requires dependent nmethods to be discarded and activations of those nmethods to be deoptimized. eden A part of the Java object heap where object can be created efficiently. free list A storage management technique in which unused parts of the Java object heap are chained one to the next, rather than having all of the unused part of the heap in a single block. garbage collection The automatic management of storage. garbage collection root A pointer into the Java object heap from outside the heap. These come up, e.g., from static fields of classes, local references in activation frames, etc. GC map A description emitted by the JIT (C1 or C2) of the locations of oops in registers or on stack in a compiled stack frame. Each code location which might execute a safepoint has an associated GC map. The GC knows how to parse a frame from a stack, to request a GC map from a frame's nmethod, and to unpack the GC map and manage the indicated oops within the stack frame. generational garbage collection A storage management technique that separates objects expected to be referenced for different lengths of time into different regions of the heap, so that different algorithms can be applied to the collection of those regions. handle A memory word containing an oop. The word is known to the GC, as a root reference. C/C++ code generally refers to oops indirectly via handles, to enable the GC to find and manage its root set more easily. Whenever C/C++ code blocks in a safepoint, the GC may change any oop stored in a handle. Handles are either 'local' (thread-specific, subject to a stack discipline though not necessarily on the thread stack) or global (long-lived and explicitly deallocated). There are a number of handle implementations throughout the VM, and the GC knows about them all. hot lock A lock that is highly contended. interpreter A VM module which implements method calls by individually executing bytecodes. The interpreter has a limited set of highly stylized stack frame layouts and register usage patterns, which it uses for all method activations. The Hotspot VM generates its own interpreter at start-up time. JIT compilers An on-line compiler which generates code for an application (or class library) during execution of the application itself. ("JIT" stands for "just in time".) A JIT compiler may create machine code shortly before the first invocation of a Java method. Hotspot compilers usually allow the interpreter ample time to "warm up" Java methods, by executing them thousands of times. This warm-up period allows a compiler to make better optimization decisions, because it can observe (after initial class loading) a more complete class hierarchy. The compiler can also inspect branch and type profile information gathered by the interpreter. JNI The Java Native Interface - a specification and API for how Java code can call out to native C code, and how native C code can call into the Java VM JVM TI The Java Virtual Machine Tools Interface - a standard specification and API that is used by development and monitoring tools. See JVM TI for more information. klass pointer The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original object. For Java objects, the "klass" contains a C++ style "vtable". mark word The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits. nmethod A block of executable code which implements some Java bytecodes. It may be a complete Java method, or an 'OSR' method. It routinely includes object code for additional methods inlined by the compiler. object header Common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.) Includes fundamental information about the heap object's layout, type, GC state, synchronization state, and identity hash code. Consists of two words. In arrays it is immediately followed by a length field. Note that both Java objects and VM-internal objects have a common object header format. object promotion The act of copying an object from one generation to another. old generation A region of the Java object heap that holds object that have remained referenced for a while. on-stack replacement Also known as 'OSR'. The process of converting an interpreted (or less optimized) stack frame into a compiled (or more optimized) stack frame. This happens when the interpreter discovers that a method is looping, requests the compiler to generate a special nmethod with an entry point somewhere in the loop (specifically, at a backward branch), and transfers control to that nmethod. A rough inverse to deoptimization. oop An object pointer. Specifically, a pointer into the GC-managed heap. (The term is traditional. One 'o' may stand for 'ordinary'.) Implemented as a native machine address, not a handle. Oops may be directly manipulated by compiled or interpreted Java code, because the GC knows about the liveness and location of oops within such code. (See GC map.) Oops can also be directly manipulated by short spans of C/C++ code, but must be kept by such code within handles across every safepoint. parallel classloading The ability to have multiple classes/type be in the process of being loaded by the same classloader at the same time. parallel garbage collection A garbage collection algorithm that uses multiple threads of control to perform more efficiently on multi-processor boxes. permanent generation A region of the address space that holds object allocated by the virtual machine itself, but which is managed by the garbage collector. The permanent generation is mis-named, in that almost all of the objects in it canbe collected, though they tend to be referenced for a long time, so they rarely become garbage. remembered set A data structure that records pointers between generations. safepoint A point during program execution at which all GC roots are known and all heap object contents are consistent. From a global point of view, all threads must block at a safepoint before the GC can run. (As a special case, threads running JNI code can continue to run, because they use only handles. During a safepoint they must block instead of loading the contents of the handle.) From a local point of view, a safepoint is a distinguished point in a block of code where the executing thread may block for the GC. Most call sites qualify as safepoints. There are strong invariants which hold true at every safepoint, which may be disregarded at non-safepoints. Both compiled Java code and C/C++ code be optimized between safepoints, but less so across safepoints. The JIT compiler emits a GC map at each safepoint. C/C++ code in the VM uses stylized macro-based conventions (e.g., TRAPS) to mark potential safepoints. sea-of-nodes The high-level intermediate representation in C2. It is an SSA form where both data and control flow are represented with explicit edges between nodes. It differs from forms used in more traditional compilers in that nodes are not bound to a block in a control flow graph. The IR allows nodes to float within the sea (subject to edge constraints) until they are scheduled late in the compilation process. Serviceability Agent (SA) The Serviceablity Agent is collection of Sun internal code that aids in debugging HotSpot problems. It is also used by several JDK tools - jstack, jmap, jinfo, and jdb. See SA for more information. stackmap Refers to the StackMapTable attribut e or a particular StackMapFrame in the table. StackMapTable An attribute of the Code attribute in a classfile which contains type information used by the new verifier during verification. It consists of an array of StackMapFrames. It is generated automatically by javac as of JDK6. survivor space A region of the Java object heap used to hold objects. There are usually a pair of survivor spaces, and collection of one is achieved by copying the referenced objects in one survivor space to the other survivor space. synchronization In general terms this is the coordination of concurrent activities to ensure the safety and liveness properties of those activities. For example, protecting access to shared data by using a lock to guard all code paths to that data. TLAB Thread-local allocation buffer. Used to allocate heap space quickly without synchronization. Compiled code has a "fast path" of a few instructions which tries to bump a high-water mark in the current thread's TLAB, successfully allocating an object if the bumped mark falls before a TLAB-specific limit address. uncommon trap When code generated by C2 reverts back to the interpreter for further execution. C2 typically compiles for the common case, allowing it to focus on optimization of frequently executed paths. For example, C2 inserts an uncommon trap in generated code when a class that is uninitialized at compile time requires run time initialization. verifier The software code in the VM which performs bytecode verification. VM Operations Operations in the VM that can be requested by Java threads, but which must be executed, in serial fashion by a specific thread known as the VM thread. These operations are often synchronous, in that the requester will block until the VM thread has completed the operation. Many of these operations also require that the VM be brought to a safepoint before the operation can be performed - a garbage collection request is a simple example. write barrier Code that is executed on every oop store. For example, to maintain a remembered set. young generation A region of the Java object heap that holds recently-allocated objects.
看到一篇非常简洁的解释并发(Concurrent)与并行(Parallel)的区别的文章,纪录一下,以供参考。原文出自:http://joearms.github.io/2013/04/05/concurrent-and-parallel-programming.html What’s the difference between concurrency and parallelism? Explain it to a five year old. Concurrent = Two queues and one coffee machine. Parallel = Two queues and two coffee machines.
因为自己在做的项目需要在香港、伦敦、纽约、东京之间实现数据同步,需要在这几个区域之间传输数据,因而简单研究了一下这几个区域的网络延时,数据本身不是那么准确,只是作为一个数量级参考,所以有点标题党之嫌,但是又想不出更好的名字了。这些数据是使用ping在两个区域中传输1K数据得出的一个简单结果: (ms) SH HK TK LDN NY SH - 48 41 274 210 HK 48 0.1 57 224 224 TK 41 57 0.1 203 170 LDN 274 224 203 0.1 74 NY 210 224 170 74 0.1 从排序上: SH <=> LDN 274ms HK <=> NY 224ms HK <=> LDN 224ms SH <=> NY 210ms TK <=> LDN 203ms TK <=> NY 170ms NY <=> LDN 74ms HK <=> TK 57ms SH <=> HK 48ms SH <=> TK 41ms LAN 0.1ms LOCAL PC 0.01-0.03ms 在自己的Server上测试,大概同Blade的延迟在0.04-0.07ms之间,而同数据中心在0.10-0.15ms之间,而跨数据中心在0.9-1.0ms之间。
前言 在《ReferenceCountSet无锁实现》中,详细介绍了如何在一个进程中实现一个无锁版本的ReferenceCountSet(或者说是在自己的代码里没有锁),但是最近遇到一个问题,如果是在分布式的环境中呢?如何实现这个引用计数?这个问题如果从头开始写,会是一个比较复杂的问题,在实际中,我们可以使用ZooKeeper设置时的version机制来实现,即CAS(Compare-And-Set)。这是一个本人在实际项目中遇到的一个问题,但是会更简单一些,因为在我们的项目中,我们使用GemFire,即已经有一个现成的分布式Map了(在Gemfire中叫做Region),所以问题简化成如果如何使用一个分布式Map实现引用计数?实现 如果对ConcurrentMap接口比较熟悉的话,这个其实是一个比较简单的问题。在ConcurrentMap中最主要的就是引入几个CAS相关的操作: public interface ConcurrentMap<K, V> extends Map<K, V> { V putIfAbsent(K key, V value); boolean remove(Object key, Object value); boolean replace(K key, V oldValue, V newValue); V replace(K key, V value); } 在《ReferenceCountSet无锁实现》中我们只需要使用putIfAbsent就可以了,剩下的实现可以交给AtomicInteger提供的CAS来实现,因为它是在同一个进程中,但是如果在分布式的环境中就不能使用这个AtomicInteger,这个时候应该怎么办呢?其实这个时候我们就可以求助于replace方法了。replace方法的注释中这样描述: /** * Replaces the entry for a key only if currently mapped to a given value. * This is equivalent to * <pre> * if (map.containsKey(key) &amp;&amp; map.get(key).equals(oldValue)) { * map.put(key, newValue); * return true; * } else return false;</pre> * except that the action is performed atomically. * * @param key key with which the specified value is associated * @param oldValue value expected to be associated with the specified key * @param newValue value to be associated with the specified key * @return <tt>true</tt> if the value was replaced * @throws UnsupportedOperationException if the <tt>put</tt> operation * is not supported by this map * @throws ClassCastException if the class of a specified key or value * prevents it from being stored in this map * @throws NullPointerException if a specified key or value is null, * and this map does not permit null keys or values * @throws IllegalArgumentException if some property of a specified key * or value prevents it from being stored in this map */ boolean replace(K key, V oldValue, V newValue); 在ConcurrentMap的value中我们只需要给Integer,然后用replace去不断的尝试,即自己实现一个CAS: private int incrementRefCount(Object key) { do { Integer curCount = distributedMap.get(key); if (curCount == null) { curCount = distributedMap.putIfAbsent(key, new Integer(1)); if (curCount == null) { return 1; } } Integer newCount = new Integer(curCount.intValue() + 1); if (distributedMap.replace(key, curCount, newCount)) { return newCount; } } while (true); } 主要逻辑就是这样了,其实比较简单,只是之前没有遇到过这个问题,所以感觉可以记录下来。或许什么时候补充一下ZooKeeper版本的实现。
Descriptor框架 对非optimize_for为LITE_RUNTIME的proto文件,protobuf编译器会在编译出的Java代码文件末尾添加一个FileDescriptor静态字段以描述该proto文件定义时的所有元数据信息、为每个message对象定义一个Descriptor静态字段以描述该message定义时的元数据信息、为每个message对象定义一个FieldAccessorTable静态字段用于使用反射读取/设置某个字段的值等(以提供GeneratedMessage中方法的反射实现): private static Descriptor inter-nal_static_levin_protobuf_Result_descriptor;private static FieldAccessorTable inter-nal_static_levin_protobuf_Result_fieldAccessorTable;private static Descriptor inter-nal_static_levin_protobuf_SearchResponse_descriptor;private static FieldAccessorTable inter-nal_static_levin_protobuf_SearchResponse_fieldAccessorTable;private static FileDescriptor descriptor; 在protobuf中存在多种类型的元数据描述类: 1. FileDescriptor:对一个proto文件的描述,它包含文件名、包名、选项(如java_package、java_outer_classname等)、文件中定义的所有message、文件中定义的所有enum、文件中定义的所有service、文件中所有定义的extension、文件中定义的所有依赖文件(import)等。在FileDescriptor中还存在一个DescriptorPool实例,它保存了所有的dependencies(依赖文件的FileDescriptor)、name到GenericDescriptor的映射、字段到FieldDescriptor的映射、枚举项到EnumValueDescriptor的映射,从而可以从该DescriptorPool中查找相关的信息,因而可以通过名字从FileDescriptor中查找Message、Enum、Service、Extensions等。 2. Descriptor:对一个message定义的描述,它包含该message定义的名字、所有字段、内嵌message、内嵌enum、关联的FileDescriptor等。可以使用字段名或字段号查找FieldDescriptor。 3. FieldDescriptor:对一个字段或扩展字段定义的描述,它包含字段名、字段号、字段类型、字段定义(required/optional/repeated/packed)、默认值、是否是扩展字段以及和它关联的Descriptor/FileDescriptor等。 4. EnumDescriptor:对一个enum定义的描述,它包含enum名、全名、和它关联的FileDescriptor。可以使用枚举项或枚举值查找EnumValueDescriptor。 5. EnumValueDescriptor:对一个枚举项定义的描述,它包含枚举名、枚举值、关联的EnumDescriptor/FileDescriptor等。 6. ServiceDescriptor:对一个service定义的描述,它包含service名、全名、关联的FileDescriptor等。 7. MethodDescriptor:对一个在service中的method的描述,它包含method名、全名、参数类型、返回类型、关联的FileDescriptor/ServiceDescriptor等。 最后,protobuf编译生成的代码末尾还有一个descriptorData字符串数组,它是序列化后的FileDescriptorProto数据,在静态初始化块中可以调用FileDescriptor.internalBuildGeneratedFileFrom()方法构造整个FileDescriptor实例,在完成FileDescriptor的构造后,还会回调传入的InternalDescriptorAssigner实例以初始化其他的静态字段,如以上提到的所有的静态字段。 在protobuf中Descriptor的类图: Message、MessageLite框架 序列化和反序列化是protobuf最基础的框架,它使用MessageLite/Message接口来抽象一个可序列化的实例,并且使用Builder从字节数组或输入字节流中构建MessageLite/Message实例,MessageLite和Message内部都定义了自己的Builder类,他们个字继承自MessageLiteOrBuilder以及MessageOrBuiler,它们定义了MessageLite/Message和它们各自Builder类的共同接口。 MessageLiteOrBuilder接口只定义了MessageLite和MessageLite.Builder两个接口共有的两个方法:getDefaultInstanceForType()方法获取一个当前还未初始化的当前Message实例(没有字段被赋值,因而所有字段返回默认值,对repeat字段返回空,在当前protobuf 2.5.0的实现中,它返回的是一个单例,和每个生成的静态方法getDefaultInstance()返回相同的实例);isInitialized()方法用来判断是否所有required字段已经被赋值。MessageLite接口中定义了两个writeTo()方法分别将当前实例序列化并写入输出字节流中,而另一个writeDelimitedTo()方法则在写入之前将当前实例的总长度写入输出字节流中(以可变长32位Int编码方式),从而可以同时向一个输出字节流中写入多个Message实例;MessageLite中还定义了获取当前MessageLite在序列化成字节流后的总字节数的方法getSerializedSize(),两个直接返回字节数组的toByteArray()/toByteString()方法,以及获取它的Parser实例(getParserForType())和返回它的Builder实例(toBuilder()-创建一个新的Builder实例/newBuilderForType()-用当前MessageLite类初始化一个新的Builder实例并返回)方法。其中Builder接口用于从字节流或字节数组中解析并构造MessageLite对象(各种版本的mergeFrom()方法,如果发送端写入了MessageLite字节长度,则使用mergeDelimitedFrom()方法),最后Builder使用build()方法构造MessageLite对象,此时如果有required字段还未被设置,会抛出UninitializedMessageException,为了避免抛出异常,可以使用buildPartial()方法;另外Builder还定义了clone()和clear()方法;在生成的每个Message对象中都定义了一个newBuilder()静态方法,一般使用该静态方法初始化一个Builder实例。Parser接口也定义了各个版本的parseFrom()/parsePartialFrom()/parseDelimitedFrom()/parsePartialDelimitedFrom()方法用来从字节数组或字节流中解析出Message实例,在生成的代码中,Builder的实现直接调用Parser实现类中的方法。 在大部分情况下,MessageLite已经能完成所有的序列化和反序列化操作了,特别是一些资源有限额手持设备,它如果运行整个protobuf库会显得太耗资源;可以在.proto文件中加入一下指令来告诉protobuf编译器只需要生成实现MessageLite的类: option optimize_for = LITE_RUNTIME 然而对一般的Server程序来说,我们并不在乎这点资源的损耗,因而会选择实现Message接口,它相比MessageLite,添加了Descriptors相关的支持,即支持使用FieldDescriptor来构建Message.Builder实例并最终构建Message实例。 MessageOrBuilder接口继承自MessageLiteOrBuilder接口,它定义了Message和Message.Builder共有的接口,即添加了Descriptor、FieldDescriptor等相关的扩展。由于实现Message和Message.Builder接口的类保存了所有Message定义时具有的信息(文件名、包名、字段列表等,使用各种Descriptor类来抽象),因而我们可以使用Message/Message.Builder类获取到更多的信息,如一个Message/Message.Builder没有赋值所有required的字段,可以使用findInitializationErrors()方法来获取所有未赋值的字段列表(字段的全路径名,getInitializationErrorString()是这个列表的字符串形式表达,为了提升性能,建议使用isInitialized()方法先做初步判断,因为它更快);另外在MessageOrBuilder中还定义了当前Message对应的Descriptor实例:getDescriptorForType()方法,获取所有已经赋值的FieldDescriptor到其值的一个Map:getAllFields(),通过FieldDescriptor取得其值:getField(),判断一个字段是否已经被赋值:hasField(),获取repeated字段的count:getRepeatedFieldCount(),通过FieldDescriptor以及index获取repeated字段在index处的值:getRepeatedField(),获取未知的字段:getUnknownFields()。Message接口除了继承自MessageOrBuilder接口的方法,并没有定义多余的方法,只是添加了equals、hashCode、toString方法的定义。而Message.Builder接口除了继承自MessageOrBuilder接口以外,它还定义了基于FieldDescriptor的方法,如通过FieldDescriptor创建/获取Builder实例:newBuilderForFileld()/getFieldBuilder(),通过FieldDescriptor设置/清除字段的值:setField()/clearField()/setRepeatedField()/addRepeatedField(),以及设置UnknownFields:setUnknownFields()/mergeUnknownFields()。 MessageLite/Message类图如下: RPC框架 除了序列化框架,protobuf还定义了一套简单的RPC框架。之所以说简单是因为它定义的Service层接口的协议,而没有具体和传输相关的实现,而只是将传输相关的逻辑抽象成RpcChannel和BlockingRpcChannel分别用于表示同步和一步方式的Service方法调用,而至于底层用什么样的协议和框架,由用户自己决定并实现。 所谓RPC框架,从用户角度上最基本的就是定义客户端和服务器端的协议,即服务器端暴露出什么样的接口供客户端调用,这个接口定义了服务器在一个Host的某个(些)端口上接收某些请求数据,并期望能返回的响应。其中服务器和端口号属于传输实现的范畴,protobuf只是用RpcChannel/BlockingRpcChannel的概念做了抽象,而没有给出具体实现;而接收某个请求数据以及期待的响应数据,在protobuf使用Service/BlockingService抽象来定义,并且这也是protobuf中RPC框架的定义部分,其中Service和RpcChannel共同构成异步方式的RPC框架,而BlockingService和BlockingRpcChannel共同构成了同步(阻塞)方式的RPC框架。 从底层实现的角度,一个RPC调用就是客户端发送一些请求数据给服务器,服务器解析并处理这些请求数据,然后将响应数据返回给客户端。为了隐藏内部实现细节,提升写代码的效率,RPC将这一过程封装成方法调用,即不同的请求用不同的方法表达,这就是protobuf中RPC的定义。在protobuf中,定义一个PRC接口比较简单:首先开启RPC功能,然后用service关键字定义一个接口,在接口中使用rpc关键字定义一个方法,方法包含方法名、方法参数、返回值,其中方法参数和返回值都必须是一个message类型,并且只能有一个: option java_generic_services = true; service MyService { rpc request(SearchRequest) returns(SearchResponse); } 在protobuf编译生成的代码中,它会生成一个MyService抽象类实现了Service接口,一般它只是作为一个命名空间,它内部定义了两个接口:Interface和BlockingInterface本别继承自Service接口和BlockingService接口,用于抽象异步和同步方式的RPC方法调用;这两个接口有两个实现类:Stub和BlockingStub,他们分别接收RpcChannel和BlockingRpcChannel实例作为构造函数参数,可以使用MyService中的静态方法newStub()和newBlockingStub()方法获取他们各自实例,他们主要用于客户端的调用。在生成的request方法中,除了request本身的参数,还有一个RpcController参数,它用于处理在RpcChannel/BlockingRpcChannel调用中的状态处理,如错误处理等,使用它可以获知此次调用是否出错,错误信息是什么等。在MyService中还定义了两个静态方法newReflectiveService/newReflectiveBlockingService,他们接收Interface/BlockingInterface实例,并返回Service/BlockingService的实现实例(暂时还没有想到使用他们的场景)。 在MyService的RPC框架实现中,在服务器端,实现MyService.Interface/MyService.BlockingInterface接口,然后将它注册到对RpcChannel/BlockingRpcChannel框架的实现中;在客户端则创建一个RpcChannel/BlockingRpcChannel实例,传入MyService.newStub()/MyService.newBlockingStub()方法获取对应的实例,然后使用这个Stub/BlockingStub实例调用相应的方法即可。
基本类型编码 在前文有提到消息是一系列的基本类型以及其他消息类型的组合,因而基本类型是probobuf编码实现的基础,这些基本类型有: .proto Type Java Type C++ Type Wire Type double double double WIRETYPE_FIXED64(1) float float float WIRETYPE_FIXED32(5) int64 long int64 WIRETYPE_VARINT(0) int32 int int32 WIRETYPE_VARINT(0) uint64 long unit64 WIRETYPE_VARINT(0) uint32 int unit32 WIRETYPE_VARINT(0) sint64 long int64 WIRETYPE_VARINT(0) sint32 int int32 WIRETYPE_VARINT(0) fixed64 long unit64 WIRETYPE_FIXED64(1) fixed32 int unit32 WIRETYPE_FIXED32(5) sfixed64 long int64 WIRETYPE_FIXED64(1) sfixed32 int int32 WIRETYPE_FIXED32(5) bool boolean bool WIRETYPE_VARINT(0) string String string WIRETYPE_LENGTH_DELIMITED(2) bytes ByteString string WIRETYPE_LENGTH_DELIMITED(2) 在Java种对不同类型的选择,其他的类型区别很明显,主要在与int32、uint32、sint32、fixed32中以及对应的64位版本的选择,因为在Java中这些类型都用int(long)来表达,但是protobuf内部使用ZigZag编码方式来处理多余的符号问题,但是在编译生成的代码中并没有验证逻辑,比如uint的字段不能传入负数之类的。而从编码效率上,对fixed32类型,如果字段值大于2^28,它的编码效率比int32更加有效;而在负数编码上sint32的效率比int32要高;uint32则用于字段值永远是正整数的情况。 在实现上,protobuf使用CodedOutputStream实现序列化逻辑、CodedInputStream实现反序列化逻辑,他们都包含write/read基本类型和Message类型的方法,write方法中同时包含fieldNumber和value参数,在写入时先写入由fieldNumber和WireType组成的tag值(添加这个WireType类型信息是为了在对无法识别的字段编码时可以通过这个类型信息判断使用那种方式解析这个未知字段,所以这几种类型值即可),这个tag值是一个可变长int类型,所谓的可变长类型就是一个字节的最高位(msb,most significant bit)用1表示后一个字节属于当前字段,而最高位0表示当前字段编码结束。在写入tag值后,再写入字段值value,对不同的字段类型采用不同的编码方式: 1. 对int32/int64类型,如果值大于等于0,直接采用可变长编码,否则,采用64位的可变长编码,因而其编码结果永远是10个字节,所有说它int32/int64类型在编码负数效率很低(然而这里我一直木有想明白对int32类型为什么需要做64位的符号扩展,不扩展,5个字节就可以了啊,而且对64位的负数也不需要用符号扩展,或者无法符号扩展,google上也没有找到具体原因)。 2. 对uint32/uint64类型,也采用变长编码,不对负数做验证。 3. 对sint32/sint64类型,首先对该值做ZigZag编码,以保留,然后将编码后的值采用变长编码。所谓ZigZag编码即将负数转换成正数,而所有正数都乘2,如0编码成0,-1编码成1,1编码成2,-2编码成3,以此类推,因而它对负数的编码依然保持比较高的效率。 4. 对fixed32/sfixed32/fixed64/sfixed64类型,直接将该值以小端模式的固定长度编码。 5. 对double类型,先将double转换成long类型,然后以8个字节固定长度小端模式写入。 6. 对float类型,先将float类型转换成int类型,然后以4个字节固定长度小端模式写入。 7. 对bool类型,写0或1的一个字节。 8. 对string类型,使用UTF-8编码获取字节数组,然后先用变长编码写入字节数组长度,然后写入所有的字节数组。 Tag msgByteSize msgByte 9. 对bytes类型(ByteString),先用变长编码写入长度,然后写入整个字节数组。 Tag msgByteSize msgByte 10. 对枚举类型(类型值WIRETYPE_VARINT),用int32编码方式写入定义枚举项时给定的值(因而在给枚举类型项赋值时不推荐使用负数,因为int32编码方式对负数编码效率太低)。 11. 对内嵌Message类型(类型值WIRETYPE_LENGTH_DELIMITED),先写入整个Message序列化后字节长度,然后写入整个Message。 Tag msgByteSize msgByte 注:ZigZag编码实现:(n << 1) ^ (n >> 31) / (n << 1) ^ (n >> 63);在CodedOutputStream中还存在一些用于计算某个字段可能占用的字节数的compute静态方法,这里不再详述。 在protobuf的序列化中,所有的类型最终都会转换成一个可变长int/long类型、固定长度的int/long类型、byte类型以及byte数组。对byte类型的写只是简单的对内部buffer的赋值: public void writeRawByte(final byte value) throws IOException { if (position == limit) { refreshBuffer(); } buffer[position++] = value; } 对32位可变长整形实现为: public void writeRawVarint32(int value) throws IOException { while (true) { if ((value & ~0x7F) == 0) { writeRawByte(value); return; } else { writeRawByte((value & 0x7F) | 0x80); value >>>= 7; } } } 对于定长,protobuf采用小端模式,如对32位定长整形的实现: public void writeRawLittleEndian32(final int value) throws IOExcep-tion { writeRawByte((value ) & 0xFF); writeRawByte((value >> 8) & 0xFF); writeRawByte((value >> 16) & 0xFF); writeRawByte((value >> 24) & 0xFF); } 对byte数组,可以简单理解为依次调用writeRawByte()方法,只是CodedOutputStream在实现时做了部分性能优化。这里不详细介绍。对CodedInputStream则是根据CodedOutputStream的编码方式进行解码,因而也不详述,其中关于ZigZag的解码:(n >>> 1) ^ -(n & 1) repeated字段编码 对于repeated字段,一般有两种编码方式: 1. 每个项都先写入tag,然后写入具体数据。如对基本类型: Tag Data Tag Data … 而对message类型: Tag Length Data Tag Length Data … 2. 先写入tag,后count,再写入count个项,每个项包含length|data数据。即: Tag Count Length Data Length Data … 从编码效率的角度来看,个人感觉第二中情况更加有效,然而不知道处于什么原因考虑,protobuf采用了第一种方式来编码,个人能想到的一个理由是第一种情况下,每个消息项都是相对独立的,因而在传输过程中接收端每接收到一个消息项就可以进行解析,而不需要等待整个repeated字段的消息包。对于基本类型,protobuf也采用了第一种编码方式,后来发现这种编码方式效率太低,因而可以添加[packed = true]的描述将其转换成第三种编码方式(第二种方式的变种,对基本数据类型,比第二种方式更加有效): 3. 先写入tag,后写入字段的总字节数,再写入每个项数据。即: Tag dataByteSize Data Data … 目前protobuf只支持基本类型的packed修饰,因而如果将packed添加到非repeated字段或非基本类型的repeated字段,编译器在编译.proto文件时会报错。 未识别字段编码 在protobuf中,将所有未识别字段保存在UnknownFieldSet中,并且在每个由protobuf编译生成的Message类以及GeneratedMessage.Builder中保存了UnknownFieldSet字段unknownFields;该字段可以从CodedInputStream中初始化(调用UnknownFieldSet.Builder的mergeFieldFrom()方法)或从用户自己通过Builder设置;在序列化时,调用UnknownFieldSet的writeTo()方法将自身内容序列化到CodedOutputStream中。 UnknownFieldSet顾名思义是未知字段的集合,其内部数据结构是一个FieldNumber到Field的Map,而一个Field用于表达一个未知字段,它可以是任何值,因而它包含了所有5中类型的List字段,这里并没有对一个Field验证,因而允许多个相同FieldNumber的未知字段,并且他们可以是任意类型值。UnknownFieldSet采用MessageLite编程模式,因而它实现了MessageLite接口,并且定义了一个Builder类实现MessageLite.Builder接口用于手动或从CodedInputStream中构建UnknownFieldSet。虽然Field本身没有实现MessageLite接口,它依然实现了该接口的部分方法,如writeTo()、getSerializedSize()用于实现向CodedOutputStream中序列化自身,并且定义了Field.Builder类用于构建Field实例。 在一个Message序列化时(writeTo()方法实现),在写完所有可识别的字段以及扩展字段,这个定义在Message中的UnknownFieldSet也会被写入CodedOutputStream中;而在从CodedInputStream中解析时,对任何未知字段也都会被写入这个UnknownFieldSet中。 扩展字段编码 在写框架代码时,经常由扩展性的需求,在Java中,只需要简单的定义一个父类或接口即可解决,如果框架本身还负责构建实例本身,可以使用反射或暴露Factory类也可以顺利实现,然而对序列化来说,就很难提供这种动态plugin机制了。然而protobuf还是提出来一个相对可以接受的机制(语法有点怪异,但是至少可以用):在一个message中定义它支持的可扩展字段值的范围,然后用户可以使用extend关键字扩展该message定义(具体参考相关章节)。在实现中,所有这些支持字段扩展的message类型继承自ExtendableMessage类(它本身继承自GeneratedMessage类)并实现ExtendableMessageOrBuilder接口,而它们的Builder类则继承自ExtendableBuilder类并且同时也实现了ExtendableMessageOrBuilder接口。 ExtendableMessage和ExtendableBuilder类都包含FieldSet<FieldDescriptor>类型的字段用于保存该message所有的扩展字段值。FieldSet中保存了FieldDescriptor到其Object值的Map,然而在ExtendableMessage和ExtendableBuilder中则使用GeneratedExtension来表识一个扩展字段,这是因为GeneratedExtension除了包含对一个扩展字段的描述信息FieldDescriptor外,还存储了该扩展字段的类型、默认值等信息,在protobuf消息定义编译器中会为每个扩展字段生成相应的GeneratedExtension实例以供用户使用: public static final GeneratedExtension<Foo, Integer> bar = Generated-Message.newFileScopedGeneratedExtension( Integer.class, null ); bar.internalInit(descriptor.getExtensions().get(0)); Base base = Base.newBuilder().setExtension(SearchRequestProtos.bar, 11).build(); 用户使用该bar静态字段用于作为key与它对应的值关联,这种关联关系写入extensions字段中。从而在序列化时,对每个字段,按正常的值字段先写Tag在写实际值内容将它序列化到CodedOutputStream中(ExtensionWriter.writeUntil()方法);在反序列化中,我们需要告诉protobuf哪些字段是扩展字段,从而它在解析到无法识别的字段可以判断这个字段是否是扩展字段,因而protobuf提供了ExtensionRegistry类,它用于注册所有识别的扩展字段,并且在protobuf编译出来的代码中也存在一个静态方法将所有已定义的扩展字段注册到用户提供的ExtensionRegistry实例中: public static void registerAllExtensions(ExtensionRegistry registry) { registry.add(SearchRequestProtos.bar); }
概述 捣鼓hdfs、yarn、hbase、zookeeper的代码一年多了,是时候整理一下了。在hadoop (2.5.2)中protobuf是节点之间以及客户端和各个节点通信的基础序列化框架(协议),而基于avro和Writable的序列化框架则是这个协议里的payload,因而这一系列的文章打算从protobuf这个框架开始入手(版本2.5.0)。 从抽象的角度来说,protobuf框架是类实例序列化和远程调用的一种实现。所谓实例序列化简单来说就是将一个类实例转换成字节数组或字节流来表达,这个字节数组或字节流可以通过文件形式保存或者通过网络发送给一个接收方;而另一个程序可以读取文件中的字节或在接收到字节流后反序列化并构造出一个新的和原来相等的实例(这里的相等主要是值);除了protobuf,目前比较流行的序列化反序列化框架有Java自带序列化、反序列化框架(本文默认使用Java,因而忽略C++、Python等语言的相关内容)、JSON格式的序列化反序列化框架、XML格式的序列化反序列化框架以及上文提到的从Hadoop中分支出来的avro框架(关于这些框架的孰优孰略不是本文要讨论的范围,相信关于它们的比较网上也很容易找到)。在protobuf中使用Message/MessageLite来抽象一个可序列化和反序列化的实例,为了提升性能,用户一般需要定义一些.proto文件,并使用protobuf自带的代码生成器(编译器)来生成对具体类的序列化和反序列化代码;并且它使用将每个字段编号的方式来排列字段在序列化后的排列顺序以及处理不同版本的兼容性问题。对于远程调用来说,protobuf并没有多少实现,它用RpcChannel接口来抽象通信层的协议,而使用Service接口来抽象每个具体的可调用接口,RpcChannel需要我们自己实现,而具体的Service可以定义在.proto文件中,由protobuf自带的代码生成器生成。代码生成器同时支持BlockingService和Service(Async)的实现,其中异步方式通过注册RpcCallback来获取返回值。 语法和使用 抽象来说,消息其实是一段段具有特定含义的数据的集合。这些一段段具有特定含义的数据可以是基本类型,如int、string、double等,也可以是几个基本类型聚合而成的另一个消息,即消息中可以内嵌消息。对一个消息字段来说,我们需要定义它的类型、字段名,在protobuf中还需要定义字段规则(field rule)和字段的唯一标识号(tag)。其中字段规则有:required表示该字段必须存在,在调用Builder的build方法时,如果有required的字段还未被设置,会抛出UninitializedMessageException;optional表示该字段是可选的,因而在build()方法中不会对它做检查,在isInitialized()方法中也不会考虑它的设值情况;repeated表示该字段可能存在多个值,在Java版本中采用List来表达(protobuf在这里采用了比较简单的语法,因而我们无法选择集合类型、集合中元素个数的限制等)。字段唯一标识号用于定位一个字段,在XML和JSON中直接使用字段名来维护序列化后和类实例字段之间的映射,而且它们的字段类型(如果存储的话)一般也以字符串的形式保存在序列化后的字节流中,这样做的一个好处是序列化后的消息和消息的定义是相对独立的,并且消息可读性非常好,然而它的坏处也是比较明显的,序列化后的消息字节数会很大,序列化和反序列化的效率也会降低很多,为了解决这个问题,protobuf采用这个字段唯一标识号来定位一个字段,对每个字段在protobuf内部还定义了其类型值,从而在对无法识别的字段编码时可以通过这个类型信息判断使用那种方式解析这个未知字段,字段唯一标识号和类型值一同组成一个int类型的值tag,其中类型值占最低三个bit,字段唯一标识号占剩下29bit,即protobuf最大支持的字段数是2^29-1(536870911,然而19000到19999是系统保留的,因而不可以使用)。因而在每次写入一个字段时都需要先写入这个tag值,而每次读取一个字段时也先读取这个tag值以判断这个字段的标识号以及类型。protobuf使用可变长的整型编码这个tag值,即对1-15的标识号值只需要一个字节(字段类型占用3个bit),16-2047需要两个字节,因而推荐将1-15的标识号值赋值给使用最频繁的字段,并且推荐保留一些空间给将来添加新的使用比较频繁的字段。 关于required字段,如果将一个字段定义为required,在使用它时必须对它进行设值,并且出于兼容性的考虑,以后需要一直保持它的required属性。出于这个原因考虑,在google内部有人提出不要使用required字段,虽然这种提议并不是所有人都赞同。 最后,protobuf还支持optional字段的默认值设置,即在定义一个optional字段时在其后加入一个[…]的修饰,指定默认值(这个默认值应该只支持基本类型和枚举类型,对消息类型貌似没法定义),此时如果该字段没有被赋值,则它返回这个默认值,然而在序列化时这个默认值不会在序列化后的字节流中出现,因而如果.proto文件定义时的默认值发生改变,可能会出现序列化和反序列化出来某个字段值不一样的情况,需要特别注意。对于没有指定默认值的字段,protobuf采用预定义默认值,即string的默认值是空字符串、bool默认值是false、数值类型默认值0、枚举类型默认值是定义的第一个枚举项。 protobuf使用message来抽象序列化、反序列化对象,通常protobuf的用法是在.proto文件中定义需要的message对象,然后使用protobuf提供的protoc将定义的message对象编译成Java/C++/Python对象;在实际项目中引入这些生成的类,对这些message对象赋值,并使用框架提供的方法实现序列化、反序列化。如一个比较简单的SearchRequest例子,其定义如下: package levin.protobuf; option java_package = "org.levin.protobuf.generated.simple"; option java_outer_classname = "SearchRequestProtos"; message SearchRequest { required string query_string = 1; optional int32 page_number = 2; optional int32 result_per_page = 3 [ default = 50 ]; }使用一下命令编译并在test目录下生成org.levin.protobuf.generated.simple.SearchRequestProtos类: protoc --java_out test SearchRequest.proto protobuf编译器为每个message对象生成一个<Message>OrBuilder接口,该接口定义了message中所有字段的get方法和has<field>方法(用以判断是否某个字段已经设值);对string类型字段,它还包含了ByteString返回类型的get方法,ByteString是protobuf中对字节数组的一种抽象,它类似String,是一个不可变对象,它有不同的实现,如LiteralByteString只是对字节数组的封装,BoundedByteString则可以从一个字节数组中取处一段作为地层内容,而RopeByteString则采用树状结构来连接一系列的ByteString,以支持大容量并且无需拷贝的字符串。在ByteString中定义了多个方法来实现ByteString和字节数组/字符串互相转换:copyFrom()、copyTo()、toByteArray()、toString()、toStringUtf8()等。在以上的SearchRequst中的接口就是:SearchRequestOrBuilder,它继承自MessageOrBuilder接口,其定义如下: public interface SearchRequestOrBuilder extends MessageOrBuilder { boolean hasQueryString(); String getQueryString(); ByteString getQueryStringBytes(); boolean hasPageNumber(); int getPageNumber(); boolean hasResultPerPage(); int getResultPerPage(); } 在生成SearchRequestOrBuilder接口后,protobuf编译器会继续生成定义的Message对象:SearchRequest,这个Message对象也是一个不可变对象(Immutable),它包含bitField<x>_字段,该字段的每一个bit用于表示一个字段是否被已经被设值,因而其个数取决于字段的个数;而后每个Message字段都会有一个对应的字段(其中string类型的字段采用Object表达因为它有可能是String类型或者ByteString类型);之后是两个缓存字段:memoizedIsInitialized和memoizedSerializedSize,用于缓存是否已经初始化以及序列化后的字节数(Message对象是不可变的);最后每个Message对象都包含一个unknownFields字段用于保存无法识别的字段,以及一个静态的default实例,用以保存Message对象初始化状态下的对象实例。编译生成的SearchRequest继承自GeneratedMessage类并实现了SearchRequestOrBuilder接口中的所有方法,它定义了几个静态方法以获取SearchRequest在初始化状态时的实例,获取一个新的Builder实例用于构造SearchRequest实例,以及从字节数组、ByteString、InputStream中解析出SearchRequest实例等。 之后protobuf编译器还会在每个Message对象内部生成一个静态的Builder类,它继承自GeneratedMessage.Builder类,并实现了SearchRequestOrBuilder接口,该Builder类和SearchRequest消息类有类似的字段,并实现了各个字段的set方法以及build()方法用于build消息对象:SearchRequest。另外,它还提供了mergeFrom()方法,可以从字节数组、ByteString、InputStream等解析字节数据。 最后protobuf编译器还会为每一个Message对象生成用于描述该Message对象的字符串,用FileDescriptor.internalBuildGeneratedFileFrom()方法将其解析成一个Descriptor和FileAccessorTable实例。 在这个SearchRequest消息定义的例子中,protobuf编译器生成的类结构如下: 在使用protobuf编译器生成这些类以后,使用起来非常简单,构建SearchRequest只需要使用其静态方法新建Builder实例,设置各个字段的值,然后调用其build()方法即可。 SearchRequest.Builder builder = SearchRequest.newBuilder(); builder.setQueryString("param1=value1&param2=value2"); builder.setPageNumber(10); builder.setResultPerPage(100); SearchRequest request = builder.build(); System.out.println(request); 这段代码的输出结果为: query_string: "param1=value1&param2=value2" page_number: 10 result_per_page: 100 在序列化时,调用可调用MessageLite中的writeTo()方法,反序列化时可调用MessageLite.Builder中的mergeFrom()方法: ByteArrayOutputStream bytes = new ByteArrayOutputStream(); request.writeTo(bytes); System.out.println(Arrays.toString(bytes.toByteArray()));//output: [10, 27, 112, 97, 114, 97, 109, 49, 61, 118, 97, 108, 117, 101, 49, 38, 112, 97, 114, 97, 109, 50, 61, 118, 97, 108, 117, 101, 50, 16, 10, 24, 100] Sys-tem.out.println(SearchRequest.newBuilder().mergeFrom(request.toByteArray()).build());//output: // query_string: "param1=value1&param2=value2"// page_number: 10// result_per_page: 100 代码生成实现细节 在protobuf编译生成的代码中,作为序列化的核心实现比较简单,它只是将每个已经赋值的字段的fieldNumber和值一起写入到CodedOutputStream中: public void writeTo(CodedOutputStream output) throws ja-va.io.IOException { getSerializedSize(); if (((bitField0_ & 0x00000001) == 0x00000001)) { output.writeBytes(1, getQueryStringBytes()); } if (((bitField0_ & 0x00000002) == 0x00000002)) { output.writeInt32(2, pageNumber_); } if (((bitField0_ & 0x00000004) == 0x00000004)) { output.writeInt32(3, resultPerPage_); } getUnknownFields().writeTo(output); } 而反序列化的核心代码也基本上是从CodedInputStream读一个tag值,根据从tag值解析出来的fieldNumber值读取对应字段类型的数据并给字段赋值: private SearchRequest(CodedInputStream input, ExtensionRegistryLite extensionRegistry) throws InvalidProtocolBufferException { initFields(); UnknownFieldSet.Builder unknownFields = UnknownFieldSet.newBuilder(); boolean done = false; while (!done) { int tag = input.readTag(); switch (tag) { case 0: { done = true; break; } case 10: { bitField0_ |= 0x00000001; queryString_ = input.readBytes(); break; } case 16: { bitField0_ |= 0x00000002; pageNumber_ = input.readInt32(); break; } case 24: { bitField0_ |= 0x00000004; resultPerPage_ = input.readInt32(); break; } default: { if (!parseUnknownField(input, unknownFields, extensionRegistry, tag)) { done = true; } break; } } this.unknownFields = unknownFields.build(); makeExtensionsImmutable(); } 在protobuf消息定义中还支持枚举类型,使用enum作为关键字,并且需要给每个枚举项定义一个唯一的值,从而在序列化时protobuf实际上是将这个唯一的int值以int32可变长编码写入字节流中,从而节省空间,也正是因为这个值采用int32的编码方式,因而不推荐给枚举项赋负数值,因为int32的负数编码要占用10个字节空间。 enum Corpus { UNIVERSAL = 10; WEB = 11; IMAGES = 12; } protobuf编译生成的代码中也会生成一个Corpus的枚举类型,它实现ProtobufMessageEnum接口,并包含index和value字段: public enum Corpus implements ProtocolMessageEnum { UNIVERSAL(0, 10), WEB(1, 11), IMAGES(2, 12); public final int getNumber() { return value; } private final int index; private final int value; private Corpus(int index, int value) { this.index = index; this.value = value; } } 而ProtocolMessageEnum的接口定义了获取一个枚举项的值以及其相关的EnumValueDescriptor和EnumDescriptor(Descriptor将在后面小结中详细讲解): public interface ProtocolMessageEnum extends Internal.EnumLite { int getNumber(); EnumValueDescriptor getValueDescriptor(); EnumDescriptor getDescriptorForType(); } 在protobuf中还可以定义一个字段为repeated字段,表示该字段时一个集合,在Java版本中使用List表达。对repeated字段,在序列化时对每个值,同时写入tag值和字段值本身: message SearchResponse { repeated Result result = 1; repeated int32 stats = 2; }for (int i = 0; i < result_.size(); i++) { output.writeMessage(1, result_.get(i)); }for (int i = 0; i < stats_.size(); i++) { output.writeInt32(2, stats_.get(i)); } 然而这种编码方式对基本类型来说效率太低,因为每项都要同时包含tag值,所以对基本类型,protobuf还只是使用packed来提升编码效率(在反序列化时不管有没有加packed关键字,它都同时支持两种编码方式的读取): message SearchResponse { repeated Result result = 1; repeated int32 stats = 2 [ packed = true ]; }if (getStatsList().size() > 0) { output.writeRawVarint32(18); output.writeRawVarint32(statsMemoizedSerializedSize); }for (int i = 0; i < stats_.size(); i++) { output.writeInt32NoTag(stats_.get(i)); } 在写框架代码时,经常由扩展性的需求,在Java中,只需要简单的定义一个父类或接口即可解决,如果框架本身还负责构建实例本身,可以使用反射或暴露Factory类也可以顺利实现,然而对序列化来说,就很难提供这种动态plugin机制了。然而protobuf还是提出来一个相对可以接受的机制(语法有点怪异,但是至少可以用):在一个message中定义它支持的可扩展字段值的范围,然后用户可以使用extend关键字扩展该message定义: message Foo { optional int32 field1 = 1; extensions 100 to 199; } extend Foo { optional int32 bar = 126; } 在protobuf编译器生成的代码中,在序列化时,在序列化未知字段之前需要先序列化已经写入的可扩展字段: public void writeTo(CodedOutputStream output) throws ja-va.io.IOException { getSerializedSize(); GeneratedMessage.ExtendableMessage<Foo>.ExtensionWriter extensionWriter = newExtensionWriter(); if (((bitField0_ & 0x00000001) == 0x00000001)) { output.writeInt32(1, field1_); } extensionWriter.writeUntil(200, output); getUnknownFields().writeTo(output); } 在反序列化时由于可扩展字段在parseUnknownField()方法中解析,因而没有多少区别,然而在该方法中会使用到ExtensionRegistry实例。另外生成的消息类Foo继承自ExtendableMessage而不是GeneratedMessage,Foo.Builder继承自ExtendableBuilder而不是GeneratedMessage.Builder。最后还会生成GeneratedExtension<Foo, Integer>类型的静态字段bar以及在静态方法registerAllExtensions()将该bar字段注册到ExtensionRegistry实例中以供反序列化时使用: Foo foo = Foo.newBuilder().setField1(10) .setExtension(ExtensionsProtos.bar, 20) .build(); System.out.println(foo);// field1: 10// [levin.protobuf.bar]: 20 ExtensionRegistry registry = ExtensionRegistry.newInstance(); ExtensionsProtos.registerAllExtensions(registry); Foo foo2 = Foo.newBuilder() .mergeFrom(foo.toByteArray(), registry) .build(); System.out.println(foo2);// field1: 10// [levin.protobuf.bar]: 20 在protobuf中默认的optimize_for选项的值是SPEED,然而有些时候我们只需要使用MessageLite的功能即可,不需要Descriptor和反射,这个时候可以指定该值为LITE_RUNTIME。使用该选项,生成的Message类直接继承自GeneratedMessageLite,并且不会生成那些Descriptor信息,此时生成Message类的toString()方法只能打印类实例因为无法通过反射获知类的元数据信息。optimize_for选项的另一个可选值是CODE_SIZE,该选项生成的Message类中不会实现writeTo()方法,它使用AbstractMessage类中实现的writeTo()方法,该方法遍历所有有赋值的字段,使用反射获取字段的值,并写入CodedOutputStream中。
URI貌似属于比较基本的一种协议,虽然我经常记不住它的具体格式。今天在WIKI上看到一个比较详细的格式描述,所以copy下来保留,仅此而已。出自:http://en.wikipedia.org/wiki/URI_scheme <scheme name> : <hierarchical part> [ ? <query> ] [ # <fragment> ] The following figure displays two example URIs (foo://username:password@example.com:8042/over/there/index.dtb?type=animal&name=narwhal#nose andurn:example:animal:ferret:nose) and their component parts. (The examples are derived from RFC 3986 — STD 66, chapter 3). foo://username:password@example.com:8042/over/there/index.dtb?type=animal&name=narwhal#nose \_/ \_______________/ \_________/ \__/ \___/ \_/ \______________________/ \__/ | | | | | | | | | userinfo host port | | query fragment | \________________________________/\_____________|____|/ \__/ \__/ scheme | | | | | | name authority | | | | | | path | | interpretable as keys | | | | \_______________________________________________|____|/ \____/ \_____/ | | | | | | scheme hierarchical part | | interpretable as values name | | | path interpretable as filename | | ___________|____________ | / \ / \ | urn:example:animal:ferret:nose interpretable as extension path _________|________ scheme / \ name userinfo hostname query _|__ ___|__ ____|____ _____|_____ / \ / \ / \ / \ mailto:username@example.com?subject=Topic
转自:http://zh.hortonworks.com/blog/hdfs-metadata-directories-explained/HDFS metadata represents the structure of HDFS directories and files in a tree. It also includes the various attributes of directories and files, such as ownership, permissions, quotas, and replication factor. In this blog post, I’ll describe how HDFS persists its metadata in Hadoop 2 by exploring the underlying local storage directories and files. All examples shown are from testing a build of the soon-to-be-released Apache Hadoop 2.6.0. WARNING: Do not attempt to modify metadata directories or files. Unexpected modifications can cause HDFS downtime, or even permanent data loss. This information is provided for educational purposes only. Persistence of HDFS metadata broadly breaks down into 2 categories of files: fsimage – An fsimage file contains the complete state of the file system at a point in time. Every file system modification is assigned a unique, monotonically increasing transaction ID. An fsimage file represents the file system state after all modifications up to a specific transaction ID. edits – An edits file is a log that lists each file system change (file creation, deletion or modification) that was made after the most recent fsimage. Checkpointing is the process of merging the content of the most recent fsimage with all edits applied after that fsimage is merged in order to create a new fsimage. Checkpointing is triggered automatically by configuration policies or manually by HDFS administration commands. NameNode Here is an example of an HDFS metadata directory taken from a NameNode. This shows the output of running the tree command on the metadata directory, which is configured by setting dfs.namenode.name.dir in hdfs-site.xml. data/dfs/name ├── current │ ├── VERSION │ ├── edits_0000000000000000001-0000000000000000007 │ ├── edits_0000000000000000008-0000000000000000015 │ ├── edits_0000000000000000016-0000000000000000022 │ ├── edits_0000000000000000023-0000000000000000029 │ ├── edits_0000000000000000030-0000000000000000030 │ ├── edits_0000000000000000031-0000000000000000031 │ ├── edits_inprogress_0000000000000000032 │ ├── fsimage_0000000000000000030 │ ├── fsimage_0000000000000000030.md5 │ ├── fsimage_0000000000000000031 │ ├── fsimage_0000000000000000031.md5 │ └── seen_txid └── in_use.lock In this example, the same directory has been used for both fsimage and edits. Alternatively, configuration options are available that allow separating fsimage and edits into different directories. Each file within this directory serves a specific purpose in the overall scheme of metadata persistence: VERSION – Text file that contains: layoutVersion – The version of the HDFS metadata format. When we add new features that require changing the metadata format, we change this number. An HDFS upgrade is required when the current HDFS software uses a layout version newer than what is currently tracked here. namespaceID/clusterID/blockpoolID – These are unique identifiers of an HDFS cluster. The identifiers are used to prevent DataNodes from registering accidentally with an incorrect NameNode that is part of a different cluster. These identifiers also are particularly important in a federated deployment. Within a federated deployment, there are multiple NameNodes working independently. Each NameNode serves a unique portion of the namespace (namespaceID) and manages a unique set of blocks (blockpoolID). The clusterID ties the whole cluster together as a single logical unit. It’s the same across all nodes in the cluster. storageType – This is either NAME_NODE or JOURNAL_NODE. Metadata on a JournalNode in an HA deployment is discussed later. cTime – Creation time of file system state. This field is updated during HDFS upgrades. edits_start transaction ID-end transaction ID – These are finalized (unmodifiable) edit log segments. Each of these files contains all of the edit log transactions in the range defined by the file name’s through . In an HA deployment, the standby can only read up through the finalized log segments. It will not be up-to-date with the current edit log in progress (described next). However, when an HA failover happens, the failover finalizes the current log segment so that it’s completely caught up before switching to active. edits_inprogress__start transaction ID – This is the current edit log in progress. All transactions starting from are in this file, and all new incoming transactions will get appended to this file. HDFS pre-allocates space in this file in 1 MB chunks for efficiency, and then fills it with incoming transactions. You’ll probably see this file’s size as a multiple of 1 MB. When HDFS finalizes the log segment, it truncates the unused portion of the space that doesn’t contain any transactions, so the finalized file’s space will shrink down. fsimage_end transaction ID – This contains the complete metadata image up through . Each fsimage file also has a corresponding .md5 file containing a MD5 checksum, which HDFS uses to guard against disk corruption. seen_txid - This contains the last transaction ID of the last checkpoint (merge of edits into a fsimage) or edit log roll (finalization of current edits_inprogress and creation of a new one). Note that this is not the last transaction ID accepted by the NameNode. The file is not updated on every transaction, only on a checkpoint or an edit log roll. The purpose of this file is to try to identify if edits are missing during startup. It’s possible to configure the NameNode to use separate directories for fsimage and edits files. If the edits directory accidentally gets deleted, then all transactions since the last checkpoint would go away, and the NameNode would start up using just fsimage at an old state. To guard against this, NameNode startup also checks seen_txid to verify that it can load transactions at least up through that number. It aborts startup if it can’t. in_use.lock – This is a lock file held by the NameNode process, used to prevent multiple NameNode processes from starting up and concurrently modifying the directory. JournalNode In an HA deployment, edits are logged to a separate set of daemons called JournalNodes. A JournalNode’s metadata directory is configured by setting dfs.journalnode.edits.dir. The JournalNode will contain a VERSION file, multiple edits__ files and an edits_inprogress_, just like the NameNode. The JournalNode will not have fsimage files or seen_txid. In addition, it contains several other files relevant to the HA implementation. These files help prevent a split-brain scenario, in which multiple NameNodes could think they are active and all try to write edits. committed-txid – Tracks last transaction ID committed by a NameNode. last-promised-epoch – This file contains the “epoch,” which is a monotonically increasing number. When a new writer (a new NameNode) starts as active, it increments the epoch and presents it in calls to the JournalNode. This scheme is the NameNode’s way of claiming that it is active and requests from another NameNode, presenting a lower epoch, must be ignored. last-writer-epoch – Similar to the above, but this contains the epoch number associated with the writer who last actually wrote a transaction. (This was a bug fix for an edge case not handled by last-promised-epoch alone.) paxos – Directory containing temporary files used in implementation of the Paxos distributed consensus protocol. This directory often will appear as empty. DataNode Although DataNodes do not contain metadata about the directories and files stored in an HDFS cluster, they do contain a small amount of metadata about the DataNode itself and its relationship to a cluster. This shows the output of running the tree command on the DataNode’s directory, configured by setting dfs.datanode.data.dir in hdfs-site.xml. data/dfs/data/ ├── current │ ├── BP-1079595417-192.168.2.45-1412613236271 │ │ ├── current │ │ │ ├── VERSION │ │ │ ├── finalized │ │ │ │ └── subdir0 │ │ │ │ └── subdir1 │ │ │ │ ├── blk_1073741825 │ │ │ │ └── blk_1073741825_1001.meta │ │ │ │── lazyPersist │ │ │ └── rbw │ │ ├── dncp_block_verification.log.curr │ │ ├── dncp_block_verification.log.prev │ │ └── tmp │ └── VERSION └── in_use.lock The purpose of these files is: BP-random integer-NameNode-IP address-creation time – The naming convention on this directory is significant and constitutes a form of cluster metadata. The name is a block pool ID. “BP” stands for “block pool,” the abstraction that collects a set of blocks belonging to a single namespace. In the case of a federated deployment, there will be multiple “BP” sub-directories, one for each block pool. The remaining components form a unique ID: a random integer, followed by the IP address of the NameNode that created the block pool, followed by creation time. VERSION – Much like the NameNode and JournalNode, this is a text file containing multiple properties, such as layoutVersion, clusterId and cTime, all discussed earlier. There is a VERSION file tracked for the entire DataNode as well as a separate VERSION file in each block pool sub-directory. In addition to the properties already discussed earlier, the DataNode’s VERSION files also contain: storageType – In this case, the storageType field is set to DATA_NODE. blockpoolID – This repeats the block pool ID information encoded into the sub-directory name. finalized/rbw - Both finalized and rbw contain a directory structure for block storage. This holds numerous block files, which contain HDFS file data and the corresponding .meta files, which contain checksum information. “Rbw” stands for “replica being written”. This area contains blocks that are still being written to by an HDFS client. The finalized sub-directory contains blocks that are not being written to by a client and have been completed. lazyPersist – HDFS is incorporating a new feature to support writing transient data to memory, followed by lazy persistence to disk in the background. If this feature is in use, then a lazyPersist sub-directory is present and used for lazy persistence of in-memory blocks to disk. We’ll cover this exciting new feature in greater detail in a future blog post. dncp_block_verification.log – This file tracks the last time each block was verified by checking its contents against its checksum. The last verification time is significant for deciding how to prioritize subsequent verification work. The DataNode orders its background block verification work in ascending order of last verification time. This file is rolled periodically, so it’s typical to see a .curr file (current) and a .prev file (previous). in_use.lock – This is a lock file held by the DataNode process, used to prevent multiple DataNode processes from starting up and concurrently modifying the directory. Commands Various HDFS commands impact the metadata directories Commands Description hdfs namenode NameNode startup automatically saves a new checkpoint. As stated earlier, checkpointing is the process of merging any outstanding edit logs with the latest fsimage, saving the full state to a new fsimage file, and rolling edits. Rolling edits means finalizing the current edits_inprogress and starting a new one. hdfs dfsadmin -safemode enterhdfs dfsadmin -saveNamespace This saves a new checkpoint (much like restarting NameNode) while the NameNode process remains running. Note that the NameNode must be in safe mode, so all attempted write activity would fail while this is running. hdfs dfsadmin -rollEdits This manually rolls edits. Safe mode is not required. This can be useful if a standby NameNode is lagging behind the active and you want it to get caught up more quickly. (The standby NameNode can only read finalized edit log segments, not the current in progress edits file.) hdfs dfsadmin -fetchImage Downloads the latest fsimage from the NameNode. This can be helpful for a remote backup type of scenario. Configuration Properties Several configuration properties in hdfs-site.xml control the behavior of HDFS metadata directories. dfs.namenode.name.dir – Determines where on the local filesystem the DFS name node should store the name table (fsimage). If this is a comma-delimited list of directories then the name table is replicated in all of the directories, for redundancy. dfs.namenode.edits.dir - Determines where on the local filesystem the DFS name node should store the transaction (edits) file. If this is a comma-delimited list of directories then the transaction file is replicated in all of the directories, for redundancy. Default value is same as dfs.namenode.name.dir. dfs.namenode.checkpoint.period - The number of seconds between two periodic checkpoints. dfs.namenode.checkpoint.txns – The standby will create a checkpoint of the namespace every ‘dfs.namenode.checkpoint.txns’ transactions, regardless of whether ‘dfs.namenode.checkpoint.period’ has expired. dfs.namenode.checkpoint.check.period – How frequently to query for the number of uncheckpointed transactions. dfs.namenode.num.checkpoints.retained - The number of image checkpoint files that will be retained in storage directories. All edit logs necessary to recover an up-to-date namespace from the oldest retained checkpoint will also be retained. dfs.namenode.num.extra.edits.retained – The number of extra transactions which should be retained beyond what is minimally necessary for a NN restart. This can be useful for audit purposes or for an HA setup where a remote Standby Node may have been offline for some time and need to have a longer backlog of retained edits in order to start again. dfs.namenode.edit.log.autoroll.multiplier.threshold – Determines when an active namenode will roll its own edit log. The actual threshold (in number of edits) is determined by multiplying this value by dfs.namenode.checkpoint.txns. This prevents extremely large edit files from accumulating on the active namenode, which can cause timeouts during namenode startup and pose an administrative hassle. This behavior is intended as a failsafe for when the standby fails to roll the edit log by the normal checkpoint threshold. dfs.namenode.edit.log.autoroll.check.interval.ms – How often an active namenode will check if it needs to roll its edit log, in milliseconds. dfs.datanode.data.dir – Determines where on the local filesystem an DFS data node should store its blocks. If this is a comma-delimited list of directories, then data will be stored in all named directories, typically on different devices. Directories that do not exist are ignored. Heterogeneous storage allows specifying that each directory resides on a different type of storage: DISK, SSD, ARCHIVE or RAM_DISK. Conclusion We briefly discussed how HDFS persists its metadata in Hadoop 2 by exploring the underlying local storage directories and files, the relevant configurations that drive specific behaviors, and appropriate HDFS metadata directory commands that print out the directory tree, initiate checkpoint, and create a fsimage. In a future blog, we’ll explore lazy persistence, a scheme to persist in-memory data to disk, in more in details.
转自:http://www.linuxidc.com/Linux/2011-03/33582.htm 1:在命令行提示符执行top命令 2:输入大写P,则结果按CPU占用降序排序。输入大写M,结果按内存占用降序排序。(注:大写P可以在capslock状态输入p,或者按Shift+p) 另外: 认识top的显示结果 top命令的显示结果如下所示: top - 01:06:48 up 1:22, 1 user, load average: 0.06, 0.60, 0.48 Tasks: 29 total, 1 running, 28 sleeping, 0 stopped, 0 zombie Cpu(s): 0.3% us, 1.0% sy, 0.0% ni, 98.7% id, 0.0% wa, 0.0% hi, 0.0% si Mem: 191272k total, 173656k used, 17616k free, 22052k buffers Swap: 192772k total, 0k used, 192772k free, 123988k cached PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 1379 root 16 0 7976 2456 1980 S 0.7 1.3 0:11.03 sshd 14704 root 16 0 2128 980 796 R 0.7 0.5 0:02.72 top 1 root 16 0 1992 632 544 S 0.0 0.3 0:00.90 init 2 root 34 19 0 0 0 S 0.0 0.0 0:00.00 ksoftirqd/0 3 root RT 0 0 0 0 S 0.0 0.0 0:00.00 watchdog/0统计信息区 前五行是系统整体的统计信息。第一行是任务队列信息,同 uptime 命令的执行结果。其内容如下: 01:06:48 当前时间 up 1:22 系统运行时间,格式为时:分 1 user 当前登录用户数 load average: 0.06, 0.60, 0.48 系统负载,即任务队列的平均长度。 三个数值分别为 1分钟、5分钟、15分钟前到现在的平均值。 第二、三行为进程和CPU的信息。当有多个CPU时,这些内容可能会超过两行。内容如下: Tasks: 29 total 进程总数 1 running 正在运行的进程数 28 sleeping 睡眠的进程数 0 stopped 停止的进程数 0 zombie 僵尸进程数 Cpu(s): 0.3% us 用户空间占用CPU百分比 1.0% sy 内核空间占用CPU百分比 0.0% ni 用户进程空间内改变过优先级的进程占用CPU百分比 98.7% id 空闲CPU百分比 0.0% wa 等待输入输出的CPU时间百分比 0.0% hi 0.0% si 最后两行为内存信息。内容如下: Mem: 191272k total 物理内存总量 173656k used 使用的物理内存总量 17616k free 空闲内存总量 22052k buffers 用作内核缓存的内存量 Swap: 192772k total 交换区总量 0k used 使用的交换区总量 192772k free 空闲交换区总量 123988k cached 缓冲的交换区总量。 内存中的内容被换出到交换区,而后又被换入到内存,但使用过的交换区尚未被覆盖, 该数值即为这些内容已存在于内存中的交换区的大小。 相应的内存再次被换出时可不必再对交换区写入。 进程信息区 统计信息区域的下方显示了各个进程的详细信息。首先来认识一下各列的含义。 序号 列名 含义 a PID 进程id b PPID 父进程id c RUSER Real user name d UID 进程所有者的用户id e USER 进程所有者的用户名 f GROUP 进程所有者的组名 g TTY 启动进程的终端名。不是从终端启动的进程则显示为 ? h PR 优先级 i NI nice值。负值表示高优先级,正值表示低优先级 j P 最后使用的CPU,仅在多CPU环境下有意义 k %CPU 上次更新到现在的CPU时间占用百分比 l TIME 进程使用的CPU时间总计,单位秒 m TIME+ 进程使用的CPU时间总计,单位1/100秒 n %MEM 进程使用的物理内存百分比 o VIRT 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES p SWAP 进程使用的虚拟内存中,被换出的大小,单位kb。 q RES 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA r CODE 可执行代码占用的物理内存大小,单位kb s DATA 可执行代码以外的部分(数据段+栈)占用的物理内存大小,单位kb t SHR 共享内存大小,单位kb u nFLT 页面错误次数 v nDRT 最后一次写入到现在,被修改过的页面数。 w S 进程状态。D=不可中断的睡眠状态R=运行S=睡眠T=跟踪/停止Z=僵尸进程 x COMMAND 命令名/命令行 y WCHAN 若该进程在睡眠,则显示睡眠中的系统函数名 z Flags 任务标志,参考 sched.h 默认情况下仅显示比较重要的 PID、USER、PR、NI、VIRT、RES、SHR、S、%CPU、%MEM、TIME+、COMMAND 列。可以通过下面的快捷键来更改显示内容。 更改显示内容 通过 f 键可以选择显示的内容。按 f 键之后会显示列的列表,按 a-z 即可显示或隐藏对应的列,最后按回车键确定。 按 o 键可以改变列的显示顺序。按小写的 a-z 可以将相应的列向右移动,而大写的 A-Z 可以将相应的列向左移动。最后按回车键确定。 按大写的 F 或 O 键,然后按 a-z 可以将进程按照相应的列进行排序。而大写的 R 键可以将当前的排序倒转。添加: top还可以用来显示一个进程中各个线程CPU的占用率: top -p <pid> -H 按CPU排序,找到对应的PID即是CPU占用最多的线程,在Java中可以使用jstack将该线程的堆栈打印出来,使用这个线程ID查找对应的线程堆栈(要先将线程ID转换成16进制)。
记得很久以前有一次面试被问到如何编写无锁程序,我当时觉得那个面试官脑子进水了,我们确实可以在某些情况下减少锁的使用,但是怎么可能避免呢?当然我现在还是持这种观点,在Java中,你可以有很多方法减少锁的使用(至少在你自己的代码中看起来): 1. 比如常见的可以使用volatile关键字来保证某个字段在一个线程中的更新对其他线程的可见性; 2. 可以使用concurrent.atomic包中的各种Atomic类来实现某些基本类型操作的,它主要采用忙等机制(CAS,compare and swap方法)以及内部的volatile变量来实现不加锁情况下对某个字段特定更新的线程安全; 3. 使用ThreadLocal,为每个线程保存保存自己的对象,保证对它的操作永远只有一个线程; 4. 使用concurrent包下已经实现的线程安全集合,如ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList、CopyOnWriteArraySet等,在这些集合的实现中也会看到volatile和CAS的身影,但是有些时候它们自身也不得不使用锁; 5. 使用BlockingQueue实现生产者消费者模式,这个BlockingQueue有点类似EventBus,很多时候可以简化多线程编程环境下的复杂性,在特定情况下还可以采用单个生产者或单个消费者的方式来避免多线程环境,但是在对一些临界资源和类操作时,还是会需要使用锁,而且在很多BlockingQueue的内部实现中也使用了锁; 6. 使用Guava提供的StripLock方式来将一些特定的操作、数据映射到固定的线程中,从而保证对它们的操作的线程安全。 然而很多时候Java提供的那些所谓的线程安全类只是一些小范围的操作,比如AtomicInteger中的incrementAndGet()/decrementAndGet(),ConcurrentHashMap中的get和put,当需要将一些操作组合在一起的时候,我们还是不得不使用锁,比如更具一个key取出Map中的值,对值进行一定操作,然后将该值写入Map中。以前我一直觉得这种操作不用锁是不太可能解决的,但是最近做的一个项目需要从REUTERS中source数据,REUTERS中的很多数据tick都非常快,为了提高项目的处理能力,我们必须要减少锁的使用,因而我开始尝试着在不用锁的情况下实现某些操作的组合而依然能保持线程安全,然后发现在组合使用CAS和ConcurrentMap接口中提供的putIfAbsent、replace、remove等根据传入的值再来更新状态的方式还真的能实现不用锁组合某些操作而依然保持线程安全(至少在自己的代码中无锁)。对于这种尝试出厂了一个ReferenceCountSet。 ReferenceCountSet实现 从这个Set的名字中应该已经能够知道它的用途了,在我的这个项目中,我们需要自己维护对某些实例的引用计数,所以最基本的,必须有一个集合存储一个实例到它的引用计数的映射关系,而一个实例在这个集合中必须是唯一存在,因而我自然想到了使用Map来保存;另外在对引用计数增加时,需要分以下几个步骤实现: 1. 判断一个新添加的实例是否已经在这个Map中存在 2. 如果不存在,则将该实例添加到这个Map中,并设置其引用计数为1 3. 如果已经存在,则需要取出已经存在的引用计数,对其加1,然后将新值写入这个Map中。 对remove方法来说也是类似的,即需要取出Map中已经存在的计数值以后,对其引用减1,然后判断是否为0来决定是否需要将当前实例真正从这个Map中移除。 既然需要那么多的组合操作,显然没有一个已经存在的Map可以实现不加锁情况下的线程安全。因而我起初的选择是使用HashMap<E, Integer>加锁实现(以add为例): synchronized(map) { Integer refCount = map.get(element); if (refCount == null) { refCount = 1; } else { refCount += 1; } map.put(element, refCount); } 但是如何不用锁实现这些组合操作呢?秘诀就在于灵活的使用AtomicInteger和ConcurrentMap接口。首先需要将这个Map改成ConcurrentMap,如ConcurrentHashMap,然后将这个Map的值改成AtomicInteger,然后采用如下实现即可: public boolean add(E element) { AtomicInteger refCount = data.get(element); if (refCount == null) { // Avoid to put AtomicInteger(0) as during remove we need this value to compare AtomicInteger newRefCount = new AtomicInteger(1); refCount = data.putIfAbsent(element, newRefCount); if (refCount == null) { return true; } } refCount.incrementAndGet(); return true; } 在这个add方法实现中,我们首先直接使用传入的element获取内部存在AtomicInteger值,如果该值为null,表示当前还没有对它有引用计数,因而我们初始化一个AtomicInteger(1)对象,但是这时我们不能直接将这个1作为该对象的引用计数,因为另一个线程可能在这中间已经添加相同对象的引用计数了,这个时候如果我们还直接写入会覆盖在这个中间添加的引用计数。所以我们需要使用ConcurrentMap中的putIfAbsent方法,即如果其他线程在这个还是没有对这个对象有引用计数更新的情况下,我们才会真正写入现在的引用计数值,从而不会覆盖在这中间写入的值,该方法返回原来已经在这个Map中的值,因而如果返回null,表示在这个过程中没有其他线程对这个对象的计数值有改变,所以直接返回;如果返回不为null,表示在这个中间其他线程有对这个对象有做引用计数的改变,并且返回更新的AtomicInteger值,此时只需要像已经存在引用计数实例的情况下对这个返回的AtomicInteger只自增即可,由于AtomicInteger是线程安全的,因而这个操作也是安全的。并且由于对每个线程都使用同一个AtomicInteger引用实例,因而每个线程的自增都会正确的反映在Map的值中,因而这个操作也是正确的。 这里,我其实是将这个ConcurrentMap封装在一个Set中,因而我们还需要实现一些其他方法,如size、contains、iterator、remove等,由于其他方法的在ConcurrentHashMap中已经是线程安全的了,因而我们只需要实现remove方法即可。这里的remove方法也包含了多个操作的组合:先取出以存在的计数,对其减1,如果发现它的计数已经减到0,将它从这个Map中移除。这里需要使用ConcurrentMap中提供的条件remove方法来实现: public boolean remove(Object obj) { AtomicInteger refCount = data.get(obj); if (refCount == null) { return false; } if (refCount.decrementAndGet() <= 0) { return data.remove(obj, refCount); } return false; } 这里需要解释的就是这个条件remove方法,即当前在对这个Object对象的引用计数已经减到0的情况下,我们不能直接将其移除,因为在这个中间可能有另一个线程又增加了对它的引用计数,所以我们需要使用条件remove,即只有当前Map中对这个Object对象的值和传入的值相等时才将它移除,这样就保证了当前线程的操作不会覆盖中间其他线程的结果。 在这个remove的实现中,有人可能会说在data.get(data)这句话执行完成后,加入它返回null,此时其他线程可能已经会添加了这个Object对象的引用,此时这个方法的执行结果就不对了,但是在这种情况下,即使加锁,也无法解决这个问题,而且很多情况下的线程安全只能保证happen-before原则。关于这个类的实现也有一些其他细节的东西,具体可以查看这里: https://github.com/dinglevin/levin-tools/blob/master/src/main/java/org/levin/tools/corejava/sets/ReferenceCountSet.java
转自:http://blog.csdn.net/chen77716/article/details/6641477前文(深入JVM锁机制-synchronized)分析了JVM中的synchronized实现,本文继续分析JVM中的另一种锁Lock的实现。与synchronized不同的是,Lock完全用Java写成,在java这个层面是无关JVM实现的。 在java.util.concurrent.locks包中有很多Lock的实现类,常用的有ReentrantLock、ReadWriteLock(实现类ReentrantReadWriteLock),其实现都依赖java.util.concurrent.AbstractQueuedSynchronizer类,实现思路都大同小异,因此我们以ReentrantLock作为讲解切入点。 1. ReentrantLock的调用过程 经过观察ReentrantLock把所有Lock接口的操作都委派到一个Sync类上,该类继承了AbstractQueuedSynchronizer: [java] view plaincopy static abstract class Sync extends AbstractQueuedSynchronizer Sync又有两个子类: [java] view plaincopy final static class NonfairSync extends Sync [java] view plaincopy final static class FairSync extends Sync 显然是为了支持公平锁和非公平锁而定义,默认情况下为非公平锁。 先理一下Reentrant.lock()方法的调用过程(默认非公平锁): 这些讨厌的Template模式导致很难直观的看到整个调用过程,其实通过上面调用过程及AbstractQueuedSynchronizer的注释可以发现,AbstractQueuedSynchronizer中抽象了绝大多数Lock的功能,而只把tryAcquire方法延迟到子类中实现。tryAcquire方法的语义在于用具体子类判断请求线程是否可以获得锁,无论成功与否AbstractQueuedSynchronizer都将处理后面的流程。 2. 锁实现(加锁) 简单说来,AbstractQueuedSynchronizer会把所有的请求线程构成一个CLH队列,当一个线程执行完毕(lock.unlock())时会激活自己的后继节点,但正在执行的线程并不在队列中,而那些等待执行的线程全部处于阻塞状态,经过调查线程的显式阻塞是通过调用LockSupport.park()完成,而LockSupport.park()则调用sun.misc.Unsafe.park()本地方法,再进一步,HotSpot在Linux中中通过调用pthread_mutex_lock函数把线程交给系统内核进行阻塞。 该队列如图: 与synchronized相同的是,这也是一个虚拟队列,不存在队列实例,仅存在节点之间的前后关系。令人疑惑的是为什么采用CLH队列呢?原生的CLH队列是用于自旋锁,但Doug Lea把其改造为阻塞锁。 当有线程竞争锁时,该线程会首先尝试获得锁,这对于那些已经在队列中排队的线程来说显得不公平,这也是非公平锁的由来,与synchronized实现类似,这样会极大提高吞吐量。 如果已经存在Running线程,则新的竞争线程会被追加到队尾,具体是采用基于CAS的Lock-Free算法,因为线程并发对Tail调用CAS可能会导致其他线程CAS失败,解决办法是循环CAS直至成功。AbstractQueuedSynchronizer的实现非常精巧,令人叹为观止,不入细节难以完全领会其精髓,下面详细说明实现过程: 2.1 Sync.nonfairTryAcquire nonfairTryAcquire方法将是lock方法间接调用的第一个方法,每次请求锁时都会首先调用该方法。 [java] view plaincopy final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } 该方法会首先判断当前状态,如果c==0说明没有线程正在竞争该锁,如果不c !=0 说明有线程正拥有了该锁。 如果发现c==0,则通过CAS设置该状态值为acquires,acquires的初始调用值为1,每次线程重入该锁都会+1,每次unlock都会-1,但为0时释放锁。如果CAS设置成功,则可以预计其他任何线程调用CAS都不会再成功,也就认为当前线程得到了该锁,也作为Running线程,很显然这个Running线程并未进入等待队列。 如果c !=0 但发现自己已经拥有锁,只是简单地++acquires,并修改status值,但因为没有竞争,所以通过setStatus修改,而非CAS,也就是说这段代码实现了偏向锁的功能,并且实现的非常漂亮。 2.2 AbstractQueuedSynchronizer.addWaiter addWaiter方法负责把当前无法获得锁的线程包装为一个Node添加到队尾: [java] view plaincopy private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; } 其中参数mode是独占锁还是共享锁,默认为null,独占锁。追加到队尾的动作分两步: 如果当前队尾已经存在(tail!=null),则使用CAS把当前线程更新为Tail 如果当前Tail为null或则线程调用CAS设置队尾失败,则通过enq方法继续设置Tail 下面是enq方法: [java] view plaincopy private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize Node h = new Node(); // Dummy header h.next = node; node.prev = h; if (compareAndSetHead(h)) { tail = node; return h; } } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } } 该方法就是循环调用CAS,即使有高并发的场景,无限循环将会最终成功把当前线程追加到队尾(或设置队头)。总而言之,addWaiter的目的就是通过CAS把当前现在追加到队尾,并返回包装后的Node实例。 把线程要包装为Node对象的主要原因,除了用Node构造供虚拟队列外,还用Node包装了各种线程状态,这些状态被精心设计为一些数字值: SIGNAL(-1) :线程的后继线程正/已被阻塞,当该线程release或cancel时要重新这个后继线程(unpark) CANCELLED(1):因为超时或中断,该线程已经被取消 CONDITION(-2):表明该线程被处于条件队列,就是因为调用了Condition.await而被阻塞 PROPAGATE(-3):传播共享锁 0:0代表无状态 2.3 AbstractQueuedSynchronizer.acquireQueued acquireQueued的主要作用是把已经追加到队列的线程节点(addWaiter方法返回值)进行阻塞,但阻塞前又通过tryAccquire重试是否能获得锁,如果重试成功能则无需阻塞,直接返回 [java] view plaincopy final boolean acquireQueued(final Node node, int arg) { try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } catch (RuntimeException ex) { cancelAcquire(node); throw ex; } } 仔细看看这个方法是个无限循环,感觉如果p == head && tryAcquire(arg)条件不满足循环将永远无法结束,当然不会出现死循环,奥秘在于第12行的parkAndCheckInterrupt会把当前线程挂起,从而阻塞住线程的调用栈。 [java] view plaincopy private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); } 如前面所述,LockSupport.park最终把线程交给系统(Linux)内核进行阻塞。当然也不是马上把请求不到锁的线程进行阻塞,还要检查该线程的状态,比如如果该线程处于Cancel状态则没有必要,具体的检查在shouldParkAfterFailedAcquire中: [java] view plaincopy private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park */ return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } 检查原则在于: 规则1:如果前继的节点状态为SIGNAL,表明当前节点需要unpark,则返回成功,此时acquireQueued方法的第12行(parkAndCheckInterrupt)将导致线程阻塞 规则2:如果前继节点状态为CANCELLED(ws>0),说明前置节点已经被放弃,则回溯到一个非取消的前继节点,返回false,acquireQueued方法的无限循环将递归调用该方法,直至规则1返回true,导致线程阻塞 规则3:如果前继节点状态为非SIGNAL、非CANCELLED,则设置前继的状态为SIGNAL,返回false后进入acquireQueued的无限循环,与规则2同 总体看来,shouldParkAfterFailedAcquire就是靠前继节点判断当前线程是否应该被阻塞,如果前继节点处于CANCELLED状态,则顺便删除这些节点重新构造队列。 至此,锁住线程的逻辑已经完成,下面讨论解锁的过程。 3. 解锁 请求锁不成功的线程会被挂起在acquireQueued方法的第12行,12行以后的代码必须等线程被解锁锁才能执行,假如被阻塞的线程得到解锁,则执行第13行,即设置interrupted = true,之后又进入无限循环。 从无限循环的代码可以看出,并不是得到解锁的线程一定能获得锁,必须在第6行中调用tryAccquire重新竞争,因为锁是非公平的,有可能被新加入的线程获得,从而导致刚被唤醒的线程再次被阻塞,这个细节充分体现了“非公平”的精髓。通过之后将要介绍的解锁机制会看到,第一个被解锁的线程就是Head,因此p == head的判断基本都会成功。 至此可以看到,把tryAcquire方法延迟到子类中实现的做法非常精妙并具有极强的可扩展性,令人叹为观止!当然精妙的不是这个Templae设计模式,而是Doug Lea对锁结构的精心布局。 解锁代码相对简单,主要体现在AbstractQueuedSynchronizer.release和Sync.tryRelease方法中: class AbstractQueuedSynchronizer [java] view plaincopy public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } class Sync [java] view plaincopy protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; } tryRelease与tryAcquire语义相同,把如何释放的逻辑延迟到子类中。tryRelease语义很明确:如果线程多次锁定,则进行多次释放,直至status==0则真正释放锁,所谓释放锁即设置status为0,因为无竞争所以没有使用CAS。 release的语义在于:如果可以释放锁,则唤醒队列第一个线程(Head),具体唤醒代码如下: [java] view plaincopy private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); } 这段代码的意思在于找出第一个可以unpark的线程,一般说来head.next == head,Head就是第一个线程,但Head.next可能被取消或被置为null,因此比较稳妥的办法是从后往前找第一个可用线程。貌似回溯会导致性能降低,其实这个发生的几率很小,所以不会有性能影响。之后便是通知系统内核继续该线程,在Linux下是通过pthread_mutex_unlock完成。之后,被解锁的线程进入上面所说的重新竞争状态。 4. Lock VS Synchronized AbstractQueuedSynchronizer通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过Lock-Free(CAS)操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。 synchronized的底层也是一个基于CAS操作的等待队列,但JVM实现的更精细,把等待队列分为ContentionList和EntryList,目的是为了降低线程的出列速度;当然也实现了偏向锁,从数据结构来说二者设计没有本质区别。但synchronized还实现了自旋锁,并针对不同的系统和硬件体系进行了优化,而Lock则完全依靠系统阻塞挂起等待线程。 当然Lock比synchronized更适合在应用层扩展,可以继承AbstractQueuedSynchronizer定义各种实现,比如实现读写锁(ReadWriteLock),公平或不公平锁;同时,Lock对应的Condition也比wait/notify要方便的多、灵活的多。
转自:http://blog.csdn.net/chen77716/article/details/6618779 目前在Java中存在两种锁机制:synchronized和Lock,Lock接口及其实现类是JDK5增加的内容,其作者是大名鼎鼎的并发专家Doug Lea。本文并不比较synchronized与Lock孰优孰劣,只是介绍二者的实现原理。 数据同步需要依赖锁,那锁的同步又依赖谁?synchronized给出的答案是在软件层面依赖JVM,而Lock给出的方案是在硬件层面依赖特殊的CPU指令,大家可能会进一步追问:JVM底层又是如何实现synchronized的? 本文所指说的JVM是指Hotspot的6u23版本,下面首先介绍synchronized的实现: synrhronized关键字简洁、清晰、语义明确,因此即使有了Lock接口,使用的还是非常广泛。其应用层的语义是可以把任何一个非null对象作为"锁",当synchronized作用在方法上时,锁住的便是对象实例(this);当作用在静态方法时锁住的便是对象对应的Class实例,因为Class数据存在于永久带,因此静态方法锁相当于该类的一个全局锁;当synchronized作用于某一个对象实例时,锁住的便是对应的代码块。在HotSpot JVM实现中,锁有个专门的名字:对象监视器。 1. 线程状态及状态转换 当多个线程同时请求某个对象监视器时,对象监视器会设置几种状态用来区分请求的线程: Contention List:所有请求锁的线程将被首先放置到该竞争队列 Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck Owner:获得锁的线程称为Owner !Owner:释放锁的线程 下图反映了个状态转换关系: 新请求锁的线程将首先被加入到ConetentionList中,当某个拥有锁的线程(Owner状态)调用unlock之后,如果发现EntryList为空则从ContentionList中移动线程到EntryList,下面说明下ContentionList和EntryList的实现方式: 1.1 ContentionList虚拟队列 ContentionList并不是一个真正的Queue,而只是一个虚拟队列,原因在于ContentionList是由Node及其next指针逻辑构成,并不存在一个Queue的数据结构。ContentionList是一个后进先出(LIFO)的队列,每次新加入Node时都会在队头进行,通过CAS改变第一个节点的的指针为新增节点,同时设置新增节点的next指向后续节点,而取得操作则发生在队尾。显然,该结构其实是个Lock-Free的队列。 因为只有Owner线程才能从队尾取元素,也即线程出列操作无争用,当然也就避免了CAS的ABA问题。 1.2 EntryList EntryList与ContentionList逻辑上同属等待队列,ContentionList会被线程并发访问,为了降低对ContentionList队尾的争用,而建立EntryList。Owner线程在unlock时会从ContentionList中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head)为Ready(OnDeck)线程。Owner线程并不是把锁传递给OnDeck线程,只是把竞争锁的权利交给OnDeck,OnDeck线程需要重新竞争锁。这样做虽然牺牲了一定的公平性,但极大的提高了整体吞吐量,在Hotspot中把OnDeck的选择行为称之为“竞争切换”。 OnDeck线程获得锁后即变为owner线程,无法获得锁则会依然留在EntryList中,考虑到公平性,在EntryList中的位置不发生变化(依然在队头)。如果Owner线程被wait方法阻塞,则转移到WaitSet队列;如果在某个时刻被notify/notifyAll唤醒,则再次转移到EntryList。 2. 自旋锁 那些处于ContetionList、EntryList、WaitSet中的线程均处于阻塞状态,阻塞操作由操作系统完成(在Linxu下通过pthread_mutex_lock函数)。线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能 缓解上述问题的办法便是自旋,其原理是:当发生争用时,若Owner线程能在很短的时间内释放锁,则那些正在争用线程可以稍微等一等(自旋),在Owner线程释放锁后,争用线程可能会立即得到锁,从而避免了系统阻塞。但Owner运行的时间可能会超出了临界值,争用线程自旋一段时间后还是无法获得锁,这时争用线程则会停止自旋进入阻塞状态(后退)。基本思路就是自旋,不成功再阻塞,尽量降低阻塞的可能性,这对那些执行时间很短的代码块来说有非常重要的性能提高。自旋锁有个更贴切的名字:自旋-指数后退锁,也即复合锁。很显然,自旋在多处理器上才有意义。 还有个问题是,线程自旋时做些啥?其实啥都不做,可以执行几次for循环,可以执行几条空的汇编指令,目的是占着CPU不放,等待获取锁的机会。所以说,自旋是把双刃剑,如果旋的时间过长会影响整体性能,时间过短又达不到延迟阻塞的目的。显然,自旋的周期选择显得非常重要,但这与操作系统、硬件体系、系统的负载等诸多场景相关,很难选择,如果选择不当,不但性能得不到提高,可能还会下降,因此大家普遍认为自旋锁不具有扩展性。 对自旋锁周期的选择上,HotSpot认为最佳时间应是一个线程上下文切换的时间,但目前并没有做到。经过调查,目前只是通过汇编暂停了几个CPU周期,除了自旋周期选择,HotSpot还进行许多其他的自旋优化策略,具体如下: 如果平均负载小于CPUs则一直自旋 如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞 如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞 如果CPU处于节电模式则停止自旋 自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差) 自旋时会适当放弃线程优先级之间的差异 那synchronized实现何时使用了自旋锁?答案是在线程进入ContentionList时,也即第一步操作前。线程在进入等待队列时首先进行自旋尝试获得锁,如果不成功再进入等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平。还有一个不公平的地方是自旋线程可能会抢占了Ready线程的锁。自旋锁由每个监视对象维护,每个监视对象一个。 3. 偏向锁 在JVM1.6中引入了偏向锁,偏向锁主要解决无竞争下的锁性能问题,首先我们看下无竞争下锁存在什么问题: 现在几乎所有的锁都是可重入的,也即已经获得锁的线程可以多次锁住/解锁监视对象,按照之前的HotSpot设计,每次加锁/解锁都会涉及到一些CAS操作(比如对等待队列的CAS操作),CAS操作会延迟本地调用,因此偏向锁的想法是一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。但还有很多概念需要解释、很多引入的问题需要解决: 3.1 CAS及SMP架构 CAS为什么会引入本地延迟?这要从SMP(对称多处理器)架构说起,下图大概表明了SMP的结构: 其意思是所有的CPU会共享一条系统总线(BUS),靠此总线连接主存。每个核都有自己的一级缓存,各核相对于BUS对称分布,因此这种结构称为“对称多处理器”。 而CAS的全称为Compare-And-Swap,是一条CPU的原子指令,其作用是让CPU比较后原子地更新某个位置的值,经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。 Core1和Core2可能会同时把主存中某个位置的值Load到自己的L1 Cache中,当Core1在自己的L1 Cache中修改这个位置的值时,会通过总线,使Core2中L1 Cache对应的值“失效”,而Core2一旦发现自己L1 Cache中的值失效(称为Cache命中缺失)则会通过总线从内存中加载该地址最新的值,大家通过总线的来回通信称为“Cache一致性流量”,因为总线被设计为固定的“通信能力”,如果Cache一致性流量过大,总线将成为瓶颈。而当Core1和Core2中的值再次一致时,称为“Cache一致性”,从这个层面来说,锁设计的终极目标便是减少Cache一致性流量。 而CAS恰好会导致Cache一致性流量,如果有很多线程都共享同一个对象,当某个Core CAS成功时必然会引起总线风暴,这就是所谓的本地延迟,本质上偏向锁就是为了消除CAS,降低Cache一致性流量。 Cache一致性: 上面提到Cache一致性,其实是有协议支持的,现在通用的协议是MESI(最早由Intel开始支持),具体参考:http://en.wikipedia.org/wiki/MESI_protocol,以后会仔细讲解这部分。 Cache一致性流量的例外情况: 其实也不是所有的CAS都会导致总线风暴,这跟Cache一致性协议有关,具体参考:http://blogs.oracle.com/dave/entry/biased_locking_in_hotspot NUMA(Non Uniform Memory Access Achitecture)架构: 与SMP对应还有非对称多处理器架构,现在主要应用在一些高端处理器上,主要特点是没有总线,没有公用主存,每个Core有自己的内存,针对这种结构此处不做讨论。 3.2 偏向解除 偏向锁引入的一个重要问题是,在多争用的场景下,如果另外一个线程争用偏向对象,拥有者需要释放偏向锁,而释放的过程会带来一些性能开销,但总体说来偏向锁带来的好处还是大于CAS代价的。 4. 总结 关于锁,JVM中还引入了一些其他技术比如锁膨胀等,这些与自旋锁、偏向锁相比影响不是很大,这里就不做介绍。 通过上面的介绍可以看出,synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。下面会继续介绍JVM锁中的Lock(深入JVM锁2-Lock)。
引用:http://blog.codinglabs.org/articles/theory-of-mysql-index.html摘要 本文以MySQL数据库为研究对象,讨论与数据库索引相关的一些话题。特别需要说明的是,MySQL支持诸多存储引擎,而各种存储引擎对索引的支持也各不相同,因此MySQL数据库支持多种索引类型,如BTree索引,哈希索引,全文索引等等。为了避免混乱,本文将只关注于BTree索引,因为这是平常使用MySQL时主要打交道的索引,至于哈希索引和全文索引本文暂不讨论。 文章主要内容分为三个部分。 第一部分主要从数据结构及算法理论层面讨论MySQL数据库索引的数理基础。 第二部分结合MySQL数据库中MyISAM和InnoDB数据存储引擎中索引的架构实现讨论聚集索引、非聚集索引及覆盖索引等话题。 第三部分根据上面的理论基础,讨论MySQL中高性能使用索引的策略。 数据结构及算法基础 索引的本质 MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。提取句子主干,就可以得到索引的本质:索引是数据结构。 我们知道,数据库查询是数据库的最主要功能之一。我们都希望查询数据的速度能尽可能的快,因此数据库系统的设计者会从查询算法的角度进行优化。最基本的查询算法当然是顺序查找(linear search),这种复杂度为O(n)的算法在数据量很大时显然是糟糕的,好在计算机科学的发展提供了很多更优秀的查找算法,例如二分查找(binary search)、二叉树查找(binary tree search)等。如果稍微分析一下会发现,每种查找算法都只能应用于特定的数据结构之上,例如二分查找要求被检索数据有序,而二叉树查找只能应用于二叉查找树上,但是数据本身的组织结构不可能完全满足各种数据结构(例如,理论上不可能同时将两列都按顺序进行组织),所以,在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。 看一个例子: 图1 图1展示了一种可能的索引方式。左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的)。为了加快Col2的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针,这样就可以运用二叉查找在O(log2n)的复杂度内获取到相应数据。 虽然这是一个货真价实的索引,但是实际的数据库系统几乎没有使用二叉查找树或其进化品种红黑树(red-black tree)实现的,原因会在下文介绍。 B-Tree和B+Tree 目前大部分数据库系统及文件系统都采用B-Tree或其变种B+Tree作为索引结构,在本文的下一节会结合存储器原理及计算机存取原理讨论为什么B-Tree和B+Tree在被如此广泛用于索引,这一节先单纯从数据结构角度描述它们。 B-Tree 为了描述B-Tree,首先定义一条数据记录为一个二元组[key, data],key为记录的键值,对于不同数据记录,key是互不相同的;data为数据记录除key外的数据。那么B-Tree是满足下列条件的数据结构: d为大于1的一个正整数,称为B-Tree的度。 h为一个正整数,称为B-Tree的高度。 每个非叶子节点由n-1个key和n个指针组成,其中d<=n<=2d。 每个叶子节点最少包含一个key和两个指针,最多包含2d-1个key和2d个指针,叶节点的指针均为null 。 所有叶节点具有相同的深度,等于树高h。 key和指针互相间隔,节点两端是指针。 一个节点中的key从左到右非递减排列。 所有节点组成树结构。 每个指针要么为null,要么指向另外一个节点。 如果某个指针在节点node最左边且不为null,则其指向节点的所有key小于v(key1),其中v(key1)为node的第一个key的值。 如果某个指针在节点node最右边且不为null,则其指向节点的所有key大于v(keym),其中v(keym)为node的最后一个key的值。 如果某个指针在节点node的左右相邻key分别是keyi和keyi+1且不为null,则其指向节点的所有key小于v(keyi+1)且大于v(keyi)。 图2是一个d=2的B-Tree示意图。 图2 由于B-Tree的特性,在B-Tree中按key检索数据的算法非常直观:首先从根节点进行二分查找,如果找到则返回对应节点的data,否则对相应区间的指针指向的节点递归进行查找,直到找到节点或找到null指针,前者查找成功,后者查找失败。B-Tree上查找算法的伪代码如下: BTree_Search(node, key) { if(node == null) return null; foreach(node.key) { if(node.key[i] == key) return node.data[i]; if(node.key[i] > key) return BTree_Search(point[i]->node); } return BTree_Search(point[i+1]->node); } data = BTree_Search(root, my_key); 关于B-Tree有一系列有趣的性质,例如一个度为d的B-Tree,设其索引N个key,则其树高h的上限为logd((N+1)/2),检索一个key,其查找节点个数的渐进复杂度为O(logdN)。从这点可以看出,B-Tree是一个非常有效率的索引数据结构。 另外,由于插入删除新的数据记录会破坏B-Tree的性质,因此在插入删除时,需要对树进行一个分裂、合并、转移等操作以保持B-Tree性质,本文不打算完整讨论B-Tree这些内容,因为已经有许多资料详细说明了B-Tree的数学性质及插入删除算法,有兴趣的朋友可以在本文末的参考文献一栏找到相应的资料进行阅读。 B+Tree B-Tree有许多变种,其中最常见的是B+Tree,例如MySQL就普遍使用B+Tree实现其索引结构。 与B-Tree相比,B+Tree有以下不同点: 每个节点的指针上限为2d而不是2d+1。 内节点不存储data,只存储key;叶子节点不存储指针。 图3是一个简单的B+Tree示意。 图3 由于并不是所有节点都具有相同的域,因此B+Tree中叶节点和内节点一般大小不同。这点与B-Tree不同,虽然B-Tree中不同节点存放的key和指针可能数量不一致,但是每个节点的域和上限是一致的,所以在实现中B-Tree往往对每个节点申请同等大小的空间。 一般来说,B+Tree比B-Tree更适合实现外存储索引结构,具体原因与外存储器原理及计算机存取原理有关,将在下面讨论。 带有顺序访问指针的B+Tree 一般在数据库系统或文件系统中使用的B+Tree结构都在经典B+Tree的基础上进行了优化,增加了顺序访问指针。 图4 如图4所示,在B+Tree的每个叶子节点增加一个指向相邻叶子节点的指针,就形成了带有顺序访问指针的B+Tree。做这个优化的目的是为了提高区间访问的性能,例如图4中如果要查询key为从18到49的所有数据记录,当找到18后,只需顺着节点和指针顺序遍历就可以一次性访问到所有数据节点,极大提到了区间查询效率。 这一节对B-Tree和B+Tree进行了一个简单的介绍,下一节结合存储器存取原理介绍为什么目前B+Tree是数据库系统实现索引的首选数据结构。 为什么使用B-Tree(B+Tree) 上文说过,红黑树等数据结构也可以用来实现索引,但是文件系统及数据库系统普遍采用B-/+Tree作为索引结构,这一节将结合计算机组成原理相关知识讨论B-/+Tree作为索引的理论基础。 一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。下面先介绍内存和磁盘存取原理,然后再结合这些原理分析B-/+Tree作为索引的效率。 主存存取原理 目前计算机使用的主存基本都是随机读写存储器(RAM),现代RAM的结构和存取原理比较复杂,这里本文抛却具体差别,抽象出一个十分简单的存取模型来说明RAM的工作原理。 图5 从抽象角度看,主存是一系列的存储单元组成的矩阵,每个存储单元存储固定大小的数据。每个存储单元有唯一的地址,现代主存的编址规则比较复杂,这里将其简化成一个二维地址:通过一个行地址和一个列地址可以唯一定位到一个存储单元。图5展示了一个4 x 4的主存模型。 主存的存取过程如下: 当系统需要读取主存时,则将地址信号放到地址总线上传给主存,主存读到地址信号后,解析信号并定位到指定存储单元,然后将此存储单元数据放到数据总线上,供其它部件读取。 写主存的过程类似,系统将要写入单元地址和数据分别放在地址总线和数据总线上,主存读取两个总线的内容,做相应的写操作。 这里可以看出,主存存取的时间仅与存取次数呈线性关系,因为不存在机械操作,两次存取的数据的“距离”不会对时间有任何影响,例如,先取A0再取A1和先取A0再取D3的时间消耗是一样的。 磁盘存取原理 上文说过,索引一般以文件形式存储在磁盘上,索引检索需要磁盘I/O操作。与主存不同,磁盘I/O存在机械运动耗费,因此磁盘I/O的时间消耗是巨大的。 图6是磁盘的整体结构示意图。 图6 一个磁盘由大小相同且同轴的圆形盘片组成,磁盘可以转动(各个磁盘必须同步转动)。在磁盘的一侧有磁头支架,磁头支架固定了一组磁头,每个磁头负责存取一个磁盘的内容。磁头不能转动,但是可以沿磁盘半径方向运动(实际是斜切向运动),每个磁头同一时刻也必须是同轴的,即从正上方向下看,所有磁头任何时候都是重叠的(不过目前已经有多磁头独立技术,可不受此限制)。 图7是磁盘结构的示意图。 图7 盘片被划分成一系列同心环,圆心是盘片中心,每个同心环叫做一个磁道,所有半径相同的磁道组成一个柱面。磁道被沿半径线划分成一个个小的段,每个段叫做一个扇区,每个扇区是磁盘的最小存储单元。为了简单起见,我们下面假设磁盘只有一个盘片和一个磁头。 当需要从磁盘读取数据时,系统会将数据逻辑地址传给磁盘,磁盘的控制电路按照寻址逻辑将逻辑地址翻译成物理地址,即确定要读的数据在哪个磁道,哪个扇区。为了读取这个扇区的数据,需要将磁头放到这个扇区上方,为了实现这一点,磁头需要移动对准相应磁道,这个过程叫做寻道,所耗费时间叫做寻道时间,然后磁盘旋转将目标扇区旋转到磁头下,这个过程耗费的时间叫做旋转时间。 局部性原理与磁盘预读 由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,磁盘的存取速度往往是主存的几百分分之一,因此为了提高效率,要尽量减少磁盘I/O。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。这样做的理论依据是计算机科学中著名的局部性原理: 当一个数据被用到时,其附近的数据也通常会马上被使用。 程序运行期间所需要的数据通常比较集中。 由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高I/O效率。 预读的长度一般为页(page)的整倍数。页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页得大小通常为4k),主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,然后异常返回,程序继续运行。 B-/+Tree索引的性能分析 到这里终于可以分析B-/+Tree索引的性能了。 上文说过一般使用磁盘I/O次数评价索引结构的优劣。先从B-Tree分析,根据B-Tree的定义,可知检索一次最多需要访问h个节点。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的,在实际实现B-Tree还需要使用如下技巧: 每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O。 B-Tree中一次检索最多需要h-1次I/O(根节点常驻内存),渐进复杂度为O(h)=O(logdN)。一般实际应用中,出度d是非常大的数字,通常超过100,因此h非常小(通常不超过3)。 综上所述,用B-Tree作为索引结构效率是非常高的。 而红黑树这种结构,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,所以红黑树的I/O渐进复杂度也为O(h),效率明显比B-Tree差很多。 上文还说过,B+Tree更适合外存索引,原因和内节点出度d有关。从上面分析可以看到,d越大索引的性能越好,而出度的上限取决于节点内key和data的大小: dmax=floor(pagesize/(keysize+datasize+pointsize)) floor表示向下取整。由于B+Tree内节点去掉了data域,因此可以拥有更大的出度,拥有更好的性能。 这一章从理论角度讨论了与索引相关的数据结构与算法问题,下一章将讨论B+Tree是如何具体实现为MySQL中索引,同时将结合MyISAM和InnDB存储引擎介绍非聚集索引和聚集索引两种不同的索引实现形式。 MySQL索引实现 在MySQL中,索引属于存储引擎级别的概念,不同存储引擎对索引的实现方式是不同的,本文主要讨论MyISAM和InnoDB两个存储引擎的索引实现方式。 MyISAM索引实现 MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址。下图是MyISAM索引的原理图: 图8 这里设表一共有三列,假设我们以Col1为主键,则图8是一个MyISAM表的主索引(Primary key)示意。可以看出MyISAM的索引文件仅仅保存数据记录的地址。在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复。如果我们在Col2上建立一个辅助索引,则此索引的结构如下图所示: 图9 同样也是一颗B+Tree,data域保存数据记录的地址。因此,MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。 MyISAM的索引方式也叫做“非聚集”的,之所以这么称呼是为了与InnoDB的聚集索引区分。 InnoDB索引实现 虽然InnoDB也使用B+Tree作为索引结构,但具体实现方式却与MyISAM截然不同。 第一个重大区别是InnoDB的数据文件本身就是索引文件。从上文知道,MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而在InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。 图10 图10是InnoDB主索引(同时也是数据文件)的示意图,可以看到叶节点包含了完整的数据记录。这种索引叫做聚集索引。因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。 第二个与MyISAM索引的不同是InnoDB的辅助索引data域存储相应记录主键的值而不是地址。换句话说,InnoDB的所有辅助索引都引用主键作为data域。例如,图11为定义在Col3上的一个辅助索引: 图11 这里以英文字符的ASCII码作为比较准则。聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。 了解不同存储引擎的索引实现方式对于正确使用和优化索引都非常有帮助,例如知道了InnoDB的索引实现后,就很容易明白为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。再例如,用非单调的字段作为主键在InnoDB中不是个好主意,因为InnoDB数据文件本身是一颗B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。 下一章将具体讨论这些与索引有关的优化策略。 索引使用策略及优化 MySQL的优化主要分为结构优化(Scheme optimization)和查询优化(Query optimization)。本章讨论的高性能索引策略主要属于结构优化范畴。本章的内容完全基于上文的理论基础,实际上一旦理解了索引背后的机制,那么选择高性能的策略就变成了纯粹的推理,并且可以理解这些策略背后的逻辑。 示例数据库 为了讨论索引策略,需要一个数据量不算小的数据库作为示例。本文选用MySQL官方文档中提供的示例数据库之一:employees。这个数据库关系复杂度适中,且数据量较大。下图是这个数据库的E-R关系图(引用自MySQL官方手册): 图12 MySQL官方文档中关于此数据库的页面为http://dev.mysql.com/doc/employee/en/employee.html。里面详细介绍了此数据库,并提供了下载地址和导入方法,如果有兴趣导入此数据库到自己的MySQL可以参考文中内容。 最左前缀原理与相关优化 高效使用索引的首要条件是知道什么样的查询会使用到索引,这个问题和B+Tree中的“最左前缀原理”有关,下面通过例子说明最左前缀原理。 这里先说一下联合索引的概念。在上文中,我们都是假设索引只引用了单个的列,实际上,MySQL中的索引可以以一定顺序引用多个列,这种索引叫做联合索引,一般的,一个联合索引是一个有序元组<a1, a2, …, an>,其中各个元素均为数据表的一列,实际上要严格定义索引需要用到关系代数,但是这里我不想讨论太多关系代数的话题,因为那样会显得很枯燥,所以这里就不再做严格定义。另外,单列索引可以看成联合索引元素数为1的特例。 以employees.titles表为例,下面先查看其上都有哪些索引: SHOW INDEX FROM employees.titles; +--------+------------+----------+--------------+-------------+-----------+-------------+------+------------+ | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Null | Index_type | +--------+------------+----------+--------------+-------------+-----------+-------------+------+------------+ | titles | 0 | PRIMARY | 1 | emp_no | A | NULL | | BTREE | | titles | 0 | PRIMARY | 2 | title | A | NULL | | BTREE | | titles | 0 | PRIMARY | 3 | from_date | A | 443308 | | BTREE | | titles | 1 | emp_no | 1 | emp_no | A | 443308 | | BTREE | +--------+------------+----------+--------------+-------------+-----------+-------------+------+------------+ 从结果中可以到titles表的主索引为<emp_no, title, from_date>,还有一个辅助索引<emp_no>。为了避免多个索引使事情变复杂(MySQL的SQL优化器在多索引时行为比较复杂),这里我们将辅助索引drop掉: ALTER TABLE employees.titles DROP INDEX emp_no; 这样就可以专心分析索引PRIMARY的行为了。 情况一:全列匹配。 EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND title='Senior Engineer' AND from_date='1986-06-26'; +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+ | 1 | SIMPLE | titles | const | PRIMARY | PRIMARY | 59 | const,const,const | 1 | | +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+ 很明显,当按照索引中所有列进行精确匹配(这里精确匹配指“=”或“IN”匹配)时,索引可以被用到。这里有一点需要注意,理论上索引对顺序是敏感的,但是由于MySQL的查询优化器会自动调整where子句的条件顺序以使用适合的索引,例如我们将where中的条件顺序颠倒: EXPLAIN SELECT * FROM employees.titles WHERE from_date='1986-06-26' AND emp_no='10001' AND title='Senior Engineer'; +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+ | 1 | SIMPLE | titles | const | PRIMARY | PRIMARY | 59 | const,const,const | 1 | | +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+ 效果是一样的。 情况二:最左前缀匹配。 EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001'; +----+-------------+--------+------+---------------+---------+---------+-------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+------+---------------+---------+---------+-------+------+-------+ | 1 | SIMPLE | titles | ref | PRIMARY | PRIMARY | 4 | const | 1 | | +----+-------------+--------+------+---------------+---------+---------+-------+------+-------+ 当查询条件精确匹配索引的左边连续一个或几个列时,如<emp_no>或<emp_no, title>,所以可以被用到,但是只能用到一部分,即条件所组成的最左前缀。上面的查询从分析结果看用到了PRIMARY索引,但是key_len为4,说明只用到了索引的第一列前缀。 情况三:查询条件用到了索引中列的精确匹配,但是中间某个条件未提供。 EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND from_date='1986-06-26'; +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+ | 1 | SIMPLE | titles | ref | PRIMARY | PRIMARY | 4 | const | 1 | Using where | +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+ 此时索引使用情况和情况二相同,因为title未提供,所以查询只用到了索引的第一列,而后面的from_date虽然也在索引中,但是由于title不存在而无法和左前缀连接,因此需要对结果进行扫描过滤from_date(这里由于emp_no唯一,所以不存在扫描)。如果想让from_date也使用索引而不是where过滤,可以增加一个辅助索引<emp_no, from_date>,此时上面的查询会使用这个索引。除此之外,还可以使用一种称之为“隔离列”的优化方法,将emp_no与from_date之间的“坑”填上。 首先我们看下title一共有几种不同的值: SELECT DISTINCT(title) FROM employees.titles; +--------------------+ | title | +--------------------+ | Senior Engineer | | Staff | | Engineer | | Senior Staff | | Assistant Engineer | | Technique Leader | | Manager | +--------------------+ 只有7种。在这种成为“坑”的列值比较少的情况下,可以考虑用“IN”来填补这个“坑”从而形成最左前缀: EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND title IN ('Senior Engineer', 'Staff', 'Engineer', 'Senior Staff', 'Assistant Engineer', 'Technique Leader', 'Manager') AND from_date='1986-06-26'; +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 59 | NULL | 7 | Using where | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ 这次key_len为59,说明索引被用全了,但是从type和rows看出IN实际上执行了一个range查询,这里检查了7个key。看下两种查询的性能比较: SHOW PROFILES; +----------+------------+-------------------------------------------------------------------------------+ | Query_ID | Duration | Query | +----------+------------+-------------------------------------------------------------------------------+ | 10 | 0.00058000 | SELECT * FROM employees.titles WHERE emp_no='10001' AND from_date='1986-06-26'| | 11 | 0.00052500 | SELECT * FROM employees.titles WHERE emp_no='10001' AND title IN ... | +----------+------------+-------------------------------------------------------------------------------+ “填坑”后性能提升了一点。如果经过emp_no筛选后余下很多数据,则后者性能优势会更加明显。当然,如果title的值很多,用填坑就不合适了,必须建立辅助索引。 情况四:查询条件没有指定索引第一列。 EXPLAIN SELECT * FROM employees.titles WHERE from_date='1986-06-26'; +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+ | 1 | SIMPLE | titles | ALL | NULL | NULL | NULL | NULL | 443308 | Using where | +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+ 由于不是最左前缀,索引这样的查询显然用不到索引。 情况五:匹配某列的前缀字符串。 EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND title LIKE 'Senior%'; +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 56 | NULL | 1 | Using where | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ 此时可以用到索引,但是如果通配符不是只出现在末尾,则无法使用索引。(原文表述有误,如果通配符%不出现在开头,则可以用到索引,但根据具体情况不同可能只会用其中一个前缀) 情况六:范围查询。 EXPLAIN SELECT * FROM employees.titles WHERE emp_no < '10010' and title='Senior Engineer'; +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 4 | NULL | 16 | Using where | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ 范围列可以用到索引(必须是最左前缀),但是范围列后面的列无法用到索引。同时,索引最多用于一个范围列,因此如果查询条件中有两个范围列则无法全用到索引。 EXPLAIN SELECT * FROM employees.titles WHERE emp_no < '10010' AND title='Senior Engineer' AND from_date BETWEEN '1986-01-01' AND '1986-12-31'; +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 4 | NULL | 16 | Using where | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ 可以看到索引对第二个范围索引无能为力。这里特别要说明MySQL一个有意思的地方,那就是仅用explain可能无法区分范围索引和多值匹配,因为在type中这两者都显示为range。同时,用了“between”并不意味着就是范围查询,例如下面的查询: EXPLAIN SELECT * FROM employees.titles WHERE emp_no BETWEEN '10001' AND '10010' AND title='Senior Engineer' AND from_date BETWEEN '1986-01-01' AND '1986-12-31'; +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 59 | NULL | 16 | Using where | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ 看起来是用了两个范围查询,但作用于emp_no上的“BETWEEN”实际上相当于“IN”,也就是说emp_no实际是多值精确匹配。可以看到这个查询用到了索引全部三个列。因此在MySQL中要谨慎地区分多值匹配和范围匹配,否则会对MySQL的行为产生困惑。 情况七:查询条件中含有函数或表达式。 很不幸,如果查询条件中含有函数或表达式,则MySQL不会为这列使用索引(虽然某些在数学意义上可以使用)。例如: EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND left(title, 6)='Senior'; +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+ | 1 | SIMPLE | titles | ref | PRIMARY | PRIMARY | 4 | const | 1 | Using where | +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+ 虽然这个查询和情况五中功能相同,但是由于使用了函数left,则无法为title列应用索引,而情况五中用LIKE则可以。再如: EXPLAIN SELECT * FROM employees.titles WHERE emp_no - 1='10000'; +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+ | 1 | SIMPLE | titles | ALL | NULL | NULL | NULL | NULL | 443308 | Using where | +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+ 显然这个查询等价于查询emp_no为10001的函数,但是由于查询条件是一个表达式,MySQL无法为其使用索引。看来MySQL还没有智能到自动优化常量表达式的程度,因此在写查询语句时尽量避免表达式出现在查询中,而是先手工私下代数运算,转换为无表达式的查询语句。 索引选择性与前缀索引 既然索引可以加快查询速度,那么是不是只要是查询语句需要,就建上索引?答案是否定的。因为索引虽然加快了查询速度,但索引也是有代价的:索引文件本身要消耗存储空间,同时索引会加重插入、删除和修改记录时的负担,另外,MySQL在运行时也要消耗资源维护索引,因此索引并不是越多越好。一般两种情况下不建议建索引。 第一种情况是表记录比较少,例如一两千条甚至只有几百条记录的表,没必要建索引,让查询做全表扫描就好了。至于多少条记录才算多,这个个人有个人的看法,我个人的经验是以2000作为分界线,记录数不超过 2000可以考虑不建索引,超过2000条可以酌情考虑索引。 另一种不建议建索引的情况是索引的选择性较低。所谓索引的选择性(Selectivity),是指不重复的索引值(也叫基数,Cardinality)与表记录数(#T)的比值: Index Selectivity = Cardinality / #T 显然选择性的取值范围为(0, 1],选择性越高的索引价值越大,这是由B+Tree的性质决定的。例如,上文用到的employees.titles表,如果title字段经常被单独查询,是否需要建索引,我们看一下它的选择性: SELECT count(DISTINCT(title))/count(*) AS Selectivity FROM employees.titles; +-------------+ | Selectivity | +-------------+ | 0.0000 | +-------------+ title的选择性不足0.0001(精确值为0.00001579),所以实在没有什么必要为其单独建索引。 有一种与索引选择性有关的索引优化策略叫做前缀索引,就是用列的前缀代替整个列作为索引key,当前缀长度合适时,可以做到既使得前缀索引的选择性接近全列索引,同时因为索引key变短而减少了索引文件的大小和维护开销。下面以employees.employees表为例介绍前缀索引的选择和使用。 从图12可以看到employees表只有一个索引<emp_no>,那么如果我们想按名字搜索一个人,就只能全表扫描了: EXPLAIN SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido'; +----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+ | 1 | SIMPLE | employees | ALL | NULL | NULL | NULL | NULL | 300024 | Using where | +----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+ 如果频繁按名字搜索员工,这样显然效率很低,因此我们可以考虑建索引。有两种选择,建<first_name>或<first_name, last_name>,看下两个索引的选择性: SELECT count(DISTINCT(first_name))/count(*) AS Selectivity FROM employees.employees; +-------------+ | Selectivity | +-------------+ | 0.0042 | +-------------+ SELECT count(DISTINCT(concat(first_name, last_name)))/count(*) AS Selectivity FROM employees.employees; +-------------+ | Selectivity | +-------------+ | 0.9313 | +-------------+ <first_name>显然选择性太低,<first_name, last_name>选择性很好,但是first_name和last_name加起来长度为30,有没有兼顾长度和选择性的办法?可以考虑用first_name和last_name的前几个字符建立索引,例如<first_name, left(last_name, 3)>,看看其选择性: SELECT count(DISTINCT(concat(first_name, left(last_name, 3))))/count(*) AS Selectivity FROM employees.employees; +-------------+ | Selectivity | +-------------+ | 0.7879 | +-------------+ 选择性还不错,但离0.9313还是有点距离,那么把last_name前缀加到4: SELECT count(DISTINCT(concat(first_name, left(last_name, 4))))/count(*) AS Selectivity FROM employees.employees; +-------------+ | Selectivity | +-------------+ | 0.9007 | +-------------+ 这时选择性已经很理想了,而这个索引的长度只有18,比<first_name, last_name>短了接近一半,我们把这个前缀索引 建上: ALTER TABLE employees.employees ADD INDEX `first_name_last_name4` (first_name, last_name(4)); 此时再执行一遍按名字查询,比较分析一下与建索引前的结果: SHOW PROFILES; +----------+------------+---------------------------------------------------------------------------------+ | Query_ID | Duration | Query | +----------+------------+---------------------------------------------------------------------------------+ | 87 | 0.11941700 | SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido' | | 90 | 0.00092400 | SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido' | +----------+------------+---------------------------------------------------------------------------------+ 性能的提升是显著的,查询速度提高了120多倍。 前缀索引兼顾索引大小和查询速度,但是其缺点是不能用于ORDER BY和GROUP BY操作,也不能用于Covering index(即当索引本身包含查询所需全部数据时,不再访问数据文件本身)。 InnoDB的主键选择与插入优化 在使用InnoDB存储引擎时,如果没有特别的需要,请永远使用一个与业务无关的自增字段作为主键。 经常看到有帖子或博客讨论主键选择问题,有人建议使用业务无关的自增主键,有人觉得没有必要,完全可以使用如学号或身份证号这种唯一字段作为主键。不论支持哪种论点,大多数论据都是业务层面的。如果从数据库索引优化角度看,使用InnoDB引擎而不使用自增主键绝对是一个糟糕的主意。 上文讨论过InnoDB的索引实现,InnoDB使用聚集索引,数据记录本身被存于主索引(一颗B+Tree)的叶子节点上。这就要求同一个叶子节点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放,因此每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点和位置,如果页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)。 如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。如下图所示: 图13 这样就会形成一个紧凑的索引结构,近似顺序填满。由于每次插入时也不需要移动已有数据,因此效率很高,也不会增加很多开销在维护索引上。 如果使用非自增主键(如果身份证号或学号等),由于每次插入主键的值近似于随机,因此每次新纪录都要被插到现有索引页得中间某个位置: 图14 此时MySQL不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增加了很多开销,同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。 因此,只要可以,请尽量在InnoDB上采用自增字段做主键。 后记 这篇文章断断续续写了半个月,主要内容就是上面这些了。不可否认,这篇文章在一定程度上有纸上谈兵之嫌,因为我本人对MySQL的使用属于菜鸟级别,更没有太多数据库调优的经验,在这里大谈数据库索引调优有点大言不惭。就当是我个人的一篇学习笔记了。 其实数据库索引调优是一项技术活,不能仅仅靠理论,因为实际情况千变万化,而且MySQL本身存在很复杂的机制,如查询优化策略和各种引擎的实现差异等都会使情况变得更加复杂。但同时这些理论是索引调优的基础,只有在明白理论的基础上,才能对调优策略进行合理推断并了解其背后的机制,然后结合实践中不断的实验和摸索,从而真正达到高效使用MySQL索引的目的。 另外,MySQL索引及其优化涵盖范围非常广,本文只是涉及到其中一部分。如与排序(ORDER BY)相关的索引优化及覆盖索引(Covering index)的话题本文并未涉及,同时除B-Tree索引外MySQL还根据不同引擎支持的哈希索引、全文索引等等本文也并未涉及。如果有机会,希望再对本文未涉及的部分进行补充吧。 参考文献 [1] Baron Scbwartz等 著,王小东等 译;高性能MySQL(High Performance MySQL);电子工业出版社,2010 [2] Michael Kofler 著,杨晓云等 译;MySQL5权威指南(The Definitive Guide to MySQL5);人民邮电出版社,2006 [3] 姜承尧 著;MySQL技术内幕-InnoDB存储引擎;机械工业出版社,2011 [4] D Comer, Ubiquitous B-tree; ACM Computing Surveys (CSUR), 1979 [5] Codd, E. F. (1970). "A relational model of data for large shared data banks". Communications of the ACM, , Vol. 13, No. 6, pp. 377-387 [6] MySQL5.1参考手册 - http://dev.mysql.com/doc/refman/5.1/zh/index.html
概述 ASM是Java中比较流行的用来读写字节码的类库,用来基于字节码层面对代码进行分析和转换。在读写的过程中可以加入自定义的逻辑以增强或修改原来已编译好的字节码,比如CGLIB用它来实现动态代理。ASM被设计用于在运行时对Java类进行生成和转换,当然也包括离线处理。ASM短小精悍、且速度很快,从而避免在运行时动态生成字节码或转换时对程序速度的影响,又因为它体积小巧,可以在很多内存受限的环境中使用。ASM的主要优势包括如下几个方面:1. 它又一个很小,但设计良好并且模块化的API,且易于使用。2. 它具有很好的文档,并且还有eclipse插件。3. 它支持最新的Java版本。4. 它短小精悍、快速、健壮。5. 它又一个很大的用户社区,可以给新用户提供支持。6. 它的开源许可允许你几乎以任何方式来使用它。关于ASM的详细文档可以参考:ASM 3.0:Java字节码引擎库,写的很详细的一个文档。ASM Core设计一览 在ASM的核心实现中,它主要有以下几个类、接口(在org.objectweb.asm包中):ClassReader类:字节码的读取与分析引擎。它采用类似SAX的事件读取机制,每当有事件发生时,调用注册的ClassVisitor、AnnotationVisitor、FieldVisitor、MethodVisitor做相应的处理。ClassVisitor接口:定义在读取Class字节码时会触发的事件,如类头解析完成、注解解析、字段解析、方法解析等。AnnotationVisitor接口:定义在解析注解时会触发的事件,如解析到一个基本值类型的注解、enum值类型的注解、Array值类型的注解、注解值类型的注解等。FieldVisitor接口:定义在解析字段时触发的事件,如解析到字段上的注解、解析到字段相关的属性等。MethodVisitor接口:定义在解析方法时触发的事件,如方法上的注解、属性、代码等。ClassWriter类:它实现了ClassVisitor接口,用于拼接字节码。AnnotationWriter类:它实现了AnnotationVisitor接口,用于拼接注解相关字节码。FieldWriter类:它实现了FieldVisitor接口,用于拼接字段相关字节码。MethodWriter类:它实现了MethodVisitor接口,用于拼接方法相关字节码。SignatureReader类:对类定义、字段定义、方法定义、本地变量定义的签名的解析。Signature因范型引入,用于存储范型定义时的元数据(因为这些元数据在运行时会被擦除)。SignatureVisitor接口:定义在解析Signature时会触发的事件,如正常的Type参数、类或接口的边界等。SignatureWriter类:它实现了SignatureVisitor接口,用于拼接范型相关字节码。Attribute类:字节码中属性的类抽象。ByteVector类:字节码二进制存储的容器。Opcodes接口:字节码指令的一些常量定义。Type类:类型相关的常量定义以及一些基于其上的操作。他们之间的类图关系如下:ClassReader实现 ClassReader是ASM中最核心的实现,它用于读取并解析Class字节码。类字节码格式可以具体参考:《Java字节码格式详解1》、《Java字节码格式详解2》、《Java字节码格式详解3》在构建ClassReader实例时,它首先保存字节码二进制数组b,然后创建items数组,数组的长度在字节码数组的第8、9个字节指定(最前面4个字节是魔数CAFEBABE,之后2个字节是次版本号,再后2个字节是主版本号),每个item表示常量池项在字节码数组的偏移量加1(常量池中每个项由1个字节的type和紧跟的字节数组表示,常量池项有12种类型,其中CONSTANT_FieldRef_Info、CONSTANT_MethodRef_Info、CONSTANT_InterfaceMethodRef_Info、CONSTANT_NameAndType_Info包括其类型字节占用5个字节,另外4个字节每2个字节为字段、方法等所在的类、其名称、描述符在当前常量池中CONSTANT_Utf8_Info类型的引用;CONSTANT_Integer_Info、CONSTANT_Float_Info包括其类型字节占用5个字节,另外四个字节为其对应的值;CONSTANT_Class_Info、CONSTANT_String_Info包括其类型字节占用3个字节,另外两个字节为在当前常量池CONSTANT_Utf8_Info项的索引;CONSTANT_Utf8_Info类型第1个字节表示类型,第2、3个字节为该项所表示的字符串的长度);CONSTANT_Double_Info、CONSTANT_Long_Info加类型字节为9个字;maxStringLength表示最长的UTF8类型的常量池项的值,用于决定在解析CONSTANT_Utf8_Info类型项时最大需要的字符数组;header表示常量池之后的字节码的第一个字节。在调用ClassReader的accept方法时,它解析字节码中常量池之后的所有元素。紧接着常量池的2个字节是该类的access标签:ACC_PUBLIC、ACC_FINAL等;之后2个字节为当前类名在常量池CONSTANT_Utf8_Info类型的索引;之后2个字节为其父类名在常量池CONSTANT_Utf8_Info类型的索引(索引值0表示父类为null,即直接继承自Object类);再之后为其实现的接口数长度和对应各个接口名在常量池中CONSTANT_Utf8_Info类型的索引值;暂时先跳过Field和Method定义信息,解析类的attribute表,它用两个字节表达attribute数组的长度,每个attribute项中最前面2个字节是attribute名称:SourceFile(读取sourceFile值)、InnerClasses(暂时纪录起始索引)、EnclosingMethod(纪录当前匿名类、本地类包含者类名以及包含者的方法名和描述符)、Signature(类的签名信息,用于范型)、RuntimeVisibleAnnotations(暂时纪录起始索引)、Deprecated(表识属性)、Synthetic(标识属性)、SourceDebugExtension(为调试器提供的自定义扩展信息,读取成一个字符串)、RuntimeInvisibleAnnotations(暂时纪录起始索引),对其他不识别的属性,纪录成Attribute链,如果attribute名称符合在accept中attribute数组中指定的attribute名,则替换传入的attribute数组对应的项;根据解析出来的信息调用以下visit方法: void visit(int version, int access, String name, String signature, String superName, String[] interfaces);// sourceFile, sourceDebugvoid visitSource(String source, String debug);// EnclosingMethod attribute: enclosingOwner, enclosingName, enclosingDesc. // Note: only when the class has EnclosingMethod attribute, meaning the class is a local class or an anonymous classvoid visitOuterClass(String owner, String name, String desc); 依次解析RuntimeVisibleAnnotations和RuntimeInvisibleAnnotations属性,首先解析定义的Annotation的描述符以及运行时可见flag,返回用户自定义的AnnotationVisitor:AnnotationVisitor visitAnnotation(String desc, boolean visible); 对每个定义的Annotation,解析其键值对,并根据不同的Annotation字段值调用AnnotationVisitor中的方法,在所有解析结束后,调用AnnotationVisitor.visitEnd方法: public interface AnnotationVisitor { // 对基本类型的数组,依然采用该方法,visitArray只是在非基本类型时调用。 void visit(String name, Object value); void visitEnum(String name, String desc, String value); AnnotationVisitor visitAnnotation(String name, String desc); AnnotationVisitor visitArray(String name); void visitEnd(); } 之前解析出的attribute链表(非标准的Attribute定义),对每个Attribute实例,调用ClassVisitor中的visitAttribute方法: void visitAttribute(Attribute attr); Attribute类包含type字段和一个字节数组: public class Attribute { public final String type; byte[] value; Attribute next; } 对每个InnerClasses属性,解析并调用ClassVisitor的visitInnerClass方法(该属性事实上保存了所有其直接内部类以及它本身到最顶层类的路径): void visitInnerClass(String name, String outerName, String innerName, int access); 解析字段,它紧跟接口数组定义之后,最前面的2个字节为字段数组的长度,对每个字段,前面2个字节为访问flag定义,再后2个字节为Name索引,以及2个字节的描述符索引,然后解析其Attribute信息:ConstantValue、Signature、Deprecated、Synthetic、RuntimeVisibleAnnotations、RuntimeInvisibleAnnotations以及非标准定义的Attribute链,而后调用ClassVisitor的visitField方法,返回FieldVisitor实例: // 其中value为静态字段的初始化值(对非静态字段,它的初始化必须由构造函数实现),如果没有初始化值,该值为null。 FieldVisitor visitField(int access, String name, String desc, String signature, Object value); 对返回的FieldVisitor依次对其Annotation以及非标准Attribute解析,调用其visit方法,并在完成后调用它的visitEnd方法: public interface FieldVisitor { AnnotationVisitor visitAnnotation(String desc, boolean visible); void visitAttribute(Attribute attr); void visitEnd(); } 解析方法定义,它紧跟字段定义之后,最前面的2个字节为方法数组长度,对每个方法,前面2个字节为访问flag定义,再后2个字节为Name索引,以及2个字节的方法描述符索引,然后解析其Attribute信息:Code、Exceptions、Signature、Deprecated、RuntimeVisibleAnnotations、AnnotationDefault、Synthetic、RuntimeInvisibleAnnotations、RuntimeVisibleParameterAnnotations、RuntimeInvisibleParameterAnnotations以及非标准定义的Attribute链,如果存在Exceptions属性,解析其异常类数组,之后调用ClassVisitor的visitMethod方法,返回MethodVisitor实例:MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions); AnnotationDefault为对Annotation定义时指定默认值的解析;然后依次解析RuntimeVisibleAnnotations、RuntimeInvisibleAnnotations、RuntimeVisibleParameterAnnotations、RuntimeInvisibleParameterAnnotations等属性,调用相关AnnotationVisitor的visit方法;对非标准定义的Attribute链,依次调用MethodVisitor的visitAttribute方法: public interface MethodVisitor { AnnotationVisitor visitAnnotationDefault(); AnnotationVisitor visitAnnotation(String desc, boolean visible); AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible); void visitAttribute(Attribute attr); } 对Code属性解析,读取2个字节的最深栈大小、最大local变量数、code占用字节数,调用MethodVisitor的visitCode()方法表示开始解析Code属性,对每条指令,创建一个Label实例并构成Label数组,解析Code属性中的异常表,对每个异常项,调用visitTryCatchBlock方法: void visitTryCatchBlock(Label start, Label end, Label handler, String type); Label包含以下信息: /** * A label represents a position in the bytecode of a method. Labels are used * for jump, goto, and switch instructions, and for try catch blocks. * * @author Eric Bruneton */public class Label { public Object info; int status; int line; int position; private int referenceCount; private int[] srcAndRefPositions; int inputStackTop; int outputStackMax; Frame frame; Label successor; Edge successors; Label next; } 解析Code属性中的内部属性信息:LocalVariableTable、LocalVariableTypeTable、LineNumberTable、StackMapTable、StackMap以及非标准定义的Attribute链,对每个Label调用其visitLineNumber方法以及对每个Frame调用visitFrame方法,并且对相应的指令调用相应的方法: void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack); // Visits a zero operand instruction.void visitInsn(int opcode);// Visits an instruction with a single int operand.void visitIntInsn(int opcode, int operand);// Visits a local variable instruction. A local variable instruction is an instruction that loads or stores the value of a local variable.void visitVarInsn(int opcode, int var);// Visits a type instruction. A type instruction is an instruction that takes the internal name of a class as parameter.void visitTypeInsn(int opcode, String type);// Visits a field instruction. A field instruction is an instruction that loads or stores the value of a field of an object.void visitFieldInsn(int opcode, String owner, String name, String desc);// Visits a method instruction. A method instruction is an instruction that invokes a method.void visitMethodInsn(int opcode, String owner, String name, String desc);// Visits a jump instruction. A jump instruction is an instruction that may jump to another instruction.void visitJumpInsn(int opcode, Label label);// Visits a label. A label designates the instruction that will be visited just after it.void visitLabel(Label label);// Visits a LDC instruction.void visitLdcInsn(Object cst);// Visits an IINC instruction.void visitIincInsn(int var, int increment);// Visits a TABLESWITCH instruction.void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels);// Visits a LOOKUPSWITCH instruction.void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels);// Visits a MULTIANEWARRAY instruction.void visitMultiANewArrayInsn(String desc, int dims);// Visits a try catch block.void visitTryCatchBlock(Label start, Label end, Label handler, String type);void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index);// Visits a line number declaration.void visitLineNumber(int line, Label start);// Visits the maximum stack size and the maximum number of local variables of the method.void visitMaxs(int maxStack, int maxLocals); 最后调用ClassVisitor的visitEnd方法: void visitEnd(); ClassWriter实现 ClassWriter继承自ClassVisitor接口,可以使用它调用其相应的visit方法动态的构造一个字节码类。它包含以下字段信息: public class ClassWriter implements ClassVisitor { //The class reader from which this class writer was constructed, if any. ClassReader cr; //Minor and major version numbers of the class to be generated. int version; //Index of the next item to be added in the constant pool. int index; //The constant pool of this class. final ByteVector pool; //The constant pool's hash table data. Item[] items; //The threshold of the constant pool's hash table. int threshold; //A reusable key used to look for items in the {@link #items} hash table. final Item key; //A reusable key used to look for items in the {@link #items} hash table. final Item key2; //A reusable key used to look for items in the {@link #items} hash table. final Item key3; //A type table used to temporarily store internal names that will not necessarily be stored in the constant pool. Item[] typeTable; //Number of elements in the {@link #typeTable} array. private short typeCount; //The access flags of this class. private int access; //The constant pool item that contains the internal name of this class. private int name; //The internal name of this class. String thisName; //The constant pool item that contains the signature of this class. private int signature; //The constant pool item that contains the internal name of the super class of this class. private int superName; // Number of interfaces implemented or extended by this class or interface. private int interfaceCount; //The interfaces implemented or extended by this class or interface. private int[] interfaces; //The index of the constant pool item that contains the name of the source file from which this class was compiled. private int sourceFile; //The SourceDebug attribute of this class. private ByteVector sourceDebug; //The constant pool item that contains the name of the enclosing class of this class. private int enclosingMethodOwner; //The constant pool item that contains the name and descriptor of the enclosing method of this class. private int enclosingMethod; //The runtime visible annotations of this class. private AnnotationWriter anns; //The runtime invisible annotations of this class. private AnnotationWriter ianns; //The non standard attributes of this class. private Attribute attrs; //The number of entries in the InnerClasses attribute. private int innerClassesCount; //The InnerClasses attribute. private ByteVector innerClasses; //The fields of this class. These fields are stored in a linked list of {@link FieldWriter} objects, linked to each other by their {@link FieldWriter#next} field. This field stores the first element of this list. FieldWriter firstField; //This field stores the last element of this list. FieldWriter lastField; //The methods of this class. These methods are stored in a linked list of {@link MethodWriter} objects, linked to each other by their {@link MethodWriter#next} field. This field stores the first element of this list. MethodWriter firstMethod; //This field stores the last element of this list. MethodWriter lastMethod; //true if the maximum stack size and number of local variables must be automatically computed. private final boolean computeMaxs; //true if the stack map frames must be recomputed from scratch. private final boolean computeFrames; //true if the stack map tables of this class are invalid. boolean invalidFrames; }
源:http://www.cnblogs.com/eoiioe/archive/2008/09/20/1294681.html.tar 解包:tar xvf FileName.tar 打包:tar cvf FileName.tar DirName (注:tar是打包,不是压缩!) ——————————————— .gz 解压1:gunzip FileName.gz 解压2:gzip -d FileName.gz 压缩:gzip FileName .tar.gz 和 .tgz 解压:tar zxvf FileName.tar.gz 压缩:tar zcvf FileName.tar.gz DirName ——————————————— .bz2 解压1:bzip2 -d FileName.bz2 解压2:bunzip2 FileName.bz2 压缩: bzip2 -z FileName .tar.bz2 解压:tar jxvf FileName.tar.bz2 压缩:tar jcvf FileName.tar.bz2 DirName ——————————————— .bz 解压1:bzip2 -d FileName.bz 解压2:bunzip2 FileName.bz 压缩:未知 .tar.bz 解压:tar jxvf FileName.tar.bz 压缩:未知 ——————————————— .Z 解压:uncompress FileName.Z 压缩:compress FileName .tar.Z 解压:tar Zxvf FileName.tar.Z 压缩:tar Zcvf FileName.tar.Z DirName ——————————————— .zip 解压:unzip FileName.zip 压缩:zip FileName.zip DirName ——————————————— .rar 解压:rar x FileName.rar 压缩:rar a FileName.rar DirName ——————————————— .lha 解压:lha -e FileName.lha 压缩:lha -a FileName.lha FileName ——————————————— .rpm 解包:rpm2cpio FileName.rpm | cpio -div ——————————————— .deb 解包:ar p FileName.deb data.tar.gz | tar zxf - ——————————————— .tar .tgz .tar.gz .tar.Z .tar.bz .tar.bz2 .zip .cpio .rpm .deb .slp .arj .rar .ace .lha .lzh .lzx .lzs .arc .sda .sfx .lnx .zoo .cab .kar .cpt .pit .sit .sea 解压:sEx x FileName.* 压缩:sEx a FileName.* FileName sEx只是调用相关程序,本身并无压缩、解压功能,请注意! gzip 命令 减少文件大小有两个明显的好处,一是可以减少存储空间,二是通过网络传输文件时,可以减少传输的时间。gzip 是在 Linux 系统中经常使用的一个对文件进行压缩和解压缩的命令,既方便又好用。 语法:gzip [选项] 压缩(解压缩)的文件名该命令的各选项含义如下: -c 将输出写到标准输出上,并保留原有文件。-d 将压缩文件解压。-l 对每个压缩文件,显示下列字段: 压缩文件的大小;未压缩文件的大小;压缩比;未压缩文件的名字-r 递归式地查找指定目录并压缩其中的所有文件或者是解压缩。-t 测试,检查压缩文件是否完整。-v 对每一个压缩和解压的文件,显示文件名和压缩比。-num 用指定的数字 num 调整压缩的速度,-1 或 --fast 表示最快压缩方法(低压缩比),-9 或--best表示最慢压缩方法(高压缩比)。系统缺省值为 6。指令实例: gzip *% 把当前目录下的每个文件压缩成 .gz 文件。gzip -dv *% 把当前目录下每个压缩的文件解压,并列出详细的信息。gzip -l *% 详细显示例1中每个压缩的文件的信息,并不解压。gzip usr.tar% 压缩 tar 备份文件 usr.tar,此时压缩文件的扩展名为.tar.gz。
在Finder顶部显示完整路径,终端输入以下命令:defaults delete com.apple.finder _FXShowPosixPathInTitle;killall Finder 恢复默认设置,终端输入以下命令:defaults delete com.apple.finder _FXShowPosixPathInTitle;killall Finder 在Finder中的一些快捷键:Shift + Command + G:定位到指定目录 Shift + Command + A:定位到应用程序(Applications) Shift + Command + C:定位的计算机(Computer) Shift + Command + D:定位到桌面(Desktop) Shift + Command + I: 定位到 iDisk Shift + Command + K:定位到网络(Network) Shift + Command + T:添加当前目录到 Dock 最喜爱部分 Shift + Command + U:定位到实用工具(Utilities)
概述 Server类是Jetty中最核心的是类,它即包含Connectors数据,有包含了Handler的集合,即它是Jetty中用于连接Connector和Handler的类。同时它还包含了一个Container,用于存储Jetty中核心类实例的关系发生变化时触发事件的Listener,接收者可以注册一个Listener以获取Jetty中某个类的关系发生变化。Server的实现 Server继承自HandlerWrapper,因而它默认是一个Handler容器,另外它也包含一个Container字段,以及Connector数组字段,ThreadPool字段,另外它还包含了一些配置字段,如:attributes、sessionIdManager、sendServerVersion、sendDateHeader、stopAtShutdown(注册shutdown hook)、maxCookieVersion、dumpAfterStart、dumpBeforeStop等,这些只是用于配置信息,而且名称本身已经很清楚它的含义了。 在使用Server时,我们需要向其添加Connector以及Handler,从而在启动时,它会首先注册ShutdownThread,即在JVM退出时会首先调用它的stop方法;如果没有手动设置ThreadPool,使用QueuedThreadPool初始化ThreadPool字段;然后启动所有Handler以及Connector。在stop时,它先close所有Connector,然后设置所有实现了Graceful接口的类的shutdown为true,并等待graceful时间后,stop所有Connector以及Handler。 Server中有两个handle方法在HttpConnection请求解析完成后调用,其中handle方法用于在非ASYNC状态下的调用,它只是从HttpConnection中取得Request、Response实例以及PathInfo作为target,传递给Handler的handle方法;而handleAsync方法则是在ASYNC状态下调用,它的baseRequest从HttpConnection中获取,但是会将其原有的RequestURI、QueryString、ContextPath设置到其Attribute(javax.servlet.async.*)中,而将他们对应的属性包括PathInfo更新为当前AsyncContext中Request的值,并且request、response实例则是从AsyncContext中获取,然后调用Handler的handle方法。 Container的实现 Container用于生成父实例和子引用的关系发生变化时生成的时间,以提供其注册的Listener接收到相应的事件。Container通过内部接口Listener可以触发如下事件: public interface Listener extends EventListener { public void addBean(Object bean); public void removeBean(Object bean); public void add(Container.Relationship relationship); public void remove(Container.Relationship relationship); } 其中Relationship表示一个父实例和一个子引用的关系,如handler、threadPool、errorHandler等。每当一个关系发生变化时,可以调用Container的update方法,而update方法内部会触发相应的remove和add事件。
概述 ContextHandler继承自ScopedHandler,它是Jetty中实现对一个Web Application的各种资源进行管理,并串联实现整个Servlet框架的类,比如它部分实现了ServletContext接口,并且在其doScope方法中为当前Request的执行提供了相应的环境,如设置servletPath、pathInfo、设置ServletContext到ThreadLocal中。在Jetty中,Servlet的执行流程和框架由ServletHandler实现,Security框架由SecurityHandler完成,而ContextHandler主要用于实现环境的配置,如Request状态的设置、ClassLoader的配置、ServletContext的实现等。在ContextHandler类中包含了一个Context的内部类,用于实现ServletContext,而ContextHandler中的很多字段和方法也是用于实现ServletContext,不细述。ContextHandler实现 ContextHandler中doStart方法实现:1. 使用ContextPath或DisplayName初始化logger字段;并设置当前线程的ContextClassLoader为配置的ClassLoader实例;初始化mimeType字段;设置Context的ThreadLocal变量。2. 初始化managedAttributes Map,并生成addBean事件;如果存在ErrorHandler,start它;生成contextInitialized事件。3. 初始化availability字段。4. 还原Context的ThreadLocal变量和ContextClassLoader回原有实例。ContextHandler中doStop方法实现:1. 设置availability字段为STOPPED状态;初始化Context的ThreadLocal变量和ContextClassLoader为当前Context实例以及设置的ClassLoader。2. 生成contextDestroyed事件,以及对managedAttributes,触发removeBean事件。3. 还原Context的ThreadLocal变量和ContextClassLoader回原有实例。ContextHandler中doScope方法实现:1. 对REQUEST、ASYNC的DispatcherType,并且是第一次进入该ContextHandler,则如果compactPath为true,compact传入的path,即把"//", "///"等替换为"/"。2. 对当前请求做初步检查以决定是否需要继续执行该请求(返回false表示不需要继续执行该请求): a. 检查availability状态,对UNAVAILABLE,发送503 Service Unavailable响应;对STOPPED、SHUTDOWN状态,或Request的handled字段为true,返回false。 b. 对设置了vhosts,检查请求消息中的ServerName请求头是否和vhosts中的某个vhost相同或比配,如果不成立,则返回false。 c. 检查设置的connectors数组,如果当前HttpConnection中的Connector.name不包含在这个设置的connectors数组中,返回false。 d. 检查contextPath,如果target不以这个contextPath开头或者在target中contextPath之后的字符不是"/",返回false;如果allowNullPathInfo设置为false,且target不以"/"结尾,发送"target + /"的重定向请求,返回false。 e. 对其他情况,返回true,表示请求可以继续处理。3. 计算pathInfo以及target为contextPath之后的路径,并设置ContextClassLoader为当前设置的ClassLoader。4. 保留当前Request的contextPath、servletPath、pathInfo信息。5. 对任意非INCLUDE的DispatcherType,设置Request的contextPath、servletPath为null、pathInfo为传入的target中contextPath之后的路径。6. 执行nextScope的逻辑。7. 还原当前Request的contextPath、servletPath、pathInfo的信息。ContextHandler中doHandle方法实现:1. 对有新更新Context的Request实例,向当前Request添加注册的ServletRequestAttributeListener,如果注册了ServletRequestListener,生成requestInitialized事件。2. 对REQUEST类型的DispatcherType,如果该target为保护资源(isProctedTarget,如WEB-INF、META-INF目录下的文件),抛出404 Not Found的HttpException。3. 执行nextHandle()逻辑。4. 如果注册了ServletRequestListener,生成requestDestroyed事件,并从Request中移除当前ContextHandler中添加的ServletRequestAttributeListener实例。ServletContextHandler实现 ServletContextHandler继承自ContextHandler类,它串连了SessionHandler、SecurityHandler和ServletHandler,在ServletContextHandler的start过程中,它会串连如下Handler:ServletContextHandler -....->SessionHandler->SecurityHandler->ServletHandler,由于ServletContextHandler、SessionHandler、ServletHandler都继承自ScopedHandler,因而他们的执行栈将会是:|->ServletContextHandler.doScope() |-> SessionHandler.doScope() |-> ServletHandler.doScope() |-> ServletContextHandler.doHandle() |-> .....handler.handle() |-> SessionHandler.doHandle() |-> SecurityHandler.handle() |-> ServletHandler.doHandle()另外ServletContextHandler还提供了一个Decorator的扩展点,可以向ServletContextHandler注册多个Decorator,在ServletContextHandler启动时,它会对每个已注册的ServletHolder和FilterHolder执行一些额外的“装饰”逻辑,出了对ServletHolder和FilterHolder的装饰,它还可以装饰Filter、Servlet、Listener等,以及在销毁他们时加入一下自定义的逻辑: public interface Decorator { <T extends Filter> T decorateFilterInstance(T filter) throws ServletException; <T extends Servlet> T decorateServletInstance(T servlet) throws ServletException; <T extends EventListener> T decorateListenerInstance(T listener) throws ServletException; void decorateFilterHolder(FilterHolder filter) throws ServletException; void decorateServletHolder(ServletHolder servlet) throws ServletException; void destroyServletInstance(Servlet s); void destroyFilterInstance(Filter f); void destroyListenerInstance(EventListener f); } Decorator扩展点的引入实现了两种方式对Servlet、Filter、EventListener的配置:Annotation方式(AnnotationDecorator)和Plus方式(PlusDecorator),其中Annotation的方式的配置是Servlet 3.0规范中新加入的特性,而Plus方式则是Jetty提供的配置注入。其中AnnotationDecorator的实现采用AnnotationInstrospector,可以向它注册不同的InstrospectableAnnotationHandler,用以处理不同的Annotation逻辑,从而实现对动态注册的Servlet、Filter、EventListener,可以使用在它们之上的Annotation来做进一步的配置,以简化配置本身。在Jetty中实现了以下几个Annotation的InstrospectableAnnotationHandler:@Resource => ResourceAnnotationHandler: 对类上的@Resource注解,将它作为一种资源绑定到当前Context或Server中,对Field或Method的@Resource注解,创建一个Injection实例放入Context的Attribute中。在PlusDecorator中会对注册的Injection实例做inject操作。@Resources => ResourcesAnnotationHandler: 对类上的@Resources注解中的每个@Resource注解作为一种资源绑定到当前Context或Server中。@RunAs => RunAsAnnotationHandler: 关联Servlet上@RunAs注解的值到该ServletHolder中。@ServletSecurity => SecurityAnnotationHandler: 为@ServletSecurity注解的Servlet配置DataConstraint、Roles、methodOmission等。@PostConstruct => PostConstructAnnotationHandler: 将有该注解的方法注册PostConstructCallback回调类,在PlusDecorator中的decorate方法中会调用该callback。@PreDestroy => PreDestroyAnnotationHandler: 将有该注解的方法注册PreDestroyCallback回调类,在PlusDecorator中的decorate方法中会调用该callback。@MultipartConfig => MultipartConfigAnnotationHandler: 将有该注解的Servlet类注册配置的MultipartConfig信息。@DeclareRoles => DeclareRolesAnnotationHandler: 向SecurityHandler注册定义的Role集合。而PlusDecorator主要处理使用以上Annotation或PlusDescriptorProcessor注册的RunAsCollection、InjectionCollection、LifeCycleCallbackCollection的逻辑实现。其中RunAsCollection用于向注册的对应的ServletHolder注册RunAsRole信息;InjectionCollection实现从JNDI中查找对应JndiName的实例,并将其设置到Injection中指定的字段或方法中;LifeCycleCallbackCollection用于实现在Servlet、Filter、EventListener创建后或销毁前调用相应的有@PostConstruct注解或@PreDestroy注解的方法。WebAppContext实现 WebAppContext继承自ServletContextHandler,主要用于整合对ServletContextHandler的配置、配置WebAppClassLoader、设置war包路径、设置contextWhiteList、保存MetaData信息等。对WebAppContext的配置,Jetty使用Configuration接口类抽象这个行为,其接口定义如下(方法名称都比较直观): public interface Configuration { public void preConfigure (WebAppContext context) throws Exception; public void configure (WebAppContext context) throws Exception; public void postConfigure (WebAppContext context) throws Exception; public void deconfigure (WebAppContext context) throws Exception; public void destroy (WebAppContext context) throws Exception; public void cloneConfigure (WebAppContext template, WebAppContext context) throws Exception; } 可以使用setConfigurationClasses或setConfigurations方法自定义当前支持的Configuration集合,Jetty默认添加集合有:WebInfConfiguration、WebXmlConfiguration、MetaInfConfiguration、FragmentConfiguration、JettyWebXmlConfiguration,另外Jetty内部默认实现的还有:AnnotationConfiguration、ContainerInitializerConfiguration、EnvConfiguration、PlusConfiguration、TagLigConfiguration等。在WebInfConfiguration实现中,在其preConfigure方法中,如果存在WEB-INF/work目录,先在该目录下创建一个名为Jetty_<host>_<port>__<resourceBase>_<contextPath>_<virtualhost><base36_hashcode_of_whole_string>的临时目录,然后设置WebAppContext的临时目录:1. 可以手动设置。2. 可以使用javax.servlet.context.tempdir属性值设置。3. 可以设置为${jetty.home}/work/Jetty_<host>_<port>__<resourceBase>_<contextPath>_<virtualhost><base36_hashcode_of_whole_string>4. 可以使用属性org.eclipse.jetty.we pasting bapp.basetempdir指定的base,然后设置为${base}/Jetty_<host>_<port>__<resourceBase>_<contextPath>_<virtualhost><base36_hashcode_of_whole_string>5. 可以设置为${java.io.tmpdir}/Jetty_<host>_<port>__<resourceBase>_<contextPath>_<virtualhost><base36_hashcode_of_whole_string>6. 所有以上设置失败,则使用File.createTempFile("JettyContext", "")的目录来设置。对于war包,如果配置了extractWAR为true,则将war包解压到war包所在目录的war包名的目录或<tempDir>/webapp目录下,如果配置了copyWebDir,则将原本配置的BaseResource下的所有内容拷贝到<tempDir>/webapp目录下,使用新的web_app目录设置BaseResource目录;如果配置了copyWebInf为true,则将WEB-INF/lib, WEB-INF/classes的两个目录拷贝到<tempDir>/webinf/lib, <tempDir>/webinf/classes目录下,并更新BaseResource为原来webapp目录和<tempDir>/webinf两个目录的组合;设置org.eclipse.jetty.server.webapp.ContainerIncludeJarParttern属性,查找URLClassLoader中URL中对应的jar包(即WebAppContext中配置的extraClassPath值),并添加到MetaData的containerJars集合中(如果不设置,则不会添加任何jar包);使用org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern属性匹配WEB-INF/lib目录下的所有jar包,并添加到MetaData的webInfJars集合中(如果不设置,默认添加所有jar包)。在其configure()方法实现中,设置WebAppClassLoader的classPath为WEB-INF/classes, WEB-INF/lib目录下的所有jar包,并将这些jar包添加到WebAppClassLoader中;并且如果配置了org.eclipse.jetty.resources属性,则将配置的List<Resource>集合添加到WebAppContext的BaseResource中。WebXmlConfiguration的实现,在preConfigure中,它向MetaData注册webdefault.xml文件描述符;web.xml(默认为WEB-INF/web.xml)文件描述符;以及override-web.xml文件描述符;在注册过程中解析它们的absolute-ordering信息,将解析的结果合并到MetaData的ordering集合中。在configure方法实现中,它向MetaData添加StandardDescriptorProcessor。MetaInfConfiguration的实现,在preConfigure()方法中,扫描MetaData中在WebInfConfiguration中注册的所有containerJars和webInfJars 的jar包,将找到的META-INF/web-fragment.xml生成的Resource注册到org.eclipse.jetty.webFragments属性中,在FragmentConfiguration中会被进一步添加到MetaData中;将META-INF/resources/对应的Resource注册到org.eclipse.jetty.resources属性中,在WebInfConfiguration的configure方法中会将这些目录添加到BaseResource集合中;将所有*.tld文件对应的Resource注册到org.eclipse.jetty.tlds属性中,在TagLibConfiguration中,会对这些注册TLD文件做进一步的处理。FragmentConfiguration的实现,在其preConfigure方法中,将MetaInfConfiguration中找到的web-fragment.xml文件对应的Resource注册到MetaData中,在注册中首先解析它的ordering信息;在其configure方法中,它使用ordering中定义的顺序逻辑对注册的jar包进行排序。JettyWebConfiguration的实现,在其configure方法中,依次查找jetty8-web.xml, jetty-web.xml, web-jetty.xml文件,如果有找到任意一个,则使用XmlCofiguration对WebAppContext进行配置。XmlConfiguration的实现参考《深入Jetty源码之XmlConfiguration实现》在AnnotationConfiguration的实现中,在其configure()方法中,它首先向WebAppContext中注册AnnotationDecorator;然后它创建AnnotationParser实例,然后向其注册WebServletAnnotationHandler、WebFilterAnnotationHandler、WebListenerAnnotationHandler、ClassInheritanceHandler、ContainerInitializerAnnotationHandler,它们都实现了DiscoverableAnnotationHandler(其中ClassInheritanceHandler实现的是ClassHandler接口);最后扫瞄所有ClassPath下jar包、WEB-INF/classes以及WEB-INF/lib中的jar包下的每个类,对于所有注册为systemClasses,非serverClasses的类,使用ClassInheritanceHandler纪录所有类包含的直接子类以及所有接口包含的直接实现类,而WebFilterAnnotationHandler、WebServletAnnotationHandler、WebListenerAnnotationHandler用于注册相应的WebFilterAnnotation、WebServletAnnotation、WebListenerAnnotation,并添加到MetaData中DiscoveredAnnotation集合中,这些DiscoveredAnnotation在MetaData的resolve方法(WebAppContext.startContext()方法中被调用)调用时会向WebAppContext注册对应的FilterHolder、ServletHolder、EventListener,而ContainerInitializerAnnotationHandler则会将所有注册的注解修饰的类添加到注册的ContainerInitializer的annotatedTypeNames集合中,该集合在ContainerInitializerConfiguration将它自身以及它的所有子类、实现类添加到applicableTypeNames集合中,集合之前注册的interestedTypes的所有子类、实现类传递到ServletContainerInitializer的onStartup()方法中。在ContainerInitializerConfiguration会使用AnnotationConfiguration中注册ContainerInitializer实例列表,构建applicableTypeNames,并调用其ServletContainerInitializer的onStartup方法。EnvConfiguration实现,在preConfigure方法中使用XmlConfiguration以及WEB-INF/jetty-env.xml文件对WebAppContext进行配置,并且绑定JNDI环境。TagLibConfiguration实现,在preConfigure方法中向WebAppContext注册TagLibListener(ServletContextListener),在TagLibListener的contextInitialized方法中,它首先查找所有能找到的web.xml中定义的*.tld文件、WEB-INF/*.tld文件、WEB-INF/tlds/*.tld文件、以及通过WebInfConfiguration在jar包中找到的*.tld文件,将每个tld文件解析成一个TldDescriptor,并且使用TldProcessor对它们进行解析成EventListener列表,并注册到WebAppContext中。PlusConfiguration实现,在preConfigure中,它向WebAppContext添加PlusDecorator;在configure方法中添加PlusDescriptorProcessor。 在WebAppContex启动时: 1. 根据WebAppContext的allowDuplicateFragmentNames属性设置MetaData实例对应的属性。 2. 调用preConfigure方法,它加载所有Configuration实例(用户自定义或默认设置:WebInfConfiguration、WebXmlConfiguration、MetaInfConfiguration、FragmentConfiguration、JettyWebXmlConfiguration);加载系统类规则集合(即不能被Web Application覆盖的类,他们必须使用System ClassLoader加载,可以通过Server属性org.eclipse.jetty.webapp.sysemClasses属性定义,或者使用默认值)以及Server类规则集合(不能被Web Application加载的类,他们需要使用System ClassLoader加载,可以使用Server属性org.eclipse.jetty.webapp.serverClasses定义,或者使用默认值,这两个的区别参考WebAppClassLoader的实现解析);设置ClassLoader,默认为WebAppClassLoader;调用所有Configuration的preConfigure方法。 3. 调用startContext方法,他会调用Configuration的configure方法,以及MetaData的resolve方法;在MetaData的resolve方法中,他首先设置WebAppContext的javax.servlet.context.orderedLibs属性,然后设置ServletContext的EffectiveMajorVersion和EffectiveMinorVersion,并遍历通过Configuration注册的DescriptorProcessor,对webDefaultRoots、webXmlRoot、webOverrideRoots等Descriptor进行处理,以读取Servlet、Filter、Listener等信息的定义,遍历在Configuration中注册的DiscoveredAnnotation,对所有找到的WebFilter、WebServlet、WebListener注解类进行解析并添加到WeAppContext中,最后对在FragmentConfiguration中注册的FragmentDescriptor以及DiscoveredAnnotation进行相应的处理已进一步配置WebAppContext。 4. 调用postConfiguration方法,即调用所有注册的Configuration的postConfigure方法以做一些清理工作。 WebAppClassLoader实现 WebAppClassLoader是Jetty中用于对Servlet规范的ClassLoader的实现,它集成子URLClassLoader。它不会加载任何System Class(使用System ClassLoader加载),对Java2中父ClassLoader优先于子ClassLoader的规则,可以使用WebAppContext的setParentLoadPriority为true来配置。如果没有配置父ClassLoader,则使用当前的Thread Context ClassLoader,如果该ClassLoader也为null,则使用加载该类的ClassLoader,如果它还为null,则使用SystemClassLoader作为其父ClassLoader。 在加载类时,WebAppClassLoader有SystemClass和ServerClass的类别,SystemClass是指在Web Application中可见的,但是他们不能被Web Application中的类(WEB-INF/classes,WEB-INF/lib中的类)覆盖的类,而ServerClass是指这些类是Server部分的实现,它对Web Application是不可见的,如果需要使用它们,可以将相应的jar包添加到WEB-INF/lib中。 WebAppClassLoader默认支持.zip,.jar为扩展名的文件中查找class定义,可以使用org.eclipse.jetty.webapp.WebAppClassLoader.extensions系统属性添加更多的扩展名文件支持(以“,”或“;”分隔)。WebAppClassLoader也会添加WebAppContext中的ExtraClassPath到其ClassPath中(以“,”或“;”分隔),即添加URL。 在WebInfConfiguration的configure方法中,他会默认的将所有WEB-INF/lib下的jar包以及WEB-INF/classes目录添加到WebAppClassLoader的ClassPath中,即添加URL。 在其loadClass的实现中,如果某class既是SystemClass又是ServerClass,则返回null;如果不是ServerClass,且是父ClassLoader优先或者是SystemClass,则使用父ClassLoader加载,然后再使用当前ClassLoader加载;在getResources和getResource的实现中,对于ServerClass它只能从当前ClassLoader中查找,对SystemClass它只能从父ClassLoader中查找。
概述 XmlConfiguration是Jetty中用来实现使用XML文件配置WebAppContext实例的框架,事实上,他可以用来配置任何的Object。类似Digester可以将一个XML配置文件直接实例化成一个类实例,XmlConfiguration可以使用一个XML文件初始化一个Object实例,它支持属性设置、方法调用、新建新实例等。 XmlConfiguration实现 在XmlConfiguration的实现中,可以使用URL、configuration字符串、InputStream作为构造函数传入XmlConfiguration中,在XmlConfiguration的构造函数初始化时,它初始化XmlParser实例,使用XmlParser对XML文件进行解析成一个Node树,并将该树的根节点赋值给config字段;在初始化config字段的同时初始化processor字段,processor字段是ConfigurationProcessor类型,他用于解析XmlParser解析出的Node树的处理类,如果根节点的Tag是Configure,则processor使用Jetty中的默认实现:JettyXmlConfiguration类,否则使用ServiceLoader查找到的ConfigurationProcessorFactory实例,并调用其getConfigurationProcessor方法,传入dtd和tag作为参数,直到找到一个非null的ConfigurationProcessor;在创建出ConfigurationProcessor实例后,首先调用其init方法,在JettyXmlConfiguration类的实现中,init方法只是将传入的参数保存:Node树的config实例、以及两个idMap、propertyMap实例用于传递参数,其中idMap用于保存解析过程中所有id属性对应的实例,也可以用它来传递XML文件用于配置的实例以及其他ref引用的实例,而propertyMap用于定义一些XML文件中用到的属性对应的值。 可以使用带Object实例的参数或不带参数两种方式调用configure方法,带参数表示将XML中内容配置实例中的属性,不带参数则先在内部创建class属性指定的类实例,然后使用XML的内容配置该新创建的实例的属性,如果根元素定义了id属性,则可以设置要配置的实例是idMap中该id值对应的实例。 在configure方法的真正实现中,它遍历根节点的所有子节点,判断tag值(在这个过程中对任何有id定义的元素,将该id属性的值做为key,该元素对应的实例保存在idMap中):Set => 用于设置name属性指定的set方法或字段的值,在查找方法中会有各种尝试:1. 尝试当前值实例作为参数的set方法;2. 使用Class实例的TYPE字段作为参数类型的set方法;3. 尝试查找public类型的字段;4. 尝试所有只有一个参数的对应的set方法;5. 尝试是否可以将其转换成Set或数组类型的参数;6. 尝试装箱后的参数。 Put => 要使用该方法,配置类型必须继承自Map接口,使用name属性作为key,获取其值实例,调用put方法。 Call => 指定class属性表示调用其静态方法;读取所有的Arg子节点(Arg节点必须紧跟Call节点),并且解析这些Arg的值类型,name属性指定方法名,在返回后如果其他节点定义,则使用它们配置返回的实例。 Get => 使用name属性指定的get无参数方法,如果没有找到get方法,则从name属性指定的字段中获取值,如果还有子元素,则对返回的实例进行配置。 New => 创建一个class指定的类实例,可以使用Arg指定构造函数参数。如果有除了Arg以外的节点定义,则使用它们对新创建的实例配置。 Array => 创建数组实例,type指定数组类型,支持以下列举的所有的值类型,使用Item指定它的元素。 Ref => 为传入的ref引用实例配置。 Property => 从传入的propertyMap中获取值,可以指定default属性。可以通过定义子元素继续配置返回的值。Map => 创建新的Map实例,使用Entry/Item, Item指定key、value的值。对于所有值类型,可以指定其type属性,Jetty默认支持的类型有String、java.lang.String、URL、java.net.URL、InetAddress、java.net.InetAddress、boolean、byte、char、double、float、int、long、short、void、java.lang.Boolean.TYPE、java.lang.Byte.TYPE、java.lang.Character.TYPE、java.lang.Double.TYPE、java.lang.Float.TYPE、java.lang.Integer.TYPE、java.lang.Long.TYPE、java.lang.Short.TYPE、java.lang.Void.TPPE、Boolean、Byte、Character、Double、Float、Integer、Long、Short、null、string等。另外值类型还可以使用Call、Get、New、Ref、Array、Map、Property、SystemProperty定义。其中SystemProperty可以定义name和default属性用于表示prop名和default的值。ref引用必须先定义,因而这里无法处理循环引用的问题。 使用 可以使用XmlConfiguration,给定一个或多个Properties、XML文件参数,在Command Line中直接启动Jetty服务器。 XmlConfiguration也可以用于EnvConfiguration中,用于进一步配置WebAppContext,可以手动设置jettyEnvXmlUrl属性,或默认使用WEB-INF/jetty-env.xml文件。 XmlConfiguration还用于JettyWebXmlConfiguration,他默认查找WEB-INF目录下的jetty8-web.xml、jetty-web.xml、web-jetty.xml作为配置文件对WebAppContext做进一步的配置。附录XmlConfiguration支持的XML文件格式使用configure_6_0.dtd文件定义: <?xml version="1.0" encoding="ISO-8859-1"?><!-- This is the document type descriptor for the org.eclipse.XmlConfiguration class. It allows a java object to be configured by with a sequence of Set, Put and Call elements. These tags are mapped to methods on the object to be configured as follows: <Set name="Test">value</Set> == obj.setTest("value"); <Put name="Test">value</Put> == obj.put("Test","value"); <Call name="test"><Arg>value</Arg></Call> == obj.test("value"); Values themselves may be configured objects that are created with the <New> tag or returned from a <Call> tag. Values are matched to arguments on a best effort approach, but types my be specified if a match is not achieved.--><!ENTITY % CONFIG "Set|Get|Put|Call|New|Ref|Array|Map|Property"><!ENTITY % VALUE "#PCDATA|Get|Call|New|Ref|Array|Map|SystemProperty|Property"><!ENTITY % TYPEATTR "type CDATA #IMPLIED " > <!-- String|Character|Short|Byte|Integer|Long|Boolean|Float|Double|char|short|byte|int|long|boolean|float|double|URL|InetAddress|InetAddrPort| #classname --><!ENTITY % IMPLIEDCLASSATTR "class NMTOKEN #IMPLIED" ><!ENTITY % CLASSATTR "class NMTOKEN #REQUIRED" ><!ENTITY % NAMEATTR "name NMTOKEN #REQUIRED" ><!ENTITY % IMPLIEDNAMEATTR "name NMTOKEN #IMPLIED" ><!ENTITY % DEFAULTATTR "default CDATA #IMPLIED" ><!ENTITY % IDATTR "id NMTOKEN #IMPLIED" ><!ENTITY % REQUIREDIDATTR "id NMTOKEN #REQUIRED" ><!-- Configure Element. This is the root element that specifies the class of object that can be configured: <Configure class="com.acme.MyClass"> </Configure>--><!ELEMENT Configure (%CONFIG;)* ><!ATTLIST Configure %IMPLIEDCLASSATTR; %IDATTR; ><!-- Set Element. This element maps to a call to a setter method or field on the current object. The name and optional type attributes are used to select the setter method. If the name given is xxx, then a setXxx method is used, or the xxx field is used of setXxx cannot be found. A Set element can contain value text and/or the value objects returned by other elements such as Call, New, SystemProperty, etc. If no value type is specified, then white space is trimmed out of the value. If it contains multiple value elements they are added as strings before being converted to any specified type. A Set with a class attribute is treated as a static set method invocation.--><!ELEMENT Set ( %VALUE; )* ><!ATTLIST Set %NAMEATTR; %TYPEATTR; %IMPLIEDCLASSATTR; ><!-- Get Element. This element maps to a call to a getter method or field on the current object. The name attribute is used to select the get method. If the name given is xxx, then a getXxx method is used, or the xxx field is used if getXxx cannot be found. A Get element can contain other elements such as Set, Put, Call, etc. which act on the object returned by the get call. A Get with a class attribute is treated as a static get method or field.--><!ELEMENT Get (%CONFIG;)*><!ATTLIST Get %NAMEATTR; %IMPLIEDCLASSATTR; %IDATTR; ><!-- Put Element. This element maps to a call to a put method on the current object, which must implement the Map interface. The name attribute is used as the put key and the optional type attribute can force the type of the value. A Put element can contain value text and/or value elements such as Call, New, SystemProperty, etc. If no value type is specified, then white space is trimmed out of the value. If it contains multiple value elements they are added as strings before being converted to any specified type.--><!ELEMENT Put ( %VALUE; )* ><!ATTLIST Put %NAMEATTR; %TYPEATTR; ><!-- Call Element. This element maps to an arbitrary call to a method on the current object, The name attribute and Arg elements are used to select the method. A Call element can contain a sequence of Arg elements followed by a sequence of other elements such as Set, Put, Call, etc. which act on any object returned by the original call: <Call id="o2" name="test"> <Arg>value1</Arg> <Set name="Test">Value2</Set> </Call> This is equivalent to: Object o2 = o1.test("value1"); o2.setTest("value2"); A Call with a class attribute is treated as a static call.--><!ELEMENT Call (Arg*,(%CONFIG;)*)><!ATTLIST Call %NAMEATTR; %IMPLIEDCLASSATTR; %IDATTR;><!-- Arg Element. This element defines a positional argument for the Call element. The optional type attribute can force the type of the value. An Arg element can contain value text and/or value elements such as Call, New, SystemProperty, etc. If no value type is specified, then white space is trimmed out of the value. If it contains multiple value elements they are added as strings before being converted to any specified type.--><!ELEMENT Arg ( %VALUE; )* ><!ATTLIST Arg %TYPEATTR; %IMPLIEDNAMEATTR; ><!-- New Element. This element allows the creation of a new object as part of a value for elements such as Set, Put, Arg, etc. The class attribute determines the type of the new object and the contained Arg elements are used to select the constructor for the new object. A New element can contain a sequence of Arg elements followed by a sequence of elements such as Set, Put, Call, etc. elements which act on the new object: <New id="o" class="com.acme.MyClass"> <Arg>value1</Arg> <Set name="test">Value2</Set> </New> This is equivalent to: Object o = new com.acme.MyClass("value1"); o.setTest("Value2");--><!ELEMENT New (Arg*,(%CONFIG;)*)><!ATTLIST New %CLASSATTR; %IDATTR;><!-- Ref Element. This element allows a previously created object to be referenced by id. A Ref element can contain a sequence of elements such as Set, Put, Call, etc. which act on the referenced object: <Ref id="myobject"> <Set name="Test">Value2</Set> </New>--><!ELEMENT Ref ((%CONFIG;)*)><!ATTLIST Ref %REQUIREDIDATTR;><!-- Array Element. This element allows the creation of a new array as part of a value of elements such as Set, Put, Arg, etc. The type attribute determines the type of the new array and the contained Item elements are used for each element of the array: <Array type="java.lang.String"> <Item>value0</Item> <Item><New class="java.lang.String"><Arg>value1</Arg></New></Item> </Array> This is equivalent to: String[] a = new String[] { "value0", new String("value1") };--><!ELEMENT Array (Item*)><!ATTLIST Array %TYPEATTR; %IDATTR; ><!-- Map Element. This element allows the creation of a new map as part of a value of elements such as Set, Put, Arg, etc. The type attribute determines the type of the new array and the contained Item elements are used for each element of the array: <Map> <Entry> <Item>keyName</Item> <Item><New class="java.lang.String"><Arg>value1</Arg></New></Item> </Entry> </Map> This is equivalent to: Map m = new HashMap(); m.put("keyName", new String("value1"));--><!ELEMENT Map (Entry*)><!ATTLIST Map %IDATTR; ><!ELEMENT Entry (Item,Item)><!-- Item Element. This element defines an entry for the Array or Map Entry elements. The optional type attribute can force the type of the value. An Item element can contain value text and/or the value object of elements such as Call, New, SystemProperty, etc. If no value type is specified, then white space is trimmed out of the value. If it contains multiple value elements they are added as strings before being converted to any specified type.--><!ELEMENT Item ( %VALUE; )* ><!ATTLIST Item %TYPEATTR; %IDATTR; ><!-- System Property Element. This element allows JVM System properties to be retrieved as part of the value of elements such as Set, Put, Arg, etc. The name attribute specifies the property name and the optional default argument provides a default value. <SystemProperty name="Test" default="value" /> This is equivalent to: System.getProperty("Test","value");--><!ELEMENT SystemProperty EMPTY><!ATTLIST SystemProperty %NAMEATTR; %DEFAULTATTR; %IDATTR;><!-- Property Element. This element allows arbitrary properties to be retrieved by name. The name attribute specifies the property name and the optional default argument provides a default value. A Property element can contain a sequence of elements such as Set, Put, Call, etc. which act on the retrieved object: <Property name="Server"> <Call id="jdbcIdMgr" name="getAttribute"> <Arg>jdbcIdMgr</Arg> </Call> </Property>--><!ELEMENT Property ((%CONFIG;)*)><!ATTLIST Property %NAMEATTR; %DEFAULTATTR; %IDATTR;> 一个简单的例子(摘自:http://www.blogjava.net/xylz/archive/2012/04/12/372999.html): <Configure id="Server" class="org.eclipse.jetty.server.Server"> <Set name="ThreadPool"> <New class="org.eclipse.jetty.util.thread.QueuedThreadPool"> <Set name="minThreads">10</Set> <Set name="maxThreads">200</Set> <Set name="detailedDump">false</Set> </New> </Set> <Call name="addConnector"> <Arg> <New class="org.eclipse.jetty.server.nio.SelectChannelConnector"> <Set name="host"><Property name="Inside in Jetty.host" /></Set> <Set name="port"><Property name="Inside in Jetty.port" default="8080"/></Set> <Set name="maxIdleTime">300000</Set> <Set name="Acceptors">2</Set> <Set name="statsOn">false</Set> <Set name="confidentialPort">8443</Set> <Set name="lowResourcesConnections">20000</Set> <Set name="lowResourcesMaxIdleTime">5000</Set> </New> </Arg> </Call> <Set name="handler"> <New id="Handlers" class="org.eclipse.jetty.server.handler.HandlerCollection"> <Set name="handlers"> <Array type="org.eclipse.jetty.server.Handler"> <Item> <New id="Contexts" class="org.eclipse.jetty.server.handler.ContextHandlerCollection"/> </Item> <Item> <New id="DefaultHandler" class="org.eclipse.jetty.server.handler.DefaultHandler"/> </Item> </Array> </Set> </New> </Set> <Set name="stopAtShutdown">true</Set> <Set name="sendServerVersion">true</Set> <Set name="sendDateHeader">true</Set> <Set name="gracefulShutdown">1000</Set> <Set name="dumpAfterStart">false</Set> <Set name="dumpBeforeStop">false</Set></Configure>
概述 在Jetty中,所有XML文件的配置使用Descriptor来表达,而对这些Descriptor的处理使用DescriptorProcessor来实现。 Descriptor和DescriptorProcessor类图 Descriptor实现 Descriptor可以表达一个*.tld文件(TldDescriptor)、一个/META-INF/web.xml文件(WebDescriptor),一个/org/eclipse/jetty/webapp/webdefault.xml(DefaultsDescriptor),一个/META-INF/web-fragment.xml文件(FragmentDescriptor),一个override-web.xml文件(OverrideDescriptor)。其中TldDescriptor在TagLibConfiguration的TagListener中查找并使用TldProcessor解析;WebDescriptor在WebXmlConfiguration的preConfigure中查找,并设置到MetaData的webXmlRoot字段中,并更新MetaData的ordering字段,其资源文件可以手动设置WebAppContext的descriptor字段,或者未设置而使用META-INF/web.xml文件;DefaultsDescriptor也在WebXmlConfiguration的preConfigure中查找,并设置到MetaData的webDefaultsRoot字段中,并更新MetaData的ordering字段,其资源文件可以手动设置WebAppContext中的defaultsDescriptor字段,或未设置而默认使用/org/eclipse/jetty/webapp/webdefault.xml文件;OverrideDescriptor也在WebXmlConfiguration的preConfigure中查找,并设置到MetaData的webOverrideRoots集合中,并更新MetaData中的ordering字段,其资源文件可以手动设置,如果未设置,则忽略;而FragmentDescriptor则是在FragmentConfiguration中的preConfigure中添加到MetaData的webFragmentResourceMap、webFragmentNameMap以及webFragmentRoots中,如果MetaData的ordering为null,且不为absolute,则更新ordering字段。 每个Descriptor使用一个xml的Resource实例作为构造函数构建,并使用XmlParser将其解析成类DOM树,保存树的root节点引用。 除了TldDescriptor在TagLibConfiguration中已经处理完成,其他的Descriptor使用StandardDescriptorProcessor以及PlusDescriptorProcessor来处理,其中StandardDescriptorProcessor在WebXmlConfiguration的configure方法中注册到MetaData的descriptorProcessors集合中,而PlusDescriptorProcessor在PlusConfiguration的configure方法中注册到MetaData中。并在MataData的resolve方法中使用注册的DescriptorProcessor依次解析webDefaultsRoot、webXmlRoot、webOverrideRoots以及webFragmentRoots对应的Descriptor实例。 DescriptorProcessor实现 DescriptorProcessor只有一个process方法,他遍历传入的Descriptor的所有Node,并对不同Node做相应的处理。在IterativeDescriptorProcessor的采用了非常巧妙的实现方法,即使用一个visitors的Map,包含节点的tag到相应处理方法的映射,因而在IterativeDescriptorProcessor的实现中,它遍历Descriptor的节点树,对每个节点查找对应的处理方法,并调用查找到的方法,其子类的实现只需要注册这个visitors的Map,然后实现注册的方法即可;为了增加可扩展性,在解析前和解析后分别添加了start、end的插入点。如在StandardDescriptorProcessor中,注册了如下几个visitor方法:context-param => visitContextParam 向WebAppContext添加InitParam信息。 display-name => visitDisplayName 向WebAppContext设置displayName属性。servlet => visitServlet 向ServletHandler中添加一个新的ServletHolder,并配置其servlet-name、init-param、servlet-class、jsp-file、load-on-startup、security-role-ref、run-as、async-supported、enabled、multipart-config等信息;如果id设置为jsp,则会在InitParam中配置scratchdir、classpath参数,以及为Jasper配置com.sun.appserv.jsp.classpath参数,而在WebAppContext中为Jasper配置org.apache.catalina.jsp_classpath属性;用于注册org.apache.jasper.servlet.JspServlet;对jsp-file,设置其forcePath为该值。 servlet-mapping=> visitServletMapping 配置ServletHandler中servlet-name对应的ServletMapping信息。 session-config => visitSessionConfig 设置SessionHandler中SessionManager的一些配置信息。 mime-mapping => visitMimeMapping 设置WebAppContext中extension到mimeType的映射。 welcome-file-list => visitWelcomeFileList 设置WebAppContext中的welcomeFiles。 locale-encoding-mapping-list => visitLocaleEncodingList 设置WebAppContext中locale到encoding的映射关系。 error-page => visitErrorPage 设置ErrorPageErrorHandler中errorCode或exceptionType到location的映射关系。 taglib => visitTagLib 设置taglib-uri到taglib-location的映射关系,即WebAppContext中taglib-uri是taglib-location的alias。 jsp-config => visitJspConfig 将jsp-property-group下url-pattern映射到JspServlet中。 security-constraint => visitSecurityConstraint 向SecurityHandler中添加ConstraintMapping。 login-config => visitLoginConfig 向SecurityHandler中设置AuthMethod、RealmName属性,以及对FORM方法的验证,设置login、error页面的InitParam。 security-role => visitSecurityRole 向SecurityHandler中注册定义的role集合。 filter => visitFilter 向ServletHandler注册FilterHolder,并配置filter-name、filter-class、init-param、async-supported等信息。 filter-mapping => visitFilterMapping 向ServletHandler注册FilterMapping信息。 listenr => visitListener 向WebAppContext注册EventListener。 distributable => visitDestributable 设置WebDescriptor的distributable属性为true。 在PlusDescriptorProcessor中,首先在其start方法中会向WebAppContext注册InjectionCollection、LifeCycleCallbackCollection、RunAsCollection(该属性在RunAsAnnotationHandler中使用)属性,并且注册了以下几个visitor方法:env-entry => visitEnvEntry 向InjectionCollection添加Injection实例,其中jndiName为env-entry-name定义的值,valueClass为env-entry-type定义的类型,而targetClass、targetName为injection-target下的injection-target-class、injection-target-name中定义的值,每个injection-target生成一个Injection实例。同时将env-entry-value中定义的值绑定到java:com/env/<name>对应的资源中。(Injection实例也可以使用@Resource注解注册,并在ResourceAnnotationHandler中解析) resource-ref => visitResourceRef 向InjectionCollection添加Injection实例,其中jndiName为res-ref-name,typeClass为res-type,并绑定该引用资源。 resource-env-ref => visitResourceEnvRef 向InjectionCollection添加Injection实例,其中jndiName为resource-env-ref-name,typeClass为resource-env-ref-type,并绑定该env引用资源。 message-destination-ref => visitMessageDestinationRef 向InjectionCollection添加Injection实例,其中jndiName为message-destination-ref-name,typeClass为message-destination-type,并绑定该message-destination引用资源。 post-construct => visitPostConstruct 向LifeCycleCallbackCollection注册一个PostConstructCallback,其targetClass由lifecycle-callback-class定义,而method由lifecycle-callback-method定义(该PostConstructCallback也可以使用@PostConstruct的Annotation方式注册,并在PostConstructAnnotationHandler中解析)。 pre-destroy => visitPreDestroy 向LifeCycleCallbackCollection注册PreDestroyCallback,其targetClass由lifecycle-callback-class定义,methodName由lifecycle-callback-method定义(该PreDestroyCallback也可以使用@PreDestroy注解注册,并在PreDestroyAnnotationHandler中解析)。 所有以上注册的RunAsCollection、InjectionCollection、LifeCycleCallbackCollection都在PlusDecorator中使用,PlusDecorator类实现Decorator方法,在所有的decorate实现方法中,使用RunAsCollection向ServletHolder中注册配置的roleName(感觉这里有bug,应该是decorate一个Servlet而不是ServletHolder);使用InjectionCollection向Servlet、Filter、EventListener注入JNDI对应的值;使用LifeCycleCallbackCollection调用所有注册的PostConstruct方法。而在destroyServlet、Filter实例时,使用LifeCycleCallbackCollection调用素有注册的PreDestroy方法。
概述 Jetty的强大之处在于可以自由的配置某些组建的存在与否,以提升性能,减少复杂度,而其本身也因为这种特性而具有很强的可扩展性。SecurityHandler就是Jetty对Servlet中Security框架部分的实现,并可以根据实际需要装卸和替换。Servlet的安全框架主要有两个部分:数据传输的安全以及数据授权,对数据传输的安全,可以使用SSL对应的Connector实现,而对于数据授权安全,Servlet定义了一套自己的框架。Servlet的安全框架支持两种方式的验证:首先,是用于登陆的验证,对于定义了role-name的资源都需要进行登陆验证,Servlet支持NONE、BASIC、CLIENT-CERT、DIGEST、FORM等5种验证方式(<login-config>/<auth-method>);除了用户登陆验证,Servlet框架还定义了role的概念,一个role可以包含一个或多个用户,一个用户可以隶属于多个role,一个资源可以有一个或多个role,只有这些定义的role才能访问该资源,用户只能访问它所隶属的role能访问的资源。另外,对一个Servlet来说,还可以定义role-name到role-link的映射关系,从文档上,这里的role-name是Servlet中使用的名字,而role-link是Container中使用的名字,感觉很模糊,从Jetty的角度,role-name是web.xml中在<security-constraint>/<auth-constraint>/<role-name>中对一个URL Pattern的role定义,而role-link则是UserIdentity中roles数组的值,而UserIdentity是LoginService中创建的,它从文件、数据库等加载已定义的user的信息:用户名、密码、它隶属的role等,如果Servlet中没有定义role-name到role-link的映射,则直接使用role-name去UserIdentity中比较role信息。关于Servlet对Security框架的具体解释,可以参考Oracle的文档:http://docs.oracle.com/cd/E19798-01/821-1841/6nmq2cpk7/index.html在web.xml中,对用于登陆验证方式的定义如下: <login-config> <auth-method>FORM</auth-method> <realm-name>Example-Based Authentiation Area</realm-name> <form-login-config> <form-login-page>/jsp/security/protected/login.jsp</form-login-page> <form-error-page>/jsp/security/protected/error.jsp</form-error-page> </form-login-config> </login-config> OR <login-config> <auth-method>BASIC</auth-method> <realm-name>Tomcat Manager Application</realm-name> </login-config> 而对资源所属role的定义如下: <security-constraint> <security-constraint> <web-resource-collection> <web-resource-name>Status interface</web-resource-name> <url-pattern>/status/*</url-pattern> </web-resource-collection> ... <auth-constraint> <role-name>manager-gui</role-name> <role-name>manager-script</role-name> <role-name>manager-jmx</role-name> <role-name>manager-status</role-name> </auth-constraint> </security-constraint> Jetty对Servlet Security实现概述和类图 在Jetty中,使用Authenticator接口抽象不同用户登陆验证的逻辑;使用LoginService接口抽象对用户名、密码的验证;使用UserIdentity保存内部定义的一个用户的用户名、密码、role集合;使用ConstraintMapping保存URL Pattern到role集合的映射;使用UserIdentity.Scope保存一个Servlet中role-name到role-link的映射。他们的类图如下:UserIdentity实现 UserIdentity表示一个用户的认证信息,它包含Subject和UserPrincipal,其中Subject是Java Security框架定义的类型,而UserPrincipal则用于存储用户名以及认证信息,在Jetty中一般使用KnownUser来存储,它包含了UserName以及Credential实例,其中Credential可以是Crypt、MD5、Password等。在Credential中定义了check方法用于验证传入的credential是否是正确的。IdentityService实现 IdentityService我猜原本用于将UserIdentity、RunAsToken和当前Thread关联在一起,以及创建UserIdentity、RunAsToken,然而我看的版本中,DefaultIdentityService貌似还没有实现完成,目前只是根据提供的Subject、Principal、roles创建DefaultUserIdentity实例,以及使用runAsName创建RoleRunAsToken,对Servlet中的runAsToken,我看的Jetty版本也还没有实现完成。 public UserIdentity newUserIdentity(final Subject subject, final Principal userPrincipal, final String[] roles) { return new DefaultUserIdentity(subject,userPrincipal,roles); } public RunAsToken newRunAsToken(String runAsName) { return new RoleRunAsToken(runAsName); } LoginService实现 在Jetty中,LoginService用来验证给定的用户名和证书信息(如密码),即对应的login方法;以及验证给定的UserIdentity,即对应的validate方法;其Name属性用于标识实例本身(即作为当前使用的realm name);另外IdentityService用于根据加载的用户名和证书信息创建UserIdentity实例。 public interface LoginService { String getName(); UserIdentity login(String username,Object credentials); boolean validate(UserIdentity user); IdentityService getIdentityService(); void setIdentityService(IdentityService service); void logout(UserIdentity user); } 为了验证用户提供的用户名和证书的正确性和合法性,需要有一个地方用来存储定义好的正确的用户名以及对应的证书信息(如密码等),Jetty提供了DB、Properties文件、JAAS、SPNEGO作为用户信息源的比较。对于DB或Properties文件方式存储用户信息,如果每次的验证都去查询数据库或读取文件内容,效率会很低,因而还有一种实现方式是将数据库或文件中定义的用户信息预先的加载到内存中,这样每次验证只需要读取内存即可,这种方式的实现性能会提高很多,但是这样就无法动态的修改用户信息,并且如果用户信息很多,会占用很多的内存,目前Jetty采用后者实现,其中数据库存储用户信息有两个:JDBCLoginService以及DataSourceLoginService,Properties文件存储对应的实现是HashLoginService,它们都继承自MappedLoginService。在MappedLoginService中保存了一个ConcurrentMap<String, UserIdentity>实例,它是一个UserName到UserIdentity的映射,在该实例start时,它会从底层的数据源中加载用户信息,对HashLoginService,它会从config指定的Properties文件中加载用户信息,并填充ConcurrentMap<String, UserIdentity>,其中Properties文件的格式为:<username>=credential, role1, role2, ....如果credential以"MD5:"开头,表示它是MD5数据,如果以"CRYPT:"开头,表示它是crypt数据,否则表示它是密码字符;如果以存在的用户不在新读取的用户列表中,则将其移除,因为HashLoginService还可以启动一个线程以隔一定的时间重新加载文件中的内容,以处理文件更新的问题。在MappedLoginService中还定义了几个Principal的实现类:KnownUser、RolePrincipal、Anonymous等,在添加加载的用户时,使用KnownUser保存username和credential信息,并将该Principal添加到Subject的Principals集合中,同时对每个role创建RolePrincipal,并添加到Subject的Principals集合中,而将credential添加到Subject的PrivateCredentials集合中,使用IdentityService创建UserIdentity,并添加到ConcurrentMap<String, UserIdentity>中。在login验证中,首先使用传入的username查找存在的UserIdentity,并使用找到的UserIdentity中的Principal的check方法验证传入的credential,如果验证失败,返回null(即调用Credential的check方法:Password/MD5/Crypt)。对DataSourceLoginService和JDBCLoginService只是从数据库中加载用户信息,不详述。而JAASLoginService和SpnegoLoginService也只是使用各自的协议进行验证,不细述。Authenticator实现 Authenticator用于验证传入的ServletRequest、ServletResponse是否包含正确的认证信息。其接口定义如下: public interface Authenticator { // Jetty支持BASIC、FORM、DIGEST、CLIENT_CERT、SPNEGO的认证,该方法返回其中的一种,或用于自定义的方法。 String getAuthMethod(); // 设置配置信息(SecurityHandler继承自AuthConfiguration接口):AuthMethod、RealmName、InitParameters、LoginService、IdentityService、IsSessionRenewedOnAuthentication void setConfiguration(AuthConfiguration configuration); // 验证逻辑的实现方法,其中mandatory若为false表示当前资源有没有配置role信息,或者@ServletSecurity中的@HttpConstraint的EmptyRoleSemantic被配置为PERMIT,此时返回Deferred类型的Authentication,如果不手动的调用其authenticate或login方法,就不会对该请求进行验证。 // 对BasicAuthenticator的实现,它从Authorization请求头中获取认证信息(用户名和用户密码,使用":"分割,并且使用Base64编码),调用LoginService进行认证,当认证通过时,如果配置了renewSession为true,则将HttpSession中的所有属性更新一遍,并且添加(org.eclipse.jetty.security.secured, True) entry,并使用UserIdentity以及AuthMethod创建UserAuthentication返回。如果认证失败,则返回401 Unauthorized错误,并且在相应消息中包含头:WWW-Authenticate: basic realm=<LoginService.name> // 对FormAuthenticator的实现,它首先要配置formLoginPage、formLoginPath(默认j_security_check)、formErrorPage、formErrorPath;只有当前请求URL是formLoginPath时,从j_username和j_password请求参数中获取username和password信息,使用LoginService验证,如果验证通过且这个请求是因为之前请求其他资源重定向过来的,这重定向到之前的URL,创建一个SessionAuthentication放入HttpSession中,并返回一个新创建的FormAuthentication;如果验证失败,如果没定义formErrorPage,返回403 Forbidden相应,否则重定向或forward到formErrorPage;对于其他URL请求,查看在当前Session中是否存在已认证的Authentication,如果有,但是重新验证缓存的Authentication失败,则将这个Authentication从HttpSession中移除;否则返回这个Session中的Authentication;对于其他情况,表示当前请求需要认证后才能访问,此时保存当前请求URI以及POST数据到Session中,以在认证之后可以直接跳转,然后重定向或forward到formLoginPage中。 // 对DigestAuthenticator的实现类似BasicAuthenticator,只是它使用Digest的方式对认证数据进行加密和解密。 // 对ClientCertAuthenticator则采用客户端证书的方式认证,SpnegoAuthenticator使用SPNEGO方式认证,JaspiAuthenticator使用JASPI方式认证。 Authentication validateRequest(ServletRequest request, ServletResponse response, boolean mandatory) throws ServerAuthException; // 只用于JaspiAuthenticator,用于所有后继handler处理完成后对ServletRequest、ServletResponse、User的进一步处理,目前不了解JASPI的协议逻辑,因而不了解具体的用途。 boolean secureResponse(ServletRequest request, ServletResponse response, boolean mandatory, User validatedUser) throws ServerAuthException; } SecurityHandler与ConstraintSecurityHandler实现 SecurityHandler继承自HandlerWrapper,并实现了Authenticator.AuthConfiguration接口,因而它包含了realm、authMethod、initParameters、loginService、identityService、renewSession等字段,在其start时,它会首先从ServletContext的InitParameters中导入org.eclipse.jetty.security.*属性的值到其InitParameters中,如果LoginService为null,则从Server中查找一个已经注册的LoginService,使用Authenticator.Factory根据AuthMethod创建对应的Authenticator实例。ConstraintSecurityHandler继承自SecurityHandler类,它定义了ConstraintMapping列表、所有定义的role、以及pathSpec到Map<String, RoleInfo>(key为httpMethod,RoleInfo包含UserDataConstraint枚举类型和roles集合)的映射,其中ConstraintMapping中保存了method、methodOmissions、pathSpec、Constraint(Constraint中包含了name、roles、dataConstraint等信息),ConstraintMapping在解析web.xml文件时添加,它对应<security-constraint>下的配置,如auth-constraint下的role-name配置对应roles数组,user-data-contraint对应dataConstraint,web-resource-name对应name,http-method对应method,url-pattern对应pathSpec;在每次添加ConstraintMapping时都会更新roles列表以及pathSpec到Map<String, RoleInfo>的映射。在SecurityHandler的handle方法中,它只需要对REQUEST、ASYNC类型的DispatcherType需要验证:它首先根据pathInContext和Request实例查找RoleInfo信息;如果RoleInfo处于forbidden状态,发送403 Forbidden相应,如果DataConstraint配置了Intergal、Confidential,但是Connector中没有配置相应的port,则发送403 Forbidden相应,否则重定向请求到Integral、Confidential对应的URL;对没有验证过的请求调用Authenticator.validateRequest()对请求进行验证;如果验证的结果是Authentication.ResponseSent,设置Request的handled为true,如果为Authentication.User,表示认证成功,设置该Authentication到Request中,并检查role,即检查当前User是否处于RoleInfo中的role集合中,如果不是,发送403 Forbidden响应,否则调用下一个handler的handle方法,之后调用Authenticator.secureResponse()方法;如果验证结果是Authentication.Deferred,在调用下一个handler的handle方法后调用Authenticator.secureResponse()方法;否则直接调用Authenticator.secureResponse()方法。
转载自(虽然它本身也是转载自其他地方):http://www.cnblogs.com/wy2325/archive/2013/03/25/2980683.html 这篇文章转自JavaEye,以前配置web.xml时都不知道为什么这样,看了之后明白了很多。贴下来,共同分享! Web.xml常用元素 <web-app> <display-name></display-name>定义了WEB应用的名字 <description></description> 声明WEB应用的描述信息 <context-param></context-param> context-param元素声明应用范围内的初始化参数。 <filter></filter> 过滤器元素将一个名字与一个实现javax.servlet.Filter接口的类相关联。 <filter-mapping></filter-mapping> 一旦命名了一个过滤器,就要利用filter-mapping元素把它与一个或多个servlet或JSP页面相关联。 <listener></listener>servlet API的版本2.3增加了对事件监听程序的支持,事件监听程序在建立、修改和删除会话或servlet环境时得到通知。 Listener元素指出事件监听程序类。 <servlet></servlet> 在向servlet或JSP页面制定初始化参数或定制URL时,必须首先命名servlet或JSP页面。Servlet元素就是用来完成此项任务的。 <servlet-mapping></servlet-mapping> 服务器一般为servlet提供一个缺省的URL:http://host/webAppPrefix/servlet/ServletName。 但是,常常会更改这个URL,以便servlet可以访问初始化参数或更容易地处理相对URL。在更改缺省URL时,使用servlet-mapping元素。 <session-config></session-config> 如果某个会话在一定时间内未被访问,服务器可以抛弃它以节省内存。 可通过使用HttpSession的setMaxInactiveInterval方法明确设置单个会话对象的超时值,或者可利用session-config元素制定缺省超时值。 <mime-mapping></mime-mapping>如果Web应用具有想到特殊的文件,希望能保证给他们分配特定的MIME类型,则mime-mapping元素提供这种保证。 <welcome-file-list></welcome-file-list> 指示服务器在收到引用一个目录名而不是文件名的URL时,使用哪个文件。 <error-page></error-page> 在返回特定HTTP状态代码时,或者特定类型的异常被抛出时,能够制定将要显示的页面。 <taglib></taglib> 对标记库描述符文件(Tag Libraryu Descriptor file)指定别名。此功能使你能够更改TLD文件的位置, 而不用编辑使用这些文件的JSP页面。 <resource-env-ref></resource-env-ref>声明与资源相关的一个管理对象。 <resource-ref></resource-ref> 声明一个资源工厂使用的外部资源。 <security-constraint></security-constraint> 制定应该保护的URL。它与login-config元素联合使用 <login-config></login-config> 指定服务器应该怎样给试图访问受保护页面的用户授权。它与sercurity-constraint元素联合使用。 <security-role></security-role>给出安全角色的一个列表,这些角色将出现在servlet元素内的security-role-ref元素 的role-name子元素中。分别地声明角色可使高级IDE处理安全信息更为容易。 <env-entry></env-entry>声明Web应用的环境项。 <ejb-ref></ejb-ref>声明一个EJB的主目录的引用。 < ejb-local-ref></ ejb-local-ref>声明一个EJB的本地主目录的应用。 </web-app> 相应元素配置 1、Web应用图标:指出IDE和GUI工具用来表示Web应用的大图标和小图标 <icon> <small-icon>/images/app_small.gif</small-icon> <large-icon>/images/app_large.gif</large-icon> </icon> 2、Web 应用名称:提供GUI工具可能会用来标记这个特定的Web应用的一个名称 <display-name>Tomcat Example</display-name> 3、Web 应用描述: 给出于此相关的说明性文本 <disciption>Tomcat Example servlets and JSP pages.</disciption> 4、上下文参数:声明应用范围内的初始化参数。 <context-param> <param-name>ContextParameter</para-name> <param-value>test</param-value> <description>It is a test parameter.</description> </context-param> 在servlet里面可以通过getServletContext().getInitParameter("context/param")得到 5、过滤器配置:将一个名字与一个实现javaxs.servlet.Filter接口的类相关联。 <filter> <filter-name>setCharacterEncoding</filter-name> <filter-class>com.myTest.setCharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>GB2312</param-value> </init-param> </filter> <filter-mapping> <filter-name>setCharacterEncoding</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> 6、监听器配置 <listener> <listerner-class>listener.SessionListener</listener-class> </listener> 7、Servlet配置 基本配置 <servlet> <servlet-name>snoop</servlet-name> <servlet-class>SnoopServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>snoop</servlet-name> <url-pattern>/snoop</url-pattern> </servlet-mapping> 高级配置 <servlet> <servlet-name>snoop</servlet-name> <servlet-class>SnoopServlet</servlet-class> <init-param> <param-name>foo</param-name> <param-value>bar</param-value> </init-param> <run-as> <description>Security role for anonymous access</description> <role-name>tomcat</role-name> </run-as> </servlet> <servlet-mapping> <servlet-name>snoop</servlet-name> <url-pattern>/snoop</url-pattern> </servlet-mapping> 元素说明 <servlet></servlet> 用来声明一个servlet的数据,主要有以下子元素: <servlet-name></servlet-name> 指定servlet的名称 <servlet-class></servlet-class> 指定servlet的类名称 <jsp-file></jsp-file> 指定web站台中的某个JSP网页的完整路径 <init-param></init-param> 用来定义参数,可有多个init-param。在servlet类中通过getInitParamenter(String name)方法访问初始化参数 <load-on-startup></load-on-startup>指定当Web应用启动时,装载Servlet的次序。 当值为正数或零时:Servlet容器先加载数值小的servlet,再依次加载其他数值大的servlet. 当值为负或未定义:Servlet容器将在Web客户首次访问这个servlet时加载它 <servlet-mapping></servlet-mapping> 用来定义servlet所对应的URL,包含两个子元素 <servlet-name></servlet-name> 指定servlet的名称 <url-pattern></url-pattern> 指定servlet所对应的URL 8、会话超时配置(单位为分钟) <session-config> <session-timeout>120</session-timeout> </session-config> 9、MIME类型配置 <mime-mapping> <extension>htm</extension> <mime-type>text/html</mime-type> </mime-mapping> 10、指定欢迎文件页配置 <welcome-file-list> <welcome-file>index.jsp</welcome-file> <welcome-file>index.html</welcome-file> <welcome-file>index.htm</welcome-file> </welcome-file-list> 11、配置错误页面 一、 通过错误码来配置error-page <error-page> <error-code>404</error-code> <location>/NotFound.jsp</location> </error-page> 上面配置了当系统发生404错误时,跳转到错误处理页面NotFound.jsp。 二、通过异常的类型配置error-page <error-page> <exception-type>java.lang.NullException</exception-type> <location>/error.jsp</location> </error-page> 上面配置了当系统发生java.lang.NullException(即空指针异常)时,跳转到错误处理页面error.jsp 12、TLD配置 <taglib> <taglib-uri>http://jakarta.apache.org/tomcat/debug-taglib</taglib-uri> <taglib-location>/WEB-INF/jsp/debug-taglib.tld</taglib-location> </taglib> 如果MyEclipse一直在报错,应该把<taglib> 放到 <jsp-config>中 <jsp-config> <taglib> <taglib-uri>http://jakarta.apache.org/tomcat/debug-taglib</taglib-uri> <taglib-location>/WEB-INF/pager-taglib.tld</taglib-location> </taglib> </jsp-config> 13、资源管理对象配置 <resource-env-ref> <resource-env-ref-name>jms/StockQueue</resource-env-ref-name> </resource-env-ref> 14、资源工厂配置 <resource-ref> <res-ref-name>mail/Session</res-ref-name> <res-type>javax.mail.Session</res-type> <res-auth>Container</res-auth> </resource-ref> 配置数据库连接池就可在此配置: <resource-ref> <description>JNDI JDBC DataSource of shop</description> <res-ref-name>jdbc/sample_db</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref> 15、安全限制配置 <security-constraint> <display-name>Example Security Constraint</display-name> <web-resource-collection> <web-resource-name>Protected Area</web-resource-name> <url-pattern>/jsp/security/protected/*</url-pattern> <http-method>DELETE</http-method> <http-method>GET</http-method> <http-method>POST</http-method> <http-method>PUT</http-method> </web-resource-collection> <auth-constraint> <role-name>tomcat</role-name> <role-name>role1</role-name> </auth-constraint> </security-constraint> 16、登陆验证配置 <login-config> <auth-method>FORM</auth-method> <realm-name>Example-Based Authentiation Area</realm-name> <form-login-config> <form-login-page>/jsp/security/protected/login.jsp</form-login-page> <form-error-page>/jsp/security/protected/error.jsp</form-error-page> </form-login-config> </login-config> 17、安全角色:security-role元素给出安全角色的一个列表,这些角色将出现在servlet元素内的security-role-ref元素的role-name子元素中。 分别地声明角色可使高级IDE处理安全信息更为容易。 <security-role> <role-name>tomcat</role-name> </security-role> 18、Web环境参数:env-entry元素声明Web应用的环境项 <env-entry> <env-entry-name>minExemptions</env-entry-name> <env-entry-value>1</env-entry-value> <env-entry-type>java.lang.Integer</env-entry-type> </env-entry> 19、EJB 声明 <ejb-ref> <description>Example EJB reference</decription> <ejb-ref-name>ejb/Account</ejb-ref-name> <ejb-ref-type>Entity</ejb-ref-type> <home>com.mycompany.mypackage.AccountHome</home> <remote>com.mycompany.mypackage.Account</remote> </ejb-ref> 20、本地EJB声明 <ejb-local-ref> <description>Example Loacal EJB reference</decription> <ejb-ref-name>ejb/ProcessOrder</ejb-ref-name> <ejb-ref-type>Session</ejb-ref-type> <local-home>com.mycompany.mypackage.ProcessOrderHome</local-home> <local>com.mycompany.mypackage.ProcessOrder</local> </ejb-local-ref> 21、配置DWR <servlet> <servlet-name>dwr-invoker</servlet-name> <servlet-class>uk.ltd.getahead.dwr.DWRServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>dwr-invoker</servlet-name> <url-pattern>/dwr/*</url-pattern> </servlet-mapping> 22、配置Struts <display-name>Struts Blank Application</display-name> <servlet> <servlet-name>action</servlet-name> <servlet-class> org.apache.struts.action.ActionServlet </servlet-class> <init-param> <param-name>detail</param-name> <param-value>2</param-value> </init-param> <init-param> <param-name>debug</param-name> <param-value>2</param-value> </init-param> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/struts-config.xml</param-value> </init-param> <init-param> <param-name>application</param-name> <param-value>ApplicationResources</param-value> </init-param> <load-on-startup>2</load-on-startup> </servlet>
概述 ServletHandler继承自ScopedHandler,是Jetty中用于存储所有Filter、FilterMapping、Servlet、ServletMapping的地方,以及用于实现一次请求所对应的Filter链和Servlet执行流程的类。对Servlet的框架实现中,它也被认为是Handler链的末端,因而在它的doHandle()方法中没有调用nextHandle()方法。ServletHandler的成员 正如前面提到的,ServletHandler是一个用于管理Filter、FilterMapping、Servlet、ServletMapping的容器,因而它需要一下成员用于存储这些它管理的实例: private FilterHolder[] _filters=new FilterHolder[0]; private FilterMapping[] _filterMappings; private ServletHolder[] _servlets=new ServletHolder[0]; private ServletMapping[] _servletMappings; private final Map<String,FilterHolder> _filterNameMap= new HashMap<String,FilterHolder>(); private List<FilterMapping> _filterPathMappings; private MultiMap<String> _filterNameMappings; private final Map<String,ServletHolder> _servletNameMap=new HashMap<String,ServletHolder>(); private PathMap _servletPathMap; 其中FilterHolder和ServletHolder分别用于存储Filter、Servlet实例以及其配置信息,即web.xml配置文件中的<filter>、<servlet>的配置信息(参考:Servlet、Filter、Registration的实现);而FilterMapping和ServletMapping则是FilterName到URL Pattern的mapping信息,以及ServletName到URL Pattern的mapping信息,即web.xml中的<filter-mapping>、<servlet-mapping>信息。其中FilterMapping包含了一个Filter适用的所有URL Pattern、Servlets、DispatcherType以及对应FilterHolder信息: private int _dispatches=DEFAULT; private String _filterName; private transient FilterHolder _holder; private String[] _pathSpecs; private String[] _servletNames; 它有appliesTo()方法用于判断传入的path和dispatcherType是否符包含当前Filter。对于URL Pattern:*.do=>anything.do, /foo/poo/abc.do, /path/to/*=>/path/to, /path/to/abdc。而ServletMapping包含了ServletName和其适用的所有URL Pattern,它的URL Pattern的mapping规则和FilterMapping中的规则一样: private String _servletName; private String[] _pathSpecs; 在管理Filter和FilterMapping中,可以使用FilterHolder、pathSpec、DispatcherType向_filters数组中添加一个FilterHolder,并向_filterMappings数组中添加一个FilterMapping实例;而由该方法引申出来的,可以直接传入Filter实例、Filter类实例、Filter类名,而由方法内部创建对应的FilterHolder实例;DispatcherType可以是一个EnumSet类型的DispatcherType;可以直接添加Filter或FilterMapping或两个同时添加;也可以使用prependFilterMapping将新的FilterMapping添加到数组前。对Servlet和ServletMapping管理也是类似,使用ServletHolder和pathSpec添加_servlets数组和_servletMappings数组,并引申出Servlet可以是实例、Servlet类名、Servlet类实例,而由内部创建ServletHolder实例;也可以单独的添加ServletHolder或ServletMapping实例。在ServletHandler中还有_filterNameMap和_servletNameMap实例用于存储FilterName到FilterHolder以及ServletName到ServletHolder的映射,它在每次_filters、_servlets数组更新时都会随着更新,并且在doStart方法中也会再更新一次;另外对Filter还有_filterPathMapping用于存储所有FilterMapping的一个List,_filterNameMapping用于存储ServletName到多个FilterMapping的MultiMap,对Servlet中也有_servletPathMap,包含pathSpec到ServletHolder的PathMap,他们在每次_filterMappings、_servletMappings更新时以及doStart方法中都会被更新。而在start时也会start所有的FilterHolder和ServletHolder,对所有FilterHolder的start按其定义顺序进行,而对ServletHolder的start,则按其InitOrder排序。doScope方法实现 doScope方法用于准备执行环境,其实现逻辑为:如果传入的target不是ServletName(即以"/"开头,表示它为Path),则使用该target从_servletPathMap中找到对应的ServletHolder,并计算出当前的ServletPath和PathInfo,如果时INCLUDE类型的Dispatch,设置Request的javax.servlet.include.serlvet_path,javax.servlet.include.path_info属性为计算出来的值,否则设置Request的ServletPath和PathInfo的值为计算出的值;对target为ServletName,ServletHolder的实例从_servletNameMap字段中查找。然后将当前找到的ServletHolder作为UserIdentityScope设置到Request中,以及设置org.eclipse.multipartConfig属性为ServletHolder中的MultipartConfig实例。在刚方法退出时,将UserIdentityScope、ServletPath、PathInfo还原回原来的值。doHandle方法实现 doHandle方法用于真正实现执行逻辑:它首先通过target找到FilterChain实例,对于target为path时,它遍历整个_filterPathMapping的列表,选出所有符合pathInContext的FilterHolder数组,以及从_filterNameMappings中找出所有ServletHolder中存储的ServletName对应的FilterMapping并且DispatcherType相符合的FilterMapping数组,以及注册的ServletName为"*"的FilterMapping且DispatcherType相符的FilterMapping数组,合并这些数组,并一同用Request、ServletHolder创建FilterChain实例;对target为ServletName时,只需要查找_filterNameMapping字段中的FilterMapping。如果没有FilterHolder实例,则向客户端发送404 Not Found响应;否则如果FilterChain实例不为null,调用其doFilter方法,传入ServletRequest和ServletResponse参数,在FilterChain的doFilter方法中它回一次遍历Filter的doFilter方法,直到最后调用ServletHolder的handle方法;否则,直接调用ServletHolder的handle方法。
概述 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实例,如果需要的话。
概述 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:实现异步请求处理。ServletRequest ServletRequest是对Servlet请求消息的封装,其子接口HttpServletRequest则是对HTTP请求消息的封装,在Servlet框架中默认实现了ServletRequestWrapper和HttpServletRequestWrapper以便利用户对请求的Wrap。在Jetty中,使用Request类实现HttpServletRequest接口,Request包含了HttpConnection引用,因为HttpConnection包含了HttpParser解析HTTP请求后的所有信息,如请求行、请求头以及请求内容。其中ServletRequest接口定义与实现如下: public interface ServletRequest { // Request级别的Attribute操作,它属于Request的私有数据,因而Request使用Attributes私有字段实现。然而Jetty添加了一些自定义的Attribute: // 在getAttribute时,org.eclipse.jetty.io.EndPoint.maxIdleTime用于获取EndPoint中MaxIdleTime属性,org.eclipse.jetty.continuation用于获取Continuation实例(如果该属性没被占用) // 在setAttribute时,org.eclipse.jetty.server.Request.queryEncoding属性同时用于设置Request的QueryEncoding属性; // org.eclipse.jetty.server.sendContent同时用于直接向客户端发送Object指定的相应内容,该值必须是HttpContent、Resource、Buffer、InputStream类型; // org.eclipse.jetty.server.ResponseBuffer同时用于设置对客户端相应的ByteBuffer数据;org.eclipse.jetty.io.EndPoint.maxIdleTime同时用于设置EndPoint中的MaxIdleTime属性。 // 如果注册了ServletRequestAttributeListener,则相应的attributeAdded、attributeReplaced、attributeRemoved方法会被调用。 public Object getAttribute(String name); public Enumeration<String> getAttributeNames(); public void setAttribute(String name, Object o); public void removeAttribute(String name); // CharacterEncoding属性,用于指定读取请求内容时使用的编码方式,如果设置的编码方式不支持,抛出UnsupportedEncodingException。CharacterEncoding的设置必须在读取Parameter或getReader方法的调用之前,不然该方法不会有效果。该属性默认为null,此时Jetty默认使用UTF-8编码Parameter,使用ISO-8859-1编码方式创建Reader实例。 public String getCharacterEncoding(); public void setCharacterEncoding(String env) throws UnsupportedEncodingException; // 请求内容的长度,以字节为单位,-1表示长度未知。该值从HttpConnection的RequestFields字段中获取,该字段在HttpParser解析HTTP请求消息时填充。 public int getContentLength(); // 返回请求内容的MIME type,即在请求头中设置的ContentType头,该值同样从HttpConnection的RequestFields字段中获取,该字段在HttpParser解析HTTP请求消息时填充。 public String getContentType(); // 使用ServletInputStream包装请求内容的读取,该方法和getReader方法,只能使用一种方式来读取请求内容,否则抛出IllegalStateException。 // ServletInputStream继承自InputStream,它只是实现了一个readLine函数。在该方法实现中返回HttpConnection的getInputStream,在该方法返回前,如果当前请求有Expect: 100-continue头,则先向客户端发送100 Continue相应,然后返回HttpInput实例,它继承自ServletInputStream,从HttpParser中读取数据。 public ServletInputStream getInputStream() throws IOException; // 请求参数相关的操作,请求参数可以在URL使用?paramName=paramValue&...的方式指定,也可是使用POST/PUT方法在请求消息体中指定(ContentType: application/x-www-form-urlencoded),或同时存在。URL的parameter信息读取使用queryEncoding字段指定的编码方式(默认编码为UTF-8),请求消息体中信息读取使用characterEncoding字段指定的编码方式(默认编码为ISO-8859-1)。在读取请求消息体中的parameter数据时,可以使用ContextHandler中的MaxFormContentSize属性或Server中的org.eclipse.jetty.server.Request.maxFormContentSize的Attribute配置最大支持的parameter数据大小,如果ContentLength超过该值,则抛出IllegalStateException。 // 相同的paramName可以多次出现,因而一个paramName可能对应多个值,此时使用getParameterValues()方法获取所有的值。 // 如果使用POST指定请求参数,使用getInputStream或getReader读取数据会对parameter的读取有影响。 public String getParameter(String name); public Enumeration<String> getParameterNames(); public String[] getParameterValues(String name); public Map<String, String[]> getParameterMap(); // 返回当前请求使用的协议以及其版本号,如HTTP/1.1,对HTTP请求,它在解析请求行时设置。 public String getProtocol(); // 返回请求的Schema,默认为http,在SSL相关的Connector的customize方法中会被设置为https。 public String getScheme(); // 返回当前请求发送的服务器名称,如果请求URI中包含Host Name信息,则ServerName使用该信息;否则使用Host头(值为host:port)中的Server Name信息; // 如果不存在Host头,尝试使用EndPont中的LocalHost(LocalAddr,如果_dns值设置为false,即Connector的ResolveNames属性为false,其默认值为false); // 否则使用当前Server的LocalHost的Address:InetAddress.getLocalHost().getHostAddress(); // 对代理服务器,如果Jetty在Connector中开启了forwarded检查,如果Connector中设置了_hostHeader值,则使用强制设置Host头为该值,清除已有的ServerName和Port,并重新计算; // 否则如果存在X-Forwarded-Host头,强制设置Host头为该头值的最左边的值,即第一个代理服务器的主机名,清除已有的ServerName和Port,并重新计算; // 如果存在X-Forwarded-Server头,则强制设置Request的ServerName值为该头值的最左边的值,即第一个代理服务器的主机名。 // 如果存在X-Forwarded-For头,则更新Request的RemoteAddr和RemoteHost值,即最原始客户端的地址和主机名。 // 如果存在X-Forwarded-Proto头,则更新Request的Schema为该头值的最左边的值。(这是原始请求的Schema还是神马呢?) // 具体参考:http://benni82.iteye.com/blog/849139 public String getServerName(); // ServerPort有一下查找路径:请求URI中的Port;Host头中值的Port;EndPoint的LocalPort;如果都没有找到,则对HTTPS默认值为443,对HTTP默认值为80 public int getServerPort(); // 将ServletInputStream包裹成BufferedReader类型,使用CharacterEncoding编码方式,如果没有设置该编码,默认使用ISO-8559-1,因而CharacterEncoding的设置必须在该方法调用之前。 public BufferedReader getReader() throws IOException; // 返回客户端的IP地址,如果开启了forwarded检查,则该值会被更新为原始的客户端请求IP,即使该请求穿越了好几个代理服务器。 public String getRemoteAddr(); // 如果Connector设置了ResolveNames属性为true,即Request中_dns字段为true,则返回客户端主机名,否则返回客户端主机IP;如果开启了forwarded检查,则该值被更新为最原始的客户端的主机名或IP,即使该请求穿越了好几个代理服务器。 public String getRemoteHost(); // 返回Accept-Language请求头中的指定的Locale值,支持多个值,以",", " ", "\t"等字符分隔,每个值都可以是如下格式:language-<country>;q<value>或language-<country>; q=<value>的格式,在选择一个Locale时使用qValue最大的那个。如果没有设置,默认使用当前服务器的Locale值。 public Locale getLocale(); // 返回Accept-Language头中指定的所有Locale枚举值,以qValue的值降序排列,如果没有指定该头,使用服务器默认的Locale值。 public Enumeration<Locale> getLocales(); // 检查当前请求是否在安全传输通道,如https。 public boolean isSecure(); // 返回一个RequestDispatcher,内部使用ServletContext获取RequestDispatcher实例,根据传入的path计算uriInContext值:如果它以"/"开头,uriInContext的值即为该值, // 如果它不以"/"开头,即表示它是相对与当前请求的path,则uriInContext的值为相对于当前Request URI,如pathInfo为/foo/goo,path为poo,则uriInContext为:<servletPath>/foo/poo public RequestDispatcher getRequestDispatcher(String path); // 返回客户端的端口,调用EndPoint的getRemotePort方法。 public int getRemotePort(); // 返回当前服务器的主机名,如果Connector的ResolveNames为true,否则为当前服务器的IP地址。 public String getLocalName(); // 返回当前服务器的IP地址,调用EndPoint的getLocalAddr方法。 public String getLocalAddr(); // 返回当前服务器的端口号,即当前连接使用的端口号,不是服务器本身监听的端口号。 public int getLocalPort(); // 返回当前Request正在执行所在ServletContext。 public ServletContext getServletContext(); // 启动当前请求的异步模式,该方法调用后,即可以推出当前请求的处理方法,在退出之前需要将返回的AsyncContext放到一个等待Queue或类似的数据结构中,从而在之后的处理中还能得到这个 // AsyncContext实例进一步处理这个Request,AsyncContext包含了对当前Request和Response实例的引用,一般唤起这个异步的请求,使用AsyncContext的dispatch方法, // 从而保证在下一次的处理过程中依然存在Filter链通道。这个方法和带ServletRequest、ServletResponse参数的方法区别在于: // 如果Servlet A对应为/url/A,在Servlet A中调用request.getRequestDispatcher("/url/B"),此时在Servlet B中,如果调用request.startAsync().dispatch(),此时会dispatch到/url/A, // 但是如果在Servlet B中调用request.startAsync(request, response).dispatch(),此时会dispatch到/url/B中。 // 另外该方法会在调用之前注册的AsyncListener的onStartAsync()方法之后,清除这些已注册的AsyncListener。在onStartAsync方法中可以将自己重新注册到AsyncContext中,只是这个设计好奇怪。。。 public AsyncContext startAsync() throws IllegalStateException; public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException; // 查看是否当前request异步模式已经启动,即已经调用startAsync方法,但是还没有调用AsyncContext中的dispatch或onComplete方法。 public boolean isAsyncStarted(); // 查看当前Request是否支持异步模式,默认为true,如果Filter或Servlet不支持异步模式,这在调用对应的doFilter、service之前会把该值设置为false。 public boolean isAsyncSupported(); // 获取最近一次调用startAsync方法时创建的AsyncContext实例,如果当前request没有异步模式还没有启动,则抛出IllegalStateException。 public AsyncContext getAsyncContext(); // 获取当前请求的Dispatcher类型。Dispatcher Type是Container用于选区对应的Filter链。请求最初为Dispatcher.REQUEST;如果调用RequestDispatcher.forward()方法, // 则变为DispatcherType.FORWARD;如果调用RequestDispatcher.include()方法,则变为DispatcherType.INCLUDE;如果调用AsyncContext.dispatch()方法,则变为 // DispatcherType.ASYNC;最后如果请求被dispatch到error page中,则为DispatcherType.ERROR。 // DispatcherType在HttpConnection中的handleRequest方法中,在调用server.handle()/server.handleAsync()方法之前,设置为REQUEST、ASYNC,根据当前Request的AsyncContext是否处于初始化状态,如果是,则为REQUEST,否则为ASYNC状态,其他值则在Dispatcher中forward或include方法中设置。 public DispatcherType getDispatcherType(); } HttpServletRequest接口定义如下: public interface HttpServletRequest extends ServletRequest { // Servlet的四种验证方式(他们在web.xml文件中的long-config/auth-method中定义): // BASIC使用Authentication头传输认证信息,该信息以Base64的编码方式编码,当需要用户验证时会弹出登陆窗口。 // FORM使用表单的方式认证,用户名和密码在j_username和j_password字段中,登陆URL为/j_security_check,第一次使用表单方式明文传输,之后将认证信息存放在Session中,在Session实效之前可以不用在手动登陆。 // DIGEST也时使用Authentication头传输认证信息,但是它使用加密算法将密码加密。 // CLIENT_CERT则使用客户端证书的方式传输认证信息。 // 更详细的内容参考:http://docs.oracle.com/cd/E19798-01/821-1841/bncas/index.html public static final String BASIC_AUTH = "BASIC"; public static final String FORM_AUTH = "FORM"; public static final String CLIENT_CERT_AUTH = "CLIENT_CERT"; public static final String DIGEST_AUTH = "DIGEST"; // 返回当前请求的认证方法,可以是BASIC、FORM、CLIENT_CERT、DIGEST。如果没有定义对应Servlet的认证配置,则返回null。在Jetty实现中,在ServletHandler根据配置信息以及认证的结果设置请求的Authentication实例,如果Authentication实例是Authentication.Deffered实例,则先验证,并设置验证后的Authentication实例,如果Authentication实例是Authentication.User实例,则设置返回其AuthMethod属性,否则返回null。 public String getAuthType(); // 返回当前请求包含的Cookie数组,从请求的Cookie头中获取,如果没有Cookie信息,则返回null。在Jetty实现中,使用CookieCutter类解析Cookie头。 // Cookie值的格式为:<name>=value; [$path=..; $domain=...; $port=...; $version=..]; <name>=<value>..... // 在Servlet中,Cookie类包含comment(描述信息)、domain(cookie只是对指定的domain域可见,默认情况下cookie只会传输给设置这个cookie的server)、maxAge(cookie的最长可存活秒数,负数表示永远不会失效,0表示cookie会被删除)、path(指定那个path下的请求这个cookie才会被发送,包括它的子目录)、secure(这个cookie是否只在https请求中发送)、version(0表示netscape最初定义的cookie标准,1表示兼容RFC 2109,一般都是0以保持最大兼容性)、isHttpOnly(HttpOnly标识的cookie一般不应该暴露给客户端的脚本,以部分解决跨站点脚本攻击问题)字段。对浏览器,一般他们应该能为每个Web Server存储至少20个cookie,总共至少能处理300个cookie,以及至少4k大小。 public Cookie[] getCookies(); // 解析指定请求头的Date值,并转换成long值,如If-Modified-Since头。Jetty支持的Date格式有:EEE, dd MMM yyyy HH:mm:ss zzz; EEE, dd-MMM-yy HH:mm:ss等。 public long getDateHeader(String name); // 返回指定的请求头的值,没有该头,返回null,如果有多个相同名字的头,则返回第一个该头的值。name是大小写无关。 public String getHeader(String name); // 返回指定请求头的所有值 public Enumeration<String> getHeaders(String name); // 返回请求头的所有名称,有些Container会禁用该方法,此时返回null。 public Enumeration<String> getHeaderNames(); // 返回指定请求头的int值。 public int getIntHeader(String name); // 返回请求方法,如GET、POST、PUT等。该值在解析请求行结束后设置。 public String getMethod(); // 设置请求的额外path信息,该值在不同的地方会被设置为不同的值,如果请求的URI为: http://host:80/context/servlet/path/info如在HttpConnection中,其值为/context/servlet/path/info // 在ContextHandler的doScope中,其值被设置为/servlet/path/info;在ServletHandler的doScope方法中,其值被设置为/path/info,并设置ServletPath为/servlet。 // 这里基于的假设为contextPath为/context,servletPath为/servlet,即在web.xml配置中Servlet相关的url-pattern为/servlet/*。 // 如果没有/path/info后缀,则PathInfo为null,如果Servlet的path-pattern设置为/*,则ServletPath为"",对url-pattern为*.do类似的配置,pathInfo永远为null,servletPath则为/path/info.do的值;有些时候ServletPath的值也可能是Servlet Name的值。 public String getPathInfo(); public String getServletPath(); // 返回pathInfo对应的在ServletContext下的真是Server路径,如果pathInfo为null,则返回null。 public String getPathTranslated(); // 返回当前请求所属的ContextPath。它在ContextHandler的doScope方法中或Dispatcher的forward方法中设置。 public String getContextPath(); // 返回当前请求的query字符串,如果设置了queryEncoding,使用该编码方式编码。 public String getQueryString(); // 返回请求的登陆用户,如果用户没有认证,则返回null。该信息从设置的Authentication实例中获取,如果其实例为Authentication.Defered,则需要先验证,然后返回获取UserIdentity,并从中获取Principal,而从Principal中可以获取用户名。 public String getRemoteUser(); // 验证当前请求的User是否在传入的Role中,即使用设置的Authentication实例验证,当前登陆的User所在的Roles中是否存在传入的RoleName。其中UserIdentity.Scope实例用于查找RoleName对应在Container使用的RoleName,如果没有这个映射则使用传入的RoleName本身,这个scope在ServletHandler的doScope方法中设置。这个映射在web.xml中的security-rol-ref(role-name->role-link)中设置。 public boolean isUserInRole(String role); // 返回当前登陆User的Principal实例,从设置的Authentication实例中获取,或者为null。 public java.security.Principal getUserPrincipal(); // 返回客户端指定的Session ID,它可以和Server的当前Session ID不同。客户端可以使用两种方式设置该值:Cookie和URL。默认先从Cookie中找,找不到再从URL中找。 // 对Cookie方式,在web.xml的cookie-config/name中配置Session的Cookie Name(默认值为JSSESSIONID),找到该Cookie Name对应的Cookie值作为Requested Session ID // 对URL方式,在;<sessionIdPathParamName>=....[;#?/]之间的值作为Requested Session ID的值。其中sessionIdPathParamName可以通过web.xml的context-param,使用org.eclipse.jetty.servlet.SessionIdPathParameterName属性值配置,默认为jsessionid。 public String getRequestedSessionId(); public boolean isRequestedSessionIdFromCookie(); public boolean isRequestedSessionIdFromURL(); // 检查当前Requested Session Id是否valid,在Jetty中valid是指RequstedSessionId存在,并且和当前请求的Server Session的ClusterId相同,即他们的SessionId相同。 public boolean isRequestedSessionIdValid(); // 返回/contextPath/servletPath/pathInfo的值。对forward后请求,该值为forward后的URI。 public String getRequestURI(); // 返回getSchema()://getServerName():getPort()/getRequestURI(),对forward后请求,该值为forward后的URL。 public StringBuffer getRequestURL(); // 返回和当前请求相关联的HttpSession,如果没有关联的HttpSession,且create为true,则创建新的HttpSession实例。没有参数即create为true。 public HttpSession getSession(boolean create); public HttpSession getSession(); //使用Container定义的认证机制验证请求用户的合法性。在Jetty实现中,只是对Authentication.Deferred的Authentication类型进行验证,否则返回401 Unauthorized错误相应,并返回false。 public boolean authenticate(HttpServletResponse response) throws IOException,ServletException; // 提供用户名和密码并交由Container对其进行认证。在Jetty实现中,只对Authentication.Deferred类型提供用户名和密码认证,否则抛出ServletException。 public void login(String username, String password) throws ServletException; // 注销当前用户,并清理_authentication字段。 public void logout() throws ServletException; // 对multipart/form-data请求类型,表示请求内容由多个部分组成,此时使用RFC1867来解析该内容到多个Part中。在Servlet中一个Part有自己的请求头和请求消息提,包含ContentType、Name、Headers、Size(已写入的大小)、InputStream等信息,它还提供了一个方法将请求内容写入指定的文件中,以及删除该内部文件。并提供MultipartConfigElement类来做相关的配置,如写文件时的位置Location;最大可上传的文件大小MaxFileSize;最大可包含的所有Part的请求大小MaxRequestSize;如果写入的数据超过配置的大小,则开始将数据写入文件中MaxFileSizeThreshold。该配置信息可以使用org.eclipse.multipartConfig属性设置,而Location信息可以使用javax.servlet.context.tempdir属性在ServletContext中设置,或这使用java.io.tmpdir中指定的值。在Jetty中使用MultiPartInputStream来表达并解析请求内容到多个Part(MultiPart)中。具体格式可以参考:http://blog.zhaojie.me/2011/03/html-form-file-uploading-programming.html public Collection<Part> getParts() throws IOException, ServletException; public Part getPart(String name) throws IOException, ServletException; } 除了以上实现,Request还包含了一些额外的字段,如_timestamp在请求开始时设置,即解析请求头完成时;_dispatchTime在RequestLogHandler的handle方法,当Request是非Initial的状态下设置,即当前Request已经为Dispatched的状态;_handled表示该请求是否已经处理完成; ServletResponse ServletResponse是对Servlet响应消息的封装,其子接口HttpServletResponse则是对HTTP响应消息的封装, ServletResponse接口定义如下: public interface ServletResponse { // 设置与返回消息体中的字符编码方式。如果没有设置,默认使用ISO-8559-1。其中setContentType和setLocale会隐式的设置该值,但是显示的设置会覆盖之前隐式的设置,而该值的设置也会影响ContentType的值。该值的设置必须在调用getWriter()方法之前,否则不会起作用。在set方法中,如果传入的charset为null,则清楚原来设置的值,并将原来的_mimeType值设置为Content-Type头;否则,更新characterEncoding的值,并更新contentType的属性以及Content-Type头(更新contentType中"charset="之后部分的值,或使用"mimeType; charset=<characterType>")。 public void setCharacterEncoding(String charset); public String getCharacterEncoding(); // 设置响应消息的Content-Type头以及contentType字段的值,它会同时影响mimeType字段以及characterEncoding字段。如果type为null,则清除contentType、mimeType的值,移除Content-Type响应头,并且如果locale也为null,同时清除characterType值;否则,设置mimeType为";"之前的值,而characterEncoding的值为"charset="之后的值,如果没有"charset="之后的值,则是使用"charset=<characterType>"值拼接以设置Content-Type响应头。 public void setContentType(String type); public String getContentType(); // 设置Locale的值,同时设置响应的Content-Language头。同时该set也会影响characterEncoding、mimeType和Content-Type响应头。其中characterEncoding从ContextHandler中的Locale到charset的映射中获取,即web.xml中的locale-encoding-mapping-list中定义,而对Content-Type的值只影响"charset="之后的值。 public void setLocale(Locale loc); public Locale getLocale(); // 设置contentLength字段以及Content-Length响应消息头。如果当前写入的数据已经大于或等于设置的值,则该方法还会关闭当前的ServletOutputStream或Writer。 public void setContentLength(int len); // 返回向客户端写数据的ServletOutputStream或PrintWriter。它们只能使用其中一个方法。在Jetty中使用HttpOutput内部封装HttpGenerator实例用于向Socket中写数据。 public ServletOutputStream getOutputStream() throws IOException; public PrintWriter getWriter() throws IOException; // 响应消息处理相关的Buffer操作,在Jetty中即为HttpGenerator中响应消息体的Buffer大小与刷新操作。设置BufferSize必须在写响应消息体之前。另外reset只是清除消息体的缓存,并不会清除响应状态码、响应头等信息,如果该方法调用时消息已经被commit,则抛出IllegalStateException。 public void setBufferSize(int size); public int getBufferSize(); public void flushBuffer() throws IOException; public void resetBuffer(); // 是否响应消息已经commit,即响应消息状态行和消息头已经发送给客户端。 public boolean isCommitted(); // 清除所有的响应消息状态,所有已经设置的响应头(但是不包括Connection头),如果响应已经commit,则抛出IllegalStateException。 public void reset(); } HttpServletResponse接口定义如下,它添加了一些Cookie、Header相关的操作: public interface HttpServletResponse extends ServletResponse { // 向响应消息中添加一个Cookie实例。即添加"Set-Cookie"头。 public void addCookie(Cookie cookie); // 消息头操作:增删改查。一个相同名字的头可能会有多个条纪录,因而可以对应多个值。在Jetty实现中代理给HttpConnection中的responseFields字段。其中Date头的格式为:EEE, dd MMM yyyy HH:mm:ss 'GMT',对于String为值的设置来说,处于INCLUDE Dispatch状态下,只能设置org.eclipse.jetty.server.include.<HeaderName>的头。 public boolean containsHeader(String name); public void setDateHeader(String name, long date); public void addDateHeader(String name, long date); public void setHeader(String name, String value); public void addHeader(String name, String value); public void setIntHeader(String name, int value); public void addIntHeader(String name, int value); public String getHeader(String name); public Collection<String> getHeaders(String name); public Collection<String> getHeaderNames(); // 编码传入的url,决定并添加是否需要在URL中加入Session ID信息以作为Session追踪。处于健壮性的考虑,所有Servlet产生的URL必须使用改方法编码,不然对不支持Cookie的浏览器将会失去Session信息。在实现中,如果Request使用Cookie作为Session追踪,则去除url中的sessionId信息;否则如果session存在并可用,则向url中添加session追踪信息,在"#"或"?"之前。 public String encodeURL(String url); // 编码传入的url,用于sendRedirect方法中。在Jetty中改方法直接调用encodeURL()方法。 public String encodeRedirectURL(String url); // 向客户端发送响应状态码和原因(如果有的话)。服务器会保留已经设置的cookie,但是会在必要情况下修改响应头。如果在web.xml中定义了响应的状态码到error page的映射,则该响应会被转发到那个错误页面中。在Jetty实现中,它清除Buffer信息,characterEncoding值,Expires、Last-Modified、Cache-Control、Content-Type、Content-Length头,设置响应状态和消息,对非204(No Content)、304(Not Modified)、206(Partial Content)、200(OK)的状态码(即允许有消息体),首先查找有没有注册的ErrorHandler ,如果有,向Request的属性中设置javax.servlet.error.status_code, javax.servlet.error.message, javax.servlet.error.request_uri, javax.servlet.error.servlet_name为相应的值(RequestDispatcher中定义的属性key),并调用ErrorHandler的handle方法;否则设置Cache-Control头为must-revalidate,no-cache,no-store,设置Content-Type头为text/html;charset=ISO-8859-1,并返回一个简单的错误页面包含状态码和消息原因。最后调用HttpConnection的completeResponse()方法以完成响应。 public void sendError(int sc, String msg) throws IOException; public void sendError(int sc) throws IOException; // 发送客户端一个重定向的消息和URL,即设者响应状态码为302 Moved Temporary,在Location中包含要重定向目的地的URL,此时客户端会使用新的URL重新发送请求。如果location以"/"开头,表示它是绝对地址,否则为相应请求的相对地址,即最终解析成Request.getRequestURI()/location,location可以包含query信息。最后调用HttpConnection的completeResponse()方法以完成当前响应。 public void sendRedirect(String location) throws IOException; // 设置响应状态码和状态描述信息。 public void setStatus(int sc); public void setStatus(int sc, String sm); public int getStatus(); }
概述 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:实现异步请求处理。Jetty中的Holder 在Jetty中,每个Servlet和其相关信息都由ServletHolder封装,并且将Servlet相关操作代理给ServletHolder;同理,对Filter也有FilterHolder与其相对应;另外ServletHolder和FilterHolder都继承自Holder实例。在Servlet 3.0中引入动态向ServletContext注册Servlet和Filter,并返回相应的Registration实例,用于进一步配置与其关联的Servlet和Filter,因而Registration也是和ServletHolde和FilterHolder相关联的接口。在Jetty中,他们的类关系图如下:Servlet Servlet和Filter是Servlet规范中用于定义用户逻辑实现的接口,Servlet是最初的版本,所有的“服务器端小程序”都要实现该接口,并交由Servlet Container管理其实例,负责其生命周期,以及当相应请求到来时调用相应方法。Servlet接口非常简单: public interface Servlet { // Servlet Container在创建一个Servlet后调用该方法,并传入ServletConfig实例,从而用户可以在这个方法中做一些自定义的初始化工作,如初始化数据库连接等。 // 并且Servlet Container可以保证一个Servlet实例init方法之后被调用一次,但是对一个Servlet类init方法可能会被多次调用,因而有些Servlet Container可能会在某些情况下将某些 // Servlet移出Servlet Container,而后又重新加载这些Servlet,如为了在处理Servlet Container资源压力比较大的情况下。 public void init(ServletConfig config) throws ServletException; // 返回在init方法中传入的ServletConfig实例。ServletConfig包含了在web.xml配置文件中配置当前Servlet的初始化参数,并且可以使用该ServletConfig实例获取当前ServletContext实例。 public ServletConfig getServletConfig(); // 当该Servlet对应的请求到来时,Servlet Container会调用这个Servlet的service方法,用于处理请求,并将相应写入ServletResponse参数中,该方法只能在init方法完成后被调用。 // 在service方法中,可以选择使用ServletResponse定义的方法返回响应给客户端,或只是向ServletResponse中写入响应,最终由Servlet Container根据ServletResponse的信息将响应返回给客户端。 // 很多情况下,Servlet Container都不会使用多线程来处理客户请求,应该该方法会在多线程环境下被使用,Servlet实现者可以实现SingleThreadMode接口以强制该方法只在单线程的环境下被使用。 // 但是SingleThreadMode接口已经在Servlet 2.3中被废弃,实现该接口也会影响Servlet的执行性能,而且有些Servlet Container会选择实例或多个Servlet实例,以保证对请求的响应性能,因而此时依然不能保证该方法的单线程特性,因而不建议使用这个SingleThreadMode接口。 public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException; // 返回当前Servlet的描述信息,如作者、版本号、版权等。在GenericServlet默认实现中,返回空字符串。 public String getServletInfo(); // 当Servlet Container销毁当前Servlet时会调用该方法,从而给Servlet提供拥有清理当前Servlet占用的资源的地方,如关闭和数据库的连接等。 // Servlet Container会保证该方法在所有执行service方法的线程完成或超时后被调用。Servlet Container一般会在三种情况下会调用该方法: // 1. Servlet Container当前资源比较紧张,需要将一些不再用或不常用的Servlet移出;2. 实现某些Servlet的热部署;3. 当前Web Application被停止。 public void destroy(); } 对每个Servlet都有一个ServletConfig实例和其对应,ServletConfig中包含了在web.xml文件中定义的Servlet的init-param参数,并且可以使用ServletConfig实例获取ServletContext实例: public interface ServletConfig { // 返回该ServletConfig对应的Servlet的名称,在web.xml文件中定义的Servlet名称或对于没有注册的Servlet为其类名。 public String getServletName(); // 返回和其关联的ServletContext。 public ServletContext getServletContext(); // 返回在web.xml配置文件中定义或者使用ServletRegistration动态定义添加的init-param参数。 public String getInitParameter(String name); public Enumeration<String> getInitParameterNames(); } 在Jetty中,不管是在web.xml中配置的Servlet还是使用ServletContext动态注册并使用ServletRegistration.Dynamic动态配置的Servlet,在ServletHandler内部都使用ServletHolder来表示一个Servlet,并且由ServletHolder来处理所有和Servlet相关的逻辑。ServletHolder的实现逻辑在之后给出。Servlet框架中默认实现了两个Servlet:GenericServlet和HttpServlet,GenericServlet只是对Servlet的简单实现,而HttpServlet会根据请求中的方法将请求分发给相应的:doGet/doPost/doPut/doHead/doOptions/doTrace等方法,它还提供了getLastModified()方法,重写该方法用于实现条件GET。以上这些方法(除doOptions和doTrace已经有具体的逻辑实现)默认实现直接方法405 Method Not Allowed响应(Http/1.1)或404 Bad Request响应(HTTP/1.0)。重写相应的方法以实现各个Method对应的逻辑。Filter 在Servlet 2.3开始引入了Filter机制,以在Servlet的service方法的执行前后添加一些公共的Filter逻辑,为面向切面的编程提供了很大的便利,这些公共的逻辑如纪录一个Request从进入Servlet Container到出所花费的总时间、为每个Request添加一些额外的信息以帮助之后处理、对所有或特定Request添加用户验证功能等。Filter可以在web.xml文件中定义,由Servlet Container负责其实例化、初始化以及doFilter方法的调用。在Servlet 3.0以后,还支持动态的给ServletContext注册Filter,并由返回的FilterRegistration.Dynamic实例做进一步的配置。Filter的接口定义也是比较简单: public interface Filter { // 由ServletContainer在初始化一个Filter时调用,Filter的实现者可以在该方法中添加一些用户自定义的初始化逻辑,同时可以保存FilterConfig实例,它可以获取定义的init-param初始化参数以及获取ServletContext实例。其他情况和Servlet类似,不赘述。 public void init(FilterConfig filterConfig) throws ServletException; // 每一次请求到来都会穿越配置的FilterChain,执行配置的Servlet,然后从这个FilterChain中返回。在doFilter方法的实现中,要调用下一个Filter,使用FilterChain的doFilter方法。 // 在调用FilterChain的doFilter之前为执行请求之前的处理,而之后为请求已经执行完成,在响应返回的路上的逻辑处理。也可以步调用FilterChain的doFilter方法,以阻止请求的进一步处理。 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException; // 在Filter被移出Servlet Container时调用,它和Servlet类似,不赘述。** public void destroy(); } Filter接口中包含对FilterConfig以及FilterChain的使用,FilterConfig和ServletConfig定义、实现以及逻辑都类似: public interface FilterConfig { public String getFilterName(); public ServletContext getServletContext(); public String getInitParameter(String name); public Enumeration<String> getInitParameterNames(); } FilterChain是Servlet中将Filter链以Channel的方式连接在一起的类,它实现了可以向执行Servlet前和后都添加一些切面逻辑,甚至阻止Servlet的执行,Filter的实现者使用FilterChain的doFilter方法调用在这个链中的下一个Filter的doFilter方法,如果当前Filter是链中最后一个Filter,则调用响应的Servlet。其接口定义如下: public interface FilterChain { // 调用该方法会调用Filter链中的下一个Filter的doFilter方法,如果当前Filter是这条链中的最后一个Filter,则该方法会调用响应的Servlet的service方法。 public void doFilter (ServletRequest request, ServletResponse response) throws IOException, ServletException; } 在Jetty中,ServletHandler的内部类Chain实现了FilterChain接口,在构造Chain实例时,首先根据Request的URL以及对应Servlet Name查找所有相关的Filter列表,然后使用这个Filter列表、Request实例、当前请求对应的ServletHolder创建这个链,在其doFilter方法实现中,它会存储一个_filter索引,它指向下一个Filter实例,当每个Filter调用doFilter方法时,Chain会根据这个索引获取下一个Filter实例,并将该索引向后移动,从而调用下一个Filter的doFilter方法,如果这个索引值到达最后一个Filter链中的Filter,且有ServletHolder实例存在,则调用该ServletHolder的handle方法,否则调用notFound方法,即向客户端发送404 NOT FOUND响应。如果Filter不支持ASYNC模式,则在调用其doFilter之前,需要将Request的ASYNC支持设置为false。在ServletHandler中还有CachedChain实现了FilterChain接口,它以链表的形式纪录找到的Filter列表,并将这个列表缓存在ServletHandler中,不同的dispatch类型有一个列表,并且可以根据请求的URL或请求的Servlet名来查找是否已经有缓存的Filter链表。在ServletHandler中,可以使用setFilterChainsCached方法来配置是否使用CachedChain还是直接使用Chain,默认使用CachedChain。Registration Registration是Servlet 3.0规范中引入的接口,用于表示向ServletContext中动态注册的Servlet、Filter的实例,从而实现对这些动态注册的Servlet、Filter实例进行进一步的配置。 对于Servlet和Filter的配置,他们的共同点是他们有响应的Name、ClassName、Init Parameters以及asyncSupported属性,而这些方法正是Registration接口的定义。Registration接口将setAsyncSupport方法定义在其内部的Dynamic接口中,Dynamic用于表示这是用于动态的配置这个Servlet或Filter的含义,但是为什么要将这个方法放在Dynamic接口中呢?如何决定不同的方法应该是在Registration本身的接口中,而那些应该放到Dynamic接口中呢? public interface Registration { // 返回这个Registration实例关联的Servlet或Filter的Name,这个Name在向ServletContext注册Servlet或Filter时给定。 public String getName(); // 返回这个Registration实例关联的Servlet或Filter的类名。 public String getClassName(); // 和这个Registration实例关联的初始化参数的操作。 public boolean setInitParameter(String name, String value); public String getInitParameter(String name); public Set<String> setInitParameters(Map<String, String> initParameters); public Map<String, String> getInitParameters(); interface Dynamic extends Registration { // 配置Registration关联的Servlet或Filter是否支持异步操作。 public void setAsyncSupported(boolean isAsyncSupported); } } Registration有两个子接口:ServletRegistration和FilterRegistration,分别用于表示Servlet相关的配置和Filter相关的配置。对ServletRegistration,它可以设置Servlet的URL Mapping、RunAsRole属性、LoadOnStartup属性等: public interface ServletRegistration extends Registration { // 添加URL patterns到这个ServletRegistration关联的Servlet的映射。如果有任意的URL patterns已经映射到其他的Servlet中,则该方法不会执行任何行为。 public Set<String> addMapping(String... urlPatterns); // 获取所有到当前ServletRegistration对应的Servlet的URL patterns。 public Collection<String> getMappings(); // 获取当前ServletRegistration对应的Servlet的RunAsRole。 public String getRunAsRole(); interface Dynamic extends ServletRegistration, Registration.Dynamic { // 设置当前ServletRegistration对应的Servlet的loadOnStartup等级。如果loadOnStartup大于或等于0,表示Servlet Container要优先初始化该Servlet, // 此时Servlet Container要在Container初始化时实例化并初始化该Servlet,即在所有注册的ContextListener的contextInitialized方法调用完成后。 // 如果loadOnStartup小于0,则表示这个Servlet可以在用到的时候实例化并初始化。默认值为-1。 public void setLoadOnStartup(int loadOnStartup); // 设置ServletRegistration相关的ServletSecurityElement属性。 public Set<String> setServletSecurity(ServletSecurityElement constraint); // 设置ServletRegistration对应的Servlet的MultipartConfigElement属性。 public void setMultipartConfig(MultipartConfigElement multipartConfig); // 设置ServletRegistration对应的Servlet的RunAsRole属性。 public void setRunAsRole(String roleName); } } 对FilterRegistration,它可以配置Filter的URL Mapping和Servlet Mapping等: public interface FilterRegistration extends Registration { // 添加FilterRegistration关联的Filter到Servlet的映射,使用Servlet Name、DispatcherType作为映射。isMatchAfter表示新添加的Mapping是在已有的Mapping之前还是之后。 public void addMappingForServletNames(EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter, String... servletNames); // 获取当前FilterRegistration关联的Filter已存在的到Servlet Name的映射。 public Collection<String> getServletNameMappings(); // 添加FilterRegistration关联的Filter到Servlet的映射,使用URL patterns、DispatcherType作为映射。isMatchAfter表示新添加的Mapping是在已有的Mapping之前还是之后。 public void addMappingForUrlPatterns(EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter, String... urlPatterns); // 获取当前FilterRegistration关联的Filter已存在的到URL patterns的映射。 public Collection<String> getUrlPatternMappings(); interface Dynamic extends FilterRegistration, Registration.Dynamic { } } 在Jetty中对Registration的实现在Holder中定义,而相应的ServletRegistration和FilterRegistration实现作为ServletHolder和FilterHolder中的内部类实现,具体参考这两个类的实现。Holder实现 在之前有提到,在Jetty中Servlet和Filter由相应的ServletHolder和FilterHolder封装,以将Servlet/Filter相关的信息和配置放在一起,并处理各自相关的逻辑,即面向对象设计中的将数据靠近操作。由于Servlet和Filter有一些相同的配置和逻辑,因而在ServletHolder和FilterHolder中提取出了Holder父类。在Holder的实现中,它主要定义了一些Servlet和Filter都要使用的字段,比高实现了所有和InitParameter相关的操作: public enum Source { EMBEDDED, JAVAX_API, DESCRIPTOR, ANNOTATION }; final private Source _source; protected transient Class<? extends T> _class; protected final Map<String,String> _initParams=new HashMap<String,String>(3); protected String _className; protected String _displayName; protected boolean _extInstance; protected boolean _asyncSupported=true; protected String _name; protected ServletHandler _servletHandler; Holder继承自AbstractLifeCycle,它在start时,如果_class字段没有被设置,则会使用ClassLoader加载_className中指定的类实例并赋值给_class字段;Source只是目前只是一种元数据的形式存在,用于表示Servlet或Filter的来源;而extInstance用于表示Servlet和Filter实例是直接通过ServletContext注册而来,而不是在当前Holder内部创建。在Holder类中还定了两个内部类:HolderConfig和HolderRegistration,其中HoldConfig实现了ServletConfig/FilterConfig相关的所有InitParameter相关的操作(代理给Holder);HolderRegistration实现了Registration.Dynamic接口,其实现也都代理给Holder类中的方法。FilterHolder实现 FilterHolder实现比较简单,它直接继承自Holder类,它额外的包含了一下几个字段: private transient Filter _filter; private transient Config _config; private transient FilterRegistration.Dynamic _registration; 其中_filter字段在start时如果没有初始化,则使用ServletContext创建该Filter实例,而_config字段则在启动时直接创建Config实例(Config是FilterHolder的内部类,且它继承自HolderConfig,并实现了FilterConfig接口),最后调用_filter.init()方法并传入_config实例。在stop时,调用_filter.destroy()方法从而该Filter有机会做一些清理工作,并且调用ServletHandler中的destroyFilter()方法,以通知ContextHandler中定义的Decorators。在注册外部实例化的Filter时,设置_extInstance为true,同时更新_class字段,以及_name字段(如果_name字段未被设置的话)。最后,FilterHolder中还定义了Registration内部类,它继承自HolderRegistration,并实现了FilterRegistration.Dynamic接口。该Registration内部类实现了Mapping相关方法,Jetty中使用FilterMapping来表达一个Filter的映射关系。在FilterMapping中定义了一下映射关系: private int _dispatches=DEFAULT; private String _filterName; private transient FilterHolder _holder; private String[] _pathSpecs; private String[] _servletNames; 它包含了两个appliesTo()方法,这两个方法在Chain用于根据dispatcherType或dispatcherType以及path计算当前FilterMapping是否匹配给定的dispatcherType或dispatcherType和path。在对dispatcherType做匹配计算时,使用FilterMapping实例的没有设置dispatcherType集合,它依然匹配REQUEST或ASYNC(如果Filter支持ASYNC模式的话)。 boolean appliesTo(int type) { if (_dispatches==0) return type==REQUEST || type==ASYNC && _holder.isAsyncSupported(); return (_dispatches&type)!=0; } ServletHolder实现 ServletHolder实现相对复杂,它继承自Holder类,并实现了UserIdentity.Scope接口以及Comparable接口,其中Comparable接口用于当ServletHandler在start时,对注册的所有ServletHolder的数组排序以决定他们的start顺序。它包含了一下额外的字段: private int _initOrder; private boolean _initOnStartup=false; private Map<String, String> _roleMap; private String _forcedPath; private String _runAsRole; private RunAsToken _runAsToken; private IdentityService _identityService; private ServletRegistration.Dynamic _registration; private transient Servlet _servlet; private transient Config _config; private transient long _unavailable; private transient UnavailableException _unavailableEx; public static final Map<String,String> NO_MAPPED_ROLES = Collections.emptyMap(); 其中_initOrder为ServletRegistration.Dynamic接口的实现,它定义ServletHolder在ServletHandler中的start顺序,即compareTo()方法的实现的主要参考信息。在Jetty中,只要设置了loadOnStart字段,则它就会在start时被初始化(即使设置的值为负数)。在设置外部Servlet实例直接到ServletHolder中时,_extInstance字段为被设置为true。如果在web.xml中的Servlet定义中有jsp-file定义,则设置该ServletHolder的forcePath值为该jsp-file中定义的值,而其className的值为servlet-class的定义,此时在ServletHolder的handle方法中会将forcePath的值设置到Request的org.apache.catalina.jsp_file属性中(这个设置用来干嘛呢?还不了解。。。);在ServletHolder启动时,如果Servlet实例实现了SingleThreadModel接口,则Servlet实例使用SingleThreadedWrapper类来表示(它包含一个Servlet栈 ,对每次请求,它会复用以前已经创建的Servlet实例或者创建一个新的实例以处理当前的请求,即该Servlet会有多个实例),如果_extInstance为true或者配置了loadOnStart属性,则在ServletHolder启动是就会初始化这个Servlet(实例化Servlet,并调用Servlet的init方法)。当一个请求到来时,ServletHolder调用其handle方法以处理该请求,在该方法中调用Servlet的service方法,如果在处理过程中出错了,则在Request中设置javax.servlet.error.servlet_name属性为ServletHolder的Name属性。类似FilterMapping,Jetty也使用ServletMapping表达Servlet和URL pattern的映射关系: private String[] _pathSpecs; private String _servletName; 其他Security相关的字段和逻辑暂不做介绍。。。。。
概述 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:实现异步请求处理。ServletContext Context在这里是指一个Web Application的上下文(Web Application是一个Server子URL下的Servlet和资源的集合),即它包含了这个Web Application级别的信息,如当前Web Application对应的根路径、使用的Servlet版本、使用的ClassLoader等,在一个JVM中的一个Web Application只能有一个Context(一个JVM可以包含多个Web Application,它们包含不同的根路径,即不同的Context路径,Context路径可以是空("/")即这个JVM只能包含一个Web Application)。ServletContext则是对这个Context的抽象,它还定义了一些和Servlet Container交互的方法,如获取文件的MINE Type、Dispatch请求到另一个URL或Context、将日志写入文件、根据提供的路径获取Resource实例、向这个ServletContext注册并获取Servlet或Filter、向这个ServletContext注册并获取Attribute或初始参数、向这个ServletContext注册或获取相关Listener等。对Distributed的Web Application来说,每个JVM下的Web Application都有一个独立的ServletContext,因而ServletContext不可以作为全局信息存储的地方,因而它并没有分布式信息同步的功能,即它只是本地的ServletContext。在Servlet中,使用ServletConfig实例可以获取ServletContext实例。 类图如下: ServletContext的接口定义如下: public interface ServletContext { // Servlet Container为当前Web Application设置临时目录,并将该临时目录的值存储到当前ServletContext的属性中使用的属性名。 // Jetty使用WebInfConfiguration(在preConfig ure()方法中)来设置该值,设置temp目录的规则: // 1. 如果存在WEB-INF/work目录,则temp目录的值为:WEB-INF/work/Jetty_<host>_<port>__<resourceBase>_<context>_<virtualhost+base36_hashcode_of_whole_string> // 2. 如果"javax.servlet.context.tempdir"已经在外部被设置,并且该目录存在且可写,则temp目录直接设置为该目录实例。 // 3. 如果系统变量中"jetty.home"目录下存在"work"目录且可写,则设置temp目录的值为:${jetty.home}/work/Jetty_<host>_<port>__<resourceBase>_<context>_<virtualhost+base36_hashcode_of_whole_string> // 4. 如果存在"org.eclipse.jetty.webapp.basetempdir"的属性,且该目录存在并可写,设置temp目录为:${org.eclipse.jetty.webapp.basetempdir}/Jetty_<host>_<port>__<resourceBase>_<context>_<virtualhost+base36_hashcode_of_whole_string> // 5. 如果以上条件都不成立,则设置temp目录为:${java.io.tmpdir}/Jetty_<host>_<port>__<resourceBase>_<context>_<virtualhost+base36_hashcode_of_whole_string>,且删除已存在临时目录。 // 注:对temp目录的父目录不是work,会注册在JVM退出时删除该temp目录,并在temp目录下创建.active目录。 public static final String TEMPDIR = "javax.servlet.context.tempdir"; // Servlet 3.0中新引入的特性,即可以在WEB-INF/lib下的jar包中定义/META-INF/web-fragment配置响应的Servlet等。 // 如果在web.xml文件中定义了absolute-ordering,或者在jar包中存在/META-INF/web-fragment.xml文件,且定义了ordering, // 则该属性的值即为根据规范中定义的规则计算出来的读取jar包中web-fragment.xml文件的jar包名顺序,它时一个List<String>类型,包含jar包名字。 // 在Jetty中使用Ordering来表示这种顺序,它有两个实现类:AbsoluteOrdering和RelativeOrdering用来分别表示在web.xml和web-fragment.xml中定义的absolute-ordering和ordering定义, // 并且将最终的解析结果汇总到Metadata类中,并根据规范中定义的规则以及metadata-complete的定义来计算实际的解析顺序, // 而对这两种配置文件的解析由WebDescriptor和FragmentDescriptor来实现,它们都包含了metadata-complete解析,而真正的解析入口在WebXmlConfiguration和FragmentConfiguration中。 // 该规范的定义参考:https://blogs.oracle.com/swchan/entry/servlet_3_0_web_fragment public static final String ORDERED_LIBS = "javax.servlet.context.orderedLibs"; // 返回当前Web Application的Context Path,即当前Web Application的根路径,Servlet Container根据该路径以及一个Request URL的匹配情况来选择一个Request应该交给那个Web Application处理该请求。 // Context Path以"/"开始,但是不能以"/"结尾,对默认的根Context,它返回"",而不是"/"。该值在配置Jetty的ContextHandler中设置。 // 有些Servlet Container支持多个Context Path指向同一个Context,此时可以使用HttpServletRequest中的getContextPath()来获取该Request实际对应的Context Path,此时这两个Context Path的值可能会不同,但是ServletContext中返回的Context Path值是主要的值。另外Jetty也不支持这种特性。 public String getContextPath(); // 通过给定一个Context Path以在当前Servlet Container中找到其对应的ServletContext实例。 // 可以通过该方法获取Servlet Container中定义的另一个Web Application的ServletContext实例,并获得其RequestDispatcher,并将当前请求Dispatch到那个Web Application中做进一步的处理。这里的uripath必须以"/"开头,且其路径相对于当前Server的根路径。出于安全考虑,该方法可能会返回null。 // 在Jetty的实现中,这里uripath可以是一个具体的路径,并且支持查找最准确的匹配。如:对uripath为/foo/goo/abc.html,在Server中由以下Context Path定义:"/", "/foo", "/foo/goo",则最终查找到的ServletContext为"/foo/goo"作为Context Path对应的ServletContext实例。 public ServletContext getContext(String uripath); //返回Servlet规范的主版本,如3 public int getMajorVersion(); // 返回Servlet规范的次版本,如0 public int getMinorVersion(); // 返回当前Web Application基于的Servlet规范的主版本,如在web.xml文件中定义的version(<web-app version="..." ...>...</web-app>,即Jetty中的实现) public int getEffectiveMajorVersion(); // 返回当前Web Application基于的Servlet规范的次版本,如在web.xml文件中定义的version(<web-app version="..." ...>...</web-app>,即Jetty中的实现) public int getEffectiveMinorVersion(); // 返回给定file的MIME type,返回null如果无法计算出其MIME type。这个映射关系由Servlet Container定义或在web.xml文件中定义(mime-mapping, extension, mine-type)。 // 常见的MIME type有:text/html, image/gif等。Jetty使用MimeTypes类来封装所有和MIME type相关的操作,MimeTypes类中定义了所有默认支持的MIME type以及编码类型, // 并且默认从org/eclipse/jetty/http/mime.properties文件中加载默认的映射,如css=text/css, doc=application/msword等,使用addMimeMapping()方法向该类注册web.xml中定义的文件名扩展名到MIME type的映射。 // 而从org/eclipse/jetty/http/encoding.properties文件中加载MIME type的默认编码类型,如text/xml=UTF-8等。 // 在使用文件名查找MIME type时,根据文件名的扩展名查找已注册或默认注册的MIME type。用户自定义的映射优先。用户定义的MIME type映射支持extension为"*",表示任意扩展名。 public String getMimeType(String file); // 对于给定目录,返回该目录下所有的资源以及目录。path必须以"/"开头,如果它不是指向一个目录,则返回空的Set。所有返回的路径都相对当前Web Application的根目录, // 或对于/WEB-INF/lib中jar包中的/META-INF/resources/目录,如一个Web Application包含以下资源:/catalog/offers/music.html, /WEB-INF/web.xml, // 以及/WEB-INF/lib/catalog.jar!/META-INF/resources/catalog/moreOffers/books.html,则getResourcePaths("/catalog")返回{"/catalog/offers/", /catalog/moreOffers/"} // Jetty的实现中,在MetaInfConfiguration中,它会扫描WEB-INF/lib目录下所有的jar包,如果发现在某个jar包中存在META-INF/resources/目录, // 就会将该目录资源作为baseResource在WebInfConfiguration中注册到ContextHandler(WebAppContext)中。从而实现jar包中的META-INF/resources/目录作为根目录的查找。 public Set<String> getResourcePaths(String path); // 返回给定path的URL,path必须以"/"开头,它相对当前Web Application的根目录或相对/WEB-INF/lib中jar包中的/META-INF/resources/目录。 // 其中查找顺序前者优先于后者,但是在/WEB-INF/lib/目录下的jar包的查找顺序未定义。该方法不同于Class.getResource()在于它不使用ClassLoader,如果没有找到给定资源,返回null。 // 在WebAppContext实现中,它还支持Alias查找,并且如果其extractWAR的变量为false,给定的资源在WAR包中,则该URL返回WAR包中的URL。 public URL getResource(String path) throws MalformedURLException; // 参考getResource(path)的查找实现,如果其返回的URL为null,则该方法返回null,否则返回URL对应的InputStream。 public InputStream getResourceAsStream(String path); // 创建一个RequestDispatcher用于将一个Request、Response分发到path对应的URL中,这里path必须以"/"开头,且它相对于当前Context Path。如果无法创建RequestDispatcher,返回null。 // path可以包含query数据用于传递参数:uriInContext?param1=abc&param2=123....该方法可以和getContext(uripath)一起使用,以将当前请求分发到另一个Web Application中。 // 该方法的另一种用法是先有一个Servlet或Filter处理基本的逻辑,然后使用这个RequestDispatcher将当前请求forward到另一个URL中或include一个JSP文件生成响应页面,如果在处理过程中出错,则将其当前请求分发到错误处理的流程中。 // RequestDispatcher支持两种类型的分发:forward和include,唯一的区别是include只可以改变Response的内容,不可以改变其Header信息,forward则没有这种限制。 public RequestDispatcher getRequestDispatcher(String path); // 创建一个RequestDispatcher用于将一个Request、Response分发到name对应的Servlet(JSP)中。如果没能找到响应的Servlet,返回null。 public RequestDispatcher getNamedDispatcher(String name); // 将msg打印到Servlet对应的log文件中,在Jetty中,使用INFO级别打印,logger名称为web.xml定义的display-name,或者context path。 // Jetty默认使用SLF4J作为日志打印框架,可以使用"org.eclipse.jetty.util.log.class"系统属性改变其日志打印框架。 public void log(String msg); // 打印message和throwable到Servlet对应的log文件中,在Jetty中使用WARN级别打印该信息。 public void log(String message, Throwable throwable); // 返回给定path在当前机器上操作系统对应的位置。对/META-INF/resources下的资源,除非他们已经解压到本地目录,否则对那些资源该方法返回null。 // 在Jetty实现中,使用getResource()方法相同的实现获取Resource实例,如果其getFile()返回不为null,则返回该File的canonical路径。 public String getRealPath(String path); // 返回Servlet Container的名称和版本号,其格式为:<servername>/<versionnumber>,如:Jetty/8.1.9.v20130131。 public String getServerInfo(); // ServletContext级别的初始参数操作,可以在web.xml中使用context-param定义,也可以手动设置。在get中如果找不到对应的项,返回null,在set时,如果已存在name,则返回false,并且不更新相应的值。 public String getInitParameter(String name); public Enumeration<String> getInitParameterNames(); public boolean setInitParameter(String name, String value); // ServletContext级别的属性操作,其中属性名遵循包命名规则。在set中,如果object为null表示移除该属性,如果name以存在,则会替换原有的值,如果注册了ServletContextAttributeListener,则会出发相应的attributeRemoved、attributeReplaced、attributeAdded事件。在remove中,如果name存在且被移除了,则会触发attributeRemoved事件。 // 在Jetty中使用ContextHandler中的Context内部类实现ServletContext,在ContextHandler中定义了两个相关字段:_attributes以及_contextAttributes,其中_attributes表示在Jetty内部通过ContextHandler设置的属性,而_contextAttributes表示用户设置的属性,但是在获取属性值时,两个字段的属性都会考虑进去,在移除属性时,如果是移除_attributes字段中的值,则不会触发attributeRemoved事件。 public Object getAttribute(String name); public Enumeration<String> getAttributeNames(); public void setAttribute(String name, Object object); public void removeAttribute(String name); // 返回当前Web Application在web.xml中的display-name定义的ServletContext名字,在Jetty实现中,如果该值为null,则返回context path。 public String getServletContextName(); // 该部分具体的使用可以参考:http://www.blogjava.net/yongboy/archive/2010/12/30/346209.html // 动态的向ServletContext中注册Servlet,注册的Servlet还可以通过返回的ServletRegistration继续配置,如addMapping、setInitParameter等。 // 在使用className实例话Servlet时,使用当前ServletContext相关联的ClassLoader。在创建Servlet实例时,会根据该类中定义的以下Annotation做相应的配置: // javax.servlet.annotation.ServletSecurity、javax.servlet.annotation.MultipartConfig、javax.annotation.security.RunAs、javax.annotation.security.DeclareRoles public ServletRegistration.Dynamic addServlet(String servletName, String className); public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet); public ServletRegistration.Dynamic addServlet(String servletName, Class <? extends Servlet> servletClass); // 创建给定Servlet类的Servlet实例,并且会根据该类中定义的以下Annotation做相应的配置: // javax.servlet.annotation.ServletSecurity、javax.servlet.annotation.MultipartConfig、javax.annotation.security.RunAs、javax.annotation.security.DeclareRoles // 在创建Servlet实例后,一般还要调用addServlet()方法将其注册到ServletContext中。 public <T extends Servlet> T createServlet(Class<T> clazz) throws ServletException; // 根据servletName获取ServletRegistration实例 public ServletRegistration getServletRegistration(String servletName); // 获取所有在ServletContext中注册的servletName到ServletRegistration映射的Map。所有动态注册和使用配置注册的映射。 public Map<String, ? extends ServletRegistration> getServletRegistrations(); // 动态的向ServletContext中注册Filter,注册的Filter可以通过返回的FilterRegistration进一步配置,如addMappingForUrlPatterns、setInitParameter等 public FilterRegistration.Dynamic addFilter(String filterName, String className); public FilterRegistration.Dynamic addFilter(String filterName, Filter filter); public FilterRegistration.Dynamic addFilter(String filterName, Class <? extends Filter> filterClass); // 创建给定Filter类实例的Filter实例,一般都会后继调用addFilter将该实例注册到ServletContext中。 public <T extends Filter> T createFilter(Class<T> clazz) throws ServletException; // 根据filterName获取FilterRegistration实例。 public FilterRegistration getFilterRegistration(String filterName); // 返回所有filterName到FilterRegistration映射的Map,包括所有动态注册和使用配置注册的映射。 public Map<String, ? extends FilterRegistration> getFilterRegistrations(); // 返回SessionCookieConfig实例,用于session tracking的cookie属性,多次调用该方法返回相同的实例。 public SessionCookieConfig getSessionCookieConfig(); // 设置session tracking模式,可以是URL、COOKIE、SSL。Jetty8只支持URL和COOKIE。 public void setSessionTrackingModes(Set<SessionTrackingMode> sessionTrackingModes); // 返回当前ServletContext默认支持的session tracking模式。 public Set<SessionTrackingMode> getDefaultSessionTrackingModes(); // 返回当前ServletContext目前使用的session tracking模式。 public Set<SessionTrackingMode> getEffectiveSessionTrackingModes(); // 向ServletContext动态的注册Listener,该Listener类或实例必须实现以下的一个或多个接口: // ServletContextAttributeListener、ServletRequestListener、ServletRequestAttributeListener、HttpSessionListener、HttpSessionAttributeListener}</tt> // 如果这个ServletContext传入ServletContainerInitializer的onStartup方法,那么这个Listener类或实例也可以实现ServletContextListener接口。 // 注:动态注册的ServletContextListener中的contextInitialized方法中不可以调用Servlet 3.0中定义的这些动态注册Servlet、Filter、Listener等方法,不然会抛出UnsupportedOperationException,看起来像是出于一致性、安全性或是兼容性的考虑,但是具体是什么原因一直想不出来。而且在Jetty实现中,它在注册EventListener实例是确取消了这种限制,而对注册EventListener类实例和类名确有这种限制,不知道这是Jetty的bug还是其他什么原因。。。。。 // 对于调用顺序按定义顺序来的EventListener(如ServletRequestListener、ServletContextListener、HttpSessionListener),那这个新的EventListener会添加到相应列表末尾。 public void addListener(String className); public <T extends EventListener> void addListener(T t); public void addListener(Class <? extends EventListener> listenerClass); // 创建clazz对应的EventListener实例,一般这个新创建的EventListener会之后注册到ServletContext中。 public <T extends EventListener> T createListener(Class<T> clazz) throws ServletException; // 返回web.xml和web-fragment.xml配置文件中定义<jsp-config>的汇总,或返回null如果没有相关配置。看起来像Jetty并没有实现该方法。 public JspConfigDescriptor getJspConfigDescriptor(); // 返回当前Web Application对应的ClassLoader实例。 public ClassLoader getClassLoader(); // 定义角色名。角色名在ServletRegistration.Dynamic.setServletSecurity()和ServletRegistration.Dynamic.setRunAsRole()中默认定义,因而不需要重新使用这个方法定义。 public void declareRoles(String roleNames); } ServletContext初始化 ServletContext的初始化从ContextHandler的doStart()方法开始,在其startContext()方法快结束时,会调用注册的ServletContextListener中的contextInitialized()方法,因而这里是用户对ServletContext初始化时做一些自定义逻辑的扩展点。 在Servlet 3.0中还引入了ServletContainerInitializer接口,它定义了onStartup()方法,该方法会在WebAppContext中的startContext方法中的configure方法中通过ContainerInitializerConfiguration.configure()中被调用,该方法的调用要早于所有ServletContextListener .contextInitialized()事件的触发。 public interface ServletContainerInitializer { // 当ServletContext对应的Web Application初始化时,该方法会被调用。其中c参数是所有继承、实现在ServletContainerInitializer实现类定义的HandlesTypes注解中定义的类, // 如果该注解定义的类数组中有注解,那么c参数还包含所有存在这个注解的类。如果实现ServletContainerInitializer接口的类在WEB-INF/lib的jar包中绑定, // 那么该方法只会在对应的Web Application初始化被调用一次,如果实现ServletContainerInitializer接口的类在WEB-INF/lib以外定义,但是还可以被Servlet Container找到, // 那么意味这这个jar包是多个Web Application共享的,因而该方法会在每个Web Application初始化时被调用。如果ServletContainerInitializer实现类没有定义HandlesTypes注解, // 那么c参数为null。ServletContainerInitializer实现类的查找使用运行时service provider机制,然而对于定义在fragment jar包中的ServletContainerInitializer实现类, // 但是该jar在absolute ordering中被exclude了,那么该jar包会被忽略,即在web.xml中的absolute-ordering中没有包含相应的fragment名。 // 在查找满足HandlesTypes注解中定义的类数组的类实例集合时,由于有些JAR包是可选的,因而在加载Class时有时会遇到问题,此时Servlet Container可以选择忽略该错误, // 但是需要提供配置已让Servlet Container决定是否要将这些错误打印到日志文件中。 public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException; } 要自定义ServletContainerInitializer逻辑,首先需要META-INF/services/目录下建立一个javax.servlet.ServletContainerInitializer文件,在该文件内部写入用户在定义的ServletContainerInitializer类,如:x.y.z.MyServletContainerInitializer,该MyServletContainerInitializer类必须实现ServletContainerInitializer接口,如果它有一些特定感兴趣的类,可以向其定义HandlesTypes注解,该注解的值可以是类实例、接口实例、注解实例,它表示所有继承自注解中定义的类、实现注解中定义的接口、有注解中定义的注解注解的类(注解在类、字段、方法中)都会收集成一个Set<Class<?>>类型,并传入onStartup()方法中,如果没有HandlesTypes注解,其onStartup()中Set<Class<?>>参数为null。 在Jetty实现中,AnnotationConfiguration会查找到所有jar包中在META-INF/services/javax.servlet.ServletContainerInitializer中定义的ServletContainerInitializer(排除那些被absolute-ordering排除在外的fragment jar包中的定义),使用这些查找到的ServletContainerInitializer创建ContainerInitializer实例,使用HandlesTypes注解中定义的Class数组初始化InterestedTypes字段,如果这个Class数组中有注解类型,则将所有存在这个注解类型的类(这个注解可以注解在该类的类、字段、方法中)添加到ContainerInitializer实例中的_annotatedTypeNames集合中(使用ContainerInitializerAnnotationHandler实现);最后将他们注册到Context的一个属性中。同时在AnnotationConfiguration中还会向Context中注册一个属性,其值是Map,它包含了所有类对应的其子类或实现类。然后在ContainerInitializerConfiguration中,对每个在Context中注册的ContainerInitializer实例,对所有注册的_annotatedTypeNames,将该类以及该类的子类、实现类注册到_applicableTypeNames集合中;对所有注册的非注解类型的_interestedTypes,将其所有的子类、实现类注册到_applicableTypeNames集合中(在Jetty当前版本的实现中没有包含_interestedTypes中的类实例本身,在ServletContainerInitializer的注释中确实也没有说明要包含这些类本身,感觉这个有点不合理。。。);最后调用ContainerInitializer中的callStartup()方法,它加载_applicableTypeNames集合中的所有类,并将其传入ServletContainerInitializer的onStartup()方法中(这里没有根据规范忽略不能加载成功的类实例)。
概述 在很多框架的设计中,都有类似Channel链的设计,类似Decorator模式或Chain Of Responsibility模式,可以向这个Channel注册不同的Handler,用户请求可以穿越这个由多个Handler组成的Channel,执行响应的切面逻辑,在最后一个Handler或者另一个Processor处理用于自定义的业务逻辑,然后生成的响应可以逆着方向从这个Channel回来。Structs2中的Interceptor、Servlet中Filter都采用这种设计。这种设计为面向切面编程提供了遍历,然而目前的Handler设计更多的像是Chain Of Responsibility模式,它的处理逻辑只能从链头走到链尾,而没有返回的路程。引入ScopedHandler的目的就是用于解决这个问题。实现 Structs2中Interceptor实现使用传入ActionInvaction来调用Channel中的后继Interceptor: public class MyInterceptor extends AbstractInterceptor { @Override public String intercept(ActionInvocation ai) throws Exception { try { // Add user customized logic here when request come into the channel return ai.invoke(); } finally { // Add user customized logic here when response pass out across the channel } } } 类似的,Servlet中的Filter实现也是使用FilterChain来调用Channel中后继的Filter: public class MyFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { // Add user customized logic here when request come into the channel chain.doFilter(request, response); } finally { // Add user customized logic here when response pass out across the channel } } } ScopedHandler采用了不同的实现方式,首先它继承自HandlerWrapper,因而它使用HandlerWrapper中的Handler字段来构建Handler链,然而并不是所有的Handler都是ScopedHandler,因而ScopedHandler内部还定义了_nextScope字段用于创建在这条Handler链表中的ScopedHandler链表,以及_outerScope字段用于将自己始终和这个ScopedHandler链表中的最外层ScopedHandler相连,对这个ScopedHandler的最外层列表,其_outScope字段为null。而ScopedHandler要实现的行为是,假设A、B、C都是ScopedHandler,并且它们组成链表:A->B->C,那么当调用A.handle()方法时的调用堆栈是:A.handle()|-A.doScope()|--B.doScope()|----C.doScope()|-----A.doHandle()|------B.doHandle()|-------C.doHandle()而如果A、B是ScopedHandler,X、Y是其他的Handler,并且它们组成链表:A->X->B->Y,那么当调用A.handle()方法时的调用栈是:A.handle()|-A.doScope()|--B.doScope()|---A.doHandle()|----X.handle()|-----B.doHandle()|------Y.handle()这种行为主要用于Servlet框架的实现,它可以保证在doScope()方法中做一些初始化工作,并且配置环境,而在后继调用中都可以使用这些配置好的环境,并且doHandle()的顺序还是使用原来定义的Handler链表顺序,即使有些Handler并不是ScopedHandler。在实现中,其Handler链表由HandlerWrapper构建,在doStart()方法中计算_nextScope字段以及_outerScope字段,在handle()方法中,如果_outerScope为null,则调用doScope()方法,否则调用doHandle()方法: @Override public final void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { if (_outerScope==null) doScope(target,baseRequest,request, response); else doHandle(target,baseRequest,request, response); } 在执行完doScope()方法后,调用nextScope()方法,该方法顺着_nextScope链表走,直到尽头,后调用doHandle()方法: public final void nextScope(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { if (_nextScope!=null) _nextScope.doScope(target,baseRequest,request, response); else if (_outerScope!=null) _outerScope.doHandle(target,baseRequest,request, response); else doHandle(target,baseRequest,request, response); } 而doHandle()方法在完成是调用nextHandle()方法,它也沿着_nextScope链表走,只要_nextScope和_handler相同,则调用其doHandle()方法,但是如果_nextScope和_handler不同,则调用_handler中的handle()方法,用于处理在ScopedHandler链表中插入非ScopedHandler的情况: public final void nextHandle(String target, final Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { if (_nextScope!=null && _nextScope==_handler) _nextScope.doHandle(target,baseRequest,request, response); else if (_handler!=null) _handler.handle(target,baseRequest, request, response); } 使用 在ScopedHandler的测试用例中给出了一个非常好的例子。首先有一个TestHandler继承自ScopedHandler: private class TestHandler extends ScopedHandler { private final String _name; private TestHandler(String name) { _name=name; } @Override public void doScope(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { try { _history.append(">S").append(_name); super.nextScope(target,baseRequest,request, response); } finally { _history.append("<S").append(_name); } } @Override public void doHandle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { try { _history.append(">W").append(_name); super.nextHandle(target,baseRequest,request,response); } finally { _history.append("<W").append(_name); } } } 然后有非ScopedHandler的实现: private class OtherHandler extends HandlerWrapper { private final String _name; private OtherHandler(String name) { _name=name; } @Override public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { try { _history.append(">H").append(_name); super.handle(target,baseRequest,request, response); } finally { _history.append("<H").append(_name); } } } 查看一下Test Case的执行结果: @Test public void testDouble() throws Exception { TestHandler handler0 = new TestHandler("0"); OtherHandler handlerA = new OtherHandler("A"); TestHandler handler1 = new TestHandler("1"); OtherHandler handlerB = new OtherHandler("B"); handler0.setHandler(handlerA); handlerA.setHandler(handler1); handler1.setHandler(handlerB); handler0.start(); handler0.handle("target",null,null,null); handler0.stop(); String history=_history.toString(); System.err.println(history); assertEquals(">S0>S1>W0>HA>W1>HB<HB<W1<HA<W0<S1<S0",history); }
Handler概述 Handler是Jetty中的核心接口,它用于处理所有连接以外的逻辑,比如对Servlet框架的实现,以及用户自定义的Handler等,它继承自LifeCycle和Destroyable接口,只有一个主要方法:handle,包含Request和Response实例。在深入Jetty源码之Connection中有写道在HttpConnection的handleRequest()方法中会最终调用Server的handle()或handleAsync()方法,并且传入HttpConnection自身作为参数以处理后续逻辑,在这里Server作为Handler的容器,在Server中以HttpConnection为参数的handle()和handleAsync()方法中,会调用向这个Handler容器中注册的所有Handler的handle()方法。即在使用Jetty时,我们首先要向Server注册相应的Handler实例。Handler接口定义 Handler的接口定义如下: public interface Handler extends LifeCycle, Destroyable { // 处理一个HTTP请求,并最终将响应写入Response中。不同的实现有不同的功能和逻辑,如WebAppContext实现一个Servlet容器, // 而ErrorHandler则向Response中写入一个包含错误码和原因的HTML页面。 // 关于参数: // target表示Request的目标,它可以时一个URI或一个名字。即Request中的pathInfo字段。 // baseRequest表示在HttpConnection中创建并解析的最初的Request,它没有被包装。 // request表示一个HttpServletRequest,它可以同baseRequest相同的实例,也可以是一个经过包装后的HttpServletRequest。 // response表示一个HttpServletResponse,它可以是经过包装的HttpServletResponse或在HttpConnection创建的最原始的HttpServletResponse。 public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException; public void setServer(Server server); public Server getServer(); public void destroy(); } Handler类图 AbstractHandler AbstractHandler继承自AggregateLifeCycle并实现了Handler接口,是基本上所有Handler的基类。它的实现也非常简单,它定义了Server成员,并实现了getServer和setServer方法,在setServer实现中,如果已存在的Server引用和新设置的Server不同,则先将自身从已存在的Server的Container中移除,然后将自身添加到新的Server的Container中,并更新内部对Server的引用实例。在destroy方法中,也会将自身从Server引用实例的Container中移除。DefaultHandler DefaultHandler直接继承自AbstractHandler,它可以用于Handler链表的末尾Handler,用于处理所有不能被其他Handler处理的请求:/favicon.ico -> 显示jetty图标,/ -> 显示404错误页面,并列出所有可用的ContextPath,任何其他请求显示404错误页面。图标的显示和可用ContextPath的列表显示都是可配的。DumpHandler DumpHandler直接继承自AbstractHandler,用于测试和调试,显示Request消息内容。它显示的信息有PathInfo、ContentType、CharacterEncoding、RequestLine、Headers、Parameters、CookieName、Cookies、Attributes、Content等。ErrorHandler ErrorHandler直接继承自AbstractHandler,用于处理错误页面,使用ContextHandler.setErrorHandler()或Server.addBean()注册。它显示出错代码、原因以及更详细的信息。其中异常栈从Request的javax.servlet.errro.exception中获取。ErrorPageErrorHandler ErrorPageErrorHandler继承自ErrorHandler,它可以配置不同的Exception和Response Status Code到不同的页面。Exception类型从Request中的javax.servlet.error.exception_type或javax.servlet.error.exception属性中获取,Response Status Code从Request中的javax.servlet.error.status_code属性中获取。使用Exception类型、Response Status Code、Response Status Code Range从_errorPages中查找在web.xml文件中注册的映射,如果有找到,则使用ServletContext中的RequestDispatcher将当前的Request、Response派发的其error处理逻辑;否则使用ErrorHandler中的逻辑。ResourceHandler ResourceHandler直接继承自AbstractHandler,用于处理静态资源以及If-Modified-Since头。使用PathInfo以及注册的或ContextHandler中的BaseResource作为Base查找Resource,如果找不到并且请求的类型是/jetty-stylesheet.css资源,则查找注册的或默认的stylesheet资源;如果查找到的资源是目录,如果URL不是以"/"结尾,则重定向到"URL/",否则查找是否有welcome list中配置的页面存在以显示,否则列出文件列表或者显示403 Forbidden页面;对If-Modified-Since请求头,如果资源存在LastModified属性,并且比请求中设置的值要小或相等(以秒为单位),返回304 Not Modified;根据文件名或PathInfo以及注册的MineTypes信息设置ContentType,设置Content-Length、Cache-Control、Last-Modified等响应头,将Resource内容写入Response中。顺便提及:Resource是Jetty中对静态资源的抽象,其子类有URLResource、FileResource、JarResource、JarFileResource、BadResource等。类似Spring中对Resource抽象,不详述。AbstractHandlerContainer AbstractHandlerContainer继承自AbstractHandler,并实现了HandlerContainer接口。HandlerContainer接口定义如下: public interface HandlerContainer extends LifeCycle { // 返回当前Handler包含的所有Handler public Handler[] getHandlers(); // 返回当前Handler和其所有子Handler包含的所有Handler public Handler[] getChildHandlers(); // 返回所有当前Handler和其所有子Handler包含的所有Handler中类型为指定类型的Handler public Handler[] getChildHandlersByClass(Class<?> byclass); // 返回第一个所有当前Handler和其所有子Handler包含的所有Handler中类型为指定类型的Handler public <T extends Handler> T getChildHandlerByClass(Class<T> byclass); } AbstractHandlerContainer主要实现了两个方法: // 将所有当前handler或其子handler中类型为byClass的Handler添加到list中,并返回该list实例protected Object expandHandler(Handler handler, Object list, Class<Handler> byClass);// 从root的HandlerContainer中找到handler所在的HandlerContainer实例,并且该HandlerContainer必须属于type类型public static <T extends HandlerContainer> T findContainerOf(HandlerContainer root,Class<T>type, Handler handler); HandlerCollection HandlerCollection继承自AbstractHandlerContainer,它使用Handler数组作为容器来存储Handler,并且可已配置是否在启动后还能修改这个容器,以及启动是是否并行启动: private final boolean _mutableWhenRunning; private volatile Handler[] _handlers; private boolean _parallelStart=false; 在handle()方法实现中,遍历数组中所有的Handler,调用其handle()方法。在setHandlers()和setServer()方法实现中,需要生成并分发handler的Relationship发生变化的事件给在Server中的Container中注册的Listener,以及更新相应Handler中对Server的引用。HandlerList 它继承自HandlerCollection,只是重写了handle()方法的逻辑,即在HandlerList中,它遍历整个Handlers 数组直到有异常发生或baseRequest的isHandled()返回true(什么时候baseRequest的handled属性会被设置为true呢?在发送请求之前或相应状态码被设置);而HandlerCollection则会遍历整个Handlers数组直到一个RuntimeException发生或IOException发生或整个Handler数组遍历完成,如果中途有出现其他异常最后统一抛出。ContextHandlerCollection ContextHandlerCollection继承自HandlerCollection,使用PathMap存储请求URI到ContextHandler的映射。HotSwapHandler HotSwapHandler继承自AbstractHandlerContainer,支持动态的替换内部的Handler(即在设置Handler时,如果当前Handler已经启动,则并立即启动新设置的Handler,并停止原来的Handler)。它使用Composite模式,内部只是使用一个Handler来保存一个集合的Handler,因而如果要注册多个Handler,则这个Handler的类型需要HandlerContainer。HandlerWrapper HandlerWrapper继承自AbstractHandlerContainer,它类似Composite模式,使用一个Handler本身来表达一个HandlerContainer,因而同HotSwapHandler,它只是包含一个Handler字段用与表示、存储Handler集合,并且其实现也和HotSwapHandler类似,所不同的是它不会动态的启动新设置的Handler,即它只是一个Handler的Wrapper。 ConnectHandler ConnectHandler继承自HandlerWrapper,它实现了一个代理服务器。DebugHandler DebugHandler继承自HandlerWrapper,用于测试时使用,在logs 目录中yyyy_mm_dd.debug.log文件的形式纪录请求的时间和URL以及响应的时间和响应状态码,并设置线程名为请求URL。IPAccessHandler IPAccessHandler继承自HandlerWrapper,用于添加可以使用或需要组织的IP列表,并返回403 Forbidden响应。RequestLogHandler RequestLogHandler继承自HandlerWrapper,在所有handler处理结束后,将Request和Response使用RequestLog类打印到日志中。StatisticsHandler StatisticsHandler继承自HandlerWrapper,用于纪录一些统计信息,如请求数、请求时间、dispatched数与时间、suspended数、resume数、expire数、1xx到5xx的响应数、响应总的字节数等。
Connector概述 Connector是Jetty中可以直接接受客户端连接的抽象,一个Connector监听Jetty服务器的一个端口,所有客户端的连接请求首先通过该端口,而后由操作系统分配一个新的端口(Socket)与客户端进行数据通信(先握手,然后建立连接,但是使用不同的端口)。不同的Connector实现可以使用不同的底层结构,如Socket Connector、NIO Connector等,也可以使用不同的协议,如Ssl Connector、AJP Connector,从而在不同的Connector中配置不同的EndPoint和Connection。EndPoint用于连接(Socket)的读写数据(参见深入Jetty源码之EndPoint),Connection用于关联Request、Response、EndPoint、Server,并将解析出来的Request、Response传递给在Server中注册的Handler来处理(参见深入Jetty源码之Connection)。在Jetty中Connector的有:SocketConnector、SslSocketConnector、Ajp13SocketConnector、BlockingChannelConnector、SelectChannelConnector、SslSelectChannelConnector、LocalConnector、NestedConnector等,其类图如下。Connector类图 Connector接口 首先Connector实现了LifeCycle接口,在启动Jetty服务器时,会调用其的start方法,用于初始化Connector内部状态,并打开Connector以接受客户端的请求(调用open方法);而在停止Jetty服务器时会调用其stop方法,以关闭Connector以及内部元件(如Connection等)以及做一些清理工作。因而open、close方法是Connector中用于处理生命周期的方法;对每个Connector都有name字段用于标记该Connector,默认值为:hostname:port;Connector中还有Server的引用,可以从中获取ThreadPool,并作为Handler的容器被使用(在创建HttpConnection时,Server实例作为构造函数参数传入,并在handleRequest()方法中将解析出来的Request、Response传递给Server注册的Handler);Connector还定义了一些用于配置当前Connector的方法,如Buffer Size、Max Idle Time、Low Resource Max Idle Time,以及一些统计信息,如当前Connector总共处理过的请求数、总共处理过的连接数、当前打开的连接数等信息。Connector的接口定义如下: public interface Connector extends LifeCycle { // Connector名字,默认值hostname:port String getName(); // 打开当前Connector void open() throws IOException; // 关闭当前Connector void close() throws IOException; // 对Server的引用 void setServer(Server server); Server getServer(); // 在处理请求消息头时使用的Buffer大小 int getRequestHeaderSize(); void setRequestHeaderSize(int size); // 在处理响应消息头时使用的Buffer大小 int getResponseHeaderSize(); void setResponseHeaderSize(int size); // 在处理请求消息内容时使用的Buffer大小 int getRequestBufferSize(); void setRequestBufferSize(int requestBufferSize); // 在处理响应消息内容使用的Buffer大小 int getResponseBufferSize(); void setResponseBufferSize(int responseBufferSize); // 在处理请求消息时使用的Buffer工厂 Buffers getRequestBuffers(); // 在处理响应消息时使用的Buffer工厂 Buffers getResponseBuffers(); // User Data Constraint的配置可以是None、Integral、Confidential,对这三种值的解释: // None:A value of NONE means that the application does not require any transport guarantees. // Integral:A value of INTEGRAL means that the application requires the data sent between the client and server to be sent in such a way that it can't be changed in transit. // Confidential:A value of CONFIDENTIAL means that the application requires the data to be transmitted in a fashion that prevents other entities from observing the contents of the transmission on. // In most cases, the presence of the INTEGRAL or CONFIDENTIAL flag indicates that the use of SSL is required.参考:这里 // 如果配置了user-data-constraint为Integral或confidential表示所有相应请求都会重定向到使用Integral/Confidential Schema/Port构建的新的URL中。 int getIntegralPort(); String getIntegralScheme(); boolean isIntegral(Request request); int getConfidentialPort(); String getConfidentialScheme(); boolean isConfidential(Request request); // 在将HttpConnection交给Server中的Handlers处理前,根据当前Connector,自定义一些EndPoint和Request的配置,如设置EndPoint的MaxIdleTime,Request的timestamp, // 清除SelectChannelEndPoint中的idleTimestamp,检查forward头等。 void customize(EndPoint endpoint, Request request) throws IOException; // 主要用于SelectChannelConnector中,重置SelectChannelEndPoint中的idleTimestamp,即重新计时idle的时间。 // 它在每个Request处理结束,EndPoint还未关闭,并且当前连接属于keep-alive类型的时候被调用。 void persist(EndPoint endpoint) throws IOException; // 底层的链接实例,如ServerSocket、ServerSocketChannel等。 Object getConnection(); //是否对"X-Forwarded-For"头进行DNS名字解析 boolean getResolveNames(); // 当前Connector绑定的主机名、端口号等。貌似在Connector的实现中没有一个默认的主机名。 // 由于端口号可以设置为0,表示由操作系统随机的分配一个还没有被使用的端口,因而这里由LocalPort用于存储Connector实际上绑定的端口号; // 其中-1表示这个Connector还未开启,-2表示Connector已经关闭。 String getHost(); void setHost(String hostname); void setPort(int port); int getPort(); int getLocalPort(); // Socket的最大空闲时间,以及在资源比较少(如线程池中的任务数比最大可用线程数要多)的情况下的最大空闲时间,当空闲时间超过这个时间后关闭当前连接(Socket)。 int getMaxIdleTime(); void setMaxIdleTime(int ms); int getLowResourceMaxIdleTime(); void setLowResourceMaxIdleTime(int ms); // 是否当前Connector处于LowResources状态,即线程池中的任务数比最大可用线程数要多 public boolean isLowResources(); /* ------------------------以下是一些获取和当前Connector相关的统计信息------------------------------------ */ // 打开或关闭统计功能 public void setStatsOn(boolean on); // 统计功能的开闭状态 public boolean getStatsOn(); // 重置统计数据,以及统计开始时间 public void statsReset(); // 统计信息的开启时间戳 public long getStatsOnMs(); // 当前Connector处理的请求数 public int getRequests(); // 当前Connector接收到过的连接数 public int getConnections() ; // 当前Connector所有当前还处于打开状态的连接数 public int getConnectionsOpen() ; // 当前Connector历史上同时处于打开状态的最大连接数 public int getConnectionsOpenMax() ; // 当前Connector所有连接的持续时间总和 public long getConnectionsDurationTotal(); // 当前Connector的最长连接持续时间 public long getConnectionsDurationMax(); // 当前Connector平均连接持续时间 public double getConnectionsDurationMean() ; // 当前Connector所有连接持续时间的标准偏差 public double getConnectionsDurationStdDev() ; // 当前Connector的所有连接的平均请求数 public double getConnectionsRequestsMean() ; // 当前Connector的所有连接的请求数标准偏差 public double getConnectionsRequestsStdDev() ; // 当前Connector的所有连接的最大请求数 public int getConnectionsRequestsMax(); } AbstractConnector实现 Jetty中所有的Connector都继承自AbstractConnector,而它自身继承自HttpBuffers,HttpBuffers包含了Request、Response的Buffer工厂的创建以及相应Size的配置。AbstractConnector还包含了Name、Server、maxIdleTime以及一些统计信息的引用,用于实现Connector接口中的方法,只是一些Get、Set方法,不详述。除了Connector接口相关的配置,AbstractConnector还为定义了两个字段:_acceptQueueSize用于表示ServerSocket 或ServerSocketChannel中最大可等待的请求数,_acceptors用于表示用于调用ServerSocket或ServerSocketChannel中accept方法的线程数,建议这个数字小于或等于可用处理器的2倍。在AbstractConnector中还定义了Acceptor内部类,它实现了Runnable接口,在其run方法实现中,它将自己的Thread实例赋值给AbstractConnector中的_acceptorThread数组中acceptor号对应的bucket,并更新线程名为:<name> Acceptor<index> <Connector.toString>。然后根据配置的_acceptorPriorityOffset设置当前线程的priority。只要当前Connector处于Running状态,并且底层链接实例不为null,不断的调用accept方法()。在退出的finally语句快中清理_acceptorThread数组中相应的Bucket值为null,并将线程原来的Name、Priority设置回来。AbstractConnector实现了doStart()方法,它首先保证Server实例的存在;然后打开当前Connector(调用其open()方法),并调用父类的doStart方法(这里是HttpBuffers,用于初始化对应的Buffers);如果没有自定义的ThreadPool,则从Server中获取ThreadPool;最后根据acceptors的值创建_acceptorThread数组,将acceptors个Acceptor实例dispatch给ThreadPool。在doStop方法实现中,它首先调用close方法,然后对非Server中的ThreadPool调用其stop方法,再调用父类的doStop方法清理Buffers的引用,最后遍历_acceptorThread数据,调用每个Thread的interrupt方法。在各个子类的accept方法实现中,他们在获取客户端过来的Socket连接后,都会对该Socket做一些配置,即调用AbstractConnector的configure方法,它首先设置Socket的TCP_NODELAY为true,即禁用Nagle算法(关于禁用的理由可以参考:http://jerrypeng.me/2013/08/mythical-40ms-delay-and-tcp-nodelay/#sec-4-2,简单的,如果该值为false,则TCP的数据包要么达到TCP Segment Size,要么收到一个Ack,才会发送出去,即有Delay);然后如果设置了_soLingerTime,则开启Socket中SO_LINGER选项,否则,关闭该选项(SO_LINGER选项用于控制关闭一个Socket的行为,如果开启了该选项,则在关闭Socket时会等待_soLingerTime时间,此时如果有数据还未发送完,则会发送这些数据;如果关闭了该选项,则Socket的关闭会立即返回,此时也有可能继续发送未发送完成的数据,具体参考:http://blog.csdn.net/factor2000/article/details/3929816)。在ServerSocket和ServerSocketChannel中还有一个SO_REUSEADDR的配置,一般来说当一个端口被释放后会等待两分钟再被使用,此时如果重启服务器,可能会导致启动时的绑定错误,设置该值可以让端口释放后可以立即被使用(具体参考:http://www.cnblogs.com/mydomain/archive/2011/08/23/2150567.html)。在AbstractConnector中可以使用setReuseAddress方法来配置,默认该值设置为true。AbstractConnector中还实现了customize方法,它在forwarded设置为true的情况下设置相应的attribute:javax.servlet.request.cipher_suite, javax.servlet.request.ssl_session_id,以及Request中对应Host、Server等头信息。这个逻辑具体含义目前还不是很了解。。。。最后关于统计数据的更新方法,AbstractConnector定义了如下方法: protected void connectionOpened(Connection connection)protected void connectionUpgraded(Connection oldConnection, Connection newConnection)protected void connectionClosed(Connection connection) SocketConnector实现 有了AbstractConnector的实现,SocketConnector的实现就变的非常简单了,它保存了一个EndPoint的Set,表示所有在这个Connector下正在使用的EndPoint,然后是ServerSocket,在open方法中创建,并在getConnection()方法中返回,还有一个localPort字段,当ServerSocket被创建时从ServerSocket实例中获取,并在getLocalPort()方法中返回。在close方法中关闭ServerSocket,并设置localPort为-2;在accept方法中,调用ServerSocket的accept方法,返回一个Socket,调用configure方法对新创建的Socket做一些基本的配置,然后使用该Socket创建ConnectorEndPoint,并调用其dispatch方法;在customize方法中,在调用AbstractConnector的customize方法的同时还设置ConnectorEndPoint的MaxIdleTime,即设置Socket的SO_TIMEOUT选项,用于配置该Socket的空闲可等待时间;在doStart中会先清理ConnectorEndPoint的集合,而在doStop中会关闭所有还处于打开状态的ConnectorEndPoint。SelectChannelConnector实现 SelectChannelConnector内部使用ServerSocketChannel,在open方法中创建ServerSocketChannel,配置其为非blocking模式,并设置localPort值;在accept方法中调用ServerSocket的accept方法获得一个SocketChannel,配置该Channel为非blocking模式,调用AbstractChannel的configure方法做相应Socket配置,最后将该SocketChannel注册给ConnectorSelectManager;在doStart方法中,它会初始化ConnectorSelectManager的SelectSets值为acceptors值、MaxIdleTime、LowResourceConnections、LowResourcesMaxIdleTime等值,并启动该Manager,并dispatch acceptors个线程,不断的调用Manager的doSelect方法;在close方法中会先stop ConnectorSelectManager,然后关闭ServerSocketChannel,设置localPort为-2;在customize方法中会清除SelectChannelEndPoint的idleTimestamp,重置其MaxIdleTime以及重置Request中的timestamp的值;在persist方法中会重置SelectChannelEndPoint中idleTimestamp的值。BlockingChannelConnector实现 BlockingChannelConnector实现类似SocketConnector,不同的是它使用ServerSocketChannel,并且其EndPoint为BlockingChannelEndPoint。所不同的是它需要在doStart方法中启动一个线程不断的检查所有还在connections集合中的BlockingChannelEndPoint是否已经超时,每400ms检查一次,如果超时则关闭该EndPoint。其他的Connector的实现都比较类似,而SSL相关的Connector需要也只是加入了SSL相关的逻辑,这里不再赘述。
基于抽象和聚合原则,Jetty中需要一个单独的类来专门处理HTTP响应消息和请求消息的生成和发送,Jetty的作者将该抽象(接口)命名为Generator,它有两个实现类:HttpGenerator和NestedGenerator。其类图如下:Generator接口 HTTP请求消息分为请求行、消息报头、请求正文三部分,HTTP响应消息分为状态行、消息报头、消息正文。在HTTP请求消息和响应消息格式的唯一区别是请求行和状态行,因而只需要将这一行的内容区分开来,其他的可以共享逻辑。请求行和响应行设置请求行包括请求方法、URI、HTTP协议版本,响应行包括HTTP协议版本、状态码、状态短语,因而在Generator接口中定义了各自的设置方法: // 设置_method、_uri字段,如果当前_version是HTTP/0.9,则_noContent置为true void setRequest(String method, String uri); // 设置_status、_reason字段,对_reason字段,将所有'\r', '\n'替换成空格(' ')。清除_method字段。如果Generator已经开始生成效应消息,则不可再调用该方法。 void setResponse(int status, String reason); // 设置HTTP协议版本,这里的值9表示HTTP/0.9, 10表示HTTP/1.0,11表示HTTP/1.1。对HTTP/0.9的请求消息,不能包含请求内容。如果Generator已经开始生成效应消息,则不可再调用该方法。 void setVersion(int version); HTTP消息报头设置在Generator中,通过completeHeader中的HttpFields参数传入HTTP消息报头,而其中的allContentAdded参数用于检查并设置_last字段状态。 //通过传入的HttpFields参数设置HTTP消息报头,allContentAdded参数用于检查并设置_last字段状态。public void completeHeader(HttpFields fields, boolean allContentAdded) throws IOExceptionvoid setDate(Buffer timeStampBuffer);void setSendServerVersion(boolean sendServerVersion);void setContentLength(long length);void setPersistent(boolean persistent); 在该方法的实现中:1. 向_header缓存中写入请求行或响应行(对HTTP/0.9的特殊处理不详述)。对HTTP响应消息,如果状态码是[100-200)、204、304,这些响应消息不能有响应消息体。对状态码是100的响应消息还不能有其他消息头。2. 如果设置了_date值(状态码在200及以上,这里貌似木有考虑请求消息),在消息头中包含"Date"消息头。3. 对fields参数中的所有消息头,依次写入到_header缓存中(消息头名移除'\r', '\n', ':'字符,消息头值移除'\r', '\n'字符)。对"Content-Length"头,记录_contentLength字段;对"Content-Type"为"multipart/byteranges"头,设置_contentLength为SLEF_DEFINING_CONTENT;对"Transfer-Encoding"头,并且_contentLength的值为CHUNKED_CONTENT,则添加"Transfer-Encoding: chunked\r\n"头(或用户自定义的以"chunked"开头的值);对"Server"头,且设置了"sendServerVersion"字段,则添加"Server"头;对"Connection"头,在请求消息中直接设置该头,同时更新"keep_alive"的值("close"->keep_alive=false, "keep-alive"->keep_alive=true),在响应消息中,更新"keep_alive"和"_persistent"的值,对"upgrade"值,直接添加,而对其他值,根据"keep_alive"和"_persistent"的值以及HTTP版本号添加"close"或"keep-alive"的值,以及用户自定义的值;根据当前_contentLength值设置"Content-Length"头;最后添加"\r\n"到_header缓存表示消息头结束。4. 将_state状态从STATE_HEADER更新到STATE_CONTENT。HTTP消息体设置Generator中有两个方法用于向其添加HTTP消息体: void addContent(Buffer content, boolean last) throws IOException;boolean addContent(byte b) throws IOException; 这两个方法的实现:1. 如果当前已存在未刷新的_content内容或者_contentLength为CHUNKED_CONTENT,则先刷新缓存。2. 更新_content字段和_contentWritten字段。3. 将_content的值写入_buffer字段中。在刷新缓存时:1. 准备Buffer:将_content值写入_buffer中,并清除_content引用;如果_contentLength为CHUNKED_CONTENT,设置_bufferChunk为true,并且对chunked内容,先写入16进制的size,紧跟"\r\n",然后是正真的内容;对最后一个chunk,添加"\r\n\r\n"。2. 然后根据_header, _buffer, _content状态,将它们中的内容写入到Endpoint中。3. 如果当前状态是STATE_FLUSHING,则将_state状态置为STATE_END。HTTP消息完成生成Generator调用complete方法表示生成已经完成: public void complete() throws IOException 在该方法中,它将_state状态设置为STATE_FLUSHING,并刷新缓存。Generator中的其他方法在Generator/HttpGenerator中还有一些发送响应消息的方法: void sendError(int code, String reason, String content, boolean close) throws IOException;public void send1xx(int code) throws IOExceptionpublic void sendResponse(Buffer response) throws IOException 其中sendError是Generator接口中的方法,它是一个工具方法用于一次将一个HTTP响应消息写入到Endpoint中: public void sendError(int code, String reason, String content, boolean close) throws IOException { if (close) _persistent=false; if (!isCommitted()) { setResponse(code, reason); if (content != null) { completeHeader(null, false); addContent(new View(new ByteArrayBuffer(content)), Generator.LAST); } else { completeHeader(null, true); } complete(); } } sendResponse方法是HttpGenerator中的方法,它将response参数直接作为响应消息体,并设置_state为STATE_FLUSHING,是一个工具方法。send1xx方法是HttpGenerator中的方法,它将1xx的响应消息直接写入到Endpoint中。
概述 Jetty作为HTTP服务器,服务器和客户端以HTTP协议格式通信,Jetty使用Parser(HttpParser)来抽象HTTP请求消息和响应消息的解析类引擎。在HttpParser实现中,它采用有限状态机算法:定义了21中状态,每解析一个字符,就根据当前的状态做相应的处理,并决定是否要迁移到下一个状态,直到HTTP请求消息或响应消息解析完成。HttpParser采用事件驱动机制,它定义了EventHandler类,用户可以通过注册的EventHandler实例获取相应的消息:请求行解析完成(startRequest)、响应行解析完成(startResponse)、每个消息头解析完成(parsedHeader)、所有消息头解析完成(headerComplete)、消息内容解析完成(content)、整个消息(请求消息或响应消息)解析完成(messageComplete)。Parser接口定义 public interface Parser { // 重置Parser的内部状态,以重用Parser实例,如果returnBuffers为true,则将内部Buffer回收。 void reset(boolean returnBuffers); // 当前Parser是否已经解析完成。 boolean isComplete(); // 当前Parser是否处于Idle状态,它还处于初始状态,解析还没有开始。 boolean isIdle(); // 内部Buffer是否还有内容没有解析。 boolean isMoreInBuffer() throws IOException; // 开始解析已接收到的消息,返回-1表示解析到流的末位,0表示没有该次调用没有解析任何消息,>0表示这次调用总共解析过的字节数。 int parseAvailable() throws IOException; } EventHandler定义 public static abstract class EventHandler { // 消息内容解析完成 public abstract void content(Buffer ref) throws IOException; // 所有消息头解析完成 public void headerComplete() throws IOException { } // 整个消息(请求消息或响应消息)解析完成 public void messageComplete(long contentLength) throws IOException { } // 每个消息头解析完成 public void parsedHeader(Buffer name, Buffer value) throws IOException { } // 请求行解析完成 public abstract void startRequest(Buffer method, Buffer url, Buffer version) throws IOException; // 响应行解析完成 public abstract void startResponse(Buffer version, int status, Buffer reason) throws IOException; } EventHandler的实现类 在HttpConnection的内部类RequestHandler类实现了HttpParser.EventHandler类,以作为HttpParser使用时的回调。startRequest:重置当前HttpConnection状态,HttpRequest的时间戳,设置新解析出来的RequestMethod、URI、version信息。 parsedHeader:将每个HTTP头(name, value)对添加到_requestFields字段中,并检查某些头的存在性以及其值的合法性。1. 如果“host”头存在,则设置_host为true。2. 对“Expect”头,如果其值是“100-continue”,设置_expect100Continue为true,若值是“102-processing”,设置_expect102Processing值为true,当信息不足时,设置_expect为true。3. 对“Accept-Encoding”和“User-Agent”头,只能是预定义的值。4. 对“Content-Type”头只能是预定义的值,并且根据该值设置_charset字段。5. 对“Connectin”头,如果是“close”值,则设置HttpGenerator的persistent属性为false,并且设置_responseFields的“Connection”值为“close”,否则为“keep-alive”。headerComplete:在HTTP消息头解析结束后,对AsyncEndpoint,调用其scheduleIdle()方法,设置HttpGenerator中的HTTP version字段,以及当前请求是否为HEAD请求,如果当前Server配置了sendDateHeader,则设置HttpGenerator的Date字段为HttpRequest的时间戳(在startRequest方法调用是设置)。对HTTP/1.1,如果没有设置Host头,直接返回400响应(调用_generator的completeHeader和complete方法);如果expect为true,表示Expect头设置有问题,直接返回417响应(调用_generator的completeHeader和complete方法)。设置_charset字段,对CHUNK请求立即开始处理请求(handleRequest),否则延迟到消息读取完成。content:对AsyncEndPoint,调用其scheduleIdle()方法,如果请求还未开始处理,则立即开始处理请求。messageComplete:如果请求还未开始处理,则立即开始处理请求。注:这里并没有在content方法中保存消息体的内容,在Jetty中使用HttpInput类从HttpParser中直接读取消息体的内容(通过HttpInput的read方法调用HttpParser.blockForContent()方法)。HttpPaser有限状态机实现 在HttpParser中定义了21中状态,其中STATE_END以前的状态用于解析HTTP头消息,而STATE_END以后的状态用于解析HTTP消息体。它们各自的状态迁移图如下。HttpParser在解析HTTP消息头时的状态迁移图 HttpParser在解析HTTP消息体时的状态迁移图 HttpParser在HttpConnection类中的使用 HttpParser在HttpConnection中的handle方法被调用时用于解析客户端过来的HTTP请求消息。 if (!_parser.isComplete()) { int parsed=_parser.parseAvailable(); if (parsed>0) progress=true; }
概述 当Jetty中的Connector收到一个客户端的连接时(ServerSocket或ServerSocketChannel的accept()方法返回),Connector会首先创建一个ConnectedEndPoint用于和连接的底层(Socket、Channel)打交道(读写数据),在创建的ConnectedEndPoint时会同时使用该EndPoint创建相应类型的Connection,然后会创建一个Task仍给线程池,最终线程池会启动一个线程启动这个Task,而在这个Task中调用Connection中的handle()方法,以处理当前的连接请求,在这个Task中,它会持续的调用Connection中的handle()方法直到连接关闭或Connector停止。在ConnectorEndPoint中,它自己就实现了Runnable接口,因而可以将它自己丢给线程池,而在SelectChannelEndPoint中则交给SelectorManager来管理客户端连接过来的Channel,并调用Connection的handle方法。 Connection接口定义 Connection的定义如下: public interface Connection { // Connection中的核心逻辑,在HttpConnection中,它使用HttpParser解析请求数据,HttpParser采用事件相应机制,可以通过注册HttpParser.EventHandler(RequestHandler)填充HttpConnection中的需要从请求消息中获取的信息,最后在messageComplete()事件相应方法中调用handleRequest()方法将解析后的Request请求交由Server实例的handle()方法处理。在Jetty中,Server是HandlerWrapper子类,它存储了所有注册的Handler,从而将最终的处理流程传导给所有注册的Handler。在所有注册的Handler处理完成后,Connection中的handle()方法会继续执行,使用HttpGenerator、NestedGenerator将缓存的数据刷新到EndPoint中。如果当前请求的相应状态是101(Switching Protocols),则handle方法返回的Connection实例是从注册的以"org.eclipse.jetty.io.Connection"为key的实例,也正是因为这个相应状态码的存在,这个handle方法的返回值是一个Connection。 Connection handle() throws IOException; // 除了handle方法,Connection中还有一些提供了一些包含Connection状态的方法: // 返回Connection创建的时间戳。 long getTimeStamp(); // 当前Connection是否处于Idle状态,如HttpParser、HttpGenerator都处于Idle状态。 boolean isIdle(); // 当前Connection是否处于Suspended状态,用于Continuation机制。 boolean isSuspended(); // 当Connection关闭时会调用这个方法。 void closed(); // 当连接的Idle时间超时后调用该方法,在HttpConnection中,该方法会关闭EndPoiont。 void idleExpired(); } Connection类图 HttpConnection实现 HttpConnection是Jetty中对Connection的主要实现,它表示Http客户端和服务器的一次连接,用于将Request、Response、EndPoint联系在一起。同时HttpConnection也是在避免使用pooling的方式下重用Request、Response、HttpParser、HttpGenerator、HttpFields(requestFields、responseFields)、Buffer、HttpURI等(因为Jetty保证了每一次连接只创建一个HttpConnection实例,这是一个可以学习的点,不用pooling方式的重用,以进一步提升性能)。另外,HttpConnection还有对Connector和Server实例的引用,并且用request字段记录了该Connection总共处理的请求数(在headerComplete回调函数中自增)。如果请求包含Expect头,并且其值是100-continue,表示客户端希望在请求被正真处理前发送一个响应以表示是否能处理该请求,因而在第一次调用getInputStream时表示服务器已经准备好开始处理请求消息体了,此时在返回ServletInputStream之前,服务器要发送100 Continue响应消息给客户端(通过调用HttpGenerator中的send1xx()方法)。在Jetty中,HttpInput类继承自ServletInputStream,它从HttpParser中读取请求消息体数据。如果请求头包含Expect头,并且它的值是102-processing,此时服务器可能会发送102状态码的响应,表示请求正在被处理,之后会发送最终的响应。在Jetty中,可以通过Response中的sendError()方法,传入102的状态码以发送102状态码的响应(使用HttpGenerator中的send1xx()方法)。handle()方法是HttpConnection中的核心方法,在每一个连接到来时,Connector会创建一个Runnable实例,将该Runnable实例扔到线程池中,在该Runnable的run()方法实现中不断的调用Connection的handle()方法直到当前连接或Connector关闭。在该方法的实现中: 它首先设置_handling字段,表示当前正在处理,并且将当前HttpConnection实例设置到__currentConnection的ThreadLocal变量中。 循环处理请求消息直到EndPoint关闭或者在more_in_buffer为true(初始值为true)。 如果当前Request处于Async状态,并且还Async状态还没有结束,直接调用handleRequest()方法,如果Async状态结束了,但是HttpParser还没有结束,则继续使用HttpParser解析,在解析过程中,可能会在headerComplete()、content()、messageComplete()回调函数中调用handleRequest()方法(具体参见HttpParser的实现);而后在HttpGenerator已经Commit(所有的响应头已经准备好,并已经写入到EndPoint中),但是还没有完成的情况下,将HttpGenerator中的数据Flush到EndPoint中;此时如果EndPoint还存在输出缓存,则将其Flush到底层链路中。 如果当前Request没有处于Async状态,如果HttpPaser还没有结束,使用HttpParser解析,在解析过程中,可能会在headerComplete()、content()、messageComplete()回调函数中调用handleRequest()方法(具体参见HttpParser的实现);而后在HttpGenerator已经Commit(所有的响应头已经准备好,并已经写入到EndPoint中),但是还没有完成的情况下,持续的将HttpGenerator中的数据Flush到EndPoint中,如果EndPoint还存在输出缓存,则将其Flush到底层链路中;如果HttpGenerator已经处于完成状态,但是EndPoint中还有输出缓存数据,此时将这些数据Flush到底层链路,如果写完缓存中的数据,将progress设置为true,表示handle方法需要继续处理。 在这些过成中如果出现任何HttpException,则使用HttpGenerator发送错误响应码给客户端(使用sendError()方法,并关闭EndPoint)。 如果HttpParser中还有数据未处理或者EndPoint中还有输入数据未处理,则循环继续。 如果此时HttpParser已经处理完成,HttpGenerator已经处理完成,并且EndPoint中的输出缓存中已经没有任何数据:1. 如果响应状态码时101 Switching Protocols,且在Request存在org.eclipse.jetty.io.Connection的Connection实例,则新的Connection从Request的该Attribute中获取,并重置HttpParser和HttpGenerator;2. 如果Request不存在该Attribute的Connection,HttpGenerator非persisent状态或EndPoint的InputStream已经关闭,则重置HttpParser,关闭EndPoint,设置more_in_buffer为false,重置当前HttpConnection。 如果HttpParser处于idle状态,并且EndPoint的InputStream已经关闭,则关闭当前EndPoint,并设置more_in_buffer为false。 如果Request的Async处于启动状态,则设置more_in_buffer为false。 如果EndPoint是AsyncEndPoint,Generator已经Commit,但是还未Complete,则该EndPoint schedule一个write操作。 最后,清理_handle字段和__currentConnection的ThreadLocal字段。 handleRequest()是HttpConnection在对请求消息头解析完成后执行的真正处理逻辑方法: 对任何Request还没有处理完成,并且Server不为null且处于Running状态,循环处理。 设置Request的handled为false,以及PathInfo字段(如果pathInfo为null,又不是Connect请求,则为400 Error)。 如果_out字段不为null,reopen it。 如果Request处于initial状态,设置Request的DispatcherType为REQUEST,使用当前的EndPoint和Request实例配置Connector,并调用使用当前HttpConnection作为参数调用Server的handle()方法;否则,设置Request的DispatcherType为ASYNC,调用Server的handleAsync()方法(传入当前HttpConnection做为参数)。 对任何非ContinuationThrowable异常,设置Request的handled为true,error为true,对HttpException使用Response发送响应状态码给客户端,而对Throwable,使用HttpGenerator发送400或500状态码给客户端。 如果此时Request处于为完成状态,调用AsyncContinuation.doComplete()方法;如果100 Continue响应没有发送给客户端,则清除该状态,但是如果此时Response还没有Commit,则设置HttpGenerator的persistent为false,表示客户端并没有发送数据过来,我们可以关闭该连接了;如果EndPoint关闭了,则调用Response的complete方法;如果EndPoint没有关闭并且有error,直接关闭EndPoint;如果EndPoint没有关闭,也没有error,但是HttpGenerator没有Commit,Request也没有被handle,则使用resonse发送404 Resource Not Found响应消息,之后调用Response的complete方法;最后设置Request的handled为true。 commitResponse()方法,用于控制HttpGenerator的执行流程: 在HttpGenerator还没有Commit之前(即响应状态行和响应消息头还没写入到EndPoint中)时,先调用HttpGenerator的setResponse()方法设置状态行。 然后调用HttpGenerator的completeHeader()方法将响应消息头写入到EndPoint中。 最后调用HttpGenerator的complete方法,不断的将HttpGenerator中的缓存写入到EndPoint中。 flushResponse()方法只是调用了commitResponse方法。 HttpOutput时Jetty中继承自ServletOutputStream的类,它使用AbstractorGenerator向底层EndPoint中写入数据。Output时HttpConnection中的内部类,它继承自HttpOutput,它在调用close/flush时会先调用commitResponse/flushReponse方法,保证响应消息先写状态行,然后是响应消息头,最后才是响应消息体。该类还实现了sendContent方法,其参数可以是HttpContent类型或Resource类型,该方法是一个Util方法,它会自动设置Content-Type、Content-Length、Last-Modified等头,并将HttpContent或Resource对应的数据写入到EndPoint中。
概述 在Jetty中,使用Connector来抽象Jetty服务器对某个端口的监听。在Connector启动时,它会启动acceptors个Acceptor线程用于监听在Connector中配置的端口。对于客户端的每次连接,Connector都会创建相应的EndPoint来表示该连接,一般在创建EndPoint的同时会同时创建Connection,这里EndPoint用于和Socket打交道,而Connection用于在从Socket中读取到数据后的处理逻辑以及生成响应数据的处理逻辑。不同的Connector会创建不同的EndPoint和Connection实例。如SocketConnector创建ConnectorEndPoint和HttpConnection,SslSocketConnector创建SslConnectorEndPoint和HttpConnection,SelectChannelConnector创建SelectChannelEndPoint和SelectChannelHttpConnection,SslSelectChannelConnector创建SslSelectChannelEndPoint和SelectChannelHttpConnection,BlockingChannelConnector创建BlockingChannelEndPoint和HttpConnection等。EndPoint接口定义 Jetty中EndPoint接口定义如下: public interface EndPoint { // EndPoint是对一次客户端到服务器连接的抽象,每一个新的连接都会创建一个新的EndPoint,并且在这个EndPoint中包含这次连接的Socket。由于EndPoint包含底层的连接Socket,因而它主要用于处理从Socket中读取数据和向Socket中写入数据,即对应EndPoint接口中的fill和flush方法。 // 从Socket中读取数据,并写入Buffer中直到数据读取完成或putIndex到Buffer的capacity。返回总共读取的字节数。在实现中,StreamEndPoint使用Buffer直接从Socket的InputStream中读取数据,而ChannelEndPoint则向Channel读取数据到Buffer。 int fill(Buffer buffer) throws IOException; // 将Buffer中的数据(从getIndex到putIndex的数据)写入到Socket中,同时清除缓存(调用Buffer的clear方法)。在实现中,StreamEndPoint使用Buffer直接向Socket的OutputStream写入数据,而ChanelEndPoint则将Buffer中的数据写入Channel中。 int flush(Buffer buffer) throws IOException; // 类似上面的flush,它会将传入的header、buffer、trailer按顺序写入Socket中(OutputStream或者Channel)。返回总共写入的字节数。 int flush(Buffer header, Buffer buffer, Buffer trailer) throws IOException; // 当在处理HTTP/1.0请求时或当前Request中的KeepAlive的值为false时,在处理完成当前请求后,需要调用shutdownOutput()方法,关闭当前连接;或在处理当前请求时出现比较严重的错误、Socket超时时。在调用完shutdownOutput()方法后,isOutputShutdown()方法返回true。 void shutdownOutput() throws IOException; boolean isOutputShutdown(); // 当Server无法从当前连接(Socket)中读取数据时(即read返回-1)时,调用shutdownInput()方法以关闭当前连接,此时isInputShutdown()返回true。 void shutdownInput() throws IOException; boolean isInputShutdown(); // 当Socket超时或在读写Socket过程中出现任何IO错误时,Server会直接调用close()方法以关闭当前连接。 void close() throws IOException; // 当前Connection是否已经打开,对ChannelEndPoint来说表示Channel.isOpen()返回true,对SocketEndPoint来说,表示Socket没有被关闭。 public boolean isOpen(); // 对StreamEndPoint来说,它的读写是阻塞式的,但是对ChannelEndPoint来说,如果它内部的channel是SelectableChannel,那么这个Channel的读写可以配置成非阻塞的(通过SelectableChannel.isBlocking()方法判断)。因而对SelectChannelEndPoint需要使用blockReadable()方法来阻塞直到超时。返回true表示阻塞读取失败,此时HttpParser会关闭这个EndPoint,并抛出异常。blockWritable()方法类似blockReadable()用于SelectChannelEndPoint以等待有数据写入到Channel中,如果返回false,表示在指定的时间内没有数据可写入Channel中(即超时),此时会关闭该EndPoint,并抛出异常。 public boolean isBlocking(); public boolean blockReadable(long millisecs) throws IOException; public boolean blockWritable(long millisecs) throws IOException; // 对SslSelectChannelEndPoint,它是Buffered,因而它的isBuffered()方法返回true,而isBufferingInput()和isBufferingOutput()根据内部的_inNIOBuffer和_outNIOBuffer字段的hasContent()方法判断是否返回true或false。对其他类型的EndPoint来说,这三个方法都返回false。而flush()方法则将_outNIOBuffer中缓存的数据写入Channel中。 public boolean isBufferred(); public boolean isBufferingInput(); public boolean isBufferingOutput(); public void flush() throws IOException; // EndPoint还定义了一些和EndPoint相关链的信息和状态: // 返回该EndPoint内部使用的传输工具,如ChannelEndPoint内部使用Channel,而SocketEndPoint内部使用Socket。该方法用于对内部传输工具的配置。 public Object getTransport(); // 用于配置Socket的SO_TIMEOUT的时间,即等待客户连接的超时时间。 public int getMaxIdleTime(); public void setMaxIdleTime(int timeMs) throws IOException; // 返回当前EndPoint所在服务器的IP地址、主机名、端口号以及客户端的IP地址、主机名、端口号。 public String getLocalAddr(); public String getLocalHost(); public int getLocalPort(); public String getRemoteAddr(); public String getRemoteHost(); public int getRemotePort(); } EndPoint类图 EndPoint接口定义概述 EndPoint最主要的方法从底层传输链路中读取数据并填入Buffer中的fill方法,以及将Buffer中的数据写入底层传输链路的flush方法;读数据对应Input,写数据对应Output,可以单独的关闭Input或Output,并提供方法判断Input或Output是否已经被关闭;可以用close方法关闭EndPoint,也可以通过isOpen方法判断是否这个EndPoint是否已经被关闭;可以以阻塞的方式读写EndPoint,并判断当前EndPoint是否处于阻塞状态(主要用于SelectChannelEndPoint中);对SslSelectChannelEndPoint来说,它在读写时都可能内部缓存数据,因而EndPoint中定义了一些方法用于判断当前EndPoint是否有输入/输出换成,以及使用flush将缓存中的数据写入到底层链路中;对底层Socket,EndPoint还可以配置其最长的空闲时间;最后EndPoint还提供一些方法用于获取本地和远程的地址、主机名、端口号,以及获取底层传输类,如Socket、Channel等。StreamEndPoint实现 StreamEndPoint采用古老的Stream方法从Socket中读写数据,它包含InputStream和OutputStream,分别表示读写数据流;它永远是阻塞式读写,因而isBlocking、blockReadable、blockWritable永远返回true;它也不会在内部缓存读写数据,因而isBufferingInput、isBufferingOutput、isBufferred永远返回false,而flush方法直接调用OutputStream的flush方法;对fill实现,直接使用传入的Buffer从InputStream中读取数据;对flush实现,直接将Buffer中的数据写入到OutputStream中;close方法同时关闭InputStream和OutputStream,并将成员变量置为null;对StreamEndPoint本身,没有本地或远程的地址、主机名、端口号信息。SocketEndPoint是StreamEndPoint的子类,它从Socket中获取InputStream和OutputStream,以及本地和远程的地址、主机名、端口号;而isInputShutdown、isOutputShutdown、shutdownInput、shutdownOutput等方法直接调用Socket中相应的方法;getTransport直接返回Socket实例;setMaxIdleTime方法同时设置Socket的SO_TIMEOUT值;当空闲超时,只关闭Input。 ConnectorEndPoint继承自SocketEndPoint,它是SocketConnector的内部类,每一个客户端的连接请求创建一个ConnectorEndPoint实例,在创建ConnectorEndPoint的同时,会在内部创建一个HttpConnection实例;它还实现了ConnectedEndPoint,因而可以从外部设置Connection实例;在读数据时,如果遇到EOF,表示连接已经断开,因而关闭当前EndPoint;在关闭EndPoint时,cancel当前Connection中Request实例的AsyncContinuation。ConnectorEndPoint还实现了Runnable接口,在其run方法的实现中,它首先更新处理的Connection的引用计数,然后保存当前Connection实例,在SocketConnector已经启动,并且ConnectorEndPoint未被关闭的状态下循环调用Connection的handle方法,在每个循环开始前检查当前Connector是否处于Low Resources状态(如线程池的可用线程已经不多),此时更新EndPoint的MaxIdleTime为当前Connector的LowResourcesMaxIdleTime的值,以减少一些连接的空闲等待时间;对任何Exception,关闭当前EndPoint;最后更新Connector中的一些统计信息,将当前Connection从Connector的当前正在处理的connections集合中移除,如果此时Socket还未关闭,读取Socket中的数据直到数据读完或超过MaxIdleTime,此时如果Socket还未关闭,则关闭当前Socket。而在Connector创建ConnectorEndPoint时,会调用其dispatch方法,将其自身仍给相应的线程池处理,以在某个时间在另一个线程中调用其run方法。SslConnectorEndPoint继承自ConnectorEndPoint,它在关闭Input和Output时会同时关闭整个EndPoint,而在执行真正的处理逻辑前有一个handle shake的过程。ChannelEndPoint实现 ChannelEndPoint采用NIO实现,从Channel中读写数据。在创建ChannelEndPoint时传入ByteChannel,如果传入的ByteChannel是SocketChannel,则同时纪录Socket实例,以及获取本地、远程的地址信息,并设置MaxIdleTime值为SO_TIMEOUT值。如果该ByteChannel是SelectableChannel类型(ServerSocketChannel、SocketChannel、DiagramChannel、SinkChannel、SourceChannel),并且其isBlocking()方法返回false,表示该Channel是非阻塞式的读写,否则这个Channel是阻塞式的读写,但是默认情况下,blockReadable、blockWritable直接返回true,表示阻塞式的读写。对非SslSelectChannelEndPoint的EndPoint不会在内部缓存数据,因而isBufferred、isBufferingOutput、isBufferingInput直接返回false,而flush方法为空实现;对SocketChannel,在设置MaxIdleTime时,同时将该值设置到底层Socket的SO_TIMEOUT的值中;getTransport直接返回底层channel实例;shutdownInput、shutdownOutput、isInputShutdown、isOutputShutdown使用Socket实现;fill实现只支持NIOBuffer,它使用Channel将数据写入内部的ByteBuffer中;flush实现使用Channel将ByteBuffer中的数据写入到Channel中,或使用GatheringByteChannel将多个ByteBuffer同时写入到Channel中。BlockingChannelEndPoint类是BlockingChannelConnector的内部类,它继承自ChannelEndPoint,并实现了ConnectedEndPoint和Runnable接口。在创建BlcokingChannelEndPoint时,同样也会创建HttpConnection实例;每次调用fill、flush方法时,都会更新_idleTimestamp的值为当前时间戳(该值也会在每一次Connection开始重新被处理时更新),在BlockingChannelConnector启动时会生成一个Task,它没400毫秒遍历一次所有正在处理的EndPoint,如果发现有EndPoint已经超时(checkIdleTimestamp()方法,即空闲时间超过MaxIdleTime),则调用其idleExpired()方法,将该EndPoint关闭;BlockingChannelConnector在接到一个连接后,先会设置SocketChannel的blockingChannel为true,然后使用这个SocketChannel创建一个BlockingChannelEndPoint,并调用其dispatch()方法,将它丢到一个线程池中,在BlockingChannelEndPoint的run方法实现中,首先更新一些统计数据,纪录当前正在处理的EndPoint;只要当前EndPoint还处于打开状态,先更新_idleTimestamp为当前时间戳,然后如果当前ThreadPool处于LowOnThread状态,将timeout时间更新为LowResourcesMaxIdleTime,而后调用Connection的handle方法;对任何Exception,直接关闭EndPoint;在最后退出时,如果EndPoint还未关闭,读取EndPoint的数据,直到超时,并强制关闭EndPoint。 SelectChannelEndPoint类在SelectChannelConnector中被使用,它继承自ChannelEndPoint,并实现了ConnectedEndPoint和AsyncEndPoint接口,SelectChannelConnector采用NIO中多路复用的机制,因而实现会比较复杂一些。在创建Connector时,首先创建ConnectorSelectorManager实例(_manager),在SelectChannelConnector启动时,设置_manager的SelectSets(acceptors)、MaxIdleTime、LowResourcesConnections、LowResourcesIdleTime,然后启动_manager,并且启动acceptors个线程,只要SelectChannelConnector处于Running状态,就不断的调用_manager.doSelect()方法。ConnectorSelectorManager在启动时会创建_selectSets个SelectSet;而doSelect方法会调用根据传入的索引号对应的SelectSet的doSelect()方法。当客户端的连接到来后,SelectChannelConnector首先会配置SocketChannel的configureBlocking为false,然后将该SocketChannel注册到_manager中,在注册过程中,根据当前的SelectSet索引值找到相应的SelectSet(之后索引自增),然后调用SelectSet的addChange(传入SocketChannel)和wakeup方法。因而这里最重要的就是SelectSet的实现,它是SelectorManager中的一个内部类。 SelectSet类的实现中,它内部有一个Selector,一个ConcurrentLinkedQueue的changes队列,以及SelectChannelEndPoint到SelectSet的集合(它用于调用SelectChannelEndPoint中的checkIdleTimestamp()方法以检查并关闭处于Idle Timeout的SelectChannelEndPoint)。SelectSet使用addChange()方法添加需要改变状态的对象,这些对象有EndPoint、ChannelAndAttachment、SocketChannel、Runnable。在doSelect()方法中,首先检查changes队列中是否有对象,如果有SelectChannelEndPoint对象,则调用其doUpdateKey()方法;如果是SocketChannel对象,则注册OP_READ操作到Selector中,创建新的SelectChannelEndPoint,attach新创建的SelectChannelEndPoint到SelectionKey中,调用SelectChannelEndPoint的schedule()方法。对ChannelAndAttachment对象,如果其Channel是SocketChannel,并且处于Connected状态,则类似对SocketChannel对象的处理,否则,注册OP_CONNECT操作到Selector;如果是Runnable对象,则dispatch该Runnable对象。然后调用Selector的selectNow()方法,如果没有任何可用的事件,则计算出等待时间,然后带等待时间的调用Selector的select()方法;遍历所有Selected Keys,对Invalid的SelectionKey,直接调用其attach的SelectChannelEndPoint的doUpdateKey()方法,否则对类型是SelectChannelEndPoint的attachment调用其schedule()方法,对connectable的SelectionKey创建新的SelectChannelEndPoint并调用schedule()方法,否则创建新的SelectChannelEndPoint并对readable的SelectionKey调用其schedule()方法。 SelectChannelEndPoint采用NIO的非阻塞读写方式,而NIO基于Channel的非阻塞操作是基于注册的操作集(OP_READ, OP_WRITE, O_CONNECT, OP_ACCEPT)以从Selector中选出已经可用的SelectionKey(包含对应的Channel、interestOps、readable、writable、attachment等),之后可以使用对应的Channel以及根据SelectionKey中对应的已经可用的操作执行相应的操作(如读写),因而SelectChannelEndPoint的其中一个任务是要实时的更新当前它感兴趣的操作集,并重新像Selector中注册。 SelectChannelEndPoint使用updateKey()方法跟新感兴趣操作集合,并且它只关注OP_READ和OP_WRITE操作,在实现时,OP_READ只需要在Socket的输入没有关闭,且还没有dispatch或当前处于readBlocked状态下才需要关注;OP_WRITE只需要在Socket的输出没有关闭,且writable为false(当需要向Channel中写数据,但是还没有写完的情况下)或当前处于writeBlocked状态下才需要关注;如果和当前已注册的操作集相同,则不需要重新注册,否者将自身通过SelectSet的addChange()方法添加到SelectSet中,在SelectSet的doSelect()方法中会最终调用SelectChannelEndPoint中的doUpdateKey()方法,该方法的实现:1. 当Channel处于Open状态,存在感兴趣的操作,SelectionKey为null或invalid,如果Channel已经注册了,重新调用updateKey()方法(感觉这里一般不会被调用到,如果被调用到了,则可能出现死循环),否则将Channel重新向Selector中重新注册interestOps的操作集(如果出错,则canncel SelectionKey,并且从SelectSet中销毁当前EndPoint)。2. 当Channel处于Open状态,存在感兴趣的操作,SelectionKey存在且valid,则直接使用interestOps更新SelectionKey的感兴趣集(调用SelectionKey的interestOps()方法)。3. 当Channel处于Open状态,不存在感兴趣的操作,清空SelectionKey的interestOps,或清理SelectionKey引用。4. 如果Channel处于关闭状态,则canncel SelectionKey,并从SelectSet中销毁当前EndPoint。 对阻塞读写(readBlocked、writeBlocked),在blockReadable()、blockWritable()方法中,会设置readBlocked、writeBlocked为true,调用updateKey()方法,然后计算等待时间并进入等待(调用wait方法),如果因为超时而退出等待,则返回false,否则返回true(在返回时设置readBlocked、writeBlocked为false);当调用SelectChannelEndPoint的schedule()方法时,它会更新readBlocked、writeBlocked、interestOps的值(同时使用该值更新SelectionKey中的状态),并调用notifyAll()方法唤醒blockReadable()、blockWritable()方法:1. 如果SelectionKey为null或invalid,readBlocked、writeBlocked设置为false,调用notifyAll(),并返回;2. 如果readBlocked或writeBlocked为true,使用SelectionKey的readable、writable更新readBlocked和writeBlocked的值,调用notifyAll(),如果已经dispatched,清除所有interestOps,并返回;3. 如果还没有dispatched,直接清除所有interestOps,并返回;4. 如果注册了OP_WRITE,并且已经可写,则清除OP_WRITE操作,设置writable为true;5. 如果还没有dispatched,则调用dispatch()方法。在dispatch()方法中,它设置dispatched为true,并将handler扔给ThreadPool(在handler调用Connection的handle()方法,由于SelectChannelEndPoint的生命周期是在SelectManager维护,并且dispatch()方法可能被多次调用,因而没有在handler的handle()方法中判断EndPoint的close状态,并循环的调用Connection的handle()方法,而是在每次handle()方法结束后退出当前线程,在下次schedule()时会使用重新将handler扔给ThreadPool以支持AsyncContinuation的实现,并且AsyncEndPoint接口的定义也是用于AsyncContinuation的实现,这个将在以后的博客中详述)。在flush()方法中,如果没有任何数据能写入Channel时,设置writable为false(从而在updateKey()方法中能将OP_WRITE注册到SelectionKey的interestOps中),并在没有dispatch的情况下调用updateKey。最后清理Selector中的selectedKeys,expire所有timeout中注册的Task(使用scheduleTimeout()方法注册),依次调用检查EndPoints中是否已经TimeOut。 在SelectChannelEndPoint的构建中,它使用SocketChannel、SelectSet、SelectionKey构建,内部从SelectSet中获取SelectorManager,并使用SelectorManager 创建Connection实例,初始化_dispatched、_redispatched为false,_open、_writable为true,_readBlocked、_writeBlocked为false,_interestOps为0,最后更新_idleTimestamp为当前时间。当一个客户端连接到来后,SelectChannelConnector会向SelectorManager(SelectSet)中注册一个SocketChannel,当后台线程调用SelectSet中的doSelect()方法时,它使用该SocketChannel,向该SelectSet中的Selector注册OP_READ得到一个SelectionKey,并使用这个SocketChannel、当前SelectSet、以及这个SelectionKey创建一个SelectChannelEndPoint,而后调用SelectChannelEndPoint的schedule()方法。 SslSelectChannelEndPoint类采用Buffer的形式先将数据读写到内部缓存中,然后使用SSLEngine来wrap或unwrap(encode/decode)数据。这里不再详述。
概述 在Jetty中Buffer是对Java中Stream IO中的buffer和NIO中的buffer的抽象表示,它主要用于缓存连接中读取和写入的数据。在Jetty中,对每个连接使用Buffer从其InputStream中读取字节数据,或将处理后的响应字节写入OutputStream中,从而Jetty其他模块在处理请求和响应数据时直接和Buffer打交道,而不需要关注底层IO流。 Buffer接口定义 Jetty中Buffer接口定义如下: public interface Buffer extends Cloneable { // 基于Buffer主要用于向当前连接读写数据,因而它定义了两个核心的方法:readFrom和writeTo。其中readFrom方法从InputStream中读取字节数据,writeTo方法将响应字节写入OutputStream中,即向Connection中读取和写入数据,写完后清理当前Buffer。 int readFrom(InputStream in, int max) throws IOException; void writeTo(OutputStream out) throws IOException; // 在Buffer从InputStream(Connection)中读取数据后,Buffer接口还提供了很多不同的方法用于从Buffer中读取或写入字节数据。 // Jetty中的Buffer是一个FIFO的字节队列,它的设计类似NIO中Buffer的设计:每次get操作都从getIndex开始,并且getIndex会向前移动读取的字节数的长度;每次的peek操作也getIndex开始,但是peek操作不会使getIndex向前移动;每次put操作都从putIndex开始,并且putIndex会向前移动写入的字节数的长度;每次poke操作也会从putIndex开始,但是poke操作不会使putIndex向前移动;mark操作会以getIndex作为基准设置markIndex的值,从而在reset时会将getIndex重置到之前mark过的位置;capacity表示该Buffer最多可存储的字节数,而length表示从getIndex到putIndex还存在的字节数。并且Buffer永远保证以下关系总是成立:markIndex<=getIndex<=putIndex<=capacity byte get(); int get(byte[] b, int offset, int length); Buffer get(int length); int getIndex(); void mark(); void mark(int offset); int markIndex(); byte peek(); byte peek(int index); Buffer peek(int index, int length); int peek(int index, byte[] b, int offset, int length); int poke(int index, Buffer src); void poke(int index, byte b); int poke(int index, byte b[], int offset, int length); int put(Buffer src); void put(byte b); int put(byte[] b,int offset, int length); int put(byte[] b); int putIndex(); int length(); void clear(); void reset(); void setGetIndex(int newStart); void setMarkIndex(int newMark); void setPutIndex(int newLimit); // 一个Buffer还有独立的两种状态:access级别和volatile。 access级别有:IMMUTABLE,表示当前Buffer所有的Index和内容都不能被改变;READONLY,表示当前Buffer是只读的,即getIndex和markIndex可以被改变,而putIndex以及Buffer内容不可以;READWRITE,表示所有的Index以及Buffer的内容可以被改变。 volatile状态表示当前Buffer是否会通过其他路径被修改,默认情况下,ByteArrayBuffer、DirectNIOBuffer等是NON_VOLATILE状态,而View是VOLATILE状态(除非View内部的Buffer是IMMUTABLE)。VOLATILE的状态感觉不是一个比较严谨的概念,比如对一个View它是VOLATILE的,但是在这种情况下,它内部包装的Buffer应该也变成VOLATILE状态,并且在所有的View被回收后,其内部包装的Buffer应该重新变成NON_VOLATILE状态。要实现这种严谨逻辑应该是可以做的到的,只是会比较麻烦,而且貌似也没必要,因而Jetty并没有尝试着去这样做。 // 返回包含当前Buffer从getIndex到putIndex内容的Buffer,并且返回的Buffer不可以被其他路径修改。如果当前Buffer是NON_VOLATILE,则直接返回当前Buffer(这个实现是不严谨的,因为在这种情况下,其实有两个Buffer实例可以修改同一份数据),否则,克隆一个新的Buffer,这样对新的Buffer的修改不会影响原Buffer的内容。 Buffer asNonVolatileBuffer(); // 返回一个只读的Buffer(View),该只读的Buffer的读取不会影响原来Buffer的Index。 Buffer asReadOnlyBuffer(); // 拷贝一个不可修改的ByteBuffer。 Buffer asImmutableBuffer(); // 返回一个可修改的Buffer,并且对返回的Buffer的内容修改会影响原有的Buffer。 Buffer asMutableBuffer(); // 当前Buffer是否不可被修改,即Buffer内容和所有Index都不能被修改。 boolean isImmutable(); // 当前Buffer是否是只读的。 boolean isReadOnly(); // 是否当前Buffer内容可以通过其他路径被修改,比如View一般情况下是VOLATILE状态(除非View内部的Buffer是IMMUTABLE)。 boolean isVolatile(); // 除了以上的操作,Buffer还提供了一些其他用于操作Buffer内部字节的方法: //如果内部使用字节数组表示,返回该字节数组,否则,返回null。 byte[] array(); // 获取从getIndex到putIndex的字节数组,其长度为length。 byte[] asArray(); // 如果当前Buffer是对另一个Buffer的包装,则返回内部被包装的Buffer实例,否则返回当前Buffer本身。 Buffer buffer(); int capacity(); // 返回当前Buffer剩余的空间,即capacity-putIndex。 int space(); // 清除Buffer内容,即设置getIndex和putIndex为0,以及markIndex为-1。 // 整理Buffer内容,即将markIndex >= 0 ? min(getIndex, markIndex) : getIndex到putIndex的内容移动到Buffer的起始位置,同时修改相应的getIndex、markIndex、putIndex。 void compact(); // 当前Buffer是否有可用字节,即是否putIndex>getIndex。 boolean hasContent(); // 跳过n个字节,即getIndex增加min(remaining(), n)的值。 int skip(int n); // 切割出当前Buffer从getIndex到putIndex的View,一般来说它是volatile的(除非它是immutable类型的Buffer)。 Buffer slice(); // 切割出当前Buffer从markIndex到putIndex的View,一般来说它是volatile的(除非它是immutable类型的Buffer)。 Buffer sliceFromMark(); Buffer sliceFromMark(int length); // 返回包含当前Buffer状态和内容的字符串。 String toDetailString(); boolean equalsIgnoreCase(Buffer buffer); } 类图 主要实现类 AbstractBuffer:所有Buffer的基类,是对Buffer接口的基本实现。ByteBuffer:它继承自AbstractBuffer主要的非NIO的Buffer实现,内部使用字节数组做缓存,直接读InputStream和写OutputStream。DirectNIOBuffer:它实现了NIOBuffer接口,继承自AbstractBuffer,内部使用Direct的ByteBuffer做缓存,使用ReadableByteChannel和WritableByteChannel分别对InputStream(readFrom传入)和OutputStream(writeTo传入)包装,并在这两个方法中使用包装后的Channel读写数据。IndirectNIOBuffer:它继承自ByteBuffer,内部使用非direct的ByteBuffer做缓存,并且它也直接对InputStream和OutputStream读写。RandomAccessFileBuffer:它继承自AbstractBuffer,内部使用RandomAccessFile做缓存。View:它继承自AbstractBuffer,内部使用另一个Buffer作为缓存,并且对非IMMUTABLE的Buffer,很多时候,它是VOLATILE。View如其名,它是对内部Buffer的视图,对View内容以及Index的修改会影响内部Buffer中相应的值。Buffers Buffers是Buffer的抽象工厂,它用于创建Header的Buffer和Body的Buffer,并且可以根据给定的size获得相应的Buffer实例,在Buffer使用完成后,还可以通过returnBuffer方法将它归还个Buffers以复用。在创建Buffers子类时,可以将指定Header和Body各自Buffer的类型,从而在内部创建相应Buffer时会创建相应类型的Buffer,支持的Buffer类型有:BYTE_ARRAY、DIRECT、INDIRECT。Jetty中有两个Buffers的实现:PooledBuffers和ThreadLocalBuffers。PooledBuffers使用ConcurrentLinkedQueue构建各自的Header、Body、Other类型的Buffer池,它有一个maxSize值用于控制该Buffers中包含的所有类型的Buffer的总数。 ThreadLocalBuffers将Header、Body、Other类型的Buffer保存在ThreadLocal中。Jetty还提供了BuffersFactory用于创建不同类型的Buffers:通过在参数中maxSize是否大于等于0以决定是使用PooledBuffers还是ThreadLocalBuffers。 BufferCache/BufferDateCache Jetty还为Buffer提供了两个特殊的类:BufferCache和BufferDateCache。BufferCache用于存储一个可以使用存储的String值、索引值等获取相应的Buffer实例,主要用于HttpHeaders、HttpMethods等一些预定义的值。BufferDateCache继承自DateCache,它存储了上一次使用一个long类型的date值格式化出的Buffer实例,从而实现部分复用(复用在同一秒得到的Request请求时创建的Buffer,因为时间也只能在这种情况下被复用,因而才会有这样的实现),在Request类中使用。
在看Jetty源码中的EndPointTest类,对EndPoint的测试,我的思路是:1. 建立一个连接(创建ServerSocket实例,一般还会给定一个端口,其实可以bind(null)以让操作系统分配一个可用端口),新启动一个线程,在新线程中监听给定端口(调用accept方法)。2. 发送客户端请求(创建一个Socket实例,并向该Socket写入请求数据)。3. 在接收端读取数据,验证写入的请求和接收到的数据相同。在以上流程实现中,accept方法返回的接收端Socket需要传给主线程,同时要保证使用该Socket是在accept方法返回之后,以我习惯,我会使用一个Lock或CountDownLatch: private static class SocketHolder { Socket socket; } @Testpublic void levinOldWayTest() throws Exception { final ServerSocket server = new ServerSocket(10240); final CountDownLatch latch = new CountDownLatch(1); final SocketHolder socketHolder = new SocketHolder(); new Thread() { public void run() { try { socketHolder.socket = server.accept(); latch.countDown(); } catch(Exception ex) { ex.printStackTrace(); } } }.start(); Socket socket = new Socket(server.getInetAddress(), server.getLocalPort()); socket.getOutputStream().write("My Test String".getBytes()); latch.await(5, TimeUnit.SECONDS); byte[] receives = new byte[4096]; int length = socketHolder.socket.getInputStream().read(receives); assertEquals("My Test String", new String(receives, 0, length)); socket.close(); socketHolder.socket.close(); server.close(); } 不知道有多少人也像我一样把这段代码写成这样?这里有两个问题:1. ServerSocket的监听的端口不一定是可用的,类似测试代码我之前没有写过,我估计自己正真在写的时候应该会想到让操作系统动态分配。2. 为了在两个线程中传递数据,这里首先创建了一个SocketHolder类,然后使用CountDownLatch,写起来好麻烦。为了简化这段代码,可以使用Exchanger,即当一个生产者线程准备好数据后可以通过Exchanger将数据传递给消费者,而消费者在生产者传递过来数据后就可以消费了,这里的数据就是Socket。改进后的代码如下:@Testpublic void levinImprovedWayTest() throws Exception { final ServerSocket server = new ServerSocket(); server.bind(null); final Exchanger<Socket> exchanger = new Exchanger<Socket>(); new Thread() { public void run() { try { exchanger.exchange(server.accept()); } catch(Exception ex) { ex.printStackTrace(); } } }.start(); Socket socket = new Socket(server.getInetAddress(), server.getLocalPort()); socket.getOutputStream().write("My Test String".getBytes()); Socket receiverSocket = exchanger.exchange(null, 5, TimeUnit.SECONDS); byte[] receives = new byte[4096]; int length = receiverSocket.getInputStream().read(receives); assertEquals("My Test String", new String(receives, 0, length)); socket.close(); receiverSocket.close(); server.close(); }
在计算机网络中,如果两台机器要通信,他们首先要定义通信数据的格式,这样在服务器收到客户端的请求消息时,它才能正确的解析请求的内容,然后根据请求内容处理逻辑,并将相应消息传递会客户端;此时,客户端也要根据已定义的响应数据格式解析响应消息。在浏览器和HTTP服务器之间的通信数据格式使用HTTP协议定义。请求消息 其中请求消息的格式为:例子:POST /index.jsp?articleId=1234&articleName=GoodArticle HTTP/1.1 Host: www.blogjava.com Content-Length: 74 Content-Type: application/x-www-form-urlencoded myText1=hello+world&myText2=%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C 请求消息由三部分组成:请求行、消息报头、请求正文。请求行格式为:请求方法、空格、URL、版本号、回车、换行。请求方法集:GET、POST、HEAD、PUT、DELETE、TRACE、CONNECT、OPTIONS。URL中'?'之后的值用于表达请求参数。版本号可以是:HTTP/1.0、HTTP/1.1。消息报头格式为:报头名字、冒号(':')、空格、报头值、回车、换行。消息报头用于传递元数据信息,用于表达消息正文的类型、编码格式、缓存等,以回车换行结束。当遇到一个空行(只有回车换行),表示消息报头结束。消息正文,可以是任意定义的格式,它接在消息报头后(空行之后)。请求消息是否包含消息体由Content-Length或Transfer-Encoding决定,如果规范定义的请求方法不允许包含消息体,则在请求消息中不可以包含消息体。Server在解析时,如果请求方法不支持消息体,则在请求消息中包含的消息体会被忽略。请求方法: HTTP1.1定义的请求方法有 OPTIONS:请求查询服务器的性能,或查询与资源相关的选项和需求。 该方法的Response不可缓存。当前版本HTTP不支持该请求方法包含消息实体。如果该请求包含请求消息体,HTTP服务器将会抛弃这些信息。如果Request-URI为*,该请求类似“ping”或“no-op”操作。 GET:请求获取Request-URI所标识的资源。 响应消息可缓存。在GET请求消息中,如果包含If-Modified-Since、If-Unmodified-Since、If-Match、If-None-Match、If-Range字段,则该请求称为“条件GET”。 HEAD:请求获取由Request-URI所标识的资源的响应消息报头。 响应消息可缓存。 POST:在Request-URI所标识的资源后附加新的数据。 如果在HTTP服务器中有创建新的资源,则该方法的响应状态码必须是201(Created),并且响应消息实体包含描述请求的状态,以及一个Location响应消息头。该方法的响应消息不可被缓存。 PUT:请求服务器存储一个资源,并用Request-URI作为其标识。 如果Request-URI指向一个已存在的资源,那么包含的消息实体被视为已存在资源的新版本,如果Request-URI没有对应的资源,则HTTP服务器可以通过该URI创建相应资源,此时HTTP服务器响应201(Created)状态码。如果修改了已存在的资源,则返回200(OK)或204(No Content)。HTTP服务器不可以忽略Content-*消息头(如Content-Range),如果HTTP服务器不能理解该消息头,则返回501(Not Implemented)响应消息码。 DELETE:请求服务器删除Request-URI所标识的资源。 即使该方法的返回状态码表明该操作以执行成功,客户端还是不能保证该方法需要删除的操作已经被执行了。但是HTTP服务器必须保证在返回响应给客户端的时候,HTTP服务器已经打算删除这个资源或把它移动到一个不可访问的位置。成功的响应码为200(OK),并且响应消息实体中可以包含一些描述信息;202(Accept)表明这个操作还没被完全执行;204(No Content)表示这个操作已经执行完成,但是没有响应消息实体。该方法的响应消息不可被缓存。 TRACE:请求服务器会送收到的请求信息,主要用于测试或诊断。 该方法以200返回标识成功。该请求消息不可包含请求消息实体。该方法的Response必须在响应消息实体中包含所有的请求消息,其相应消息的Content-Type值为:message/http,该Response不可被缓存。 CONNECT:保留将来使用。 扩展的方法:用户自定义扩展方法。 如果Server能识别某个请求方法但是不允许该请求方法,则应该返回405(Method Not Allowed)响应状态。如果Server无法识别某个请求方法或者当前Server没有实现这个请求方法,则应该返回501(Not Implemented)状态码。 Request-URI支持的值有:*|absoluteURI|abs_path|authority *表示请求不应用于某个特定的资源,并且只对于某些不需要应用于特定资源的请求方法,如:OPTIONS * HTTP/1.1 absoluteURI:当客户端是向一个代理发送请求时需要使用absoluteURI,然后这个代理会转发这个请求,并返回响应。虽然按规范,HTTP1.1客户端只发送absoluteURI到代理服务器,但是为了在将来的HTTP版本中可以允许请求都转换成absoluteURI,所有HTTP1.1 Server必须可以解析absoluteURI风格的请求:GET http://www.w3.org/pub/WWW/TheProject.html HTTP/1.1 authority只在CONNECT请求方法中使用。 abs_path:用于表示Server的资源,而Server本身的信息在Host消息头中表示: GET /put/www/TheProject.html HTTP/1.1 Host: www.w3.org Resource Identification rules: 如果Request-URI是absoluteURI,并且这个absolute的host和server的host相同,则忽略Host头。 如果Request-URI不是absoluteURI,并且请求消息包含Host头,host由Host消息头决定。 如果1或2中的host不是一个合法的host,则返回400(Bad Request)响应消息。 响应消息 响应消息的格式定义为:例子:HTTP/1.1 200 OK //请求成功 Server: Microsoft-IIS/5.0 //web服务器 Date: Thu,08 Mar 200707:17:51 GMT Connection: Keep-Alive Content-Length: 23330 Content-Type: text/html Expries: Thu,08 Mar 2007 07:16:51 GMT Set-Cookie: ASPSESSIONIDQAQBQQQB=BEJCDGKADEDJKLKKAJEOIMMH; path=/ Cache-control: private 响应消息也有三部分组成:状态行、消息报头、响应正文。状态行格式:版本号、空格、状态、空格、状态短语、回车、换行。版本号可以是:HTTP/1.0、HTTP/1.1。状态号和状态短语由HTTP协议定义,状态号有5中取值可能:1xx:指示信息--表示请求已经接收,继续处理。2xx:成功--表示请求已经被成功接收、理解、处理。3xx:重定向--要完成请求,必须进行更进一步操作。4xx:客户端错误--请求有语法错误或请求无法实现。5xx:服务器端错误--服务器未能实现合法的请求。常见的状态号和状态短语有:200 OK --请求成功。304 Not Modified --资源没有改变。400 Bad Request --客户端请求有语法错误,不能被服务器理解。401 Unauthorized --请求未经授权(和WWW-Authenticate报头一起使用)。403 Forbidden --服务器收到请求,但是拒绝提供服务。404 Not Found --请求资源不存在。500 Internal Server Error --服务器发生不可预期的错误。503 Server Unavailable --服务器当前不能处理客户端的请求,一段时间后可能恢复正常。响应报头和请求报头格式一样:报头名、冒号(':')、空格、报头值、回车、换行。用于记录响应消息的元数据,表达响应消息的长度、编码方式、Cookiee等信息。遇到一个空行(只有回车换行)表示响应消息报头结束。响应消息正文紧随响应消息报头(在空行后),它可以是任意的内容,由客户端解析。在响应消息中是否包含消息体是由请求方法和响应状态码决定,所有对HEAD请求方法的响应消息不能包含任何消息体,即使在响应消息中可能会包含实体消息头,以至于有人会认为这个响应消息包含消息体。所有1XX(informational)、204(no content)、304(not modified)响应消息不能包含消息体。所有其他的响应消息都包含消息体,即使有些时候消息体的长度是0。杂记 协议本身,最终要的在于消息格式,HTTP协议的请求消息和响应消息已经详细说明了,剩下的就是一些具体细节的问题,比如URI的格式、各种消息报头代表的含义、响应状态号对应的含义等。因为时间有限,不做整理,所以只是一些阅读协议的杂记。URI(Uniform Resource Identifiers),又名:UDI(Universal Document Identifiers),是URL(Uniform Resource Locators)和URN(Unifrom Resource Names)的组合。从HTTP协议的角度,URL只是一个由字符串组成的用于名称、位置等的标识符。在HTTP协议中使用URL作为定位符,它的格式为:http://${host}[:${port}][${abs_path}[?${query}]]Date/Time格式:因为历史原因,HTTP支持三种日期、时间格式:Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsolted by RFC 1036Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format其中第一种格式是推荐的网络格式,而且它是固定长度的。HTTP1.1客户端和服务器端需要能接收所有以上三种日期格式,但是只生成第一种日期格式。所有HTTP日期、时间都必须是格林威治时间(GMT,Greenwich Mean Time),在HTTP中,GMT和UTC(Coordinated Universal Time)时间相同。编码集:同MINE格式规范定义。在Cotent-Type头中定义。内容编码:主要用于对消息实体是否压缩、采用什么压缩算法的表示。在HTTP1.1中使用Accept-Encoding和Content-Encoding头中定义,支持的值有:gzip、compress(废弃)、deflate(zlib格式)、identity(默认不压缩,只能用于Accept-Encoding中,不能用于Content-Encoding)。这些支持的格式在IANA(Internet Assigned Numbers Authority)中注册。传输编码(transfer-coding):用表示可以、需要应用到实体主体以确保通过网络“安全传输”的编码转换。这与内容编码不同,传输编码是消息而非原始实体的属性。所有传输编码值大小写无关,它类似于MINE编码中的Content-Transfer-Encoding。可用的值为:chunked,identity,gzip,compress,deflat。HTTP/1.0不支持。媒体类型:HTTP通过Content-Type和Accept头部域以提供可扩展的数据类型。值格式:${type}/${subtype};${paramName}=${paramValue};....Product符号:用于允许通信应用程序通过软件名称和版本号来标识它自己,比如:User-Agent: CERN-LineMode/2.15 libwww/2.17b3Server: Apache/0.84qvalue:使用[0-1]的值来表达参数的重要性,0表示不可接受,该值的小数部分不可操作三位。语言标签:用于表达消息实体的自然语言,用Accept-Language和Content-Language字段表达。它的值可以是:en、en-US、en-cockney、i-cherokee、x-pig-latin等。实体标签:用于比较相同请求资源的两个或多个实体的比较。如If-Match、If-None-Match、If-Range等头部域名。范围标签:HTTP/1.1允许客户端值请求响应实体的某部分(范围)作为响应消息,如Range、Content-Range头部域,他们的单位在HTTP/1.1中只支持byte。消息报头详解 在请求消息和响应消息中都有消息报头,消息报头在HTTP1.1协议中(RFC2616)有三种类型的头:通用头(General Header)、请求头(Request Header)、响应头(Response Header)、实体头(Entity Header)。其格式为:header-name: header-value。其中header-name大小写无关,以一个空行(只包含回车和换行)结束。header-value可以以任意数量的LWS开头(一般是一个空格)。消息头可以以至少一个SP或HT开头的方式扩展成多行(原文:header fields can be extended over multiple lines by preceding each extra line with at least one SP or HT,感觉理解的有问题....)。相同的header-name可以重复出现。通用消息头: Cache-Control: 指定缓存指令。如请求相关的指令:no-cache、no-store、max-age、max-stale、min-fresh、no-transform、only-if-cached、cache-extension,响应相关的指令:public、private、no-cache、no-store、no-transform、must-revalidate、proxy-revalidate、max-age、s-maxage、cache-extension。 Connection:客户端通过发送包含close值的Connection头,表达在这次请求结束后,Server可以关闭这个连接,此时Server如果选择发送响应后关闭连接,则在响应消息中需要包含值为close的Connection头。 允许发送者指定当前Connection的一些选项。HTTP/1.1只定义了close的值,表示响应返回后,当前Connection将会被关闭。 Date: 表示消息发送的时间,你的描述格式由RFC822定义。例如Mon, 31 Dec 2001 04:25:57GMT Pragma: 用于包含实现相关的指令。如no-cache Trailer: 表示指定的头在chunked消息的尾部。 Transfer-Encoding: 消息在传输时使用的编码。如chunked。 Upgrade: 允许客户端指定它额外支持的传输协议,如果服务器发现更新的传输协议更合适当前请求,则它可以将当前传输协议转换成更新的传输协议。如HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11等。 Via: 用于网关或代理服务器,以指示客户端和服务器之间的中间协议和接收者。 Warning: 用于添加一些额外的状态或转换信息。 请求消息头: 请求消息头允许客户端传递一些额外关乎客户端信息给Server,这些字段类似在方法调用中的参数。 Accept: 指定当前请求响应可以接受的媒体类型,以逗号间隔。如:“audio/*; q=0.2, audio/basic, text/html, */*”。 Accept-Charset: 指定当前请求响应可以接受的字符编码集,以逗号间隔。如“iso-8859-5, Unicode-1-1; q=0.8”。 Accept-Encoding: 类似Accept,定义消息实体的编码方式。如“compress, gzip, *, identity; q=0.5”等。 Accept-Language: 类似Accept,定义自然语言的限制。如“da, en-gb; q=0.8, en; q=0.7”等。 Authorization: 客户端向服务器传递认证信息。 Expect:客户端发送一个包含100-continue值的Expect字段头,以在不发送真正消息实体的情况下测试服务器是否能接收这个消息。此时Server响应417(Expectation Failed)或100(Continue),然后客户端决定是否要继续发送请求消息体。 请求消息头,用于指定客户端对服务器端响应行为的需求。如100-continue、102-processing等。 From: 请求头,指定用户的email地址。 Host: 请求头,指定服务器的主机名和端口。如:www.w3.org:8080 If-Match: 请求头,用于条件请求方法。 If-Modified-Since: 请求头,用于条件请求方法:请求变体自从指定的时间内没有发生改变。 If-None-Match: 请求头,用于条件请求方法。 If-Range: 表示如果实体没有变法,则发送给客户端指定部分的实体。 If-Unmodified-Since: 如果实体在指定时间内没有发生变化,则直接发送响应,否则返回412(Precondition Failed)的响应。 Max-Forwards: 请求头,指定最大可以被代理、网关服务器转发的次数。 Proxy-Authorization: 请求头,用于客户端包含对代理服务器的认证信息。 Range: 指示范围,如bytes=0-499 Referer: 请求头,允许客户端指定当前URI是从哪个URI中获得的。 TE: 请求头,用于指示在响应中希望接收的扩展传输编码。如deflate、trailers, deflate;q=0.5等。 User-Agent: 请求头,用于添加客户端软件信息。 响应消息头 响应消息头允许Server传递一些关于响应的额外信息给客户端。 Accept-Ranges: 响应头,允许服务器指定它可接受的请求范围。如“bytes”、“none”等。 Age: 响应头,当代理服务器用自己缓存的实体去响应请求时,该头部表示该实体从产生到现在经过多少时间了。该数值的代为秒。 ETag: 响应消息头,用于指定请求变体中的实体标签的当前值。如xyzzy,W/xyzzy等。 Location: 响应头,用于指示接收方重定向。 Proxy-Authenticate: 响应头,在407(Proxy Authenticate Required)响应中,它包含代理服务器需要的验证模式和参数。 Retry-After: 响应头,通503(Service Unavailable)响应一起使用,用于指定服务器预计不可用时间;或者3xx,用于指定客户端在重定向之前等待的时间。 Server: 响应头,用于添加服务器软件信息。 Vary: 用于指示用于决定当响应是最新时,是否cache可以用于接下来的响应并且不用验证的请求字段集合。 WWW-Authenticate: 响应头,用于401(Unauthorized)响应消息中,用于指定服务器需要的认证模式和参数。 实体消息头 实体消息头属于实体的一部分,是实体的元数据。 Allow: 实体头,列出所有对当前Request-URI指定资源支持的方法。 Content-Encoding: 实体头,用于指定实体内容的编码方式。如gzip、identity等。 Content-Language: 实体头,用于定义实体内容的自然语言。如da、en、mi等。 Content-Length: 实体头,用于指定实体内容的长度。 Content-Location: 实体头,用于指定资源所在的URI。 Content-MD5: 实体头,用于表示实体内容的数字摘要。 Content-Range: 实体头,用于指定实体内容的范围。如bytes 0-499/1234(即单位 范围/总长度)。 Content-Type: 实体头,用于指定实体内容的媒体类型。如text/html; charset=ISO-8859-4等。 Expires: 实体头,用于响应在多少时间后在Cache中失效。 Last-Modified: 实体头,用于指定服务器认为当前变体的修改时间。 响应状态码详解 1xx: Informational - Request received, continuing process 这是一个临时性的响应,在HTTP/1.0协议中不存在,因而不可以向HTTP/1.0的客户端发送该状态码响应。客户端必须在获得正常响应之前能接收一个或多个1xx响应,即使它并没有预计会收到1xx响应。该响应码只有状态行和可选的响应消息头,没有响应消息实体。 100 - Continue客户端应该继续它的请求,这个暂时的响应用于通知客户端初始的请求已经被服务器接受,并且暂时没有被拒绝。此时客户端会继续发送剩余的请求,或者当所有请求已经发送完成时忽略该响应码。服务器必须在请求结束时发送一个最终的响应。 101 - Switching Protocols服务器理解并打算执行客户端的请求,并且使用“Upgrade”字段头用户表示服务器会在这个连接中的协议升级到“Upgrade”头标识的版本号。 2xx: Success - The action was successfully received, understood, and accepted 这个系列的响应状态码表示客户端的请求已经成功的接收并处理。 200 - OK请求成功处理。 201 - Created请求成功处理并且新的资源被创建。新创建的资源可以使用URI标识,并且该URI在响应消息的Location头中。服务器在返回201响应时必须保证新的资源已经被创建,如果服务器在返回响应时还没来得及创建新的资源,服务器应该返回202(Accepted)响应。 201响应还可以包含“ETag”响应头,表示实体标签的当前值。 202 - Accepted请求被接受并处理,但是处理还未完成。这个请求不一定被成功执行,并且也不会在有结果后重新异步发送响应消息。该响应状态主要用于一些类似batch的操作,当客户发送请求以后,不需要继续保持和服务器的连接。返回的消息实体需要包含请求当前的状态以及一个指向状态监视器或客户能得到结果的估计值。 203 - Non-Authoritative Information响应消息实体头部返回的元信息不是在原始服务器有效的集合,而是从本地或第三方中拷贝收集。当前的集合可能是原始集合的子集或超集,这个响应码不是必须的,可以使用200(OK)替代。 204 - No Content服务器已经成功的完成请求,该请求没有消息实体,只是返回一些最新的元信息。 205 - Reset Content服务器已经成功的完成请求,客户端必须重置由该请求引起的文档视图。该响应主要用于清除用于之前输入的表单。该响应不可以包含消息实体。 206 - Partial Content服务器已经成功完成“Partial GET”的请求。该响应的请求必须包含“Range”头,以及可选的“If-Range”头。响应必须包含以下头:Content-Range(或值为multipart/byteranges的Content-Type头)、Date、ETag或Content-Location、Expires、Cache-Control、Vary等。 3xx: Redirection - Further action must be taken in order to complete the request 这个系列的状态码表示为了完成当前请求,客户端必须要有进一步的处理。如果接下来的请求方法是GET或者HEAD,客户端可以自行发送接下来的请求,而不需要用于干预。并且客户端应该能检测到死循环以减少网络的堵塞。 300 - Multiple Choices当前请求包含多个资源,并且在返回消息中包含每个资源的location信息。客户端可以根据一定的算法自行选择使用那个资源(没有定义算法)。服务器也可以指定一个推荐的选择(在Location头中),客户端可能会使用这个值重定向。 301 - Moved Permanently请求的资源已经被永久的移动到一个新的URI上。客户端可以自动跳转到新的URI上。新的URI需要在响应消息的Location头中包含。 302 - Found请求的资源临时的存在于另一个URI中。因为这个重定向还可能会改变,客户端需要继续使用旧的URI。临时的URI需要包含在响应消息的Location头中。 303 - See Other请求的响应可以使用另一个URI中获得,并且必须使用GET方法获取另一个URI上的响应。该响应码主要用于将一个POST产生的输出重定向到一个新选择的资源上。新的URI需要在Location响应头中给出。 304 - Not Modified如果客户端发送一个“Conditional GET”请求,并且该请求是被允许的,但是它所对应的文档没有改变,则服务器返回该响应。该响应不能包含消息体,但必须包含一些消息头:Date、ETag、Content-Location、Expires、Cache-Control、Vary。 305 - Use Proxy请求的资源必须通过Proxy使用Location响应头中的URI访问。 306 - Unused 以前版本使用,现在已经不使用,但是响应码保留。 307 - Temporary Redirect请求的资源临时的指向另一个URI,但是由于这个重定向可能会在将来被更改,因而客户端需要继续使用原来的URI。临时的URI在Location响应头中指定。 4xx: Client Error - The request contains bad syntax or connote be fulfilled 这个系列的响应码用于表示客户端错误请求,并且在响应实体消息中需要包含出错原因的解释(对HEAD的响应除外)。 400 - Bad Request语法错误,请求不能被服务器理解。 401 - Unauthorized请求需要包含用于认证。响应必须包含WWW-Authenticate头,包含请求认证需要的信息。客户端可以使用包含Authorization头重新发送请求。 402 - Payment Required为将来使用保留。 403 - Forbidden服务器拒绝该请求。如果服务器希望让客户端知道拒绝的原因,可以将原因放在响应消息体重,如果服务器想暴露该原因,则可以返回404(Not Found)响应。 404 - Not Found服务器没有发现任何匹配的请求URI。如果服务器知道某些资源已经永久的被移出,并且没有重定向地址,则需要返回410(Gone)响应。该响应也可以用于服务器不想暴露客户请求被拒绝的原因。 405 - Method Not Allowed请求方法不被对请求的资源允许。在响应消息中必须包含Allow头,指定请求资源允许的请求方法。 406 - Not Acceptable请求的资源产生的响应包含了不被Accept请求头指定的特性。 407 - Proxy Authentication Required类似401(Unauthorized),表示客户端必须在Proxy中通过认证。Proxy必须返回Proxy-Authenticate头,包含请求认证需要的信息。 408 - Request Time-out服务器已经准备好并在等待,但是客户端在指定的时间里没有发送请求。 409 - Conflict因为和资源当前状态冲突而导致请求没有完成。该响应码只有在用户知道任何解决这个冲突,并且重新提交请求时产生。 410 - Gone请求的资源已经不在服务器上,并且没有更进一步的重定向地址。 411 - Length Required请求消息必须包含Content-Length消息头。 412 - Precondition Failed服务器对一个或多个请求消息头的测试失败。 413 - Request Entity Too Large请求消息太大。如果这个条件是临时的,则服务器需要包含Retry-After响应头,表示这个响应时临时的,并在指定的时间以后重试。 414 - Request-URI Too Large请求的URI太长。 415 - Unsupported Media Type请求消息格式不被支持。 416 - Request range not satisfiable在请求包含Range头,不包含If-Range头,并且请求的资源不在Range指定的范围中。响应头中需要包含Content-Range表示指定资源当前的长度。 417 - Expectation FailedExpect请求头指定的值不能匹配服务器的逻辑。 5xx: Server Error - The server failed to fulfill an apparently valid request 这个系列的响应码用于表示服务器存在错误,不能完成相应的请求。服务器需要在响应消息体中包含出错的描述信息。 500 - Internal Server Error服务器内部错误。 501 - Not Implemented服务器没有实现当前请求。如没有实现对应的请求方法。 502 - Bad Gateway代理或网关服务器从上游服务器中接收到一个不合法的响应。 503 - Service Unavailable服务器因为临时负载过重或处于维护状态而不能处理请求。该响应暗示服务器当前的状态是临时的,如果服务器知道什么时候恢复可用状态,则可以包含Retry-After响应头,如果没有包含Retry-After头,则客户端可以把它视为500(Internal Server Error)来处理。 504 - Gateway Time-out代理服务器或网关服务器在指定的时间内没有收到上游服务器的响应。 505 - HTTP Version not supported服务器不支持或拒绝支持请求消息中指定的HTTP版本。 参考:RFC2616RFC1867http://blog.zhaojie.me/2011/03/html-form-file-uploading-programming.htmlhttp://www.cnblogs.com/li0803/archive/2008/11/03/1324746.htmlhttp://www.360doc.com/content/10/0930/17/3668821_57590979.shtml
当前JDK对并发编程的支持 Sun在Java5中引入了concurrent包,它对Java的并发编程提供了强大的支持。首先,它提供了Lock接口,可用了更细粒度的控制锁的区域,它的实现类有ReentrantLock,ReadLock,WriteLock,其中ReadLock和WriteLock共同用于实现ReetrantReadWriteLock(它继承自ReadWriteLock,但是没有实现Lock接口,ReadWriteLock接口也没有继承Lock接口)。而且,它还提供了一些常用并发场景下的类工具:Semaphore、CountDownLatch和CyclicBarrier。它们个字的应用场景: Semaphore(信号量) 有n个非线程安全的资源(资源池),这些资源使用一个Semaphore(计数信号量)保护,每个线程在使用这些资源时需要首先获得一个信号量(acquire)表示当前资源池还有可用资源,然后线程从该资源池中获取并移除一个资源,在使用完后,将该资源交回给资源池,并释放已经获得信号量(release)(这里的“移除”、“交回”并不一定需要显示操作,只是一种形象的描述,之所以这么描述是应为这里的各个资源是一样的,因而对一个线程它每次拿到的资源不一定是同一个资源,用于区分Stripe的使用场景),其中Pool是一种典型的应用。 CountDownLatch(闭锁) 有n个Task,它们执行完成后需要执行另外一个收尾的Task(Aggregated Task),比如在做Report计算中,有n个Report要计算,而在所有Report计算完成后需要生成一个基于所有Report结果的一个总的Report,而这个总的Report需要等到所有Report计算出结果后才能开始,此时就可以定义一个CountDownLatch,其初始值是n,在总的Report计算前调用CountDownLatch的await方法等待其他Report执行完成,而其他Report在完成后都会调用CountDownLatch中的countDown方法。 CyclicBarrier(关卡) 每个线程执行完成后需要等待,直到n个线程都执行完成后,才能继续执行,在n个线程执行完成之后,而下一次执行开始之前可以添加自定义逻辑(通过构建CyclicBarrier实例时传入一个Runnable实例自定义逻辑),即在每个线程执行完成后调用CyclicBarrier的await方法并等待(即所谓的关卡),当n个线程都完成后,自定义的Runnable实例会自动被执行(如果存在这样的Runnable实例的话),然后所有线程继续下一次执行。这个现实中的例子没有想到比较合适的。。。。 Exchanger(交换者) Exchanger是一种特殊的CyclicBarrier,它只有两个线程参与,一个生产者,一个消费者,有两个队列共同参与,生产者和消费者各自有一个队列,其中生产者向它的队列添加数据,而消费者从它包含的队列中拿数据,当生产者中的队列满时调用exchange方法,传入自己原有的队列,期待交换得到消费者中空的队列;而当消费者中的队列满时同样调用exchange方法,传入自己的原有队列,期待获取到生产者中已经填满的队列。这样,生产者和消费者可以和谐的生产消费,并且它们的步骤是一致的(不管哪一方比另一方快都会等待另一方)。 最后,Java5中还提供了一些atomic类以实现简单场景下高效非lock方式的线程安全,以及BlockingQueue、Synchronizer、CompletionService、ConcurrentHashMap等工具类。在这里需要特别添加对ConcurrentHashMap的描述,因为Guava中的Stripe就是对ConcurrentHashMap实现思想的抽象。在《Java Core系列之ConcurrentHashMap实现(JDK 1.7)》一文中已经详细讲述了ConcurrentHashMap的实现,我们都知道ConcurrentHashMap的实现是基于Segment的,它内部包含了多个Segment,因而它内部的锁是基于Segment而不是整个Map,从而减小了锁的粒度,提升了性能。而这种分段锁不仅仅在HashMap用到。Stripe的应用场景 虽然JDK中已经为我们提供了很多用于并发编程的工具类,但是它并没有提供对以下应用场景的支持:有n个资源,我们希望对每个资源的操作都是线程安全的,这里我们不能用Semaphore,因为Semaphore是一个池的概念,它所管理的资源是同质的,比如从数据库的连接池中获取Connection操作的一种实现方式是内部保存一个Semaphore变量,在每次获取Connection时,先调用Semaphore的acquire方法以保证连接池中还有空闲的Connection,如果有,则可以随机的选择一个Connection实例,当Connection实例返回时,该Connection实例必须从空闲列表中移除,从而保证只有一个线程获取到Connection,以保证一次只有一个线程使用一个Connection(在Java中数据库的Connection是线程安全,但是我们在使用时依然会用连接池的方式创建多个Connection而不是在一个应用程序中只用一个Connection是因为有些数据库厂商在实现Connection时,一个Connection内的所有操作都时串行的,而不是并行的,比如MySQL的Connection实现,因而为了提升并行性,采用多个Connection方式)。而这里的需求是对每个资源的操作都是线程安全的,比如对JDK中HashMap的实现采用一个数组链表的结构(参考《Java Core系列之HashMap实现》),如果我们将链表作为一个资源单位(这里的链表资源和上述的数据库连接资源是不一样的,对数据库连接每个线程只需要拿到任意一个Connection实例即可,而这里的链表资源则是不同链表是不一样的,因而对每个操作,我们需要获取特定的链表,然后对链表以线程安全的方式操作,因为这里多个线程会对同一个链表同时操作),那么为了保证对各个单独链表操作的线程安全(如HashMap的put操作,不考虑rehash的情况,有些其他操作需要更大粒度的线程安全,比如contains等),其中一种简单的实现方式是为每条链表关联一个锁,对每条链表的读写操作使用其关联锁即可。然而如果链表很多,就需要使用很多锁,会消耗很多资源,虽然它的锁粒度最小,并发性很高。然而如果各个链表之间没有很高的并发性,我们就可以让多个链表共享一个锁以减少锁的使用量,虽然增大了锁的粒度,但是如果这些链表的并发程度并不是很高,那增大的锁的粒度对并发性并没有很大的影响。在实际应用中,我们有一个Cache系统,它包含key和payload的键值对(Map),在Cache中Map的实现已经是线程安全了,然而我们不仅仅是向Cache中写数据要保证线程安全,在操作payload时,也需要保证线程安全。因为我们在Cache中的数据量很大,为每个payload配置一个单独的锁显然不现实,也不需要因为它们没有那么高的并发行,因而我们需要一种机制将key分成不同的group,而每个group共享一个锁(这就是ConcurrentHashMap的实现思路)。通过key即可获得一个锁,并且每个相同的key获得的锁实例是相同的(获得相同锁实例的key它们不一定相等,因为这是一对多的关系)。Stripe的简单实现 根据以上应用场景,Stripe的实现很简单,只需要内部保存一个Lock数组,对每个给定的key,计算其hash值,根据hash值计算其锁对应的数组下标,而该下标下的Lock实例既是和该key关联的Lock实例。这里通过hash值把key和Lock实例关联起来,为了扩展性,在实现时还可以把计算数组下标的逻辑抽象成一个接口,用户可以通过传入自定义该接口的实现类实例加入用户自定义的关联逻辑,默认采用hash值关联方式。Stripe在Guava中的实现 在Guava中,Stripe以抽象类的形式存在,它定义了通过给定key或index获得相应Lock/Semaphore/ReadWriteLock实例: public abstract class Striped<L> { /** * Returns the stripe that corresponds to the passed key. It is always guaranteed that if * {@code key1.equals(key2)}, then {@code get(key1) == get(key2)}. * * @param key an arbitrary, non-null key * @return the stripe that the passed key corresponds to */ public abstract L get(Object key); /** * Returns the stripe at the specified index. Valid indexes are 0, inclusively, to * {@code size()}, exclusively. * * @param index the index of the stripe to return; must be in {@code [0size())} * @return the stripe at the specified index */ public abstract L getAt(int index); /** * Returns the index to which the given key is mapped, so that getAt(indexFor(key)) == get(key). */ abstract int indexFor(Object key); /** * Returns the total number of stripes in this instance. */ public abstract int size(); /** * Returns the stripes that correspond to the passed objects, in ascending (as per * {@link #getAt(int)}) order. Thus, threads that use the stripes in the order returned * by this method are guaranteed to not deadlock each other. * * <p>It should be noted that using a {@code Striped<L>} with relatively few stripes, and * {@code bulkGet(keys)} with a relative large number of keys can cause an excessive number * of shared stripes (much like the birthday paradox, where much fewer than anticipated birthdays * are needed for a pair of them to match). Please consider carefully the implications of the * number of stripes, the intended concurrency level, and the typical number of keys used in a * {@code bulkGet(keys)} operation. See <a href="http://www.mathpages.com/home/kmath199.htm">Balls * in Bins model</a> for mathematical formulas that can be used to estimate the probability of * collisions. * * @param keys arbitrary non-null keys * @return the stripes corresponding to the objects (one per each object, derived by delegating * to {@link #get(Object)}; may contain duplicates), in an increasing index order. */ public Iterable<L> bulkGet(Iterable<?> keys); } 可以使用一下几个静态工厂方法创建相应的Striped实例,其中lazyWeakXXX创建的Striped实例中锁以弱引用的方式存在(在什么样的场景中使用呢?): /** * Creates a {@code Striped<Lock>} with eagerly initialized, strongly referenced locks. * Every lock is reentrant. * * @param stripes the minimum number of stripes (locks) required * @return a new {@code Striped<Lock>} */public static Striped<Lock> lock(int stripes);/** * Creates a {@code Striped<Lock>} with lazily initialized, weakly referenced locks. * Every lock is reentrant. * * @param stripes the minimum number of stripes (locks) required * @return a new {@code Striped<Lock>} */public static Striped<Lock> lazyWeakLock(int stripes);/** * Creates a {@code Striped<Semaphore>} with eagerly initialized, strongly referenced semaphores, * with the specified number of permits. * * @param stripes the minimum number of stripes (semaphores) required * @param permits the number of permits in each semaphore * @return a new {@code Striped<Semaphore>} */public static Striped<Semaphore> semaphore(int stripes, final int permits);/** * Creates a {@code Striped<Semaphore>} with lazily initialized, weakly referenced semaphores, * with the specified number of permits. * * @param stripes the minimum number of stripes (semaphores) required * @param permits the number of permits in each semaphore * @return a new {@code Striped<Semaphore>} */public static Striped<Semaphore> lazyWeakSemaphore(int stripes, final int permits);/** * Creates a {@code Striped<ReadWriteLock>} with eagerly initialized, strongly referenced * read-write locks. Every lock is reentrant. * * @param stripes the minimum number of stripes (locks) required * @return a new {@code Striped<ReadWriteLock>} */public static Striped<ReadWriteLock> readWriteLock(int stripes);/** * Creates a {@code Striped<ReadWriteLock>} with lazily initialized, weakly referenced * read-write locks. Every lock is reentrant. * * @param stripes the minimum number of stripes (locks) required * @return a new {@code Striped<ReadWriteLock>} */public static Striped<ReadWriteLock> lazyWeakReadWriteLock(int stripes); Striped有两个具体实现类,CompactStriped和LazyStriped,他们都继承自PowerOfTwoStriped(用于表达内部保存的stripes值是2的指数值)。PowerOfTwoStriped实现了indexFor()方法,它使用hash值做映射函数: private abstract static class PowerOfTwoStriped<L> extends Striped<L> { /** Capacity (power of two) minus one, for fast mod evaluation */ final int mask; @Override final int indexFor(Object key) { int hash = smear(key.hashCode()); return hash & mask; } } private static int smear(int hashCode) { hashCode ^= (hashCode >>> 20) ^ (hashCode >>> 12); return hashCode ^ (hashCode >>> 7) ^ (hashCode >>> 4); } CompactStriped类使用一个数组保存所有的Lock/Semaphore/ReadWriteLock实例,在初始化时就建立所有的锁实例;而LazyStriped类使用一个值为WeakReference的ConcurrentMap做为数据结构,index值为key,Lock/Semaphore/ReadWriteLock的WeakReference为值,所有锁实例在用到时动态创建。在CompactStriped中创建锁实例时对ReentrantLock/Semaphore创建采用PaddedXXX版本,不知道为何要做Pad。Striped类实现的类图如下:
解决方案一:通过maven取运行时参数,eclipse提供的环境变量,基本类似System.getProperty("java.home") <dependency> <groupId>com.sun</groupId> <artifactId>tools</artifactId> <version>1.5.0</version> <scope>system</scope> <systemPath>${java.home}/lib/tools.jar</systemPath> </dependency> 如果不起作用的话,$(java.home)eclipse解析的不对,eclipse 没有使用 JAVA_HOME 默认,eclipse 使用 C:"windows"system32"javaw.exe 作为 JVM,当然找不到tools.jar 解决方法如下: 修改 eclipse.exe 目录下的 eclipse.ini 指定vm,注意 -vm后面不能有空格。 -vm D:\Program Files\Java\jdk1.6.0_23\bin\javaw.exe -vmargs -Dosgi.requiredJavaVersion=1.6 -Xms40m -Xmx512m -XX:PermSize=64M -XX:MaxPermSize=512M 注意: 要写在两行,写在一行不能生效 注意: 这两行要定在-vmargs之前,不然也不能生效 解决方案二: <properties> <project.build.sourceEncoding>UTF8</project.build.sourceEncoding> <java.home>C:\Program Files\Java\jdk1.6.0_25</java.home> </properties> <profiles> <profile> <id>default-tools.jar</id> <activation> <activeByDefault>true</activeByDefault> <property> <name>java.vendor</name> <value>Sun Microsystems Inc.</value> </property> </activation> <dependencies> <dependency> <groupId>com.sun</groupId> <artifactId>tools</artifactId> <version>1.5.0</version> <scope>system</scope> <systemPath>${java.home}/lib/tools.jar</systemPath> </dependency> </dependencies> </profile> </profiles> 通过profile来设置,方便决定是否启用 解决方案三: <dependency> <groupId>com.sun</groupId> <artifactId>tools</artifactId> <version>1.6.0</version> <scope>system</scope> <systemPath>${env.JAVA_HOME}/lib/tools.jar</systemPath> <optional>true</optional> </dependency> 直接使用Maven获取系统的环境变量 原文:http://drizzlewalk.blog.51cto.com/2203401/1054211
写了那么多,终于到Store了。Store是EHCache中Element管理的核心,所有的Element都存放在Store中,也就是说Store用于所有和Element相关的处理。EHCache中的Element 在EHCache中,它将所有的键值对抽象成一个Element,作为面向对象的设计原则,把数据和操作放在一起,Element除了包含key、value属性以外,它还加入了其他和一个Element相关的统计、配置信息以及操作: public class Element implements Serializable, Cloneable { //the cache key. 从1.2以后不再强制要求Serializable,因为如果只是作为内存缓存,则不需要对它做序列化。IgnoreSizeOf注解表示在做SizeOf计算时key会被忽略。 @IgnoreSizeOf private final Object key; //the value. 从1.2以后不再强制要求Serializable,因为如果只是作为内存缓存,则不需要对它做序列化。 private final Object value; //version of the element. 这个属性只是作为纪录信息,EHCache实际代码中并没有用到,用户代码可以通过它来实现不同版本的处理问题。默认值是1。 //如果net.sf.ehcache.element.version.auto系统属性设置为true,则当Element加入到Cache中时会被更新为当前系统时间。此时,用户设置的值会丢失。 private volatile long version; //The number of times the element was hit.命中次数,在每次查找到一个Element会加1。 private volatile long hitCount; //The amount of time for the element to live, in seconds. 0 indicates unlimited. 即一个Element自创建(CreationTime)以后可以存活的时间。 private volatile int timeToLive = Integer.MIN_VALUE; //The amount of time for the element to idle, in seconds. 0 indicates unlimited. 即一个Element自最后一次被使用(min(CreationTime,LastAccessTime))以后可以存活的时间。 private volatile int timeToIdle = Integer.MIN_VALUE; //Pluggable element eviction data instance,它存储这个Element的CreationTime、LastAccessTime等信息,窃以为这个抽取成一个单独的类没什么理由,而且这个类的名字也不好。 private transient volatile ElementEvictionData elementEvictionData; //If there is an Element in the Cache and it is replaced with a new Element for the same key, //then both the version number and lastUpdateTime should be updated to reflect that. The creation time //will be the creation time of the new Element, not the original one, so that TTL concepts still work. 在put和replace操作中该属性会被更新。 private volatile long lastUpdateTime; //如果timeToLive和timeToIdle没有手动设置,该值为true,此时在计算expired时使用CacheConfiguration中的timeTiLive、timeToIdle的值,否则使用Element自身的值。 private volatile boolean cacheDefaultLifespan = true; //这个ID值用于EHCache内部,但是暂时不知道怎么用。 private volatile long id = NOT_SET_ID; //判断是否expired,这里如果timeToLive、timeToIdle都是Integer.MIN_VALUE时返回false,当他们都是0时,isEternal返回true public boolean isExpired() { if (!isLifespanSet() || isEternal()) { return false; } long now = System.currentTimeMillis(); long expirationTime = getExpirationTime(); return now > expirationTime; } //expirationTime算法:如果timeToIdle没有设置,或设置了,但是该Element还没有使用过,取timeToLive计算出的值;如果timeToLive没有设置,则取timeToIdle计算出的值, //否则,取他们的最小值。 public long getExpirationTime() { if (!isLifespanSet() || isEternal()) { return Long.MAX_VALUE; } long expirationTime = 0; long ttlExpiry = elementEvictionData.getCreationTime() + TimeUtil.toMillis(getTimeToLive()); long mostRecentTime = Math.max(elementEvictionData.getCreationTime(), elementEvictionData.getLastAccessTime()); long ttiExpiry = mostRecentTime + TimeUtil.toMillis(getTimeToIdle()); if (getTimeToLive() != 0 && (getTimeToIdle() == 0 || elementEvictionData.getLastAccessTime() == 0)) { expirationTime = ttlExpiry; } else if (getTimeToLive() == 0) { expirationTime = ttiExpiry; } else { expirationTime = Math.min(ttlExpiry, ttiExpiry); } return expirationTime; } //在将Element加入到Cache中并且它的timeToLive和timeToIdle都没有设置时,它的timeToLive和timeToIdle会根据CacheConfiguration的值调用这个方法更新。 protected void setLifespanDefaults(int tti, int ttl, boolean eternal) { if (eternal) { this.timeToIdle = 0; this.timeToLive = 0; } else if (isEternal()) { this.timeToIdle = Integer.MIN_VALUE; this.timeToLive = Integer.MIN_VALUE; } else { timeToIdle = tti; timeToLive = ttl; } } }public class DefaultElementEvictionData implements ElementEvictionData { private long creationTime; private long lastAccessTime; }public class Cache implements InternalEhcache, StoreListener { private void applyDefaultsToElementWithoutLifespanSet(Element element) { if (!element.isLifespanSet()) { element.setLifespanDefaults(TimeUtil.convertTimeToInt(configuration.getTimeToIdleSeconds()), TimeUtil.convertTimeToInt(configuration.getTimeToLiveSeconds()), configuration.isEternal()); } } } EHCache中的Store设计 Store是EHCache中用于存储、管理所有Element的仓库,它抽象出了所有对Element在内存中以及磁盘中的操作。基本的它可以向一个Store中添加Element(put、putAll、putWithWriter、putIfAbsent)、从一个Store获取一个或一些Element(get、getQuiet、getAll、getAllQuiet)、获取一个Store中所有key(getKeys)、从一个Store中移除一个或一些Element(remove、removeElement、removeAll、removeWithWriter)、替换一个Store中已存储的Element(replace)、pin或unpin一个Element(unpinAll、isPinned、setPinned)、添加或删除StoreListener(addStoreListener、removeStoreListener)、获取一个Store的Element数量(getSize、getInMemorySize、getOffHeapSize、getOnDiskSize、getTerracottaClusteredSize)、获取一个Store的Element以Byte为单位的大小(getInMemorySizeInBytes、getOffHeapSizeInBytes、getOnDiskSizeInBytes)、判断一个key的存在性(containsKey、containsKeyOnDisk、containsKeyOffHeap、containsKeyInMemory)、query操作(setAttributeExtractors、executeQuery、getSearchAttribute)、cluster相关操作(isCacheCoherent、isClusterCoherent、isNodeCoherent、setNodeCoherent、waitUtilClusterCoherent)、其他操作(dispose、getStatus、getMBean、hasAbortedSizeOf、expireElements、flush、bufferFull、getInMemoryEvictionPolicy、setInMemoryEvictionPolicy、getInternalContext、calculateSize)。 所谓Cache,就是将部分常用数据缓存在内存中,从而提升程序的效率,然而内存大小毕竟有限,因而有时候也需要有磁盘加以辅助,因而在EHCache中真正的Store实现就两种(不考虑分布式缓存的情况下):存储在内存中的MemoryStore和存储在磁盘中的DiskStore,而所有其他Store都是给予这两个Store的基础上来扩展Store的功能,如因为内存大小的限制,有些时候需要将内存中的暂时不用的Element写入到磁盘中,以腾出空间给其他更常用的Element,此时就需要MemoryStore和DiskStore共同来完成,这就是FrontCacheTier做的事情,所有可以结合FrontEndCacheTier一起使用的Store都要实现TierableStore接口(DiskStore、MemoryStore、NullStore);对于可控制Store占用空间大小做限制的Store还可以实现PoolableStore(DiskStore、MemoryStore);对于具有Terracotta特性的Store还实现了TerracottaStore接口(TransactionStore等)。 EHCache中Store的设计类结构图如下: AbstractStore几乎所有的Store实现都继承自AbstractStore,它实现了Query、Cluster等相关的接口,但没有涉及Element的管理,而且这部分现在也不了解,不详述。MemoryStore和NotifyingMemoryStoreMemoryStore是EHCache中存储在内存中的Element的仓库。它使用SelectableConcurrentHashMap作为内部的存储结构,该类实现参考ConcurrentHashMap,只是它加入了pinned、evict等逻辑,不详述(注:它的setPinned方法中,对不存在的key,会使用一个DUMMY_PINNED_ELEMENT来创建一个节点,并将它添加到HashEntry的链中,这时为什么?窃以为这个应该是为了在以后这个key添加进来后,当前的pinned设置可以对它有影响,因为MemoryStore中并没有包含所有的Element,还有一部分Element是在DiskStore中)。而MemoryStore中的基本实现都代理给SelectableConcurrentHashMap,里面的其他细节在之前的文章中也有说明,不再赘述。 而NotifyingMemoryStore继承自MemoryStore,它在Element evict和exipre时会调用注册的CacheEventListener。DiskStoreDiskStore依然采用ConcurrentHashMap的实现思想,因而这部分逻辑不赘述。对DiskStore,当一个Element添加进来后,需要将其写入到磁盘中,这是接下来关注的重点。在DiskStore中,一个Element不再以Element本身而存在,而是以DiskSubstitute的实例而存在,DiskSubstitute有两个子类:PlaceHolder和DiskMarker,当一个Element初始被添加到DiskStore中时,它是以PlaceHolder的形式存在,当这个PlaceHolder被写入到磁盘中时,它会转换成DiskMarker。 public abstract static class DiskSubstitute { protected transient volatile long onHeapSize; @IgnoreSizeOf private transient volatile DiskStorageFactory factory; DiskSubstitute(DiskStorageFactory factory) { this.factory = factory; } abstract Object getKey(); abstract long getHitCount(); abstract long getExpirationTime(); abstract void installed(); public final DiskStorageFactory getFactory() { return factory; } } final class Placeholder extends DiskSubstitute { @IgnoreSizeOf private final Object key; private final Element element; private volatile boolean failedToFlush; Placeholder(Element element) { super(DiskStorageFactory.this); this.key = element.getObjectKey(); this.element = element; } @Override public void installed() { DiskStorageFactory.this.schedule(new PersistentDiskWriteTask(this)); } } public static class DiskMarker extends DiskSubstitute implements Serializable { @IgnoreSizeOf private final Object key; private final long position; private final int size; private volatile long hitCount; private volatile long expiry; DiskMarker(DiskStorageFactory factory, long position, int size, Element element) { super(factory); this.position = position; this.size = size; this.key = element.getObjectKey(); this.hitCount = element.getHitCount(); this.expiry = element.getExpirationTime(); } @Override public void installed() { //no-op } void hit(Element e) { hitCount++; expiry = e.getExpirationTime(); } } 当向DiskStore添加一个Element时,它会先创建一个PlaceHolder,并将该PlaceHolder添加到DiskStore中,并在添加完成后调用PlaceHolder的installed()方法,该方法会使用DiskStorageFactory schedule一个PersistentDiskWriteTask,将该PlaceHolder写入到磁盘(在DiskStorageFactory有一个DiskWriter线程会在一定的时候执行该Task)生成一个DiskMarker,释放PlaceHolder占用的内存。在从DiskStore移除一个Element时,它会先读取磁盘中的数据,将其解析成Element,然后释放这个Element占用的磁盘空间,并返回这个被移除的Element。在从DiskStore读取一个Element时,它需要找到DiskStore中的DiskSubstitute,对DiskMarker读取磁盘中的数据,解析成Element,然后返回。 FrontEndCacheTier 上述的MemoryStore和DiskStore,他们是各自独立的,然而Cache的一个重要特点是可以将部分内存中的数据evict出到磁盘,因为内存毕竟是有限的,所以需要有另一个Store可以将MemoryStore和DiskStore联系起来,这就是FrontEndCacheTier做的事情。FrontEndCacheTier有两个子类:DiskBackedMemoryStore和MemoryOnlyStore,这两个类的名字已经能很好的说明他们的用途了,DiskBackedMemoryStore可以将部分Element先evict出到磁盘,它也支持把磁盘文件作为persistent介质,在下一次读取时可以直接从磁盘中的文件直接读取并重新构建原来的缓存;而MemoryOnlyStore则只支持将Element存储在内存中。FrontEndCacheTier有两个Store属性:cache和authority,它将基本上所有的操作都直接同时代理给这两个Store,其中把authority作为主的存储Store,而将cache作为缓存的Store。在DiskBackedMemoryStore中,authority是DiskStore,而cache是MemoryStore,即DiskBackedMemoryStore将DiskStore作为主的存储Store,这刚开始让我很惊讶,不过仔细想想也是合理的,因为毕竟这里的Disk是作为persistent介质的;在MemoryOnlyStore中,authority是MemoryStore,而cache是NullStore。FrontEndCacheTier在实现get方法时,添加了faults属性的ConcurrentHashMap,它是用于多个线程在同时读取同一key的Element时避免多次读取,每次之前将key和一个Fault新实例添加到faults中,这样第二个线程发现已经有另一个线程在读这个Element了,它就可以等待第一个线程读完直接拿第一个线程读取的结果即可,以提升性能。所有作为FrontEndCacheTier的内部Store都必须实现TierableStore接口,其中fill、removeIfNotPinned、isTierPinned、getPresentPinnedKeys为cache store准备,而removeNoReturn、isPersistent为authority store准备。 public interface TierableStore extends Store { void fill(Element e); boolean removeIfNotPinned(Object key); void removeNoReturn(Object key); boolean isTierPinned(); Set getPresentPinnedKeys(); boolean isPersistent(); } LruMemoryStore和LegacyStoreWrapper这两个Store只是为了兼容而存在,其中LruMemoryStore使用LinkedHashMap作为其存储结构,他只支持一种Evict算法:LRU,这个Store的名字也因此而来,其他功能它类似MemoryStore,而LegacyStoreWrapper则类似FrontEndCacheTier。这两个Store的代码比较简单,而且他们也不应该再被使用,因而不细究。 TerraccottaStore对所有实现这个接口的Store都还不了解,看以后有没有时间回来了。。。。