这里分析Hive中部分UDF及UDTF函数的源码并依此进行自定义
1.UDF
这里UDF以Upper方法为例
此方法会将传入的字符串变为大写后返回
代码如下:
该类主要继承了GenericUDF类,该类有三个需要重写的方法
分别为:
1.public ObjectInspector initialize(ObjectInspector[] arguments)
该方法是UDF方法的初始化方法,主要用于验证输入参数的数据类型及输入参数的个数,返回值为ObjectInspector类型,该类型为Hive内部的类型监视器类,可以将它理解为Hive各种数据类型在java中对应的各种类的监视器。在这里传入的方法参数就是对应hive类型不同ObjectInspector类的子类,我们可以通过传入的参数来判断传入数据的类型。而返回值用于限定UDF方法对应的返回类型,比如UDF方法需要返回String类型对象,那么这里就要返回对应ObjectInspector对应String类型的子类监视器。
2.public Object evaluate(DeferredObject[] arguments)
主要的逻辑方法,当UDF被调用时,对每行的数据进行逻辑处理,参数为DeferredObject类型,可以将它理解为对应Hive中UDF传入参数的类型,在这里我们可以发现Java中并没有类似直接对应Hive的基本数据类型,而是有对应Hive各种类型的监视器(ObjectInspector类),以及对应参数的(DeferredObject类),这样就把判断和数据处理解耦了,在处理数据之前即调用evaluate方法前,initialize方法首先对传入数据的类型进行判断,判断通过后才会执行evaluate方法进行逻辑处理。
需要注意的是返回值必须是WritableComparable类型,即MR中key必须实现的接口类型。
3.public Stringget DisplayString(String[] children)
展示方法,主要用于打印UDF的一些基本信息,类似Log4j的功能。
public ObjectInspector initialize(ObjectInspector[] arguments)
该方法首先进行了两次判断,第一次判断参数个数是否为1,不为则抛出异常。(54-57行)
第二次判断传入的参数是否为Hive的基本数据类型,判断的依据是通过ObjectInspector监视器类获取传入参数的数据类型与枚举类型的Category.PRIMITIVE做比较。
(59-62行)
(这里可以稍微看下Category的枚举类型
PRIMITIVE对应Hive的基本数据类型,其他的List,Map等则对应Hive的非基本数据类型。)
接着是获取StringConverter类型转换器,后用于Hive基本类型的String类型转换。传入的参数为UDF方法参数的类型监视器,表示转换器用于该类型转String类型的数据转换,可以看到这里并没有限定类型必须为String类型,所以我可以推断出Upper方法在Hive中对int等基本数据类型甚至是boolean类型一样可以使用(65行)
接着就是通过参数的类型监视器获取到传入参数的基本数据类型,并通过switch来决定返回不同种类的监视器,这一点我们就可以看出如果UDF参数为char则最终返回char,如果为varchar则最终返回varchar,如果为其他(如int)则最终返回String。(case中代码)
需要注意的是char和varchar类型需要特殊处理,与String不同的是确定一个char或varchar的返回类型还需要同时确定返回的char或varchar字符串的长度,这就需要通过TypeInfo读取传入的参数监视器获取对应的长度,并以此确定返回值中char和varchar的长度
最后则是根据输入的类型获取不同的StringHelper(91行)
如果说StringConverter是用于将evaluate中的参数转化为String的工具类,那么StringHelper则是用于将java的基本数据类型转化为MR可以序列化的WritableComparable的工具类。这两个类都将在evaluate方法中用到。
public Objectevaluate(DeferredObject[] arguments)
逻辑很简单,我就不讲了,需要注意的是StringConverter和StringHelper两个类。
其实如果传入的Hive类型为String,其实并不需要使用StringConverter进行转换,直接arguments[0].get()获取到的就是String类型。
但是如果传入的参数比较特殊为Float,Byte等特殊类型,使用StringConverter就能帮我们把它们转化为String类型。
这里再强调一下,StringConverter是在initialize初始化方法执行时传入了UDF的数据类型,所以在evaluate中我们使用它的时候,它已经知道我们需要将什么数据类型转化为String了。
它内部的逻辑:
StringHelper则帮助我们把字符串等基本数据类型转换为各种MR的可序列化Key类型,这里是将String类型转化为MR中的Text类型。
需要注意的是,与StringConverter类似,StringHelper也在初始化方法中传入了PrimitiveCategory对应hive的基本数据类型。
比如你设定了UDF的返回类型为PrimitiveObjectInspector.PrimitiveCategory.STRING;,那么这里的type就是String,只能帮你把String转化为Text类型。
(我在网课上看到的老师写的代码当需要返回int时并没有进行包装而是直接返回java的int类型,而源码中类似的int类型都是通过intWritible包装后返回,原因大概是因为老师选择的返回类型为JavaStringObjectInspector,而源码里是writableStringObjectInspector及其他类型,找了挺久没找到相关资料,但是应该与hive的serde过程有关,可能非writable的类型无法进行下一步Operator?)
public String getDisplayString(String[] children)
该方法可以不写,但是必须得返回空字符串!否则会报空指针。害我忙活半小时,呸。
下面是我自己的自定义UDF
实现把首字母大写的功能。
这里因为代码差不多看懂了,所以该复制的复制,该改的改,一点都没有不好意思,说实话我看源码就是为了复制起来不心虚,嘿嘿。
关键的地方都给了注释
源码的upper方法中返回类型会根据传入类型有关,而我这里写死了不管是什么传入类型,一律以String输出。
evalute方法,就改了一行代码。
UDTF
这里稍微搂一眼UDTF
一样找个方法来看,就找最常用的explode
主要需要重写两个方法,
public StructObjectInspector initialize(ObjectInspector[] args)
该方法对应UDF的initialize方法
public void process(Object[] o)
该方法对应UDF的evaluate方法,为主要逻辑方法。
相信有了上面UDF的基础,这里应该没什么看不懂的了,拿到传入参数的监视器,通过监视器的类型判断参数类型为List还是Map,如果不为这俩直接报错。
这里主要的只有一点,就是该方法的返回类型为StructObjectInspector,而UDF方法这里需要返回ObjectInspector即对应evaluate的最终返回类型。
StructObjectInspector的创建方式很死,正常只能通过ObjectInspectorFactory.getStandardStructObjectInspector(fieldNames, fieldOIs)方式创建
其构造函数的两个参数分别为两个数组,一个数组fieldNames里面的内容是该字段的默认列名,也就是我们查询出来之后改列的列名,当然如果我们手动设置的话会将其覆盖。
另一个数组fieldOIs的参数是explode方法将集合炸裂后可能形成多列,每列结果在process方法中的返回类型。比如Map集合就有key和value,炸裂后会形成两列的多行数据,我们需要分别指定每列数据的类型,这也是UDTF和UDF最大的不同之处。
在上面我们可以看到initialize方法中拿到参数的类型监视器后对其进行switch判断,
如果为List则设置列名为col,并且设置列的类型为((ListObjectInspector)inputOI).getListElementObjectInspector(),其实就是通过List类型监视器拿到该List内元素的类型,如果是字符串集合这里就拿到String的对应监视器类,如果是Int这里就拿到Int的对应监视器类。
如果为Map则设置两个列的默认列名分别为key和value,并且通过Map的监视器类分别拿到key和value对应的监视器类型进行设置,与上面的List监视器异曲同工。
public void process(Object[] o)
主要需要注意的只有一点,就是UDF只需要返回一个值,而UDTF需要返回多行甚至多列值,这里就没办法仅仅通过return来直接返回结果。而是使用GenericUDTF的forward方法来返回结果。
这里它正好有List和Map的对比,我们可以看到forward方法的参数为数组,如果你UDTF的结果为两列,那么该数组需包含至少两个元素,如果为一列,则一个数组只需要包含一个元素。
总结
代码比SQL有意思多了