18年公司由于业务数据增长很快,技术架构也随之进行了升级,由原来的定时离线计算相关报表升级成准实时实现:即用canal监听某些表的数据变化将消息push到消息队列待处理。
起先,同事是写自己的converter去进行canal消息转JavaBean,这种方式很繁琐、重复,故直接想写一个公共转换方法,参考网友的实现思想及自己的优化处理做出一个util供大家参考。
简述实现思想:利用反射将DB表列名和JavaBean属性名去除特殊字符"_"并转成小写对应起来进行赋值,故要求表列名和类属性名命名合理。
这里写成抽象类,供SpringBean使用以处理监听的对应表的消息,其中用@PostConstruct注解标注的意图是注册自定义转换器和属性名别名。
"Talk is cheap, show me your code".
贴出核心代码:
public abstract class AbstractCanalLogMsgProcessor {
private DefaultConversionService conversionService = new DefaultConversionService() {
{
addConverter(new Converter() {
@Override
public Date convert(String source) {
if (StringUtils.isBlank(source)) {
return null;
}
return DateUtils.convertStringToDate(source);
}
});
}
};
private ConcurrentHashMap> cachedClzFields = new ConcurrentHashMap<>();
/**
* 获取改变前后的数据,将canal消息数据转为bean(只赋值private、public、protected属性,不赋值static、final等其他属性)
* 注意:属性名不能包含特殊字符
*
* @param rowChange
* @param clz
* @return
* @throws IllegalAccessException 参数为空时会抛异常
* @throws InstantiationException
*/
public List> getChanges(CanalEntry.RowChange rowChange, Class clz) throws InstantiationException, IllegalAccessException {
if (rowChange == null || clz == null || rowChange.getRowDatasList() == null) {
throw new IllegalArgumentException("rowChange or clz can't be empty.");
}
Map beanFields = getClzFields(clz);
List> result = Lists.newArrayList();
for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
T dataBefore = convertRowData(rowData.getBeforeColumnsList(), beanFields, clz);
T dataAfter = convertRowData(rowData.getAfterColumnsList(), beanFields, clz);
result.add(new RowDataPair<>(dataBefore, dataAfter));
}
return result;
}
/**
* 获取改变前的数据,将canal消息数据转为bean(只赋值private、public、protected属性,不赋值static、final等其他属性)
* 注意:属性名不能包含特殊字符
*
* @param rowChange
* @param clz
* @return
* @throws IllegalAccessException 参数为空时会抛异常
* @throws InstantiationException
*/
public List getChangesBefore(CanalEntry.RowChange rowChange, Class clz) throws IllegalAccessException, InstantiationException {
return getChangesBeforeOrAfter(rowChange, clz, true);
}
/**
* 获取改变后的数据,将canal消息数据转为bean(只赋值private、public、protected属性,不赋值static、final等其他属性)
* 注意:属性名不能包含特殊字符
*
* @param rowChange
* @param clz
* @return
* @throws IllegalAccessException 参数为空时会抛异常
* @throws InstantiationException
*/
public List getChangesAfter(CanalEntry.RowChange rowChange, Class clz) throws IllegalAccessException, InstantiationException {
return getChangesBeforeOrAfter(rowChange, clz, false);
}
/**
* bean初始化完成后注册需要的转换器,已默认添加Date(格式:yyyy-MM-dd HH:mm:ss)转换器
* 在方法体内,如下使用:
*
* addConverter(new Converter() {
*
* });
*
*/
@PostConstruct
protected void registerConverters() {
}
/**
* 给class对应field设置别名
*/
@PostConstruct
protected void aliasClzFields() {
}
/**
* 给field名称设置别名,将忽略大小写并去除下划线
* 不建议业务逻辑中设置别名,请重写aliasClzFields方法
*
* 如:canal msg: {“birth_day”:"1970-01-01 00:00:00"}
* bean中属性为 birth
* 那么 aliasField(Bean.class, birth, birth_day) 或者 aliasField(Bean.class, birth, birthday)等
*
*
* @param clz
* @param originName
* @param aliasName
* @param
*/
protected void aliasField(Class clz, String originName, String aliasName) {
Map clzFields = getClzFields(clz);
aliasName = aliasName.toLowerCase().replace("_", "");
clzFields.put(aliasName, clzFields.get(originName.toLowerCase()));
}
/**
* 注册自定义配置
* 不可直接调用,请重写registerConverters()
*
* @param converter
* @see com.tqmall.lsc.mq_canallog.impl.AbstractCanalLogMsgProcessor#registerConverters()
*/
protected void addConverter(Converter converter) {
conversionService.addConverter(converter);
}
private List getChangesBeforeOrAfter(CanalEntry.RowChange rowChange, Class clz, boolean isBefore) throws InstantiationException, IllegalAccessException {
if (rowChange == null || clz == null || rowChange.getRowDatasList() == null) {
throw new IllegalArgumentException("rowChange or clz can't be empty.");
}
Map beanFields = getClzFields(clz);
List result = Lists.newArrayList();
for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
List columnsList = isBefore ? rowData.getBeforeColumnsList() : rowData.getAfterColumnsList();
T data = convertRowData(columnsList, beanFields, clz);
result.add(data);
}
return result;
}
private Map getClzFields(Class clz) {
Map beanFields = cachedClzFields.get(clz.getName());
if (beanFields == null || beanFields.size() <= 0) {
beanFields = getAllFieldsForBean(clz);
cachedClzFields.putIfAbsent(clz.getName(), beanFields);
beanFields = cachedClzFields.get(clz.getName());
}
return beanFields;
}
private T convertRowData(List cols, Map beanFields, Class clz) throws IllegalAccessException, InstantiationException {
if (CollectionUtils.isEmpty(cols)) {
return null;
}
T bean = clz.newInstance();
for (CanalEntry.Column col : cols) {
String name = col.getName().toLowerCase().replace("_", "");
String value = col.getValue();
Field field = beanFields.get(name);
if (field == null) {
continue;
}
field.set(bean, value == null ? null : conversionService.convert(value, field.getType()));
}
return bean;
}
private Map getAllFieldsForBean(Class clz) {
Map result = Maps.newHashMap();
Class tmpClz = clz;
// 不获取Object层的属性
String finalParent = "java.lang.object";
while (tmpClz != null && !tmpClz.getName().toLowerCase().equals(finalParent)) {
// 只获取bean普通属性
for (Field field : tmpClz.getDeclaredFields()) {
// 不在设置数据时设置访问权限
field.setAccessible(true);
int modifiers = field.getModifiers();
if (modifiers == Modifier.PUBLIC || modifiers == Modifier.PRIVATE || modifiers == Modifier.PROTECTED) {
result.put(field.getName().toLowerCase(), field);
}
}
tmpClz = tmpClz.getSuperclass();
}
return result;
}
@Getter
@Setter
public static class RowDataPair {
private T before;
private T after;
public RowDataPair(T before, T after) {
this.before = before;
this.after = after;
}
}
}
github : https://github.com/hzhqk/java/blob/master/util/canal/AbstractCanalLogMsgProcessor.java