0%

HBase协处理器详解:原理、实践与加载方式

HBase 协处理器(Coprocessor)是 HBase 提供的高级特性,允许用户在 RegionServer 或 Master 节点上运行自定义代码,实现类似 “触发器” 或 “存储过程” 的功能。本文将深入解析协处理器的类型、使用场景、实战示例及加载方式,帮助开发者扩展 HBase 的功能边界。

协处理器核心概念

协处理器的设计目标是将计算逻辑下沉到数据存储节点,减少数据传输开销,同时扩展 HBase 的原生功能(如触发器、聚合计算等)。其核心价值在于:

  • 靠近数据计算:避免全表扫描后的数据传输,提升计算效率;
  • 功能扩展:实现 HBase 原生不支持的特性(如数据同步、自定义权限校验)。

协处理器类型

HBase 协处理器分为 ObserverEndpoint 两大类,适用场景不同:

Observer 协处理器(触发器模式)

Observer 类似数据库的 “触发器”,在特定事件发生时自动执行,如数据写入、表结构修改等。根据监听对象不同,分为以下三类:

类型 监听对象 典型场景
RegionObserver Region 级事件(如 Put、Get、Delete、Scan) 数据写入校验、二级索引同步、数据脱敏
MasterObserver Master 级事件(如建表、删表、Region 拆分) 表结构权限控制、DDL 操作审计
WALObserver 预写日志(WAL)事件(如日志写入、同步) 日志加密、自定义 WAL 存储

Endpoint 协处理器(存储过程模式)

Endpoint 类似 “存储过程”,允许通过自定义 RPC 方法扩展 HBase 的 API,支持在服务端执行聚合计算(如 COUNTSUM),减少客户端与服务端的数据传输。

阅读全文 »

HBase客户端API实战指南:从基础操作到高级功能

HBase 提供了丰富的 Java 客户端 API,用于实现数据的增删改查、过滤器查询和计数器等功能。本文基于实战场景,详细讲解 HBase 客户端 API 的核心用法,包括连接管理、数据操作、过滤器使用及计数器功能,帮助开发者快速上手。

环境准备与连接管理

在使用 HBase API 前,需确保项目引入 HBase 依赖(以 Maven 为例),并正确配置 HBase 连接信息(hbase-site.xml 需放在项目 classpath 下)。

依赖引入

1
2
3
4
5
<dependency>  
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>2.2.7</version> <!-- 与 HBase 集群版本一致 -->
</dependency>

连接初始化

HBase 客户端通过 Connection 对象管理与集群的连接,Admin 用于 DDL 操作,Table 用于 DML 操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.apache.hadoop.hbase.client.Admin;  
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.TableName;

public class HBaseClient {
// 全局连接对象(线程安全,推荐单例)
private static Connection conn;
// DDL 操作对象
private static Admin admin;
// DML 操作对象(针对 test 表)
private static Table table;

static {
try {
// 从配置文件加载连接信息(hbase-site.xml)
conn = ConnectionFactory.createConnection();
admin = conn.getAdmin();
// 指定操作的表名(需提前创建)
table = conn.getTable(TableName.valueOf("test"));
} catch (IOException e) {
e.printStackTrace();
}
}
}

核心数据操作 API

插入数据(Put)

使用 Put 类向表中插入或更新数据,需指定 RowKey、列族、列名和值(均为字节数组)。

方法实现
阅读全文 »

EasyExcel合并单元格:基于 AbstractMergeStrategy 的灵活实现

在生成 Excel 报表时,合并单元格是优化表格可读性的常用手段(如合并相同值的相邻单元格)。EasyExcel 并未直接提供合并单元格的 API,但通过其钩子机制(尤其是 AbstractMergeStrategy 抽象类),可灵活实现自定义合并逻辑。本文将详细讲解如何基于 AbstractMergeStrategy 实现单元格合并,并通过实例演示 “相同值自动合并” 的功能。

合并单元格的核心思路

EasyExcel 合并单元格的核心是通过 单元格钩子(CellWriteHandler) 监听单元格创建过程,在数据写入时动态判断是否需要合并。具体步骤:

  1. 记录当前单元格的值及其所在行索引;
  2. 与上一行同列单元格的值对比,若相同则标记为待合并区域;
  3. 当值发生变化或到达最后一行时,执行合并操作(通过 POI 的 Sheet.addMergedRegion 方法)。

AbstractMergeStrategy 是 EasyExcel 提供的合并策略抽象类,实现了 CellWriteHandler 接口,简化了合并逻辑的开发(只需重写 merge 方法)。

自定义合并策略:相同值自动合并

以下实现一个通用的合并策略:对指定字段(列),自动合并相邻的相同值单元格。

实现自定义合并策略类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import com.alibaba.excel.metadata.Head;  
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
* 自定义合并策略:自动合并指定字段的相邻相同值单元格
*/
public class SameValueMergeStrategy extends AbstractMergeStrategy {

// 需要合并的字段名(与实体类的 @ExcelProperty 字段对应)
private final List<String> mergeFieldNames;
// 数据总条数(用于判断是否为最后一行)
private final int totalRowCount;

// 存储上一行的数据:key=字段名,value=Pair(值, 行索引)
private final Map<String, RowData> lastRowDataMap = new HashMap<>();

// 内部类:存储行数据(值和行索引)
private static class RowData {
Object value; // 单元格值
int rowIndex; // 行索引(从 0 开始)

RowData(Object value, int rowIndex) {
this.value = value;
this.rowIndex = rowIndex;
}
}

public SameValueMergeStrategy(List<String> mergeFieldNames, int totalRowCount) {
this.mergeFieldNames = mergeFieldNames;
this.totalRowCount = totalRowCount;
}

@Override
protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
// relativeRowIndex:相对行索引(0 表示表头行,1 开始为数据行)
// 跳过表头行(只处理数据行)
if (relativeRowIndex == null || relativeRowIndex < 1) {
return;
}

// 当前数据行的绝对索引(0 开始,表头行占 1 行)
int currentRowIndex = relativeRowIndex;
// 当前字段名(从 Head 中获取)
String currentFieldName = head.getFieldName();

// 仅处理需要合并的字段
if (mergeFieldNames.contains(currentFieldName)) {
// 获取当前单元格的值
Object currentValue = getCellValue(cell);

// 从缓存中获取上一行同字段的数据
RowData lastRowData = lastRowDataMap.get(currentFieldName);

if (lastRowData == null) {
// 首次处理该字段,直接缓存当前行数据
lastRowDataMap.put(currentFieldName, new RowData(currentValue, currentRowIndex));
} else {
// 对比当前值与上一行值
if (!Objects.equals(currentValue, lastRowData.value)) {
// 值不同:合并上一段相同值的单元格(如果行数 >1)
mergeIfNeeded(sheet, lastRowData, currentRowIndex - 1, cell.getColumnIndex());
// 更新缓存为当前行数据
lastRowDataMap.put(currentFieldName, new RowData(currentValue, currentRowIndex));
} else if (currentRowIndex == totalRowCount) {
// 值相同且为最后一行:合并到最后一行
mergeIfNeeded(sheet, lastRowData, currentRowIndex, cell.getColumnIndex());
}
}
}
}

/**
* 执行合并操作(如果需要)
* @param sheet 工作表
* @param lastRowData 上一行数据
* @param endRowIndex 结束行索引
* @param columnIndex 列索引
*/
private void mergeIfNeeded(Sheet sheet, RowData lastRowData, int endRowIndex, int columnIndex) {
int startRowIndex = lastRowData.rowIndex;
// 只有当行数差 >0 时才合并(至少 2 行)
if (endRowIndex - startRowIndex > 0) {
CellRangeAddress mergeRegion = new CellRangeAddress(
startRowIndex, // 起始行
endRowIndex, // 结束行
columnIndex, // 起始列
columnIndex // 结束列(同列)
);
// 添加合并区域
sheet.addMergedRegion(mergeRegion);
}
}

/**
* 获取单元格的值(兼容不同数据类型)
*/
private Object getCellValue(Cell cell) {
if (cell == null) {
return "";
}
CellType cellType = cell.getCellType();
switch (cellType) {
case STRING:
return cell.getStringCellValue();
case NUMERIC:
// 处理数字和日期(简化处理,实际可根据需求优化)
return cell.getNumericCellValue();
case BOOLEAN:
return cell.getBooleanCellValue();
default:
return "";
}
}
}

实体类定义

假设需要合并 “部门” 列,实体类如下:

阅读全文 »

EasyExcel钩子(Handler)机制:深度定制 Excel 写入过程

EasyExcel 作为高效的 Excel 处理框架,不仅提供了简单的读写 API,还通过钩子机制(Handler) 允许开发者在 Excel 写入的关键节点(如 Workbook 创建、Sheet 初始化、行 / 单元格生成等)插入自定义逻辑。本文将详细解析 EasyExcel 中的四大钩子接口(Workbook、Sheet、Row、Cell),并通过实例演示如何利用钩子实现复杂需求(如自定义样式、数据校验、动态调整结构等)。

钩子机制的核心作用

钩子机制(Handler)是 EasyExcel 提供的扩展点,允许开发者在 Excel 写入的生命周期节点中嵌入自定义代码,实现以下功能:

  • 自定义样式(如表头加粗、奇数行变色、特定单元格高亮);
  • 数据校验(如单元格值超出范围时报错或自动修正);
  • 动态调整结构(如根据数据内容新增列、合并单元格);
  • 资源管理(如在 Workbook 关闭后释放临时资源)。

EasyExcel 将写入过程划分为四个层级的生命周期,对应四类钩子接口,层级关系如下:

1
Workbook(工作簿) → Sheet(工作表) → Row(行) → Cell(单元格)  

四大钩子接口详解

WorkbookWriteHandler:工作簿级钩子

作用于整个 Excel 工作簿的创建前后及销毁阶段,用于全局配置(如设置工作簿属性、加密文件等)。

核心方法
方法名 触发时机 用途示例
beforeWorkbookCreate 工作簿创建前 配置工作簿全局参数(如版本)
afterWorkbookCreate 工作簿创建后 设置工作簿加密密码
afterWorkbookDispose 工作簿所有操作完成并关闭后 释放全局资源

SheetWriteHandler:工作表级钩子

作用于单个 Sheet 的创建前后,用于配置 Sheet 特性(如设置默认列宽、隐藏 Sheet 等)。

核心方法
方法名 触发时机 用途示例
beforeSheetCreate Sheet 创建前 指定 Sheet 索引、名称
afterSheetCreate Sheet 创建后 设置默认列宽、冻结首行

RowWriteHandler:行级钩子

作用于行的创建前后及销毁阶段,用于行级定制(如行高调整、行样式统一等)。

核心方法
方法名 触发时机 用途示例
beforeRowCreate 行创建前 预设行高、判断是否需要跳过此行
afterRowCreate 行创建后 调整行高、设置行背景色
afterRowDispose 行所有操作完成后 行数据校验、记录行索引

CellWriteHandler:单元格级钩子

作用于单元格的创建、数据转换及销毁阶段,是最常用的钩子,用于单元格样式、数据处理等细节定制。

核心方法
方法名 触发时机 用途示例
beforeCellCreate 单元格创建前 预设单元格类型(文本 / 数字)
afterCellCreate 单元格创建后 设置单元格边框、对齐方式
afterCellDataConverted 单元格数据转换为 Excel 格式后 修正数据格式(如日期格式化)
afterCellDispose 单元格所有操作完成后 高亮特定值(如错误数据标红)

实战:自定义钩子实现复杂需求

以下通过三个典型案例,演示如何使用钩子接口解决实际问题。

案例 1:表头样式定制(使用 CellWriteHandler)

需求:表头文字加粗、背景色设为浅灰色,对齐方式居中。

阅读全文 »

Maven 上传项目至中央仓库:从申请到发布的完整指南

将自己开发的工具包上传至 Maven 中央仓库,能让全球开发者通过简单的依赖坐标直接使用,是开源贡献的重要一步。本文基于 Sonatype OSSRH(Open Source Software Repository Hosting)平台,详细讲解从账号注册到最终发布的全流程。

准备工作

必要条件

  • 一个公开的代码仓库(如 GitHub、Gitee),用于托管项目源码。
  • 一个可验证的域名(如个人域名或代码平台域名,用于 GroupId 验证)。
  • 安装 GPG(用于对发布文件进行签名,确保完整性)。

环境配置

  • GPG 安装

    • Windows:使用 Gpg4win
    • Mac:使用 GPG Suite
    • Linux:通过包管理器安装(如 sudo apt install gnupg)。

创建gpg

  • 生成 GPG 密钥
    打开终端执行以下命令,按提示输入姓名、邮箱和密码:

    1
    gpg --full-generate-key

    生成后,上传公钥至公共服务器(确保 Maven 能验证签名):

    1
    gpg --keyserver hkp://keyserver.ubuntu.com:80 --send-keys 你的密钥ID

    (密钥 ID 可通过 gpg --list-keys 查看,格式为 8 位十六进制数)

Sonatype 账号与项目申请

Maven 中央仓库通过 Sonatype OSSRH 管理开源项目,需先注册并申请项目托管。

注册 Sonatype 账号

访问 Sonatype JIRA 注册账号,填写用户名、邮箱等信息(建议使用常用邮箱,后续验证需要)。

注册地址:https://issues.sonatype.org/secure/Signup!default.jspa

注册页

创建项目发布申请(New Issue)

  1. 登录后点击 Create 按钮,选择:

    • ProjectCommunity Support - Open Source Project Repository Hosting
    • Issue TypeNew Project
  2. 填写申请信息:

    • Summary:项目名称(如 tools-java: 通用工具类库)。
    • Group Id:项目的 Maven GroupId(关键!需验证所有权)。
      • 若有个人域名(如 example.com),可设为 com.example
      • 若无域名,使用代码托管平台域名(如 Gitee 项目 gitee.com/yourname/tools-java,则 GroupId 为 io.gitee.yourname)。
    • Project URL:项目源码地址(如 https://gitee.com/yourname/tools-java)。
    • SCM URL:源码仓库地址(如 https://gitee.com/yourname/tools-java.git)。
    • Other Details:简要描述项目功能。

    新建项目

提交完成之后,会创建一个Issues

issue详细

验证 GroupId 所有权

提交申请后,Sonatype 工作人员会在 1-2 个工作日内回复,要求验证 GroupId 所有权:

阅读全文 »