老司机开发技巧,如何扩展三方包功能(二)

474 阅读6分钟

前言

最近,业务上了个canal,通过roaketMQ同步数据。本来也没什么,非常常见的场景,用的也比较多了。也是直接上手开始干。然后就发现了canal-spring-boot-starter下面的一些用法,觉得很不错,很符合自己的使用预期,先来简单介绍下。

用法

1.引入包

<dependency>  
<groupId>top.javatool</groupId>  
<artifactId>canal-spring-boot-starter</artifactId>  
<version>1.2.1-RELEASE</version>  
</dependency>

2.创建实体映射

@Data  
public class User implements Serializable {
    private Long id;
    
    private String userName;
    
    private Date createDate;
}

3.创建处理类

@Slf4j  
@Component  
@CanalTable(value = "sys_user")  
public class UserHandler implements EntryHandler<User> {  
    @Override  
    public void insert(User data) {  
        log.info("添加:" + JSON.toJSON(data));  
    }  
  
    @Override  
    public void update(User before, User after) {  
        log.info("改前:" + JSON.toJSON(before));  
        log.info("改后:" + JSON.toJSON(after));  
    }  
    @Override  
    public void delete(User data) {  
        log.info("删除:" + JSON.toJSON(data));  
    }  
}

4.接收事件数据
???
好了,没了。因为我发现这个包默认的连接方式是tcp,而实际上我们大部分时候使用canal都是走的mq。想想spring出品的没理由只支持个基础版本,就去对应的包下面找找。
好消息是确实有支持mq形式的,坏消息是只支持kafka。
什么鬼?我记得rocketmq、canal都是阿里开源的吧,这都不给老东家支持支持,真的有点说不过去。

微信截图_20240823152938.png 于是又去仔细翻了翻源码,又在网上找了找。不知道是不是能力有限确实没找到,但我耐心有限了。光找解决办法的这半天时间,说不定自己都能写出来了,又少了半天摸鱼时间,那个气啊。

思路

不过敢情好它还实现了kafka,要是没有,那这玩意真的毫无意义。
让我们先看看kafaka怎么搞的,都是mq,照搬就完了。

    public void process() {
        KafkaCanalConnector connector = (KafkaCanalConnector)this.getConnector();
        MessageHandler messageHandler = this.getMessageHandler();

        while(this.flag) {
            try {
                connector.connect();
                connector.subscribe();

                while(this.flag) {
                    try {
                        List<FlatMessage> messages = connector.getFlatListWithoutAck(this.timeout, this.unit);
                        this.log.info("获取消息 {}", messages);
                        if (messages != null) {
                            Iterator var4 = messages.iterator();

                            while(var4.hasNext()) {
                                FlatMessage flatMessage = (FlatMessage)var4.next();
                                messageHandler.handleMessage(flatMessage);
                            }
                        }

                        connector.ack();
                    } catch (Exception var6) {
                        this.log.error("canal 消费异常", var6);
                    }
                }
            } catch (Exception var7) {
                this.log.error("canal 连接异常", var7);
            }
        }

        connector.unsubscribe();
        connector.disconnect();
    }

核心方法就这个,接受消息,处理请求,没什么大不了的,主要调的是messageHandler.handleMessage(flatMessage)这个方法。另外一点是继承了AbstractCanalClient这个类。
照猫画虎,按着rocketmq的方式写一个。

@Component
@Slf4j
@RocketMQMessageListener(topic = MqTopicConstant.canal_topic, consumerGroup = MqTopicConstant.canal_group, selectorExpression = MqTopicConstant.canal_tag)
public class RocketCanalClient implements RocketMQListener<MessageExt> {
    @Autowired
    private MessageHandler messageHandler;

    @Override
    public void onMessage(MessageExt messageExt) {
        byte[] body = messageExt.getBody();
        String msg = new String(body);
        log.info("[{}]监听到消息:msg:{}", messageExt.getMsgId(), msg);
        if (StrUtil.isNotBlank(msg)) {
            AsyncMsgDTO data = JSON.parseObject(msg, AsyncMsgDTO.class);
            messageHandler.handleMessage(data);
        }
    }
}

没什么问题,先走一个。很好启动不了,给我报了个什么SimpleCanalClient类找不到,需要实例化。
见都没见过这个类,先点进去看看。
没什么特殊的东西,看上去像是直接用tcp方式处理的类,同样继承了AbstractCanalClient。懂了,都是些老套路,肯定是哪个地方需要去动态选取需要的处理类。
跟着报错信息一路往下看,发现最早报错的地方是在SimpleClientAutoConfiguration这里,它自带的配置类上。

@Configuration
@EnableConfigurationProperties({CanalSimpleProperties.class})
@ConditionalOnBean({EntryHandler.class})
@ConditionalOnProperty(
    value = {"canal.mode"},
    havingValue = "simple",
    matchIfMissing = true
)
@Import({ThreadPoolAutoConfiguration.class})
public class SimpleClientAutoConfiguration {
	...
}

原来这还有个配置,默认走simple方式,就是tcp形式。
好好,顺便也看到了kafka的。

微信截图_20240823154613.png 这简单,直接复制一个

@Configuration
@EnableConfigurationProperties({CanalKafkaProperties.class})
@ConditionalOnBean({EntryHandler.class})
@ConditionalOnProperty(
    value = {"canal.mode"},
    havingValue = "rocketMQ"
)
@Import({ThreadPoolAutoConfiguration.class})
public class RocketClientAutoConfiguration {

    @Bean
    public RowDataHandler<List<Map<String, String>>> rowDataHandler() {
        return new MapRowDataHandlerImpl(new MapColumnModelFactory());
    }

    @Bean
    @ConditionalOnProperty(
        value = {"canal.async"},
        havingValue = "true",
        matchIfMissing = true
    )
    public MessageHandler messageHandler(RowDataHandler<List<Map<String, String>>> rowDataHandler, List<EntryHandler> entryHandlers, ExecutorService executorService) {
        return new AsyncFlatMessageHandlerImpl(entryHandlers, rowDataHandler, executorService);
    }

    @Bean
    @ConditionalOnProperty(
        value = {"canal.async"},
        havingValue = "false"
    )
    public MessageHandler messageHandler(RowDataHandler<List<Map<String, String>>> rowDataHandler, List<EntryHandler> entryHandlers) {
        return new SyncFlatMessageHandlerImpl(entryHandlers, rowDataHandler);
    }

kafka配置项先不要了,该删的删,该改的改。
修改xml配置

canal:  
    mode: rocketMQ

好,启动走一波。

image.png

成功了,也没成功。东西是打出来,说明确实是走到了我自定义的处理类上,但是修改前的数据呢,怎么全是null,修改后的就只有一个id。
看来并没有想象中简单。
回到我自己的RocketCanalClient类上,一步步断点看看哪一步有问题。
首先排除是否是消息内容丢了,看了,接收到的消息没问题。
那么就只有看看messageHandler.handleMessage(data)在干嘛了。

image.png

image.png

image.png

image.png 最终处理是在MapColumnModelFactory类下的这个方法执行完的,会将解析后的消息转换成对应实体,valueMap就是解析后的消息数据,c就是需要转换的实体class。
虽然不想说,但这写的是不是脱产。
消息中的字段为

{
    "id": 1,
    "user_name": "123",
    "create_date": "2024-08-23 16:19:01"
}

数据库表字段用的是下划线命名,实体字段用的驼峰法。两个字段名能这样匹配上才有鬼呢,难怪只有id是对的。
第二个问题,修改前的旧数据消息只传了修改的数据,所以只能将修改了字段的值设置到实体,没改的就不能。所以修改前的日志打出来全是null。

{
    "user_name": "323",
    "create_date": "2024-08-23 16:20:01"
}

这也让我觉得有点恼火,万一改成null,那我怎么知道修改前的字段是因为没改是null还是数据库改成了null?还要和修改后的数据去一一比对吗?
所以求人不如靠自己,还是只有自己写。
先实现IModelFactory接口,把字段名称匹配不上的问题改了。

public class DataMapColumnModelFactory implements IModelFactory<Map<String, String>> {

    public DataMapColumnModelFactory() {
    }

    @Override
    public <R> R newInstance(EntryHandler entryHandler, Map<String, String> t) throws Exception {
        String canalTableName = HandlerUtil.getCanalTableName(entryHandler);
        if (TableNameEnum.ALL.name().toLowerCase().equals(canalTableName)) {
            return (R) t;
        } else {
            Class<R> tableClass = GenericUtil.getTableClass(entryHandler);
            return tableClass != null ? this.newInstance(tableClass, t) : null;
        }
    }

    /**
     * 字段转换实现
     * @param c
     * @param valueMap
     * @return
     * @param <R>
     * @throws Exception
     */
    public <R> R newInstance(Class<R> c, Map<String, String> valueMap) throws Exception {
        R object = c.newInstance();
        Map<String, String> columnNames = EntryUtil.getFieldName(object.getClass());
        Iterator var5 = valueMap.entrySet().iterator();

        while(var5.hasNext()) {
            Map.Entry<String, String> entry = (Map.Entry)var5.next();
            String fieldName = this.getFieldName(columnNames, entry);
            if (StringUtils.isNotEmpty(fieldName)) {
                FieldUtil.setFieldValue(object, fieldName, (String)entry.getValue());
            }
        }

        return object;
    }

    /**
     * 获取字段名称,主要是下划线转驼峰命名法,由于实体和表字段命名方式可能不一致
     * @param columnNames
     * @param entry
     * @return
     */
    private String getFieldName(Map<String, String> columnNames, Map.Entry<String, String> entry) {
        String entryKey = entry.getKey();
        String fileName = columnNames.get(entryKey);
        if (StringUtils.isNotBlank(fileName)) {
            return fileName;
        }
        if (entryKey.contains("_")) {
            entryKey = StrUtil.toCamelCase(entryKey);
        }
        return columnNames.get(entryKey);
    }
}

再来实现RowDataHandler接口,修改下处理数据的方式

public class DataMapRowDataHandlerImpl implements RowDataHandler<List<Map<String, String>>> {
    private IModelFactory<Map<String, String>> modelFactory;

    public DataMapRowDataHandlerImpl(IModelFactory<Map<String, String>> modelFactory) {
        this.modelFactory = modelFactory;
    }

    /**
     * 自定义数据处理
     * @param list
     * @param entryHandler
     * @param eventType
     * @param <R>
     * @throws Exception
     */
    @Override
    public <R> void handlerRowData(List<Map<String, String>> list, EntryHandler<R> entryHandler, CanalEntry.EventType eventType) throws Exception {
        if (entryHandler != null) {
            switch (eventType) {
                case INSERT:
                    R entry = this.modelFactory.newInstance(entryHandler, list.get(0));
                    entryHandler.insert(entry);
                    break;
                case UPDATE:
                    Map<String, String> afterMap = list.get(0);
                    Map<String, String> beforeMap = this.buildAllBeforeMap(afterMap, list.get(1));
                    R before = this.modelFactory.newInstance(entryHandler, beforeMap);
                    R after = this.modelFactory.newInstance(entryHandler, afterMap);
                    entryHandler.update(before, after);
                    break;
                case DELETE:
                    R o = this.modelFactory.newInstance(entryHandler, list.get(0));
                    entryHandler.delete(o);
            }
        }

    }

    /**
     * 构建完整的old数据
     * @param afterMap
     * @param beforeMap
     * @return
     */
    private Map<String, String> buildAllBeforeMap(Map<String, String> afterMap, Map<String, String> beforeMap) {
        if (CollUtil.isEmpty(afterMap) || CollUtil.isEmpty(beforeMap)) {
            return beforeMap;
        }
        for (Map.Entry<String, String> item : afterMap.entrySet()) {
            if (!beforeMap.containsKey(item.getKey())) {
                beforeMap.put(item.getKey(), item.getValue());
            }
        }
        return beforeMap;
    }

}

最后修改配置类,将我们自己实现的工厂和处理器注入进去

@Configuration
@EnableConfigurationProperties({CanalKafkaProperties.class})
@ConditionalOnBean({EntryHandler.class})
@ConditionalOnProperty(
        value = {"canal.mode"},
        havingValue = "rocketMQ"
)
@Import({ThreadPoolAutoConfiguration.class})
public class RocketMQClientAutoConfiguration {
    @Bean
    public RowDataHandler<List<Map<String, String>>> rowDataHandler() {
        return new DataMapRowDataHandlerImpl(new DataMapColumnModelFactory());
    }

    @Bean
    @ConditionalOnProperty(
            value = {"canal.async"},
            havingValue = "true",
            matchIfMissing = true
    )
    public MessageHandler messageHandler(RowDataHandler<List<Map<String, String>>> rowDataHandler, List<EntryHandler> entryHandlers, ExecutorService executorService) {
        return new AsyncFlatMessageHandlerImpl(entryHandlers, rowDataHandler, executorService);
    }

    @Bean
    @ConditionalOnProperty(
            value = {"canal.async"},
            havingValue = "false"
    )
    public MessageHandler messageHandler(RowDataHandler<List<Map<String, String>>> rowDataHandler, List<EntryHandler> entryHandlers) {
        return new SyncFlatMessageHandlerImpl(entryHandlers, rowDataHandler);
    }
}

总结

测试了下效果,还是很不错的,需要监听表的时候就创建一个表对应的实体,然后实现一个对应处理器。总的来说这次改造没花多少时间,又可以报一周工作量继续摸鱼了。
源码