前言

最近因为业务关系,用到了Hbase,因为用的是Spring boot框架 ,所以自然而然就用到了spring封装的HbaseTemplate工具类。然而HbaseTemplate封装的代码实在比较糟糕,出了一些基本的CRUD操作之外并没有给我们提供太多便利之处。先来看看痛处:

痛处一及改进

  • 我们先来看看HabaseTemplate最基本的查询操作(以下只是demo演示):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class UserInfo{
string name;
string password;
}
public void putUserInfo(UserInfo userInfo) {
hBaseTemplate.execute(TABLE_NAME, (table) -> {
//根据rowKey定义一个put对象,可用作插入和更新
Put put = new Put(Bytes.toBytes(rowKey));
//name是否为空
if(userInfo.name!=null){
put.addColumn(COLUMN_FAMILY_NAME.getBytes(), Bytes.toBytes(COLUMN_RAW_DATA),Bytes.toBytes(userInfo.name));
}
//password是否为空
if(userInfo.password!=null){
put.addColumn(COLUMN_FAMILY_NAME.getBytes(), Bytes.toBytes(COLUMN_RAW_DATA),Bytes.toBytes(userInfo.password));
}
table.put(put);
return true;
});
}

相信大家也看出来了,如果待插入的对象有很多字段呢?还要逐个写if语句来判读非空么?这明显使得代码非常地不简洁。于是,个人封装了一个插入更新模版类(其实只是简单的对Put对象的一个扩展):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//继承并扩展Put对象
public class PutExtension extends Put {

String columnFamilyName = "demo";

public PutExtension(String columnFamilyName, byte[] row) {
super(row);
this.columnFamilyName = columnFamilyName;
}

public PutExtension build(String paramName, Object param) throws IOException {
if (param != null) {
this.addColumn(columnFamilyName.getBytes(), paramName.getBytes(),
Bytes.toBytes(param.toString()));
}
return this;
}
}

封装之后,之前累赘的查询操作可以变得如下所示:

1
2
3
4
5
6
7
8
9

//然后操作如下
hBaseTemplate.execute(TABLE_NAME, (table) -> {
PutExtension putExtension = new PutExtension(familyName, rowKey.getBytes());
putExtension.build("name",userInfo.name)
.build("password", userInfo.password);
table.put(putExtension);
return true;
})

其实这也就是一个简单的封装,只不过把冗余的逻辑判断给丢出去了而已。

痛处二及改进

  • 在HbaseTemplate中,根据rowKey查询出来的原始数据是字节数组,我们要将字节数组转化成业务逻辑中希望的java bean需要做很多重复的判断匹配逻辑,以下是没改进前的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

public UserInfo getUserInfo() {
return (UserInfo) hBaseTemplate.get(TABLE_NAME, rowKey, familyName,(result, i) ->{
UserInfo userInfo=new UserInfo()
//重复逻辑一
bytes[] nameBytes=result.getValue(familyName.getBytes(), "name".getBytes()));
if(nameBytes!=null){
userInfo.setName(Bytes.toString(nameBytes));
}
//重复逻辑二
bytes[] passwordBytes=result.getValue(familyName.getBytes(), "password".getBytes()));
if(passwordBytes!=null){
userInfo.setPassword(Bytes.toString(passwordBytes));
}
});
}

可以看出,这样做的缺点是一旦java bean的字段一多,重复的非空判断逻辑也会增多,从而使得代码变得十分累赘且不可维护。于是我参考Spring JDBC的RowMapper的封装,利用了Spring框架自带的反射工具beanUtils和beanWrapper,自己实现了如下封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
public class HBaseResultBuilder<T> {
private Class<T> mappedClass;
private Map<String, PropertyDescriptor> mappedFields;
private Set<String> mappedProperties;
HashSet populatedProperties;
private BeanWrapper beanWrapper;
private Result result;
private String columnFamilyName;
private T t;
//接受一些列参数并实例化要返回的结果对象
public HBaseResultBuilder(String columnFamilyName, Result result, Class<T> clazz) {
this.columnFamilyName = columnFamilyName;
this.result = result;
this.mappedClass = clazz;
mappedFields = new HashMap<>();
mappedProperties = new HashSet<>();
populatedProperties = new HashSet<>();
this.t = BeanUtils.instantiate(clazz);
PropertyDescriptor[] pds = BeanUtils.getPropertyDescriptors(mappedClass);
PropertyDescriptor[] var3 = pds;
int var4 = pds.length;
for (int var5 = 0; var5 < var4; ++var5) {
PropertyDescriptor pd = var3[var5];
if (pd.getWriteMethod() != null) {
this.mappedFields.put(this.lowerCaseName(pd.getName()), pd);
String underscoredName = this.underscoreName(pd.getName());
if (!this.lowerCaseName(pd.getName()).equals(underscoredName)) {
this.mappedFields.put(underscoredName, pd);
}
this.mappedProperties.add(pd.getName());
}
}
beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(t);
}

private String underscoreName(String name) {
if (!StringUtils.hasLength(name)) {
return "";
} else {
StringBuilder result = new StringBuilder();
result.append(this.lowerCaseName(name.substring(0, 1)));

for (int i = 1; i < name.length(); ++i) {
String s = name.substring(i, i + 1);
String slc = this.lowerCaseName(s);
if (!s.equals(slc)) {
result.append("_").append(slc);
} else {
result.append(s);
}
}

return result.toString();
}
}

private String lowerCaseName(String name) {
return name.toLowerCase(Locale.US);
}
//使用时根据要解析的字段频繁调用此方法即可,仿造java8 流式操作
public HBaseResultBuilder build(String columnName) {
byte[] value = result.getValue(columnFamilyName.getBytes(), columnName.getBytes());
if (value == null || value.length == 0) {
return this;
} else {
String field = this.lowerCaseName(columnName.replaceAll(" ", ""));
PropertyDescriptor pd = this.mappedFields.get(field);
if (pd == null) {
log.error("HBaseResultBuilder error: can not find property: " + field);
} else {
beanWrapper.setPropertyValue(pd.getName(), Bytes.toString(value));
populatedProperties.add(pd.getName());
}
}
return this;
}

//伪造Java8的即视感,“流最后的终端操作“。
public T fetch() {
//只要有一个属性被解析出来就返回结果对象,毕竟hbase存的是稀疏数据,不一定全量
if (CollectionUtils.isNotEmpty(populatedProperties)) {
return this.t;
} else {
return null;
}
}

通过利用反射的基本原理,我们可以通过结果数据构造出我们需要的java bean。最后我们的调用过程可以简化成如下:

1
2
3
4
5

public UserInfo getUserInfo() {
return (UserInfo) hBaseTemplate.get(TABLE_NAME, rowKey, familyName,
(result, i) -> new HBaseResultBuilder<>(familyName, result, UserInfo.class).build("name").build("password").fetch());
}

成功!!!是不是代码整洁多了,其实也就是将一些复杂的逻辑给抽出去了,正好最近看了Java8实战,从而萌生的一点小想法。