hive udf Description信息中文乱码解决过程
注:此现象仅针对hive 2.3.5版本,其他版本请自行参考
1、生产现象
自定义udf上线,查看其udf描述信息时,中文内容乱码。使用其方法时,中文可正常显示。
描述信息乱码截图:
udf查询内容中文正常显示
2、问题排查
2.1、jar包打包未指定udf-8编码?
默认 就是udf-8了
2.2、jar包加载的时候未指定file.encoding编码?
排查加载环境及系统环境编码
2.2.1 方法一:直接查看该类描述字段信息
排查问题时其实首先想到的是去查看的源码中类加载或者打印输出的时候是否因为未指定编码格式导致的,由于源码中并未看到相关编码信息,故自己测试其是否加载问题导致,所以写了如下代码进行测试。结果跟想法往往是相反的,打印的信息显示正常,未出现乱码现有。作证了不是由jvm加载导致的乱码。(此处未截图,获取描述字段信息未乱码)
public String evaluate(String str) {
Class<?> aClass = null;
try {
aClass = Class.forName(str);
Description description = (Description) aClass.getAnnotation(Description.class);
System.out.println("测试类名称:" + description.extended());
System.out.println("udf name:" + description.name());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return "测试乱码";
}
2.2.2 方法二:输出其环境实际编码
udf中添加静态代码块获取其实际使用的编码格式
static {
System.out.println("测试类已加载");
//获取系统默认编码
System.out.println("系统默认编码:" + System.getProperty("file.encoding")); //查询结果GBK
//系统默认字符编码
System.out.println("系统默认字符编码:" + Charset.defaultCharset()); //查询结果GBK
//操作系统用户使用的语言
System.out.println("系统默认语言:" + System.getProperty("user.language")); //查询结果zh
}
由打印信息可看出其实际使用的皆是UTF-8编码,故排除此项因素
2.3、查看源码
2.3.1、查看描述信息所在源码位置
查看描述信息时,发现由Function class 和 Function type字样非描述内容,故搜索其在源码中的位置,便于快速定位。搜索“Function type”可快速定位到DDLTask类的describeFunction方法
源码中对应方法内容
/**
* Shows a description of a function.
* @param db
*
* @param descFunc
* is the function we are describing
* @throws HiveException
*/
//todo 控制台显示中文乱码问题
private int describeFunction(Hive db, DescFunctionDesc descFunc) throws HiveException, SQLException {
String funcName = descFunc.getName();
// write the results in the file
//此处将显示的结果写到了文件,而并非控制台输出
DataOutputStream outStream = getOutputStream(descFunc.getResFile());
try {
// get the function documentation
//获取方法描述信息
Description desc = null;
Class<?> funcClass = null;
//通过方法名称 获取该方法的详细信息
FunctionInfo functionInfo = FunctionRegistry.getFunctionInfo(funcName);
if (functionInfo != null) {
funcClass = functionInfo.getFunctionClass();
}
//此处自定义udf的注释信息
if (funcClass != null) {
desc = AnnotationUtils.getAnnotation(funcClass, Description.class);
}
if (desc != null) {
//将描述信息中的_FUNC_ 使用实际的方法名替换掉
String str = desc.value().replace("_FUNC_", funcName);
outStream.writeBytes(str);
//判断是否输出 扩展信息
if (descFunc.isExtended()) {
Set<String> synonyms = FunctionRegistry.getFunctionSynonyms(funcName);
if (synonyms.size() > 0) {
outStream.writeBytes("\nSynonyms: " + join(synonyms, ", "));
}
if (desc.extended().length() > 0) {
//输出扩展信息 具体内容 中文乱码所处位置
outStream.writeBytes("\n"
+ desc.extended().replace("_FUNC_", funcName));
}
}
} else {
if (funcClass != null) {
outStream.writeBytes("There is no documentation for function '"
+ funcName + "'");
} else {
outStream.writeBytes("Function '" + funcName + "' does not exist.");
}
}
outStream.write(terminator);
//如果需要输出扩展信息
if (descFunc.isExtended()) {
if (funcClass != null) {
outStream.writeBytes("Function class:" + funcClass.getName() + "\n");
}
if (functionInfo != null) {
outStream.writeBytes("Function type:" + functionInfo.getFunctionType() + "\n");
FunctionResource[] resources = functionInfo.getResources();
if (resources != null) {
for (FunctionResource resource : resources) {
outStream.writeBytes("Resource:" + resource.getResourceURI() + "\n");
}
}
}
}
} catch (FileNotFoundException e) {
LOG.warn("describe function: " + stringifyException(e));
return 1;
} catch (IOException e) {
LOG.warn("describe function: " + stringifyException(e));
return 1;
} catch (Exception e) {
throw new HiveException(e);
} finally {
IOUtils.closeStream(outStream);
}
return 0;
}
2.3.2、描述信息输出到文件
查看源码发现其描述信息是被写到了文件中,而且直接在控制台打印输出。
约3044行,行数仅供参考–因本人添加个人注释,其行数已有新增
// write the results in the file
DataOutputStream outStream = getOutputStream(descFunc.getResFile());
2.3.3、查看乱码信息输出源码
约3070行,若udf Description 信息不为空,且extended有值时,则将其内容写入文件。[由2.2.1可知](#2.2.1 方法一:直接查看该类描述字段信息)其描述信息加载是正常,故此处重点排查写出时是否产生乱码。
if (desc.extended().length() > 0) {
//输出扩展信息 具体内容 中文乱码所处位置
outStream.writeBytes("\n"
+ desc.extended().replace("_FUNC_", funcName));
}
查看writeBytes方法,此处发现将字符串转为char,然后强转为byte,众所周知,中文占2个或3个字节,此处将其强转了一个字节,故怀疑此处导致了中文乱码。
public final void writeBytes(String s) throws IOException {
int len = s.length();
for (int i = 0 ; i < len ; i++) {
out.write((byte)s.charAt(i));
}
incCount(len);
}
2.3.4、验证乱码原因
2.3.4.1、模拟乱码情况
参照源码进行写入中文写入测试。发现使用该方法将中文写入文件,会产生乱码,将文件内容读取时,显示乱码内容与生产显示乱码比较一致。
private static void hadoopWriteFile(String message,String resFile) throws IOException {
//创建输出流
Path outputFile = new Path(resFile);
Configuration conf = new Configuration();
FileSystem fs = outputFile.getFileSystem(conf);
DataOutputStream outStream = fs.create(outputFile);
//outStream.write(message.getBytes());
outStream.writeBytes(message);
IOUtils.closeStream(outStream);
}
3、解决方法
3.1、尝试解决乱码情况–更改写文件方法
查看DataOutputStream还有其他write方法,尝试使用DataOutputStream.write(byte b[])方法。更改写入方法后,发现文件内容未出现乱码,读取文件内容也可正常显示。
private static void hadoopWriteFile(String message,String resFile) throws IOException {
//创建输出流
Path outputFile = new Path(resFile);
Configuration conf = new Configuration();
FileSystem fs = outputFile.getFileSystem(conf);
DataOutputStream outStream = fs.create(outputFile);
outStream.write(message.getBytes());
//outStream.writeBytes(message);
IOUtils.closeStream(outStream);
}
3.2、尝试解决乱码情况–变更写入字符串内容
将需要显示的中文内容转换成字节数组,然后将每个字节转换为char,将char数组转换成字符串,使用DataOutputStream.writeBytes 方式写入文件。通过结果可知,该方法可将保证写入文件中的中文正常显示。
==注:本方法仅当无法使用方法1解决此问题时,才考虑使用本方法解决。本方法描述信息中文字内容可读性较差,建议慎用。
package test;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class TT {
public static void main(String[] args) throws Exception {
String resFile = "D:\\development\\parse-vehicle-track\\spark-warehouse\\test.txt";
String strTest = "hive udf 注释中文乱码测试\n" +
"SELECT bigdata_warehouse.get_car_number_city('川A88888') ==> 四川省,510000,成都市,510100;\n" +
"SELECT bigdata_warehouse.get_car_number_city('渝66666') ==> 重庆市,500000,,";
String s = chinese2CharString(strTest);
hadoopWriteBytesFile(s, resFile);
hadoopReadFile(resFile);
}
private static void hadoopWriteBytesFile(String message, String resFile) throws IOException {
//创建输出流
Path outputFile = new Path(resFile);
Configuration conf = new Configuration();
FileSystem fs = outputFile.getFileSystem(conf);
DataOutputStream outStream = fs.create(outputFile);
outStream.writeBytes(message);
IOUtils.closeStream(outStream);
}
/**
* @param message 需要转换中文的原字符串内容
* @return 转换后的字符串内容,转换后的内容不可读,但DataOutputStream.write(String s)会将其转为正确的中文。
*/
public static String chinese2CharString(String message) {
//1、将中字符串转成字节数组,
//2、然后将字节数组转为对应的char拼接成字符串
//3、再将拼接后的字符串写入文件
byte[] bytes3 = message.getBytes(StandardCharsets.UTF_8);
char[] chars1 = new char[bytes3.length];
for (int i = 0; i < bytes3.length; i++) {
chars1[i] = (char) bytes3[i];
}
return new String(chars1);
}
private static void hadoopReadFile(String resFile) throws IOException {
//创建输入流
Path outputFile = new Path(resFile);
Configuration conf = new Configuration();
FileSystem fs = outputFile.getFileSystem(conf);
FSDataInputStream in = fs.open(outputFile);
FileStatus fss = fs.getFileStatus(outputFile);
IOUtils.copyBytes(in, System.out, 4096, false);
}
}
3.3、验证更改乱码的解决方法
1.将需要描述的内容转字节数组在转为char数组,再将char数组转为字符串打印出来;
2.将打印出来的字符串粘贴到自定义udf对应的描述信息位置
3. 将udf成jar包,上传hive并添加自定义函数。验证其更改后描述信息中文已显示正常。
4、总结
解决此方法乱码有以下两种方式。可更改源码的情况下,建议使用方式1,若不方便可使用方式二,但方式的描述信息源码中可读性差,请自行评估是否使用。
方式1、更改源码。
更改DDLTask类中describeFunction(Hive db, DescFunctionDesc descFunc)方法中的 outStream.writeBytes 更改为使用 outStream.write();
方式2、将描述的字符串转为字节数组,再转为char数组,再转为string。用转后的string字符串作为其描述信息内容。
/**
* @param message 需要转换中文的原字符串内容
* @return 转换后的字符串内容,转换后的内容不可读,但DataOutputStream.write(String s)会将其转为正确的中文。
*/
public static String chinese2CharString(String message) {
//1、将中字符串转成字节数组,
//2、然后将字节数组转为对应的char拼接成字符串
//3、再将拼接后的字符串写入文件
byte[] bytes3 = message.getBytes(StandardCharsets.UTF_8);
char[] chars1 = new char[bytes3.length];
for (int i = 0; i < bytes3.length; i++) {
chars1[i] = (char) bytes3[i];
}
return new String(chars1);
}
essage) {
//1、将中字符串转成字节数组,
//2、然后将字节数组转为对应的char拼接成字符串
//3、再将拼接后的字符串写入文件
byte[] bytes3 = message.getBytes(StandardCharsets.UTF_8);
char[] chars1 = new char[bytes3.length];
for (int i = 0; i < bytes3.length; i++) {
chars1[i] = (char) bytes3[i];
}
return new String(chars1);
}