前言

  • Hive:2.3.0
  • 由于实际生产环境中,Hive自带的内建函数无法覆盖所有的应用场景,所以时常需要进行自定义函数User-Defined Function(UDF),以满足实际生产需求。
  • 本文主要演示如何实现自定义表生成函数User-Defined Table-Generating Function(UDTF),此类函数的特点是一进多出
  • 创建Hive函数时,如果指定为临时的(temporary)则可以在所有数据库下使用,但只能在当前会话中使用,退出后自动删除;如果指定为持久的(permanent)则只能在指定数据库(默认当前数据库)下使用,但可以在其他会话中使用,退出后依旧保留
  • 官方参考文档1,官方参考文档2
  • 温馨提示:Hive自定义组件打包成jar包时,不要同时打包依赖,避免各种版本冲突,只将额外的依赖添加到classpath中即可

自定义UDTF函数的实现步骤

1)继承org.apache.hadoop.hive.ql.udf.generic.GenericUDTF

2)实现initializeprocessclose方法

3)打包成jar文件

4)在Hive CLI中将jar包添加到classpath

add jar <path_to_jar>

5)创建对应函数

create function <db_name>.<function_name> AS '<full_class_name>'

自定义UDTF实现Demo

自定义实现了UDTF函数split_explode,此函数的功能是,将传入的字符串按照指定的正则表达式拆分成字符串数组,然后形成多个列,类似于split函数和explode函数的功能组合,源码如下:

package com.tomandersen.hive.udtfs;

import org.apache.hadoop.hive.ql.exec.Description;
import org.apache.hadoop.hive.ql.exec.UDFArgumentException;
import org.apache.hadoop.hive.ql.metadata.HiveException;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDTF;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDTFExplode;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDTFJSONTuple;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDTFPosExplode;
import org.apache.hadoop.hive.serde.serdeConstants;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory;
import org.apache.hadoop.hive.serde2.objectinspector.StructField;
import org.apache.hadoop.hive.serde2.objectinspector.StructObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.StringObjectInspector;
import org.apache.hadoop.io.Text;

import java.util.ArrayList;
import java.util.List;

/**
 * <h3>Hive自定义表生成函数UDTF的实现Demo</h3>
 * 功能: 将字符串按照指定分隔符分割成独立的单词,并返回多行.
 * <p>
 * UDTF输入: 待分割字符串,分隔符正则表达式
 * UDTF输出: 分割后产生的单词列
 * <p>
 * 参考: {@link GenericUDTFJSONTuple},{@link GenericUDTFExplode},{@link GenericUDTFPosExplode}
 *
 * @author TomAndersen
 * @version 1.0
 * @date 2020/5/21
 */

// 用于生成Function document.可以使用desc function <function_name>命令查看函数使用文档.
@Description(
        name = "split_explode",
        value = "_FUNC_(str,regex) - Splits str around occurrences that match regex and " +
                "separates the elements into multiple rows."
)
public class CustomUDTFSplitAndExplode extends GenericUDTF {

    // 保存所有输入字段对应的 ObjectInspector
    private transient ObjectInspector[] inputFieldIOs;
    // 保存输出列个数
    private int numCols;
    // 保存输出列
    private transient Text[] retCols;
    // 保存空输出列,用于在特殊情况输出
    private transient Object[] nullCols;
    // 保存输入参数中的正则表达式
    private String regex;
    // 保存正则表达式regex是否已经获取
    private boolean regexParsed = false;


    /**
     * 此方法一般用于检查输入参数是否合法,每个实例只会调用一次.
     * 一般用于用于检查UDTF函数的输入参数,以及生成一个用于查看新生成字段数据的StructObjectInspector.
     *
     * @param argOIs {@link StructObjectInspector},传入的是所有输入参数字段的查看器
     * @return {@link StructObjectInspector},返回的是新生成字段的查看器
     * @date 2020/5/22
     */
    @Override
    public StructObjectInspector initialize(StructObjectInspector argOIs) throws UDFArgumentException {
        // 1.输入参数检查
        List<? extends StructField> inputFields = argOIs.getAllStructFieldRefs();// 获取所有的输入参数字段
        int argsLength = inputFields.size();

        inputFieldIOs = new ObjectInspector[argsLength];// 获取所有输入参数字段对应的检查器(Inspector)
        for (int i = 0; i < argsLength; i++) {
            inputFieldIOs[i] = inputFields.get(i).getFieldObjectInspector();
        }

        if (argsLength != 2) {// 检查参数个数
            throw new UDFArgumentException("split_explode(str,regex) takes only two arguments: " +
                    "the string and regular expression");
        }

        for (int i = 0; i < argsLength; ++i) {// 检查参数类型
            if (inputFieldIOs[i].getCategory() != ObjectInspector.Category.PRIMITIVE ||
                    !inputFieldIOs[i].getTypeName().equals(serdeConstants.STRING_TYPE_NAME)) {
                throw new UDFArgumentException("split_explode(str,regex)'s arguments have to be string type");
            }
        }

        // 2.成员变量赋值
        numCols = 1;
        retCols = new Text[numCols];
        nullCols = new Object[numCols];
        for (int i = 0; i < numCols; i++) {
            retCols[i] = new Text();
            nullCols[i] = null;
        }

        // 3.定义输出字段的列名(字段名)
        // 此处设置的各个列名很可能会在用户执行Hive SQL查询时,会被其指定的列别名所覆盖,如果查询时未指定别名,则显示此列名.
        // 此自定义表生成函数UDTF只输出单列,所以只添加了单个列的列名
        ArrayList<String> fieldNames = new ArrayList<>();
        fieldNames.add("split_explode_col");

        // 4.定义输出字段对应字段类型的ObjectInspector(对象检查器),一般用于查看/转换/操控该字段的内存对象
        // 只是查看器,并非是真正的字段对象
        ArrayList<ObjectInspector> fieldOIs = new ArrayList<>();
        // 由于输出字段的数据类型都是String类型
        // 因此使用检查器PrimitiveObjectInspectorFactory.javaStringObjectInspector
        fieldOIs.add(PrimitiveObjectInspectorFactory.javaStringObjectInspector);

        // 5.生成并返回输出对象的对象检查器
        return ObjectInspectorFactory.getStandardStructObjectInspector(fieldNames, fieldOIs);
    }

    // 此方法为核心处理方法,声明主要的处理过程,如果是对表使用函数,则会对表中每一行数据通过同一个实例调用一次此方法.
    // 输入参数为待处理的字符串以及分隔符正则表达式,每次处理完后直接使用forward方法将数据传给收集器.
    @Override
    public void process(Object[] args) throws HiveException {
        // 1.通过 ObjectInspector 获取待处理字符串
        String str = ((StringObjectInspector) inputFieldIOs[0]).getPrimitiveJavaObject(args[0]);
        if (str == null || str.length() == 0) {// 如果对象为null则直接输出空行
            forward(nullCols);
            return;
        }
        // 2.获取分割符正则表达式
        if (!regexParsed) {
            regex = ((StringObjectInspector) inputFieldIOs[1]).getPrimitiveJavaObject(args[1]);
            regexParsed = true;
        }
        // 3.按照正则表达式分割字符串
        String[] words = str.split(regex);
        // 4.输出数据
        // 将输出行通过forward方法传给收集器,每调用一次forward则代表在新生成字段中生成一行数据
        for (String word : words) {
            // 将输出数据放入输出数组的指定位置(每次写入时为覆盖写入),然后输出.本UDTF固定输出单列.
            retCols[0].set(word);
            forward(retCols);
        }
    }

    // 释放相关资源,一般不需要
    @Override
    public void close() throws HiveException {

    }

    // 返回此UDTF对应的函数名
    @Override
    public String toString() {
        return "split_explode";
    }
}

End~