前言
最近,业务上了个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都是阿里开源的吧,这都不给老东家支持支持,真的有点说不过去。
于是又去仔细翻了翻源码,又在网上找了找。不知道是不是能力有限确实没找到,但我耐心有限了。光找解决办法的这半天时间,说不定自己都能写出来了,又少了半天摸鱼时间,那个气啊。
思路
不过敢情好它还实现了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的。
这简单,直接复制一个
@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
好,启动走一波。
成功了,也没成功。东西是打出来,说明确实是走到了我自定义的处理类上,但是修改前的数据呢,怎么全是null,修改后的就只有一个id。
看来并没有想象中简单。
回到我自己的RocketCanalClient类上,一步步断点看看哪一步有问题。
首先排除是否是消息内容丢了,看了,接收到的消息没问题。
那么就只有看看messageHandler.handleMessage(data)在干嘛了。
最终处理是在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);
}
}
总结
测试了下效果,还是很不错的,需要监听表的时候就创建一个表对应的实体,然后实现一个对应处理器。总的来说这次改造没花多少时间,又可以报一周工作量继续摸鱼了。
源码