Canal消息转JavaBean实现参考

  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<String, Date>() {
                @Override
                public Date convert(String source) {
                    if (StringUtils.isBlank(source)) {
                        return null;
                    }
                    return DateUtils.convertStringToDate(source);
                }
            });
        }
    };

    private ConcurrentHashMap<String, Map<String, Field>> cachedClzFields = new ConcurrentHashMap<>();


    /**
     * 获取改变<b>前后</b>的数据,将canal消息数据转为bean(只赋值private、public、protected属性,不赋值static、final等其他属性)
     * 注意:属性名不能包含特殊字符
     *
     * @param rowChange
     * @param clz
     * @return
     * @throws IllegalAccessException 参数为空时会抛异常
     * @throws InstantiationException
     */
    public <T> List<RowDataPair<T>> getChanges(CanalEntry.RowChange rowChange, Class<T> clz) throws InstantiationException, IllegalAccessException {
        if (rowChange == null || clz == null || rowChange.getRowDatasList() == null) {
            throw new IllegalArgumentException("rowChange or clz can't be empty.");
        }
        Map<String, Field> beanFields = getClzFields(clz);
        List<RowDataPair<T>> 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;
    }

    /**
     * 获取改变<b>前</b>的数据,将canal消息数据转为bean(只赋值private、public、protected属性,不赋值static、final等其他属性)
     * 注意:属性名不能包含特殊字符
     *
     * @param rowChange
     * @param clz
     * @return
     * @throws IllegalAccessException 参数为空时会抛异常
     * @throws InstantiationException
     */
    public <T> List<T> getChangesBefore(CanalEntry.RowChange rowChange, Class<T> clz) throws IllegalAccessException, InstantiationException {
        return getChangesBeforeOrAfter(rowChange, clz, true);
    }

    /**
     * 获取改变<b>后</b>的数据,将canal消息数据转为bean(只赋值private、public、protected属性,不赋值static、final等其他属性)
     * 注意:属性名不能包含特殊字符
     *
     * @param rowChange
     * @param clz
     * @return
     * @throws IllegalAccessException 参数为空时会抛异常
     * @throws InstantiationException
     */
    public <T> List<T> getChangesAfter(CanalEntry.RowChange rowChange, Class<T> clz) throws IllegalAccessException, InstantiationException {
        return getChangesBeforeOrAfter(rowChange, clz, false);
    }

    /**
     * bean初始化完成后注册需要的转换器,已默认添加Date(格式:yyyy-MM-dd HH:mm:ss)转换器
     * 在方法体内,如下使用:
     * <pre>
     *    addConverter(new Converter<String, Date>() {
     *
     *    });
     * </pre>
     */
    @PostConstruct
    protected void registerConverters() {

    }

    /**
     * 给class对应field设置别名
     */
    @PostConstruct
    protected void aliasClzFields() {

    }

    /**
     * 给field名称设置别名,将忽略大小写并去除下划线
     * 不建议业务逻辑中设置别名,请重写aliasClzFields方法
     * <pre>
     * 如:canal msg:  {“birth_day”:"1970-01-01 00:00:00"}
     *    bean中属性为 birth
     *    那么 aliasField(Bean.class, birth, birth_day) 或者 aliasField(Bean.class, birth, birthday)等
     * </pre>
     *
     * @param clz
     * @param originName
     * @param aliasName
     * @param <T>
     */
    protected <T> void aliasField(Class<T> clz, String originName, String aliasName) {
        Map<String, Field> 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 <T> List<T> getChangesBeforeOrAfter(CanalEntry.RowChange rowChange, Class<T> 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<String, Field> beanFields = getClzFields(clz);
        List<T> result = Lists.newArrayList();
        for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
            List<CanalEntry.Column> columnsList = isBefore ? rowData.getBeforeColumnsList() : rowData.getAfterColumnsList();
            T data = convertRowData(columnsList, beanFields, clz);
            result.add(data);
        }
        return result;
    }

    private <T> Map<String, Field> getClzFields(Class<T> clz) {
        Map<String, Field> 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> T convertRowData(List<CanalEntry.Column> cols, Map<String, Field> beanFields, Class<T> 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 <T> Map<String, Field> getAllFieldsForBean(Class<T> clz) {
        Map<String, Field> 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<T> {
        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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335