UDF开发手册 - UDF

6,905 阅读9分钟

一行输入,返回一个结果

可以满足日常工作中的大部分需求

UDF的实现方法

Hive 提供了两个实现 UDF 的方式:

第一种:继承 UDF
  • 优点:
    • 实现简单
    • 支持Hive的基本类型、数组和Map
    • 支持函数重载
  • 缺点:
    • 逻辑较为简单,只适合用于实现简单的函数

这种方式编码少,代码逻辑清晰,可以快速实现简单的UDF

第二种:继承 GenericUDF
  • 优点:
    • 支持任意长度、任意类型的参数
    • 可以根据参数个数和类型实现不同的逻辑
    • 可以实现初始化和关闭资源的逻辑(initialize、close)
  • 缺点:
    • 实现比继承UDF要复杂一些

与继承 UDF 相比,GenericUDF 更加灵活,可以实现更为复杂的函数

关于两者的选择

如果函数具有以下特点,优先继承 UDF 类:

  1. 逻辑简单,比如英文转小写函数
  2. 参数和返回值类型简单,都是Hive的基本类型、数组或Map
  3. 没有初始化或关闭资源的需求

否则考虑继承 GenericUDF 类

接下来介绍这两种实现方式的具体步骤

继承 UDF 类

第一种方式的代码实现最为简单,只需新建一个类 继承UDF,然后编写 evaluate() 即可

import org.apache.hadoop.hive.ql.exec.UDF;

/**
 * 继承 org.apache.hadoop.hive.ql.exec.UDF
 */
public class SimpleUDF extends UDF {

    /**
     * 编写一个函数,要求如下:
     * 1. 函数名必须为 evaluate
     * 2. 参数和返回值类型可以为:Java基本类型、Java包装类、org.apache.hadoop.io.Writable等类型、List、Map
     * 3. 函数一定要有返回值,不能为 void
     */
    public int evaluate(int a, int b) {
        return a + b;
    }

    /**
     * 支持函数重载
     */
    public Integer evaluate(Integer a, Integer b, Integer c) {
        if (a == null || b == null || c == null)
            return 0;

        return a + b + c;
    }
}

继承UDF类的方式非常简单,但还有一些需要注意的地方:

  1. evaluate() 方法并不是继承自 UDF 类(编写代码时名字容易打错,哈哈哈~)
  2. evaluate() 的返回值类型不能为 void(你这个函数返回值都没有,我要你干嘛?)

支持的参数和返回值类型

支持 hive基本类型、数组和Map

Hive基本类型

Java可以使用Java原始类型、Java包装类或对应的Writable类

PS:对于基本类型,最好不要使用 Java原始类型,当 null 传给 Java原始类型 参数时,UDF 会报错。Java包装类还可以用于null值判断

Hive类型Java原始类型Java包装类hadoop.io.Writable
tinyintbyteByteByteWritable
smallintshortShortShortWritable
intintIntegerIntWritable
bigintlongLongLongWritable
stringString-Text
booleanbooleanBooleanBooleanWritable
floatfloatFloatFloatWritable
doubledoubleDoubleDoubleWritable
数组和Map
Hive类型Java类型
arrayList
Map<K, V>Map<K, V>

继承 GenericUDF

这种方式最为灵活,但实现起来也比上一种方法要复杂一些

继承 GenericUDF 后,必须实现其三个方法:

  1. initialize()
  2. evaluate()
  3. getDisplayString()

initialize()

/**
 * 初始化 GenericUDF,每个 GenericUDF 示例只会调用一次初始化方法
 *
 * @param arguments
 *          自定义UDF参数的 ObjectInspector 实例
 * @throws UDFArgumentException
 *           参数个数或类型错误时,抛出该异常
 * @return 函数返回值类型
*/
public abstract ObjectInspector initialize(ObjectInspector[] arguments)
    throws UDFArgumentException;

initialize() 在函数在 GenericUDF 初始化时被调用一次,执行一些初始化操作,包括:

  1. 判断函数参数个数
  2. 判断函数参数类型
  3. 确定函数返回值类型

除此之外,用户在这里还可以做一些自定义的初始化操作,比如初始化HDFS客户端等

其一:判断函数参数个数

可通过 arguments 数组的长度来判断函数参数的个数

判断函数参数个数示例:

// 1. 参数个数检查,只有一个参数
if (arguments.length != 1) // 函数只接受一个参数
    throw new UDFArgumentException("函数需要一个参数"); // 当自定义UDF参数与预期不符时,抛出异常
其二:判断函数参数类型

ObjectInspector 可用于侦测参数数据类型,其内部有一个枚举类 Category,代表了当前 ObjectInspector 的类型

public interface ObjectInspector extends Cloneable {
  public static enum Category {
    PRIMITIVE, // Hive原始类型
    LIST, // Hive数组
    MAP, // Hive Map
    STRUCT, // 结构体
    UNION // 联合体
  };
}

Hive原始类型又细分了多种子类型,PrimitiveObjectInspector 实现了 ObjectInspector,可以更加具体的表示对应的Hive原始类型

public interface PrimitiveObjectInspector extends ObjectInspector {

  /**
   * The primitive types supported by Hive.
   */
  public static enum PrimitiveCategory {
    VOID, BOOLEAN, BYTE, SHORT, INT, LONG, FLOAT, DOUBLE, STRING,
    DATE, TIMESTAMP, BINARY, DECIMAL, VARCHAR, CHAR, INTERVAL_YEAR_MONTH, INTERVAL_DAY_TIME,
    UNKNOWN
  };
}

关于 PrimitiveCategory 的枚举类型就不一一解释了

参数类型判断示例:

// 2. 参数类型检查,参数类型为String
if (arguments[0].getCategory() != ObjectInspector.Category.PRIMITIVE // 参数是Hive原始类型
        || !PrimitiveObjectInspector.PrimitiveCategory.STRING.equals(((PrimitiveObjectInspector)arguments[0]).getPrimitiveCategory())) // 参数是Hive的string类型
    throw new UDFArgumentException("函数第一个参数为字符串"); // 当自定义UDF参数与预期不符时,抛出异常
其三:确定函数返回值类型

initialize() 需要 return 一个 ObjectInspector 实例,用于表示自定义UDF返回值类型。initialize() 的返回值决定了 evaluate() 的返回值类型

ObjectInspector 的源码中,有这么一段注释,其大意是 ObjectInspector 的实例应该由对应的工厂类获取,以保证实例的单例等属性

/**
 * An efficient implementation of ObjectInspector should rely on factory, so
 * that we can make sure the same ObjectInspector only has one instance. That
 * also makes sure hashCode() and equals() methods of java.lang.Object directly
 * works for ObjectInspector as well.
 */
public interface ObjectInspector extends Cloneable { }

对于基本类型(byte、short、int、long、float、double、boolean、string),可以通过 PrimitiveObjectInspectorFactory 的静态字段直接获取

Hive类型writable类型Java包装类型
tinyintwritableByteObjectInspectorjavaByteObjectInspector
smallintwritableShortObjectInspectorjavaShortObjectInspector
intwritableIntObjectInspectorjavaIntObjectInspector
bigintwritableLongObjectInspectorjavaLongObjectInspector
stringwritableStringObjectInspectorjavaStringObjectInspector
booleanwritableBooleanObjectInspectorjavaBooleanObjectInspector
floatwritableFloatObjectInspectorjavaFloatObjectInspector
doublewritableDoubleObjectInspectorjavaDoubleObjectInspector

注意:基本类型返回值有两种:Writable类型Java包装类型

  • 在 initialize 指定的返回值类型为 Writable类型 时,在 evaluate() 中 return 的就应该是对应的 Writable实例
  • 在 initialize 指定的返回值类型为 Java包装类型 时,在 evaluate() 中 return 的就应该是对应的 Java包装类实例

Array、Map<K, V>等复杂类型,则可以通过 ObjectInspectorFactory 的静态方法获取

Hive类型ObjectInspectorFactory的静态方法evaluate()返回值类型
ArraygetStandardListObjectInspector(T t)List
Map<K, V>getStandardMapObjectInspector(K k, V v);Map<K, V>

返回值类型为 Map<String, int> 的示例:

// 3. 自定义UDF返回值类型为 Map<String, int>
return ObjectInspectorFactory.getStandardMapObjectInspector(
        PrimitiveObjectInspectorFactory.javaStringObjectInspector, // Key 是 String
        PrimitiveObjectInspectorFactory.javaIntObjectInspector // Value 是 int
);
完整的 initialize() 函数如下:
/**
 * 初始化 GenericUDF,每个 GenericUDF 示例只会调用一次初始化方法
 *
 * @param arguments
 *          自定义UDF参数的 ObjectInspector 实例
 * @throws UDFArgumentException
 *           参数个数或类型错误时,抛出该异常
 * @return 函数返回值类型
*/
@Override
public ObjectInspector initialize(ObjectInspector[] arguments) throws UDFArgumentException {
    // 1. 参数个数检查,只有一个参数
    if (arguments.length != 1) // 函数只接受一个参数
        throw new UDFArgumentException("函数需要一个参数"); // 当自定义UDF参数与预期不符时,抛出异常

    // 2. 参数类型检查,参数类型为String
    if (arguments[0].getCategory() != ObjectInspector.Category.PRIMITIVE // 参数是Hive原始类型
            || !PrimitiveObjectInspector.PrimitiveCategory.STRING.equals(((PrimitiveObjectInspector)arguments[0]).getPrimitiveCategory())) // 参数是Hive的string类型
        throw new UDFArgumentException("函数第一个参数为字符串"); // 当自定义UDF参数与预期不符时,抛出异常

    // 3. 自定义UDF返回值类型为 Map<String, int>
    return ObjectInspectorFactory.getStandardMapObjectInspector(
            PrimitiveObjectInspectorFactory.javaStringObjectInspector, // Key 是 String
            PrimitiveObjectInspectorFactory.javaIntObjectInspector // Value 是 int
    );
}

evaluate()

核心方法,自定义UDF的实现逻辑

代码实现步骤可以分为三部分:

  1. 参数接收
  2. 自定义UDF核心逻辑
  3. 返回处理结果
第一步:参数接收

evaluate() 的参数就是 自定义UDF 的参数,

/**
 * Evaluate the GenericUDF with the arguments.
 *
 * @param arguments
 *          The arguments as DeferedObject, use DeferedObject.get() to get the
 *          actual argument Object. The Objects can be inspected by the
 *          ObjectInspectors passed in the initialize call.
 * @return The
 */
public abstract Object evaluate(DeferredObject[] arguments)
  throws HiveException;

通过源码注释可知,DeferedObject.get() 可以获取参数的值

/**
 * A Defered Object allows us to do lazy-evaluation and short-circuiting.
 * GenericUDF use DeferedObject to pass arguments.
 */
public static interface DeferredObject {
  void prepare(int version) throws HiveException;
  Object get() throws HiveException;
};

再看看 DeferredObject 的源码,DeferedObject.get() 返回的是 Object,传入的参数不同,会是不同的Java类型,以下是Hive常用参数类型对应的Java类型

对于Hive基本类型,传入的都是 Writable类型

Hive类型Java类型
tinyintByteWritable
smallintShortWritable
intIntWritable
bigintLongWritable
stringText
booleanBooleanWritable
floatFloatWritable
doubleDoubleWritable
ArrayArrayList
Map<K, V>HashMap<K, V>

参数接收示例:

// 只有一个参数:Map<String, int>

// 1. 参数为null时的特殊处理
if (arguments[0] == null)
    return ...

// 2. 接收参数
Map<Text, IntWritable> map = (Map<Text, IntWritable>)arguments[0].get();
第二步:自定义UDF核心逻辑

获取参数之后,到这里就是自由发挥了~

第三步:返回处理结果

这一步和 initialize() 的返回值一一对应

基本类型返回值有两种:Writable类型Java包装类型

  • 在 initialize 指定的返回值类型为 Writable类型 时,在 evaluate() 中 return 的就应该是对应的 Writable实例
  • 在 initialize 指定的返回值类型为 Java包装类型 时,在 evaluate() 中 return 的就应该是对应的 Java包装类实例

Hive数组和Map的返回值类型如下:

Hive类型Java类型
Array<T>List<T>
Map<K, V>Map<K, V>

getDisplayString()

getDisplayString() 返回的是 explain 时展示的信息

/**
 * Get the String to be displayed in explain.
 */
public abstract String getDisplayString(String[] children);

注意:这里不能return null,否则可能在运行时抛出空指针异常,而且这个出现这个问题还不容易排查~

ERROR [b1c82c24-bfea-4580-9a0c-ff47d7ef4dbe main] ql.Driver: FAILED: NullPointerException null
java.lang.NullPointerException
	at java.util.regex.Matcher.getTextLength(Matcher.java:1283)
	...
	at org.apache.hadoop.util.RunJar.main(RunJar.java:136)

close()

资源关闭回调函数

不是抽象方法,可以不实现

/**
 * Close GenericUDF.
 * This is only called in runtime of MapRedTask.
 */
@Override
public void close() throws IOException { }

自定义GenericUDF完整示例

import org.apache.hadoop.hive.ql.exec.UDFArgumentException;
import org.apache.hadoop.hive.ql.metadata.HiveException;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDF;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory;
import org.apache.hadoop.hive.serde2.objectinspector.PrimitiveObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory;
import org.apache.hadoop.io.Text;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class SimpleGenericUDF extends GenericUDF {
    @Override
    public ObjectInspector initialize(ObjectInspector[] arguments) throws UDFArgumentException {
        // 1. 参数个数检查
        if (arguments.length != 1) // 函数只接受一个参数
            throw new UDFArgumentException("函数需要一个参数"); // 当自定义UDF参数与预期不符时,抛出异常

        // 2. 参数类型检查
        if (arguments[0].getCategory() != ObjectInspector.Category.PRIMITIVE // 参数是Hive原始类型
                || !PrimitiveObjectInspector.PrimitiveCategory.STRING.equals(((PrimitiveObjectInspector)arguments[0]).getPrimitiveCategory())) // 参数是Hive的string类型
            throw new UDFArgumentException("函数第一个参数为字符串"); // 当自定义UDF参数与预期不符时,抛出异常

        // 3. 自定义UDF返回值类型为 Map<String, int>
        return ObjectInspectorFactory.getStandardMapObjectInspector(
                PrimitiveObjectInspectorFactory.javaStringObjectInspector, // Key 是 String
                PrimitiveObjectInspectorFactory.javaIntObjectInspector // Value 是 int
        );
    }

    @Override
    public Object evaluate(DeferredObject[] arguments) throws HiveException {
        // 1. 参数接收
        if (arguments[0] == null)
            return new HashMap<>();
        String str = ((Text) arguments[0].get()).toString();

        // 2. 自定义UDF核心逻辑
        // 统计字符串中每个字符的出现次数,并将其记录在Map中
        Map<String, Integer> map = new HashMap<>();
        for (char ch : str.toCharArray()) {
            String key = String.valueOf(ch);
            Integer count = map.getOrDefault(key, 0);
            map.put(key, count + 1);
        }

        // 3. 返回处理结果
        return map;
    }

    @Override
    public String getDisplayString(String[] children) {
        return "这是一个简单的测试自定义UDF~";
    }
}