
我的个人博客地址:http://wuyudong.com/
理解索引过程中的核心类 欢迎访问我的个人网站http://wuyudong.com/ 执行简单索引的时候需要用的类有 IndexWriter、Directory、Analyzer、Document、Field 1、IndexWriter IndexWriter写索引是索引过程的核心组件这个类负责创建新的索引或者打开已有的索引以及向索引中添加、删除或更新被索引文档的信息但不能读取或搜索索引。IndexWriter需要开辟一定的空间来存储索引该功能由Directory完成 2、Directory /** A Directory is a flat list of files. Files may be written once, when they * are created. Once a file is created it may only be opened for read, or * deleted. Random access is permitted both when reading and writing. * * <p> Java's i/o APIs not used directly, but rather all i/o is * through this API. This permits things such as: <ul> * <li> implementation of RAM-based indices; * <li> implementation indices stored in a database, via JDBC; * <li> implementation of an index as a single file; * </ul> * * Directory locking is implemented by an instance of {@link * LockFactory}, and can be changed for each Directory * instance using {@link #setLockFactory}. * */ Directory描述了索引的存放位置是一个抽象类其子类负责具体指定索引的存放路径 3、Analyzer Analyzer由IndexWriter构造方法指定负责从被索引的文本文件中提取词汇Analyzer是一个抽象类由其子类完成相关的功能 4、Document 代表一些域Field的集合Lucene只能从二进制文档中提取以Field实例形式的文本 5、Field 一篇文档包含不同类型的信息可以分开索引比如标题时间正文作者等都可以保存在不同的域里。 理解索引与搜索过程中的核心类 Lucene提供的搜素接口一样很简单易懂 IndexSearcher、Term、Query、TermQuery、TopDocs 1、IndexSearcher IndexSearcher用于搜索由IndexWriter类创建的索引它需要Directory实例来掌控前期创建的索引然后才能提供大量 的搜索方法。最简单的搜索方法是将单个的Query对象和int topN计数作为该方法的参数并返回一个TopDocs对象该方法的一个典型应用如下 Directory dir = FSDirectory.open(new File("/tmp/index")); IndexSearcher searcher = new IndexSearcher(dir); Query q = new TermQuery(new Term("contents", "lucene")); TopDocs hits = searcher.search(q, 10); searcher.close(); 2、Term Term对象是搜索功能的基本单元。在搜索过程中可以创建Term对象和TermQuery对象一起使用 Query q = new TermQuery(new Term("contents", "lucene")); TopDocs hits = searcher.search(q, 10); 上面代码的含义是寻找content域中包含lucene的前10个文档并按照降序排列 3、Query lucene中包含很多具体的Query查询子类。TermQuery、BooleanQuery、PhraseQuery、 PrefixQuery、 PhrasePrefixQuery、TermRangeQuery、NumericRangeQuery、 FilteredQuery、SpanQuery 4、TermQuery TermQuery是lucene中最基本的查询类型用来匹配指定域中包含特定项的文档 5、TopDocs TopDocs类是一个简单的指针容器指针一般指向前N个排名的搜索结果搜索结果即匹配查询条件的文档
欢迎访问我的个人网站:http://wuyudong.com/ 搭建lucene的步骤这里就不详细介绍了,无外乎就是下载相关jar包,在eclipse中新建java工程,引入相关的jar包即可 本文主要在没有剖析lucene的源码之前实战一下,通过实战来促进研究 建立索引 下面的程序展示了indexer的使用 package com.wuyudong.mylucene; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.store.Directory; import org.apache.lucene.util.Version; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.io.FileReader; public class IndexerTest { public static void main(String[] args) throws Exception { if (args.length != 2) { throw new IllegalArgumentException("Usage: java " + IndexerTest.class.getName() + " <index dir> <data dir>"); } String indexDir = args[0]; //1 指定目录创建索引 String dataDir = args[1]; //2 对指定目录中的*.txt文件进行索引 long start = System.currentTimeMillis(); IndexerTest indexer = new IndexerTest(indexDir); int numIndexed; try { numIndexed = indexer.index(dataDir, new TextFilesFilter()); } finally { indexer.close(); } long end = System.currentTimeMillis(); System.out.println("Indexing " + numIndexed + " files took " + (end - start) + " milliseconds"); } private IndexWriter writer; public IndexerTest(String indexDir) throws IOException { Directory dir = FSDirectory.open(new File(indexDir)); writer = new IndexWriter(dir, //3 创建IndexWriter new StandardAnalyzer( //3 Version.LUCENE_30),//3 true, //3 IndexWriter.MaxFieldLength.UNLIMITED); //3 } public void close() throws IOException { writer.close(); //4 关闭IndexWriter } public int index(String dataDir, FileFilter filter) throws Exception { File[] files = new File(dataDir).listFiles(); for (File f: files) { if (!f.isDirectory() && !f.isHidden() && f.exists() && f.canRead() && (filter == null || filter.accept(f))) { indexFile(f); } } return writer.numDocs(); //5 返回被索引的文档数 } private static class TextFilesFilter implements FileFilter { public boolean accept(File path) { return path.getName().toLowerCase() //6 只索引*.txt文件,采用FileFilter .endsWith(".txt"); //6 } } protected Document getDocument(File f) throws Exception { Document doc = new Document(); doc.add(new Field("contents", new FileReader(f))); //7 索引文件内容 doc.add(new Field("filename", f.getName(), //8 索引文件名 Field.Store.YES, Field.Index.NOT_ANALYZED));//8 doc.add(new Field("fullpath", f.getCanonicalPath(), //9 索引文件完整路径 Field.Store.YES, Field.Index.NOT_ANALYZED));//9 return doc; } private void indexFile(File f) throws Exception { System.out.println("Indexing " + f.getCanonicalPath()); Document doc = getDocument(f); writer.addDocument(doc); //10 向Lucene索引中添加文档 } } 在eclipse中配置好参数: E:\luceneinaction\index E:\luceneinaction\lia2e\src\lia\meetlucene\data 运行结果如下: Indexing E:\luceneinaction\lia2e\src\lia\meetlucene\data\apache1.0.txtIndexing E:\luceneinaction\lia2e\src\lia\meetlucene\data\apache1.1.txtIndexing E:\luceneinaction\lia2e\src\lia\meetlucene\data\apache2.0.txtIndexing E:\luceneinaction\lia2e\src\lia\meetlucene\data\cpl1.0.txtIndexing E:\luceneinaction\lia2e\src\lia\meetlucene\data\epl1.0.txtIndexing E:\luceneinaction\lia2e\src\lia\meetlucene\data\freebsd.txtIndexing E:\luceneinaction\lia2e\src\lia\meetlucene\data\gpl1.0.txtIndexing E:\luceneinaction\lia2e\src\lia\meetlucene\data\gpl2.0.txtIndexing E:\luceneinaction\lia2e\src\lia\meetlucene\data\gpl3.0.txtIndexing E:\luceneinaction\lia2e\src\lia\meetlucene\data\lgpl2.1.txtIndexing E:\luceneinaction\lia2e\src\lia\meetlucene\data\lgpl3.txtIndexing E:\luceneinaction\lia2e\src\lia\meetlucene\data\lpgl2.0.txtIndexing E:\luceneinaction\lia2e\src\lia\meetlucene\data\mit.txtIndexing E:\luceneinaction\lia2e\src\lia\meetlucene\data\mozilla1.1.txtIndexing E:\luceneinaction\lia2e\src\lia\meetlucene\data\mozilla_eula_firefox3.txtIndexing E:\luceneinaction\lia2e\src\lia\meetlucene\data\mozilla_eula_thunderbird2.txtIndexing 16 files took 888 milliseconds 在index文件内会产生索引文件: 由于被索引的文件都很小,数量也不大(如下图),但是会花费888ms,还是很让人不安 总体说来,搜索索引比建立索引重要,因为搜索很多次,而索引只是建立一次 搜索索引 接下来将创建一个程序 来对上面创建的索引进行搜索: import org.apache.lucene.document.Document; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TopDocs; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.store.Directory; import org.apache.lucene.queryParser.QueryParser; import org.apache.lucene.queryParser.ParseException; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.util.Version; import java.io.File; import java.io.IOException; public class SearcherTest { public static void main(String[] args) throws IllegalArgumentException, IOException, ParseException { if (args.length != 2) { throw new IllegalArgumentException("Usage: java " + SearcherTest.class.getName() + " <index dir> <query>"); } String indexDir = args[0]; //1 解析输入的索引路径 String q = args[1]; //2 解析输入的查询字符串 search(indexDir, q); } public static void search(String indexDir, String q) throws IOException, ParseException { Directory dir = FSDirectory.open(new File(indexDir)); //3 打开索引文件 IndexSearcher is = new IndexSearcher(dir); //3 QueryParser parser = new QueryParser(Version.LUCENE_30, // 4 解析查询字符串 "contents", //4 new StandardAnalyzer( //4 Version.LUCENE_30)); //4 Query query = parser.parse(q); //4 long start = System.currentTimeMillis(); TopDocs hits = is.search(query, 10); //5 搜索索引 long end = System.currentTimeMillis(); System.err.println("Found " + hits.totalHits + //6 记录索引状态 " document(s) (in " + (end - start) + // 6 " milliseconds) that matched query '" + // 6 q + "':"); // 6 for(ScoreDoc scoreDoc : hits.scoreDocs) { Document doc = is.doc(scoreDoc.doc); //7 返回匹配文本 System.out.println(doc.get("fullpath")); //8 显示匹配文件名 } is.close(); //9 关闭IndexSearcher } } 设置好参数:E:\luceneinaction\index patent 运行结果如下: Found 8 document(s) (in 12 milliseconds) that matched query 'patent':E:\luceneinaction\lia2e\src\lia\meetlucene\data\cpl1.0.txtE:\luceneinaction\lia2e\src\lia\meetlucene\data\mozilla1.1.txtE:\luceneinaction\lia2e\src\lia\meetlucene\data\epl1.0.txtE:\luceneinaction\lia2e\src\lia\meetlucene\data\gpl3.0.txtE:\luceneinaction\lia2e\src\lia\meetlucene\data\apache2.0.txtE:\luceneinaction\lia2e\src\lia\meetlucene\data\gpl2.0.txtE:\luceneinaction\lia2e\src\lia\meetlucene\data\lpgl2.0.txtE:\luceneinaction\lia2e\src\lia\meetlucene\data\lgpl2.1.txt 可以看到速度很快(12ms),打印的是文件的绝对路径,这是因为indexer存储的是文件的绝对路径
欢迎访问我的个人网站:http://wuyudong.com/ Lucene总的来说是: • 一个高效的,可扩展的,全文检索库。 • 全部用Java实现,无须配置。 • 仅支持纯文本文件的索引(Indexing)和搜索(Search)。 • 不负责由其他格式的文件抽取纯文本文件,或从网络中抓取文件的过程。 在Lucene in action中,Lucene 的构架和过程如下图 说明Lucene是有索引和搜索的两个过程,包含索引创建,索引,搜索三个要点。 让我们更细一些看Lucene的各组件 • 被索引的文档用Document对象表示。 • IndexWriter通过函数addDocument将文档添加到索引中,实现创建索引的过程。 • Lucene的索引是应用反向索引。 • 当用户有请求时,Query代表用户的查询语句。 • IndexSearcher通过函数search搜索Lucene Index。 • IndexSearcher计算term weight和score并且将结果返回给用户。 • 返回给用户的文档集合用TopDocsCollector表示。 那么如何应用这些组件呢? 让我们再详细到对Lucene API 的调用实现索引和搜索过程。 索引过程如下: ◦ 创建一个IndexWriter用来写索引文件,它有几个参数,INDEX_DIR就是索引文件所存放的位置,StandardAnalyzer便是用来对文档进行词法分析和语言处理的。 ◦ 创建一个Document代表我们要索引的文档。 ◦ 将不同的Field加入到文档中。我们知道,一篇文档有多种信息,如题目,作者,修改时间,内容等。不同类型的信息用不同的Field来表示,在本例子 中,一共有两类信息进行了索引,一个是文件路径,一个是文件内容。其中FileReader的SRC_FILE就表示要索引的源文件。 ◦ IndexWriter调用函数addDocument将索引写到索引文件夹中。 当然上面的api已经过时,lucene3.0采用的api如下: final File docDir = new File(args[0]); IndexWriter writer = new IndexWriter(FSDirectory.open(INDEX_DIR), new StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED); indexDocs(writer, docDir); writer.optimize(); writer.close(); 搜索过程如下: ◦ IndexReader将磁盘上的索引信息读入到内存,INDEX_DIR就是索引文件存放的位置。 ◦ 创建IndexSearcher准备进行搜索。 ◦ 创建Analyer用来对查询语句进行词法分析和语言处理。 ◦ 创建QueryParser用来对查询语句进行语法分析。 ◦ QueryParser调用parser进行语法分析,形成查询语法树,放到Query中。 ◦ IndexSearcher调用search对查询语法树Query进行搜索,得到结果TopScoreDocCollector。 以上便是Lucene API函数的简单调用。 然而当进入Lucene的源代码后,发现Lucene有很多包,关系错综复杂。 然而通过下图,我们不难发现,Lucene的各源码模块,都是对普通索引和搜索过程的一种实现。 此图是上一节介绍的全文检索的流程对应的Lucene实现的包结构。(参照http://www.lucene.com.cn/about.htm中文章《开放源代码的全文检索引擎Lucene》) • Lucene的analysis模块主要负责词法分析及语言处理而形成Term。 • Lucene的index模块主要负责索引的创建,里面有IndexWriter。 • Lucene的store模块主要负责索引的读写。 • Lucene的QueryParser主要负责语法分析。 • Lucene的search模块主要负责对索引的搜索。 • Lucene的similarity模块主要负责对相关性打分的实现。
欢迎访问我的个人网站:http://wuyudong.com/ HBase 进行数据建模的方式和你熟悉的关系型数据库有些不同。关系型数据库围绕表、列和数据类型——数据的形态使用严格的规则。遵守这些严格规则的数据称为结构化 数据。HBase 设计上没有严格形态的数据。数据记录可能包含不一致的列、不确定大小等。这种数据称为半结构化数据(semistructured data)。 在逻辑模型里针对结构化或半结构化数据的导向影响了数据系统物理模型的设计。关系型数据库假定表中的记录都是结构化的和高度有规律的。因此,在物理 实现时,利用这一点相应优化硬盘上的存放格式和内存里的结构。同样,HBase 也会利用所存储数据是半结构化的特点。随着系统发展,物理模型上的不同也会影响逻辑模型。因为这种双向紧密的联系,优化数据系统必须深入理解逻辑模型和物 理模型。 除了面向半结构化数据的特点外,HBase 还有另外一个重要考虑因素——可扩展性。在半结构化逻辑模型里数据构成是松耦合的,这一点有利于物理分散存放。HBase 的物理模型设计上适合于物理分散存放,这一点也影响了逻辑模型。此外,这种物理模型设计迫使HBase 放弃了一些关系型数据库具有的特性。特别是,HBase 不能实施关系约束(constraint)并且不支持多行事务(multirow transaction)。这种关系影响了下面几个主题。 逻辑模型:有序映射的映射集合 HBase 中使用的逻辑数据模型有许多有效的描述。图2-6 把这个模型解释为键值数据库。我们考虑的一种描述是有序映射的映射(sorted map of maps)。你大概熟悉编程语言里的映射集合或者字典结构。可以把HBase 看做这种结构的无限的、实体化的、嵌套的版本。 我们先来思考映射的映射这个概念。HBase 使用坐标系统来识别单元里的数据:[行键,列族,列限定符,时间版本]。例如,从users 表里取出Mark 的记录 理解映射的映射的概念时,把这些坐标从里往外看。你可以想象,开始以时间版本为键、数据为值建立单元映射,往上一层以列限定符为键、单元映射为值建 立列族映射,最后以行键为键列族映射为值建立表映射。这个庞然大物用Java 描述是这样的:Map<RowKey, Map<ColumnFamily, Map<ColumnQualifier, Map<Version,Data>>>>。不算漂亮,但是简单易懂。 注意我们说映射的映射是有序的。上述例子只显示了一条记录,即使如此也可以看到顺序。注意password 单元有两个时间版本。最新时间版本排在稍晚时间版本之前。HBase 按照时间戳降序排列各时间版本,所以最新数据总是在最前面。这种物理设计明显导致可以快速访问最新时间版本。其他的映射键按照升序排列。现在的例子看不到 这一点,让我们插入几行记录看看是什么样子: wu@ubuntu:~/opt/twitbase$ java -cp target/twitbase-1.0.0.jar HBaseIA.TwitBase.UsersTool add HMS_Surprise "Patrick O'Brian" aubrey@sea.com abc123 Successfully added user <User: HMS_Surprise, Patrick O'Brian, aubrey@sea.com, 0> wu@ubuntu:~/opt/twitbase$ java -cp target/twitbase-1.0.0.jar HBaseIA.TwitBase.UsersTool add GrandpaD "Fyodor Dostoyevsky" fyodor@brothers.net abc123 Successfully added user <User: GrandpaD, Fyodor Dostoyevsky, fyodor@brothers.net, 0> wu@ubuntu:~/opt/twitbase$ java -cp target/twitbase-1.0.0.jar HBaseIA.TwitBase.UsersTool add SirDoyle "Sir Arthur Conan Doyle" art@TheQueensMen.co.uk abc123 Successfully added user <User: SirDoyle, Sir Arthur Conan Doyle, art@TheQueensMen.co.uk, 0> 现在再次列出Users 表的内容,可以看到: wu@ubuntu:~/opt/twitbase$ java -cp target/twitbase-1.0.0.jar HBaseIA.TwitBase.UsersTool list 16/03/18 01:31:51 INFO TwitBase.UsersTool: Found 4 users.<User: GrandpaD, Fyodor Dostoyevsky, fyodor@brothers.net, 0><User: HMS_Surprise, Patrick O'Brian, aubrey@sea.com, 0><User: SirDoyle, Sir Arthur Conan Doyle, art@TheQueensMen.co.uk, 0><User: TheRealMT, Mark Twain, samul@clemens.org, 0> 实践中,设计HBase 表模式时这种排序设计是一个关键考虑因素。这是另外一个物理数据模型影响逻辑模型的地方。掌握这些细节可以帮助你在设计模式时利用这个特性。 物理模型:面向列族 就像关系型数据库一样,HBase 中的表由行和列组成。HBase 中列按照列族分组。这种分组表现在映射的映射逻辑模型中是其中一个层次。列族也表现在物理模型中。每个列族在硬盘上有自己的HFile 集合。这种物理上的隔离允许在列族底层HFile 层面上分别进行管理。进一步考虑到合并,每个列族的HFile 都是独立管理的。 HBase 的记录按照键值对存储在HFile 里。HFile 自身是二进制文件,不是直接可读的。存储在硬盘上HFile 里的Mark 用户数据如图2-8 所示。注意,在HFile 里Mark 这一行使用了多条记录。每个列限定符和时间版本有自己的记录。另外,文件里没有空记录(null)。如果没有数据,HBase 不会存储任何东西。因此列族的存储是面向列的,就像其他列式数据库一样。一行中一个列族的数据不一定存放在同一个HFile 里。Mark 的info数据可能分散在多个HFile 里。唯一的要求是,一行中列族的数据需要物理存放在一起。 "TheRealMT" , "info" , "email" , 1329088321289 , "samuel@clemens.org" "TheRealMT" , "info" , "name" 1329088321289 , "Mark Twain" "TheRealMT" , "info", "password" , 1329088818321 , "abc123", "TheRealMT" , "info" , "password", 1329088321289 , "Langhorne" 如果users 表有了另一个列族,并且Mark 在那些列里有数据。Mark 的行也会在那些HFile 里有数据。每个列族使用自己的HFile 意味着,当执行读操作时HBase 不需要读出一行中所有的数据,只需要读取用到列族的数据。面向列意味着当检索指定单元时,HBase 不需要读占位符(placeholder)记录。这两个物理细节有利于稀疏数据集合的高效存储和快速读取。 让我们增加另外一个列族到users 表,以存储TwitBase 网站上的活动,这会生成多个HFile。让HBase 管理整行的一整套工具如图所示。HBase 称这种机制为region,我们在后面会讨论。 在图中可以看到,访问不同列族的数据涉及完全不同的MemStore 和HFile。列族activity 数据的增长并不影响列族info 的性能。
HBase 写路径工作机制 在HBase 中无论是增加新行还是修改已有的行,其内部流程都是相同的。HBase 接到命令后存下变化信息,或者写入失败抛出异常。默认情况下,执行写入时会写到两个地方:预写式日志(write-ahead log,也称HLog)和MemStore。HBase 的默认方式是把写入动作记录在这两个地方,以保证数据持久化。只有当这两个地方的变化信息都写入并确认后,才认为写动作完成。 MemStore 是内存里的写入缓冲区,HBase 中数据在永久写入硬盘之前在这里累积。当MemStore 填满后,其中的数据会刷写到硬盘,生成一个HFile。HFile 是HBase 使用的底层存储格式。HFile 对应于列族,一个列族可以有多个HFile,但一个HFile 不能存储多个列族的数据。在集群的每个节点上,每个列族有一个MemStore。 大型分布式系统中硬件故障很常见,HBase 也不例外。设想一下,如果MemStore还没有刷写,服务器就崩溃了,内存中没有写入硬盘的数据就会丢失。HBase 的应对办法是在写动作完成之前先写入WAL。HBase 集群中每台服务器维护一个WAL 来记录发生的变化。WAL 是底层文件系统上的一个文件。直到WAL 新记录成功写入后,写动作才被认为成功完成。这可以保证HBase 和支撑它的文件系统满足持久性。大多数情况下,HBase 使用Hadoop 分布式文件系统(HDFS)来作为底层文件系统。 如果HBase 服务器宕机,没有从MemStore 里刷写到HFile 的数据将可以通过回放WAL 来恢复。你不需要手工执行。Hbase 的内部机制中有恢复流程部分来处理。每台HBase 服务器有一个WAL,这台服务器上的所有表(和它们的列族)共享这个WAL。 写操作会写入WAL和内存写缓冲区MemStore,客户端在写的过程中不与底层的HFile直接交互 当MemStore写满时,会刷写到硬盘,生成一个新的HFile 你可能想到,写入时跳过WAL 应该会提升写性能。但我们不建议禁用WAL,除非你愿意在出问题时丢失数据。如果你想测试一下,如下代码可以禁用WAL: Put p = new Put(); p.setWriteToWAL(false); 注意:不写入WAL 会在RegionServer 故障时增加丢失数据的风险。关闭WAL,出现故障时HBase 可能无法恢复数据,没有刷写到硬盘的所有写入数据都会丢失。 HBase 读路径工作机制 如果你想快速访问数据,通用的原则是数据保持有序并尽可能保存在内存里。HBase实现了这两个目标,大多情况下读操作可以做到毫秒级。HBase 读动作必须重新衔接持久化到硬盘上的HFile 和内存中MemStore 里的数据。HBase 在读操作上使用了LRU(最近最少使用算法)缓存技术。这种缓存也叫做BlockCache,和MemStore 在一个JVM 堆里。BlockCache 设计用来保存从HFile 里读入内存的频繁访问的数据,避免硬盘读。每个列族都有自己的BlockCache。 掌握BlockCache 是优化HBase 性能的一个重要部分。BlockCache 中的Block 是HBase从硬盘完成一次读取的数据单位。HFile 物理存放形式是一个Block 的序列外加这些Block的索引。这意味着,从HBase 里读取一个Block 需要先在索引上查找一次该Block 然后从硬盘读出。Block 是建立索引的最小数据单位,也是从硬盘读取的最小数据单位。Block大小按照列族设定,默认值是64 KB。根据使用场景你可能会调大或者调小该值。如果主要用于随机查询,你可能需要细粒度的Block 索引,小一点儿的Block 更好一些。Block变小会导致索引变大,进而消耗更多内存。如果你经常执行顺序扫描,一次读取多个Block,大一点儿的Block 更好一些。Block 变大意味着索引项变少,索引变小,因此节省内存。 从HBase 中读出一行,首先会检查MemStore 等待修改的队列,然后检查BlockCache看包含该行的Block 是否最近被访问过,最后访问硬盘上的对应HFile。HBase 内部做了很多事情,这里只是简单概括。读路径如图所示 注意,HFile 存放某个时刻MemStore 刷写时的快照。一个完整行的数据可能存放在多个HFile 里。为了读出完整行,HBase 可能需要读取包含该行信息的所有HFile。
系统采用ubuntu-14.04,64bit 1、安装git sudo apt-get install git 出现下面错误: Err http://us.archive.ubuntu.com/ubuntu/ trusty-updates/main git-man all 1:1.9.1-1ubuntu0.1 404 Not Found [IP: 91.189.91.15 80] Err http://security.ubuntu.com/ubuntu/ trusty-security/main git-man all 1:1.9.1-1ubuntu0.1 404 Not Found [IP: 91.189.91.14 80] Err http://security.ubuntu.com/ubuntu/ trusty-security/main git i386 1:1.9.1-1ubuntu0.1 404 Not Found [IP: 91.189.91.14 80] E: Failed to fetch http://security.ubuntu.com/ubuntu/pool/main/g/git/git-man_1.9.1-1ubuntu0.1_all.deb 404 Not Found [IP: 91.189.91.14 80] E: Failed to fetch http://security.ubuntu.com/ubuntu/pool/main/g/git/git_1.9.1-1ubuntu0.1_i386.deb 404 Not Found [IP: 91.189.91.14 80] E: Unable to fetch some archives, maybe run apt-get update or try with --fix-missing? 参考: Installing Latest version of git in ubuntu(http://stackoverflow.com/questions/19109542/installing-latest- version-of-git-in-ubuntu)中给出的PPA源。 sudo add-apt-repository ppa:git-core/ppasudo apt-get updatesudo apt-get install git wu@ubuntu:~/opt/tmp$ git --version git version 2.7.3 2、安装maven wu@ubuntu:~/opt$ tar -xzvf apache-maven-3.0.4-bin.tar.gz 设置环境变量 wu@ubuntu:~/opt$ sudo gedit /etc/profile 在文件末尾追加: # set maven environment export M2_HOME=/home/wu/opt/apache-maven-3.0.4 export PATH=M2HOME/bin: PATH 是环境变量生效: wu@ubuntu:~/opt$ source /etc/profile 验证证maven是否安装成功: wu@ubuntu:~/opt$ mvn --versionApache Maven 3.0.4 (r1232337; 2012-01-17 00:44:56-0800)Maven home: /home/wu/opt/apache-maven-3.0.4Java version: 1.6.0_24, vendor: Sun Microsystems Inc.Java home: /home/wu/jdk1.6.0_24/jreDefault locale: en_US, platform encoding: UTF-8OS name: "linux", version: "3.13.0-32-generic", arch: "i386", family: "unix" 3、按照下面顺序开始编译项目: wu@ubuntu:/tmp$ git clone git://github.com/larsgeorge/hbase-book.git Cloning into 'hbase-book'... remote: Counting objects: 3148, done. remote: Total 3148 (delta 0), reused 0 (delta 0), pack-reused 3148 Receiving objects: 100% (3148/3148), 1.60 MiB | 66.00 KiB/s, done. Resolving deltas: 100% (1412/1412), done. Checking connectivity... done. wu@ubuntu:/tmpcdhbase−book/wu@ubuntu:/tmp/hbase−book mvn package -DskipTests=true [INFO] Scanning for projects...[INFO] ------------------------------------------------------------------------[INFO] Reactor Build Order:[INFO] [INFO] HBase Book[INFO] HBase Book Common Code[INFO] HBase Book Chapter 3[INFO] HBase Book Chapter 4[INFO] HBase Book Chapter 5[INFO] HBase Book Chapter 6[INFO] HBase Book Chapter 7[INFO] HBase Book Chapter 8[INFO] HBase Book Chapter 9[INFO] HBase Book Chapter 11[INFO] HBase URL Shortener[INFO] [INFO] ------------------------------------------------------------------------[INFO] Building HBase Book 2.0[INFO] ------------------------------------------------------------------------…… [INFO] Reactor Summary:[INFO] [INFO] HBase Book ........................................ SUCCESS [0.002s][INFO] HBase Book Common Code ............................ SUCCESS [4.846s][INFO] HBase Book Chapter 3 .............................. SUCCESS [1.592s][INFO] HBase Book Chapter 4 .............................. SUCCESS [2.331s][INFO] HBase Book Chapter 5 .............................. SUCCESS [1.119s][INFO] HBase Book Chapter 6 .............................. SUCCESS [8.721s][INFO] HBase Book Chapter 7 .............................. SUCCESS [1.620s][INFO] HBase Book Chapter 8 .............................. SUCCESS [1.172s][INFO] HBase Book Chapter 9 .............................. SUCCESS [0.528s][INFO] HBase Book Chapter 11 ............................. SUCCESS [0.575s][INFO] HBase URL Shortener ............................... SUCCESS [19.475s][INFO] ------------------------------------------------------------------------[INFO] BUILD SUCCESS[INFO] ------------------------------------------------------------------------[INFO] Total time: 42.526s[INFO] Finished at: Wed Mar 16 20:15:39 PDT 2016[INFO] Final Memory: 37M/168M[INFO] ------------------------------------------------------------------------ 如果你在虚拟机中运行,中途出错,可能是jdk的版本问题,可以将jdk1.6换为jdk1.7 或者是网络问题,多运行几次就OK wu@ubuntu:~/opt/tmp/hbase-book$ ls -l ch04/target/total 188drwxrwxr-x 5 wu wu 4096 Mar 16 20:15 classesdrwxrwxr-x 3 wu wu 4096 Mar 16 20:15 generated-sources-rw-rw-r-- 1 wu wu 168340 Mar 16 20:15 hbase-book-ch04-2.0.jardrwxrwxr-x 2 wu wu 4096 Mar 16 20:15 maven-archiverdrwxrwxr-x 3 wu wu 4096 Mar 16 20:15 maven-statusdrwxrwxr-x 2 wu wu 4096 Mar 16 20:15 surefire
一个成熟的大型网站(如淘宝、京东等)的系统架构并不是开始设计就具备完整的高性能、高可用、安全等特性,它总是随着用户量的增加,业务功能的扩展 逐渐演变完善的,在这个过程中,开发模式、技术架构、设计思想也发生了很大的变化,就连技术人员也从几个人发展到一个部门甚至一条产品线。 (欢迎大家访问我的个人网站:工学1号馆) 所以成熟的系统架构是随业务扩展而完善出来的,并不是一蹴而就;不同业务特征的系统,会有各自的侧重点,例如淘宝,要解决海量的商品信息的搜索、下 单、支付,例如腾讯,要解决数亿的用户实时消息传输,百度它要处理海量的搜索请求,他们都有各自的业务特性,系统架构也有所不同。尽管如此我们也可以从这 些不同的网站背景下,找出其中共用的技术,这些技术和手段可以广泛运行在大型网站系统的架构中,下面就通过介绍大型网站系统的演化过程,来认识这些技术和 手段。 1、初始阶段的网站架构 应用程序、数据库、文件等都在一台服务器上,典型的LAMP结构 2、应用服务和数据服务分离 随着业务的扩展,一台服务器已经不能满足性能需求,故将应用程序、数据库、文件各自部署在独立的服务器上,这 3 台服务器对硬件资源的要求各不相同:应用服务器需要处理大量的业务逻辑,因此需要更快更强大的CPU;数据库服务器需要快速磁盘检索和数据缓存,因此需要 更快的磁盘和更大的内存;文件服务器需要存储大量用户上传的文件,因此需要更大的硬盘。 应用和数据分离后,不同特性的服务器承担不同的服务角色,网站的并发处理能力和数据存储空间得到了很大改善,支持网站业务进一步发展。但是随着用户 逐渐增多,网站又一次面临挑战:数据库压力太大导致访问延迟,进而影响整个网站的性能,用户体验受到影响。这时需要对网站架构进一步优化。 3、使用缓存改善网站性能 网站访问的特点和现实世界的财富分配一样遵循二八定律:80% 的业务访问集中在20% 的数据上。既然大部分业务访问集中在一小部分数据上,那么如果把这一小部分数据缓存在内存中,就可以减少数据库的访问压力,提高整个网站的数据访问速度, 改善数据库的写入性能了。 网站使用的缓存可以分为两种:缓存在应用服务器上的本地缓存和缓存在专门的分布式缓存服务器上的远程缓存。 本地缓存的访问速度更快一些,但是受应用服务器内存限制,其缓存数据量有限,而且会出现和应用程序争用内存的情况。远程分布式缓存可以使用集群的方式,部署大内存的服务器作为专门的缓存服务器,可以在理论上做到不受内存容量限制的缓存服务。 使用缓存后,数据访问压力得到有效缓解,但是单一应用服务器能够处理的请求连接有限,在网站访问高峰期,应用服务器成为整个网站的瓶颈。 4、使用应用服务器集群改善网站的并发处理能力 使用集群是网站解决高并发、海量数据问题的常用手段。当一台服务器的处理能力、存储空间不足时,不要企图去更换更强大的服务器,对大型网站而言,不 管多么强大的服务器,都满足不了网站持续增长的业务需求。这种情况下,更恰当的做法是增加一台服务器分担原有服务器的访问及存储压力。 对网站架构而言,只要能通过增加一台服务器的方式改善负载压力,就可以以同样的方式持续增加服务器不断改善系统性能,从而实现系统的可伸缩性。应用服务器 实现集群是网站可伸缩架构设计中较为简单成熟的一种。 通过负载均衡调度服务器,可以将来自用户浏览器的访问请求分发到应用服务器集群中的任何一台服务器上,如果有更多用户,就在集群中加入更多的应用服务器,使应用服务器的压力不再成为整个网站的瓶颈。 5、数据库读写分离 网站在使用缓存后,使对大部分数据读操作访问都可以不通过数据库就能完成,但是仍有一部分读操作(缓存访问不命中、缓存过期)和全部的写操作都需要 访问数据库,在网站的用户达到一定规模后,数据库因为负载压力过高而成为网站的瓶颈。 目前大部分的主流数据库都提供主从热备功能,通过配置两台数据库主从关系,可以将一台数据库服务器的数据更新同步到另一台服务器上。网站利用数据库的这一 功能,实现数据库读写分离,从而改善数据库负载压力。 应用服务器在写数据的时候,访问主数据库,主数据库通过主从复制机制将数据更新同步到从数据库,这样当应用服务器读数据的时候,就可以通过从数据库 获得数据。为了便于应用程序访问读写分离后的数据库,通常在应用服务器端使用专门的数据访问模块,使数据库读写分离对应用透明。 6、使用反向代理和 CDN 加速网站响应 随着网站业务不断发展,用户规模越来越大,由于中国复杂的网络环境,不同地区的用户访问网站时,速度差别也极大。有研究表明,网站访问延迟和用户流 失率正相关,网站访问越慢,用户越容易失去耐心而离开。为了提供更好的用户体验,留住用户,网站需要加速网站访问速度。主要手段有使用 CDN 和方向代理。 CDN 和反向代理的基本原理都是缓存。CDN 部署在网络提供商的机房,使用户在请求网站服务时,可以从距离自己最近的网络提供商机房获取数据。反向代理则部署在网站的中心机房,当用户请求到达中心机 房后,首先访问的服务器是反向代理服务器,如果反向代理服务器中缓存着用户请求的资源,就将其直接返回给用户 使用 CDN 和反向代理的目的都是尽早返回数据给用户,一方面加快用户访问速度,另一方面也减轻后端服务器的负载压力。 7、使用分布式文件系统和分布式数据库系统 任何强大的单一服务器都满足不了大型网站持续增长的业务需求。数据库经过读写分离后,从一台服务器拆分成两台服务器,但是随着网站业务的发展依然不能满足需求,这时需要使用分布式数据库。文件系统也一样,需要使用分布式文件系统。 分布式数据库是网站数据库拆分的最后手段,只有在单表数据规模非常庞大的时候才使用。不到不得已时,网站更常用的数据库拆分手段是业务分库,将不同业务的数据部署在不同的物理服务器上。 8、使用 NoSQL 和搜索引擎 随着网站业务越来越复杂,对数据存储和检索的需求也越来越复杂,网站需要采用一些非关系数据库技术如 NoSQL 和非数据库查询技术如搜索引擎。 NoSQL 和搜索引擎都是源自互联网的技术手段,对可伸缩的分布式特性具有更好的支持。应用服务器则通过一个统一数据访问模块访问各种数据,减轻应用程序管理诸多数据源的麻烦。 9、业务拆分 大型网站为了应对日益复杂的业务场景,通过使用分而治之的手段将整个网站业务分成不同的产品线。如大型购物交易网站都会将首页、商铺、订单、买家、卖家等拆分成不同的产品线,分归不同的业务团队负责。 具体到技术上,也会根据产品线划分,将一个网站拆分成许多不同的应用,每个应用独立部署。应用之间可以通过一个超链接建立关系(在首页上的导航链接 每个都指向不同的应用地址),也可以通过消息队列进行数据分发,当然最多的还是通过访问同一个数据存储系统来构成一个关联的完整系统。 10、分布式服务 随着业务拆分越来越小,存储系统越来越庞大,应用系统的整体复杂度呈指数级增加,部署维护越来越困难。由于所有应用要和所有数据库系统连接,在数万台服务器规模的网站中,这些连接的数目是服务器规模的平方,导致数据库连接资源不足,拒绝服务。 既然每一个应用系统都需要执行许多相同的业务操作,比如用户管理、商品管理等,那么可以将这些共用的业务提取出来,独立部署。由这些可复用的业务连接数据库,提供共用业务服务,而应用系统只需要管理用户界面,通过分布式服务调用共用业务服务完成具体业务操作。 大型网站的架构演化到这里,基本上大多数的技术问题都得以解决,诸如跨数据中心的实时数据同步和具体网站业务相关的问题也都可以通过组合改进现有技术架构解决。
采用开源的OpenJDK版本,获取其源码的方式有两种: 通Mercurial代码版本管理工具从Repository中直接取得源码,但是速度太慢,需要花费数小时 直接下载官方打包好的源码包(推荐) 下载源码openjdk-7u6-fcs-src-b24-28_aug_2012.zip,解压,下载地址如下: http://www.java.net/download/openjdk/jdk7u6/promoted/b24/openjdk-7u6-fcs-src-b24-28_aug_2012.zip 本次编译采用的是64位的OS,编译也是64位的JDK,内存至少需要512MB 1.基本流程 (阅读README和README-builds.html) README-builds.html中包含有详细的安装信息,最好能完整的阅读一下。 2.安装基础软件包 我的centos6.5安装在vmware10上,安装时使用最小化(Minimal)安装 [root@localhost ~]# cat /etc/redhat-release CentOS release 6.5 (Final) [root@localhost ~]# uname -m x86_64 [root@localhost ~]# uname -r 2.6.32-431.el6.x86_64 配置更新源 cd /etc/yum.repos.d/ curl http://mirrors.163.com/.help/CentOS6-Base-163.repo > CentOS6-Base-163.repo 当前wget还不能用 #wget http://mirrors.163.com/.help/CentOS6-Base-163.repo mv CentOS-Base.repo CentOS-Base.repo.bak mv CentOS6-Base-163.repo CentOS-Base.repo yum makecache yum -y groupinstall 'base' yum -y install make 安装jdk必备软件包: yum -y install alsa-lib-devel yum -y install cups-devel yum -y install libXi-devel yum -y install gcc gcc-c++ yum -y install libX* 上传或下载相关文件到/application/tools mkdir -p /application/tools cd /application/tools 上传或下载下列相关文件到/application/tools freetype-2.3.12.tar.gz 下载地址:http://sourceforge.net/projects/freetype/files/freetype2/ openjdk-7u6-fcs-src-b24-28_aug_2012.zip 下载地址前文 apache-ant-1.7.1-bin.zip 下载地址:http://archive.apache.org/dist/ant/binaries/ jdk-6u26-linux-x64.bin 下载地址:http://www.oracle.com/technetwork/java/javase/downloads/java- archive-downloads-javase6-419409.html#jdk-6u26-oth-JPR 编译安装freetype: tar -xzf freetype-2.3.12.tar.gz cd freetype-2.3.12 ./configure make make install 安装JDK: 解压缩jdk-6u26-linux-i586.bin到application/java/目录下 设置环境变量如下: #set java environment JAVA_HOME=/application/java/jdk export JRE_HOME=/application/java/jdk/jre export CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH 安装ant: cd /application/tools/ unzip apache-ant-1.7.1-bin.zip ln -s /application/tools/apache-ant-1.7.1/bin/ant /usr/bin/ant 检查java与ant: [root@localhost ~]# java -version java version "1.6.0_26" Java(TM) SE Runtime Environment (build 1.6.0_26-b03) Java HotSpot(TM) 64-Bit Server VM (build 20.1-b02, mixed mode) [root@localhost ~]# ant Buildfile: build.xml does not exist! Build failed 3.配置变量 nset CLASSPATH unset JAVA_HOME export LANG=C export ALT_BOOTDIR=/application/java/jdk export ANT_HOME=/application/tools/apache-ant-1.8.1/ export ALT_FREETYPE_LIB_PATH=/usr/local/lib export SKIP_DEBUG_BUILD=false export SKIP_FASTDEBUG_BUILD=true export DEBUG_NAME=debug export ALT_FREETYPE_HEADERS_PATH=/usr/local/include/freetype2 4.检查环境是否配置OK与编译jdk源码 [root@localhost tools]# cd openjdk [root@localhost openjdk]# pwd /application/tools/openjdk [root@localhost openjdk]# make sanity …… OpenJDK-specific settings:FREETYPE_HEADERS_PATH = /usr/local/include/freetype2ALT_FREETYPE_HEADERS_PATH = /usr/local/include/freetype2FREETYPE_LIB_PATH = /usr/local/libALT_FREETYPE_LIB_PATH = /usr/local/libPrevious JDK Settings:PREVIOUS_RELEASE_PATH = USING-PREVIOUS_RELEASE_IMAGEALT_PREVIOUS_RELEASE_PATH = PREVIOUS_JDK_VERSION = 1.6.0ALT_PREVIOUS_JDK_VERSION = PREVIOUS_JDK_FILE = ALT_PREVIOUS_JDK_FILE = PREVIOUS_JRE_FILE = ALT_PREVIOUS_JRE_FILE = PREVIOUS_RELEASE_IMAGE = /application/java/jdkALT_PREVIOUS_RELEASE_IMAGE = Sanity check passed. 执行下列命令: [root@localhost openjdk]# make all Build times ----------Target debug_buildStart 2015-05-05 09:34:37End 2015-05-05 10:24:3300:05:23 corba00:10:49 hotspot00:00:55 jaxp00:01:07 jaxws00:30:05 jdk00:01:36 langtools00:49:56 TOTAL-------------------------make[1]: Leaving directory `/application/tools/openjdk' 查看成果: [root@localhost openjdk]# ./build/linux-amd64/bin/java -version openjdk version "1.7.0-internal-debug"OpenJDK Runtime Environment (build 1.7.0-internal-debug-root_2015_05_05_09_15-b00)OpenJDK 64-Bit Server VM (build 23.2-b09-jvmg, mixed mode)
1、什么是 nutch Nutch 是一个开源的、 Java 实现的搜索引擎。它提供了我们运行自己的搜 索引擎所需的全部工具。2、研究 nutch 的原因 (1) 透明度: nutch 是开放源代码的,因此任何人都可以查看他的排序算法是如何工作的。商业的搜索引擎排序算法都是保密的,我们无法知道为 什么搜索出来的排序结果是如何算出来的。更进一步,一些搜索引擎允 许竞价排名,比如百度,这样的索引结果并不是和站点内容相关的。因 此 nutch 对学术搜索和政府类站点的搜索来说,是个好选择,因为一 个公平的排序结果是非常重要的。 (2) 对搜索引擎的理解:我们并没有 google 的源代码,因此学习搜索引擎 Nutch 是个不错的选择。了解一个大型分布式的搜索引擎如何工作是一 件让人很受益的事情。在写 Nutch 的过程中,从学院派和工业派借鉴了 很多知识:比如, Nutch 的核心部分目前已经被重新用 Map Reduce 实 现了。 Map Reduce 是一个分布式的处理模型,最先是从 Google 实验 室提出来的。并且 Nutch 也吸引了很多研究者,他们非常乐于尝试新 的搜索算法,因为对 Nutch 来说,这是非常容易实现扩展的。 (3) 扩展性:你是不是不喜欢其他的搜索引擎展现结果的方式呢?那就用 Nutch 写你自己的搜索引擎吧。 Nutch 是非常灵活的:他可以被很好 的客户订制并集成到你的应用程序中,使用 Nutch 的插件机制, Nutch 可以作为一个搜索不同信息载体的搜索平台。当然,最简单的就是集成 Nutch 到你的站点,为你的用户提供搜索服务。3、nutch 的目标 nutch 致力于让每个人能很容易, 同时花费很少就可以配置世界一流的 Web 搜索引擎. 为了完成这一宏伟的目标, nutch 必须能够做到: • 每个月取几十亿网页 • 为这些网页维护一个索引 • 对索引文件进行每秒上千次的搜索 • 提供高质量的搜索结果 • 以最小的成本运作这将是一个巨大的挑战。4、nutch VS lucene 简单的说: Lucene 不是完整的应用程序,而是一个用于实现全文检索的软件库。 Nutch 是一个应用程序,可以以 Lucene 为基础实现搜索引擎应用。 Lucene 为 Nutch 提供了文本索引和搜索的 API。 一个常见的问题是;我应该使用 Lucene 还是 Nutch? 最简单的回答是:如果你不需要抓取数据的话,应该使用 Lucene。常见的应用场合是:你有数据源,需要为这些数据提供一个搜索页 面。在这种情况下,最好的方式是直接从数据库中取出数据并用 Lucene API 建立 索引。
搭建Struts2环境时,我们一般需要做以下几个步骤的工作: 1、找到开发Struts2应用需要使用到的jar文件. 2、创建Web工程 3、在web.xml中加入Struts2 MVC框架启动配置 4、编写Struts2的配置文件 开发Struts2应用依赖的jar文件 大家可以到http://struts.apache.org/download.cgi#struts2014下载struts-2.x.x- all.zip。下载完后解压文件,开发struts2应用需要依赖的jar文件在解压目录的lib文件夹下。不同的应用需要的JAR包是不同的。下面给 出了开发Struts 2程序最少需要的JAR。 struts2-core-2.x.x.jar :Struts 2框架的核心类库 xwork-core-2.x.x.jar :XWork类库,Struts 2在其上构建 ognl-2.6.x.jar :对象图导航语言(Object Graph Navigation Language),struts2框架通过其读写对象的属性 freemarker-2.3.x.jar :Struts 2的UI标签的模板使用FreeMarker编写 commons-logging-1.x.x.jar :ASF出品的日志包,Struts 2框架使用这个日志包来支持Log4J和JDK 1.4+的日志记录。 commons-fileupload-1.2.1.jar :文件上传组件,2.1.6版本后必须加入此文件 以上这些Jar文件拷贝 到Web项目的WEB-INF/lib目录中。 Struts2在web.xml中的启动配置 在struts1.x中, struts框架是通过Servlet启动的。在struts2中,struts框架是通过Filter启动的。在web.xml中的配置如下: <filter> <filter-name>struts2</filter-name> <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class> </filter> <filter-mapping> <filter-name>struts2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> 在StrutsPrepareAndExecuteFilter的init()方法中将会读取类路径下默认的配置文件struts.xml完成初始化操作。 注意:struts2读取到struts.xml的内容后,以javabean形式存放在内存中,以后struts2对用户的每次请求处理将使用内存中的数据,而不是每次都读取struts.xml文件 Struts2应用的配置文件 Struts2默认的配置文件为struts.xml ,该文件需要存放在src目录下,该文件的配置模版如下: <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.1//EN" "http://struts.apache.org/dtds/struts-2.1.dtd"> <struts> </struts> 实战--helloworld Struts2的每个请求需要实现以下内容: JSP Action struts.xml中Action配置 1、新建项目12.01 2、在默认的配置文件struts.xml 中加入如下配置: <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN" "http://struts.apache.org/dtds/struts-2.3.dtd"> <struts> <constant name="struts.devMode" value="true" /> <package name="Hello_World_Struts2" extends="struts-default"> <action name="index"> <result>/index.jsp</result> </action> <action name="hello" class="com.wuyudong.helloworld.action.HelloWorldAction" method="execute"> <result name="success">/hello.jsp</result> </action> </package> </struts> 在struts2框架中使用包来管理Action,包的作用和java中的类包是非常类似的,它主要用于管理一组业务功能相关的action。在实际应用中,我们应该把一组业务功能相关的Action放在同一个包下。 配置包时必须指定name属性,该name属性值可以任意取名,但必须唯一,他不对应java的类包,如果其他包要继承该包,必须通过该属性进行引 用。包的namespace属性用于定义该包的命名空间,命名空间作为访问该包下Action的路径的一部分,如访问上面例子的Action,访问路径 为:/test/helloworld.action。 namespace属性可以不配置,对本例而言,如果不指定该属性,默认的命名空间为“”(空字符串)。 通常每个包都应该继承struts-default包, 因为Struts2很多核心的功能都是拦截器来实现。如:从请求中把请求参数封装到action、文件上传和数据验证等等都是通过拦截器实现的。 struts-default定义了这些拦截器和Result类型。可以这么说:当包继承了struts-default才能使用struts2提供的核 心功能。 struts-default包是在struts2-core-2.x.x.jar文件中的struts-default.xml中定义。 struts-default.xml也是Struts2默认配置文件。 Struts2每次都会自动加载 struts-default.xml文件。 例子中使用到的com.wuyudong.helloworld.action.HelloWorldAction类如下: package com.wuyudong.helloworld.action; import com.opensymphony.xwork2.ActionSupport; import com.wuyudong.helloworld.model.MessageStore; public class HelloWorldAction extends ActionSupport { private static final long serialVersionUID = -4958566543551999157L; private MessageStore msgStore; @Override public String execute() throws Exception { msgStore = new MessageStore("HelloWorld!"); return SUCCESS; } public MessageStore getMsgStore() { return msgStore; } public void setMsgStore(MessageStore msgStore) { this.msgStore = msgStore; } } 例子中使用到的hello.jsp如下: <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="s" uri="/struts-tags"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Hello World!</title> </head> <body> <h2> <s:property value="msgStore.message" /> </h2> </body> </html> 在struts2中,访问struts2中action的URL路径由两部份组 成:包的命名空间+action的名称,例如访问本例子HelloWorldAction的URL路径为:/helloworld (注意:完整路径为:http://localhost:端口/内容路径/helloworld)。另外我们也可以加上.action后缀访问此 Action。 Action名称的搜索顺序 1.获得请求路径的URI,例如url是:http://server/struts2/path1/path2/path3/test.action 2.首先寻找namespace为/path1/path2/path3的package,如果不存在这个package则执行步骤3;如果存在这 个package,则在这个package中寻找名字为test的action,当在该package下寻找不到action 时就会直接跑到默认namaspace的package里面去寻找action(默认的命名空间为空字符串“” ) ,如果在默认namaspace的package里面还寻找不到该action,页面提示找不到action 3.寻找namespace为/path1/path2的package,如果不存在这个package,则转至步骤4;如果存在这个 package,则在这个package中寻找名字为test的action,当在该package中寻找不到action 时就会直接跑到默认namaspace的package里面去找名字为test的action ,在默认namaspace的package里面还寻找不到该action,页面提示找不到action 4.寻找namespace为/path1的package,如果不存在这个package则执行步骤5;如果存在这个package,则在这个 package中寻找名字为test的action,当在该package中寻找不到action 时就会直接跑到默认namaspace的package里面去找名字为test的action ,在默认namaspace的package里面还寻找不到该action,页面提示找不到action 5.寻找namespace为/的package,如果存在这个package,则在这个package中寻找名字为test的action, 当在package中寻找不到action或者不存在这个package时,都会去默认namaspace的package里面寻找action,如果还 是找不到,页面提示找不到action。 Action配置中的各项默认值 创建 Model类MessageStore package com.wuyudong.helloworld.model; public class MessageStore { private String message; public MessageStore(String msg){ this.setMessage(msg); } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } } 创建Action类HelloWorldAction,充当Controller package com.wuyudong.helloworld.action; import com.opensymphony.xwork2.ActionSupport; import com.wuyudong.helloworld.model.MessageStore; public class HelloWorldAction extends ActionSupport { private static final long serialVersionUID = -4958566543551999157L; private MessageStore msgStore; @Override public String execute() throws Exception { msgStore = new MessageStore("HelloWorld!"); return SUCCESS; } public MessageStore getMsgStore() { return msgStore; } public void setMsgStore(MessageStore msgStore) { this.msgStore = msgStore; } } 配置web.xml <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0"> <display-name>12.01</display-name> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <filter> <filter-name>struts2</filter-name> <filter-class> org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter </filter-class> </filter> <filter-mapping> <filter-name>struts2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app> 运行后如图所示
Struts 2以WebWork优秀的设计思想为核心,吸收了Struts 1的部分优点,建立了一个兼容WebWork和Struts 1的MVC框架,Struts 2的目标是希望可以让原来使用Struts 1、WebWork的开发人员,都可以平稳过渡到使用Struts 2框架。在2005年12月,WebWork与Struts Ti宣布合并。与此同时,Struts Ti改名为Struts Action Framework 2.0,成为Struts真正的继承者。 (1)支持的表现层技术单一 那时候还没有FreeMarker、Velocity等技术,因而没有考虑与这些FreeMarker、Velocity等视图技术的整合 (2)与Servlet API严重耦合,难于测试 (3)代码严重依赖于Struts 1 API,属于侵入式设计 Struts2优点 Struts2是在WebWork2基础发展而来的。和struts1一样, Struts2也属于MVC框架。不过有一点大家需要注意的是:尽管Struts2和struts1在名字上的差别不是很大,但Struts2和 struts1在代码编写风格上几乎是不一样的。那么既然有了struts1,为何还要推出struts2。主要是因为struts2有以下优点: (1)在软件设计上Struts2没有像struts1那样跟Servlet API和struts API有着紧密的耦合,Struts2的应用可以不依赖于Servlet API和struts API。 Struts2的这种设计属于无侵入式设计,而Struts1却属于侵入式设计。下面为Struts1的Action设计: public class OrderListAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { } } (2)Struts2提供了拦截器,利用拦截器可以进行AOP编程,实现如权限拦截等功能。 (3) Strut2提供了类型转换器,我们可以把特殊的请求参数转换成需要的类型。在Struts1中,如果我们要实现同样的功能,就必须向Struts1的底层实现BeanUtil注册类型转换器才行。 (4)Struts2提供支持多种表现层技术,如:JSP、freeMarker、Velocity等 (5)Struts2的输入校验可以对指定方法进行校验,解决了Struts1长久之痛。 (6)提供了全局范围、包范围和Action范围的国际化资源文件管理实现 工作原理 1.客户端初始化一个指向Servlet容器(例如Tomcat)的请求; 2.这个请求经过一系列的过滤器(Filter)(这些过滤器中有一个叫做ActionContextCleanUp的可选过滤器,这个过滤器对于Struts2和其他框架的集成很有帮助,例如:SiteMesh Plugin) 3.接着FilterDispatcher被调用,FilterDispatcher询问ActionMapper来决定这个请是否需要调用某个Action 4.如果ActionMapper决定需要调用某个Action,FilterDispatcher把请求的处理交给ActionProxy 5.ActionProxy通过Configuration Manager询问框架的配置文件,找到需要调用的Action类 6.ActionProxy创建一个ActionInvocation的实例。 7.ActionInvocation实例使用命名模式来调用,在调用Action的过程前后,涉及到相关拦截器(Intercepter)的调用。 8.一旦Action执行完毕,ActionInvocation负责根据struts.xml中的配置找到对应的返回结果。返回结果通常是 (但不总是,也可 能是另外的一个Action链)一个需要被表示的JSP或者FreeMarker的模版。在表示的过程中可以使用Struts2 框架中继承的标签。在这个过程中需要涉及到ActionMapper Struts2的处理流程 StrutsPrepareAndExecuteFilter是Struts 2框架的核心控制器,它负责拦截由<url-pattern>/*</url-pattern>指定的所有用户请求,当用户请求 到达时,该Filter会过滤用户的请求。默认情况下,如果用户请求的路径不带后缀或者后缀以.action结尾,这时请求将被转入Struts 2框架处理,否则Struts 2框架将略过该请求的处理。当请求转入Struts 2框架处理时会先经过一系列的拦截器,然后再到Action。与Struts1不同,Struts2对用户的每一次请求都会创建一个Action,所以Struts2中的Action是线程安全的。
1、判断用户名是否为空,空则显示提示信息 (1)编写index.jsp页面 <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> <form name="form1" method="post" action="deal.jsp"> 用户名:<input name="user" type="text" id="user"> <br><br> 密&nbsp;&nbsp;码:<input name="pwd" type="password" id="pwd"> <br><br><br> <input type="submit" name="Submit" value="登录"> <input type="reset" name="Submit2" value="重置"> </form> </body> </html> (2)编写deal.jsp页面 <body> ${empty param.user?"请输入用户名":"" } ${empty param.pwd?"请输入密码":"欢迎访问! " } <br> <a href="index.jsp">返回</a> </body> 2、显示客户使用的浏览器 index.jsp页面的代码如下: <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> 客户端使用的浏览器为:<br> ${header["user-agent"]} </body> </html>
(1)编写index.jsp页面,用于收集投票信息 <%@ page language="java" pageEncoding="GBK"%> <% String path = request.getContextPath(); String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/"; %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <base href="<%=basePath%>"> <title>应用EL表达式显示投票结果</title> <link rel="stylesheet" type="text/css" href="CSS/style.css"> </head> <body><form name="form1" method="post" action="PollServlet"> <table width="403" height="230" border="0" align="center" cellpadding="0" cellspacing="1" bgcolor="#666666"> <tr> <td height="30" bgcolor="#EFEFEF">·您最需要哪方面的编程类图书?</td> </tr> <tr> <td bgcolor="#FFFFFF"> <input name="item" type="radio" class="noborder" value="基础教程类" checked> 基础教程类</td> </tr> <tr> <td bgcolor="#FFFFFF"> <input name="item" type="radio" class="noborder" value="实例集锦类"> 实例集锦类 </td> </tr> <tr> <td bgcolor="#FFFFFF"> <input name="item" type="radio" class="noborder" value="经验技巧类"> 经验技巧类</td> </tr> <tr> <td bgcolor="#FFFFFF"> <input name="item" type="radio" class="noborder" value="速查手册类"> 速查手册类</td> </tr> <tr> <td bgcolor="#FFFFFF"> <input name="item" type="radio" class="noborder" value="案例剖析类"> 案例剖析类</td> </tr> <tr> <td align="center" bgcolor="#FFFFFF"> <input name="Submit" type="submit" class="btn_grey" value="投票"> &nbsp; <input name="Submit2" type="button" class="btn_grey" value="查看投票结果" onClick="window.location.href='showResult.jsp'"></td> </tr> </table> </form> </body> </html> 界面如下: (2)编写投票功能的Servlet package com.wuyudong.servlet; import java.io.IOException; import java.io.PrintWriter; import java.util.*; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class PollServlet extends HttpServlet { private static final long serialVersionUID = -7264414153802032772L; /** * The doPost method of the servlet. <br> * * This method is called when a form has its tag value method equals to * post. * * @param request * the request send by the client to the server * @param response * the response send by the server to the client * @throws ServletException * if an error occurred * @throws IOException * if an error occurred */ public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("GBK"); // 设置请求的编码方式 String item = request.getParameter("item"); // 获取投票项 ServletContext servletContext = request.getSession() .getServletContext(); // 获取ServletContext对象该对象在application范围内有效 Map map = null; if (servletContext.getAttribute("pollResult") != null) { map = (Map) servletContext.getAttribute("pollResult"); // 获取投票结果 map.put(item, Integer.parseInt(map.get(item).toString()) + 1); // 将当前的投票项加1 } else { // 初始化一个保存投票信息的Map集合,并将选定投票项的投票数设置为1,其他为0 String[] arr = { "基础教程类", "实例集锦类", "经验技巧类", "速查手册类", "案例剖析类" }; map = new HashMap(); for (int i = 0; i < arr.length; i++) { if (item.equals(arr[i])) { // 判断是否为选定的投票项 map.put(arr[i], 1); } else { map.put(arr[i], 0); } } } servletContext.setAttribute("pollResult", map); // 保存投票结果到ServletContext对象中 response.setCharacterEncoding("GBK"); // 设置响应的编码方式,如果不设置弹出的对话框中的文字将乱码 PrintWriter out = response.getWriter(); out.println("<script>alert('投票成功!');window.location.href='showResult.jsp';</script>"); } } (3)编写showResult.jsp页面 <%@ page language="java" pageEncoding="GBK"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>显示示投票结果页面</title> <link rel="stylesheet" type="text/css" href="CSS/style.css"> </head> <body> <table width="403" height="230" border="0" align="center" cellpadding="0" cellspacing="1" bgcolor="#666666"> <tr> <td height="30" colspan="2" bgcolor="#EFEFEF">·您最需要哪方面的编程类图书?</td> </tr> <tr> <td width="79" align="center" bgcolor="#FFFFFF"> 基础教程类</td> <td width="321" bgcolor="#FFFFFF">&nbsp;<img src="bar.gif" width='${220*(applicationScope.pollResult["基础教程类"]/(applicationScope.pollResult["基础教程类"]+applicationScope.pollResult["实例集锦类"]+applicationScope.pollResult["经验技巧类"]+applicationScope.pollResult["速查手册类"]+applicationScope.pollResult["案例剖析类"]))}' height="13"> (${empty applicationScope.pollResult["基础教程类"]? 0 :applicationScope.pollResult["基础教程类"]})</td> </tr> <tr> <td align="center" bgcolor="#FFFFFF"> 实例集锦类 </td> <td bgcolor="#FFFFFF">&nbsp;<img src="bar.gif" width='${220*(applicationScope.pollResult["实例集锦类"]/(applicationScope.pollResult["基础教程类"]+applicationScope.pollResult["实例集锦类"]+applicationScope.pollResult["经验技巧类"]+applicationScope.pollResult["速查手册类"]+applicationScope.pollResult["案例剖析类"]))}' height="13"> (${empty applicationScope.pollResult["实例集锦类"] ? 0 :applicationScope.pollResult["实例集锦类"]})</td> </tr> <tr> <td align="center" bgcolor="#FFFFFF"> 经验技巧类</td> <td bgcolor="#FFFFFF">&nbsp;<img src="bar.gif" width='${220*(applicationScope.pollResult["经验技巧类"]/(applicationScope.pollResult["基础教程类"]+applicationScope.pollResult["实例集锦类"]+applicationScope.pollResult["经验技巧类"]+applicationScope.pollResult["速查手册类"]+applicationScope.pollResult["案例剖析类"]))}' height="13"> (${empty applicationScope.pollResult["经验技巧类"] ? 0 :applicationScope.pollResult["经验技巧类"]})</td> </tr> <tr> <td align="center" bgcolor="#FFFFFF"> 速查手册类</td> <td bgcolor="#FFFFFF">&nbsp;<img src="bar.gif" width='${220*(applicationScope.pollResult["速查手册类"]/(applicationScope.pollResult["基础教程类"]+applicationScope.pollResult["实例集锦类"]+applicationScope.pollResult["经验技巧类"]+applicationScope.pollResult["速查手册类"]+applicationScope.pollResult["案例剖析类"]))}' height="13"> (${empty applicationScope.pollResult["速查手册类"] ? 0 : applicationScope.pollResult["速查手册类"]})</td> </tr> <tr> <td align="center" bgcolor="#FFFFFF"> 案例剖析类</td> <td bgcolor="#FFFFFF">&nbsp;<img src="bar.gif" width='${220*(applicationScope.pollResult["案例剖析类"]/(applicationScope.pollResult["基础教程类"]+applicationScope.pollResult["实例集锦类"]+applicationScope.pollResult["经验技巧类"]+applicationScope.pollResult["速查手册类"]+applicationScope.pollResult["案例剖析类"]))}' height="13"> (${empty applicationScope.pollResult["案例剖析类"] ? 0 :applicationScope.pollResult["案例剖析类"]})</td> </tr> <tr> <td colspan="2" align="center" bgcolor="#FFFFFF"> 合计:${applicationScope.pollResult["基础教程类"]+applicationScope.pollResult["实例集锦类"]+applicationScope.pollResult["经验技巧类"]+applicationScope.pollResult["速查手册类"]+applicationScope.pollResult["案例剖析类"]}人投票! <input name="Button" type="button" class="btn_grey" value="返回" onClick="window.location.href='index.jsp'"></td> </tr> </table> </body> </html> l> 最后运行界面如下:
(1)编写index.jsp页面,用来收集用户的注册信息 <%@ page language="java" pageEncoding="GBK"%> <% String path = request.getContextPath(); String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/"; %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <base href="<%=basePath%>"> <title>应用EL表达式访问JavaBean的属性</title> <link rel="stylesheet" type="text/css" href="CSS/style.css"> </head> <body><form name="form1" method="post" action="deal.jsp"> <table width="403" height="230" border="0" align="center" cellpadding="0" cellspacing="1" bgcolor="#666666"> <tr> <td height="30" colspan="2" bgcolor="#EFEFEF">·用户注册</td> </tr> <tr> <td width="88" align="center" bgcolor="#FFFFFF">用 户 名:</td> <td width="359" bgcolor="#FFFFFF"><input name="username" type="text" id="username"></td> </tr> <tr> <td align="center" bgcolor="#FFFFFF">密&nbsp;&nbsp;&nbsp;&nbsp;码:</td> <td bgcolor="#FFFFFF"><input name="pwd" type="password" id="pwd"></td> </tr> <tr> <td align="center" bgcolor="#FFFFFF">确认密码:</td> <td bgcolor="#FFFFFF"><input name="repwd" type="password" id="repwd"></td> </tr> <tr> <td align="center" bgcolor="#FFFFFF">性&nbsp;&nbsp;&nbsp;&nbsp;别:</td> <td bgcolor="#FFFFFF"><input name="sex" type="radio" class="noborder" value="男"> 男 <input name="sex" type="radio" class="noborder" value="女"> 女</td> </tr> <tr> <td align="center" bgcolor="#FFFFFF">爱&nbsp;&nbsp;&nbsp;&nbsp;好:</td> <td bgcolor="#FFFFFF"><input name="affect" type="checkbox" class="noborder" id="affect" value="体育"> 体育 <input name="affect" type="checkbox" class="noborder" id="affect" value="美术"> 美术 <input name="affect" type="checkbox" class="noborder" id="affect" value="音乐"> 音乐 <input name="affect" type="checkbox" class="noborder" id="affect" value="旅游"> 旅游 </td> </tr> <tr> <td colspan="2" align="center" bgcolor="#FFFFFF"> <input name="Submit" type="submit" class="btn_grey" value="提交"> &nbsp; <input name="Submit2" type="reset" class="btn_grey" value="重置"></td> </tr> </table> </form> </body> </html> (2)编写JavaBean package com.wuyudong; public class UserForm { private String username = ""; private String pwd = ""; private String sex = ""; private String[] affect = null; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPwd() { return pwd; } public void setPwd(String pwd) { this.pwd = pwd; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public String[] getAffect() { return affect; } public void setAffect(String[] affect) { this.affect = affect; } } (3)编写deal.jsp页面 <%@ page language="java" pageEncoding="GBK"%> <%request.setCharacterEncoding("GBK");%> <jsp:useBean id="userForm" class="com.wuyudong.UserForm" scope="page"/> <jsp:setProperty name="userForm" property="*"/> <!-- jsp:setProperty name="userForm" property="affect" value='<%=request.getParameterValues("affect")%>'/> --> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>应用EL表达式访问JavaBean的属性</title> <link rel="stylesheet" type="text/css" href="CSS/style.css"> </head> <body> <table width="403" height="218" border="0" align="center" cellpadding="0" cellspacing="1" bgcolor="#666666"> <tr> <td height="30" colspan="2" bgcolor="#EFEFEF">·显示用户填写的注册信息</td> </tr> <tr> <td width="88" align="center" bgcolor="#FFFFFF">用 户 名:</td> <td width="359" bgcolor="#FFFFFF">&nbsp;${userForm.username}</td> </tr> <tr> <td align="center" bgcolor="#FFFFFF">密&nbsp;&nbsp;&nbsp;&nbsp;码:</td> <td bgcolor="#FFFFFF">&nbsp;${userForm.pwd}</td> </tr> <tr> <td align="center" bgcolor="#FFFFFF">性&nbsp;&nbsp;&nbsp;&nbsp;别:</td> <td bgcolor="#FFFFFF">&nbsp;${userForm.sex}</td> </tr> <tr> <td align="center" bgcolor="#FFFFFF">爱&nbsp;&nbsp;&nbsp;&nbsp;好:</td> <td bgcolor="#FFFFFF">&nbsp;${userForm.affect[0]} ${userForm.affect[1]} ${userForm.affect[2]} ${userForm.affect[3]}</td> </tr> <tr> <td colspan="2" align="center" bgcolor="#FFFFFF"> <input name="Button" type="button" class="btn_grey" value="返回" onClick="window.location.href='index.jsp'"> &nbsp;</td> </tr> </table> </body> </html> 运行如下图所示: 提交后显示:
E L(Expression Language) 目的:为了使JSP写起来更加简单。表达式语言的灵感来自于 ECMAScript 和 XPath 表达式语言,它提供了在 JSP 中简化表达式的方法。 禁用EL表达式的3种方式 1、语法结构 ${expression} 2、[ ]与.运算符 EL 提供“.“和“[ ]“两种运算符来存取数据。 当要存取的属性名称中包含一些特殊字符,如 . 或 - 等并非字母或数字的符号,就一定要使用“[ ]“。例如: user.My−Name应当改为 {user["My-Name"]} 如果要动态取值时,就可以用“[ ]“来做,而“.“无法做到动态取值。例如: ${sessionScope.user[data]}中data 是一个变量 3、变量 EL存取变量数据的方法很简单,例如:${username}。它的意思是取出某一范围中名称为username的 变量。因为我们并没有指定哪一个范围的username,所以它会依序从Page、Request、Session、Application范围查找。假 如途中找到username,就直接回传,不再继续找下去,但是假如全部的范围都没有找到时,就回传null。 4、操作符 JSP 表达式语言提供以下操作符,其中大部分是 Java 中常用的操作符: 术语 定义 算术型 +、-(二元)、*、/、div、%、mod、-(一元) 逻辑型 and、&&、or、||、!、not 关系型 ==、eq、!=、ne、lt、gt、<=、le、>=、ge。可以与其他值进行比较,或与布尔型、字符串型、整型或浮点型文字进行比较。 空 empty 空操作符是前缀操作,可用于确定值是否为空。 条件型 A ?B :C。根据 A 赋值的结果来赋值 B 或 C。 4、隐式对象 JSP 表达式语言定义了一组隐式对象,其中许多对象在 JSP scriplet 和表达式中可用: pageContext JSP 页的上下文。它可以用于访问 JSP 隐式对象,如请求、响应、会话、输出、servletContext 等。例如,${pageContext.response} 为页面的响应对象赋值。 此外,还提供几个隐式对象,允许对以下对象进行简易访问: 术语 定义 param 将请求参数名称映射到单个字符串参数值(通过调用 ServletRequest.getParameter (String name) 获得)。getParameter (String) 方法返回带有特定名称的参数。表达式 ${param . name}相当于 request.getParameter (name)。 paramValues 将请求参数名称映射到一个数值数组(通过调用 ServletRequest.getParameter (String name) 获得)。它与 param 隐式对象非常类似,但它检索一个字符串数组而不是单个值。表达式 ${paramvalues. name} 相当于 request.getParamterValues(name)。 header 将请求头名称映射到单个字符串头值(通过调用 ServletRequest.getHeader(String name) 获得)。表达式 ${header. name} 相当于 request.getHeader(name)。 headerValues 将请求头名称映射到一个数值数组(通过调用 ServletRequest.getHeaders(String) 获得)。它与头隐式对象非常类似。表达式 ${headerValues. name} 相当于 request.getHeaderValues(name)。 cookie 将 cookie 名称映射到单个 cookie 对象。向服务器发出的客户端请求可以获得一个或多个 cookie。表达式 cookie.name.value返回带有特定名称的第一个cookie值。如果请求包含多个同名的cookie,则应该使用 {headerValues. name} 表达式。 initParam 将上下文初始化参数名称映射到单个值(通过调用 ServletContext.getInitparameter(String name) 获得)。 除了上述两种类型的隐式对象之外,还有些对象允许访问多种范围的变量,如 Web 上下文、会话、请求、页面: 术语 定义 pageScope 将页面范围的变量名称映射到其值。例如,EL 表达式可以使用 pageScope.objectName访问一个JSP中页面范围的对象,还可以使用 {pageScope .objectName. attributeName} 访问对象的属性。 requestScope 将请求范围的变量名称映射到其值。该对象允许访问请求对象的属性。例如,EL 表达式可以使用 requestScope.objectName访问一个JSP请求范围的对象,还可以使用 {requestScope. objectName. attributeName} 访问对象的属性。 sessionScope 将会话范围的变量名称映射到其值。该对象允许访问会话对象的属性。例如: ${sessionScope. name} applicationScope 将应用程序范围的变量名称映射到其值。该隐式对象允许访问应用程序范围的对象。 特别强调 1、注意当表达式根据名称引用这些对象之一时,返回的是相应的对象而不是相应的属性。例如:即使现有的 pageContext 属性包含某些其他值,${pageContext} 也返回PageContext 对象。 2、 注意 <%@ page isELIgnored="true" %> 表示是否禁用EL语言,TRUE表示禁止.FALSE表示不禁止.JSP2.0中默认的启用EL语言。 EL表达式的特点 1、在EL表达式中可以获得命名空间 2、EL表达式不仅可以访问一般变量,还可以访问JavaBean中的属性 3、在EL表达式中可以执行关系运算、逻辑运算、算术运算 4、扩展函数可以与Java类的静态方法进行映射 5、在表达式中可以访问JSP的作用域 6、EL表达式可以与JSTL结合使用,也可以与js语句结合使用
1、在eclise中安装hadoop的插件并配置 在上篇文章《编译hadoop eclipse的插件(hadoop1.0)》,已经介绍过怎样编译基于hadoop1.0的eclipse插件 将jar包放在eclipse安装目录下的plugins文件夹下。然后启动eclipse 进入后,在菜单window->Rreferences下打开设置: 点击“Ant” 出现: 点击browse选择hadoop的源码下的build目录,然后点OK 打开Window->Show View->Other 选择Map/Reduce Tools,单击Map/Reduce Locations,会打开一个View: 添加Hadoop Loacation,其中Host和Port的内容这里的host和port对应mapred-site.xml中mapred.job.tracker的值,UserName 是用户名,我配置的是localhost和9001 但是出现如下问题,eclipse的左侧看不到project explorer,更看不到其中的dfs 解决办法: 应该在菜单栏 选择:Window->Open pespective-><Map/Reduce>。然后就能看到HDFS文件系统已经所创建得一些项目。 添加Hadoop Loacation,其中Host和Port的内容跟据conf/hadoop-site.xml的配置填写,UserName 是用户名,如下图 成功添加Hadoop Loacation后还可能出现如下错误: 解决办法: 这时候,需要对namenode进行格式化:bin/hadoop namenode -format 执行命令:bin/start-all.sh 如果test下面的文件夹显示(1)而不是(2)也是正常的,如果要显示(2),运行《安装并运行hadoop》一文中最后的那几个命令。 在配置完后,在Project Explorer中就可以浏览到DFS中的文件,一级级展开,可以看到之前我们上传的in文件夹,以及当是存放的2个txt文件,同时看到一个在计算完后的out文件夹。 现在我们要准备自己写个Hadoop 程序了,所以我们要把这个out文件夹删除,有两种方式,一是可以在这树上,执行右健删除。 二是可以用命令行: $ bin/hadoop fs -rmr out 用$bin/hadoop fs -ls 查看 2、编写HelloWorld 环境搭建好了,之前运行Hadoop时,直接用了examples中的示例程序跑了下,现在可以自己来写这个HelloWorld了。在eclipse菜单下 new Project 可以看到,里面增加了Map/Reduce选项: 选中,点下一步: 输入项目名称后,继续(next), 再点Finish 然后在Project Explorer中就可以看到该项目了,展开,src发现里面啥也没有,于是右健菜单,新建类(new->new class): 然后点击Finish,就可以看到创建了一个java类了: 然后在这个类中填入下面代码: public static class TokenizerMapper extends Mapper<Object, Text, Text, IntWritable>{ private final static IntWritable one = new IntWritable(1); private Text word = new Text(); public void map(Object key, Text value, Context context ) throws IOException, InterruptedException { StringTokenizer itr = new StringTokenizer(value.toString()); while (itr.hasMoreTokens()) { word.set(itr.nextToken()); context.write(word, one); } } } public static class IntSumReducer extends Reducer<Text,IntWritable,Text,IntWritable> { private IntWritable result = new IntWritable(); public void reduce(Text key, Iterable<IntWritable> values, Context context ) throws IOException, InterruptedException { int sum = 0; for (IntWritable val : values) { sum += val.get(); } result.set(sum); context.write(key, result); } } public static void main(String[] args) throws Exception { Configuration conf = new Configuration(); String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs(); if (otherArgs.length != 2) { System.err.println("Usage: wordcount <in> <out>"); System.exit(2); } Job job = new Job(conf, "word count"); job.setJarByClass(wordCount.class); job.setMapperClass(TokenizerMapper.class); job.setCombinerClass(IntSumReducer.class); job.setReducerClass(IntSumReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); FileInputFormat.addInputPath(job, new Path(otherArgs[0])); FileOutputFormat.setOutputPath(job, new Path(otherArgs[1])); System.exit(job.waitForCompletion(true) ? 0 : 1); } 填入代码后,会看到一些错误,没关系,点击边上的红叉,然后选择里面的import即可: import java.io.IOException; import java.util.StringTokenizer; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.Mapper; import org.apache.hadoop.mapreduce.Reducer; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import org.apache.hadoop.util.GenericOptionsParser; 这里,如果直接用源码来操作,可能会GenericOptionsParser这个类找不到定义,还是红叉,添加commons-cli- 1.2.jar这个jar包,在build/ivy/lib/Hadoop/Common下,右健Project Explorer中的MyHelloWorld工程,选择Build Path->Config Build Path 在Liberaries Tab页下,点击Add External JARs 在弹出窗口中,跟据前面说的目录,找到这个jar包,点确定后,回到工程,可以看到红叉消失,说明编译都通过了。 在确保整个工程没有错误后,点击上面的小绿箭头,然后在弹出的小窗口上,选择Run On Hadoop: 点OK后,会弹出小窗口: 然手中选择Choose an existing server from the list below。然后找到之前配置的地址项,选中后,点Finish,然后系统不会Run起来,在控制台(双击可最大化)中可以看到运行结果: 运行完后,可以看到多了一个out文件夹,双击打开out文件可以看到单词的统计结果来 3、可能出现的问题: 问题1: 运行后,如果Console里只输出Usage :wordcount<in> <out>, 则需要修改下参数,在运行菜单边上小箭头,下拉,点击Run Configuration,: 左边选中 JavaApplication中的 WordCount,右边,在Arguments中输入 in out。然后再点Run 就可以看到结果了。 左边选中 JavaApplication中的 WordCount,右边,在Arguments中输入 in out。然后再点Run 就可以看到结果了。 问题2: 第二次运行会报错,仔细看提示,可以看到报错的是out目录已经存在,所以需要手动来删除一下。 更进一步 上面我们写了一个MapReduce的HelloWorld程序,现在,我们就也学一学HDFS程序的编写。HDFS是什么,它是一个分布式文件存 储系统。一般常用操作有哪些? 当然我们可以从编程角度来:创建、读、写一个文件,列出文件夹中的文件及文件夹列表,删除文件夹,删除目录,移动文件或文件夹,重命名文件或文件夹。 启动eclipse,新建Hadoop项目,名称MyHDFSTest,新建类HDFSTest,点击确定,然后同样工程属性Configure BuildPath中把 build/ivy/lib/Hadoop下的所有jar包都引用进来(不详细说明了,可参考上面的步骤) 在类中,添加main函数: public static void main(String[] args) { } 或者也可以在添加类时,勾选上创建main,则会自动添加上。 在mian函数中添加以下内容: try { Configuration conf = new Configuration(); conf.set("fs.default.name", "hdfs://localhost:9000"); FileSystem hdfs = FileSystem.get(conf); Path path = new Path("in/test3.txt"); FSDataOutputStream outputStream = hdfs.create(path); byte[] buffer = "Hello".getBytes(); outputStream.write(buffer, 0, buffer.length); outputStream.flush(); outputStream.close(); System.out.println("Create OK"); } catch (IOException e) { e.printStackTrace(); } 直接添加进来会报错,然后需要添加一些引用才行: import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; 在没有错误后,点击工具条上的运行, 但这次跟前次不一样,选择Run as Java Application。然后,就可以在输出框中看到Create OK的字样了,表明程序运行成功。 这段代码的意思是在in文件夹下,创建test3.txt,里面的内容是”Hello”。 在运行完后,我们可以到eclipse的Project Explorer中查看是否有这文件以及内容。同样也可以用命令行查看$bin/hadoop fs -ls in。 ok,第一个操作HDFS的程序跑起来了,那其它功能只要套上相应的处理类就可以了。为了方便查找操作,我们列举了张表: 操作说明 操作本地文件 操作DFS文件 主要命名空间 java.io.File java.io.FileInputStream java.io.FileOutputStream org.apache.hadoop.conf.Configuration org.apache.hadoop.fs.FileSystem org.apache.hadoop.fs.Path org.apache.hadoop.fs.FSDataInputStream; org.apache.hadoop.fs.FSDataOutputStream 初使化对象 new File(路径); Configuration FileSystem hdfs 创建文件 File.createNewFile(); FSDataOutputStream = hdfs.create(path) FSDataOutputStream.write( buffer, 0, buffer.length); 创建文件夹 File.mkdir() hdfs.mkdirs(Path); 读文件 new FileInputStream(); FileInputStream.read(buffer) FSDataInputStream = hdfs.open(path); FSDataInputStream.read(buffer); 写文件 FileOutputStream.write( buffer, 0, buffer.length); FSDataOutputStream = hdfs.append(path) FSDataOutputStream.write( buffer, 0, buffer.length); 删除文件(夹) File.delete() FileSystem.delete(Path) 列出文件夹内容 File.list(); FileSystem.listStatus() 重命令文件(夹) File.renameTo(File) FileSystem.rename(Path, Path) 有了这张表,以后在需要的时候就可以方便查询了。
在hadoop-1.0中,不像0.20.2版本,有现成的eclipse-plugin源码包,而是在HADOOP_HOME/src /contrib/eclipse-plugin目录下放置了eclipse插件的源码,这篇文章 ,我想详细记录一下自己是如何编译此源码生成适用于Hadoop1.0的eclipse插件 1、安装环境 操作系统:Ubuntu14.4 软件: eclipse java Hadoop 1.0 2、编译步骤 (1)首先下载ant与ivy的安装包 将安装包解压缩到指定的目录,然后将ivy包中的ivy-2.2.0.jar包放到ant安装目录的lib目录下,然后在/etc/profile中添加以下内容以设置配置环境: export ANT_HOME=/home/wu/opt/apache-ant-1.8.3 export PATH=”ANTHOME/bin: PATH” (2)终端转到hadoop安装目录下,执行ant compile,结果如下: …………………… compile:[echo] contrib: vaidya[javac] /home/wu/opt/hadoop-1.0.1/src/contrib/build-contrib.xml:185: warning: ‘includeantruntime’ was not set, defaulting to build.sysclasspath=last; set to false for repeatable builds[javac] Compiling 14 source files to /home/wu/opt/hadoop-1.0.1/build/contrib/vaidya/classes[javac] Note: /home/wu/opt/hadoop-1.0.1/src/contrib/vaidya/src/java/org/apache/hadoop/vaidya/statistics/job/JobStatistics.java uses unchecked or unsafe operations.[javac] Note: Recompile with -Xlint:unchecked for details. compile-ant-tasks:[javac] /home/wu/opt/hadoop-1.0.1/build.xml:2170: warning: ‘includeantruntime’ was not set, defaulting to build.sysclasspath=last; set to false for repeatable builds[javac] Compiling 5 source files to /home/wu/opt/hadoop-1.0.1/build/ant compile: BUILD SUCCESSFUL Total time: 12 minutes 29 seconds 可以看到编译成功!花的时间比较长,可以泡壶茶休息一下~~ (3)再将终端定位到HADOOP_HOME/src/contrib/eclipse-plugin,然后执行下面的命令: ant -Declipse.home=/home/wu/opt/eclipse -Dversion=1.0.1 jar 编译完成后就可以找到eclipse插件了 3、安装步骤 (1)伪分布式的配置过程也很简单,只需要修改几个文件,在代码的conf文件夹内,就可以找到下面几个配置文件,具体过程我就不多说了,这里列出我的配置: core-site.xml <configuration> <property> <name>fs.default.name</name> <value>hdfs://localhost:9000</value> </property> <property> <name>hadoop.tmp.dir</name> <value>/home/wu/hadoop-0.20.2/tmp</value> </property> </configuration> hdfs-site.xml <configuration> <property> <name>dfs.replication</name> <value>1</value> </property> </configuration> mapred-site.xml <configuration> <property> <name>fs.default.name</name> <value>hdfs://localhost:9000</value> </property> <property> <name>mapred.job.tracker</name> <value>hdfs://localhost:9001</value> </property> </configuration> 进入conf文件夹,修改配置文件:hadoop-env.sh,将里面的JAVA_HOME注释打开,并把里面的地址配置正确 (2)运行hadoop 进入hadoop目录,首次运行,需要格式化文件系统,输入命令: bin/hadoop namenode -format 输入命令,启动所有进出: bin/start-all.sh 关闭hadoop可以用: bin/stop-all.sh 最后验证hadoop是否安装成功,打开浏览器,分别输入: http://localhost:50030/ (MapReduce的web页面) http://localhost:50070/ (HDFS的web页面) 用jps命令看一下有几个java进程在运行,如果是下面几个就正常了 wu@ubuntu:~/opt/hadoop-1.0.1$ jps4113 SecondaryNameNode4318 TaskTracker3984 DataNode3429 3803 NameNode4187 JobTracker4415 Jps 系统启动正常后,现在来跑个程序: $mkdir input $cd input $echo "hello world">test1.txt $echo "hello hadoop">test2.txt $cd .. $bin/hadoop dfs -put input in $bin/hadoop jar hadoop-examples-1.0.1.jar wordcount in out $bin/hadoop dfs -cat out/* 出现一长串的运行过程: ****hdfs://localhost:9000/user/wu/in15/05/29 10:51:41 INFO input.FileInputFormat: Total input paths to process : 215/05/29 10:51:42 INFO mapred.JobClient: Running job: job_201505291029_000115/05/29 10:51:43 INFO mapred.JobClient: map 0% reduce 0%15/05/29 10:52:13 INFO mapred.JobClient: map 100% reduce 0%15/05/29 10:52:34 INFO mapred.JobClient: map 100% reduce 100%15/05/29 10:52:39 INFO mapred.JobClient: Job complete: job_201505291029_000115/05/29 10:52:39 INFO mapred.JobClient: Counters: 2915/05/29 10:52:39 INFO mapred.JobClient: Job Counters 15/05/29 10:52:39 INFO mapred.JobClient: Launched reduce tasks=115/05/29 10:52:39 INFO mapred.JobClient: SLOTS_MILLIS_MAPS=4372415/05/29 10:52:39 INFO mapred.JobClient: Total time spent by all reduces waiting after reserving slots (ms)=015/05/29 10:52:39 INFO mapred.JobClient: Total time spent by all maps waiting after reserving slots (ms)=015/05/29 10:52:39 INFO mapred.JobClient: Launched map tasks=215/05/29 10:52:39 INFO mapred.JobClient: Data-local map tasks=215/05/29 10:52:39 INFO mapred.JobClient: SLOTS_MILLIS_REDUCES=2007215/05/29 10:52:39 INFO mapred.JobClient: File Output Format Counters 15/05/29 10:52:39 INFO mapred.JobClient: Bytes Written=2515/05/29 10:52:39 INFO mapred.JobClient: FileSystemCounters15/05/29 10:52:39 INFO mapred.JobClient: FILE_BYTES_READ=5515/05/29 10:52:39 INFO mapred.JobClient: HDFS_BYTES_READ=23915/05/29 10:52:39 INFO mapred.JobClient: FILE_BYTES_WRITTEN=6483715/05/29 10:52:39 INFO mapred.JobClient: HDFS_BYTES_WRITTEN=2515/05/29 10:52:39 INFO mapred.JobClient: File Input Format Counters 15/05/29 10:52:39 INFO mapred.JobClient: Bytes Read=2515/05/29 10:52:39 INFO mapred.JobClient: Map-Reduce Framework15/05/29 10:52:39 INFO mapred.JobClient: Map output materialized bytes=6115/05/29 10:52:39 INFO mapred.JobClient: Map input records=215/05/29 10:52:39 INFO mapred.JobClient: Reduce shuffle bytes=6115/05/29 10:52:39 INFO mapred.JobClient: Spilled Records=815/05/29 10:52:39 INFO mapred.JobClient: Map output bytes=4115/05/29 10:52:39 INFO mapred.JobClient: CPU time spent (ms)=733015/05/29 10:52:39 INFO mapred.JobClient: Total committed heap usage (bytes)=24727552015/05/29 10:52:39 INFO mapred.JobClient: Combine input records=415/05/29 10:52:39 INFO mapred.JobClient: SPLIT_RAW_BYTES=21415/05/29 10:52:39 INFO mapred.JobClient: Reduce input records=415/05/29 10:52:39 INFO mapred.JobClient: Reduce input groups=315/05/29 10:52:39 INFO mapred.JobClient: Combine output records=415/05/29 10:52:39 INFO mapred.JobClient: Physical memory (bytes) snapshot=33884569615/05/29 10:52:39 INFO mapred.JobClient: Reduce output records=315/05/29 10:52:39 INFO mapred.JobClient: Virtual memory (bytes) snapshot=113943347215/05/29 10:52:39 INFO mapred.JobClient: Map output records=4 查看out文件夹: wu@ubuntu:~/opt/hadoop-1.0.1$ bin/hadoop dfs -cat out/* hadoop 1hello 2world 1
对于全分布式的HBase安装,需要通过hbase-site.xml文档来配置本机的HBase特性,由于各个HBase之间通过zookeeper来进行通信,因此需要维护一组zookeeper系统,关于zookeeper的安装使用,参考《hadoop2.6完全分布式安装zookeeper3.4.6》 关于HBase的介绍,可以看这里《HBase简介》 1、安装Hbase (1)下载hbase版本 下载地址:http://www.apache.org/dyn/closer.cgi/hbase/,在此路径下选择相应的版本下载,本次安装下载hadoop1.1.0.1版本 (2)解压hbase-1.1.0.1-bin.tar.gz hadoop@master:~/opt$ tar zxvf hbase-1.1.0.1-bin.tar.gz (3)将hbase添加到环境变量中 export HBASE_HOME=/home/hadoop/opt/hbase-1.1.0.1 export PATH=$HBASE_HOME/bin:$PATH (4)修改配置文件 修改hbase-env.sh export JAVA_HOME=/home/hadoop/opt/jdk1.8.0_65 修改hbase-site.xml <configuration> <property> <name>hbase.rootdir</name> <value>hdfs://master:9000/hbase</value> </property> <property> <name>hbase.cluster.distributed</name> <value>true</value> </property> <property> <name>hbase.zookeeper.quorum</name> <value>master,slave1</value> </property> <property> <name>hbase.zookeeper.property.dataDir</name> <value>/home/hadoop/opt/zookeeper-3.4.6</value> </property> </configuration> 修改regionservers,将文件内容设置为: master slave1 2、运行HBase 启动hbase时要确保hdfs已经启动,HBase的启动顺序为:HDFS->Zookeeper->HBase,运行命令如下: hadoop@master:~/opt/hadoop-2.6.0$ bin/hdfs namenode -format hadoop@master:~/opt/hadoop-2.6.0$ sbin/start-dfs.sh hadoop@master:~/opt/zookeeper-3.4.6$ ./zkServer.sh start hadoop@master:~/opt/hbase-1.1.0.1$ start-hbase.sh 启动成功后集群会多出如下进程: hadoop@master:~/opt/hbase-1.1.0.1$ jps 8145 SecondaryNameNode7940 NameNode8550 HRegionServer5719 QuorumPeerMain8600 Jps8424 HMaster 运行成功后可以看到QuorumPeerMain进程 再看看slave1的进程: hadoop@slave1:~/opt/zookeeper-3.4.6/bin$ jps 6915 QuorumPeerMain7012 HRegionServer7268 Jps6823 DataNode 进入HBase Shell hadoop@master:~/opt/hbase-1.1.0.1$ ./bin/hbase shell 输入status命令: hbase(main):001:0> status 出现错误: 2015-05-25 20:26:14,949 ERROR [main] client.ConnectionManager$HConnectionImplementation: Can’t get connection to ZooKeeper: KeeperErrorCode = ConnectionLoss for /hbase 设置 conf/hbase-env.sh文件中的HBASE_MANAGES_ZK 属性为 false 问题搞定! hbase(main):001:0> status 2 servers, 0 dead, 1.0000 average load
为什么我们需要HDFS 文件系统由三部分组成:与文件管理有关软件、被管理文件以及实施文件管理所需数据结构。 既然读取一块磁盘的所有数据需要很长时间,写入更是需要更长时间(写入时间一般是读取时间的3倍)。我们需要一个巨大文件难道得换传输速度10GB/S的磁盘(现在没有这样的磁盘),而且即使有文件为1ZB,或者小点10EB时,这样的磁盘也无法做到随读随取。 当数据集的大小超过一台独立物理计算机的存储能力时,就有必要对它进行分区并存储到若干台单独的计算机上。 从概念图上看,分布化的文件系统会因为分布后的结构不完整,导致系统复杂度加大,并且引入的网络编程,同样导致分布式文件系统更加复杂。 对于以上的问题我们来HDFS是如何迎刃而解的? HDFS以流处理访问模式来存储文件的。 一次写入,多次读取。数据源通常由源生成或从数据源直接复制而来,接着长时间在此数据集上进行各类分析,大数据不需要搬来搬去。 DFS是用流处理方式处理文件,每个文件在系统里都能找到它的本地化映像,所以对于用户来说,别管文件是什么格式的,也不用在意被分到哪里,只管从DFS里取出就可以了。 一般来说,文件处理过程中无法保证文件安全顺利到达,传统文件系统是使用本地校验方式保证数据完整,文件被散后,难道需要特意安排每个分片文件的校验码? 分片数量和大小是不确定的,海量的数据本来就需要海量的校验过程,分片后加入每个分片的跟踪校验完全是在数满天恒星的同时数了他们的行星。× HDFS的解决方案是分片冗余,本地校验。 数据冗余式存储,直接将多份的分片文件交给分片后的存储服务器去校验 冗余后的分片文件还有个额外功能,只要冗余的分片文件中有一份是完整的,经过多次协同调整后,其他分片文件也将完整。 经过协调校验,无论是传输错误,I/O错误,还是个别服务器宕机,整个系统里的文件是完整的 分布后的文件系统有个无法回避的问题,因为文件不在一个磁盘导致读取访问操作的延时,这个是HDFS现在遇到的主要问题。 现阶段,HDFS的配置是按照高数据吞吐量优化的,可能会以高时间延时为代价。但万幸的是,HDFS是具有很高弹性,可以针对具体应用再优化。 HDFS的概念 HDFS可以用下面这个抽象图的具体实现 何为元数据? 元数据是用于描述要素、数据集或数据集系列的内容、覆盖范围、质量、管理方式、数据的所有者、数据的提供方式等有关的信息。更简单的说,是关于数据的数据。 HDFS就是将巨大的数据变成大量数据的数据。 PS: 磁盘存储文件时,是按照数据块来存储的,也就是说,数据块是磁盘的读/写最小单位。数据块也称磁盘块。构建于单个磁盘上的文件系统是通过磁盘块来管理文件 系统,一般来说,文件系统块的大小是磁盘块的整数倍。特别的,单个磁盘文件系统,小于磁盘块的文件会占用整个磁盘块。磁盘块的大小一般是512字节。 在HDFS中,也有块(block)这个概念,默认为64MB,每个块作为独立的存储单元。 与其他文件系统不一样,HDFS中每个小于块大小的文件不会占据整个块的空间。具体原因在后面的介绍。下面介绍为什么是64MB一个文件块 在文件系统中,系统存储文件时,需要定位该数据在磁盘中的位置,再进行传输处理。 定位在磁盘的位置是需要时间的,同样文件传输也是需要时间。 T(存储时间)=T(定位时间)+T(传输时间) 如果每个要传输的块设置得足够大,那么从磁盘传输数据的时间可以明显大于定位这个块开始位置的时间 T(存储时间)=T(定位时间) )[-∞]+T(传输时间)[∞] 近似等于:T(存储时间)=T(传输时间) 举个例子:我们来传输一个10000MB的文件 单个磁盘下: 存储1个10000MB的文件我们需要时间是 10msX100+1000msX100=101s 10台数据节点: 传输10000MB的文件所花的时间:10msX10+10ms+10s=10.11s 此例子是理论数据,实际比这个稍长。 总结: 这样的设定使存储一个文件主要时间就花在传输过程中,块大小决定传输由多个快组成文件的存储速率,这也是HSDF的核心技术。 当然不是设置每个块越大越好。 HDFS提供给MapReduce数据服务,而一般来说MapReduce的Map任务通常一次处理一个块中的数据,如果任务数太少(少于集群中节点的数量),就没有发挥多节点的优势,甚至作业的运行速度就会和单节点一样。 分布式的文件抽象能够带来的优势是: 1、一个文件可以大于每个磁盘 2、文件不用全在一个磁盘上。 3、简化了存储子系统的设计。 不仅如此,基于元数据块的存储方式非常适合用于备份,利用备份可提供数据容错能力和可用性。 HDFS的关键运作机制 HDFS是基于主从结构(master/slaver)构件。 详细运行机制将在下篇文章介绍。。。。。。 如何使用HDFS HDFS是在安装hadoop-0.20.2.tar.gz并成功配置后即可使用。具体安装过程不再赘述。参见:《安装并运行hadoop》、《Ubuntu 14.04下安装JDK8 》 无论是使用shell脚本,或者使用WEB UI进行操作,使用前必须得明白HDFS的配置。便于存储操作或者操作优化。
适配器模式(别名:包装器) 将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 概述 适配器模式是将一个类的接口(被适配者)转换成客户希望的另外一个接口(目标)的成熟模式,该模式中涉及有目标、被适配者和适配器。适配器模式的关键是建立一个适配器,这个适配器实现了目标接口并包含有被适配者的引用。 适用性 1.你想使用一个已经存在的类,而它的接口不符合你的需求。 2.你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作。 3.(仅适用于对象Adapter)你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口。对象适配器可以适配它的父类接口。 参与者 1.Target 定义Client使用的与特定领域相关的接口。 2.Client 与符合Target接口的对象协同。 3.Adaptee 定义一个已经存在的接口,这个接口需要适配。 4.Adapter 对Adaptee的接口与Target接口进行适配 适配器模式的结构与使用 模式的结构中包括三种角色: 目标(Target) 被适配者(Adaptee) 适配器(Adapter) 模式的UML类图: 实战部分 用户已有一个两相的插座,但最近用户又有了一个新的三相插座。用户现在已经有一台洗衣机和一台电视机,洗衣机按着三相插座的标准配有三相插头,而电视机按着两相插座的标准配有两相插头。现在用户想用新的三相插座来使用洗衣机和电视机。 1.目标(Target) : ThreeElectricOutlet.java public interface ThreeElectricOutlet{ public abstract void connectElectricCurrent(); } 2.被适配者(Adaptee): TwoElectricOutlet.java public interface TwoElectricOutlet{ public abstract void connectElectricCurrent(); } 3.适配器(Adapter): TreeElectricAdapter.java public class TreeElectricAdapter implements ThreeElectricOutlet{ TwoElectricOutlet outlet; TreeElectricAdapter(TwoElectricOutlet outlet){ this.outlet=outlet; } public void connectElectricCurrent(){ outlet.connectElectricCurrent(); } } 4.应用 Application.java_1 public class Application{ public static void main(String args[]){ ThreeElectricOutlet outlet; Wash wash=new Wash(); outlet=wash; System.out.println("使用三相插座接通电流:"); outlet.connectElectricCurrent(); TV tv=new TV(); TreeElectricAdapter adapter=new TreeElectricAdapter(tv); outlet=adapter; System.out.println("使用三相插座接通电流:"); outlet.connectElectricCurrent(); } } 4.应用 Application.java_2 class Wash implements ThreeElectricOutlet{ String name; Wash(){ name="黄河洗衣机"; } Wash(String s){ name=s; } public void connectElectricCurrent(){ turnOn(); } public void turnOn(){ System.out.println(name+"开始洗衣物。"); } } 4.应用 Application.java_3 class TV implements TwoElectricOutlet{ String name; TV(){ name="长江电视机"; } TV(String s){ name=s; } public void connectElectricCurrent(){ turnOn(); } public void turnOn(){ System.out.println(name+"开始播放节目。"); } } 适配器模式的优点 •目标(Target)和被适配者(Adaptee)是完全解耦的关系。 •适配器模式满足“开-闭原则”。当添加一个实现Adaptee接口的新类时,不必修改Adapter,Adapter就能对这个新类的实例进行适配。
原型模式 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。 概述 原型模式是从一个对象出发得到一个和自己有相同状态的新对象的成熟模式,该模式的关键是将一个对象定义为原型,并为其提供复制自己的方法。 java.lang.Object类的clone方法 参见《java中的深浅克隆》 适用性 1.当一个系统应该独立于它的产品创建、构成和表示时。 2.当要实例化的类是在运行时刻指定时,例如,通过动态装载。 3.为了避免创建一个与产品类层次平行的工厂类层次时。 4.当一个类的实例只能有几个不同状态组合中的一种时。 建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。 参与者 1. Prototype 声明一个克隆自身的接口。 2. ConcretePrototype 实现一个克隆自身的操作。 3. Client 让一个原型克隆自身从而创建一个新的对象。 原型模式的结构与使用 模式的结构中包括两种角色: •抽象原型(Prototype) •具体原型(Concrete Prototype) 模式的UML类图: 实战部分 【例1】:实现一个克隆接口,然后实现一个克隆自身的操作并加以应用 1.抽象原型(Prototype): Prototype.java public interface Prototype { public Object cloneMe() throws CloneNotSupportedException; } 2.具体原型(Concrete Prototype)_1: Cubic.java public class Cubic implements Prototype, Cloneable{ double length, width, height; Cubic(double a, double b, double c){ length = a; width = b; height = c; } public Object cloneMe() throws CloneNotSupportedException{ Cubic object = (Cubic)clone(); return object; } } 2.具体原型(Concrete Prototype)_2 : Goat.java import java.io.*; public class Goat implements Prototype,Serializable{ StringBuffer color; public void setColor(StringBuffer c){ color = c; } public StringBuffer getColor(){ return color; } public Object cloneMe() throws CloneNotSupportedException{ Object object = null; try{ ByteArrayOutputStream outOne = new ByteArrayOutputStream(); ObjectOutputStream outTwo = new ObjectOutputStream(outOne); outTwo.writeObject(this); ByteArrayInputStream inOne= new ByteArrayInputStream(outOne.toByteArray()); ObjectInputStream inTwo = new ObjectInputStream(inOne); object=inTwo.readObject(); } catch(Exception event){ System.out.println(event); } return object; } } 3.应用 Application.java public class Application { public static void main(String[] args) { Cubic cubic = new Cubic(12, 20, 66); System.out.println("cubic的长、宽和高: "); System.out.println(cubic.length + "," + cubic.width + "," + cubic.height); try { Cubic cubicCopy = (Cubic)cubic.cloneMe(); System.out.println("cubicCopy的长、宽和高: "); System.out.println(cubicCopy.length + "," + cubicCopy.width + "," + cubicCopy.height); } catch(CloneNotSupportedException ex){} Goat goat = new Goat(); goat.setColor(new StringBuffer("白色的山羊")); System.out.println("goat是" + goat.getColor()); try { Goat goatCopy = (Goat)goat.cloneMe(); System.out.println("goatCopy是" + goatCopy.getColor()); System.out.println("goatCopy将自己的颜色改变成黑色"); goatCopy.setColor(new StringBuffer("黑颜色的山羊")); System.out.println("goat仍然是"+ goat.getColor()); System.out.println("goatCopy是"+ goatCopy.getColor()); } catch(CloneNotSupportedException ex){} } } 原型模式的优点 •当创建类的新实例的代价更大时,使用原型模式复制一个已有的实例可以提高创建新实例的效率。 •可以动态地保存当前对象的状态。在运行时刻,可以随时使用对象流保存当前对象的一个复制品。
假设有一个对象object,在某处又需要一个跟object一样的实例object2,强调的是object和object2是两个独立的实例, 只是在开始的时候,他们是具有相同状态的(属性字段的值都相同)。遇到这种情况的做法一般是,重新new一个对象object2,将object的字段值 赋予object2,即:object2=object; 这样的话两个引用仍然指向的是同一个对象,不是两个对象。 克隆方法clone() Java中跟克隆有关的两个类分别是Cloneable接口和Object类中的clone方法,通过两者的协作来实现克隆。 首先来看看Object的clone()源代码: /** * Creates and returns a copy of this {@code Object}. The default * implementation returns a so-called "shallow" copy: It creates a new * instance of the same class and then copies the field values (including * object references) from this instance to the new instance. A "deep" copy, * in contrast, would also recursively clone nested objects. A subclass that * needs to implement this kind of cloning should call {@code super.clone()} * to create the new instance and then create deep copies of the nested, * mutable objects. * * @return a copy of this object. * @throws CloneNotSupportedException * if this object's class does not implement the {@code * Cloneable} interface. */ protected Object clone() throws CloneNotSupportedException { if (!(this instanceof Cloneable)) { throw new CloneNotSupportedException("Class doesn't implement Cloneable"); } return internalClone((Cloneable) this); } /* * Native helper method for cloning. */ private native Object internalClone(Cloneable o); 首先看一下java api doc中关于Cloneable接口和Object类中的clone方法的描述: java.lang.Cloneable 接口(以下源引JavaTM 2 Platform Standard Ed. 5.0 API DOC) 此类实现了 Cloneable 接口,以指示 Object.clone() 方法可以合法地对该类实例进行按字段复制。 如果在没有实现 Cloneable 接口的实例上调用 Object 的 clone 方法,则会导致抛出 CloneNotSupportedException异常。 按照惯例,实现此接口的类应该使用公共方法重写 Object.clone(它是受保护的)。请参阅 Object.clone(),以获得有关重写此方法的详细信息。 注意,此接口不包含 clone 方法。因此,因为某个对象实现了此接口就克隆它是不可能的。即使 clone 方法是反射性调用的,也无法保证它将获得成功。 Cloneable接口没有任何方法,仅是个标志接口(tagging interface),若要具有克隆能力,实现Cloneable接口的类必须重写从Object继承来的clone方法,并调用Object的clone方法(见下面Object#clone的定义),重写后的方法应为public 的。 clone方法首先会判对象是否实现了Cloneable接口,若无则抛出CloneNotSupportedException, 最后会调用internalClone. intervalClone是一个native方法,一般来说native方法的执行效率高于非native方法。 当某个类要复写clone方法时,要继承Cloneable接口。通常的克隆对象都是通过super.clone()方法来克隆对象。 浅克隆(shadow clone) 克隆就是复制一个对象的复本,若只需要复制对象的字段值(对于基本数据类型,如:int,long,float等,则复制值;对于复合数据类型仅复制该字段值,如数组变量则复制地址,对于对象变量则复制对象的reference。 举个例子: public class ShadowClone implements Cloneable{ private int a; // 基本类型 private int[] b; // 非基本类型 // 重写Object.clone()方法,并把protected改为public @Override public Object clone(){ ShadowClone sc = null; try { sc = (ShadowClone) super.clone(); } catch (CloneNotSupportedException e){ e.printStackTrace(); } return sc; } public int getA() { return a; } public void setA(int a) { this.a = a; } public int[] getB() { return b; } public void setB(int[] b) { this.b = b; } } 测试代码如下: public class Test{ public static void main(String[] args) throws CloneNotSupportedException{ ShadowClone c1 = new ShadowClone(); //对c1赋值 c1.setA(100) ; c1.setB(new int[]{1000}) ; System.out.println("克隆前c1: a="+c1.getA()+" b="+c1.getB()[0]); //克隆出对象c2,并对c2的属性A,B,C进行修改 ShadowClone c2 = (ShadowClone) c1.clone(); //对c2进行修改 c2.setA(50) ; int []a = c2.getB() ; a[0]=5 ; c2.setB(a); System.out.println("克隆前c1: a="+c1.getA()+" b="+c1.getB()[0]); System.out.println("克隆后c2: a="+c2.getA()+ " b[0]="+c2.getB()[0]); } } 运行结果: 克隆前c1: a=100 b=1000克隆前c1: a=100 b=5克隆后c2: a=50 b[0]=5 c1和c2的对象模型: 可以看出,基本类型可以使用浅克隆,而对于引用类型,由于引用的是内容相同,所以改变c2实例对象中的属性就会影响到c1。所以引用类型需要使用深克隆。另外,在开发一个不可变类的时候,如果这个不可变类中成员有引用类型,则就需要通过深克隆来达到不可变的目的。 深克隆(deep clone) 深克隆与浅克隆的区别在于对复合数据类型的复制。若对象中的某个字段为复合类型,在克隆对象的时候,需要为该字段重新创建一个对象。 再举一个例子: public class DeepClone implements Cloneable { private int a; // 基本类型 private int[] b; // 非基本类型 // 重写Object.clone()方法,并把protected改为public @Override public Object clone(){ DeepClone sc = null; try { sc = (DeepClone) super.clone(); int[] t = sc.getB(); int[] b1 = new int[t.length]; for (int i = 0; i < b1.length; i++) { b1[i] = t[i]; } sc.setB(b1); } catch (CloneNotSupportedException e){ e.printStackTrace(); } return sc; } public int getA() { return a; } public void setA(int a) { this.a = a; } public int[] getB() { return b; } public void setB(int[] b) { this.b = b; } } 运行结果: 克隆前c1: a=100 b=1000 克隆前c1: a=100 b=1000 克隆后c2: a=50 b[0]=5 对象模型: 使用序列化实现深克隆 public class DeepClone implements Serializable{ private int a; private int[] b; public int getA() { return a; } public void setA(int a) { this.a = a; } public int[] getB() { return b; } public void setB(int[] b) { this.b = b; } } 然后编写测试类: package test2; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class Test2{ public static void main(String[] args) throws CloneNotSupportedException{ Test2 t = new Test2(); DeepClone dc1 = new DeepClone(); // 对dc1赋值 dc1.setA(100); dc1.setB(new int[] { 1000 }); System.out.println("克隆前dc1: a=" + dc1.getA()+"b[0]=" + dc1.getB()[0]); DeepClone dc2 = (DeepClone) t.deepClone(dc1); // 对c2进行修改 dc2.setA(50); int[] a = dc2.getB(); a[0] = 500; System.out.println("克隆后dc1: a=" + dc1.getA()+"b[0]=" + dc1.getB()[0]); System.out.println("克隆后dc2: a=" + dc2.getA()+"b[0]=" + dc2.getB()[0]); } // 用序列化与反序列化实现深克隆 public Object deepClone(Object src){ Object o = null; try{ if (src != null){ ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(src); oos.close(); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); o = ois.readObject(); ois.close(); } } catch (IOException e){ e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } return o; } } 运行后的结果如下: 克隆前dc1: a=100 b[0]=1000 克隆后dc1: a=100 b[0]=1000 克隆后dc2: a=50 b[0]=500 可以看到,两个引用所指向的对象在堆中相互独立,互不干扰,这样就实现了深度克隆。 总结: 1、克隆方法用于创建对象的拷贝,为了使用clone方法,类必须实现java.lang.Cloneable接口重写protected方法clone,如果没有实现Clonebale接口会抛出CloneNotSupportedException. 2、在克隆java对象的时候不会调用构造器 3、java提供一种叫浅拷贝(shallow copy)的默认方式实现clone,创建好对象的副本后然后通过赋值拷贝内容,意味着如果你的类包含引用类型,那么原始对象和克隆都将指向相同的引用内 容,这是很危险的,因为发生在可变的字段上任何改变将反应到他们所引用的共同内容上。为了避免这种情况,需要对引用的内容进行深度克隆。 4、按照约定,实例的克隆应该通过调用super.clone()获取,这样有助克隆对象的不变性。如:clone!=original和clone.getClass()==original.getClass(),尽管这些不是必须的
建造者模式 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。 概述 当系统准备为用户提供一个内部结构复杂的对象时,就可以使用生成器模式,使用该模式可以逐步地构造对象,使得对象的创建更具弹性。生成器模式的关键是将一个包含有多个组件对象的创建分成若干个步骤,并将这些步骤封装在一个称作生成器的接口中。 适用性 1.当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。 2.当构造过程必须允许被构造的对象有不同的表示时。 参与者 1.Builder 为创建一个Product对象的各个部件指定抽象接口。 2.ConcreteBuilder 实现Builder的接口以构造和装配该产品的各个部件。 定义并明确它所创建的表示。 提供一个检索产品的接口。 3.Director 构造一个使用Builder接口的对象。 4.Product 表示被构造的复杂对象。ConcreteBuilder创建该产品的内部表示并定义它的装配过程。 包含定义组成部件的类,包括将这些部件装配成最终产品的接口。 建造者模式的结构与使用 模式的结构中包括四种角色: •产品(Product) •抽象生成器(Builder) •具体生成器(ConcreteBuilder) •指挥者(Director) 模式的UML类图 实战部分 【例1】:创建含有按钮、标签和文本框组件的容器。不同用户对容器有不同的要求,比如某些用户希望容器中只含有 按钮和标签,某些用户希望容器只含有按钮和文本框等。另外用户对组件在容器中的顺序位置也有不同的要求,比如某些用户要求组件在容器中从左至右的排列顺序 是按钮、标签、文本框,而某些用户要求从左至右的排序时标签、文本框、按钮。 模式的结构的描述与使用 1.产品(Product): PanelProduct.java import javax.swing.*; public class PanelProduct extends JPanel{ JButton button; JLabel label; JTextField textField; } 2.抽象生成器(Builder): Builer.java import.javax.swing.*; public interface Builder{ public abstract void buildButton(); public abstract void buildLabel(); public abstract void buildTextField(); public abstract JPanel getPanel(); } 3.具体生成器(ConcreteBuilder)_1 : ConcreteBuilderOne.java import javax.swing.*; public class ConcreteBuilderOne implements Builder{ private PanelProduct panel; ConcreteBuilderOne(){ panel=new PanelProduct(); } public void buildButton(){ panel.button=new JButton("按钮"); } public void buildLabel(){ panel.label=new JLabel("标签"); } public void buildTextField(){ } public JPanel getPanel(){ panel.add(panel.button); panel.add(panel.label); return panel; } } 3.具体生成器(ConcreteBuilder)_2 : ConcreteBuilderTwo.java import javax.swing.*; public class ConcreteBuilderTwo implements Builder{ private PanelProduct panel; ConcreteBuilderTwo(){ panel=new PanelProduct(); } public void buildButton(){ panel.button=new JButton("button"); } public void buildLabel(){ } public void buildTextField(){ panel.textField=new JTextField("textField"); } public JPanel getPanel(){ panel.add(panel.textField); panel.add(panel.button); return panel; } } 4.指挥者(Director): Director.java import javax.swing.*; public class Director{ private Builder builder; Director(Builder builder){ this.builder=builder; } public JPanel constructProduct(){ builder.buildButton(); builder.buildLabel(); builder.buildTextField(); JPanel product=builder.getPanel(); return product; } } 5.应用 Application.java import javax.swing.*; public class Application{ public static void main(String args[]){ Builder builder=new ConcreteBuilderOne(); Director director=new Director(builder); JPanel panel=director.constructProduct(); JFrame frameOne=new JFrame(); frameOne.add(panel); frameOne.setBounds(12,12,200,120); frameOne.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); frameOne.setVisible(true); builder=new ConcreteBuilderTwo(); director=new Director(builder); panel=director.constructProduct(); JFrame frameTwo=new JFrame(); frameTwo.add(panel); frameTwo.setBounds(212,12,200,120); frameTwo.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); frameTwo.setVisible(true); } } 【例2】:构建一个男人的类,使得构建和表示分离 类图如下所示: Builder public interface PersonBuilder { void buildHead(); void buildBody(); void buildFoot(); Person buildPerson(); } ConcreteBuilder public class ManBuilder implements PersonBuilder { Person person; public ManBuilder() { person = new Man(); } @Override public void buildHead() { person.setHead("建造男人的头"); } @Override public void buildBody() { person.setBody("建造男人的身体"); } @Override public void buildFoot() { person.setFoot("建造男人的脚"); } @Override public Person buildPerson() { return person; } } Director public class PersonDirector { public Person constructPerson(PersonBuilder pb) { pb.buildHead(); pb.buildBody(); pb.buildFoot(); return pb.buildPerson(); } } Product public class Person { private String head; private String body; private String foot; public String getHead() { return head; } public void setHead(String head) { this.head = head; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } public String getFoot() { return foot; } public void setFoot(String foot) { this.foot = foot; } } public class Man extends Person { } Test public class Test { public static void main(String[] args) { PersonDirector pd = new PersonDirector(); Person person = pd.constructPerson(new ManBuilder()); System.out.println(person.getBody()); System.out.println(person.getFoot()); System.out.println(person.getHead()); } } 生成器模式的优点 •生成器模式将对象的构造过程封装在具体生成器中,用户使用不同的具体生成器就可以得到该对象的不同表示。 •可以更加精细有效地控制对象的构造过程。生成器将对象的构造过程分解成若干步骤,这就使得程序可以更加精细,有效地控制整个对象的构造。 •生成器模式将对象的构造过程与创建该对象类解耦,使得对象的创建更加灵活有弹性。 •当增加新的具体生成器时,不必修改指挥者的代码,即该模式满足开-闭原则。
单例模式 保证一个类仅有一个实例,并提供一个访问它的全局访问点。 概述 单例模式是关于怎样设计一个类,并使得该类只有一个实例的成熟模式,该模式的关键是将类的构造方法设置为private权限,并提供一个返回它的唯一实例的类方法。 适用性 1.当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。 2.当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时。 参与者 Singleton 定义一个Instance操作,允许客户访问它的唯一实例。Instance是一个类操作。 可能负责创建它自己的唯一实例。 单例模式的结构与使用 模式的结构中只包括一个角色: •单件类(Singleton) 模式的UML类图 实战部分 模式的结构的描述与使用 1.单例类(Singleton): Moon.java public class Moon { private static Moon uniqueMoon; double radius; double distanceToEarth; private Moon() { uniqueMoon = this; radius = 1738; distanceToEarth = 363300; } public static synchronized Moon getMoon() { if(uniqueMoon == null) { uniqueMoon = new Moon(); } return uniqueMoon; } public String show() { String s = "月亮的半径是" + radius + "km, 距地球是" + distanceToEarth + "km"; return s; } } 2.应用 Application.java import javax.swing.*; import java.awt.*; public class Application { public static void main(String[] args) { MyFrame f1 = new MyFrame("张三看月亮"); MyFrame f2 = new MyFrame("李四看月亮"); f1.setBounds(10, 10, 360, 150); f2.setBounds(370, 10, 360, 150); f1.validate(); f2.validate(); } } class MyFrame extends JFrame { String str; MyFrame(String title) { setTitle(title); Moon moon = Moon.getMoon(); str = moon.show(); setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); setVisible(true); repaint(); } public void paint(Graphics g) { super.paint(g); g.setFont(new Font("宋体", Font.BOLD, 14)); g.drawString(str, 5, 100); } } 单例模式的优点 单件类的唯一实例由单件类本身来控制,所以可以很好地控制用户何时访问它。
抽象工厂模式(别名:配套) 提供一个创建一系列(相互依赖)对象的接口,而无需指定它们具体的类。 概述 当系统准备为用户提供一系列相关的对象,又不想让用户代码和创建这些对象的类形成耦合时,就可以使用抽象工厂方法模式来设计系统。抽象工厂模式的关 键是在一个抽象类或接口中定义若干个抽象方法,这些抽象方法分别返回某个类的实例,该抽象类或接口让其子类或实现该接口的类重写这些抽象方法,为用户提供 一系列相关的对象。 适用性 1.一个系统要独立于它的产品的创建、组合和表示时。 2.一个系统要由多个产品系列中的一个来配置时。 3.当你要强调一系列相关的产品对象的设计以便进行联合使用时。 4.当你提供一个产品类库,而只想显示它们的接口而不是实现时。 参与者 1.AbstractFactory 声明一个创建抽象产品对象的操作接口。 2.ConcreteFactory 实现创建具体产品对象的操作。 3.AbstractProduct 为一类产品对象声明一个接口。 4.ConcreteProduct 定义一个将被相应的具体工厂创建的产品对象。 实现AbstractProduct接口。 5.Client 仅使用由AbstractFactory和AbstractProduct类声明的接口 抽象工厂模式的结构与使用 模式的结构中包括四种角色: •抽象产品(Prodcut) •具体产品(ConcreteProduct) •抽象工厂(AbstractFactory) •具体工厂(ConcreteFactory) 模式的UML类图 实战部分 【例1】:建立一个系统,该系统可以为用户提供西服套装(上衣+裤子)和牛仔套装(上衣+裤子)。 模式的结构的描述与使用 1.抽象产品(Product) : UpperClothes.java public abstract class UpperClothes{ public abstract int getChestSize(); public abstract int getHeight(); public abstract String getName(); } Trousers.java public abstract class Trousers{ public abstract int getWaistSize(); public abstract int getHeight(); public abstract String getName(); } 2.具体产品(ConcreteProduct)_1: WesternUpperClothes.java public class WesternUpperClothes extends UpperClothes{ private int chestSize; private int height; private String name; WesternUpperClothes(String name,int chestSize,int height){ this.name=name; this.chestSize=chestSize; this.height=height; } public int getChestSize(){ return chestSize; } public int getHeight(){ return height; } public String getName(){ return name; } } 2.具体产品(ConcreteProduct)_2: CowboyUpperClothes.java public class CowboyUpperClothes extends UpperClothes{ private int chestSize; private int height; private String name; CowboyUpperClothes(String name,int chestSize,int height){ this.name=name; this.chestSize=chestSize; this.height=height; } public int getChestSize(){ return chestSize; } public int getHeight(){ return height; } public String getName(){ return name; } } 2.具体产品(ConcreteProduct)_3: WesternTrousers.java public class WesternTrousers extends Trousers{ private int waistSize; private int height; private String name; WesternTrousers(String name,int waistSize,int height){ this.name=name; this.waistSize=waistSize; this.height=height; } public int getWaistSize(){ return waistSize; } public int getHeight(){ return height; } public String getName(){ return name; } } 2.具体产品(ConcreteProduct)_4: CowboyTrousers.java public class CowboyTrousers extends Trousers{ private int waistSize; private int height; private String name; CowboyTrousers(String name,int waistSize,int height){ this.name=name; this.waistSize=waistSize; this.height=height; } public int getWaistSize(){ return waistSize; } public int getHeight(){ return height; } public String getName(){ return name; } } 3.抽象工厂(AbstractFactory): ClothesFactory.java public abstract class ClothesFactory{ public abstract UpperClothes createUpperClothes(int chestSize,int height); public abstract Trousers createTrousers(int waistSize,int height); } 4.具体工厂(ConcreteFactory): BeijingClothesFactory.java public class BeijingClothesFactory extends ClothesFactory { public UpperClothes createUpperClothes(int chestSize,int height){ return new WesternUpperClothes("北京牌西服上衣",chestSize,height); } public Trousers createTrousers(int waistSize,int height){ return new WesternTrousers("北京牌西服裤子",waistSize,height); } } ShanghaiClothesFactory.java public class ShanghaiClothesFactory extends ClothesFactory { public UpperClothes createUpperClothes(int chestSize,int height){ return new WesternUpperClothes("上海牌牛仔上衣",chestSize,height); } public Trousers createTrousers(int waistSize,int height){ return new WesternTrousers("上海牌牛仔裤",waistSize,height); } } 5.应用_1: Shop.java public class Shop{ UpperClothes cloth; Trousers trouser; public void giveSuit(ClothesFactory factory,int chestSize,int waistSize,int height){ cloth=factory.createUpperClothes(chestSize,height); trouser=factory.createTrousers(waistSize,height); showMess(); } private void showMess(){ System.out.println("<套装信息>"); System.out.println(cloth.getName()+":"); System.out.print("胸围:"+cloth.getChestSize()); System.out.println("身高:"+cloth.getHeight()); System.out.println(trouser.getName()+":"); System.out.print("腰围:"+trouser.getWaistSize()); System.out.println("身高:"+trouser.getHeight()); } } 5.应用_2: Application.java public class Application{ public static void main(String args[]){ Shop shop=new Shop(); ClothesFactory factory=new BeijingClothesFactory(); shop.giveSuit(factory,110,82,170); factory=new ShanghaiClothesFactory(); shop.giveSuit(factory,120,88,180); } } 抽象工厂模式的优点 •抽象工厂模式可以为用户创建一系列相关的对象,使得用户和创建这些对象的类脱耦。 •使用抽象工厂模式可以方便的为用户配置一系列对象。用户使用不同的具体工厂就能得到一组相关的对象,同时也能避免用户混用不同系列中的对象。 •在抽象工厂模式中,可以随时增加“具体工厂”为用户提供一组相关的对象。
工厂方法模式(别名:虚拟构造) 定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使一个类的实例化延迟到其子类。 概述 当系统准备为用户提供某个类的子类的实例,又不想让用户代码和该子类形成耦合时,就可以使用工厂方法模式来设计系统。工厂方法模式的关键是在一个接口或抽象类中定义一个抽象方法,该方法返回某个类的子类的实例,该抽象类或接口让其子类或实现该接口的类通过重写这个抽象方法返回某个子类的实例。 适用性 1.当一个类不知道它所必须创建的对象的类的时候。 2.当一个类希望由它的子类来指定它所创建的对象的时候。 3.当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类是代理者这一信息局部化的时候。 参与者 1.Product 定义工厂方法所创建的对象的接口。 2.ConcreteProduct 实现Product接口。 3.Creator 声明工厂方法,该方法返回一个Product类型的对象。 Creator也可以定义一个工厂方法的缺省实现,它返回一个缺省的ConcreteProduct对象。 可以调用工厂方法以创建一个Product对象。 4.ConcreteCreator 重定义工厂方法以返回一个ConcreteProduct实例。 工厂方法模式的结构与使用 模式的结构中包括四种角色: •抽象产品(Product) •具体产品(ConcreteProduct) •构造者(Creator) •具体构造者(ConcreteCreator) 模式的UML类图 实战部分 【例1】:假设有三个笔芯,分别是红笔芯、蓝笔芯和黑笔芯。用户希望通过圆珠笔来明确笔芯的颜色。 模式的结构的描述与使用 1.抽象产品(Product): PenCore.java public abstract class PenCore{ String color; public abstract void writeWord(String s); } 2.具体产品(ConcreteProduct)_1 : RedPenCore.java public class RedPenCore extends PenCore{ RedPenCore(){ color="红色"; } public void writeWord(String s){ System.out.println("写出"+color+"的字:"+s); } } 具体产品(ConcreteProduct)_2 : BluePenCore.java public class BluePenCore extends PenCore{ BluePenCore(){ color="蓝色"; } public void writeWord(String s){ System.out.println("写出"+color+"的字:"+s); } } 具体产品(ConcreteProduct)_3: BlackPenCore.java public class BlackPenCore extends PenCore{ BlackPenCore(){ color="黑色"; } public void writeWord(String s){ System.out.println("写出"+color+"的字:"+s); } } 3.构造者(Creator): BallPen.java public abstract class BallPen{ BallPen(){ System.out.println("生产了一只装有"+getPenCore().color+"笔芯的圆珠笔"); } public abstract PenCore getPenCore(); //工厂方法 } 4.具体构造者(ConcreteCreator): RedBallPen.java public class RedBallPen extends BallPen{ public PenCore getPenCore(){ return new RedPenCore(); } } BlueBallPen.java public class BlueBallPen extends BallPen{ public PenCore getPenCore(){ return new BluePenCore(); } } BlackBallPen.java public class BlackBallPen extends BallPen{ public PenCore getPenCore(){ return new BlackPenCore(); } } 5.应用 Application.java public class Application{ public static void main(String args[]){ PenCore penCore; BallPen ballPen=new BlueBallPen(); penCore=ballPen.getPenCore(); penCore.writeWord("你好,很高兴认识你"); ballPen=new RedBallPen(); penCore=ballPen.getPenCore(); penCore.writeWord("How are you"); ballPen=new BlackBallPen(); penCore=ballPen.getPenCore(); penCore.writeWord("nice to meet you"); } } 工厂方法模式的优点 •使用工厂方法可以让用户的代码和某个特定类的子类的代码解耦。 •工厂方法使用户不必知道它所使用的对象是怎样被创建的,只需知道该对象有哪些方法即可。
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 设计模式的起源 软件领域的设计模式起源于建筑学。 1977年,建筑大师Alexander出版了《A Pattern Language:Towns, Building, Construction》一书。受Alexander著作的影响 ,Kent Beck和Ward Cunningham在1987年举行的一次面向对象的会议上发表了论文:《在面向对象编程中使用模式》。 设计模式的分类 1.创建型模式 社会化的分工越来越细,自然在软件设计方面也是如此,因此对象的创建和对象的使 用分开也就成为了必然趋势。对象的创建会消耗掉系统的很多资源,所以单独对对象的创建进行研究,从而能够高效地创建对象就是创建型模式要探讨的问题。这里 有5个具体的创建型模式可供研究,它们分别是: 工厂方法模式(Factory Method) 抽象工厂模式(Abstract Factory) 创建者模式(Builder) 原型模式(Prototype) 单例模式(Singleton) 2.结构型模式 在解决了对象的创建问题之后,对象的组成以及对象之间的依赖关系就成了开发人员关注的焦点,如何设计对象的结构、继承和依赖关系会影响到后续程序的 维护性、代码的健壮性、耦合性等。对象结构的设计很容易体现出设计人员水平的高低,这里有7个具体的结构型模式可供研究,它们分别是: 外观模式(Facade) 适配器模式(Adapter) 代理模式(Proxy) 装饰模式(Decorator) 桥模式(Bridge) 组合模式(Composite) 享元模式(Flyweight) 3.行为型模式 在对象的结构和对象的创建问题都解决了之后,就剩下对象的行为问题了,如果对象的行为设计的好,那么对象的行为就会更清晰,它们之间的协作效率就会提高,这里有11个具体的行为型模式可供研究,它们分别是: 模板方法模式(Template Method) 观察者模式(Observer) 状态模式(State) 策略模式(Strategy) 职责链模式(Chain of Responsibility) 命令模式(Command) 访问者模式(Visitor) 调停者模式(Mediator) 备忘录模式(Memento) 迭代器模式(Iterator) 解释器模式(Interpreter) GOF的著作 目前,被公认在设计模式领域最具影响力的著作是Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides在1994年合作出版的著作:《Design Patterns:Elements of Reusable Object-Oriented Software》(中译本《设计模式:可复用的面向对象软件的基本原理》 或《设计模式》),该书被广大喜爱者昵称为GOF(Gang of Four)之书,被认为是学习设计模式的必读著作,GOF之书已经被公认为是设计模式领域的奠基之作。 面向对象的几个基本原则 1.面向抽象原则 设计一个类时,不让该类面向具体的类,而是面向抽象类或接口 。 举个例子:定义底面为三角形的柱体 2.开-闭原则 设计应当对扩展开放,对修改关闭。 如果您的设计遵守了“开-闭原则”,那么这个设计一定是易维护的,因为在设计中增加新的模块时,不必去修改设计中的核心模块。 3.高内聚-低耦合原则 如果类中的方法是一组相关的行为,则称该类是高内聚的,反之称为低内聚的。 所谓低耦合就是尽量不要让一个类含有太多的其它类的实例的引用,以避免修改系统的其中一部分会影响到其它部分。 学习设计模式不仅可以使我们使用好这些成功的模式,更重要的是可以使我们更加深刻地理解面向对象的设计思想,非常有利于我们更好地使用面向对象语言解决设计中的问题。 合理使用模式 1.正确使用; 2.避免教条; 3.模式挖掘
hadoop借鉴了Linux虚拟文件系统的概念,引入了hadoop抽象文件系统,并在此基础上,提供了大量的具体文件系统的实现,满足构建于hadoop上应用的各种数据访问需求 hadoop文件系统API hadoop提供一个抽象的文件系统,HDFS只是这个抽象文件系统的一个具体的实现。hadoop文件系统的抽象类org.apache.hadoop.fs.FileSystem hadoop抽象文件系统的方法可以分为两部分: 1、用于处理文件和目录的相关事务 2、用于读写文件数据 hadoop抽象文件系统的操作 Hadoop的FileSystem Java操作 Linux操作 描述 URL.openSteam FileSystem.open FileSystem.create FileSystem.append URL.openStream open 打开一个文件 FSDataInputStream.read InputSteam.read read 读取文件中的数据 FSDataOutputStream.write OutputSteam.write write 向文件写入数据 FSDataInputStream.close FSDataOutputStream.close InputSteam.close OutputSteam.close close 关闭一个文件 FSDataInputStream.seek RandomAccessFile.seek lseek 改变文件读写位置 FileSystem.getFileStatus FileSystem.get* File.get* stat 获取文件/目录的属性 FileSystem.set* File.set* Chmod等 改变文件的属性 FileSystem.createNewFile File.createNewFile create 创建一个文件 FileSystem.delete File.delete remove 从文件系统中删除一个文件 FileSystem.rename File.renameTo rename 更改文件/目录名 FileSystem.mkdirs File.mkdir mkdir 在给定目录下创建一个子目录 FileSystem.delete File.delete rmdir 从一个目录中删除一个空的子目录 FileSystem.listStatus File.list readdir 读取一个目录下的项目 FileSystem.getWorkingDirectory getcwd/getwd 返回当前工作目录 FileSystem.setWorkingDirectory chdir 更改当前工作目录 通过FileSystem.getFileStatus()方法,Hadoop抽象文件系统可以一次获得文件/目录的所有属性,这些属性被保存在类FileStatus中 public class FileStatus implements Writable, Comparable { private Path path; //文件路径 private long length; //文件长度 private boolean isdir; //是否是目录 private short block_replication; //副本数(为HDFS而准的特殊参数) private long blocksize; //块大小(为HDFS而准的特殊参数) private long modification_time; //最后修改时间 private long access_time; //最后访问时间 private FsPermission permission; //许可信息 private String owner; //文件所有者 private String group; //用户组 …… } FileStatus实现了Writable接口,也就是说,FileStatus可以被序列化后在网络上传输,同时一次性将文件的所有属性读出并返回到客户端,可以减少在分布式系统中进行网络传输的次数 完整的FileStatus类的源代码如下: FileStatus 出现在FileSystem中的,但在java文件API中找不到对应的方法有:setReplication()、getReplication()、getContentSummary(),其声明如下: public boolean setReplication(Path src, short replication) throws IOException { return true; } public short getReplication(Path src) throws IOException { return getFileStatus(src).getReplication(); } public ContentSummary getContentSummary(Path f) throws IOException { FileStatus status = getFileStatus(f); if (!status.isDir()) { // f is a file return new ContentSummary(status.getLen(), 1, 0); } // f is a directory long[] summary = {0, 0, 1}; for(FileStatus s : listStatus(f)) { ContentSummary c = s.isDir() ? getContentSummary(s.getPath()) : new ContentSummary(s.getLen(), 1, 0); summary[0] += c.getLength(); summary[1] += c.getFileCount(); summary[2] += c.getDirectoryCount(); } return new ContentSummary(summary[0], summary[1], summary[2]); } 实现一个Hadoop具体文件系统,需要实现的功能有哪些?下面整理org.apache.hadoop.fs.FileSystem中的抽象方法: //获取文件系统URI public abstract URI getUri(); //为读打开一个文件,并返回一个输入流 public abstract FSDataInputStream open(Path f, int bufferSize) throws IOException; //创建一个文件,并返回一个输出流 public abstract FSDataOutputStream create(Path f, FsPermission permission, boolean overwrite, int bufferSize, short replication, long blockSize, Progressable progress) throws IOException; //在一个已经存在的文件中追加数据 public abstract FSDataOutputStream append(Path f, int bufferSize, Progressable progress) throws IOException; //修改文件名或目录名 public abstract boolean rename(Path src, Path dst) throws IOException; //删除文件 public abstract boolean delete(Path f) throws IOException; public abstract boolean delete(Path f, boolean recursive) throws IOException; //如果Path是一个目录,读取一个目录下的所有项目和项目属性 //如果Path是一个文件,获取文件属性 public abstract FileStatus[] listStatus(Path f) throws IOException; //设置当前的工作目录 public abstract void setWorkingDirectory(Path new_dir); //获取当前的工作目录 public abstract Path getWorkingDirectory(); //如果Path是一个文件,获取文件属性 public abstract boolean mkdirs(Path f, FsPermission permission ) throws IOException; //获取文件或目录的属性 public abstract FileStatus getFileStatus(Path f) throws IOException; 实现一个具体的文件系统,至少需要实现上面的这些抽象方法 hadoop完整的FileSystem类的源代码如下: FileSystem Hadoop 输入/输出流 Hadoop抽象文件系统和java类似,也是使用流机制进行文件的读写,用于读文件数据流和写文件的抽象类分别是:FSDataInputStream和FSDataOutputStream 1、FSDataInputStream public class FSDataInputStream extends DataInputStream implements Seekable, PositionedReadable { …… } 可以看到,FSDataInputStream继承自DataInputStream类,实现了Seekable和PositionedReadable接口 Seekable接口提供在(文件)流中进行随机存取的方法,其功能类似于RandomAccessFile中的getFilePointer()和seek()方法,它提供了某种随机定位文件读取位置的能力 Seekable接口代码以及相关注释如下: /** 接口,用于支持在流中定位. */ public interface Seekable { /** * 将当前偏移量设置到参数位置,下次读取数据将从该位置开始 */ void seek(long pos) throws IOException; /**得到当前偏移量 */ long getPos() throws IOException; /**重新选择一个副本 */ boolean seekToNewSource(long targetPos) throws IOException; } 完整的FSDataInputStream类源代码如下: FSDataInputStream FSDataInputStream实现的另一个接口是PositionedReadable,它提供了从流中某一个位置开始读数据的一系列方法: //接口,用于在流中进行定位读 public interface PositionedReadable { //从指定位置开始,读最多指定长度的数据到buffer中offset开始的缓冲区中 //注意,该函数不改变读流的当前位置,同时,它是线程安全的 public int read(long position, byte[] buffer, int offset, int length) throws IOException; //从指定位置开始,读指定长度的数据到buffer中offset开始的缓冲区中 public void readFully(long position, byte[] buffer, int offset, int length) throws IOException; public void readFully(long position, byte[] buffer) throws IOException; } PositionedReadable中的3个读方法,都不会改变流的当前位置,而且还是线程安全的 2、FSInputStream org.apache.hadoop.fs包中还包含抽象类FSInputStream。Seekable接口和PositionedReadable中的方法都成为这个类的抽象方法 在FSInputStream类中,通过Seekable接口的seek()方法实现了PositionedReadable接口中的read()方法 //实现PositionedReadable.read()方法 public int read(long position, byte[] buffer, int offset, int length) throws IOException { /** * 由于PositionedReadable.read()是线程安全的,所以此处要借助synchronized (this) * 来保证方法被调用的时候其他方法不会被调用,也保证不会有其他线程改变Seekable.getPos()保存的 * 当前读位置 */ synchronized (this) { long oldPos = getPos(); //保存当前读的位置,调用 Seekable.getPos() int nread = -1; try { seek(position); //移动读数据的位置,调用Seekable.seek() nread = read(buffer, offset, length); //调用InputStream.read()读取数据 } finally { seek(oldPos); //调用Seekable.seek()恢复InputStream.read()前的位置 } return nread; } } 完整的FSInputStream源代码如下: FSInputStream 注意:hadoop中没有相对应的FSOutputStream类 3、FSDataOutputStream FSDataOutputStream用于写数据,和FSDataInputStream类似,继承自DataOutputStream,提供 writeInt()和writeChar()等方法,但是FSDataOutputStream更加的简单,没有实现Seekable接口,也就是说,Hadoop文件系统不支持随机写,用户不能在文件中重新定位写位置,并通过写数据来覆盖文件原有的内容。 单用户可以通过getPos()方法获得当前流的写位置,为了实现getPos()方法,FSDataOutputStream定义了内部类 PositionCache,该类继承自FilterOutputStream,并通过重载write()方法跟踪目前流的写位置. PositionCache是一个典型的过滤流,在基础的流功能上添加了getPos()方法,同时利用FileSystem.Statistics实现了文件系统读写的一些统计。 public class FSDataOutputStream extends DataOutputStream implements Syncable { private OutputStream wrappedStream; private static class PositionCache extends FilterOutputStream { private FileSystem.Statistics statistics; long position; //当前流的写位置 public PositionCache(OutputStream out, FileSystem.Statistics stats, long pos) throws IOException { super(out); statistics = stats; position = pos; } public void write(int b) throws IOException { out.write(b); position++; //跟新当前位置 if (statistics != null) { statistics.incrementBytesWritten(1); //跟新文件统计值 } } public void write(byte b[], int off, int len) throws IOException { out.write(b, off, len); position += len; // update position if (statistics != null) { statistics.incrementBytesWritten(len); } } public long getPos() throws IOException { return position; //返回当前流的写位置 } public void close() throws IOException { out.close(); } } @Deprecated public FSDataOutputStream(OutputStream out) throws IOException { this(out, null); } public FSDataOutputStream(OutputStream out, FileSystem.Statistics stats) throws IOException { this(out, stats, 0); } public FSDataOutputStream(OutputStream out, FileSystem.Statistics stats, long startPosition) throws IOException { super(new PositionCache(out, stats, startPosition)); //直接生成PositionCache对象并调用父类构造方法 wrappedStream = out; } public long getPos() throws IOException { return ((PositionCache)out).getPos(); } public void close() throws IOException { out.close(); // This invokes PositionCache.close() } // Returns the underlying output stream. This is used by unit tests. public OutputStream getWrappedStream() { return wrappedStream; } /** {@inheritDoc} */ public void sync() throws IOException { if (wrappedStream instanceof Syncable) { ((Syncable)wrappedStream).sync(); } } } FSDataOutputStream实现了Syncable接口,该接口只有一个函数sync(),其目的和Linux中系统调用sync()类似,用于将流中保存的数据同步到设备中 /** This interface declare the sync() operation. */ public interface Syncable { /** * Synchronize all buffer with the underlying devices. * @throws IOException */ public void sync() throws IOException; }
IO(Input Output)流 IO流用来处理设备之间的数据传输,对数据的操作是通过流的方式,Java用于操作流的对象都在IO包中 输入/输出流可以从以下几个方面进行分类 从流的方向划分: 输入流、输出流 从流的分工划分: 节点流、处理流 从流的内容划分: 面向字符的流、面向字节的流 字符流和字节流 字符流的由来: 因为数据编码的不同,而有了对字符进行高效操作的流对象。本质其实就是基于字节流读取时,去查了指定的码表。 字节流和字符流的区别: 读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。 处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的数据。 结论:只要是处理纯文本数据,就优先考虑使用字符流。 除此之外都使用字节流。 流按流向分为:输入流、输出流 IO流常用基类 字节流的抽象基类: •InputStream,OutputStream。 字符流的抽象基类: •Reader, Writer。 注:由这四个类派生出来的子类名称都是以其父类名作为子类名的后缀。 如:InputStream的子类FileInputStream。 如:Reader的子类FileReader。 Java流操作有关的类或接口: Java流类图结构: 读写文本文件 写文本文件 例:在C盘根目录创建文本文件Hello.txt,并往里写入若干行文本 import java.io.*; class Ex1{ public static void main ( String[] args ) throws IOException { //main方法中声明抛出IO异常 String fileName = "C:\\Hello.txt"; FileWriter writer = new FileWriter( fileName ); writer.write( "Hello!\n"); writer.write( "This is my first text file,\n" ); writer.write( "You can see how this is done.\n" ); writer.write("输入一行中文也可以\n"); writer.close(); } } 说明: 每次运行这个程序,都将删除已经存在的”Hello.txt”文件,创建一个新的同名文件。FileWriter的构造方法有五个,本例是通过一个字符串指定文件名来创建。FileWriter类的write方法向文件中写入字符 Writer类的流可实现内部格式到外部磁盘文件格式的转换 “Hello.txt”是一个普通的ASCII码文本文件,每个英文字符占一个字节,中文字符占两个字节 Java程序中的字符串则是每个字符占两个字节的,采用Unicode编码 close方法清空流里的内容并关闭它。如果不调用该方法,可能系统还没有完成所有数据的写操作,程序就结束了 在看一个例子:处理IO异常 import java.io.*; class Ex2 { public static void main ( String[] args ) { String fileName = "c:\\Hello.txt" ; try { //将所有IO操作放入try块中 FileWriter writer = new FileWriter( fileName ,true ); writer.write( "Hello!\n"); writer.write( "This is my first text file,\n" ); writer.write( "You can see how this is done. \n" ); writer.write("输入一行中文也可以\n"); writer.close(); } catch ( IOException iox) { System.out.println("Problem writing" + fileName ); } } } 说明: 运行此程序,会发现在原文件内容后面又追加了重复的内容,这就是将构造方法的第二个参数设为true的效果 如果将文件属性改为只读属性,再运行本程序,就会出现IO错误,程序将转入catch块中,给出出错信息 BufferedWriter类 如果需要写入的内容很多,就应该使用更为高效的缓冲器流类BufferedWriter FileWriter和BufferedWriter类都用于输出字符流,包含的方法几乎完全一样,但BufferedWriter多提供了一个newLine()方法用于换行 使用BufferedWriter完成上面的功能: import java.io.*; class Ex3 { public static void main ( String[] args ) throws IOException { String fileName = "C:/newHello.txt" ; BufferedWriter out = new BufferedWriter( new FileWriter( fileName ) ); out.write( "Hello!" ); out.newLine() ; out.write( "This is another text file using BufferedWriter," ); out.newLine(); ; out.write( "So I can use a common way to start a newline" ); out.close(); } } 读文本文件 FileReader类 从文本文件中读取字符 继承自Reader抽象类的子类InputStreamReader BufferedReader 读文本文件的缓冲器类 具有readLine()方法,可以对换行符进行鉴别,一行一行地读取输入流中的内容 继承自Reader 文件输入方法: BufferedReader in = new BufferedReader(new FileReader( fileName) ); 从Hello.txt中读取文本并显示在屏幕上 import java.io.*; class Ex4 { public static void main ( String[] args ) { String fileName = "C:/Hello.txt" , line; try { BufferedReader in = new BufferedReader( new FileReader( fileName ) ); line = in.readLine(); //读取一行内容 while ( line != null ) { System.out.println( line ); line = in.readLine(); } in.close(); } catch ( IOException iox ) { System.out.println("Problem reading " + fileName ); } } } 运行该程序,屏幕上将逐行显示出Hello.txt文件中的内容 FileReader对象:创建后将打开文件,如果文件不存在,会抛出一个IOException BufferedReader类的readLine()方法:从一个面向字符的输入流中读取一行文本。如果其中不再有数据,返回null Reader类的read()方法:也可用来判别文件结束。该方法返回的一个表示某个字符的int型整数,如果读到文件末尾,返回 -1。据此,可修改本例中的读文件部分: int c; while((c=in.read())!= -1) System.out.print((char)c); close()方法:为了操作系统可以更为有效地利用有限的资源,应该在读取完毕后,调用该方法 指定源文件和目标文件名,将源文件的内容拷贝至目标文件。调用方式为: java copy sourceFile destinationFile class CopyMaker { String sourceName, destName; BufferedReader source; BufferedWriter dest; String line; private boolean openFiles() { try { source = new BufferedReader(new FileReader(sourceName)); } catch (IOException ex) { System.out.println("Problem opening " + sourceName); return false; } try { dest = new BufferedWriter(new FileWriter(destName)); } catch (IOException ex) { System.out.println("Problem opening " + destName); return false; } return true; } private boolean copyFiles() { try { line = source.readLine(); while(line != null) { dest.write(line); dest.newLine(); line = source.readLine(); } } catch (IOException ex) { System.out.println("Problem reading or writing"); return false; } return true; } private boolean closeFiles() { boolean retVal = true; try { source.close(); } catch (IOException ex) { System.out.println("Prolem closing " + sourceName); retVal = false; } try { dest.close(); } catch (IOException ex) { System.out.println("Problem closing " + destName); retVal = false; } return retVal; } public boolean copy(String src, String dst) { sourceName= src; destName = dst; return openFiles() && copyFiles() && closeFiles(); } } public class CopyFile { public static void main(String[] args) { if(args.length == 2) new CopyMaker().copy(args[0], args[1]); else System.out.println("Please Enter names"); } } 读写二进制文件 二进制文件 原则上讲,所有文件都是由8位的字节组成的 如果文件字节中的内容应被解释为字符,则文件被称为文本文件;如果被解释为其它含义,则文件被称为二进制文件 例如文字处理程序,例如字处理软件Word产生的doc文件中,数据要被解释为字体、格式、图形和其他非字符信息。因此,这样的文件是二进制文件,不能用Reader流正确读取 为什么需要二进制文件? 输入输出更快 比文本文件小很多 有些数据不容易被表示为字符 抽象类OutputStream 派生类FileOutputStream 用于一般目的输出(非字符输出) 用于成组字节输出 派生类DataOutputStream 具有写各种基本数据类型的方法 将数据写到另一个输出流 它在所有的计算机平台上使用同样的数据格式 其常用的一些方法见下表 例:将三个int型数字255/0/-1写入数据文件data1.dat public class ext6_7 { public static void main(String[] args) { String fileName = "c:/data1.dat"; int value0 = 255, value1 = 0, value2 = -1; try { DataOutputStream out = new DataOutputStream( new FileOutputStream(fileName)); out.writeInt(value0); out.writeInt(value1); out.writeInt(value2); out.close(); } catch (IOException ex) { System.out.println("Problem writing " + fileName); } } } 说明: FileOutputStream类的构造方法负责打开文件“data1.dat”用于写数据 FileOutputStream类的对象与DataOutputStream对象连接,写基本类型的数据 BufferedOutputStream 写二进制文件的缓冲流类 类似于文本文件中的BufferedWriter 对于大量数据的写入,可提高效率 用法示例: DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream( fileName ) ) ); 例:向文件中写入各种数据类型的数,并统计写入的字节数 public class ex6_8 { public static void main(String[] args) throws IOException { String fileName = "c:/mixedTypes.dat"; DataOutputStream dataOut = new DataOutputStream( new BufferedOutputStream( new FileOutputStream(fileName))); dataOut.writeInt(0); System.out.println(dataOut.size() + "bytes have been written."); dataOut.writeDouble(31.2); System.out.println(dataOut.size() + "bytes have been written."); dataOut.writeBytes("java"); System.out.println(dataOut.size() + "bytes have been written."); dataOut.close(); } } 读二进制文件 过滤流 读或写的同时对数据进行处理 通过另外一个流来构造一个过滤流 大部分java.io 包所提供过滤流都是FilterInputStream和FilterOutputStream的子类: DataInputStream 和 DataOutputStream BufferedInputStream 和 BufferedOutputStream LineNumberInputStream PushbackInputStream PrintStream 读取上面的例子创建的数据文件中的3个int型数字,显示相加结果 public class ex6_10 { public static void main(String[] args) { String fileName = "C:\\data1.dat"; int sum = 0; try { DataInputStream instr = new DataInputStream( new BufferedInputStream(new FileInputStream(fileName))); sum += instr.readInt(); sum += instr.readInt(); sum += instr.readInt(); System.out.println("The sum is: " + sum); instr.close(); } catch (IOException ex) { System.out.println("Problem reading " + fileName); } } } 分析: readInt方法可以从输入流中读入4个字节并将其当作int型数据 由于知道文件中存储的是3个int型数据,所以使用了3个读入语句 如果不知道数据的个数该怎么办呢?因为DataInputStream的读入操作如遇到文件结尾就会抛出EOFException异常,所以我们可以将读操作放入try块中 将读操作放入try块中,使遇到文件结尾就会抛出EOFException异常,进入到相应的catch块中 try { while(true) sum += instr.readInt(); } catch ( EOFException eof ) { System.out.println("The sum is: " + sum); instr.close(); } File类 表示磁盘文件信息 定义了一些与平台无关的方法来操纵文件 –创建、删除文件 –重命名文件 –判断文件的读写权限及是否存在 –设置和查询文件的最近修改时间等 构造文件流可以使用File类的对象作为参数 File类常用方法: 例:在C盘创建文件Hello.txt,如果存在则删除旧文件,不存在则直接创建新的 public class ex6_13 { public static void main(String[] args) { File f = new File("C:" + File.separator + "hello.txt"); if(f.exists()) { f.delete(); } else { try { f.createNewFile(); } catch (Exception e) { System.out.println(e.getMessage()); } } } } 处理压缩文件 压缩流类 –java.util.zip包中提供了一些类,使我们可以以压缩格式对流进行读写 –它们都继承自字节流类OutputStream和InputStream –其中GZIPOutputStream和ZipOutputStream可分别把数据压缩成GZIP格式和Zip格式 –GZIPInputStream和ZipInputStream可以分别把压缩成GZIP格式或Zip的数据解压缩恢复原状 public class ex6_14 { public static void main(String[] args) throws IOException{ FileInputStream in = new FileInputStream("c:/Hello.txt"); GZIPOutputStream out = new GZIPOutputStream( //生成压缩文件test.gz new FileOutputStream("c:/test.gz")); System.out.println("Writing compressing file from" + "c:/Hello.txt to c:/test.gz"); int c; while((c = in.read()) != -1) { out.write(c); } in.close(); out.close(); System.out.println("Reading file form c:/test.gz to monitor"); BufferedReader in2 = new BufferedReader( new InputStreamReader( new GZIPInputStream( new FileInputStream("c:/test.gz")))); String s; while((s = in2.readLine()) != null) System.out.println(s); in2.close(); System.out.println("Writing decompression to c:/newHello.txt"); GZIPInputStream in3 = new GZIPInputStream( //读取test.gz中的内容 new FileInputStream("c:/test.gz")); FileOutputStream out2 = new FileOutputStream("c:/newHello.txt"); while((c = in3.read()) != -1) out2.write(c); in3.close(); out2.close(); } } Zip文件 –可能含有多个文件,所以有多个入口(Entry) –每个入口用一个ZipEntity对象表示,该对象的getName()方法返回文件的最初名称 ZipOutputStream –父类是DeflaterOutputStream –可以把数据压缩成ZIP格式 ZipInputStream –父类是InflaterInputStream –可以把压缩成ZIP格式的数据解压缩 例:指定若干文件名,将所有文件压缩为"c:/test.zip",再从此压缩文件中解压缩并显示 public class ex6_15 { public static void main(String[] args) throws IOException { ZipOutputStream out = new ZipOutputStream( new BufferedOutputStream( new FileOutputStream("c:/test.zip"))); String[] s = {"c:/t1.txt", "c:/t2.txt", "c:/t3.txt"}; //文件路径 for(int i = 0; i < s.length; i++) { System.out.println("Writing file" + s[i]); BufferedInputStream in = new BufferedInputStream( new FileInputStream(s[i])); out.putNextEntry(new ZipEntry(s[i])); int c; while((c = in.read()) != -1) out.write(c); in.close(); } out.close(); System.out.println("Reading file"); ZipInputStream in2 = new ZipInputStream( new BufferedInputStream( new FileInputStream("c:/test.zip"))); ZipEntry ze; while((ze = in2.getNextEntry()) != null) { System.out.println("Reading file " + ze.getName()); int x; while((x = in2.read()) != -1) System.out.write(x); System.out.println(); } in2.close(); } } 再看一个例子:解压缩Zip文件,并恢复其原来路径 class Unzip { byte[] doc = null; ; //存储解压缩数据的缓冲字节数组 String FileName = null; //压缩文件名字符串 String UnZipPath = null; //解压缩路径字符串 public Unzip(String filename, String unZipPath) { this.FileName = filename; this.UnZipPath = unZipPath; this.setUnZipPath(this.UnZipPath); } public Unzip(String filename) { this.FileName = new String(filename); this.UnZipPath = null; this.setUnZipPath(this.UnZipPath); } private void setUnZipPath(String unZipPath) { if(unZipPath.endsWith("\\")) this.UnZipPath = new String(unZipPath); else this.UnZipPath = new String(unZipPath + "\\"); } public void doUnZip() { try { ZipInputStream zipis = new ZipInputStream( new FileInputStream(FileName)); ZipEntry fEntry = null; while((fEntry = zipis.getNextEntry()) != null) { if(fEntry.isDirectory()) { checkFilePath(UnZipPath + fEntry.getName()); } else { //是文件则解压缩文件 String fname = new String(UnZipPath + fEntry.getName()); try { FileOutputStream out = new FileOutputStream(fname); doc = new byte[512]; int n; while((n = zipis.read(doc, 0, 512)) != -1) { out.write(doc, 0, n); } out.close(); out = null; doc = null; } catch (Exception ex) { } } } zipis.close(); //关闭输入流 } catch (IOException ioe) { System.out.println(ioe); } } private void checkFilePath(String dirName) { File dir = new File(dirName); if(!dir.exists()) dir.mkdirs(); } } public class ex6_16 { public static void main(String[] args) { String zipFile = "c:/test.zip"; String unZipPath = ""; Unzip myZip = new Unzip(zipFile, unZipPath); myZip.doUnZip(); } } 对象序列化 保存对象的信息,在需要的时候,再读取这个对象 内存中的对象在程序结束时就会被垃圾回收机制清除 用于对象信息存储和读取的输入输出流类: ObjectInputStream、ObjectOutputStream 实现对象的读写 通过ObjectOutputStream把对象写入磁盘文件 通过ObjectInputStream把对象读入程序 –不保存对象的transient和static类型的变量 –对象要想实现序列化,其所属的类必须实现Serializable接口 必须通过另一个流构造ObjectOutputStream: FileOutputStream out = new FileOutputStream("theTime"); ObjectOutputStream s = new ObjectOutputStream(out); s.writeObject("Today"); s.writeObject(new Date()); s.flush(); 必须通过另一个流构造ObjectInputStream: FileInputStream in = new FileInputStream("theTime"); ObjectInputStream s = new ObjectInputStream(in); String today = (String)s.readObject(); Date date = (Date)s.readObject(); 空接口,使类的对象可实现序列化 Serializable 接口的定义: package java.io; public interface Serializable { // there's nothing in here! }; 实现Serializable接口的语句 public class MyClass implements Serializable { ... } 使用关键字transient可以阻止对象的某些成员被自动写入文件 看一个例子: 创建一个书籍对象,并把它输出到一个文件book.dat中,然后再把该对象读出来,在屏幕上显示对象信息 class Book implements Serializable { int id; String name; String author; float price; public Book(int id, String name, String author, float price) { this.id = id; this.name = name; this.author = author; this.price = price; } } public class ex6_17 { public static void main(String[] args) throws IOException, ClassNotFoundException { Book book = new Book(100000, "java programming", "Wu", 23); ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("c:/book.dat")); oos.writeObject(book); oos.close(); System.out.println("ID is: " + book.id); System.out.println("name is: " + book.name); System.out.println("author is: " + book.author); System.out.println("price is: " + book.price); } } Externalizable 接口 –实现该接口可以控制对象的读写 –API中的说明为 public interface Externalizable extends Serializable –其中有两个方法writeExternal()和readExternal(),因此实现该接口的类必须实现这两个方法 –ObjectOutputStream的writeObject()方法只写入对象的标识,然后调用对象所属类的writeExternal() –ObjectInputStream的readObject()方法调用对象所属类的readExternal() 随机文件读写 RandomAccessFile类 –可跳转到文件的任意位置读/写数据 –可在随机文件中插入数据,而不破坏该文件的其他数据 –实现了DataInput 和 DataOutput 接口,可使用普通的读写方法 –有个位置指示器,指向当前读写处的位置。刚打开文件时,文件指示器指向文件的开头处。对文件指针显式操作的方法有: int skipBytes(int n):把文件指针向前移动指定的n个字节 void seek(long):移动文件指针到指定的位置。 long getFilePointer():得到当前的文件指针。 –在等长记录格式文件的随机读取时有很大的优势,但仅限于操作文件,不能访问其它IO设备,如网络、内存映像等 –可用来实现读和写,构造方法包括 public RandomAccessFile(File file,String mode) throws FileNotFoundException public RandomAccessFile(String name, String mode) throws FileNotFoundException –建立一个RandomAccessFile时,要指出你要执行的操作:仅从文件读,还是同时读写 new RandomAccessFile("farrago.txt", "r"); new RandomAccessFile("farrago.txt", "rw"); RandomAccessFile类常用API 例:创建一个雇员类,包括姓名、年龄。姓名不超过8个字符,年龄是int类型。每条记录固定为20个字节。使用RandomAccessFile向文件添加、修改、读取雇员信息 class Employee { char name[] = {'\u0000', '\u0000','\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000'}; int age; public Employee(String name, int age) throws Exception { if(name.toCharArray().length > 8) System.arraycopy(name.toCharArray(), 0, this.name, 0, 8); else System.arraycopy(name.toCharArray(), 0, this.name, 0, name.toCharArray().length); this.age = age; } } public class ex6_18 { String FileName; public ex6_18(String FileName) { this.FileName = FileName; } public void writeEmployee(Employee e, int n) throws Exception { RandomAccessFile ra = new RandomAccessFile(FileName, "rw"); ra.seek(n * 20); //将位置指示器移到指定位置上 for(int i = 0; i < 8; i++) ra.writeChar(e.name[i]); ra.writeInt(e.age); ra.close(); } public void readEmployee(int n) throws Exception { char buf[] = new char[8]; RandomAccessFile ra = new RandomAccessFile(FileName, "r"); ra.seek(n * 20); for(int i = 0; i < 8; i++) buf[i] = ra.readChar(); System.out.print("name: "); System.out.println(buf); System.out.println("age: " + ra.readInt()); ra.close(); } public static void main(String[] args) throws Exception { ex6_18 t = new ex6_18("c:/temp.txt"); Employee e1 = new Employee("zhangsan", 22); Employee e2 = new Employee("lisi", 20); Employee e3 = new Employee("wangwu", 25); t.writeEmployee(e1, 0); t.writeEmployee(e3, 2); System.out.println("第1个雇员的信息"); t.readEmployee(0); System.out.println("第3个雇员的信息"); t.readEmployee(2); System.out.println("第2个雇员的信息"); t.readEmployee(1); } }
一般来说,计算机处理的数据都存在一些冗余度,同时数据中间,尤其是相邻数据间存在着相关性,所以可以通过一些有别于原始编码的特殊编码方式来保存数据,使数据占用的存储空间比较小,这个过程一般叫压缩。和压缩对应的概念是解压缩,就是将被压缩的数据从特殊编码方式还原为原始数据的过程。 压缩广泛应用于海量数据处理中,对数据文件进行压缩,可以有效减少存储文件所需的空间,并加快数据在网络上或者到磁盘上的传输速度。在Hadoop中,压缩应用于文件存储、Map阶段到Reduce阶段的数据交换(需要打开相关的选项)等情景。 数据压缩的方式非常多,不同特点的数据有不同的数据压缩方式:如对声音和图像等特殊数据的压缩,就可以采用有损的压缩方法,允许压缩过程中损失一定 的信息,换取比较大的压缩比;而对音乐数据的压缩,由于数据有自己比较特殊的编码方式,因此也可以采用一些针对这些特殊编码的专用数据压缩算法。 Hadoop压缩简介 Hadoop作为一个较通用的海量数据处理平台,在使用压缩方式方面,主要考虑压缩速度和压缩文件的可分割性。 所有的压缩算法都会考虑时间和空间的权衡,更快的压缩和解压缩速度通常会耗费更多的空间(压缩比较低)。例如,通过gzip命令压缩数据时,用户可以设置不同的选项来选择速度优先或空间优先,选项–1表示优先考虑速度,选项–9表示空间最优,可以获得最大的压缩比。 需要注意的是,有些压缩算法的压缩和解压缩速度会有比较大的差别:gzip和zip是通用的压缩工具,在时间/空间处理上相对平衡,gzip2压缩比gzip和zip更有效,但速度较慢,而且bzip2的解压缩速度快于它的压缩速度。 当使用MapReduce处理压缩文件时,需要考虑压缩文件的可分割性。考 虑我们需要对保持在HDFS上的一个大小为1GB的文本文件进行处理,当前HDFS的数据块大小为64MB的情况下,该文件被存储为16块,对应的 MapReduce作业将会将该文件分为16个输入分片,提供给16个独立的Map任务进行处理。但如果该文件是一个gzip格式的压缩文件(大小不 变),这时,MapReduce作业不能够将该文件分为16个分片,因为不可能从gzip数据流中的某个点开始,进行数据解压。但是,如果该文件是一个 bzip2格式的压缩文件,那么,MapReduce作业可以通过bzip2格式压缩文件中的块,将输入划分为若干输入分片,并从块开始处开始解压缩数 据。bzip2格式压缩文件中,块与块间提供了一个48位的同步标记,因此,bzip2支持数据分割。 Hadoop支持的压缩格式: 压缩格式 Unix工具 算法 文件扩展名 支持多文件 可分割 DEFLATE 无 DEFLATE .deflate 否 否 gzip gzip DEFLATE .gz 否 否 zip zip DEFLATE .zip 是 是 bzip bzip2 bzip2 .bz2 否 是 LZO lzop LZO .lzo 否 否 为了支持多种压缩解压缩算法,Hadoop引入了编码/解码器。与Hadoop序列化框架类似,编码/解码器也是使用抽象工厂的设计模式。目前,Hadoop支持的编码/解码器如下 压缩算法及其编码/解码器: 压缩格式 对应的编码/解码器 DEFLATE org.apache.hadoop.io.compress.DefaultCodec gzip org.apache.hadoop.io.compress.GzipCodec bzip org.apache.hadoop.io.compress.BZip2Codec Snappy org.apache.hadoop.io.compress.SnappyCodec 同一个压缩方法对应的压缩、解压缩相关工具,都可以通过相应的编码/解码器获得。 Hadoop压缩API应用实例 下面将介绍使用编码/解码器的典型实例(代码在org.hadoopinternal.compress包中)。其中,compress()方法接 受一个字符串参数,用于指定编码/解码器,并用对应的压缩算法对文本文件README.txt进行压缩。字符串参数使用Java的反射机制创建对应的编码 /解码器对象,通过CompressionCodec对象,进一步使用它的createOutputStream()方法构造一个 CompressionOutputStream流,未压缩的数据通过IOUtils.copyBytes()方法,从输入文件流中复制写入 CompressionOutputStream流,最终以压缩格式写入底层的输出流中。 在本实例中,底层使用的是文件输出流FileOutputStream,它关联文件的文件名,是在原有文件名的基础上添加压缩算法相应的扩展名生 成。该扩展名可以通过CompressionCodec对象的getDefaultExtension()方法获得。相关代码如下: public static void compress(String method) throws…… { File fileIn = new File("README.txt"); //输入流 InputStream in = new FileInputStream(fileIn); Class<?> codecClass = Class.forName(method); Configuration conf = new Configuration(); //通过名称找对应的编码/解码器 CompressionCodec codec = (CompressionCodec) ReflectionUtils.newInstance(codecClass, conf); File fileOut = new File("README.txt"+codec.getDefaultExtension()); fileOut.delete(); //文件输出流 OutputStream out = new FileOutputStream(fileOut); //通过编码/解码器创建对应的输出流 CompressionOutputStream cout = codec.createOutputStream(out); //压缩 IOUtils.copyBytes(in, cout, 4096, false); in.close(); cout.close(); } 需要解压缩文件时,通常通过其扩展名来推断它对应的编码/解码器,进而用相应的解码流对数据进行解码,如扩展名为gz的文件可以使用GzipCodec阅读。 CompressionCodecFactory提供了getCodec()方法,用于将文件扩展名映射到对应的编码/解码器,如下面的例子。有了 CompressionCodec对象,就可以使用和压缩类似的过程,通过对象的createInputStream()方法获得 CompressionInputStream对象,解码数据。相关代码如下: public static void decompress(File file) throws IOException { Configuration conf = new Configuration(); CompressionCodecFactory factory = new CompressionCodecFactory(conf); //通过文件扩展名获得相应的编码/解码器 CompressionCodec codec = factory.getCodec(new Path(file.getName())); if( codec == null ) { System.out.println("Cannot find codec for file "+file); return; } File fileOut = new File(file.getName()+".txt"); //通过编码/解码器创建对应的输入流 InputStream in = codec.createInputStream( new FileInputStream(file) ); …… } Hadoop压缩框架 Hadoop通过以编码/解码器为基础的抽象工厂方法,提供了一个可扩展的框架,支持多种压缩方法。下面就来研究Hadoop压缩框架的实现。 1. 编码/解码器 前面已经提过,CompressionCodec接口实现了编码/解码器,使用的是抽象工厂的设计模式。CompressionCodec提供了一系列方法,用于创建特定压缩算法的相关设施,其类图如图所示: CompressionCodec中的方法很对称,一个压缩功能总对应着一个解压缩功能。其中,与压缩有关的方法包括: createOutputStream()用于通过底层输出流创建对应压缩算法的压缩流,重载的createOutputStream()方法可使用压缩器创建压缩流; createCompressor()方法用于创建压缩算法对应的压缩器。后续会继续介绍压缩流CompressionOutputStream和压缩器Compressor。解压缩也有对应的方法和类。 CompressionCodec中还提供了获取对应文件扩展名的方法getDefaultExtension(),如对于 org.apache.hadoop.io.compress.BZip2Codec,该方法返回字符串“.bz2”,注意字符串的第一个字符。相关代码 如下: public interface CompressionCodec { //在底层输出流out的基础上创建对应压缩算法的压缩流CompressionOutputStream对象 CompressionOutputStream createOutputStream(OutputStream out)…… //使用压缩器compressor,在底层输出流out的基础上创建对应的压缩流 CompressionOutputStream createOutputStream(OutputStream out, Compressor compressor) …… …… //创建压缩算法对应的压缩器 Compressor createCompressor(); //在底层输入流in的基础上创建对应压缩算法的解压缩流CompressionInputStream对象 CompressionInputStream createInputStream(InputStream in) …… …… //获得压缩算法对应的文件扩展名 String getDefaultExtension(); } CompressionCodecFactory是Hadoop压缩框架中的另一个类,它应用了工厂方法,使用者可以通过它提供的方法获得CompressionCodec。 注意:抽象工厂方法和工厂方法这两个设计模式有很大的区别,抽象工厂方法用于创建一 系列相关或互相依赖的对象,如CompressionCodec可以获得和某一个压缩算法相关的对象,包括压缩流和解压缩流等。而工厂方法(严格来 说,CompressionCodecFactory是参数化工厂方法),用于创建多种产品,如通过CompressionCodecFactory的 getCodec()方法,可以创建GzipCodec对象或BZip2Codec对象。 在前面的实例中已经使用过getCodec()方法,为某一个压缩文件寻找对应的CompressionCodec。为了分析该方法,需要了解CompressionCodec类中保存文件扩展名和CompressionCodec映射关系的成员变量codecs。 codecs是一个有序映射表,即它本身是一个Map,同时它对Map的键排序,下面是codecs中保存的一个可能的映射关系: { 2zb.: org.apache.hadoop.io.compress.BZip2Codec, etalfed.: org.apache.hadoop.io.compress.DeflateCodec, yppans.: org.apache.hadoop.io.compress.SnappyCodec, zg.: org.apache.hadoop.io.compress.GzipCodec } 可以看到,Map中的键是排序的。 getCodec()方法的输入是Path对象,保存着文件路径,如实例中的“README.txt.bz2”。 首先通过获取Path对象对应的文件名并逆转该字符串得到“2zb.txt.EMDAER”,然后通过有序映射SortedMap的 headMap()方法,查找最接近上述逆转字符串的有序映射的部分视图,如输入“2zb.txt.EMDAER”的查找结果subMap,只包含 “2zb.”对应的那个键–值对,如果输入是“zg.txt.EMDAER”,则subMap会包含成员变量codecs中保存的所有键–值对。然后,简 单地获取subMap最后一个元素的键,如果该键是逆转文件名的前缀,那么就找到了文件对应的编码/解码器,否则返回空。实现代码如下: public class CompressionCodecFactory { …… //该有序映射保存了逆转文件后缀(包括后缀前的“.”)到CompressionCodec的映射 //通过逆转文件后缀,我们可以找到最长匹配后缀 private SortedMap<String, CompressionCodec> codecs = null; …… public CompressionCodec getCodec(Path file) { CompressionCodec result = null; if (codecs != null) { String filefilename = file.getName(); //逆转字符串 String reversedFilename = new StringBuffer(filename).reverse().toString(); SortedMap<String, CompressionCodec> subMap = codecs.headMap(reversedFilename); if (!subMap.isEmpty()) { String potentialSuffix = subMap.lastKey(); if (reversedFilename.startsWith(potentialSuffix)) { result = codecs.get(potentialSuffix); } } } return result; } } CompressionCodecFactory.getCodec()方法的代码看似复杂,但通过灵活使用有序映射SortedMap,实现其实还是非常简单的。 2. 压缩器和解压器 压缩器(Compressor)和解压器(Decompressor)是Hadoop压缩框架中的一对重要概念。 Compressor可以插入压缩输出流的实现中,提供具体的压缩功能;相反,Decompressor提供具体的解压功能并插入 CompressionInputStream中。Compressor和Decompressor的这种设计,最初是在Java的zlib压缩程序库中 引入的,对应的实现分别是java.util.zip.Deflater和java.util.zip.Inflater。下面以Compressor为 例介绍这对组件。 Compressor的用法相对复杂,Compressor通过setInput()方法接收数据到内部缓冲区,自然可以多次调用 setInput()方法,但内部缓冲区总是会被写满。如何判断压缩器内部缓冲区是否已满呢?可以通过needsInput()的返回值,如果是 false,表明缓冲区已经满,这时必须通过compress()方法获取压缩后的数据,释放缓冲区空间。 为了提高压缩效率,并不是每次用户调用setInput()方法,压缩器就会立即工作,所以,为了通知压缩器所有数据已经写入,必须使用 finish()方法。finish()调用结束后,压缩器缓冲区中保持的已经压缩的数据,可以继续通过compress()方法获得。至于要判断压缩器 中是否还有未读取的压缩数据,则需要利用finished()方法来判断。 注意:finished()和finish()的作用不同,finish()结束数据输入的过程,而finished()返回false,表明压缩器中还有未读取的压缩数据,可以继续通过compress()方法读取。 Compressor接口源代码如下: public interface Compressor { /** * 输入要压缩的数据. * 根据#needsInput() 返回的值,判断是否执行 * 如果返回为true,表示需要更多的数据. * * @param b 输入数据 * @param off 偏移的起始位置 * @param len 长度 */ public void setInput(byte[] b, int off, int len); /** * 当输入数据缓存为空的时候返回 true,并且 * #setInput() 应该被调用用来提供输入数据. */ public boolean needsInput(); /** * 设置当前路径为压缩. * * @param b Dictionary data bytes * @param off Start offset * @param len Length */ public void setDictionary(byte[] b, int off, int len); /** * 返回输入的还未被压缩的字节数. */ public long getBytesRead(); /** * 返回输出的已经被压缩的字节数. */ public long getBytesWritten(); /** * 当调用的时候,表示当前输入缓存的内容的压缩操作应该结束 */ public void finish(); /** * 当压缩器中的最末端的输出数据到达则返回true. */ public boolean finished(); /** * 向缓存中填充压缩的数据. 返回实际的压缩的字节数. 返回0表明 needsInput()被调用 * 来判断是否需要更多的输入数据. * * @param b Buffer for the compressed data * @param off Start offset of the data * @param len Size of the buffer * @return The actual number of bytes of compressed data. */ public int compress(byte[] b, int off, int len) throws IOException; /** * 重置 compressor, 预备一些新的输入数据. */ public void reset(); /** * 关闭 compressor并丢弃为处理的输入. */ public void end(); } 使用Compressor的一个典型实例如下: public static void compressor() throws ClassNotFoundException, IOException { //读入被压缩的内容 File fileIn = new File("README.txt"); InputStream in = new FileInputStream(fileIn); int datalength=in.available(); byte[] inbuf = new byte[datalength]; in.read(inbuf, 0, datalength); in.close(); //长度受限制的输出缓冲区,用于说明finished()方法 byte[] outbuf = new byte[compressorOutputBufferSize]; Compressor compressor=new BuiltInZlibDeflater();//构造压缩器 int step=100;//一些计数器 int inputPos=0; int putcount=0; int getcount=0; int compressedlen=0; while(inputPos < datalength) { //进行多次setInput() int len=(datalength-inputPos>=step)? step:datalength-inputPos; compressor.setInput(inbuf, inputPos, len ); putcount++; while (!compressor.needsInput()) { compressedlen=compressor.compress(outbuf, 0, ……); if(compressedlen>0) { getcount++; //能读到数据 } } // end of while (!compressor.needsInput()) inputPos+=step; } compressor.finish(); while(!compressor.finished()) { //压缩器中有数据 getcount++; compressor.compress(outbuf, 0, compressorOutputBufferSize); } System.out.println("Compress "+compressor.getBytesRead() //输出信息 +" bytes into "+compressor.getBytesWritten()); System.out.println("put "+putcount+" times and get "+getcount+" times"); compressor.end();//停止 } 以上代码实现了setInput()、needsInput()、finish()、compress()和finished()的配合过程。将输 入inbuf分成几个部分,通过setInput()方法送入压缩器,而在finish()调用结束后,通过finished()循序判断压缩器是否还有 未读取的数据,并使用compress()方法获取数据。 在压缩的过程中,Compressor可以通过getBytesRead()和getBytesWritten()方法获得Compressor输 入未压缩字节的总数和输出压缩字节的总数,如实例中最后一行的输出语句。Compressor和Decompressor的类图如图所示。 Compressor.end()方法用于关闭解压缩器并放弃所有未处理的输入;reset()方法用于重置压缩器,以处理新的输入数据集合;reinit()方法更进一步允许使用Hadoop的配置系统,重置并重新配置压缩器。 3. 压缩流和解压缩流 Java最初版本的输入/输出系统是基于流的,流抽象了任何有能力产出数据的数据源,或者是有能力接收数据的接收端。一般来说,通过设计模式装饰, 可以为流添加一些额外的功能,如前面提及的序列化流ObjectInputStream和ObjectOutputStream。 压缩流(CompressionOutputStream)和解压缩流(CompressionInputStream)是Hadoop压缩框架中 的另一对重要概念,它提供了基于流的压缩解压缩能力。如图3-7所示是从java.io.InputStream和 java.io.OutputStream开始的类图。 这里只分析和压缩相关的代码,即CompressionOutputStream及其子类。 OutputStream是一个抽象类,提供了进行流输出的基本方法,它包含三个write成员函数,分别用于往流中写入一个字节、一个字节数组或一个字节数组的一部分(需要提供起始偏移量和长度)。 注意:流实现中一般需要支持的close()和flush()方法,是java.io包中的相应接口的成员函数,不是OutputStream的成员函数。 CompressionOutputStream继承自OutputStream,也是个抽象类。如前面提到的 ObjectOutputStream、CompressionOutputStream为其他流添加了附加额外的压缩功能,其他流保存在类的成员变量 out中,并在构造的时候被赋值。 CompressionOutputStream实现了OutputStream的close()方法和flush()方法,但用于输出数据的 write()方法、用于结束压缩过程并将输入写到底层流的finish()方法和重置压缩状态的resetState()方法还是抽象方法,需要 CompressionOutputStream的子类实现。相关代码如下: public abstract class CompressionOutputStream extends OutputStream { //输出压缩结果的流 protected final OutputStream out; //构造函数 protected CompressionOutputStream(OutputStream out) { this.out = out; } public void close() throws IOException { finish(); out.close(); } public void flush() throws IOException { out.flush(); } public abstract void write(byte[] b, int off, int len) throws IOException; public abstract void finish() throws IOException; public abstract void resetState() throws IOException; } CompressionOutputStream规定了压缩流的对外接口,如果已经有了一个压缩器的实现,能否提供一个通用的、使用压缩器的压缩流实现呢?答案是肯定的,CompressorStream使用压缩器实现了一个通用的压缩流,其主要代码如下: public class CompressorStream extends CompressionOutputStream { protected Compressor compressor; protected byte[] buffer; protected boolean closed = false; //构造函数 public CompressorStream(OutputStream out, Compressor compressor, int bufferSize) { super(out); ……//参数检查,略 this.compressor = compressor; buffer = new byte[bufferSize]; } …… public void write(byte[] b, int off, int len) throws IOException { //参数检查,略 …… compressor.setInput(b, off, len); while (!compressor.needsInput()) { compress(); } } protected void compress() throws IOException { int len = compressor.compress(buffer, 0, buffer.length); if (len > 0) { out.write(buffer, 0, len); } } //结束输入 public void finish() throws IOException { if (!compressor.finished()) { compressor.finish(); while (!compressor.finished()) { compress(); } } } …… //关闭流 public void close() throws IOException { if (!closed) { finish();//结束压缩 out.close();//关闭底层流 closed = true; } } …… } CompressorStream提供了几个不同的构造函数,用于初始化相关的成员变量。上述代码片段中保留了参数最多的构造函数,其 中,CompressorStream需要的底层输出流out和压缩时使用的压缩器,都作为参数传入构造函数。另一个参数是 CompressorStream工作时使用的缓冲区buffer的大小,构造时会利用这个参数分配该缓冲区。 CompressorStream.write()方法用于将待压缩的数据写入流中。待压缩的数据在进行一番检查后,最终调用压缩器的 setInput()方法进入压缩器。setInput()方法调用结束后,通过Compressor.needsInput()判断是否需要调用 compress()方法,获取压缩后的输出数据。上一节已经讨论了这个问题,如果内部缓冲区已满,则需要通过compress()方法提取数据,提取后 的数据直接通过底层流的write()方法输出。 当finish()被调用(往往是CompressorStream被关闭),这时CompressorStream流调用压缩器的finish()方法通知输入已经结束,然后进入另一个循环,该循环不断读取压缩器中未读取的数据,然后输出到底层流out中。 CompressorStream中的其他方法,如resetState()和close()都比较简单,不再一一介绍了。 CompressorStream利用压缩器Compressor实现了一个通用的压缩流,在Hadoop中引入一个新的压缩算法,如果没有特殊的 考虑,一般只需要实现相关的压缩器和解压器,然后通过CompressorStream和DecompressorStream,就实现相关压缩算法的输 入/输出流了。 CompressorStream的实现并不复杂,只需要注意压缩器几个方法间的配合,下图给出了这些方法的一个典型调用顺序: 4. Java本地方法 数据压缩往往是计算密集型的操作,考虑到性能,建议使用本地库(Native Library)来压缩和解压。在某个测试中,与Java实现的内置gzip压缩相比,使用本地gzip压缩库可以将解压时间减少50%,而压缩时间大概减少10%。 Hadoop的DEFLATE、gzip和Snappy都支持算法的本地实现,其中Apache发行版中还包含了DEFLATE和gzip的32位 和64位Linux本地压缩库(Cloudera发行版还包括Snappy压缩方法)。默认情况下,Hadoop会在它运行的平台上查找本地库。 假设有一个C 函数,它实现了某些功能,同时因为某种原因(如效率),使得用户不希望用Java语言重新实现该功能,那么Java本地方法(Native Method)就是一个不错的选择。Java提供了一些钩子函数,使得调用本地方法成为可能,同时,JDK也提供了一些工具,协助用户减轻编程负担。 Java语言中的关键字native用于表示某个方法为本地方法,显然,本地方法是类的成员方法。下 面是一个本地方法的例子,代码片段来自Cloudera的Snappy压缩实现,在 org.apache.hadoop.io.compress.snappy包中。其中,静态方法initIDs()和方法 compressBytesDirect()用关键字native修饰,表明这是一个Java本地方法。相关代码如下: public class SnappyCompressor implements Compressor { …… private native static void initIDs(); private native int compressBytesDirect(); }
Hadoop将很多Writable类归入org.apache.hadoop.io包中,在这些类中,比较重要的有Java基本类、Text、Writable集合、ObjectWritable等,重点介绍Java基本类和ObjectWritable的实现。 1. Java基本类型的Writable封装 目前Java基本类型对应的Writable封装如下表所示。所有这些Writable类都继承自WritableComparable。也就是说,它们是可比较的。同时,它们都有get()和set()方法,用于获得和设置封装的值。 Java基本类型对应的Writable封装 Java基本类型 Writable 序列化后长度 布尔型(boolean) BooleanWritable 1 字节型(byte) ByteWritable 1 整型(int) IntWritable VIntWritable 4 1~5 浮点型(float) FloatWritable 4 长整型(long) LongWritable VLongWritable 8 1~9 双精度浮点型(double) DoubleWritable 8 在表中,对整型(int和long)进行编码的时候,有固定长度格式(IntWritable和LongWritable)和可变长度格式(VIntWritable和VLongWritable)两种选择。固定长度格式的整型,序列化后的数据是定长的,而可变长度格式则使用一种比较灵活的编码方式,对于数值比较小的整型,它们往往比较节省空间。同时,由于VIntWritable和VLongWritable的编码规则是一样的,所以VIntWritable的输出可以用VLongWritable读入。下面以VIntWritable为例,说明Writable的Java基本类封装实现。代码如下: public class VIntWritable implements WritableComparable { private int value; …… // 设置VIntWritable的值 public void set(int value) { this.value = value; } // 获取VIntWritable的值 public int get() { return value; } public void readFields(DataInput in) throws IOException { value = WritableUtils.readVInt(in); } public void write(DataOutput out) throws IOException { WritableUtils.writeVInt(out, value); } …… } 首先,每个Java基本类型的Writable封装,其类的内部都包含一个对应基本类型的成员变量value,get()和set()方法就是用来 对该变量进行取值/赋值操作的。而Writable接口要求的readFields()和write()方法,VIntWritable则是通过调用 Writable工具类中提供的readVInt()和writeVInt()读/写数据。方法readVInt()和writeVInt()的实现也只 是简单调用了readVLong()和writeVLong(),所以,通过writeVInt()写的数据自然可以通过readVLong()读入。 writeVLong ()方法实现了对整型数值的变长编码,它的编码规则如下: 如果输入的整数大于或等于–112同时小于或等于127,那么编码需要1字节;否则,序列化结果的第一个字节,保存了输入整数的符号和后续编码的字节数。符号和后续字节数依据下面的编码规则(又一个规则): 如果是正数,则编码值范围落在–113和–120间(闭区间),后续字节数可以通过–(v+112)计算。 如果是负数,则编码值范围落在–121和–128间(闭区间),后续字节数可以通过–(v+120)计算。 后续编码将高位在前,写入输入的整数(除去前面全0字节)。代码如下: public final class WritableUtils { public stati cvoid writeVInt(DataOutput stream, int i) throws IOException { writeVLong(stream, i); } /** * @param stream保存系列化结果输出流 * @param i 被序列化的整数 * @throws java.io.IOException */ public static void writeVLong(DataOutput stream, long i) throws…… { //处于[-112, 127]的整数 if (i >= -112 && i <= 127) { stream.writeByte((byte)i); return; } //计算情况2的第一个字节 int len = -112; if (i < 0) { i ^= -1L; len = -120; } long tmp = i; while (tmp != 0) { tmp = tmp >> 8; len--; } stream.writeByte((byte)len); len = (len < -120) ? -(len + 120) : -(len + 112); //输出后续字节 for (int idx = len; idx != 0; idx--) { int shiftbits = (idx - 1) * 8; long mask = 0xFFL << shiftbits; stream.writeByte((byte)((i & mask) >> shiftbits)); } } } 2. ObjectWritable类的实现 针对Java基本类型、字符串、枚举、Writable、空值、Writable的其他子类,ObjectWritable提供了一个封装,适用于字段需要使用多种类型。ObjectWritable可应用于Hadoop远程过程调用中参数的序列化和反序列化;ObjectWritable的另一个典型应用是在需要序列化不同类型的对象到某一个字段,如在一个SequenceFile的值中保存不同类型的对象(如LongWritable值或Text值)时,可以将该值声明为ObjectWritable。 ObjectWritable的实现比较冗长,需要根据可能被封装在ObjectWritable中的各种对象进行不同的处理。 ObjectWritable有三个成员变量,包括被封装的对象实例instance、该对象运行时类的Class对象和Configuration对 象。 ObjectWritable的write方法调用的是静态方法ObjectWritable.writeObject(),该方法可以往DataOutput接口中写入各种Java对象。 writeObject()方法先输出对象的类名(通过对象对应的Class 对象的getName()方法获得),然后根据传入对象的类型,分情况序列化对象到输出流中,也就是说,对象通过该方法输出对象的类名,对象序列化结果对 到输出流中。在ObjectWritable.writeObject()的逻辑中,需要分别处理null、Java数组、字符串String、Java 基本类型、枚举和Writable的子类6种情况,由于类的继承,处理Writable时,序列化的结果包含对象类名,对象实际类名和对象序列化结果三部 分。 为什么需要对象实际类名呢?根据Java的单根继承规则,ObjectWritable中传入的declaredClass,可以是传入instance对象对应的类的类对象,也可以是instance对象的父类的类对象。但是,在序列化和反序列化的时候,往往不能使用父类的序列化方法(如write方法)来序列化子类对象,所以,在序列化结果中必须记住对象实际类名。相关代码如下: public class ObjectWritable implements Writable, Configurable { private Class declaredClass;//保存于ObjectWritable的对象对应的类对象 private Object instance;//被保留的对象 private Configuration conf; public ObjectWritable() {} public ObjectWritable(Object instance) { set(instance); } public ObjectWritable(Class declaredClass, Object instance) { this.declaredClass = declaredClass; this.instance = instance; } …… public void readFields(DataInput in) throws IOException { readObject(in, this, this.conf); } public void write(DataOutput out) throws IOException { writeObject(out, instance, declaredClass, conf); } …… public static void writeObject(DataOutput out, Object instance, Class declaredClass,Configuration conf) throws……{ if (instance == null) {//空 instance = new NullInstance(declaredClass, conf); declaredClass = Writable.class; } // 写出declaredClass的规范名 UTF8.writeString(out, declaredClass.getName()); if (declaredClass.isArray()) {//数组 …… } else if (declaredClass == String.class) {//字符串 …… } else if (declaredClass.isPrimitive()) {//基本类型 if (declaredClass == Boolean.TYPE) { //boolean out.writeBoolean(((Boolean)instance).booleanValue()); } else if (declaredClass == Character.TYPE) { //char …… } } else if (declaredClass.isEnum()) {//枚举类型 …… } else if (Writable.class.isAssignableFrom(declaredClass)) { //Writable的子类 UTF8.writeString(out, instance.getClass().getName()); ((Writable)instance).write(out); } else { …… } public static Object readObject(DataInput in, ObjectWritable objectWritable, Configuration conf){ …… Class instanceClass = null; …… Writable writable = WritableFactories.newInstance(instanceClass, conf); writable.readFields(in); instance = writable; …… } } 和输出对应,ObjectWritable的readFields()方法调用的是静态方法 ObjectWritable.readObject(),该方法的实现和writeObject()类似,唯一值得研究的是Writable对象处理部 分,readObject()方法依赖于WritableFactories类。WritableFactories类允许非公有的Writable子类 定义一个对象工厂,由该工厂创建Writable对象,如在上面的readObject()代码中,通过WritableFactories的静态方法 newInstance(),可以创建类型为instanceClass的Writable子对象。相关代码如下: public class WritableFactories { //保存了类型和WritableFactory工厂的对应关系 private static final HashMap<Class, WritableFactory>CLASS_TO_FACTORY = new HashMap<Class, WritableFactory>(); …… public static Writable newInstance(Class<? extends Writable> c, Configuration conf) { WritableFactory factory = WritableFactories.getFactory(c); if (factory != null) { Writable result = factory.newInstance(); if (result instanceof Configurable) { ((Configurable) result).setConf(conf); } return result; } else { //采用传统的反射工具ReflectionUtils,创建对象 return ReflectionUtils.newInstance(c, conf); } } } WritableFactories.newInstance()方法根据输入的类型查找对应的WritableFactory工厂对象,然后调用 该对象的newInstance()创建对象,如果该对象是可配置的,newInstance()还会通过对象的setConf()方法配置对象。 WritableFactories提供注册机制,使得这些Writable子类可以将该工厂登记到WritableFactories的静态成员 变量CLASS_TO_FACTORY中。下面是一个典型的WritableFactory工厂实现,来自于HDFS的数据块Block。其 中,WritableFactories.setFactory()需要两个参数,分别是注册类对应的类对象和能够构造注册类的 WritableFactory接口的实现,在下面的代码里,WritableFactory的实现是一个匿名类,其newInstance()方法会创 建一个新的Block对象。 public class Block implements Writable, Comparable<Block> { static { WritableFactories.setFactory (Block.class,//类对象 new WritableFactory() {//对应类的WritableFactory实现 public Writable newInstance() { return new Block(); } }); } …… } ObjectWritable作为一种通用机制,相当浪费资源,它需要为每一个输出写入封装类型的名字。如果类型的数量不是很多,而且可以事先知 道,则可以使用一个静态类型数组来提高效率,并使用数组索引作为类型的序列化引用。GenericWritable就是因为这个目的被引入 org.apache.hadoop.io包中。
线程的生命周期 1、线程的生命周期 线程从产生到消亡的过程 一个线程在任何时刻都处于某种线程状态(thread state) 线程生命周期状态图 诞生状态 线程刚刚被创建 就绪状态 线程的 start 方法已被执行 线程已准备好运行 运行状态 处理机分配给了线程,线程正在运行 阻塞状态(Blocked) 在线程发出输入/输出请求且必须等待其返回 遇到用synchronized标记的方法而未获得其监视器暂时不能进入执行时 休眠状态(Sleeping) 执行sleep方法而进入休眠 死亡状态 线程已完成或退出 2、死锁问题 死锁 线程在运行过程中,其中某个步骤往往需要满足一些条件才能继续进行下去,如果这个条件不能满足,线程将在这个步骤上出现阻塞 线程A可能会陷于对线程B的等待,而线程B同样陷于对线程C的等待,依次类推,整个等待链最后又可能回到线程A。如此一来便陷入一个彼此等待的轮回中,任何线程都动弹不得,此即所谓死锁(deadlock) 对于死锁问题,关键不在于出现问题后调试,而是在于预防 设想一个游戏,规则为3个人站在三角形的三个顶点的位置上,三个边上放着三个球,如图所示。每个人都必须先拿到自己左手边的球,才能再拿到右手边的球,两手都有球之后,才能够把两个球都放下 创建3个线程模拟3个游戏者的行为。 class Balls { boolean flag0 = false; //0号球的标志变量,true表示已被人拿,false表示未被任何人拿 boolean flag1 = false; //1号球的标志变量 boolean flag2 = false; //2号球的标志变量 } class Player0 extends Thread { //0号游戏者的类 private Balls ball; public Player0(Balls b) { this.ball = b; } public void run() { while(true) { while(ball.flag1 == true){} //如果1号球已被拿走,则等待 ball.flag1 = true; //拿起1号球 while(ball.flag0 == true){} //如果0号球已被拿走,则等待 if(ball.flag1 == true && ball.flag0 == false) { ball.flag0 = true; //拿起0号球 System.out.println("Player0 has got two balls!"); ball.flag1 = false; //放下1号球 ball.flag0 = false; //放下0号球 try { sleep(1); //放下后休息1ms } catch (Exception e) { } } } } } class Player1 extends Thread { //1号游戏者的类 private Balls ball; public Player1(Balls b) { this.ball = b; } public void run() { while(true) { while(ball.flag0 == true){} ball.flag0 = true; while(ball.flag1 == true){} if(ball.flag0 == true && ball.flag1 == false) { ball.flag1 = true; System.out.println("Player0 has got two balls!"); ball.flag0 = false; ball.flag1 = false; try { sleep(1); } catch (Exception e) { } } } } } class Player2 extends Thread { //2号游戏者的类 private Balls ball; public Player2(Balls b) { this.ball = b; } public void run() { while(true) { while(ball.flag2 == true){} ball.flag2 = true; while(ball.flag1 == true){} if(ball.flag2 == true && ball.flag1 == false) { ball.flag1 = true; System.out.println("Player0 has got two balls!"); ball.flag2 = false; ball.flag1 = false; try { sleep(1); } catch (Exception e) { } } } } } public class playball { public static void main(String[] args) { Balls ball = new Balls(); //创建一个球类对象 Player0 p0 = new Player0(ball); //创建0号游戏者 Player1 p1 = new Player1(ball); //创建1号游戏者 Player2 p2 = new Player2(ball); //创建2号游戏者 p0.start(); //启动0号游戏者 p1.start(); //启动1号游戏者 p2.start(); //启动2号游戏者 } } 运行结果: 若干次后将陷入死锁,不再有输出信息,即任何人都不能再同时拥有两侧的球 程序说明: 如果刚好3个人都拿到了左手边的球,都等待那右手边的球,则因为谁都不能放手,则这3个线程都将陷入无止尽的等待当中,这就构成了死锁 为了便于观察死锁发生的条件,我们在每个游戏者放下两边的球后增加了sleep语句 为了避免死锁,需要修改游戏规则,使每个人都只能先抢到两侧中号比较小的球,才能拿另一只球,这样就不会再出现死锁现象 3、控制线程的生命 结束线程的生命 用stop方法可以结束线程的生命 但如果一个线程正在操作共享数据段,操作过程没有完成就用stop结束的话,将会导致数据的不完整,因此并不提倡使用此方法 通常,可通过控制run方法中循环条件的方式来结束一个线程 线程不断显示递增整数,按下回车键则停止执行: class TestThread1 extends Thread { private boolean flag = true; public void stopme() { //在此方法中控制循环条件 flag = false; } public void run() { int i = 0; while(flag) { System.out.println(i++); //如果flag为真则一直显示递增整数 } } } public class ext8_12 { public static void main(String[] args) throws IOException{ TestThread1 t = new TestThread1(); t.start(); new BufferedReader(new InputStreamReader(System.in)).readLine(); //等待键盘输入 t.stopme(); //调用stopme方法结束t线程 } } 运行效果为按下回车键后则停止显示 线程的优先级 线程调度 在单CPU的系统中,多个线程需要共享CPU,在任何时间点上实际只能有一个线程在运行 控制多个线程在同一个CPU上以某种顺序运行称为线程调度 Java虚拟机支持一种非常简单的、确定的调度算法,叫做固定优先级算法。这个算法基于线程的优先级对其进行调度 线程的优先级 每个Java线程都有一个优先级,其范围都在1和10之间。默认情况下,每个线程的优先级都设置为5 在线程A运行过程中创建的新的线程对象B,初始状态具有和线程A相同的优先级 如果A是个后台线程,则B也是个后台线程 可在线程创建之后的任何时候,通过setPriority(int priority)方法改变其原来的优先级 基于线程优先级的线程调度 具有较高优先级的线程比优先级较低的线程优先执行 对具有相同优先级的线程,Java的处理是随机的 底层操作系统支持的优先级可能要少于10个,这样会造成一些混乱。因此,只能将优先级作为一种很粗略的工具使用。最后的控制可以通过明智地使用yield()函数来完成 我们只能基于效率的考虑来使用线程优先级,而不能依靠线程优先级来保证算法的正确性 假设某线程正在运行,则只有出现以下情况之一,才会使其暂停运行 一个具有更高优先级的线程变为就绪状态(Ready); 由于输入/输出(或其他一些原因)、调用sleep、wait、yield方法使其发生阻塞; 对于支持时间分片的系统,时间片的时间期满 例子:创建两个具有不同优先级的线程,都从1递增到400000,每增加50000显示一次 class TestThread2 extends Thread { private int tick = 1; private int num; public TestThread2(int i) { this.num = i; } public void run() { while(tick < 400000) { tick++; if((tick % 50000) == 0) { //每隔50000进行显示 System.out.println("Thread#" + num +",tick=" + tick); yield(); //放弃执行权 } } } } public class ext8_13 { public static void main(String[] args) { TestThread2[] runners = new TestThread2[2]; for(int i = 0; i < 2; i++) runners[i] = new TestThread2(i); runners[0].setPriority(2); runners[1].setPriority(3); for(int i = 0; i < 2; i++) runners[i].start(); } } 运行结果如下: Thread #1, tick = 50000 Thread #1, tick = 100000 Thread #1, tick = 150000 Thread #1, tick = 200000 Thread #1, tick = 250000 Thread #1, tick = 300000 Thread #1, tick = 350000 Thread #1, tick = 400000 Thread #0, tick = 50000 Thread #0, tick = 100000 Thread #0, tick = 150000 Thread #0, tick = 200000 Thread #0, tick = 250000 Thread #0, tick = 300000 Thread #0, tick = 350000 Thread #0, tick = 400000 结果说明: 具有较高优先级的线程1一直运行到结束,具有较低优先级的线程0才开始运行 虽然具有较高优先级的线程1调用了yield方法放弃CPU资源,允许线程0进行争夺,但马上又被线程1抢夺了回去,所以有没有yield方法都没什么区别 如果在yield方法后增加一行sleep语句,让线程1暂时放弃一下在CPU上的运行,哪怕是1毫秒,则线程0也可以有机会被调度。修改后的run方法如下: public void run() { while(tick < 400000) { tick++; if((tick % 50000) == 0) { //每隔50000进行显示 System.out.println("Thread#" + num +",tick=" + tick); yield(); //放弃执行权 try { sleep(1); } catch(Exception e) { } } } } 运行结果如下: Thread #1, tick = 50000 Thread #1, tick = 100000 Thread #1, tick = 150000 Thread #1, tick = 200000 Thread #0, tick = 50000 Thread #1, tick = 250000 Thread #1, tick = 300000 Thread #0, tick = 100000 Thread #1, tick = 350000 Thread #1, tick = 400000 Thread #0, tick = 150000 Thread #0, tick = 200000 Thread #0, tick = 250000 Thread #0, tick = 300000 Thread #0, tick = 350000 Thread #0, tick = 400000 说明 具有较低优先权的线程0在线程1没有执行完毕前也获得了一部分执行,但线程1还是优先完成了执行 Java虚拟机本身并不支持某个线程抢夺另一个正在执行的具有同等优先级线程的执行权 通常,我们在一个线程内部插入yield()语句,这个方法会使正在运行的线程暂时放弃执行,这是具有同样优先级的线程就有机会获得调度开始运行,但较低优先级的线程仍将被忽略不参加调度
多线程编程基础 多进程 一个独立程序的每一次运行称为一个进程,例如:用字处理软件编辑文稿时,同时打开mp3播放程序听音乐,这两个独立的程序在同时运行,称为两个进程 进程要占用相当一部分处理器时间和内存资源 进程具有独立的内存空间 通信很不方便,编程模型比较复杂 多线程 一个程序中多段代码同时并发执行,称为多线程,线程比进程开销小,协作和数据交换容易 Java是第一个支持内置线程操作的主流编程语言,多数程序设计语言支持多线程要借助于操作系统“原语(primitives)” Thread类 直接继承了Object类,并实现了Runnable接口。位于java.lang包中封装了线程对象需要的属性和方法 继承Thread类——创建多线程的方法之一,类派生一个子类,并创建子类的对象,子类应该重写Thread类的run方法,写入需要在新线程中执行的语句段。调用start方法来启动新线程,自动进入run方法。 (实例1)在新线程中完成计算某个整数的阶乘 class FactorialThread extends Thread { private int num; public FactorialThread(int num) { this.num = num; } public void run() { int i = num; int result = 1; System.out.println("new thread started"); while(i > 0) { result = result * i; i--; } System.out.println("The factorial of " + num + " is " + result); System.out.println("new thread ends"); } } public class javatest { public static void main(String args[]) { System.out.println("main thread start"); FactorialThread thread = new FactorialThread(10); thread.start(); System.out.println("main thread ends"); } } 运行结果: main thread startmain thread endsnew thread startedThe factorial of 10 is 3628800new thread ends 结果说明: main线程已经执行完后,新线程才执行完。main函数调用thread.start()方法启动新线程后并不等待其run方法返回就继续运行,thread.run函数在一边独自运行,不影响原来的main函数的运行 如果启动新线程后希望主线程多持续一会再结束,可在start语句后加上让当前线程(这里当然是main)休息1毫秒的语句: try { Thread.sleep(1); } catch(Exception e){}; 常用API方法: 名称 说明 public Thread() 构造一个新的线程对象,默认名为Thread-n,n是从0开始递增的整数 public Thread(Runnable target) 构造一个新的线程对象,以一个实现Runnable接口的类的对象为参数。默认名为Thread-n,n是从0开始递增的整数 public Thread(String name) 构造一个新的线程对象,并同时指定线程名 public static Thread currentThread() 返回当前正在运行的线程对象 public static void yield() 使当前线程对象暂停,允许别的线程开始运行 public static void sleep(long millis) 使当前线程暂停运行指定毫秒数,但此线程并不失去已获得的锁旗标。 public void start() 启动线程,JVM将调用此线程的run方法,结果是将同时运行两个线程,当前线程和执行run方法的线程 public void run() Thread的子类应该重写此方法,内容应为该线程应执行的任务。 public final void stop() 停止线程运行,释放该线程占用的对象锁旗标。 public void interrupt() 中断此线程 public final void join() 如果此前启动了线程A,调用join方法将等待线程A死亡才能继续执行当前线程 public final void join(long millis) 如果此前启动了线程A,调用join方法将等待指定毫秒数或线程A死亡才能继续执行当前线程 public final void setPriority(int newPriority) 设置线程优先级 public final void setDaemon(Boolean on) 设置是否为后台线程,如果当前运行线程均为后台线程则JVM停止运行。这个方法必须在start()方法前使用 public void setName(String name) 更改本线程的名称为指定参数 public final boolean isAlive() 测试线程是否处于活动状态,如果线程被启动并且没有死亡则返回true public final void checkAccess() 判断当前线程是否有权力修改调用此方法的线程 (实例2):创建3个新线程,每个线程睡眠一段时间(0~6秒),然后结束。 class TestThread extends Thread { private int sleeptime; public TestThread(String name) { super(name); sleeptime = (int)(Math.random() * 6000); } public void run() { try { System.out.println(getName() + "going to sleep for " + sleeptime); Thread.sleep(sleeptime); } catch (InterruptedException ex) { } System.out.println(getName() + " finished"); } } public class javatest { public static void main(String args[]) { TestThread thr1 = new TestThread("thread1"); TestThread thr2 = new TestThread("thread2"); TestThread thr3 = new TestThread("thread3"); System.out.println("staring threads"); thr1.start(); thr2.start(); thr3.start(); System.out.println("Thread started, main ends"); } } 运行结果: staring threadsthread1going to sleep for 2925Thread started, main endsthread3going to sleep for 1222thread2going to sleep for 5007thread3 finishedthread1 finishedthread2 finished Runnable接口 Thread类实现了Runnable接口,只有一个run()方法,更便于多个线程共享资源。Java不支持多继承,如果 已经继承了某个基类,便需要实现Runnable接口来生成多线程以实现runnable的对象为参数建立新的线程,start方法启动线程就会运行 run()方法 (实例3)使用Runnable接口实现实例1的功能: class FactorialThread implements Runnable { private int num; public FactorialThread(int num) { this.num = num; } public void run() { int i = num; int result = 1; while(i > 0) { result = result * i; i--; } System.out.println("The factorial of " + num + " is " + result); System.out.println("new thread ends"); } } public class javatest { public static void main(String args[]) { System.out.println("main thread starts"); FactorialThread t = new FactorialThread(10); new Thread(t).start(); System.out.println("new thread started main thread ends"); } } (实例4)使用Runnable接口实现实例2的功能: class TestThread implements Runnable { private int sleepTime; public TestThread() { sleepTime = (int)(Math.random() * 6000); } public void run() { try { System.out.println(Thread.currentThread().getName() + " going to sleep for " + sleepTime); Thread.sleep(sleepTime); } catch(InterruptedException ex) { }; System.out.println(Thread.currentThread().getName() + " finished"); } } public class javatest { public static void main(String args[]) { TestThread thread1 = new TestThread(); TestThread thread2 = new TestThread(); TestThread thread3 = new TestThread(); System.out.println("Starting threads"); new Thread(thread1, "Thread1").start(); new Thread(thread2, "Thread2").start(); new Thread(thread3, "Thread3").start(); System.out.println("Threads started, main ends"); } } 线程间的数据共享 用同一个实现了Runnable接口的对象作为参数创建多个线程 多个线程共享同一对象中的相同的数据 修改实例4,只用一个Runnable类型的对象为参数创建3个新线程: class TestThread implements Runnable { private int sleepTime; public TestThread() { sleepTime = (int)(Math.random() * 6000); } public void run() { try { System.out.println(Thread.currentThread().getName() + " going to sleep for " + sleepTime); Thread.sleep(sleepTime); } catch(InterruptedException ex) { }; System.out.println(Thread.currentThread().getName() + " finished"); } } public class javatest { public static void main(String args[]) { TestThread threadobj = new TestThread(); System.out.println("Starting threads"); new Thread(threadobj, "Thread1").start(); new Thread(threadobj, "Thread2").start(); new Thread(threadobj, "Thread3").start(); System.out.println("Threads started, main ends"); } } 说明:因为是用一个Runnable类型对象创建的3个新线程,这三个线程就共享了这个对象的私有成员sleepTime,在本次运行中,三个线程都休眠了966毫秒 独立的同时运行的线程有时需要共享一些数据并且考虑到彼此的状态和动作 举个例子:用三个线程模拟三个售票口,总共出售200张票 class SellTicks implements Runnable { private int tickets = 200; public void run() { while(tickets > 0) { System.out.println(Thread.currentThread().getName() + " is selling ticket " + tickets--); } } } public class javatest { public static void main(String args[]) { SellTicks t = new SellTicks(); new Thread(t).start(); new Thread(t).start(); new Thread(t).start(); } } 说明: 在这个例子中,创建了3个线程,每个线程调用的是同一个SellTickets对象中的run()方法,访问的是同一个对象中的变量(tickets) 如果是通过创建Thread类的子类来模拟售票过程,再创建3个新线程,则每个线程都会有各自的方法和变量,虽然方法是相同的,但变量却是各有200张票,因而结果将会是各卖出200张票,和原意就不符了 多线程的同步控制 有时线程之间彼此不独立、需要同步 线程间的互斥 同时运行的几个线程需要共享一个(些)数据 共享的数据,在某一时刻只允许一个线程对其进行操作 “生产者/消费者” 问题 假设有一个线程负责往数据区写数据,另一个线程从同一数据区中读数据,两个线程可以并行执行,如果数据区已满,生产者要等消费者取走一些数据后才能再写。当数据区空时,消费者要等生产者写入一些数据后再取 举个例子: 用两个线程模拟存票、售票过程 假定开始售票处并没有票,一个线程往里存票,另外一个线程则往出卖票,我们新建一个票类对象,让存票和售票线程都访问它。本例采用两个线程共享同一个数据对象来实现对同一份数据的操作 class Tickets { int number = 0; //票号 int size; //总票数 boolean available = false; //表示目前是否有票可售 public Tickets(int size) { //构造函数,传入总票参数 this.size = size; } } class Producer extends Thread { Tickets t = null; public Producer(Tickets t) { this.t = t; } public void run() { while(t.number < t.size) { System.out.println("Producer puts ticket" + (++t.number)); t.available = true; } } } class Consumer extends Thread { Tickets t = null; int i = 0; public Consumer(Tickets t) { this.t = t; } public void run() { while(i < t.size) { if(t.available == true && i <= t.number) System.out.println("Consumer buys ticket" + (++i)); if(i == t.number) t.available = false; } } } public class TicketSell { public static void main(String[] args) { Tickets t = new Tickets(10); new Consumer(t).start(); new Producer(t).start(); } } 运行结果: Producer puts ticket1Producer puts ticket2Consumer buys ticket1Producer puts ticket3Consumer buys ticket2Producer puts ticket4Producer puts ticket5Producer puts ticket6Producer puts ticket7Producer puts ticket8Producer puts ticket9Producer puts ticket10Consumer buys ticket3Consumer buys ticket4Consumer buys ticket5Consumer buys ticket6Consumer buys ticket7Consumer buys ticket8Consumer buys ticket9Consumer buys ticket10 通过让两个线程操纵同一个票类对象,实现了数据共享的目的, 线程同步(Synchronization) 互斥:许多线程在同一个共享数据上操作而互不干扰,同一时刻只能有一个线程访问该共享数据。因此有些方法或程序段在同一时刻只能被一个线程执行,称之为监视区 协作:多个线程可以有条件地同时操作共享数据。执行监视区代码的线程在条件满足的情况下可以允许其它线程进入监视区 synchronized ——线程同步关键字 用于指定需要同步的代码段或方法,也就是监视区 可实现与一个锁旗标的交互。例如: synchronized(对象){ 代码段 } synchronized的功能是:首先判断对象的锁旗标是否在,如果在就获得锁旗标,然后就可以执行紧随其后的代码段;如果对象的锁旗标不在(已被其他线程拿走),就进入等待状态,直到获得锁旗标 当被synchronized限定的代码段执行完,就释放锁旗标 互斥:存票线程和售票线程应保持互斥关系。即售票线程执行时不进入存票线程、存票线程执行时不进入售票线程 Java 使用监视器机制 –每个对象只有一个“锁旗标” ,利用多线程对“锁旗标”的争夺实现线程间的互斥 –当线程A获得了一个对象的锁旗标后,线程B必须等待线程A完成规定的操作、并释放出锁旗标后,才能获得该对象的锁旗标,并执行线程B中的操作 将需要互斥的语句段放入synchronized(object){}语句框中,且两处的object是相同的 修改上面的代码: class Producer extends Thread { Tickets t = null; public Producer(Tickets t) { this.t = t; } public void run() { while(t.number < t.size) { synchronized (t) { //申请对象t的锁旗标 System.out.println("Producer puts ticket" + (++t.number)); t.available = true; } //释放对象t的锁旗标 } } } class Consumer extends Thread { Tickets t = null; int i = 0; public Consumer(Tickets t) { this.t = t; } public void run() { while(i < t.size) { synchronized (t) { ////申请对象t的锁旗标 if(t.available == true && i <= t.number) System.out.println("Consumer buys ticket" + (++i)); if(i == t.number) t.available = false; } } //释放对象t的锁旗标 } } 运行结果: Producer puts ticket1Producer puts ticket2Producer puts ticket3Producer puts ticket4Producer puts ticket5Producer puts ticket6Producer puts ticket7Producer puts ticket8Producer puts ticket9Producer puts ticket10Consumer buys ticket1Consumer buys ticket2Consumer buys ticket3Consumer buys ticket4Consumer buys ticket5Consumer buys ticket6Consumer buys ticket7Consumer buys ticket8Consumer buys ticket9Consumer buys ticket10 说明: 1、存票程序段和售票程序段为获得同一对象的锁旗标而实现互斥操作 2、当线程执行到synchronized的时候,检查传入的实参对象,并申请得到该对象的锁旗标。如果得不到,那么线程就被放到一个与该对象锁旗标相对应的等待线程池中。直到该对象的锁旗标被归还,池中的等待线程才能重新去获得锁旗标,然后继续执行下去 3、除了可以对指定的代码段进行同步控制之外,还可以定义整个方法在同步控制下执行,只要在方法定义前加上synchronized关键字即可 把上面的程序再修改一下: class Tickets { int number = 0; //票号 int size; //总票数 int i = 0; //售票序号 boolean available = false; //表示是否有票可售 public Tickets(int size) { //构造函数,传入总票参数 this.size = size; } public synchronized void put() { //同步方法,实现存票的功能 System.out.println("Producer puts ticket" + (++number)); available = true; } public synchronized void sell() { //同步方法,实现售票的功能 if(available == true && i <= number) System.out.println("Consumer buys ticket" + (++i)); if(i == number) available = false; } } class Producer extends Thread { Tickets t = null; public Producer(Tickets t) { this.t = t; } public void run() { //如果存票数小于限定总量,则不断存入票 while(t.number < t.size) t.put(); } } class Consumer extends Thread { Tickets t = null; int i = 0; public Consumer(Tickets t) { this.t = t; } public void run() { //如果售票数小于限定总量,则不断售票 while(i < t.size) t.sell(); } } 线程之间的通信 为了更有效地协调不同线程的工作,需要在线程间建立沟通渠道,通过线程间的“对话”来解决线程间的同步问题 java.lang.Object 类的一些方法为线程间的通讯提供了有效手段 wait() 如果当前状态不适合本线程执行,正在执行同步代码(synchronized)的某个线程A调用该方法(在对象x上),该线程暂停执行而进入对象x的等待 池,并释放已获得的对象x的锁旗标。线程A要一直等到其他线程在对象x上调用notify或notifyAll方法,才能够在重新获得对象x的锁旗标后继 续执行(从wait语句后继续执行) notify()和notifyAll()方法: notify() 随机唤醒一个等待的线程,本线程继续执行 线程被唤醒以后,还要等发出唤醒消息者释放监视器,这期间关键数据仍可能被改变 被唤醒的线程开始执行时,一定要判断当前状态是否适合自己运行 notifyAll() 唤醒所有等待的线程,本线程继续执行 修改上面的例子,使每存入一张票,就售一张票,售出后,再存入。 代码如下: package javatest; import com.sun.org.apache.xalan.internal.xslt.Process; class Tickets { int number = 0; //票号 int size; //总票数 int i = 0; //售票序号 boolean available = false; //表示是否有票可售 public Tickets(int size) { //构造函数,传入总票参数 this.size = size; } public synchronized void put() { if(available) { //如果还有存票待售,则存票线程等待 try { wait(); } catch (Exception e) { // TODO: handle exception } } System.out.println("Producer puts ticket" + (++number)); available = true; notify(); //存票后唤醒售票线程开始售票 } public synchronized void sell() { if(!available) { //如果没有存票,则售票线程等待 try { wait(); } catch(Exception e) { } } System.out.println("Consumer buys ticket" + (number)); available = false; notify(); if(number == size) number = size + 1; //在售完最后一张票后, //设置一个结束标志,number>size表示售票结束 } } class Producer extends Thread { Tickets t = null; public Producer(Tickets t) { this.t = t; } public void run() { //如果存票数小于限定总量,则不断存入票 while(t.number < t.size) t.put(); } } class Consumer extends Thread { Tickets t = null; int i = 0; public Consumer(Tickets t) { this.t = t; } public void run() { //如果售票数小于限定总量,则不断售票 while(i < t.size) t.sell(); } } public class TicketSell { public static void main(String[] args) { Tickets t = new Tickets(10); new Consumer(t).start(); new Producer(t).start(); } } 运行结果: Producer puts ticket1Consumer buys ticket1Producer puts ticket2Consumer buys ticket2Producer puts ticket3Consumer buys ticket3Producer puts ticket4Consumer buys ticket4Producer puts ticket5Consumer buys ticket5Producer puts ticket6Consumer buys ticket6Producer puts ticket7Consumer buys ticket7Producer puts ticket8Consumer buys ticket8Producer puts ticket9Consumer buys ticket9Producer puts ticket10Consumer buys ticket10 程序说明: 当Consumer线程售出票后,available值变为false,当Producer线程放入票后,available值变为true 只有available为true时,Consumer线程才能售票,否则就必须等待Producer线程放入新的票后的通知 只有available为false时,Producer线程才能放票,否则必须等待Consumer线程售出票后的通知 后台线程 也叫守护线程,通常是为了辅助其它线程而运行的线程 它不妨碍程序终止 一个进程中只要还有一个前台线程在运行,这个进程就不会结束;如果一个进程中的所有前台线程都已经结束,那么无论是否还有未结束的后台线程,这个进程都会结束 “垃圾回收”便是一个后台线程 如果对某个线程对象在启动(调用start方法)之前调用了setDaemon(true)方法,这个线程就变成了后台线程
Java的反射机制 在Java运行时环境中,对于任意一个类,能否知道这个类有哪些属性和方法?对于任意一个对象,能否调用它的任意一个方法?答案是肯定的。 这种动态获取类的信息以及动态调用对象的方法的功能来自于Java 语言的反射(Reflection)机制。 Java 反射机制主要提供了以下功能: 在运行时判断任意一个对象所属的类。 在运行时构造任意一个类的对象。 在运行时判断任意一个类所具有的成员变量和方法。 在运行时调用任意一个对象的方法 Reflection 是Java被视为动态(或准动态)语言的一个关键性质。这个机制允许程序在运行时透过Reflection API取得任何一个已知名称的class的内部信息,包括其modifiers(诸如public, static 等等)、superclass(例如Object)、实现之interfaces(例如Serializable),也包括fields和methods 的所有信息,并可于运行时改变fields内容或调用methods 在JDK中,主要由以下类来实现Java反射机制,这些类都位于java.lang.reflect包中 Class类:代表一个类。 Field 类:代表类的成员变量(成员变量也称为类的属性)。 Method类:代表类的方法。 Constructor 类:代表类的构造方法。 Array类:提供了动态创建数组,以及访问数组的元素的静态方法 Proxy类以及InvocationHandler接口:提供了动态生成代理类以及实例的方法 其中,Class类是Reflection API 中的核心类,它有以下方法: getName():获得类的完整名字 getFields():获得类的public类型的属性 getDeclaredFields():获得类的所有属性 getMethods():获得类的public类型的方法 getDeclaredMethods():获得类的所有方法 getMethod(String name, Class[] parameterTypes):获得类的特定方法,name参数指定方法的名字,parameterTypes 参数指定方法的参数类型 getConstructors():获得类的public类型的构造方法 getConstructor(Class[] parameterTypes):获得类的特定构造方法,parameterTypes 参数指定构造方法的参数类型 newInstance():通过类的不带参数的构造方法创建这个类的一个对象 每当一個类被载入时,JVM就自动为其生成一个Class对象,通过操作class对象,我们可以得到该对象的所有成员并操作它们,举个例子: package javatest; import java.util.*; class Student { private String name; private int age; private int ID; public Student() { } public Student(String name, int age, int ID) { this.name = name; this.age = age; this.ID = ID; } public String getName() { return name; } public void setName(String name) { this.name = name; } } public class javatest { public static void main(String[] args) { Student s1 = new Student("java", 20, 123); Class ss = s1.getClass(); System.out.println("getName: " + ss.getName()); System.out.println("getFields: " + ss.getFields()); System.out.println("getDeclaredFields: " + ss.getDeclaredFields()); System.out.println("getMethods: " + ss.getMethods()); System.out.println("isInterface: " + ss.isInterface()); System.out.println("isPrimitive: " + ss.isPrimitive()); System.out.println("isArray: " + ss.isArray()); System.out.println("SuperClass: " + ss.getSuperclass().getName()); } } 运行结果如下: getName: javatest.StudentgetFields: [Ljava.lang.reflect.Field;@4e25154fgetDeclaredFields: [Ljava.lang.reflect.Field;@70dea4egetMethods: [Ljava.lang.reflect.Method;@5c647e05isInterface: falseisPrimitive: falseisArray: falseSuperClass: java.lang.Object 通过反射得到类对象: 获取方式 说明 示例 object.getClass() 每个对象都有此方法 获取指定实例对象的Class List list = new ArrayList(); Class listClass = list.getClass(); class. getSuperclass() 获取当前Class的继承类Class List list = new ArrayList(); Class listClass = list.getClass(); Class superClass = listClass. getSuperclass(); Object.class .class直接获取 Class listClass = ArrayList.class; Class.forName(类名) 用Class的静态方法,传入类的全称即可 try { Class c = Class.forName("java.util.ArrayList"); } catch (ClassNotFoundException e) { e.printStackTrace(); } Primitive.TYPE 基本数据类型的封装类获取Class的方式 Class longClass = Long.TYPE; Class integerClass = Integer.TYPE; Class voidClass = Void.TYPE; 平常情况我们通过new Object来生成一个类的实例,但有时候我们没法直接new,只能通过反射动态生成。 通过反射实例化对象: 实例化无参构造函数的对象,两种方式: ① Class. newInstance(); ② Class. getConstructor (new Class[]{}).newInstance(new Object[]{}) 实例化带参构造函数的对象: class.getConstructor(Class<?>... parameterTypes) . newInstance(Object... initargs) 接下来举个例子实战一下: package javatest; import java.util.*; class BaseUser { public int baseId; public int getBaseId() { return baseId; } public void setBaseId(int baseId) { this.baseId = baseId; } } class User extends BaseUser { private int id; public String name; public User(){} public User(String name) { this.name = name; } private int getId() { return id; } private void serId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } public class javatest { public static void main(String[] args) { Class<?> userClass = User.class; try { User user = (User)userClass.newInstance(); System.out.println("1.反射实例化(无参): " + user); User user2 = (User)userClass.getConstructor(new Class[]{}).newInstance(new Object[]{}); System.out.println("2.反射实例化(无参): " + user2); User user3 = (User)userClass.getConstructor(new Class[]{String.class}).newInstance(new Object[]{"test"}); System.out.println("反射实例化(带参): " + user3 + " 属性Name的值: " + user3.getName()); User user4 = new User(); System.out.println("正常实例化: " + user4); } catch(Exception e) { e.printStackTrace(); } } } 运行结果如下: 1.反射实例化(无参): javatest.User@7852e9222.反射实例化(无参): javatest.User@4e25154f反射实例化(带参): javatest.User@70dea4e 属性Name的值: test正常实例化: javatest.User@5c647e05 通过反射调用Method(方法): 获得当前类以及超类的public Method: Method[] arrMethods = classType.getMethods(); 获得当前类申明的所有Method: Method[] arrMethods = classType.getDeclaredMethods(); 获得当前类以及超类指定的public Method: Method method = classType.getMethod(String name, Class<?>... parameterTypes); 获得当前类申明的指定的Method: Method method = classType.getDeclaredMethod(String name, Class<?>... parameterTypes) 通过反射动态运行指定Method: Object obj = method.invoke(Object obj, Object... args) 例:动态操纵Method,改变一下上面的例子的主函数: public class reflectMethodDemo { public static void main(String[] args) { User user = new User(); Class<?> userClass = User.class; Method[] publicMethod = userClass.getMethods(); for(Method method : publicMethod) { System.out.println("获得当前类以及超类的所有publicMethod: " + method); } Method[] currentMethod = userClass.getDeclaredMethods(); for(Method method : currentMethod) { System.out.println("获得当前类自己声明的所有的Method: " + method); } try { Method setBaseIdMethod = userClass.getMethod("setBaseId", new Class[]{int.class}); System.out.println("获得当前类或超类的public Method setBaseId: " + setBaseIdMethod); Method setIdMethod = userClass.getDeclaredMethod("setId", new Class[]{int.class}); System.out.println("获得当前类的Method setId: " + setIdMethod); setIdMethod.setAccessible(true); setIdMethod.invoke(user, new Object[]{110}); Method getIdMethod = userClass.getDeclaredMethod("getId", new Class[]{}); getIdMethod.setAccessible(true); Integer getId = (Integer)getIdMethod.invoke(user, new Object[]{}); System.out.println("调用getId方法获得: " + getId); } catch(Exception e) { e.printStackTrace(); } } } 通过反射调用Field(变量): 获得当前类以及超类的public Field: Field[] arrFields = classType.getFields(); 获得当前类申明的所有Field: Field[] arrFields = classType.getDeclaredFields(); 获得当前类以及超类指定的public Field: Field field = classType.getField(String name); 获得当前类申明的指定的Field: Field field = classType.getDeclaredField(String name); 通过反射动态设定Field的值: fieldType.set(Object obj, Object value); 通过反射动态获取Field的值: Object obj = fieldType.get(Object obj) ; 例:动态操纵Field,改变一下上面的例子的主函数: public class reflectFieldDemo { public static void main(String[] args) { User1 user = new User1(); Class<?> userClass = user.getClass(); Field[] publicField = userClass.getFields(); for(Field field : publicField) { System.out.println("获得该类及超类所有public Field: " + field); } Field[] currentField = userClass.getDeclaredFields(); for(Field field : currentField) { System.out.println("获得该类自己声明的所有Field: " + field); } try { Field baseIdField = userClass.getField("baseId"); System.out.println("获得该类或超类名为baseId的public Field: " + baseIdField); Field idField = userClass.getDeclaredField("id"); System.out.println("获得该类自己声明的名为id的Field: " + idField); idField.setAccessible(true); idField.set(user, 110); Integer id = (Integer)idField.get(user); System.out.println("id的值为: " + id); } catch (Exception e) { e.printStackTrace(); } } } Java反射总结: 1、只要用到反射,先获得Class Object 2、没有方法能获得当前类的超类的private方法和属性,你必须通过getSuperclass()找到超类以后再去尝试获得 3、通常情况即使是当前类,private属性或方法也是不能访问的,你需要设置压制权限setAccessible(true)来取得private的访问权。但说实话,这已经破坏了面向对象的规则,所以除非万不得已,请尽量少用。 Java的动态代理机制 代理:设计模式 代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。 图 1. 代理模式 为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。Java 动态代理机制以巧妙的方式近乎完美地实践了代理模式的设计理念。 相关的类和接口: 要了解 Java 动态代理的机制,首先需要了解以下相关的类或接口: java.lang.reflect.Proxy:这是 Java 动态代理机制的主类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象。 清单 1. Proxy 的静态方法 // 方法 1: 该方法用于获取指定代理对象所关联的调用处理器 static InvocationHandler getInvocationHandler(Object proxy) // 方法 2:该方法用于获取关联于指定类装载器和一组接口的动态代理类的类对象 static Class getProxyClass(ClassLoader loader, Class[] interfaces) // 方法 3:该方法用于判断指定类对象是否是一个动态代理类 static boolean isProxyClass(Class cl) // 方法 4:该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例 static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) java.lang.reflect.InvocationHandler:这是调用处理器接口,它自定义了一个 invoke 方法,用于集中处理在动态代理类对象上的方法调用,通常在该方法中实现对委托类的代理访问。 清单 2. InvocationHandler 的核心方法 // 该方法负责集中处理动态代理类上的所有方法调用。第一个参数既是代理类实例,第二个参数是被调用的方法对象 // 第三个方法是调用参数。调用处理器根据这三个参数进行预处理或分派到委托类实例上发射执行 Object invoke(Object proxy, Method method, Object[] args) 每次生成动态代理类对象时都需要指定一个实现了该接口的调用处理器对象(参见 Proxy 静态方法 4 的第三个参数)。 java.lang.ClassLoader:这是类装载器类,负责将类的字节码装载到 Java 虚拟机(JVM)中并为其定义类对象,然后该类才能被使用。Proxy 静态方法生成动态代理类同样需要通过类装载器来进行装载才能使用,它与普通类的唯一区别就是其字节码是由 JVM 在运行时动态生成的而非预存在于任何一个 .class 文件中。 每次生成动态代理类对象时都需要指定一个类装载器对象(参见 Proxy 静态方法 4 的第一个参数) 代理机制及其特点: 首先让我们来了解一下如何使用 Java 动态代理。具体有如下四步骤: 1、通过实现 InvocationHandler 接口创建自己的调用处理器; 2、通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类; 3、通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型; 4、通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。 清单 3. 动态代理对象创建过程 // InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发 // 其内部通常包含指向委托类实例的引用,用于真正执行分派转发过来的方法调用 InvocationHandler handler = new InvocationHandlerImpl(..); // 通过 Proxy 为包括 Interface 接口在内的一组接口动态创建代理类的类对象 Class clazz = Proxy.getProxyClass(classLoader, new Class[] { Interface.class, ... }); // 通过反射从生成的类对象获得构造函数对象 Constructor constructor = clazz.getConstructor(new Class[] { InvocationHandler.class }); // 通过构造函数对象创建动态代理类实例 Interface Proxy = (Interface)constructor.newInstance(new Object[] { handler }); 实际使用过程更加简单,因为 Proxy 的静态方法 newProxyInstance 已经为我们封装了步骤 2 到步骤 4 的过程,所以简化后的过程如下 清单 4. 简化的动态代理对象创建过程 // InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发 InvocationHandler handler = new InvocationHandlerImpl(..); // 通过 Proxy 直接创建动态代理类实例 Interface proxy = (Interface)Proxy.newProxyInstance( classLoader, new Class[] { Interface.class }, handler ); 接下来让我们来了解一下 Java 动态代理机制的一些特点。 首先是动态生成的代理类本身的一些特点。 1)包:如果所代理的接口都是 public 的,那么它将被定义在顶层包(即包路径为空),如果所代理的接口中有非 public 的接口(因为接口不能被定义为 protect 或 private,所以除 public 之外就是默认的 package 访问级别),那么它将被定义在该接口所在包(假设代理了 com.cnblogs.wu 包中的某非 public 接口 A,那么新生成的代理类所在的包就是 com.cnblogs.wu),这样设计的目的是为了最大程度的保证动态代理类不会因为包管理的问题而无法被成功定义并访问; 2)类修饰符:该代理类具有 final 和 public 修饰符,意味着它可以被所有的类访问,但是不能被再度继承; 3)类名:格式是“$ProxyN”,其中 N 是一个逐一递增的阿拉伯数字,代表 Proxy 类第 N 次生成的动态代理类,值得注意的一点是,并不是每次调用 Proxy 的静态方法创建动态代理类都会使得 N 值增加,原因是如果对同一组接口(包括接口排列的顺序相同)试图重复创建动态代理类,它会很聪明地返回先前已经创建好的代理类的类对象,而不会再尝试去创建一个全新的代理类,这样可以节省不必要的代码重复生成,提高了代理类的创建效率。 4)类继承关系:该类的继承关系如图: 图 2. 动态代理类的继承图 由图可见,Proxy 类是它的父类,这个规则适用于所有由 Proxy 创建的动态代理类。而且该类还实现了其所代理的一组接口,这就是为什么它能够被安全地类型转换到其所代理的某接口的根本原因。 接下来让我们了解一下代理类实例的一些特点。每个实例都会关联一个调用处理器对象,可以通过 Proxy 提供的静态方法 getInvocationHandler 去获得代理类实例的调用处理器对象。在代理类实例上调用其代理的接口中所声明的方法时,这些方法最终都会由调用处理器的 invoke 方法执行,此外,值得注意的是,代理类的根类 java.lang.Object 中有三个方法也同样会被分派到调用处理器的 invoke 方法执行,它们是 hashCode,equals 和 toString,可能的原因有:一是因为这些方法为 public 且非 final 类型,能够被代理类覆盖;二是因为这些方法往往呈现出一个类的某种特征属性,具有一定的区分度,所以为了保证代理类与委托类对外的一致性,这三个方法也应该被分派到委托类执行。当代理的一组接口有重复声明的方法且该方法被调用时,代理类总是从排在最前面的接口中获取方法对象并分派给调用处理器,而无论代理类实例是否正在以该接口(或继承于该接口的某子接口)的形式被外部引用,因为在代理类内部无法区分其当前的被引用类型。 接着来了解一下被代理的一组接口有哪些特点。首先,要注意不能有重复的接口,以避免动态代理类代码生成时的编译错误。其次,这些接口对于类装载器必须可见,否则类装载器将无法链接它们,将会导致类定义失败。再次,需被代理的所有非 public 的接口必须在同一个包中,否则代理类生成也会失败。最后,接口的数目不能超过 65535,这是 JVM 设定的限制。 最后再来了解一下异常处理方面的特点。从调用处理器接口声明的方法中可以看到理论上它能够抛出任何类型的异常,因为所有的异常都继承于 Throwable 接口,但事实是否如此呢?答案是否定的,原因是我们必须遵守一个继承原则:即子类覆盖父类或实现父接口的方法时,抛出的异常必须在原方法支持的异常列表之内。所以虽然调用处理器理论上讲能够,但实际上往往受限制,除非父接口中的方法支持抛 Throwable 异常。那么如果在 invoke 方法中的确产生了接口方法声明中不支持的异常,那将如何呢?放心,Java 动态代理类已经为我们设计好了解决方法:它将会抛出 UndeclaredThrowableException 异常。这个异常是一个 RuntimeException 类型,所以不会引起编译错误。通过该异常的 getCause 方法,还可以获得原来那个不受支持的异常对象,以便于错误诊断。 机制和特点都介绍过了,接下来让我们通过源代码来了解一下 Proxy 到底是如何实现的。 首先记住 Proxy 的几个重要的静态变量: 清单 5. Proxy 的重要静态变量 // 映射表:用于维护类装载器对象到其对应的代理类缓存 private static Map loaderToCache = new WeakHashMap(); // 标记:用于标记一个动态代理类正在被创建中 private static Object pendingGenerationMarker = new Object(); // 同步表:记录已经被创建的动态代理类类型,主要被方法 isProxyClass 进行相关的判断 private static Map proxyClasses = Collections.synchronizedMap(new WeakHashMap()); // 关联的调用处理器引用 protected InvocationHandler h; 然后,来看一下 Proxy 的构造方法: 清单 6. Proxy 构造方法 // 由于 Proxy 内部从不直接调用构造函数,所以 private 类型意味着禁止任何调用 private Proxy() {} // 由于 Proxy 内部从不直接调用构造函数,所以 protected 意味着只有子类可以调用 protected Proxy(InvocationHandler h) {this.h = h;} 接着,可以快速浏览一下 newProxyInstance 方法,因为其相当简单: 清单 7. Proxy 静态方法 newProxyInstance public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException { // 检查 h 不为空,否则抛异常 if (h == null) { throw new NullPointerException(); } // 获得与制定类装载器和一组接口相关的代理类类型对象 Class cl = getProxyClass(loader, interfaces); // 通过反射获取构造函数对象并生成代理类实例 try { Constructor cons = cl.getConstructor(constructorParams); return (Object) cons.newInstance(new Object[] { h }); } catch (NoSuchMethodException e) { throw new InternalError(e.toString()); } catch (IllegalAccessException e) { throw new InternalError(e.toString()); } catch (InstantiationException e) { throw new InternalError(e.toString()); } catch (InvocationTargetException e) { throw new InternalError(e.toString()); } } 由此可见,动态代理真正的关键是在 getProxyClass 方法,该方法负责为一组接口动态地生成代理类类型对象。在该方法内部,您将能看到 Proxy 内的各路英雄(静态变量)悉数登场。该方法总共可以分为四个步骤: 1、对这组接口进行一定程度的安全检查,包括检查接口类对象是否对类装载器可见并且与类装载器所能识别的接口类对象是完全相同的,还会检查确保是 interface 类型而不是 class 类型。这个步骤通过一个循环来完成,检查通过后将会得到一个包含所有接口名称的字符串数组,记为 String[] interfaceNames。总体上这部分实现比较直观,所以略去大部分代码,仅保留留如何判断某类或接口是否对特定类装载器可见的相关代码。 清单 8. 通过 Class.forName 方法判接口的可见性 try { // 指定接口名字、类装载器对象,同时制定 initializeBoolean 为 false 表示无须初始化类 // 如果方法返回正常这表示可见,否则会抛出 ClassNotFoundException 异常表示不可见 interfaceClass = Class.forName(interfaceName, false, loader); } catch (ClassNotFoundException e) { } 2、从 loaderToCache 映射表中获取以类装载器对象为关键字所对应的缓存表,如果不存在就创建一个新的缓存表并更新到 loaderToCache。缓存表是一个 HashMap 实例,正常情况下它将存放键值对(接口名字列表,动态生成的代理类的类对象引用)。当代理类正在被创建时它会临时保存(接口名字列表,pendingGenerationMarker)。标记 pendingGenerationMarke 的作用是通知后续的同类请求(接口数组相同且组内接口排列顺序也相同)代理类正在被创建,请保持等待直至创建完成。 清单 9. 缓存表的使用 do { // 以接口名字列表作为关键字获得对应 cache 值 Object value = cache.get(key); if (value instanceof Reference) { proxyClass = (Class) ((Reference) value).get(); } if (proxyClass != null) { // 如果已经创建,直接返回 return proxyClass; } else if (value == pendingGenerationMarker) { // 代理类正在被创建,保持等待 try { cache.wait(); } catch (InterruptedException e) { } // 等待被唤醒,继续循环并通过二次检查以确保创建完成,否则重新等待 continue; } else { // 标记代理类正在被创建 cache.put(key, pendingGenerationMarker); // break 跳出循环已进入创建过程 break; } while (true); 3、动态创建代理类的类对象。首先是确定代理类所在的包,其原则如前所述,如果都为 public 接口,则包名为空字符串表示顶层包;如果所有非 public 接口都在同一个包,则包名与这些接口的包名相同;如果有多个非 public 接口且不同包,则抛异常终止代理类的生成。确定了包后,就开始生成代理类的类名,同样如前所述按格式“$ProxyN”生成。类名也确定了,接下来就是见证奇迹的发生 —— 动态生成代理类: 清单 10. 动态生成代理类 // 动态地生成代理类的字节码数组 byte[] proxyClassFile = ProxyGenerator.generateProxyClass( proxyName, interfaces); try { // 动态地定义新生成的代理类 proxyClass = defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); } catch (ClassFormatError e) { throw new IllegalArgumentException(e.toString()); } // 把生成的代理类的类对象记录进 proxyClasses 表 proxyClasses.put(proxyClass, null); 由此可见,所有的代码生成的工作都由神秘的 ProxyGenerator 所完成了,当你尝试去探索这个类时,你所能获得的信息仅仅是它位于并未公开的 sun.misc 包,有若干常量、变量和方法以完成这个神奇的代码生成的过程,但是 sun 并没有提供源代码以供研读。至于动态类的定义,则由 Proxy 的 native 静态方法 defineClass0 执行。 代码生成过程进入结尾部分,根据结果更新缓存表,如果成功则将代理类的类对象引用更新进缓存表,否则清楚缓存表中对应关键值,最后唤醒所有可能的正在等待的线程。 走完了以上四个步骤后,至此,所有的代理类生成细节都已介绍完毕,剩下的静态方法如 getInvocationHandler 和 isProxyClass 就显得如此的直观,只需通过查询相关变量就可以完成,所以对其的代码分析就省略了。 代理类实现推演: 分析了 Proxy 类的源代码,相信对 Java 动态代理机制形成一个更加清晰的理解,整理一下思绪,一起来完成一次完整的推演过程吧。 清单 11. 代理类中方法调用的分派转发推演实现 // 假设需代理接口 Simulator public interface Simulator { short simulate(int arg1, long arg2, String arg3) throws ExceptionA, ExceptionB; } // 假设代理类为 SimulatorProxy, 其类声明将如下 final public class SimulatorProxy implements Simulator { // 调用处理器对象的引用 protected InvocationHandler handler; // 以调用处理器为参数的构造函数 public SimulatorProxy(InvocationHandler handler){ this.handler = handler; } // 实现接口方法 simulate public short simulate(int arg1, long arg2, String arg3) throws ExceptionA, ExceptionB { // 第一步是获取 simulate 方法的 Method 对象 java.lang.reflect.Method method = null; try{ method = Simulator.class.getMethod( "simulate", new Class[] {int.class, long.class, String.class} ); } catch(Exception e) { // 异常处理 1(略) } // 第二步是调用 handler 的 invoke 方法分派转发方法调用 Object r = null; try { r = handler.invoke(this, method, // 对于原始类型参数需要进行装箱操作 new Object[] {new Integer(arg1), new Long(arg2), arg3}); }catch(Throwable e) { // 异常处理 2(略) } // 第三步是返回结果(返回类型是原始类型则需要进行拆箱操作) return ((Short)r).shortValue(); } } 模拟推演为了突出通用逻辑所以更多地关注正常流程,而淡化了错误处理,但在实际中错误处理同样非常重要。从以上的推演中我们可以得出一个非常通用的结构化流程:第一步从代理接口获取被调用的方法对象,第二步分派方法到调用处理器执行,第三步返回结果。在这之中,所有的信息都是可以已知的,比如接口名、方法名、参数类型、返回类型以及所需的装箱和拆箱操作。 接下来让我们把注意力重新回到先前被淡化的错误处理上来。在异常处理 1 处,由于我们有理由确保所有的信息如接口名、方法名和参数类型都准确无误,所以这部分异常发生的概率基本为零,所以基本可以忽略。而异常处理 2 处,我们需要思考得更多一些。回想一下,接口方法可能声明支持一个异常列表,而调用处理器 invoke 方法又可能抛出与接口方法不支持的异常,再回想一下先前提及的 Java 动态代理的关于异常处理的特点,对于不支持的异常,必须抛 UndeclaredThrowableException 运行时异常。所以通过再次推演,我们可以得出一个更加清晰的异常处理 2 的情况: 清单 12. 细化的异常处理 2 Object r = null; try { r = handler.invoke(this, method, new Object[] {new Integer(arg1), new Long(arg2), arg3}); } catch( ExceptionA e) { // 接口方法支持 ExceptionA,可以抛出 throw e; } catch( ExceptionB e ) { // 接口方法支持 ExceptionB,可以抛出 throw e; } catch(Throwable e) { // 其他不支持的异常,一律抛 UndeclaredThrowableException throw new UndeclaredThrowableException(e); }
引入实例:贝叶斯分类 贝叶斯分类是一种利用概率统计知识进行分类的统计学分类方法。该方法包括两个步骤:训练样本和分类。 其实现由多个MapReduce 作业完成,如图所示。其中,训练样本可由三个 MapReduce 作业实现: 第一个作业(ExtractJob)抽取文档特征,该作业只需要 Map 即可完成 ; 第二个作业(ClassPriorJob)计算类别的先验概率,即统计每个类别中文档的数目,并计算类别概率; 第三个作业(ConditionalProbilityJob)计算单词的条件概率,即统计<label, word> 在所有文档中出现的次数并计算单词的条件概率。 后两个作业的具体实现类似于WordCount。分类过程由一个作业(PredictJob)完成。该作业的 map()函数计算每个待分类文档属于每个类别的概率,reduce() 函数找出每个文档概率最高的类别,并输出 <docid, label>( 编号为 docid 的文档属于类别 label)。 一个完整的贝叶斯分类算法可能需要 4 个有依赖关系的 MapReduce 作业完成,传统的做法是:为每个作业创建相应的 JobConf 对象,并按照依赖关系依次(串行)提交各个作业,如下所示: // 为 4 个作业分别创建 JobConf 对象 JobConf extractJobConf = new JobConf(ExtractJob.class); JobConf classPriorJobConf = new JobConf(ClassPriorJob.class); JobConf conditionalProbilityJobConf = new JobConf(ConditionalProbilityJob. class) ; JobConf predictJobConf = new JobConf(PredictJob.class); ...// 配置各个 JobConf // 按照依赖关系依次提交作业 JobClient.runJob(extractJobConf); JobClient.runJob(classPriorJobConf); JobClient.runJob(conditionalProbilityJobConf); JobClient.runJob(predictJobConf); 如果使用 JobControl,则用户只需使用 addDepending() 函数添加作业依赖关系接口,JobControl 会按照依赖关系调度各个作业,具体代码如下: Configuration extractJobConf = new Configuration(); Configuration classPriorJobConf = new Configuration(); Configuration conditionalProbilityJobConf = new Configuration(); Configuration predictJobConf = new Configuration(); ...// 设置各个Configuration // 创建Job对象。注意,JobControl要求作业必须封装成Job对象 Job extractJob = new Job(extractJobConf); Job classPriorJob = new Job(classPriorJobConf); Job conditionalProbilityJob = new Job(conditionalProbilityJobConf); Job predictJob = new Job(predictJobConf); //设置依赖关系,构造一个DAG作业 classPriorJob.addDepending(extractJob); conditionalProbilityJob.addDepending(extractJob); predictJob.addDepending(classPriorJob); predictJob.addDepending(conditionalProbilityJob); //创建JobControl对象,由它对作业进行监控和调度 JobControl JC = new JobControl("Native Bayes"); JC.addJob(extractJob);//把4个作业加入JobControl中 JC.addJob(classPriorJob); JC.addJob(conditionalProbilityJob); JC.addJob(predictJob); JC.run(); //提交DAG作业 在实际运行过程中,不依赖于其他任何作业的 extractJob 会优先得到调度,一旦运行完成,classPriorJob 和 conditionalProbilityJob 两个作业同时被调度,待它们全部运行完成后,predictJob 被调度。对比以上两种方案,可以得到一个简单的结论:使用 JobControl 编写 DAG 作业更加简便,且能使多个无依赖关系的作业并行运行。 JobControl 设计原理分析 JobControl 由两个类组成:Job 和 JobControl。其中,Job 类封装了一个 MapReduce 作业及其对应的依赖关系,主要负责监控各个依赖作业的运行状态,以此更新自己的状态,其 状态转移图如图所示。作业刚开始处于 WAITING 状态。如果没有依赖作业或者所有依赖作业均已运行完成,则进入READY 状态。一旦进入 READY 状态,则作业可被提交到 Hadoop 集群上运行,并进入 RUNNING 状态。在 RUNNING 状态下,根据作业运行情况,可能进入 SUCCESS 或者 FAILED 状态。需要注意的是,如果一个作业的依赖作业失败,则该作业也会失败,于是形成“多米诺骨牌效应”, 后续所有作业均会失败。 JobControl 封装了一系列 MapReduce 作业及其对应的依赖关系。 它将处于不同状态的作业放入不同的哈希表中,并按照图所示的状态转移作业,直到所有作业运行完成。在实现的时候,JobControl 包含一个线程用于周期性地监控和更新各个作业的运行状态,调度依赖作业运行完成的作业,提交处于 READY 状态的作业等。同时,它还提供了一些API 用于挂起、恢复和暂停该线程。 Job类深入剖析 在Job类的起始部分,定义了一些数据域,包括job所处的状态,以及其他相关的信息,具体代码如下: import java.util.ArrayList; import org.apache.hadoop.mapred.JobClient; import org.apache.hadoop.mapred.JobConf; import org.apache.hadoop.mapred.JobID; import org.apache.hadoop.mapred.jobcontrol.Job; // 一个 job 将处于如下的一种状态 final public static int SUCCESS = 0; //成功 final public static int WAITING = 1; //警告 final public static int RUNNING = 2; //运行 final public static int READY = 3; //准备 final public static int FAILED = 4; //失败 final public static int DEPENDENT_FAILED = 5; //依赖的作业失败 private JobConf theJobConf; private int state; private String jobID; // 通过JobControl class分配和使用 private JobID mapredJobID; // 通过map/reduce分配的job ID private String jobName; // 外部名字, 通过client app分配/使用 private String message; // 一些有用的信息例如用户消耗, // e.g. job失败的原因 private ArrayList<Job> dependingJobs; // 当前job所依赖的jobs列表 private JobClient jc = null; // map reduce job client 接着定义了两个构造函数: /** * Construct a job. * @param jobConf a mapred job configuration representing a job to be executed. * @param dependingJobs an array of jobs the current job depends on */ public Job(JobConf jobConf, ArrayList<Job> dependingJobs) throws IOException { this.theJobConf = jobConf; this.dependingJobs = dependingJobs; this.state = Job.WAITING; this.jobID = "unassigned"; this.mapredJobID = null; //not yet assigned this.jobName = "unassigned"; this.message = "just initialized"; this.jc = new JobClient(jobConf); } /** * Construct a job. * * @param jobConf mapred job configuration representing a job to be executed. * @throws IOException */ public Job(JobConf jobConf) throws IOException { this(jobConf, null); } 接着重写了String类中的toString方法,代码如下: toString 接下来是一长串的get/set获取设置属性的代码: get/set 当Job处于writing状态下的时候,可以向依赖列表中添加所依赖的Job: /** * Add a job to this jobs' dependency list. Dependent jobs can only be added while a Job * is waiting to run, not during or afterwards. * * @param dependingJob Job that this Job depends on. * @return <tt>true</tt> if the Job was added. */ public synchronized boolean addDependingJob(Job dependingJob) { if (this.state == Job.WAITING) { //only allowed to add jobs when waiting if (this.dependingJobs == null) { this.dependingJobs = new ArrayList<Job>(); } return this.dependingJobs.add(dependingJob); } else { return false; } } 还提供了是否处于完成状态和是否处于准备状态的判断方法: /** * @return true if this job is in a complete state */ public boolean isCompleted() { return this.state == Job.FAILED || this.state == Job.DEPENDENT_FAILED || this.state == Job.SUCCESS; } /** * @return true if this job is in READY state */ public boolean isReady() { return this.state == Job.READY; } 提供了检查正在运行的Job的状态,如果完成,判断是成功还是失败,代码如下: /** * Check the state of this running job. The state may * remain the same, become SUCCESS or FAILED. */ private void checkRunningState() { RunningJob running = null; try { running = jc.getJob(this.mapredJobID); if (running.isComplete()) { if (running.isSuccessful()) { this.state = Job.SUCCESS; } else { this.state = Job.FAILED; this.message = "Job failed!"; try { running.killJob(); } catch (IOException e1) { } try { this.jc.close(); } catch (IOException e2) { } } } } catch (IOException ioe) { this.state = Job.FAILED; this.message = StringUtils.stringifyException(ioe); try { if (running != null) running.killJob(); } catch (IOException e1) { } try { this.jc.close(); } catch (IOException e1) { } } } 实现了检查并更新Job的状态的checkState()方法: /** * Check and update the state of this job. The state changes * depending on its current state and the states of the depending jobs. */ synchronized int checkState() { if (this.state == Job.RUNNING) { checkRunningState(); } if (this.state != Job.WAITING) { return this.state; } if (this.dependingJobs == null || this.dependingJobs.size() == 0) { this.state = Job.READY; return this.state; } Job pred = null; int n = this.dependingJobs.size(); for (int i = 0; i < n; i++) { pred = this.dependingJobs.get(i); int s = pred.checkState(); if (s == Job.WAITING || s == Job.READY || s == Job.RUNNING) { break; // a pred is still not completed, continue in WAITING // state } if (s == Job.FAILED || s == Job.DEPENDENT_FAILED) { this.state = Job.DEPENDENT_FAILED; this.message = "depending job " + i + " with jobID " + pred.getJobID() + " failed. " + pred.getMessage(); break; } // pred must be in success state if (i == n - 1) { this.state = Job.READY; } } return this.state; } 最后包含提交Job的方法submit(),代码如下: /** * Submit this job to mapred. The state becomes RUNNING if submission * is successful, FAILED otherwise. */ protected synchronized void submit() { try { if (theJobConf.getBoolean("create.empty.dir.if.nonexist", false)) { FileSystem fs = FileSystem.get(theJobConf); Path inputPaths[] = FileInputFormat.getInputPaths(theJobConf); for (int i = 0; i < inputPaths.length; i++) { if (!fs.exists(inputPaths[i])) { try { fs.mkdirs(inputPaths[i]); } catch (IOException e) { } } } } RunningJob running = jc.submitJob(theJobConf); this.mapredJobID = running.getID(); this.state = Job.RUNNING; } catch (IOException ioe) { this.state = Job.FAILED; this.message = StringUtils.stringifyException(ioe); } } } 完整的Job类源代码如下: Job JobControl类深入剖析 在JobControl类的起始部分,定义了一些数据域,包括线程所处的状态,以及其他相关的信息,具体代码如下: // The thread can be in one of the following state private static final int RUNNING = 0; private static final int SUSPENDED = 1; private static final int STOPPED = 2; private static final int STOPPING = 3; private static final int READY = 4; private int runnerState; // the thread state private Map<String, Job> waitingJobs; private Map<String, Job> readyJobs; private Map<String, Job> runningJobs; private Map<String, Job> successfulJobs; private Map<String, Job> failedJobs; private long nextJobID; private String groupName; 接下来是对应的构造函数: /** * Construct a job control for a group of jobs. * @param groupName a name identifying this group */ public JobControl(String groupName) { this.waitingJobs = new Hashtable<String, Job>(); this.readyJobs = new Hashtable<String, Job>(); this.runningJobs = new Hashtable<String, Job>(); this.successfulJobs = new Hashtable<String, Job>(); this.failedJobs = new Hashtable<String, Job>(); this.nextJobID = -1; this.groupName = groupName; this.runnerState = JobControl.READY; } 接着是一个将Map的Jobs转换为ArrayList的转换方法(toArrayList),代码如下: private static ArrayList<Job> toArrayList(Map<String, Job> jobs) { ArrayList<Job> retv = new ArrayList<Job>(); synchronized (jobs) { for (Job job : jobs.values()) { retv.add(job); } } return retv; } 类中当然少不了一些get方法: /** * @return the jobs in the success state */ public ArrayList<Job> getSuccessfulJobs() { return JobControl.toArrayList(this.successfulJobs); } public ArrayList<Job> getFailedJobs() { return JobControl.toArrayList(this.failedJobs); } private String getNextJobID() { nextJobID += 1; return this.groupName + this.nextJobID; } 类中还有将Job插入Job队列的方法: private static void addToQueue(Job aJob, Map<String, Job> queue) { synchronized(queue) { queue.put(aJob.getJobID(), aJob); } } private void addToQueue(Job aJob) { Map<String, Job> queue = getQueue(aJob.getState()); addToQueue(aJob, queue); } 既然有插入队列,就有从Job队列根据Job运行状态而取出的方法,代码如下: private Map<String, Job> getQueue(int state) { Map<String, Job> retv = null; if (state == Job.WAITING) { retv = this.waitingJobs; } else if (state == Job.READY) { retv = this.readyJobs; } else if (state == Job.RUNNING) { retv = this.runningJobs; } else if (state == Job.SUCCESS) { retv = this.successfulJobs; } else if (state == Job.FAILED || state == Job.DEPENDENT_FAILED) { retv = this.failedJobs; } return retv; } 添加一个新的Job的方法: /** * Add a new job. * @param aJob the new job */ synchronized public String addJob(Job aJob) { String id = this.getNextJobID(); aJob.setJobID(id); aJob.setState(Job.WAITING); this.addToQueue(aJob); return id; } /** * Add a collection of jobs * * @param jobs */ public void addJobs(Collection<Job> jobs) { for (Job job : jobs) { addJob(job); } } 获取线程的状态,设置、停止线程的方法: /** * @return the thread state */ public int getState() { return this.runnerState; } /** * set the thread state to STOPPING so that the * thread will stop when it wakes up. */ public void stop() { this.runnerState = JobControl.STOPPING; } /** * suspend the running thread */ public void suspend () { if (this.runnerState == JobControl.RUNNING) { this.runnerState = JobControl.SUSPENDED; } } /** * resume the suspended thread */ public void resume () { if (this.runnerState == JobControl.SUSPENDED) { this.runnerState = JobControl.RUNNING; } } 检查运行、等待的Jobs,将符合条件的添加至相应的队列: synchronized private void checkRunningJobs() { Map<String, Job> oldJobs = null; oldJobs = this.runningJobs; this.runningJobs = new Hashtable<String, Job>(); for (Job nextJob : oldJobs.values()) { int state = nextJob.checkState(); /* if (state != Job.RUNNING) { System.out.println("The state of the running job " + nextJob.getJobName() + " has changed to: " + nextJob.getState()); } */ this.addToQueue(nextJob); } } synchronized private void checkWaitingJobs() { Map<String, Job> oldJobs = null; oldJobs = this.waitingJobs; this.waitingJobs = new Hashtable<String, Job>(); for (Job nextJob : oldJobs.values()) { int state = nextJob.checkState(); /* if (state != Job.WAITING) { System.out.println("The state of the waiting job " + nextJob.getJobName() + " has changed to: " + nextJob.getState()); } */ this.addToQueue(nextJob); } } synchronized private void startReadyJobs() { Map<String, Job> oldJobs = null; oldJobs = this.readyJobs; this.readyJobs = new Hashtable<String, Job>(); for (Job nextJob : oldJobs.values()) { //System.out.println("Job to submit to Hadoop: " + nextJob.getJobName()); nextJob.submit(); //System.out.println("Hadoop ID: " + nextJob.getMapredJobID()); this.addToQueue(nextJob); } } 判断是否所有的JOb都结束的方法: synchronized public boolean allFinished() { return this.waitingJobs.size() == 0 && this.readyJobs.size() == 0 && this.runningJobs.size() == 0; } 检查运行Jobs的状态、更新等待Job状态、在准备状态下提交的Run方法: /** * The main loop for the thread. * The loop does the following: * Check the states of the running jobs * Update the states of waiting jobs * Submit the jobs in ready state */ public void run() { this.runnerState = JobControl.RUNNING; while (true) { while (this.runnerState == JobControl.SUSPENDED) { try { Thread.sleep(5000); } catch (Exception e) { } } checkRunningJobs(); checkWaitingJobs(); startReadyJobs(); if (this.runnerState != JobControl.RUNNING && this.runnerState != JobControl.SUSPENDED) { break; } try { Thread.sleep(5000); } catch (Exception e) { } if (this.runnerState != JobControl.RUNNING && this.runnerState != JobControl.SUSPENDED) { break; } } this.runnerState = JobControl.STOPPED; } } 完整的JobControl类: /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.hadoop.mapred.jobcontrol; import java.util.ArrayList; import java.util.Collection; import java.util.Hashtable; import java.util.Map; /** This class encapsulates a set of MapReduce jobs and its dependency. It tracks * the states of the jobs by placing them into different tables according to their * states. * * This class provides APIs for the client app to add a job to the group and to get * the jobs in the group in different states. When a * job is added, an ID unique to the group is assigned to the job. * * This class has a thread that submits jobs when they become ready, monitors the * states of the running jobs, and updates the states of jobs based on the state changes * of their depending jobs states. The class provides APIs for suspending/resuming * the thread,and for stopping the thread. * */ public class JobControl implements Runnable{ // The thread can be in one of the following state private static final int RUNNING = 0; private static final int SUSPENDED = 1; private static final int STOPPED = 2; private static final int STOPPING = 3; private static final int READY = 4; private int runnerState; // the thread state private Map<String, Job> waitingJobs; private Map<String, Job> readyJobs; private Map<String, Job> runningJobs; private Map<String, Job> successfulJobs; private Map<String, Job> failedJobs; private long nextJobID; private String groupName; /** * Construct a job control for a group of jobs. * @param groupName a name identifying this group */ public JobControl(String groupName) { this.waitingJobs = new Hashtable<String, Job>(); this.readyJobs = new Hashtable<String, Job>(); this.runningJobs = new Hashtable<String, Job>(); this.successfulJobs = new Hashtable<String, Job>(); this.failedJobs = new Hashtable<String, Job>(); this.nextJobID = -1; this.groupName = groupName; this.runnerState = JobControl.READY; } private static ArrayList<Job> toArrayList(Map<String, Job> jobs) { ArrayList<Job> retv = new ArrayList<Job>(); synchronized (jobs) { for (Job job : jobs.values()) { retv.add(job); } } return retv; } /** * @return the jobs in the waiting state */ public ArrayList<Job> getWaitingJobs() { return JobControl.toArrayList(this.waitingJobs); } /** * @return the jobs in the running state */ public ArrayList<Job> getRunningJobs() { return JobControl.toArrayList(this.runningJobs); } /** * @return the jobs in the ready state */ public ArrayList<Job> getReadyJobs() { return JobControl.toArrayList(this.readyJobs); } /** * @return the jobs in the success state */ public ArrayList<Job> getSuccessfulJobs() { return JobControl.toArrayList(this.successfulJobs); } public ArrayList<Job> getFailedJobs() { return JobControl.toArrayList(this.failedJobs); } private String getNextJobID() { nextJobID += 1; return this.groupName + this.nextJobID; } private static void addToQueue(Job aJob, Map<String, Job> queue) { synchronized(queue) { queue.put(aJob.getJobID(), aJob); } } private void addToQueue(Job aJob) { Map<String, Job> queue = getQueue(aJob.getState()); addToQueue(aJob, queue); } private Map<String, Job> getQueue(int state) { Map<String, Job> retv = null; if (state == Job.WAITING) { retv = this.waitingJobs; } else if (state == Job.READY) { retv = this.readyJobs; } else if (state == Job.RUNNING) { retv = this.runningJobs; } else if (state == Job.SUCCESS) { retv = this.successfulJobs; } else if (state == Job.FAILED || state == Job.DEPENDENT_FAILED) { retv = this.failedJobs; } return retv; } /** * Add a new job. * @param aJob the new job */ synchronized public String addJob(Job aJob) { String id = this.getNextJobID(); aJob.setJobID(id); aJob.setState(Job.WAITING); this.addToQueue(aJob); return id; } /** * Add a collection of jobs * * @param jobs */ public void addJobs(Collection<Job> jobs) { for (Job job : jobs) { addJob(job); } } /** * @return the thread state */ public int getState() { return this.runnerState; } /** * set the thread state to STOPPING so that the * thread will stop when it wakes up. */ public void stop() { this.runnerState = JobControl.STOPPING; } /** * suspend the running thread */ public void suspend () { if (this.runnerState == JobControl.RUNNING) { this.runnerState = JobControl.SUSPENDED; } } /** * resume the suspended thread */ public void resume () { if (this.runnerState == JobControl.SUSPENDED) { this.runnerState = JobControl.RUNNING; } } synchronized private void checkRunningJobs() { Map<String, Job> oldJobs = null; oldJobs = this.runningJobs; this.runningJobs = new Hashtable<String, Job>(); for (Job nextJob : oldJobs.values()) { int state = nextJob.checkState(); /* if (state != Job.RUNNING) { System.out.println("The state of the running job " + nextJob.getJobName() + " has changed to: " + nextJob.getState()); } */ this.addToQueue(nextJob); } } synchronized private void checkWaitingJobs() { Map<String, Job> oldJobs = null; oldJobs = this.waitingJobs; this.waitingJobs = new Hashtable<String, Job>(); for (Job nextJob : oldJobs.values()) { int state = nextJob.checkState(); /* if (state != Job.WAITING) { System.out.println("The state of the waiting job " + nextJob.getJobName() + " has changed to: " + nextJob.getState()); } */ this.addToQueue(nextJob); } } synchronized private void startReadyJobs() { Map<String, Job> oldJobs = null; oldJobs = this.readyJobs; this.readyJobs = new Hashtable<String, Job>(); for (Job nextJob : oldJobs.values()) { //System.out.println("Job to submit to Hadoop: " + nextJob.getJobName()); nextJob.submit(); //System.out.println("Hadoop ID: " + nextJob.getMapredJobID()); this.addToQueue(nextJob); } } synchronized public boolean allFinished() { return this.waitingJobs.size() == 0 && this.readyJobs.size() == 0 && this.runningJobs.size() == 0; } /** * The main loop for the thread. * The loop does the following: * Check the states of the running jobs * Update the states of waiting jobs * Submit the jobs in ready state */ public void run() { this.runnerState = JobControl.RUNNING; while (true) { while (this.runnerState == JobControl.SUSPENDED) { try { Thread.sleep(5000); } catch (Exception e) { } } checkRunningJobs(); checkWaitingJobs(); startReadyJobs(); if (this.runnerState != JobControl.RUNNING && this.runnerState != JobControl.SUSPENDED) { break; } try { Thread.sleep(5000); } catch (Exception e) { } if (this.runnerState != JobControl.RUNNING && this.runnerState != JobControl.SUSPENDED) { break; } } this.runnerState = JobControl.STOPPED; } }
旧版 API 的 Partitioner 解析 Partitioner 的作用是对 Mapper 产生的中间结果进行分片,以便将同一分组的数据交给同一个 Reducer 处理,它直接影响 Reduce 阶段的负载均衡。旧版 API 中 Partitioner 的类图如图所示。它继承了JobConfigurable,可通过 configure 方法初始化。它本身只包含一个待实现的方法 getPartition。 该方法包含三个参数, 均由框架自动传入,前面两个参数是key/value,第三个参数 numPartitions 表示每个 Mapper 的分片数,也就是 Reducer 的个数。 MapReduce 提供了两个Partitioner 实 现:HashPartitioner和TotalOrderPartitioner。其中 HashPartitioner 是默认实现,它实现了一种基于哈希值的分片方法,代码如下: public int getPartition(K2 key, V2 value, int numReduceTasks) { return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks; } TotalOrderPartitioner 提供了一种基于区间的分片方法,通常用在数据全排序中。在MapReduce 环境中,容易想到的全排序方案是归并排序,即在 Map 阶段,每个 Map Task进行局部排序;在 Reduce 阶段,启动一个 Reduce Task 进行全局排序。由于作业只能有一个 Reduce Task,因而 Reduce 阶段会成为作业的瓶颈。为了提高全局排序的性能和扩展性,MapReduce 提供了 TotalOrderPartitioner。它能够按照大小将数据分成若干个区间(分片),并保证后一个区间的所有数据均大于前一个区间数据,这使得全排序的步骤如下:步骤1:数据采样。在 Client 端通过采样获取分片的分割点。Hadoop 自带了几个采样算法,如 IntercalSampler、 RandomSampler、 SplitSampler 等(具体见org.apache.hadoop.mapred.lib 包中的 InputSampler 类)。 下面举例说明。 采样数据为: b, abc, abd, bcd, abcd, efg, hii, afd, rrr, mnk 经排序后得到: abc, abcd, abd, afd, b, bcd, efg, hii, mnk, rrr 如果 Reduce Task 个数为 4,则采样数据的四等分点为 abd、 bcd、 mnk,将这 3 个字符串作为分割点。步骤2:Map 阶段。本 阶段涉及两个组件,分别是 Mapper 和 Partitioner。其中,Mapper 可采用 IdentityMapper,直接将输入数据输出,但 Partitioner 必须选用TotalOrderPartitioner,它将步骤 1 中获取的分割点保存到 trie 树中以便快速定位任意一个记录所在的区间,这样,每个 Map Task 产生 R(Reduce Task 个数)个区间,且区间之间有序。TotalOrderPartitioner 通过 trie 树查找每条记录所对应的 Reduce Task 编号。 如图所示, 我们将分割点 保存在深度为 2 的 trie 树中, 假设输入数据中 有两个字符串“ abg”和“ mnz”, 则字符串“ abg” 对应 partition1, 即第 2 个 Reduce Task, 字符串“ mnz” 对应partition3, 即第 4 个 Reduce Task。步骤 3:Reduce 阶段。每 个 Reducer 对分配到的区间数据进行局部排序,最终得到全排序数据。从以上步骤可以看出,基于 TotalOrderPartitioner 全排序的效率跟 key 分布规律和采样算法有直接关系;key 值分布越均匀且采样越具有代表性,则 Reduce Task 负载越均衡,全排序效率越高。TotalOrderPartitioner 有两个典型的应用实例: TeraSort 和 HBase 批量数据导入。 其中,TeraSort 是 Hadoop 自 带的一个应用程序实例。 它曾在 TB 级数据排序基准评估中 赢得第一名,而 TotalOrderPartitioner正是从该实例中提炼出来的。HBase 是一个构建在 Hadoop之上的 NoSQL 数据仓库。它以 Region为单位划分数据,Region 内部数据有序(按 key 排序),Region 之间也有序。很明显,一个 MapReduce 全排序作业的 R 个输出文件正好可对应 HBase 的 R 个 Region。 新版 API 的 Partitioner 解析 新版 API 中的Partitioner类图如图所示。它不再实现JobConfigurable 接口。当用户需要让 Partitioner通过某个JobConf 对象初始化时,可自行实现Configurable 接口,如: public class TotalOrderPartitioner<K, V> extends Partitioner<K,V> implements Configurable Partition所处的位置 Partition主要作用就是将map的结果发送到相应的reduce。这就对partition有两个要求: 1)均衡负载,尽量的将工作均匀的分配给不同的reduce。 2)效率,分配速度一定要快。 Mapreduce提供的Partitioner patition类结构 1. Partitioner<k,v>是partitioner的基类,如果需要定制partitioner也需要继承该类。源代码如下: package org.apache.hadoop.mapred; /** * Partitions the key space. * * <p><code>Partitioner</code> controls the partitioning of the keys of the * intermediate map-outputs. The key (or a subset of the key) is used to derive * the partition, typically by a hash function. The total number of partitions * is the same as the number of reduce tasks for the job. Hence this controls * which of the <code>m</code> reduce tasks the intermediate key (and hence the * record) is sent for reduction.</p> * * @see Reducer * @deprecated Use {@link org.apache.hadoop.mapreduce.Partitioner} instead. */ @Deprecated public interface Partitioner<K2, V2> extends JobConfigurable { /** * Get the paritition number for a given key (hence record) given the total * number of partitions i.e. number of reduce-tasks for the job. * * <p>Typically a hash function on a all or a subset of the key.</p> * * @param key the key to be paritioned. * @param value the entry value. * @param numPartitions the total number of partitions. * @return the partition number for the <code>key</code>. */ int getPartition(K2 key, V2 value, int numPartitions); } 2. HashPartitioner<k,v>是mapreduce的默认partitioner。源代码如下: package org.apache.hadoop.mapreduce.lib.partition; import org.apache.hadoop.mapreduce.Partitioner; /** Partition keys by their {@link Object#hashCode()}. */ public class HashPartitioner<K, V> extends Partitioner<K, V> { /** Use {@link Object#hashCode()} to partition. */ public int getPartition(K key, V value, int numReduceTasks) { return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks; } } 3. BinaryPatitioner继承于Partitioner<BinaryComparable ,V>,是Partitioner<k,v>的偏特化子类。该类提供leftOffset和rightOffset,在计算which reducer时仅对键值K的[rightOffset,leftOffset]这个区间取hash。 reducer=(hash & Integer.MAX_VALUE) % numReduceTasks 4. KeyFieldBasedPartitioner<k2, v2="">也是基于hash的个partitioner。和BinaryPatitioner不同,它提供了多个区间用于计算hash。当区间数 为0时KeyFieldBasedPartitioner退化成HashPartitioner。 源代码如下: package org.apache.hadoop.mapred.lib; import java.io.UnsupportedEncodingException; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.mapred.JobConf; import org.apache.hadoop.mapred.Partitioner; import org.apache.hadoop.mapred.lib.KeyFieldHelper.KeyDescription; /** * Defines a way to partition keys based on certain key fields (also see * {@link KeyFieldBasedComparator}. * The key specification supported is of the form -k pos1[,pos2], where, * pos is of the form f[.c][opts], where f is the number * of the key field to use, and c is the number of the first character from * the beginning of the field. Fields and character posns are numbered * starting with 1; a character position of zero in pos2 indicates the * field's last character. If '.c' is omitted from pos1, it defaults to 1 * (the beginning of the field); if omitted from pos2, it defaults to 0 * (the end of the field). * */ public class KeyFieldBasedPartitioner<K2, V2> implements Partitioner<K2, V2> { private static final Log LOG = LogFactory.getLog(KeyFieldBasedPartitioner.class.getName()); private int numOfPartitionFields; private KeyFieldHelper keyFieldHelper = new KeyFieldHelper(); public void configure(JobConf job) { String keyFieldSeparator = job.get("map.output.key.field.separator", "\t"); keyFieldHelper.setKeyFieldSeparator(keyFieldSeparator); if (job.get("num.key.fields.for.partition") != null) { LOG.warn("Using deprecated num.key.fields.for.partition. " + "Use mapred.text.key.partitioner.options instead"); this.numOfPartitionFields = job.getInt("num.key.fields.for.partition",0); keyFieldHelper.setKeyFieldSpec(1,numOfPartitionFields); } else { String option = job.getKeyFieldPartitionerOption(); keyFieldHelper.parseOption(option); } } public int getPartition(K2 key, V2 value, int numReduceTasks) { byte[] keyBytes; List <KeyDescription> allKeySpecs = keyFieldHelper.keySpecs(); if (allKeySpecs.size() == 0) { return getPartition(key.toString().hashCode(), numReduceTasks); } try { keyBytes = key.toString().getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException("The current system does not " + "support UTF-8 encoding!", e); } // return 0 if the key is empty if (keyBytes.length == 0) { return 0; } int []lengthIndicesFirst = keyFieldHelper.getWordLengths(keyBytes, 0, keyBytes.length); int currentHash = 0; for (KeyDescription keySpec : allKeySpecs) { int startChar = keyFieldHelper.getStartOffset(keyBytes, 0, keyBytes.length, lengthIndicesFirst, keySpec); // no key found! continue if (startChar < 0) { continue; } int endChar = keyFieldHelper.getEndOffset(keyBytes, 0, keyBytes.length, lengthIndicesFirst, keySpec); currentHash = hashCode(keyBytes, startChar, endChar, currentHash); } return getPartition(currentHash, numReduceTasks); } protected int hashCode(byte[] b, int start, int end, int currentHash) { for (int i = start; i <= end; i++) { currentHash = 31*currentHash + b[i]; } return currentHash; } protected int getPartition(int hash, int numReduceTasks) { return (hash & Integer.MAX_VALUE) % numReduceTasks; } } 5. TotalOrderPartitioner这个类可以实现输出的全排序。不同于以上3个partitioner,这个类并不是基于hash的。下面详细的介绍TotalOrderPartitioner TotalOrderPartitioner 类 每一个reducer的输出在默认的情况下都是有顺序的,但是reducer之间在输入是无序的情况下也是无序的。如果要实现输出是全排序的那就会用到TotalOrderPartitioner。 要使用TotalOrderPartitioner,得给TotalOrderPartitioner提供一个partition file。这个文件要求Key(这些key就是所谓的划分)的数量和当前reducer的数量-1相同并且是从小到大排列。对于为什么要用到这样一个文 件,以及这个文件的具体细节待会还会提到。 TotalOrderPartitioner对不同Key的数据类型提供了两种方案: 1) 对于非BinaryComparable 类型的Key,TotalOrderPartitioner采用二分发查找当前的K所在的index。 例如:reducer的数量为5,partition file 提供的4个划分为【2,4,6,8】。如果当前的一个key/value 是<4,”good”>,利用二分法查找到index=1,index+1=2那么这个key/value 将会发送到第二个reducer。如果一个key/value为<4.5, “good”>。那么二分法查找将返回-3,同样对-3加1然后取反就是这个key/value将要去的reducer。 对于一些数值型的数据来说,利用二分法查找复杂度是O(log(reducer count)),速度比较快。 2) 对于BinaryComparable类型的Key(也可以直接理解为字符串)。字符串按照字典顺序也是可以进行排序的。 这样的话也可以给定一些划分,让不同的字符串key分配到不同的reducer里。这里的处理和数值类型的比较相近。 例如:reducer的数量为5,partition file 提供了4个划分为【“abc”, “bce”, “eaa”, ”fhc”】那么“ab”这个字符串将会被分配到第一个reducer里,因为它小于第一个划分“abc”。 但是不同于数值型的数据,字符串的查找和比较不能按照数值型数据的比较方法。mapreducer采用的Tire tree(关于Tire tree可以参考《字典树(Trie Tree)》)的字符串查找方法。查找的时间复杂度o(m),m为树的深度,空间复杂度o(255^m-1)。是一个典型的空间换时间的案例。 Tire tree的构建 假设树的最大深度为3,划分为【aaad ,aaaf, aaaeh,abbx】 Mapreduce里的Tire tree主要有两种节点组成: 1) Innertirenode Innertirenode在mapreduce中是包含了255个字符的一个比较长的串。上图中的例子只包含了26个英文字母。 2) 叶子节点{unslipttirenode, singesplittirenode, leaftirenode} Unslipttirenode 是不包含划分的叶子节点。 Singlesplittirenode 是只包含了一个划分点的叶子节点。 Leafnode是包含了多个划分点的叶子节点。(这种情况比较少见,达到树的最大深度才出现这种情况。在实际操作过程中比较少见) Tire tree的搜索过程 接上面的例子: 1)假如当前 key value pair <aad, 10="">这时会找到图中的leafnode,在leafnode内部使用二分法继续查找找到返回 aad在划分数组中的索引。找不到会返回一个和它最接近的划分的索引。 2)假如找到singlenode,如果和singlenode的划分相同或小返回他的索引,比singlenode的划分大则返回索引+1。 3)假如找到nosplitnode则返回前面的索引。如<zaa, 20="">将会返回abbx的在划分数组中的索引。 TotalOrderPartitioner的疑问 上面介绍了partitioner有两个要求,一个是速度,另外一个是均衡负载。使用tire tree提高了搜素的速度,但是我们怎么才能找到这样的partition file 呢?让所有的划分刚好就能实现均衡负载。 InputSampler 输入采样类,可以对输入目录下的数据进行采样。提供了3种采样方法。 采样类结构图 采样方式对比表: 类名称 采样方式 构造方法 效率 特点 SplitSampler<K,V> 对前n个记录进行采样 采样总数,划分数 最高 RandomSampler<K,V> 遍历所有数据,随机采样 采样频率,采样总数,划分数 最低 IntervalSampler<K,V> 固定间隔采样 采样频率,划分数 中 对有序的数据十分适用 writePartitionFile这个方法很关键,这个方法就是根据采样类提供的样本,首先进行排序,然后选定(随机的方法)和reducer 数目-1的样本写入到partition file。这样经过采样的数据生成的划分,在每个划分区间里的key/value就近似相同了,这样就能完成均衡负载的作用。 SplitSampler类的源代码如下: /** * Samples the first n records from s splits. * Inexpensive way to sample random data. */ public static class SplitSampler<K,V> implements Sampler<K,V> { private final int numSamples; private final int maxSplitsSampled; /** * Create a SplitSampler sampling <em>all</em> splits. * Takes the first numSamples / numSplits records from each split. * @param numSamples Total number of samples to obtain from all selected * splits. */ public SplitSampler(int numSamples) { this(numSamples, Integer.MAX_VALUE); } /** * Create a new SplitSampler. * @param numSamples Total number of samples to obtain from all selected * splits. * @param maxSplitsSampled The maximum number of splits to examine. */ public SplitSampler(int numSamples, int maxSplitsSampled) { this.numSamples = numSamples; this.maxSplitsSampled = maxSplitsSampled; } /** * From each split sampled, take the first numSamples / numSplits records. */ @SuppressWarnings("unchecked") // ArrayList::toArray doesn't preserve type public K[] getSample(InputFormat<K,V> inf, JobConf job) throws IOException { InputSplit[] splits = inf.getSplits(job, job.getNumMapTasks()); ArrayList<K> samples = new ArrayList<K>(numSamples); int splitsToSample = Math.min(maxSplitsSampled, splits.length); int splitStep = splits.length / splitsToSample; int samplesPerSplit = numSamples / splitsToSample; long records = 0; for (int i = 0; i < splitsToSample; ++i) { RecordReader<K,V> reader = inf.getRecordReader(splits[i * splitStep], job, Reporter.NULL); K key = reader.createKey(); V value = reader.createValue(); while (reader.next(key, value)) { samples.add(key); key = reader.createKey(); ++records; if ((i+1) * samplesPerSplit <= records) { break; } } reader.close(); } return (K[])samples.toArray(); } } RandomSampler类的源代码如下: /** * Sample from random points in the input. * General-purpose sampler. Takes numSamples / maxSplitsSampled inputs from * each split. */ public static class RandomSampler<K,V> implements Sampler<K,V> { private double freq; private final int numSamples; private final int maxSplitsSampled; /** * Create a new RandomSampler sampling <em>all</em> splits. * This will read every split at the client, which is very expensive. * @param freq Probability with which a key will be chosen. * @param numSamples Total number of samples to obtain from all selected * splits. */ public RandomSampler(double freq, int numSamples) { this(freq, numSamples, Integer.MAX_VALUE); } /** * Create a new RandomSampler. * @param freq Probability with which a key will be chosen. * @param numSamples Total number of samples to obtain from all selected * splits. * @param maxSplitsSampled The maximum number of splits to examine. */ public RandomSampler(double freq, int numSamples, int maxSplitsSampled) { this.freq = freq; this.numSamples = numSamples; this.maxSplitsSampled = maxSplitsSampled; } /** * Randomize the split order, then take the specified number of keys from * each split sampled, where each key is selected with the specified * probability and possibly replaced by a subsequently selected key when * the quota of keys from that split is satisfied. */ @SuppressWarnings("unchecked") // ArrayList::toArray doesn't preserve type public K[] getSample(InputFormat<K,V> inf, JobConf job) throws IOException { InputSplit[] splits = inf.getSplits(job, job.getNumMapTasks()); ArrayList<K> samples = new ArrayList<K>(numSamples); int splitsToSample = Math.min(maxSplitsSampled, splits.length); Random r = new Random(); long seed = r.nextLong(); r.setSeed(seed); LOG.debug("seed: " + seed); // shuffle splits for (int i = 0; i < splits.length; ++i) { InputSplit tmp = splits[i]; int j = r.nextInt(splits.length); splits[i] = splits[j]; splits[j] = tmp; } // our target rate is in terms of the maximum number of sample splits, // but we accept the possibility of sampling additional splits to hit // the target sample keyset for (int i = 0; i < splitsToSample || (i < splits.length && samples.size() < numSamples); ++i) { RecordReader<K,V> reader = inf.getRecordReader(splits[i], job, Reporter.NULL); K key = reader.createKey(); V value = reader.createValue(); while (reader.next(key, value)) { if (r.nextDouble() <= freq) { if (samples.size() < numSamples) { samples.add(key); } else { // When exceeding the maximum number of samples, replace a // random element with this one, then adjust the frequency // to reflect the possibility of existing elements being // pushed out int ind = r.nextInt(numSamples); if (ind != numSamples) { samples.set(ind, key); } freq *= (numSamples - 1) / (double) numSamples; } key = reader.createKey(); } } reader.close(); } return (K[])samples.toArray(); } } IntervalSampler类的源代码为: /** * Sample from s splits at regular intervals. * Useful for sorted data. */ public static class IntervalSampler<K,V> implements Sampler<K,V> { private final double freq; private final int maxSplitsSampled; /** * Create a new IntervalSampler sampling <em>all</em> splits. * @param freq The frequency with which records will be emitted. */ public IntervalSampler(double freq) { this(freq, Integer.MAX_VALUE); } /** * Create a new IntervalSampler. * @param freq The frequency with which records will be emitted. * @param maxSplitsSampled The maximum number of splits to examine. * @see #getSample */ public IntervalSampler(double freq, int maxSplitsSampled) { this.freq = freq; this.maxSplitsSampled = maxSplitsSampled; } /** * For each split sampled, emit when the ratio of the number of records * retained to the total record count is less than the specified * frequency. */ @SuppressWarnings("unchecked") // ArrayList::toArray doesn't preserve type public K[] getSample(InputFormat<K,V> inf, JobConf job) throws IOException { InputSplit[] splits = inf.getSplits(job, job.getNumMapTasks()); ArrayList<K> samples = new ArrayList<K>(); int splitsToSample = Math.min(maxSplitsSampled, splits.length); int splitStep = splits.length / splitsToSample; long records = 0; long kept = 0; for (int i = 0; i < splitsToSample; ++i) { RecordReader<K,V> reader = inf.getRecordReader(splits[i * splitStep], job, Reporter.NULL); K key = reader.createKey(); V value = reader.createValue(); while (reader.next(key, value)) { ++records; if ((double) kept / records < freq) { ++kept; samples.add(key); key = reader.createKey(); } } reader.close(); } return (K[])samples.toArray(); } } InputSampler类完整源代码如下: InputSampler TotalOrderPartitioner实例 public class SortByTemperatureUsingTotalOrderPartitioner extends Configured implements Tool { @Override public int run(String[] args) throws Exception { JobConf conf = JobBuilder.parseInputAndOutput(this, getConf(), args); if (conf == null) { return -1; } conf.setInputFormat(SequenceFileInputFormat.class); conf.setOutputKeyClass(IntWritable.class); conf.setOutputFormat(SequenceFileOutputFormat.class); SequenceFileOutputFormat.setCompressOutput(conf, true); SequenceFileOutputFormat .setOutputCompressorClass(conf, GzipCodec.class); SequenceFileOutputFormat.setOutputCompressionType(conf, CompressionType.BLOCK); conf.setPartitionerClass(TotalOrderPartitioner.class); InputSampler.Sampler<IntWritable, Text> sampler = new InputSampler.RandomSampler<IntWritable, Text>( 0.1, 10000, 10); Path input = FileInputFormat.getInputPaths(conf)[0]; input = input.makeQualified(input.getFileSystem(conf)); Path partitionFile = new Path(input, "_partitions"); TotalOrderPartitioner.setPartitionFile(conf, partitionFile); InputSampler.writePartitionFile(conf, sampler); // Add to DistributedCache URI partitionUri = new URI(partitionFile.toString() + "#_partitions"); DistributedCache.addCacheFile(partitionUri, conf); DistributedCache.createSymlink(conf); JobClient.runJob(conf); return 0; } public static void main(String[] args) throws Exception { int exitCode = ToolRunner.run( new SortByTemperatureUsingTotalOrderPartitioner(), args); System.exit(exitCode); } }
1 . 旧版 API 的 Mapper/Reducer 解析 Mapper/Reducer 中封装了应用程序的数据处理逻辑。为了简化接口,MapReduce 要求所有存储在底层分布式文件系统上的数据均要解释成 key/value 的形式,并交给Mapper/Reducer 中的 map/reduce 函数处理,产生另外一些 key/value。Mapper 与 Reducer 的类体系非常类似,我们以 Mapper 为例进行讲解。Mapper 的类图如图所示,包括初始化、Map操作和清理三部分。 (1)初始化 Mapper 继承了 JobConfigurable 接口。该接口中的 configure 方法允许通过 JobConf 参数对 Mapper 进行初始化。 (2)Map 操作 MapReduce 框架会通过 InputFormat 中 RecordReader 从 InputSplit 获取一个个 key/value 对, 并交给下面的 map() 函数处理: void map(K1 key, V1 value, OutputCollector<K2, V2> output, Reporter reporter) throws IOException; 该函数的参数除了 key 和 value 之外, 还包括 OutputCollector 和 Reporter 两个类型的参数, 分别用于输出结果和修改 Counter 值。 (3)清理 Mapper 通过继承 Closeable 接口(它又继承了 Java IO 中的 Closeable 接口)获得 close方法,用户可通过实现该方法对 Mapper 进行清理。 MapReduce 提供了很多 Mapper/Reducer 实现,但大部分功能比较简单,具体如图所示。它们对应的功能分别是: ChainMapper/ChainReducer:用于支持链式作业。 IdentityMapper/IdentityReducer:对于输入 key/value 不进行任何处理, 直接输出。 InvertMapper:交换 key/value 位置。 RegexMapper:正则表达式字符串匹配。 TokenMapper:将字符串分割成若干个 token(单词),可用作 WordCount 的 Mapper。 LongSumReducer:以 key 为组,对 long 类型的 value 求累加和。 对于一个 MapReduce 应用程序,不一定非要存在 Mapper。MapReduce 框架提供了比 Mapper 更通用的接口:MapRunnable,如图所示。用 户可以实现该接口以定制Mapper 的调用 方式或者自己实现 key/value 的处理逻辑,比如,Hadoop Pipes 自行实现了MapRunnable,直接将数据通过 Socket 发送给其他进程处理。提供该接口的另外一个好处是允许用户实现多线程 Mapper。 如图所示, MapReduce 提供了两个 MapRunnable 实现,分别是 MapRunner 和MultithreadedMapRunner,其中 MapRunner 为默认实现。 MultithreadedMapRunner 实现了一种多线程的 MapRunnable。 默认情况下,每个 Mapper 启动 10 个线程,通常用于非 CPU类型的作业以提供吞吐率。 2. 新版 API 的 Mapper/Reducer 解析 从图可知, 新 API 在旧 API 基础上发生了以下几个变化: Mapper 由接口变为类,且不再继承 JobConfigurable 和 Closeable 两个接口,而是直接在类中添加了 setup 和 cleanup 两个方法进行初始化和清理工作。 将参数封装到 Context 对象中,这使得接口具有良好的扩展性。 去掉 MapRunnable 接口,在 Mapper 中添加 run 方法,以方便用户定制 map() 函数的调用方法,run 默认实现与旧版本中 MapRunner 的 run 实现一样。 新 API 中 Reducer 遍历 value 的迭代器类型变为 java.lang.Iterable,使得用户可以采用“ foreach” 形式遍历所有 value,如下所示: void reduce(KEYIN key, Iterable<VALUEIN> values, Context context) throws IOException, InterruptedException { for(VALUEIN value: values) { // 注意遍历方式 context.write((KEYOUT) key, (VALUEOUT) value); } } Mapper类的完整代码如下: package org.apache.hadoop.mapreduce; import java.io.IOException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.io.RawComparator; import org.apache.hadoop.io.compress.CompressionCodec; /** * Maps input key/value pairs to a set of intermediate key/value pairs. * * <p>Maps are the individual tasks which transform input records into a * intermediate records. The transformed intermediate records need not be of * the same type as the input records. A given input pair may map to zero or * many output pairs.</p> * * <p>The Hadoop Map-Reduce framework spawns one map task for each * {@link InputSplit} generated by the {@link InputFormat} for the job. * <code>Mapper</code> implementations can access the {@link Configuration} for * the job via the {@link JobContext#getConfiguration()}. * * <p>The framework first calls * {@link #setup(org.apache.hadoop.mapreduce.Mapper.Context)}, followed by * {@link #map(Object, Object, Context)} * for each key/value pair in the <code>InputSplit</code>. Finally * {@link #cleanup(Context)} is called.</p> * * <p>All intermediate values associated with a given output key are * subsequently grouped by the framework, and passed to a {@link Reducer} to * determine the final output. Users can control the sorting and grouping by * specifying two key {@link RawComparator} classes.</p> * * <p>The <code>Mapper</code> outputs are partitioned per * <code>Reducer</code>. Users can control which keys (and hence records) go to * which <code>Reducer</code> by implementing a custom {@link Partitioner}. * * <p>Users can optionally specify a <code>combiner</code>, via * {@link Job#setCombinerClass(Class)}, to perform local aggregation of the * intermediate outputs, which helps to cut down the amount of data transferred * from the <code>Mapper</code> to the <code>Reducer</code>. * * <p>Applications can specify if and how the intermediate * outputs are to be compressed and which {@link CompressionCodec}s are to be * used via the <code>Configuration</code>.</p> * * <p>If the job has zero * reduces then the output of the <code>Mapper</code> is directly written * to the {@link OutputFormat} without sorting by keys.</p> * * <p>Example:</p> * <p><blockquote><pre> * public class TokenCounterMapper * extends Mapper<Object, Text, Text, IntWritable>{ * * private final static IntWritable one = new IntWritable(1); * private Text word = new Text(); * * public void map(Object key, Text value, Context context) throws IOException { * StringTokenizer itr = new StringTokenizer(value.toString()); * while (itr.hasMoreTokens()) { * word.set(itr.nextToken()); * context.collect(word, one); * } * } * } * </pre></blockquote></p> * * <p>Applications may override the {@link #run(Context)} method to exert * greater control on map processing e.g. multi-threaded <code>Mapper</code>s * etc.</p> * * @see InputFormat * @see JobContext * @see Partitioner * @see Reducer */ public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> { public class Context extends MapContext<KEYIN,VALUEIN,KEYOUT,VALUEOUT> { public Context(Configuration conf, TaskAttemptID taskid, RecordReader<KEYIN,VALUEIN> reader, RecordWriter<KEYOUT,VALUEOUT> writer, OutputCommitter committer, StatusReporter reporter, InputSplit split) throws IOException, InterruptedException { super(conf, taskid, reader, writer, committer, reporter, split); } } /** * Called once at the beginning of the task. */ protected void setup(Context context ) throws IOException, InterruptedException { // NOTHING } /** * Called once for each key/value pair in the input split. Most applications * should override this, but the default is the identity function. */ @SuppressWarnings("unchecked") protected void map(KEYIN key, VALUEIN value, Context context) throws IOException, InterruptedException { context.write((KEYOUT) key, (VALUEOUT) value); } /** * Called once at the end of the task. */ protected void cleanup(Context context ) throws IOException, InterruptedException { // NOTHING } /** * Expert users can override this method for more complete control over the * execution of the Mapper. * @param context * @throws IOException */ public void run(Context context) throws IOException, InterruptedException { setup(context); while (context.nextKeyValue()) { map(context.getCurrentKey(), context.getCurrentValue(), context); } cleanup(context); } } 从代码中可以看到,Mapper类中定义了一个新的类Context,继承自MapContext 我们来看看MapContext类的源代码: package org.apache.hadoop.mapreduce; import java.io.IOException; import org.apache.hadoop.conf.Configuration; /** * The context that is given to the {@link Mapper}. * @param <KEYIN> the key input type to the Mapper * @param <VALUEIN> the value input type to the Mapper * @param <KEYOUT> the key output type from the Mapper * @param <VALUEOUT> the value output type from the Mapper */ public class MapContext<KEYIN,VALUEIN,KEYOUT,VALUEOUT> extends TaskInputOutputContext<KEYIN,VALUEIN,KEYOUT,VALUEOUT> { private RecordReader<KEYIN,VALUEIN> reader; private InputSplit split; public MapContext(Configuration conf, TaskAttemptID taskid, RecordReader<KEYIN,VALUEIN> reader, RecordWriter<KEYOUT,VALUEOUT> writer, OutputCommitter committer, StatusReporter reporter, InputSplit split) { super(conf, taskid, writer, committer, reporter); this.reader = reader; this.split = split; } /** * Get the input split for this map. */ public InputSplit getInputSplit() { return split; } @Override public KEYIN getCurrentKey() throws IOException, InterruptedException { return reader.getCurrentKey(); } @Override public VALUEIN getCurrentValue() throws IOException, InterruptedException { return reader.getCurrentValue(); } @Override public boolean nextKeyValue() throws IOException, InterruptedException { return reader.nextKeyValue(); } } MapContext类继承自TaskInputOutputContext,再看看TaskInputOutputContext类的代码: package org.apache.hadoop.mapreduce; import java.io.IOException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.util.Progressable; /** * A context object that allows input and output from the task. It is only * supplied to the {@link Mapper} or {@link Reducer}. * @param <KEYIN> the input key type for the task * @param <VALUEIN> the input value type for the task * @param <KEYOUT> the output key type for the task * @param <VALUEOUT> the output value type for the task */ public abstract class TaskInputOutputContext<KEYIN,VALUEIN,KEYOUT,VALUEOUT> extends TaskAttemptContext implements Progressable { private RecordWriter<KEYOUT,VALUEOUT> output; private StatusReporter reporter; private OutputCommitter committer; public TaskInputOutputContext(Configuration conf, TaskAttemptID taskid, RecordWriter<KEYOUT,VALUEOUT> output, OutputCommitter committer, StatusReporter reporter) { super(conf, taskid); this.output = output; this.reporter = reporter; this.committer = committer; } /** * Advance to the next key, value pair, returning null if at end. * @return the key object that was read into, or null if no more */ public abstract boolean nextKeyValue() throws IOException, InterruptedException; /** * Get the current key. * @return the current key object or null if there isn't one * @throws IOException * @throws InterruptedException */ public abstract KEYIN getCurrentKey() throws IOException, InterruptedException; /** * Get the current value. * @return the value object that was read into * @throws IOException * @throws InterruptedException */ public abstract VALUEIN getCurrentValue() throws IOException, InterruptedException; /** * Generate an output key/value pair. */ public void write(KEYOUT key, VALUEOUT value ) throws IOException, InterruptedException { output.write(key, value); } public Counter getCounter(Enum<?> counterName) { return reporter.getCounter(counterName); } public Counter getCounter(String groupName, String counterName) { return reporter.getCounter(groupName, counterName); } @Override public void progress() { reporter.progress(); } @Override public void setStatus(String status) { reporter.setStatus(status); } public OutputCommitter getOutputCommitter() { return committer; } } TaskInputOutputContext类继承自TaskAttemptContext,实现了Progressable接口,先看看Progressable接口的代码: package org.apache.hadoop.util; /** * A facility for reporting progress. * * <p>Clients and/or applications can use the provided <code>Progressable</code> * to explicitly report progress to the Hadoop framework. This is especially * important for operations which take an insignificant amount of time since, * in-lieu of the reported progress, the framework has to assume that an error * has occured and time-out the operation.</p> */ public interface Progressable { /** * Report progress to the Hadoop framework. */ public void progress(); } TaskAttemptContext类的代码: package org.apache.hadoop.mapreduce; import java.io.IOException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.util.Progressable; /** * The context for task attempts. */ public class TaskAttemptContext extends JobContext implements Progressable { private final TaskAttemptID taskId; private String status = ""; public TaskAttemptContext(Configuration conf, TaskAttemptID taskId) { super(conf, taskId.getJobID()); this.taskId = taskId; } /** * Get the unique name for this task attempt. */ public TaskAttemptID getTaskAttemptID() { return taskId; } /** * Set the current status of the task to the given string. */ public void setStatus(String msg) throws IOException { status = msg; } /** * Get the last set status message. * @return the current status message */ public String getStatus() { return status; } /** * Report progress. The subtypes actually do work in this method. */ public void progress() { } } TaskAttemptContext继承自类JobContext,最后来看看JobContext的源代码: package org.apache.hadoop.mapreduce; import java.io.IOException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.RawComparator; import org.apache.hadoop.mapreduce.Mapper; import org.apache.hadoop.mapreduce.lib.input.TextInputFormat; import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat; import org.apache.hadoop.mapreduce.lib.partition.HashPartitioner; /** * A read-only view of the job that is provided to the tasks while they * are running. */ public class JobContext { // Put all of the attribute names in here so that Job and JobContext are // consistent. protected static final String INPUT_FORMAT_CLASS_ATTR = "mapreduce.inputformat.class"; protected static final String MAP_CLASS_ATTR = "mapreduce.map.class"; protected static final String COMBINE_CLASS_ATTR = "mapreduce.combine.class"; protected static final String REDUCE_CLASS_ATTR = "mapreduce.reduce.class"; protected static final String OUTPUT_FORMAT_CLASS_ATTR = "mapreduce.outputformat.class"; protected static final String PARTITIONER_CLASS_ATTR = "mapreduce.partitioner.class"; protected final org.apache.hadoop.mapred.JobConf conf; private final JobID jobId; public JobContext(Configuration conf, JobID jobId) { this.conf = new org.apache.hadoop.mapred.JobConf(conf); this.jobId = jobId; } /** * Return the configuration for the job. * @return the shared configuration object */ public Configuration getConfiguration() { return conf; } /** * Get the unique ID for the job. * @return the object with the job id */ public JobID getJobID() { return jobId; } /** * Get configured the number of reduce tasks for this job. Defaults to * <code>1</code>. * @return the number of reduce tasks for this job. */ public int getNumReduceTasks() { return conf.getNumReduceTasks(); } /** * Get the current working directory for the default file system. * * @return the directory name. */ public Path getWorkingDirectory() throws IOException { return conf.getWorkingDirectory(); } /** * Get the key class for the job output data. * @return the key class for the job output data. */ public Class<?> getOutputKeyClass() { return conf.getOutputKeyClass(); } /** * Get the value class for job outputs. * @return the value class for job outputs. */ public Class<?> getOutputValueClass() { return conf.getOutputValueClass(); } /** * Get the key class for the map output data. If it is not set, use the * (final) output key class. This allows the map output key class to be * different than the final output key class. * @return the map output key class. */ public Class<?> getMapOutputKeyClass() { return conf.getMapOutputKeyClass(); } /** * Get the value class for the map output data. If it is not set, use the * (final) output value class This allows the map output value class to be * different than the final output value class. * * @return the map output value class. */ public Class<?> getMapOutputValueClass() { return conf.getMapOutputValueClass(); } /** * Get the user-specified job name. This is only used to identify the * job to the user. * * @return the job's name, defaulting to "". */ public String getJobName() { return conf.getJobName(); } /** * Get the {@link InputFormat} class for the job. * * @return the {@link InputFormat} class for the job. */ @SuppressWarnings("unchecked") public Class<? extends InputFormat<?,?>> getInputFormatClass() throws ClassNotFoundException { return (Class<? extends InputFormat<?,?>>) conf.getClass(INPUT_FORMAT_CLASS_ATTR, TextInputFormat.class); } /** * Get the {@link Mapper} class for the job. * * @return the {@link Mapper} class for the job. */ @SuppressWarnings("unchecked") public Class<? extends Mapper<?,?,?,?>> getMapperClass() throws ClassNotFoundException { return (Class<? extends Mapper<?,?,?,?>>) conf.getClass(MAP_CLASS_ATTR, Mapper.class); } /** * Get the combiner class for the job. * * @return the combiner class for the job. */ @SuppressWarnings("unchecked") public Class<? extends Reducer<?,?,?,?>> getCombinerClass() throws ClassNotFoundException { return (Class<? extends Reducer<?,?,?,?>>) conf.getClass(COMBINE_CLASS_ATTR, null); } /** * Get the {@link Reducer} class for the job. * * @return the {@link Reducer} class for the job. */ @SuppressWarnings("unchecked") public Class<? extends Reducer<?,?,?,?>> getReducerClass() throws ClassNotFoundException { return (Class<? extends Reducer<?,?,?,?>>) conf.getClass(REDUCE_CLASS_ATTR, Reducer.class); } /** * Get the {@link OutputFormat} class for the job. * * @return the {@link OutputFormat} class for the job. */ @SuppressWarnings("unchecked") public Class<? extends OutputFormat<?,?>> getOutputFormatClass() throws ClassNotFoundException { return (Class<? extends OutputFormat<?,?>>) conf.getClass(OUTPUT_FORMAT_CLASS_ATTR, TextOutputFormat.class); } /** * Get the {@link Partitioner} class for the job. * * @return the {@link Partitioner} class for the job. */ @SuppressWarnings("unchecked") public Class<? extends Partitioner<?,?>> getPartitionerClass() throws ClassNotFoundException { return (Class<? extends Partitioner<?,?>>) conf.getClass(PARTITIONER_CLASS_ATTR, HashPartitioner.class); } /** * Get the {@link RawComparator} comparator used to compare keys. * * @return the {@link RawComparator} comparator used to compare keys. */ public RawComparator<?> getSortComparator() { return conf.getOutputKeyComparator(); } /** * Get the pathname of the job's jar. * @return the pathname */ public String getJar() { return conf.getJar(); } /** * Get the user defined {@link RawComparator} comparator for * grouping keys of inputs to the reduce. * * @return comparator set by the user for grouping values. * @see Job#setGroupingComparatorClass(Class) for details. */ public RawComparator<?> getGroupingComparator() { return conf.getOutputValueGroupingComparator(); } }
OutputFormat 主要用于描述输出数据的格式,它能够将用户提供的 key/value 对写入特定格式的文件中。 本文将介绍 Hadoop 如何设计 OutputFormat 接口 , 以及一些常用的OutputFormat 实现。 1.旧版 API 的 OutputFormat 解析 如图所示, 在旧版 API 中,OutputFormat 是一个接口,它包含两个方法: RecordWriter<K, V> getRecordWriter(FileSystem ignored, JobConf job, String name, Progressable progress) throws IOException; void checkOutputSpecs(FileSystem ignored, JobConf job) throws IOException; checkOutputSpecs 方法一般在用户作业被提交到 JobTracker 之前, 由 JobClient 自动调用,以检查输出目录是否合法。 getRecordWriter 方法返回一个 RecordWriter 类对象。 该类中的方法 write 接收一个key/value 对, 并将之写入文件。在 Task 执行过程中, MapReduce 框架会将 map() 或者reduce() 函数产生的结果传入 write 方法, 主要代码(经过简化)如下。假设用户编写的 map() 函数如下: public void map(Text key, Text value, OutputCollector<Text, Text> output, Reporter reporter) throws IOException { // 根据当前 key/value 产生新的输出 <newKey, newValue>, 并输出 …… output.collect(newKey, newValue); } 则函数 output.collect(newKey, newValue) 内部执行代码如下: RecordWriter<K, V> out = job.getOutputFormat().getRecordWriter(...); out.write(newKey, newValue); Hadoop 自带了很多OutputFormat 实现, 它们与 InputFormat 实现相对应,具体如图所示。所有基于文件的 OutputFormat 实现的基类为 FileOutputFormat, 并由此派生出一些基于文本文件格式、 二进制文件格式的或者多输出的实现。 为了深入分析OutputFormat的实现方法,选取比较有代表性的FileOutputFormat类进行分析。同介绍 InputFormat 实现的思路一样,我们先介绍基类FileOutputFormat,再介绍其派生类 TextOutputFormat。基类 FileOutputFormat 需要提供所有基于文件的 OutputFormat 实现的公共功能,总结起来,主要有以下两个:(1) 实现 checkOutputSpecs 接口 该接口 在作业运行之前被调用, 默认功能是检查用户配置的输出目 录是否存在,如果存在则抛出异常,以防止之前的数据被覆盖。(2) 处理 side-effect file 任 务的 side-effect file 并不是任务的最终输出文件,而是具有特殊用途的任务专属文件。 它的典型应用是执行推测式任务。 在 Hadoop 中,因为硬件老化、网络故障等原因,同一个作业的某些任务执行速度可能明显慢于其他任务,这种任务会拖慢整个作业的执行速度。为了对这种“ 慢任务” 进行优化, Hadoop 会为之在另外一个节点上启动一个相同的任务,该任务便被称为推测式任务,最先完成任务的计算结果便是这块数据对应的处理结果。为防止这两个任务同 时往一个输出 文件中 写入数据时发生写冲突, FileOutputFormat会为每个 Task 的数据创建一个 side-effect file,并将产生的数据临时写入该文件,待 Task完成后,再移动到最终输出目 录中。 这些文件的相关操作, 比如创建、删除、移动等,均由 OutputCommitter 完成。它是一个接口,Hadoop 提供了默认实现 FileOutputCommitter,用户也可以根据自己的需求编写 OutputCommitter 实现, 并通过参数 {mapred.output.committer.class} 指定。OutputCommitter 接口定义以及 FileOutputCommitter 对应的实现如表所示。 表-- OutputCommitter 接口定义以及 FileOutputCommitter 对应的实现 方法 何时被调用 FileOutputCommitter 实现 setupJob 作业初始化 创建临时目录 ${mapred.out.dir} /_temporary commitJob 作业成功运行完成 删除临时目录,并在${mapred.out.dir} 目录下创建空文件_SUCCESS abortJob 作业运行失败 删除临时目录 setupTask 任务初始化 不进行任何操作。原本是需要在临时目录下创建 side-effect file 的,但它是用时创建的(create on demand) needsTaskCommit 判断是否需要提交结果 只要存在side-effect file,就返回 true commitTask 任务成功运行完成 提交结果, 即将 side-effect file 移动到 ${mapred.out.dir} 目录下 abortTask 任务运行失败 删除任务的 side-effect file注意默认情况下,当作业成功运行完成后,会在最终结果目录 ${mapred.out.dir} 下生成 注意:默认情况下,当作业成功运行完成后,会在最终结果目录 ${mapred.out.dir} 下生成空文件 _SUCCESS。该文件主要为高层应用提供作业运行完成的标识,比如,Oozie 需要通过检测结果目 录下是否存在该文件判 断作业是否运行完成。 2. 新版 API 的 OutputFormat 解析 如图所示,除了接口变为抽象类外,新 API 中的 OutputFormat 增加了一个新的方法:getOutputCommitter,以允许用户自 己定制合适的 OutputCommitter 实现。
InputFormat 主要用于描述输入数据的格式, 它提供以下两个功能。 数据切分:按照某个策略将输入数据切分成若干个 split, 以便确定 Map Task 个数以及对应的 split。 为 Mapper 提供输入数据: 给定某个 split, 能将其解析成一个个 key/value 对。 本文将介绍 Hadoop 如何设计 InputFormat 接口,以及提供了哪些常用的 InputFormat实现。 1 .旧版 API 的 InputFormat 解析 如图所示: 在旧版 API 中, InputFormat 是一个接口 , 它包含两种方法: InputSplit[] getSplits(JobConf job, int numSplits) throws IOException; RecordReader<K, V> getRecordReader(InputSplit split, JobConf job, Reporter reporter) throws IOException; getSplits 方法主要完成数据切分的功能, 它会尝试着将输入数据切分成 numSplits 个InputSplit。 InputSplit 有以下两个特点。逻辑分片:它只是在逻辑上对输入数据进行分片, 并不会在磁盘上将其切分成分片进行存储。 InputSplit 只记录了分片的元数据信息,比如起始位置、长度以及所在的 节点列表等。可序列化:在 Hadoop 中,对象序列化主要有两个作用:进程间通信和永久存储。 此处,InputSplit 支持序列化操作主要是为了进程间通信。 作业被提交到 JobTracker 之前,Client 会调用作业 InputFormat 中的 getSplits 函数, 并将得到的 InputSplit 序列化到文件中。这样,当作业提交到 JobTracker 端对作业初始化时,可直接读取该文件,解析出所有 InputSplit, 并创建对应的 MapTask。 getRecordReader 方法返回一个RecordReader 对象,该对象可将输入的 InputSplit解析成若干个 key/value 对。 MapReduce 框架在 MapTask 执行过程中,会不断调用RecordReader 对象中的方法, 迭代获取 key/value 对并交给 map() 函数处理, 主要代码(经过简化)如下: //调用 InputSplit 的 getRecordReader 方法获取 RecordReader<K1, V1> input …… K1 key = input.createKey(); V1 value = input.createValue(); while (input.next(key, value)) { //调用用户编写的 map() 函数 } input.close(); 前面分析了 InputFormat 接口的定义, 接下来介绍系统自带的各种 InputFormat 实现。为了方便用户编写 MapReduce 程序, Hadoop 自带了一些针对数据库和文件的 InputFormat实现, 具体如图所示。通常而言用户需要处理的数据均以文件形式存储到 HDFS 上,所以这里重点针对文件的 InputFormat 实现进行讨论。 如 图所示, 所有基于文件的 InputFormat 实现的基类是 FileInputFormat, 并由此派生出针对文本文件格式的 TextInputFormat、 KeyValueTextInputFormat 和 NLineInputFormat,针对二进制文件格式的 SequenceFileInputFormat 等。 整个基于文件的 InputFormat 体系的设计思路是,由公共基类FileInputFormat 采用统一的方法 对各种输入文件进行切分,比如按照某个固定大小等分,而由各个派生 InputFormat 自己提供机制将进一步解析InputSplit。 对应到具体的实现是,基类 FileInputFormat 提供 getSplits 实现, 而派生类提供getRecordReader 实现。 为了深入理解这些 InputFormat 的实现原理, 选取extInputFormat 与SequenceFileInputFormat 进行重点介绍。 首先介绍基类FileInputFormat的实现。它最重要的功能是为各种 InputFormat 提供统一的getSplits 函数。该函数实现中最核心的两个算法是文件切分算法和 host 选择算法。(1) 文件切分算法 文件切分算法主要用于确定 InputSplit 的个数以及每个 InputSplit 对应的数据段。FileInputFormat 以文件为单位切分生成 InputSplit。 对于每个文件, 由以下三个属性值确定其对应的 InputSplit 的个数。 goalSize : 它是根据用户期望的 InputSplit 数目计算出来的, 即 totalSize/numSplits。其中, totalSize 为文件总大小; numSplits 为用户设定的 MapTask 个数, 默认情况下是 1。 minSize: InputSplit 的最小值, 由配置参数 mapred.min.split.size 确定, 默认是 1。 blockSize: 文件在 HDFS 中存储的 block 大小, 不同文件可能不同, 默认是 64 MB。这三个参数共同决定 InputSplit 的最终大小, 计算方法如下:splitSize = max{minSize, min{goalSize, blockSize}} 一旦确定 splitSize 值后, FileInputFormat 将文件依次切成大小为 splitSize 的 InputSplit,最后剩下不足 splitSize 的数据块单独成为一个 InputSplit。 【实 例】 输入目录下有三个文件 file1、file2 和 file3,大小依次为 1 MB,32 MB 和250 MB。 若 blockSize 采用 默认值 64 MB, 则不同 minSize 和 goalSize 下, file3 切分结果如表所示(三种情况下, file1 与 file2 切分结果相同, 均为 1 个 InputSplit)。 表-minSize、 goalSize、 splitSize 与 InputSplit 对应关系 minSize goalSize splitSize file3 对应的 InputSplit 数目 输入目 录对应的 InputSplit 总数 1 MB totalSize (numSplits=1 ) 64 MB 4 6 32 MB totalSize/5 50 MB 5 7 128 MB totalSize/2 128 MB 2 4 结合表和公式可以知道, 如果想让 InputSplit 尺寸大于 block 尺寸, 则直接增大配置参数 mapred.min.split.size 即可。(2) host 选择算法待 InputSplit 切分方案确定后,下一步要确定每个 InputSplit 的元数据信息。 这通常由四部分组成:<file, start, length, hosts>, 分别表示 InputSplit 所在的文件、起始位置、长度以及所在的 host(节点)列表。 其中,前三项很容易确定,难点在于 host 列表的选择方法。 InputSplit 的 host 列表选择策略直接影响到运行过程中的任务本地性。 HDFS 上的文件是以 block 为单位组织的,一个大文件对应的block 可能遍布整个 Hadoop 集群, 而 InputSplit 的划分算法可能导致一个 InputSplit 对应多个 block , 这些 block 可能位于不同节点上, 这使得 Hadoop 不可能实现完全的数据本地性。为此,Hadoop 将数据本地性按照代价划分成三个等级:node locality、rack locality 和 datacenter locality(Hadoop 还未实现该 locality 级别)。在进行任务调度时, 会依次考虑这 3 个节点的 locality, 即优先让空闲资源处理本节点上的数据,如果节点上没有可处理的数据,则处理同一个机架上的数据, 最差情况是处理其他机架上的数据(但是必须位于同一个数 据中心)。 虽 然 InputSplit 对应的 block 可能位于多个节点上, 但考虑到任务调度的效率,通常不会把所有节点加到 InputSplit 的 host 列表中,而是选择包含(该 InputSplit)数据总量最大的前几个节点(Hadoop 限制最多选择 10 个,多余的会过滤掉),以作为任务调度时判断任务是否具有本地性的主要凭证。为此,FileInputFormat 设计了一个简单有效的启发式算法 :首先按照 rack 包含的数据量对 rack 进行排序, 然后在 rack 内部按照每个 node 包含的数据量对 node 排序, 最后取前 N个node 的 host 作为InputSplit 的 host 列表, 这里的 N为 block副本数。这样,当任务调度器调度 Task 时,只要将 Task 调度给位于 host 列表的节点,就认为该 Task 满足本地性。 【实例】某个 Hadoop 集群的网络拓扑结构如图所示, HDFS中block 副本数为3,某个InputSplit 包含 3 个 block,大小依次是100、150 和 75,很容易计算,4 个rack 包 含的(该 InputSplit 的)数据量分别是175、250、150 和 75。rack2 中的 node3 和 node4,rack1 中的 node1 将被添加到该 InputSplit 的 host 列表中。 从以上 host 选择算法可知, 当 InputSplit 尺寸大于 block 尺寸时, Map Task 并不能实现完全数据本地性, 也就是说, 总有一部分数据需要从远程节点上读取, 因而可以得出以下结论: 当使用基于 FileInputFormat 实现 InputFormat 时, 为了提高 Map Task 的数据本地性,应尽量使 InputSplit 大小与 block 大小相同。 分 析完 FileInputFormat 实现方法, 接下来分析派生类 TextInputFormat 与 SequenceFileInputFormat 的实现。前面提到, 由派生类实现 getRecordReader 函数, 该函数返回一个 RecordReader 对象。它实现了类似于迭代器的功能, 将某个 InputSplit 解析成一个个 key/value 对。在具体实现时, RecordReader 应考虑以下两点: 定位记录边界:为了能够识别一条完整的记录,记录之间应该添加一些同步标识。对于 TextInputFormat, 每两条记录之间存在换行符;对于 SequenceFileInputFormat,每隔若干条记录会添加固定长度的同步字符串。 通过换行符或者同步字符串, 它们很容易定位到一个完整记录的起始位置。另外,由于FileInputFormat 仅仅按照数据量多少对文件进行切分, 因而 InputSplit 的第一条记录和最后一条记录可能会被从中间切开。 为了解决这种记录跨越 InputSplit 的读取问 题, RecordReader 规定每个InputSplit 的第一条不完整记录划给前一个 InputSplit 处理。 解析 key/value:定位到一条新的记录后, 需将该记录分解成 key 和 value 两部分。对于TextInputFormat, 每一行的内容即为 value,而该行在整个文件中的偏移量为key。对于 SequenceFileInputFormat, 每条记录的格式为: [record length] [key length] [key] [value] 其中, 前两个字段分别是整条记录的长度和 key 的长度, 均为 4 字节, 后两个字段分别是 key 和 value 的内容。 知道每条记录的格式后, 很容易解析出 key 和 value。 2. 新版 API 的 InputFormat 解析 新版API的InputFormat 类图如图所示。新 API 与旧 API 比较,在形式上发生了较大变化,但仔细分析,发现仅仅是对之前的一些类进行了封装。 正如前面介绍的那样,通过封装,使接口的易用性和扩展性得以增强。 public abstract class InputFormat<K, V> { public abstract List<InputSplit> getSplits(JobContext context ) throws IOException, InterruptedException; public abstract RecordReader<K,V> createRecordReader(InputSplit split, TaskAttemptContext context ) throws IOException, InterruptedException; } 查看InputSplit.java文件源代码: public abstract class InputSplit { /** * 获取split的大小, 这样就能将输入的splits按照大小排序. * @return split的字节大小 * @throws IOException * @throws InterruptedException */ public abstract long getLength() throws IOException, InterruptedException; /** * 通过name获取那些及将定位的nodes列表,其中的数据为split准备 * 位置不必序列化 * @return a new array of the node nodes. * @throws IOException * @throws InterruptedException */ public abstract String[] getLocations() throws IOException, InterruptedException; } 此外, 对于基类 FileInputFormat, 新版 API 中有一个值得注意的改动 : InputSplit 划分算法不再考虑用户设定的 Map Task 个数, 而用 mapred.max.split.size( 记为 maxSize) 代替,即 InputSplit 大小的计算公式变为:splitSize = max{minSize, min{maxSize, blockSize}}
1.序列化 序列化是指将结构化对象转为字节流以便于通过网络进行传输或写入持久存储的过程。反序列化指的是将字节流转为结构化对象的过程。 在 Hadoop MapReduce 中, 序列化的主 要 作用有两个: 永久存储和进程间通信。为了能够读取或者存储 Java 对象, MapReduce 编程模型要求用户输入和输出数据中的 key 和 value 必须是可序列化的。 在 Hadoop MapReduce 中 , 使一个 Java 对象可序列化的方法是让其对应的类实现 Writable 接口 。 但对于 key 而言,由于它是数据排序的关键字, 因此还需要提供比较两个 key 对象的方法。 为此,key对应类需实现WritableComparable 接口 , 它的类如图: 在package org.apache.hadoop.io 中的WritableComparable.java文件中定义: public interface WritableComparable<T> extends Writable, Comparable<T> { } 再来看看Writable接口的定义: public interface Writable { /** * Serialize the fields of this object to <code>out</code>. * * @param out <code>DataOuput</code> to serialize this object into. * @throws IOException */ void write(DataOutput out) throws IOException; /** * Deserialize the fields of this object from <code>in</code>. * * <p>For efficiency, implementations should attempt to re-use storage in the * existing object where possible.</p> * * @param in <code>DataInput</code> to deseriablize this object from. * @throws IOException */ void readFields(DataInput in) throws IOException; } 可以很明显的看出,write(DataOutput out)方法的作用是将指定对象的域序列化为out相同的类型;readFields(DataInput in)方法的作用是将in对象中的域反序列化,考虑效率因素,实现接口的时候应该使用已经存在的对象存储。 DataInput接口定义源代码如下: public interface DataInput { void readFully(byte b[]) throws IOException; void readFully(byte b[], int off, int len) throws IOException; int skipBytes(int n) throws IOException; boolean readBoolean() throws IOException; byte readByte() throws IOException; int readUnsignedByte() throws IOException; short readShort() throws IOException; int readUnsignedShort() throws IOException; char readChar() throws IOException; int readInt() throws IOException; long readLong() throws IOException; float readFloat() throws IOException; double readDouble() throws IOException; String readLine() throws IOException; String readUTF() throws IOException; } 每个方法的含义差不多,具体可参见java jdk源码 DataOutput接口定义源代码如下: public interface DataOutput { void write(int b) throws IOException; void write(byte b[]) throws IOException; void write(byte b[], int off, int len) throws IOException; void writeBoolean(boolean v) throws IOException; void writeByte(int v) throws IOException; void writeShort(int v) throws IOException; void writeChar(int v) throws IOException; void writeInt(int v) throws IOException; void writeLong(long v) throws IOException; void writeFloat(float v) throws IOException; void writeDouble(double v) throws IOException; void writeBytes(String s) throws IOException; void writeChars(String s) throws IOException; void writeUTF(String s) throws IOException; } WritableComparable可以用来比较,通常通过Comparator . 在hadoop的Map-Reduce框架中任何被用作key的类型都要实现这个接口。 看一个例子: public class MyWritableComparable implements WritableComparable { // Some data private int counter; private long timestamp; public void write(DataOutput out) throws IOException { out.writeInt(counter); out.writeLong(timestamp); } public void readFields(DataInput in) throws IOException { counter = in.readInt(); timestamp = in.readLong(); } public int compareTo(MyWritableComparable w) { int thisValue = this.value; int thatValue = ((IntWritable)o).value; return (thisValue &lt; thatValue ? -1 : (thisValue==thatValue ? 0 : 1)); } } 2.Reporter 参数 Reporter 是 MapReduce 提供给应用程序的工具。 如图所示,应用程序可使用Reporter 中的方法报告完成进度(progress)、设定状态消息(setStatus 以及更新计数器( incrCounter)。 Reporter 是一个基础参数。 MapReduce 对外提供的大部分组件, 包括 InputFormat、Mapper 和 Reducer 等,均在其主要方法中添加了该参数。 3.回调机制 回调机制是一种常见的设计模式。它将工作流内的某个功能按照约定的接口暴露给外部使用者, 为外部使用者提供数据,或要求外部使用者提供数据。 Hadoop MapReduce 对外提供的 5 个组件( InputFormat、 Mapper、 Partitioner、 Reducer 和 OutputFormat) 实际上全部属于回调接口 。 当用户按照约定实现这几个接口后, MapReduce运行时环境会自 动调用它们。如图所示,MapReduce 给用户暴露了接口 Mapper, 当用户按照自己的应用程序逻辑实现自己的 MyMapper 后,Hadoop MapReduce 运行时环境会将输入数据解析成 key/value 对, 并调用 map() 函数迭代处理。
MapReduce 编程模型给出了其分布式编程方法,共分 5 个步骤: 1) 迭代(iteration)。遍历输入数据, 并将之解析成 key/value 对。 2) 将输入 key/value 对映射(map) 成另外一些 key/value 对。 3) 依据 key 对中间数据进行分组(grouping)。 4) 以组为单位对数据进行归约(reduce)。 5) 迭代。 将最终产生的 key/value 对保存到输出文件中。 MapReduce 将计算过程分解成以上 5 个步骤带来的最大好处是组件化与并行化。为了实现 MapReduce 编程模型, Hadoop 设计了一系列对外编程接口 。用户可通过实现这些接口完成应用程序的开发。 MapReduce 编程接口体系结构 MapReduce 编程模型对外提供的编程接口体系结构如图所示,整个编程模型位于应用程序层和 MapReduce 执行器之间,可以分为两层。第一层是最基本的 Java API,主要有 5个可编程组件,分别是 InputFormat、Mapper、Partitioner、Reducer 和 OutputFormat 。Hadoop 自带了很多直接可用的 InputFormat、Partitioner 和 OutputFormat,大部分情况下,用户只需编写 Mapper 和 Reducer 即可。 第二层是工具层,位于基本 Java API 之上,主要是为了方便用户编写复杂的 MapReduce 程序和利用其他编程语言增加 MapReduce 计算平台的兼容性而提出来的。在该层中,主要提供了 4 个编程工具包: JobControl:方便用户编写有依赖关系的作业, 这些作业往往构成一个有向图, 所以通常称为 DAG( Directed Acyclic Graph) 作业。 ChainReducer:方便用户编写链式作业, 即在 Map 或者 Reduce 阶段存在多个 Mapper,形式如下:[MAPPER+ REDUCER MAPPER*] Hadoop Streaming:方便用户采用非 Java 语言编写作业,允许用户指定可执行文件或者脚本作为 Mapper/Reducer。 Hadoop Pipes:专门为 C/C++ 程序员编写 MapReduce 程序提供的工具包。 新旧 MapReduce API 比较 从 0.20.0 版本开始, Hadoop 同 时提供了 新旧 两套 MapReduce API。 新 API 在旧 API基础上进行了封装, 使得其在扩展性和易用性方面更好。 新旧版 MapReduce API 的主要区别如下。 (1)存放位置 旧版 API 放在 org.apache.hadoop.mapred 包中, 而新版 API 则放在 org.apache.hadoop.mapreduce 包及其子包中。 (2)接口变为抽象类 接 口通常作为一种严格的“协议约束”。 它只有方法声明而没有方法实现,且要求所有实现类(不包括抽象类)必须实现接口中的每一个方法。接口的最大优点是允许一个类实现多个接口,进而实现类似 C++ 中的“多重继承”。抽象类则是一种较宽松的“约束协议”,它可为某些方法提供默认实现。 而继承类则可选择是否重新实现这些方法。正是因为这一点,抽象类在类衍化方面更有优势,也就是说,抽象类具有良好的向后兼容性,当需要为抽象类添加新的方法时,只要新添加的方法提供了默认实现, 用户之前的代码就不必修改了。 考虑到抽象类在API衍化方面的优势, 新 API 将 InputFormat、 OutputFormat、Mapper、 Reducer 和 Partitioner 由接口变为抽象类。 (3)上下文封装新版 API 将变量和函数封装成各种上下文(Context)类,使得 API 具有更好的易用性和扩展性。 首先,函数参数列表经封装后变短, 使得函数更容易使用;其次,当需要修改或添加某些变量或函数时,只需修改封装后的上下文类即可,用户代码无须修改,这样保证了向后兼容性,具有良好的扩展性。 图展示了新版 API 中树形的 Context 类继承关系。这些 Context 各自封装了一种实体的基本信息及对应的操作(setter 和 getter 函数),如 JobContext、TaskAttemptContext 分别封装了 Job 和 Task 的基本信息,TaskInputOutputContext 封装了Task 的各种输入输出 操作,MapContext 和 ReduceContext 分别封装了 Mapper 和 Reducer 对外的公共接口 。 除了以上三点不同之外,新旧 API 在很多其他细节方面也存在小的差别,具体将在接下来的内容中讲解。 由于新版和旧版 API 在类层次结构、编程接口名称及对应的参数列表等方面存在较大差别, 所以两种 API 不能兼容。但考虑到应用程序的向后兼容性,短时间内不会将旧 API 从 MapReduce 中去掉。即使在完全采用新 API 的 0.21.0/0.22.X 版本系列中,也仅仅将旧API 标注为过期(deprecated),用户仍然可以使用。
HDFS 架构 HDFS 是一个具有高度容错性的分布式文件系统, 适合部署在廉价的机器上。 HDFS 能提供高吞吐量的数据访问, 非常适合大规模数据集上的应用。HDFS 的架构如图所示, 总体上采用了 master/slave 架构, 主要由以下几个组件组成 :Client、 NameNode、 Secondary NameNode 和 DataNode。 下面分别对这几个组件进行介绍: (1) ClientClient(代表用 户) 通过与 NameNode 和 DataNode 交互访问 HDFS 中 的文件。 Client提供了一个类似 POSIX 的文件系统接口供用户调用。 (2) NameNode 整个Hadoop 集群中只有一个 NameNode。 它是整个系统的“ 总管”, 负责管理 HDFS的目录树和相关的文件元数据信息。 这些信息是以“ fsimage”( HDFS 元数据镜像文件)和“ editlog”(HDFS 文件改动日志)两个文件形式存放在本地磁盘,当 HDFS 重启时重新构造出来的。此外, NameNode 还负责监控各个 DataNode 的健康状态, 一旦发现某个DataNode 宕掉,则将该 DataNode 移出 HDFS 并重新备份其上面的数据。 (3) Secondary NameNodeSecondary NameNode 最重要的任务并不是为 NameNode 元数据进行热备份, 而是定期合并 fsimage 和 edits 日志, 并传输给 NameNode。 这里需要注意的是,为了减小 NameNode压力, NameNode 自己并不会合并fsimage 和 edits, 并将文件存储到磁盘上, 而是交由Secondary NameNode 完成。 (4) DataNode 一般而言, 每个 Slave 节点上安装一个 DataNode, 它负责实际的数据存储, 并将数据信息定期汇报给 NameNode。 DataNode 以固定大小的 block 为基本单位组织文件内容, 默认情况下 block 大小为 64MB。 当用户上传一个大的文件到 HDFS 上时, 该文件会被切分成若干个 block, 分别存储到不同的 DataNode ; 同时,为了保证数据可靠, 会将同一个block以流水线方式写到若干个(默认是 3,该参数可配置)不同的 DataNode 上。 这种文件切割后存储的过程是对用户透明的。 MapReduce 架构 同 HDFS 一样,Hadoop MapReduce 也采用了 Master/Slave(M/S)架构,具体如图所示。它主要由以下几个组件组成:Client、JobTracker、TaskTracker 和 Task。 下面分别对这几个组件进行介绍。 (1) Client用户编写的 MapReduce 程序通过 Client 提交到 JobTracker 端; 同时, 用户可通过 Client 提供的一些接口查看作业运行状态。 在 Hadoop 内部用“作业”(Job) 表示 MapReduce 程序。 一个MapReduce 程序可对应若干个作业,而每个作业会被分解成若干个 Map/Reduce 任务(Task)。 (2) JobTrackerJobTracker 主要负责资源监控和作业调度。JobTracker监控所有TaskTracker与作业的健康状况,一旦发现失败情况后,其会将相应的任务转移到其他节点;同时JobTracker 会跟踪任务的执行进度、资源使用量等信息,并将这些信息告诉任务调度器,而调度器会在资源出现空闲时,选择合适的任务使用这些资源。在 Hadoop 中,任务调度器是一个可插拔的模块,用户可以根据自己的需要设计相应的调度器。 (3) TaskTracker TaskTracker 会周期性地通过 Heartbeat 将本节点上资源的使用情况和任务的运行进度汇报给 JobTracker, 同时接收 JobTracker 发送过来的命令并执行相应的操作(如启动新任务、 杀死任务等)。TaskTracker 使用“slot” 等量划分本节点上的资源量。“slot” 代表计算资源(CPU、内存等)。一个Task 获取到一个slot 后才有机会运行,而Hadoop 调度器的作用就是将各个TaskTracker 上的空闲 slot 分配给 Task 使用。 slot 分为 Map slot 和 Reduce slot 两种,分别供 MapTask 和 Reduce Task 使用。 TaskTracker 通过 slot 数目(可配置参数)限定 Task 的并发度。 (4) TaskTask 分为 Map Task 和 Reduce Task 两种, 均由 TaskTracker 启动。 HDFS 以固定大小的 block 为基本单位存储数据, 而对于 MapReduce 而言, 其处理单位是 split。split 与 block 的对应关系如图所示。 split 是一个逻辑概念, 它只包含一些元数据信息, 比如数据起始位置、数据长度、数据所在节点等。它的划分方法完全由用户自己决定。 但需要注意的是,split 的多少决定了 Map Task 的数目 ,因为每个 split 会交由一个 Map Task 处理。 Map Task 执行过程如图所示。 由该图可知,Map Task 先将对应的 split 迭代解析成一个个 key/value 对,依次调用用户自定义的 map() 函数进行处理,最终将临时结果存放到本地磁盘上,其中临时数据被分成若干个 partition,每个 partition 将被一个Reduce Task 处理。 Reduce Task 执行过程如图所示。该过程分为三个阶段①从远程节点上读取MapTask中间结果(称为“Shuffle 阶段”);②按照key对key/value对进行排序(称为“ Sort 阶段”);③依次读取<key, value list>,调用用户自定义的 reduce() 函数处理,并将最终结果存到 HDFS 上(称为“ Reduce 阶段”)。 Hadoop MapReduce 作业的生命周期 假设用户编写了一个 MapReduce 程序,并将其打包成 xxx.jar 文件,然后使用 以下命令提交作业: $HADOOP_HOME/bin/hadoop jar xxx.jar \ -D mapred.job.name="xxx" \ -D mapred.map.tasks=3 \ -D mapred.reduce.tasks=2 \ -D input=/test/input \ -D output=/test/output 则该作业的运行过程如图所示: 这个过程分为以下 5 个步骤:步骤 1:作业提交与初始化。 用户提交作业后, 首先由 JobClient 实例将作业相关信息, 比如将程序 jar 包、作业配置文件、 分片元信息文件等上传到分布式文件系统( 一般为HDFS)上,其中,分片元信息文件记录了每个输入分片的逻辑位置信息。 然后 JobClient通过 RPC 通知 JobTracker。 JobTracker 收到新作业提交请求后, 由 作业调度模块对作业进行初始化:为作业创建一个 JobInProgress 对象以跟踪作业运行状况, 而 JobInProgress 则会为每个Task 创建一个 TaskInProgress 对象以跟踪每个任务的运行状态, TaskInProgress 可能需要管理多个“ Task 运行尝试”( 称为“ Task Attempt”)。步骤 2:任务调度与监控。 前面提到,任务调度和监控的功能均由 JobTracker 完成。TaskTracker 周期性地通过 Heartbeat 向 JobTracker 汇报本节点的资源使用 情况, 一旦出 现空闲资源, JobTracker 会按照一定的策略选择一个合适的任务使用该空闲资源, 这由任务调度器完成。 任务调度器是一个可插拔的独立模块, 且为双层架构, 即首先选择作业, 然后从该作业中选择任务, 其中,选择任务时需要重点考虑数据本地性。 此外,JobTracker 跟踪作业的整个运行过程,并为作业的成功运行提供全方位的保障。 首先, 当 TaskTracker 或者Task 失败时, 转移计算任务 ; 其次, 当某个 Task 执行进度远落后于同一作业的其他 Task 时,为之启动一个相同 Task, 并选取计算快的 Task 结果作为最终结果。步骤 3:任务运行环境准备。 运行环境准备包括 JVM 启动和资源隔 离, 均由TaskTracker 实现。 TaskTracker 为每个 Task 启动一个独立的 JVM 以避免不同 Task 在运行过程中相互影响 ; 同时,TaskTracker 使用了操作系统进程实现资源隔离以防止 Task 滥用资源。 步骤 4 :任务执行。 TaskTracker 为 Task 准备好运行环境后, 便会启动 Task。 在运行过程中, 每个 Task 的最新进度首先由 Task 通过 RPC 汇报给 TaskTracker, 再由 TaskTracker汇报给 JobTracker。 步骤 5 作业完成。 待所有 Task 执行完毕后, 整个作业执行成功。 PS:这篇文章出自《Hadoop技术内幕 深入理解MapReduce架构设计与实现原理》一书,并非原创,文章的目的为笔记使用,方便个人查看,高手勿喷!
1 . 查看一个基类或接口的派生类或实现类 在 Eclipse 中, 选中 某个基类或接口名称,右击,在弹出 的快捷菜单中选择“ Quick Type Hierarchy”, 可在新窗口中看到对应的所有派生类或实现类。 例如, 打开 src\mapred\ 目 录下 org.apache.hadoop.mapred 包中的 InputFormat.java 文件, 查看接口 InputFormat 的所有实现类 在 Eclipse 中查看 Hadoop 源代码中接口 InputFormat 的所有实现类 结果如下所示: 2. 查看函数的调用关系 在 Eclipse 中, 选中某个方法名称, 右击,在弹出的快捷菜单中选择“Open CallHierarchy”, 可在窗口“Call Hierarchy” 中看到所有调用该方法的函数。 例如,如 图所示,打开src\mapred\ 目录下org.apache.hadoop.mapred 包中的JobTracker.java 文件, 查看调用方法 initJob 的所有函数, 在 Eclipse 中查看 Hadoop 源代码中所有调用 JobTracker.java 中 initJob 方法的函数 Eclipse 列出所有调用 initJob 方法的函数 3. 快速查找类对象的相关信息 同前两个技巧类似, 选中类对象, 右击, 在弹出的快捷菜单中选择“ Open Declaration”,可跳转到类定义 ; 选择“ Quick Outline”, 可查看类所有的成员变量和成员方法。 这里就不再赘述 4. Hadoop 源代码组织结构 直接解压 Hadoop 压缩包后, 可看到图 1 -11 所示的目 录结构, 其中, 比较重要的目录有 src、 conf、 lib、 bin 等。 下面分别介绍这几个目录的作用: src:Hadoop源代码所在的目录。 最核心的代码所在子目 录分别 是 core、 hdfs 和mapred, 它们分别实现了 Hadoop 最重要的三个模块, 即基础公共库、 HDFS 实现和MapReduce 实现 conf:配置文件所在目 录。 Hadoop 的配置文件比较多, 其设计原则可概括为如下两点。 ○ 尽可能模块化,即每个重要模块拥有自己的配置文件,这样使得维护以及管理变得简单。 ○ 动静分离, 即将可动态加载的配置选项剥离出 来, 组成独立配置文件。 比如,Hadoop 1 .0.0 版本之前, 作业队列权限管理相关的配置选项被放在配置文件 mapredsite.xml 中, 而该文件是不可以动态加载的, 每次修改后必须重启 MapReduce。 但从 1 .0.0 版本开始, 这些配置选项被剥离放到独立配置文件 mapred-queue-acls.xml中, 该文件可以通过 Hadoop 命令行动态加载。 conf 目 录下最重要的配置文件有core-site.xml、 hdfs-site.xml 和 mapred-site.xml, 分别设置了 基础公共库 core、 分布式文件系统 HDFS 和分布式计算框架 MapReduce 的配置选项。 lib:Hadoop 运行时依赖的三方库, 包括编译好的 jar 包以及其他语言生成的动态库。Hadoop 启动或者用户提交作业时, 会自动加载这些库。 bin:运行以及管理 Hadoop 集群相关的脚本。 这里介绍几个常用的脚本。 ○ hadoop:最基本且功能最完备的管理脚本,其他大部分脚本都会调用该脚本。 ○ start-all.sh/stop-all.sh:启动 / 停止所有节点上的 HDFS 和 MapReduce 相关服务。 ○ start-mapred.sh/stop-mapred.sh:单独启动 / 停止 MapReduce 相关服务。 ○ start-dfs.sh/stop-dfs.sh:单独启动 / 停止 HDFS 相关服务。 下面就 Hadoop MapReduce 源代码组织结构进行介绍。 Hadoop MapReduce 源代码组织结构,如图所示: 总 体上看, Hadoop MapReduce 分为两部分: 一部分是 org.apache.hadoop.mapred.*, 这里面主要包含旧的对外编程接口 以及 MapReduce 各个服务( JobTracker 以及 TaskTracker)的实现 ; 另一部分是 org.apache.hadoop.mapreduce.*, 主要内容涉及新版本的对外编程接口以及一些新特性( 比如 MapReduce 安全)。 1 . MapReduce 编程模型相关 org.apache.hadoop.mapred.lib.* : 这一系列 Java 包提供了各种可直接在应用程序中使用的 InputFormat、 Mapper、 Partitioner、 Reducer 和 OuputFormat, 以减少用户编写MapReduce 程序的工作量。 org.apache.hadoop.mapred.jobcontrol : 该 Java 包允许用 户管理具有相互依赖关系的作业(DAG 作业)。 org.apache.hadoop.mapred.join:该Java包实现了map-side join 算法 。 该算法要求数据已经按照 key 排好序,且分好片,这样可以只使用Map Task实现join算法, 避免 re-partition、 sort、 shuffling 等开销。 org.apache.hadoop.mapred.pipes: 该 Java 包允许用户用 C/C++ 编写 MapReduce 作业。 org.apache.hadoop.mapreduce: 该 Java 包定义了一套新版本的编程接口 , 这套接口比旧版接口封装性更好。 org.apache.hadoop.mapreduce.*:这一系列 Java 包根据新版接口实现了各种InputFormat、 Mapper、 Partitioner、 Reducer 和 OuputFormat。 2. MapReduce 计算框架相关 org.apache.hadoop.mapred:Hadoop MapReduce 最核心的实现代码, 包括各个服务的 具体实现。 org.apache.hadoop.mapred.filecache:Hadoop DistributedCache 实现。 DistributedCache是 Hadoop 提供的数据分发工具, 可将用 户 应用 程序中 需要的文件分发到各个节点上。 org.apache.hadoop.mapred.tools:管理控制 Hadoop MapReduce, 当 前功能仅包括允许用户动态更新服务级别的授权策略和 ACL( 访问权限控制) 属性。 org.apache.hadoop.mapreduce.split:该 Java 包的主要功能是根据作业的 InputFormat生成相应的输入 split。 org.apache.hadoop.mapreduce.server.jobtracker:该 Java 包维护了 JobTracker 可看到的 TaskTracker 状态信息和资源使用情况。 org.apache.hadoop.mapreduce.server.tasktracker.*:TaskTracker 的一些辅助类。 3. MapReduce 安全机制相关 这里只涉及 org.apache.hadoop.mapreduce.security.*。这一系列 Java 包实现了 MapReduce 安全机制。
Hadoop中有一套Writable实现可以满足大部分需求,但是在有些情况下,我们需要根据自己的需要构造一个新的实现,有了定制的Writable,我们就可以完全控制二进制表示和排序顺序。 为了演示如何新建一个定制的writable类型,我们需要写一个表示一对字符串的实现: blic class TextPair implements WritableComparable<TextPair> { private Text first; private Text second; public TextPair() { set(new Text(), new Text()); } public TextPair(String first, String second) { set(new Text(first), new Text(second)); } public TextPair(Text first, Text second) { set(first, second); } public void set(Text first, Text second) { this.first = first; this.second = second; } public Text getFirst() { return first; } public Text getScond() { return second; } public void write(DataOutput out) throws IOException { first.write(out); second.write(out); } public void readFields(DataInput in) throws IOException { first.readFields(in); second.readFields(in); } public int hashCode() { return first.hashCode() * 163 + second.hashCode(); } public boolean equals(Object o) { if(o instanceof TextPair) { TextPair tp = (TextPair)o; return first.equals(tp.first) && second.equals(tp.second); } return false; } public String toString() { return first + "\t" + second; } public int compareTo(TextPair tp) { int cmp = first.compareTo(tp.first); if(cmp != 0) { return cmp; } return second.compareTo(tp.second); } } 为速度实现一个RawComparator 还可以进一步的优化,当作为MapReduce里的key,需要进行比较时,因为他已经被序列化,想要比较他们,那么首先要先反序列化成一个对象, 然后再调用compareTo对象进行比较,但是这样效率太低了,有没有可能可以直接比较序列化后的结果呢,答案是肯定的,可以。 RawComparator接口允许执行者比较流中读取的未被反序列化为对象的记录,从而省去了创建对象的所有的开销,其中,compare() 比较时需要的两个参数所对应的记录位于字节数组b1和b2指定开始位置s1和s2,记录长度为l1和l2,代码如下: public interface RawComparator<T> extends Comparator<T> { public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2); } 以IntWritable为例,它的RawComparator实现中,compare() 方法通过readInt()直接在字节数组中读入需要比较的两个整数,然后输出Comparable接口要求的比较结果。 值得注意的是,该过程中compare()方法避免使用IntWritable对象,从而避免了不必要的对象分配,相关代码如下: /** A Comparator optimized for IntWritable. */ public static class Comparator extends WritableComparator { public Comparator() { super(IntWritable.class); } public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) { int thisValue = readInt(b1, s1); int thatValue = readInt(b2, s2); return (thisValue<thatValue ? -1 : (thisValue==thatValue ? 0 : 1)); } } Writablecomparator是RawComparator对WritableComparable类的一个通用实现,它提供两个主要功能: 1、提供了一个RawComparator的compare()默认实现,该实现从数据流中反序列化要进行比较的对象,然后调用对象的compare()方法进行比较 2、它充当了RawComparator实例的一个工厂方法。例如,可以通过下面的代码获得IntWritable的RawComparator: RawComparator<IntWritable> comparator = WritableComparator.get(IntWritable.class); 我们只需要把EmploeeWritable的序列化后的结果拆成成员对象,然后比较成员对象即可: class Comparator extends WritableComparator { private static final Text.Comparator TEXT_COMPARATOR = new Text.Comparator(); public Comparator() { super(TextPair.class); } public int compara(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) { try { int firstL1 = WritableUtils.decodeVIntSize(b1[s1]) + readVInt(b1, s1); int firstL2 = WritableUtils.decodeVIntSize(b2[s2]) + readVInt(b2, s2); int cmp = TEXT_COMPARATOR.compare(b1, s1, firstL1, b2, s2, firstL2); if(cmp != 0) { return cmp; } return TEXT_COMPARATOR.compare(b1, s1 + firstL1, l1 - firstL1, b2, s2 + firstL2, l2 - firstL2); } catch(IOException e) { throw new IllegalArgumentException(e); } } } 定制comparators 有时候,除了默认的comparator,你可能还需要一些自定义的comparator来生成不同的排序队列,看一下下面这个示例: public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) { try { int firstL1 = WritableUtils.decodeVIntSize(b1[s1])+ readVInt(b1, s1); int firstL2 = WritableUtils.decodeVIntSize(b2[s2])+ readVInt(b2, s2); return TEXT_COMPARATOR.compare(b1, s1, firstL1, b2, s2, firstL2); } catch (IOException e) { throw new IllegalArgumentException(e); } } public int compare(WritableComparable a, WritableComparable b) { if(a instanceof Textpair && b instanceof TextPair) { return ((TextPair) a).first.compareTo(((TextPair) b).first); } return super.compare(a, b); }
hadoop中自带的org.apache.hadoop.io包中有广泛的writable类可供选择,它们形成下图所示的层次结构: java基本类型的Writable封装器 Writable类对java基本类型提供封装,short和char除外,所有的封装包含get()和set()两个方法用于读取或设置封装的值 java基本类型的Writable类 java原生类型 除char类型以外,所有的原生类型都有对应的Writable类,并且通过get和set方法可以他们的值。IntWritable和 LongWritable还有对应的变长VIntWritable和VLongWritable类。固定长度还是变长的选用类似与数据库中的char或者 vchar,在这里就不赘述了。 Text类型 Text类型使用变长int型存储长度,所以Text类型的最大存储为2G. Text类型采用标准的utf-8编码,所以与其他文本工具可以非常好的交互,但要注意的是,这样的话就和java的String类型差别就很多了。 检索的不同 Text的chatAt返回的是一个整型,及utf-8编码后的数字,而不是象String那样的unicode编码的char类型。 public void testTextIndex(){ Text text=new Text("hadoop"); Assert.assertEquals(text.getLength(), 6); Assert.assertEquals(text.getBytes().length, 6); Assert.assertEquals(text.charAt(2),(int)'d'); Assert.assertEquals("Out of bounds",text.charAt(100),-1); } Text还有个find方法,类似String里indexOf方法: public void testTextFind() { Text text = new Text("hadoop"); Assert.assertEquals("find a substring",text.find("do"),2); Assert.assertEquals("Find first 'o'",text.find("o"),3); Assert.assertEquals("Find 'o' from position 4 or later",text.find("o",4),4); Assert.assertEquals("No match",text.find("pig"),-1); } Unicode的不同 当uft-8编码后的字节大于两个时,Text和String的区别就会更清晰,因为String是按照unicode的char计算,而Text是按照字节计算。我们来看下1到4个字节的不同的unicode字符 4个unicode分别占用1到4个字节,u+10400在java的unicode字符重占用两个char,前三个字符分别占用1个char. 我们通过代码来看下String和Text的不同 import java.io.*; import org.apache.hadoop.io.*; import org.apache.hadoop.util.StringUtils; import junit.framework.Assert; public class textandstring { public static void string() throws UnsupportedEncodingException { String str = "\u0041\u00DF\u6771\uD801\uDC00"; Assert.assertEquals(str.length(), 5); Assert.assertEquals(str.getBytes("UTF-8").length, 10); Assert.assertEquals(str.indexOf("\u0041"), 0); Assert.assertEquals(str.indexOf("\u00DF"), 1); Assert.assertEquals(str.indexOf("\u6771"), 2); Assert.assertEquals(str.indexOf("\uD801\uDC00"), 3); Assert.assertEquals(str.charAt(0), '\u0041'); Assert.assertEquals(str.charAt(1), '\u00DF'); Assert.assertEquals(str.charAt(2), '\u6771'); Assert.assertEquals(str.charAt(3), '\uD801'); Assert.assertEquals(str.charAt(4), '\uDC00'); Assert.assertEquals(str.codePointAt(0), 0x0041); Assert.assertEquals(str.codePointAt(1), 0x00DF); Assert.assertEquals(str.codePointAt(2), 0x6771); Assert.assertEquals(str.codePointAt(3), 0x10400); } public static void text() { Text text = new Text("\u0041\u00DF\u6771\uD801\uDC00"); Assert.assertEquals(text.getLength(), 10); Assert.assertEquals(text.find("\u0041"), 0); Assert.assertEquals(text.find("\u00DF"), 1); Assert.assertEquals(text.find("\u6771"), 3); Assert.assertEquals(text.find("\uD801\uDC00"), 6); Assert.assertEquals(text.charAt(0), 0x0041); Assert.assertEquals(text.charAt(1), 0x00DF); Assert.assertEquals(text.charAt(3), 0x6771); Assert.assertEquals(text.charAt(6), 0x10400); } public static void main(String[] args) { // TODO Auto-generated method stub text(); try { string(); } catch(UnsupportedEncodingException ex) { } } } 这样一比较就很明显了。 1.String的length()方法返回的是char的数量,Text的getLength()方法返回的是字节的数量。 2.String的indexOf()方法返回的是以char为单元的偏移量,Text的find()方法返回的是以字节为单位的偏移量。 3.String的charAt()方法不是返回的整个unicode字符,而是返回的是java中的char字符 4.String的codePointAt()和Text的charAt方法比较类似,不过要注意,前者是按char的偏移量,后者是字节的偏移量 Text的迭代 在Text中对unicode字符的迭代是相当复杂的,因为与unicode所占的字节数有关,不能简单的使用index的增长来确定。首先要把 Text对象转换为java.nio.ByteBuffer对象,然后再利用缓冲区对Text对象反复调用bytesToCodePoint方法,该方法 能获取下一代码的位置,并返回相应的int值,最后更新缓冲区中的位置。通过bytesToCodePoint()方法可以检测出字符串的末尾,并返回 -1值。看一下示例代码: import java.io.*; import java.nio.ByteBuffer; import org.apache.hadoop.io.*; import org.apache.hadoop.util.StringUtils; import junit.framework.Assert; public class textandstring { public static void main(String[] args) { // TODO Auto-generated method stub Text t = new Text("\u0041\u00DF\u6771\uD801\uDC00"); ByteBuffer buf = ByteBuffer.wrap(t.getBytes(), 0, t.getLength()); int cp; while(buf.hasRemaining() && (cp = Text.bytesToCodePoint(buf)) != -1) { System.out.println(Integer.toHexString(cp)); } } } 运行结果: 41df677110400 Text的修改 除了NullWritable是不可更改外,其他类型的Writable都是可以修改的。你可以通过Text的set方法去修改去修改重用这个实例。 public void testTextMutability() { Text text = new Text("hadoop"); text.set("pig"); Assert.assertEquals(text.getLength(), 3); Assert.assertEquals(text.getBytes().length, 3); } 注意:在某些情况下,getBytes()方法返回的字节数组可能比getLength()函数返回的长度更长: import java.io.*; import java.nio.ByteBuffer; import org.apache.hadoop.io.*; import org.apache.hadoop.util.StringUtils; import junit.framework.Assert; public class textandstring { public static void main(String[] args) { // TODO Auto-generated method stub Text t = new Text("hadoop"); t.set(new Text("pig")); Assert.assertEquals(t.getLength(), 3); Assert.assertEquals(t.getBytes().length, 6); } } Text类并不像String类那样有丰富的字符串操作API,所以多数情况下,需要将Text对象转换成String对象。这一转换过程通过调用ToString()方法来实现
简介 序列化和反序列化就是结构化对象和字节流之间的转换,主要用在内部进程的通讯和持久化存储方面。 通讯格式需求 hadoop在节点间的内部通讯使用的是RPC,RPC协议把消息翻译成二进制字节流发送到远程节点,远程节点再通过反序列化把二进制流转成原始的信息。RPC的序列化需要实现以下几点: 1.压缩,可以起到压缩的效果,占用的宽带资源要小。 2.快速,内部进程为分布式系统构建了高速链路,因此在序列化和反序列化间必须是快速的,不能让传输速度成为瓶颈。 3.可扩展的,新的服务端为新的客户端增加了一个参数,老客户端照样可以使用。 4.兼容性好,可以支持多个语言的客户端 存储格式需求 表面上看来序列化框架在持久化存储方面可能需要其他的一些特性,但事实上依然是那四点: 1.压缩,占用的空间更小 2.快速,可以快速读写 3.可扩展,可以老格式读取老数据 4.兼容性好,可以支持多种语言的读写 Writable接口 Writable接口定义了两个方法: 一个将其状态写到DataOutput二进制流,另一个从DataInput二进制流读取其状态: package org.apache.hadoop.io; import java.io.*; public interface Writable { void write(DataOutput out) throws IOException; void readFields(DataInput in) throws IOException; } 我们再来看下Writable接口与序列化和反序列化是如何关联的: package org.apache.hadoop.io; import java.io.*; import org.apache.hadoop.util.StringUtils; import junit.framework.Assert; public class WritableExample { public static byte[] bytes = null; //将一个实现了Writable接口的对象序列化成字节流 public static byte[] serialize(Writable writable) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); DataOutputStream dataOut = new DataOutputStream(out); writable.write(dataOut); dataOut.close(); return out.toByteArray(); } //将字节流转化为实现了Writable接口的对象 public static byte[] deserialize(Writable writable, byte[] bytes) throws IOException { ByteArrayInputStream in = new ByteArrayInputStream(bytes); DataInputStream dataIn = new DataInputStream(in); writable.readFields(dataIn); dataIn.close(); return bytes; } public static void main(String[] args) { // TODO Auto-generated method stub try { IntWritable writable = new IntWritable(123); bytes = serialize(writable); System.out.println("After serialize " + bytes); Assert.assertEquals(bytes.length, 4); Assert.assertEquals(StringUtils.byteToHexString(bytes), "0000007b"); IntWritable newWritable = new IntWritable(); deserialize(newWritable, bytes); System.out.println("After deserialize " + bytes); Assert.assertEquals(newWritable.get(),123); } catch(IOException ex){ } } } Hadoop序列化机制中还包含另外几个重要的接口:WritableComparable、RawComparator 和 WritableComparator WritableComparable提供类型比较的能力,继承自Writable接口和Comparable接口,其中Comparable进行 类型比较。ByteWritable、IntWritable、DoubleWritable等java基本类型对应的Writable类型,都继承自 WritableComparable 效率在Hadoop中非常重要,因此Hadoop I/O包中提供了具有高效比较能力的RawComparator接口,其中RawComparator和WritableComparable的类图如下: WritableComparable和comparators IntWritable实现了WritableComparable,WritableComparable是Writable接口和java.lang.Comparable<T>的一个子接口。 package org.apache.hadoop.io; public interface WritableComparable <T> extends org.apache.hadoop.io.Writable, java.lang.Comparable<T> { } MapReduce在排序部分要根据key值的大小进行排序,因此类型的比较相当重要,RawComparator是Comparator的增强版 package org.apache.hadoop.io; public interface RawComparator <T> extends java.util.Comparator<T> { int compare(byte[] bytes, int i, int i1, byte[] bytes1, int i2, int i3); } 它可以做到,不先反序列化就可以直接比较二进制字节流的大小: package org.apache.hadoop.io; import java.io.*; import org.apache.hadoop.util.StringUtils; import junit.framework.Assert; public class ComparatorExample { public static byte[] serialize(Writable writable) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); DataOutputStream dataOut = new DataOutputStream(out); writable.write(dataOut); dataOut.close(); return out.toByteArray(); } public static void main(String[] args) { // TODO Auto-generated method stub RawComparator<IntWritable> comparator; IntWritable w1, w2; comparator = WritableComparator.get(IntWritable.class); w1 = new IntWritable(123); w2 = new IntWritable(32); if(comparator.compare(w1, w2) <= 0) System.exit(0); try { byte[] b1 = serialize(w1); byte[] b2 = serialize(w2); if(comparator.compare(b1, 0, b1.length, b2, 0, b2.length) <= 0) { System.exit(0); } } catch(IOException ex) { } } }
1、异常处理概述 从一个读取两个整数并显示商的例子: public static void main(String args[]) { Scanner input = new Scanner(System.in); System.out.print("Enter two integers: "); int number1 = input.nextInt(); int number2 = input.nextInt(); System.out.println(number1 + " / " + number2 + " is " + (number1 / number2)); } Enter two integers: 3 0 Exception in thread "main" java.lang.ArithmeticException: / by zeroat Main.main(Main.java:18) 解决的一个简单的办法是添加一个if语句来测试第二个数字: public class Main { public static void main(String args[]) { Scanner input = new Scanner(System.in); System.out.print("Enter two integers: "); int number1 = input.nextInt(); int number2 = input.nextInt(); if(number2 != 0) System.out.println(number1 + " / " + number2 + " is " + (number1 / number2)); else System.out.println("Divisor cannot be zero "); } } 为了演示异常处理的概念,包括如何创建、抛出、捕获以及处理异常,继续改写上面的程序如下: public class Main { public static void main(String args[]) { Scanner input = new Scanner(System.in); System.out.print("Enter two integers: "); int number1 = input.nextInt(); int number2 = input.nextInt(); try { if(number2 == 0) throw new ArithmeticException("Divisor cannot be zero"); System.out.println(number1 + " / " + number2 + " is " + (number1 / number2)); } catch(ArithmeticException ex) { System.out.println("Exception: an integer " + "cannot be divided by zero "); } System.out.println("Execution continues ..."); } } 2、异常处理的优势 改用方法来计算商: public class Main { public static int quotient(int number1, int number2) { if(number2 == 0) throw new ArithmeticException("Divisor cannot be zero"); return number1 / number2; } public static void main(String args[]) { Scanner input = new Scanner(System.in); System.out.print("Enter two integers: "); int number1 = input.nextInt(); int number2 = input.nextInt(); try { int result = quotient(number1, number2); System.out.println(number1 + " / " + number2 + " is " + result); } catch(ArithmeticException ex) { System.out.println("Exception: an integer " + "cannot be divided by zero "); } System.out.println("Execution continues ..."); } } 异常处理的优势就是将检测错误从处理错误中分离出来。 3、异常类型 4、关于异常处理的更多知识 java的异常处理模型基于三种操作:声明一个异常、抛出一个异常、捕获一个异常 声明异常 在方法中声明异常,就是在方法头中使用关键字throws,如下所示: public void myMethod throws Exception1,Exception2,……,ExceptionN 抛出异常 检测一个错误的程序可以创建一个正确的异常类型的实例并抛出它 实例: IllegalArgumentException ex = new IllegalArgumentException("Worng Argument"); throw ex; 或者直接: throw new IllegalArgumentException("Worng Argument"); 捕获异常 当抛出一个异常时,可以在try-catch中捕获和处理它: try { statements; } catch (exception1 ex1){ handler for exception1; } catch (exception1 ex2){ handler for exception2; } …… catch (exception1 exN){ handler for exceptionN; } 从异常中获取信息 可以采用Throwable类中的方法获取异常的信息 public class test { public static void main(String[] args) { try { System.out.println(sum(new int[]{1,2,3,4,5})); } catch(Exception ex) { ex.printStackTrace(); System.out.println(ex.getMessage()); System.out.println(ex.toString()); System.out.println("Trace Info Obtained from getBackTrace"); StackTraceElement[] traceElements = ex.getStackTrace(); for(int i = 0; i < traceElements.length; i++) { System.out.print("monthod " + traceElements[i].getMethodName()); System.out.println("(" + traceElements[i].getClassName()); System.out.println(traceElements[i].getLineNumber() + ")"); } } } private static int sum(int[] list) { int sum = 0; for(int i = 0; i <= list.length; i++) { sum += list[i]; } return sum; } } finally语句 无论异常是否出现,都希望执行某些代码,这时可以采取finally子句: public class test { public static void main(String[] args) { PrintWriter output = null; try { output = new PrintWriter("wu.txt"); output.println("wlecome tio java"); } catch(IOException ex) { ex.printStackTrace(); } finally { if(output != null) output.close(); } System.out.println("End of the program"); } }
3.1 public class test { public static void main(String[] args) { System.out.println("Enter a, b, c: "); Scanner input = new Scanner(System.in); double a = input.nextDouble(); double b = input.nextDouble(); double c = input.nextDouble(); double delta = b * b - 4 * a * c; double t = Math.pow(delta, 0.5); if(delta > 0) { double x1 = (-b + t) / 2; double x2 = (-b - t) / 2; System.out.println("The roots are " + x1 + " and " + x2); } else if (delta == 0) { System.out.println("The root is " + -b / (2 * a)); } else { System.out.println("The equation has no real roots"); } } } 3.2 public class test { public static void main(String[] args) { System.out.println("Enter an integer: "); Scanner input = new Scanner(System.in); int n = input.nextInt(); System.out.print("Is " + n + " an even number? "); if(n % 2 == 0) System.out.println("true"); else System.out.println("false"); } } 3.3 public class test { public static void main(String[] args) { System.out.print("Enter a, b, c, d, e, f: "); Scanner input = new Scanner(System.in); double a = input.nextDouble(); double b = input.nextDouble(); double c = input.nextDouble(); double d = input.nextDouble(); double e = input.nextDouble(); double f = input.nextDouble(); double fm = a * d - b * c; if(fm == 0) { System.out.println("The equation has no solution"); } else { System.out.println("a is " + ((e * d - b * f) / fm) + " and y is " + ((a * f - e * c) / fm)); } } } 3.4 public class test { public static void main(String[] args) { Scanner input = new Scanner(System.in); int a = (int)(Math.random() * 100); int b = (int)(Math.random() * 100); System.out.print("Enter the sum of the two integer(0~100): " + a + " and " + b + ": "); int c = input.nextInt(); if(c == a + b) System.out.println("True"); else System.out.println("False"); } } 3.5 public class test { public static int judge(int year, int month) { boolean leap; leap = (year % 4 ==0 && year % 100 != 0) || (year % 400 == 0); if(month == 2) { if(leap) return 29; else return 28; } else if(month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) { return 31; } else { return 30; } } public static void main(String[] args) { String[] months = {" ", "January","February","March","April", "May","June","July","August","September", "October","November","December"}; System.out.print("Please inpit month and year: "); Scanner input = new Scanner(System.in); int month = input.nextInt(); int year = input.nextInt(); System.out.println(months[month] + " " + year + " has " + judge(year, month) + " days"); } } 4.7 public class test { public static void main(String[] args) {double n = 10000; double s1, s2, t; s1 = s2 = 0; t = 1; final double rate = 0.05; for(int i = 1; i < 11; i++) { t *= (1 + rate); } s1 = n * t; System.out.println("s1 = " + s1); } } 4.16 public class test { public static void main(String[] args) { System.out.print("Enter a number: "); Scanner input = new Scanner(System.in); int n = input.nextInt(); int i = 2; while(true) { while(n % i == 0 && n != i) { System.out.print(i + ", "); n /= i; } i++; if(n == i) { System.out.println(i); break; } } } } 4.25 public class test { public static double countPi(int n) { double pi = 0; double t; int m=1; for(int i = 1; i < n; i++) { t=1.0/(2*i-1); t*=m; pi+=t; m*=-1; } pi *= 4; return pi; } public static void main(String[] args) { System.out.print("Enter a number: "); Scanner input = new Scanner(System.in); for(int i = 10000; i <= 100000; i++) { System.out.println("pi(" + i + ") = " + countPi(i));; } } } 4.27 public class test { public static boolean isLeapYear(int n) { return ((n % 4 == 0 && n % 100 != 0) || n % 400 == 0); } public static void main(String[] args) { int n = 0; for(int i = 2001; i < 2100; i++) { if(isLeapYear(i)) { n++; if(n % 11 == 0) { System.out.println("\n"); } else { System.out.print(i + " "); } } } } } 4.33 public class test { public static boolean test(int n) { int i, sum; int m = n / 2; sum = 0; for(i = 1; i <= m; i++) { if(n % i == 0) sum += i; } if(sum == n) return true; else return false; } public static void main(String[] args) { for(int i = 2; i < 10000; i++) { if(test(i)) System.out.print(i + "\n"); } } } 4.41 public class test { public static void main(String[] args) { int n, count , max, t; Scanner input = new Scanner(System.in); System.out.println("Enter a number: "); n = input.nextInt(); t = max = n; count = 0; while(t != 0) { if(t > max) { count = 1; max = t; } else { count++; } System.out.println("Enter a number: "); t = input.nextInt(); } System.out.println("max= " + max + ", count= " + count); } }
2.1 public class test { public static void main(String[] args) { Scanner input = new Scanner(System.in); double f, c; c = input.nextDouble(); f = (9.0/5)*c+32; System.out.println(f); } } 2.2 public class test { public static void main(String[] args) { double r, h; final double PI = 3.1415925; System.out.println("Enter the radius and length of a cylinder: "); Scanner input = new Scanner(System.in); r = input.nextDouble(); h = input.nextDouble(); System.out.println("The area is " + PI*r*r); System.out.println("The volume is " + PI*r*r*h); } } 2.3 public class test { public static void main(String[] args) { double f, m; Scanner input = new Scanner(System.in); System.out.println("Enter a value for feet: "); f = input.nextDouble(); System.out.println(f + " feet is " + 0.305 *f + " meters"); } } 2.4 public class test { public static void main(String[] args) { double p, k; Scanner input = new Scanner(System.in); System.out.println("Enter a number in pounds: "); p = input.nextDouble(); System.out.println(p + " pounds is " + 0.454 * p + " kilograms"); } } 2.6 public class test { public static void main(String[] args) { int n, sum, t; Scanner input = new Scanner(System.in); System.out.println("Enter a number between 0 and 1000: "); n = input.nextInt(); sum = 0; t = n % 10; while(t != 0) { sum += t; n /= 10; t = n % 10; } System.out.println("The sum of the digits is " + sum); } } 2.7 public class test { public static void main(String[] args) { int m = 0; int years, days, t; System.out.println("Enter the number of minutes: "); Scanner input = new Scanner(System.in); m = input.nextInt(); t = (m / 60) / 24; years = t / 365; days = t % 365; System.out.println(m + " minutes is approximately " + years + " years and " + days + "days."); } } 2.8 public class test { public static void main(String[] args) { int n; char c; Scanner input = new Scanner(System.in); System.out.print("Enter an ASCII code: "); n = input.nextInt(); c = (char)n; System.out.println("The character for ASCII code " + n + " is " + c); } } 2.11 public class test { public static void main(String[] args) { Scanner input = new Scanner(System.in); System.out.println("Enter employee's name: "); String name = input.next(); System.out.println("Enter number of hours worked in a week: "); float hours = input.nextFloat(); System.out.println("Enter hourly pay rate: "); float payRate = input.nextFloat(); System.out.println("Enter federal tax withholding rate: "); float ftwr = input.nextFloat(); System.out.println("Enter state tax withholding rate: "); float stwr = input.nextFloat(); System.out.println("Employee Name " + name); System.out.println("Hours Worked " + hours); System.out.println("Pay Rate: $" + payRate); System.out.println("Gross Pay: $" + hours * payRate); System.out.println("Deductions:"); System.out.println(" Federal Withholding (" + ftwr * 100 +"%): $" + payRate * ftwr); System.out.println(" State Withholding (" + stwr * 100 +"%): $" + payRate * stwr); System.out.println(" Total Deduction: $" + payRate * (ftwr + stwr); } } 2.12 public class test { public static void main(String[] args) { Scanner input = new Scanner(System.in); System.out.println("Enter balance and interest rate (e.g., 3 for 3%): "); double balance = input.nextDouble(); double rate = input.nextDouble(); System.out.printf("The interest is %.4f", balance * (rate / 1200)); } } 2.13 public class test { public static void main(String[] args) { Scanner input = new Scanner(System.in); //System.out.println("Enter balance and interest rate (e.g., 3 for 3%): "); System.out.print("Enter investment amount: "); double investmount = input.nextDouble(); System.out.print("Enter monthly interest rate: "); double rate = input.nextDouble(); System.out.print("Enter number of years: "); int year = input.nextInt(); double s = investmount * Math.pow((1 + rate / 100), (year * 12)); System.out.println("Accumulated value is " + s); } } 2.14 public class test { public static void main(String[] args) { Scanner input = new Scanner(System.in); System.out.print("Enter weigth in pounds: "); float weigth = input.nextFloat(); System.out.print("Enter heigth in inches: "); float height = input.nextFloat(); System.out.println("BMI is " + 0.45359237 * weigth / Math.pow(height * 0.0254, 2)); } } 2.15 public class test { public static void main(String[] args) { double t, s; s = t = 0; Scanner input = new Scanner(System.in); for(int i = 0; i < 6; i++) { s = (100 + t) * (1 + 0.00417); t = s; } System.out.println("After six months, result is: " + s); } } 2.16 public class test { public static void main(String[] args) { Scanner input = new Scanner(System.in); System.out.print("Enter the amount of water in kilogram: " ); double m = input.nextDouble(); System.out.print("Enter the initial temperature: " ); double it = input.nextDouble(); System.out.print("Enter the final temperature: " ); double ft = input.nextDouble(); System.out.println("The energy needed is " + m * (ft - it) * 4184); } } 2.17 public class test { public static void main(String[] args) { Scanner input = new Scanner(System.in); System.out.print("Enter the temperature in Fahrenheit: " ); double f = input.nextDouble(); System.out.print("Enter the wind miles per hour: "); double speed = input.nextDouble(); System.out.println("The wind chill index is " + (35.74 + 0.6215 * f - 35.75 * Math.pow(speed, 0.16) + 0.427 * f * Math.pow(speed, 0.16))); } } 2.18 public class test { public static void print() { System.out.print(" "); } public static void main(String[] args) { System.out.println("a b pow(a, b)"); for(int i = 1; i < 6; i++) { System.out.print(i); print(); System.out.print(i + 1); print(); System.out.println((int)Math.pow(i, i +1)); } } }
1.1 public class test { public static void main(String[] args) { System.out.println("Welcome to java"); System.out.println("Welcome to Computer Science"); System.out.println("Programming is fun"); } } 1.2 public class test { public static void main(String[] args) { for(int i = 0; i < 5; i++) { System.out.println("Welcome to Java"); } } } 1.3 public class test { public static void main(String[] args) { System.out.println(" J A V V A"); System.out.println(" J A A V V A A"); System.out.println("J J AAAAA V V AAAAA"); System.out.println(" JJ A A V A A"); } } 1.4 public class test { public static void main(String[] args) { System.out.println("a a^2 a^3"); System.out.println("1 1 1"); System.out.println("2 4 8"); System.out.println("3 9 27"); System.out.println("4 16 64"); } } 1.5 public class test { public static void main(String[] args) { double s = (9.5*4.5-2.5*3)/(45.5-3.5); System.out.println(s); //0.8392857142857143 } } 1.6 public class test { public static void main(String[] args) { int s=0; for(int i=0; i<10; ++i) { s+=i; } System.out.println(s); //45 } } 1.7 public class test { public static void main(String[] args) { double pi = 0; double t; int m=1; for(int i=1; i<12121358; i++) { t=1.0/(2*i-1); t*=m; pi+=t; m*=-1; } pi*=4; System.out.println(pi); //3.1415927360890374 } }