小菜鸟

java菜鸟号正在起航

HBase 协处理器:在 RegionServer 上跑自定义代码,实现触发器、存储过程和二级索引

HBase 原生不支持触发器、不支持存储过程、不支持二级索引。但协处理器可以补上这些缺口。

协处理器是运行在 RegionServer 上的用户自定义代码——数据在哪儿,计算就在哪儿,不用把数据拉到客户端再算。

两种类型:

类型 类比 触发方式
Observer 数据库触发器 事件驱动(写数据前、写数据后、读数据时)
Endpoint 存储过程 主动调用(客户端发 RPC 到 RegionServer)

Observer:事件触发,自动执行

Observer 在特定事件发生时自动执行,适合”顺便做点事”的场景。

三种 Observer:

类型 监听什么 典型用途
RegionObserver Region 上的读写操作(Put/Get/Delete/Scan) 写入校验、二级索引同步、数据脱敏
MasterObserver Master 上的 DDL 操作(建表/删表/改表) 权限校验、操作审计
WALObserver WAL 日志写入事件 日志加密、自定义存储

一个最简单的 RegionObserver:在 Put 之后打印 RowKey

import org.apache.hadoop.hbase.Coprocessor;  
import org.apache.hadoop.hbase.TableName;  
import org.apache.hadoop.hbase.coprocessor.*;  
import org.apache.hadoop.hbase.util.Bytes;  
import org.apache.hadoop.hbase.client.Put;  
import org.apache.hadoop.hbase.wal.WALEdit;  
import java.io.IOException;  
import java.util.Optional;  

// 实现 RegionCoprocessor 和 RegionObserver 接口  
public class RegionObserverExample implements RegionCoprocessor, RegionObserver {  

    // 返回当前类作为 RegionObserver 实例  
    @Override  
    public Optional<RegionObserver> getRegionObserver() {  
        return Optional.of(this);  // 必须返回当前实例,否则无法触发  
    }  

    // 重写 postPut 方法:数据写入后执行  
    @Override  
    public void postPut(  
        ObserverContext<RegionCoprocessorEnvironment> c,  
        Put put,  
        WALEdit edit,  
        Durability durability  
    ) throws IOException {  
        // 从 Put 对象中获取 RowKey 并打印  
        String rowKey = Bytes.toString(put.getRow());  
        System.out.println("[PostPut] 写入的 RowKey: " + rowKey);  
    }  
}

常用 Observer 钩子方法:

方法 触发时机
prePut 写入前(可做校验,抛异常可阻止写入)
postPut 写入后(做索引同步等)
preGet 查询前
postGet 查询后
preDelete 删除前
postDelete 删除后

Endpoint:主动调用,服务端计算

Endpoint 是”存储过程”——客户端主动调用,在 RegionServer 上执行自定义逻辑,只把结果返回给客户端。

典型场景: 统计一张表有多少行数据。如果用客户端扫全表,数据量大的时候网络传输巨大。用 Endpoint 在服务端计数,只返回一个数字。

Endpoint 实现步骤(简版):

  1. 定义协议接口(继承 CoprocessorProtocol
  2. 实现服务端逻辑
  3. 客户端通过 Table.coprocessorService() 调用

抽象理解:

阅读全文 »

HBase Java API 实战:连接、增删改查、过滤器、计数器,一套代码全搞定

HBase 的 Java API 不复杂,核心就几个类:

管什么
Connection 连集群(线程安全,全局一个)
Admin 建表、删表、改表结构
Table 增删改查数据
Put / Get / Delete / Scan 各操作的参数封装

环境准备与连接管理

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

依赖引入

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

连接初始化:全局一个 Connection

Connection 是线程安全的,整个应用生命周期里用一个就行,不要每次操作都 new。

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 connection;
    private static Table table;
    
    static {
        try {
            // 自动加载 classpath 下的 hbase-site.xml
            connection = ConnectionFactory.createConnection();
            table = connection.getTable(TableName.valueOf("test"));
        } catch (IOException e) {
            throw new RuntimeException("HBase 连接初始化失败", e);
        }
    }
    
    // 如果操作多张表,用这个方法获取 Table 实例
    public static Table getTable(String tableName) throws IOException {
        return connection.getTable(TableName.valueOf(tableName));
    }
}

关键点: ConnectionFactory.createConnection() 会自动读取 hbase-site.xml,把它放在 src/main/resources 下就行。

插入数据(Put)

阅读全文 »

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

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

合并单元格的核心思路

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

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

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

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

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

实现自定义合并策略类

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 将写入过程划分为四个层级的生命周期,对应四类钩子接口,层级关系如下:

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 密钥
    打开终端执行以下命令,按提示输入姓名、邮箱和密码:

    gpg --full-generate-key

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

    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 所有权:

阅读全文 »
0%