# 四、使用 Java API 来连接

客户端连接到 HBase 的本机应用编程接口是 Java 应用编程接口。该功能可以分为两部分——元数据和管理功能——它们连接到主服务器，以及数据访问功能，它们连接到区域服务器(我们将在第 8 章“HBase 的体系结构”中更深入地介绍这些服务器)。

除了确保端口打开(默认情况下，Zookeeper 为 2181，Master 为 60000，Region Servers 为 60020)之外，您不需要做任何事情来从服务器支持 Java API。

HBase 客户端包位于 Maven 存储库中，其中包含适用于所有已发布版本的 HBase 的 JAR 文件。在撰写本文时，最新的版本是 1.1.2(这是我在 Docker 容器中用于课程的版本)，但是 0.9x 版本很常见，在 Maven 中仍然可用。

![](img/00011.jpeg)注意:我在这里不会介绍使用 Maven 或 Java IDE 启动和运行，但是这本书的源代码包含一个 [NetBeans Java 项目](https://github.com/sixeyed/hbase-succinctly/tree/master/java)，其中包含使用 Maven 的示例代码。

HBase Java 客户端可在 Maven Central 存储库中获得，并且经过版本化，因此客户端版本号与服务器版本相匹配。对于 HBase 的当前版本(以及在 HBase-简洁地说是 Docker 容器中运行的版本)，我们需要对 org.apache.hbase.hbase-client 包的版本 1.1.2 的依赖，如代码清单 21 所示:

21:Maven HBase 客户端依赖

```java
          <dependency>
             <groupId>org.apache.hbase</groupId>
             <artifactId>hbase-client</artifactId>
             <version>1.1.2</version>
           </dependency>

```

使用 Java API，您可以从一个 Configuration 对象开始，该对象包含服务器的连接详细信息，当您为表或管理创建客户机对象时，可以使用该对象。当您创建一个配置对象时，默认情况下，它会在包含配置设置的正在运行的应用的资源中查找一个 hbase-site.xml 文件。

hbase-site.xml 配置文件也存在于服务器上，您可以将相同的内容用于客户端连接—它指定了服务器端口和 Zookeeper 仲裁地址等关键细节。代码清单 22 显示了站点文件的一些示例属性:

 22

```java
          <configuration>
              <property>
                  <name>hbase.cluster.distributed</name>
                  <value>true</value>
              </property>
              <property>
                  <name>hbase.master.port</name>
                  <value>60000</value>
              </property>
              ...
          </configuration>

```

Java 客户端只需要知道 Zookeeper 仲裁地址；它从 Zookeeper 获取主服务器和区域服务器地址。

![](img/00011.jpeg)注意:Zookeeper 将地址存储为主机名，而不是 IP，因此您需要确保运行 Java 客户端的机器可以访问区域服务器的主机名。如果您使用代码清单 6 中的 Docker run 命令，那么主机名将是 hbase，您应该在 hosts 文件中添加一行，将 hbase 关联到 127.0.0.1

您通过使用 ConnectionFactory 类创建一个 Connection 对象来连接到 HBase，该对象使用本地 hbase-site.xml 文件中的配置，如代码清单 23 所示:

 23:获得与 HBase 的连接

```java
          Configuration config = HBaseConfiguration.create();
          Connection connection = ConnectionFactory.createConnection(config);

```

您可以在代码中设置配置对象的属性，但是使用服务器的 XML 配置文件更容易管理。

连接对象很贵，应该重新使用。它们用于为 DML 和 DDL 操作创建表和管理对象。当数据访问完成时，连接对象应该关闭，通常在 finally 块中调用 close()。

使用 Connection 对象，您可以获得对特定表的引用，并可以使用该引用来读取和写入数据。Java API 在字节数组级别工作，因此您需要决定如何对数据进行编码(本机编码，或将所有值转换为字符串)，并对所有数据进行编码和解码。

HBase 客户端包中有帮助类，这简化了操作。代码清单 24 显示了如何从连接中获取一个 Table 对象，并使用 get 对象获取一整行:

 24:用 Java 读一行

```java
          Table access_logs = connection.getTable(TableName.valueOf("access-logs"));
          Get get = new Get(Bytes.toBytes("elton|jericho|201511"));
          Result result = access_logs.get(get);

```

请注意，表名是使用 table name 类创建的，行键是使用 bytes 实用程序类编码成字节的。当这段代码运行时，整行将在结果对象中，该对象包含完整的字节数组。

Result 类有一个 listCells()方法，它返回一个 Cell 对象列表；导航这些对象中的字节数组很麻烦，但是另一个助手类 CellUtil 简化了它。代码清单 25 显示了如何导航单元格数组，打印出每个单元格的列名和值:

 25:用 Java 读取单元格值

```java
          for (Cell cell : result.listCells()){
            System.out.println(Bytes.toString(CellUtil.cloneFamily(cell)) + ":" +     
                               Bytes.toString(CellUtil.cloneQualifier(cell)) + " = " +
                               Bytes.toString(CellUtil.cloneValue(cell)));
          }

          //output -            
          //t:1106 = 120
          //t:1107 = 650     

```

Get 类可用于从行中返回一组受限制的单元格。代码清单 26 展示了如何使用 addFamily()方法返回该行一个列族中的单元格，以及如何使用 addColumn()方法将响应限制为单个单元格。

同样，标识符需要字节数组，因此字节类用于编码字符串值:

 26:用 Java 读取特定单元格

```java
           get = new Get(Bytes.toBytes("elton|jericho|201511"));
           get.addFamily(Bytes.toBytes("t"));
           result = access_logs.get(get);           
           printCells(result);

           //output - single column family:          
           //t:1106 = 120
           //t:1107 = 650 

           get = new Get(Bytes.toBytes("elton|jericho|201511"));
           get.addColumn(Bytes.toBytes("t"), Bytes.toBytes("1106"));
           result = access_logs.get(get);           
           printCells(result);

           //output - single column:          
           //t:1106 = 120    

```

![](img/00010.jpeg)提示。如果您看到使用 HTable 类并直接用 Configuration 对象实例化它们的代码示例，则该代码在 API 中被否决。我正在使用的新方法是连接工厂、连接和表类。

要扫描某个范围的行，可以创建一个具有开始和(可选)停止行键边界的扫描对象，并将其传递给 Table 类上的 getScanner()方法。这将在服务器上创建扫描程序，并返回一个 ResultScanner 对象，您可以使用该对象来迭代行。

每次迭代都会返回一个 Result 对象，如代码清单 27 所示，其中我使用了一个辅助方法 printCells()来编写输出:

 27:用 Java 扫描行

```java
          Table access_logs = connection.getTable(TableName.valueOf("access-logs"));
           Scan scan = new Scan(Bytes.toBytes("elton|jericho|201510"),
                                Bytes.toBytes("elton|jericho|x"));
           ResultScanner scanner = access_logs.getScanner(scan);
           for (Result result : scanner) {
               printCells(result);
           }

           //output - three cells, two whole rows:
           //[elton|jericho|201510] t:2908 = 80
           //[elton|jericho|201511] t:1106 = 120
           //[elton|jericho|201511] t:1107 = 650

```

您可以通过指定扫描对象的属性来调整扫描仪性能:

设置缓存–指定要在服务器上缓存多少行。较大的缓存值意味着客户端可以以服务器内存为代价更快地迭代扫描仪。

setMaxResultSize–指定整个扫描仪应该返回的最大单元格数。用于验证大型表中数据子集的逻辑。

设置批处理–指定批处理每次迭代返回的最大单元格数。

请注意，扫描实例会随着扫描仪的迭代而修改，因此您应该为要执行的每次扫描创建新实例。除了原始的连接对象，客户机对象创建起来很便宜，不需要重用。

您还可以使用 addFamily()和 addColumn()方法限制扫描仪结果中的单元格，这两种方法的工作方式与 Get 类相同。

通过向扫描仪添加过滤器，您可以执行复杂的查询。所有的选择都发生在服务器端，但是您需要记住，虽然行扫描很快，但是列过滤比较慢，尤其是对于有很多列的宽表。

过滤器在 Java 应用编程接口中是强类型的，继承自抽象的过滤器基类。有许多不同用途的过滤器——包 org.apache.hadoop.hbase.filter 的 Javadoc 中的树视图是检查它们的好地方。

ValueFilter 是一个有用的例子；它通过将单元格的值与提供的比较运算符和另一个筛选器进行比较来筛选单元格。如果将单元格值存储为字符串，则可以过滤对匹配正则表达式的列值的响应，如代码清单 28 所示:

 28:用 Java 扫描和过滤行

```java
           scan = new Scan(Bytes.toBytes("elton|jericho|201510"),
                           Bytes.toBytes("elton|jericho|x"));
           scan.setFilter(new ValueFilter(CompareOp.EQUAL,
                           new RegexStringComparator("[5-9][0-9]0")));
           scanner = access_logs.getScanner(scan);
           for (Result result : scanner) {
               printCells(result);
           }

           //output - one cell:
           //[elton|jericho|201511] t:1107 = 650

```

ValueFilter 和 RegexStringComparator 的组合意味着只有当单元格的三位数在 500 到 990 之间，以零结尾时，它们才会包含在响应中。该过滤器适用于所有族中的所有列；不需要姓氏或限定符。

使用 Java API，您还可以使用 FilterList 对象组合许多过滤器，并指定包含条件，无论行必须匹配所有过滤器还是只匹配一个过滤器。

您可以组合列表中的任何筛选器；代码清单 29 显示了一个使用正则表达式过滤列限定符名称和单元格值的列表:

 29:用 Java 中的多个过滤器进行扫描

```java
           FilterList filterList = new  
            FilterList(FilterList.Operator.MUST_PASS_ALL);
           filterList.addFilter(new QualifierFilter(CompareOp.EQUAL,
                                 new RegexStringComparator("[0-9]{2}0[7-8]")));
           filterList.addFilter(new ValueFilter(CompareOp.EQUAL,
                                 new RegexStringComparator("[0-9]0")));
           scan = new Scan(Bytes.toBytes("elton|jericho|201510"),
                           Bytes.toBytes("elton|jericho|x"));
           scan.setFilter(filterList);
           scanner = access_logs.getScanner(scan);
           for (Result result : scanner) {
               printCells(result);
           }

           //output - two cells:
           //[elton|jericho|201510] t:2908 = 80
           //[elton|jericho|201511] t:1107 = 650

```

Java API 提供了基本的数据更新，很像 HBase Shell 中的 put 命令，但也增加了一些更灵活的功能。

Put 类是 Get 类的写等价类。在对相关的 Table 对象调用 put()方法进行更改之前，您可以为指定的行键实例化它，然后添加一个或多个列值，如代码清单 30 所示:

 30:用 Java 中的 Put 更新单元格

```java
          Table access_logs = connection.getTable(TableName.valueOf("access-logs"));
           Put log = new Put(Bytes.toBytes("elton|jericho|201511"));
           log.addColumn(Bytes.toBytes("t"),      //family
                         Bytes.toBytes("1621"),   //qualifier
                         Bytes.toBytes("340"));   //value
           access_logs.put(log);       

           //result - updated cell value:
           //t:1621 = 120

```

您可以向 Put 对象添加多个单元格值，这会自动在一行上设置多个值，addColumn()方法的重载允许您为单元格指定时间戳。

Put 对象也用在 Table 方法 checkAndPut()中，该方法对单元格进行条件更新。在进行更新之前，方法需要检查列名和单元格值。如果提供的值匹配，则自动执行 put 如果没有，则不会更改该行。

代码清单 31 显示了如何使用 checkAndPut()向行中添加新单元格，但前提是现有单元格(在该行中，或在表的另一行中)具有预期值。在这种情况下，我告诉 HBase 添加一列 t:1622，但前提是 t:1621 的值是 34000，而不是 34000，因此不应该进行更新:

 31:用 Java 有条件更新

```java
          Put newLog = new Put(Bytes.toBytes("elton|jericho|201511"));
           log.addColumn(Bytes.toBytes("t"),
                         Bytes.toBytes("1622"),
                         Bytes.toBytes("100"));
           access_logs.checkAndPut(Bytes.toBytes("elton|jericho|201511"),
                                   Bytes.toBytes("t"), //family
                                   Bytes.toBytes("1621"),
                                   Bytes.toBytes("34000"),
                                   newLog);

           //result - not updated, checked value doesn't match

```

代码清单 32 显示了运行两个 put 方法的结果。单元格 t:1621 的值为 340，因此没有添加新的单元格 t:1622:

 32:获取用 Java 更新的单元格

```java
          hbase(main):002:0> get 'access-logs', 'elton|jericho|201511'
          COLUMN                CELL                                                      
           t:1106               timestamp=1447703111745, value=120                       
           t:1107               timestamp=1447703111735, value=650                       
           t:1621               timestamp=1447709413579, value=340                        
          3 row(s) in 0.0730 seconds

```

Java 应用编程接口还允许您在一个批处理中对不同的行进行多次更新。相同的 Put 类用于定义更改，多个 Put 对象被添加到列表中。该列表与 Table 类上的 batch()方法一起使用，该方法在单个服务器调用中写入更新，如代码清单 33 所示:

 33:用 Java 批量更新单元格

```java
           List<Row> batch = new ArrayList<Row>();

           Put put1 = new Put(Bytes.toBytes("elton|jericho|201512"));
           put1.addColumn(Bytes.toBytes("t"),
                          Bytes.toBytes("0109"),
                          Bytes.toBytes("670"));       
           batch.add(put1);

           Put put2 = new Put(Bytes.toBytes("elton|jericho|201601"));
           put2.addColumn(Bytes.toBytes("t"),
                          Bytes.toBytes("0110"),
                          Bytes.toBytes("110"));       
           batch.add(put2);

           Put put3 = new Put(Bytes.toBytes("elton|jericho|201602"));
           put3.addColumn(Bytes.toBytes("t"),
                          Bytes.toBytes("0206"),
                          Bytes.toBytes("500"));       
           batch.add(put3);

           Table access_logs = connection.getTable(TableName.valueOf("access-logs"));
           Object[] results = new Object[batch.size()];   
           access_logs.batch(batch, results);

```

您可以在批处理中包含其他操作，因此您可以使用 Put 对象添加 Delete 对象。批处理可以包含 Get 对象以返回一组结果，但是批处理的顺序不能保证——因此，如果 Get 包含与 Put 相同的单元格，您可能会获得 Put 之前状态的数据。

在代码清单 34 中，执行该批处理的结果显示在来自 HBase Shell 的扫描命令中:

 34:从 Java 批处理更新中获取单元格

```java
          hbase(main):003:0> scan 'access-logs', {STARTROW => 'elton|jericho|201512'}
          ROW                   COLUMN+CELL                                              
           elton|jericho|201512 column=t:0109, timestamp=1447710255527, value=670        
           elton|jericho|201601 column=t:0110, timestamp=1447710255527, value=110        
           elton|jericho|201602 column=t:0206, timestamp=1447710255527, value=500        
          3 row(s) in 0.0680 seconds

```

![](img/00012.jpeg)提示:这是第一次提到在 HBase 中删除数据。我没有涉及它，因为我发现你很少这样做，但是你可以删除表格中的单个单元格和行。HBase 使用删除标记来标记已删除的值，而不是立即从磁盘中删除数据，因此删除操作很快。

Java API 是 HBase 最丰富的客户端界面，还有比我在这里介绍的更多的功能，包括递增计数器列、用于访问随机数据项的函数(对集成测试有用)以及一组管理操作。

Java 访问是直接到区域服务器(或管理功能的主服务器)，这是最有效的应用编程接口。它不需要在区域服务器上运行任何额外的 JVM，并且客户端是区域感知的，因此它直接从托管该区域的服务器请求数据。

Java 也是在服务器上扩展 HBase 的原生语言。协处理器是一个高级主题，我不会在本书中介绍，但它们在许多场景中都很有价值。你用 Java 编写一个协处理器，并把这个包提供给 HDFS 的区域服务器。然后可以在服务器上调用您的代码来响应数据操作，就像添加行或更改单元格值一样，类似于 SQL 数据库中的触发器。

即使您只在 Java 中工作，了解外部 HBase APIs 提供了什么以及它们是如何工作的也是很好的。在下一章中，我们将介绍节俭应用编程接口，它与 Java 功能集几乎相同，可以从许多客户端库中使用。我们将在 Python 中介绍如何使用节俭。