前提

这是一篇憋了很久的文章,一直想写,却又一直忘记了写。整篇文章可能会有点流水账,相对详细地介绍怎么写一个小型的"框架"。这个精悍的胶水层已经在生产环境服役超过半年,这里尝试把耦合业务的代码去掉,提炼出一个相对简洁的版本。

之前写的几篇文章里面其中一篇曾经提到过Canal解析MySQLbinlog事件后的对象如下(来源于Canal源码com.alibaba.otter.canal.protocol.FlatMessage):

如果直接对此原始对象进行解析,那么会出现很多解析模板代码,一旦有改动就会牵一发动全身,这是我们不希望发生的一件事。于是花了一点点时间写了一个Canal胶水层,让接收到的FlatMessage根据表名称直接转换为对应的DTO实例,这样能在一定程度上提升开发效率并且减少模板化代码,这个胶水层的数据流示意图如下:

要编写这样的胶水层主要用到:

  • 反射。
  • 注解。
  • 策略模式。
  • IOC容器(可选)。

项目的模块如下:

  • canal-glue-core:核心功能。
  • spring-boot-starter-canal-glue:适配SpringIOC容器,添加自动配置。
  • canal-glue-example:使用例子和基准测试。

下文会详细分析此胶水层如何实现。

引入依赖

为了不污染引用此模块的外部服务依赖,除了JSON转换的依赖之外,其他依赖的scope定义为provide或者test类型,依赖版本和BOM如下:

  1. <properties>
  2. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  3. <maven.compiler.source>1.8</maven.compiler.source>
  4. <maven.compiler.target>1.8</maven.compiler.target>
  5. <spring.boot.version>2.3.0.RELEASE</spring.boot.version>
  6. <maven.compiler.plugin.version>3.8.1</maven.compiler.plugin.version>
  7. <lombok.version>1.18.12</lombok.version>
  8. <fastjson.version>1.2.73</fastjson.version>
  9. </properties>
  10. <dependencyManagement>
  11. <dependencies>
  12. <dependency>
  13. <groupId>org.springframework.boot</groupId>
  14. <artifactId>spring-boot-dependencies</artifactId>
  15. <version>${spring.boot.version}</version>
  16. <scope>import</scope>
  17. <type>pom</type>
  18. </dependency>
  19. </dependencies>
  20. </dependencyManagement>
  21. <dependencies>
  22. <dependency>
  23. <groupId>org.projectlombok</groupId>
  24. <artifactId>lombok</artifactId>
  25. <version>${lombok.version}</version>
  26. <scope>provided</scope>
  27. </dependency>
  28. <dependency>
  29. <groupId>org.springframework.boot</groupId>
  30. <artifactId>spring-boot-starter-test</artifactId>
  31. <scope>test</scope>
  32. </dependency>
  33. <dependency>
  34. <groupId>org.springframework.boot</groupId>
  35. <artifactId>spring-boot-starter</artifactId>
  36. <scope>provided</scope>
  37. </dependency>
  38. <dependency>
  39. <groupId>com.alibaba</groupId>
  40. <artifactId>fastjson</artifactId>
  41. <version>${fastjson.version}</version>
  42. </dependency>
  43. </dependencies>

其中,canal-glue-core模块本质上只依赖于fastjson,可以完全脱离spring体系使用。

基本架构

这里提供一个"后知后觉"的架构图,因为之前为了快速怼到线上,初版没有考虑这么多,甚至还耦合了业务代码,组件是后来抽离出来的:

设计配置模块(已经移除)

设计配置模块在设计的时候考虑使用了外置配置文件和纯注解两种方式,前期使用了JSON外置配置文件的方式,纯注解是后来增加的,二选一。这一节简单介绍一下JSON外置配置文件的配置加载,纯注解留到后面处理器模块时候分析。

当初是想快速进行胶水层的开发,所以配置文件使用了可读性比较高的JSON格式:

  1. {
  2. "version": 1,
  3. "module": "canal-glue",
  4. "databases": [
  5. {
  6. "database": "db_payment_service",
  7. "processors": [
  8. {
  9. "table": "payment_order",
  10. "processor": "x.y.z.PaymentOrderProcessor",
  11. "exceptionHandler": "x.y.z.PaymentOrderExceptionHandler"
  12. }
  13. ]
  14. },
  15. {
  16. ......
  17. }
  18. ]
  19. }

JSON配置在设计的时候尽可能不要使用JSON Array作为顶层配置,因为这样做设计的对象会比较怪

因为使用该模块的应用有可能需要处理Canal解析多个上游数据库的binlog事件,所以配置模块设计的时候需要以databaseKEY,挂载多个table以及对应的表binlog事件处理器以及异常处理器。然后对着JSON文件的格式撸一遍对应的实体类出来:

  1. @Data
  2. public class CanalGlueProcessorConf {
  3. private String table;
  4. private String processor;
  5. private String exceptionHandler;
  6. }
  7. @Data
  8. public class CanalGlueDatabaseConf {
  9. private String database;
  10. private List<CanalGlueProcessorConf> processors;
  11. }
  12. @Data
  13. public class CanalGlueConf {
  14. private Long version;
  15. private String module;
  16. private List<CanalGlueDatabaseConf> database;
  17. }

实体编写完,接着可以编写一个配置加载器,简单起见,配置文件直接放ClassPath之下,加载器如下:

  1. public interface CanalGlueConfLoader {
  2. CanalGlueConf load(String location);
  3. }
  4. // 实现
  5. public class ClassPathCanalGlueConfLoader implements CanalGlueConfLoader {
  6. @Override
  7. public CanalGlueConf load(String location) {
  8. ClassPathResource resource = new ClassPathResource(location);
  9. Assert.isTrue(resource.exists(), String.format("类路径下不存在文件%s", location));
  10. try {
  11. String content = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
  12. return JSON.parseObject(content, CanalGlueConf.class);
  13. } catch (IOException e) {
  14. // should not reach
  15. throw new IllegalStateException(e);
  16. }
  17. }
  18. }

读取ClassPath下的某个location为绝对路径的文件内容字符串,然后使用Fasfjson转成CanalGlueConf对象。这个是默认的实现,使用canal-glue模块可以覆盖此实现,通过自定义的实现加载配置。

JSON配置模块在后来从业务系统抽离此胶水层的时候已经完全废弃,使用纯注解驱动和核心抽象组件继承的方式实现。

核心模块开发

主要包括几个模块:

  • 基本模型定义。
  • 适配器层开发。
  • 转换器和解析器层开发。
  • 处理器层开发。
  • 全局组件自动配置模块开发(仅限于Spring体系,已经抽取到spring-boot-starter-canal-glue模块)。
  • CanalGlue开发。

基本模型定义

定义顶层的KEY,也就是对于某个数据库的某一个确定的表,需要一个唯一标识:

  1. // 模型表对象
  2. public interface ModelTable {
  3. String database();
  4. String table();
  5. static ModelTable of(String database, String table) {
  6. return DefaultModelTable.of(database, table);
  7. }
  8. }
  9. @RequiredArgsConstructor(access = AccessLevel.PACKAGE, staticName = "of")
  10. public class DefaultModelTable implements ModelTable {
  11. private final String database;
  12. private final String table;
  13. @Override
  14. public String database() {
  15. return database;
  16. }
  17. @Override
  18. public String table() {
  19. return table;
  20. }
  21. @Override
  22. public boolean equals(Object o) {
  23. if (this == o) {
  24. return true;
  25. }
  26. if (o == null || getClass() != o.getClass()) {
  27. return false;
  28. }
  29. DefaultModelTable that = (DefaultModelTable) o;
  30. return Objects.equals(database, that.database) &&
  31. Objects.equals(table, that.table);
  32. }
  33. @Override
  34. public int hashCode() {
  35. return Objects.hash(database, table);
  36. }
  37. }

这里实现类DefaultModelTable重写了equals()hashCode()方法便于把ModelTable实例应用为HashMap容器的KEY,这样后面就可以设计ModelTable -> Processor的缓存结构。

由于Canal投放到Kafka的事件内容是一个原始字符串,所以要定义一个和前文提到的FlatMessage基本一致的事件类CanalBinLogEvent

  1. @Data
  2. public class CanalBinLogEvent {
  3. /**
  4. * 事件ID,没有实际意义
  5. */
  6. private Long id;
  7. /**
  8. * 当前更变后节点数据
  9. */
  10. private List<Map<String, String>> data;
  11. /**
  12. * 主键列名称列表
  13. */
  14. private List<String> pkNames;
  15. /**
  16. * 当前更变前节点数据
  17. */
  18. private List<Map<String, String>> old;
  19. /**
  20. * 类型 UPDATE\INSERT\DELETE\QUERY
  21. */
  22. private String type;
  23. /**
  24. * binlog execute time
  25. */
  26. private Long es;
  27. /**
  28. * dml build timestamp
  29. */
  30. private Long ts;
  31. /**
  32. * 执行的sql,不一定存在
  33. */
  34. private String sql;
  35. /**
  36. * 数据库名称
  37. */
  38. private String database;
  39. /**
  40. * 表名称
  41. */
  42. private String table;
  43. /**
  44. * SQL类型映射
  45. */
  46. private Map<String, Integer> sqlType;
  47. /**
  48. * MySQL字段类型映射
  49. */
  50. private Map<String, String> mysqlType;
  51. /**
  52. * 是否DDL
  53. */
  54. private Boolean isDdl;
  55. }

根据此事件对象,再定义解析完毕后的结果对象CanalBinLogResult

  1. // 常量
  2. @RequiredArgsConstructor
  3. @Getter
  4. public enum BinLogEventType {
  5. QUERY("QUERY", "查询"),
  6. INSERT("INSERT", "新增"),
  7. UPDATE("UPDATE", "更新"),
  8. DELETE("DELETE", "删除"),
  9. ALTER("ALTER", "列修改操作"),
  10. UNKNOWN("UNKNOWN", "未知"),
  11. ;
  12. private final String type;
  13. private final String description;
  14. public static BinLogEventType fromType(String type) {
  15. for (BinLogEventType binLogType : BinLogEventType.values()) {
  16. if (binLogType.getType().equals(type)) {
  17. return binLogType;
  18. }
  19. }
  20. return BinLogEventType.UNKNOWN;
  21. }
  22. }
  23. // 常量
  24. @RequiredArgsConstructor
  25. @Getter
  26. public enum OperationType {
  27. /**
  28. * DML
  29. */
  30. DML("dml", "DML语句"),
  31. /**
  32. * DDL
  33. */
  34. DDL("ddl", "DDL语句"),
  35. ;
  36. private final String type;
  37. private final String description;
  38. }
  39. @Data
  40. public class CanalBinLogResult<T> {
  41. /**
  42. * 提取的长整型主键
  43. */
  44. private Long primaryKey;
  45. /**
  46. * binlog事件类型
  47. */
  48. private BinLogEventType binLogEventType;
  49. /**
  50. * 更变前的数据
  51. */
  52. private T beforeData;
  53. /**
  54. * 更变后的数据
  55. */
  56. private T afterData;
  57. /**
  58. * 数据库名称
  59. */
  60. private String databaseName;
  61. /**
  62. * 表名称
  63. */
  64. private String tableName;
  65. /**
  66. * sql语句 - 一般是DDL的时候有用
  67. */
  68. private String sql;
  69. /**
  70. * MySQL操作类型
  71. */
  72. private OperationType operationType;
  73. }

开发适配器层

定义顶层的适配器SPI接口:

  1. public interface SourceAdapter<SOURCE, SINK> {
  2. SINK adapt(SOURCE source);
  3. }

接着开发适配器实现类:

  1. // 原始字符串直接返回
  2. @RequiredArgsConstructor(access = AccessLevel.PACKAGE, staticName = "of")
  3. class RawStringSourceAdapter implements SourceAdapter<String, String> {
  4. @Override
  5. public String adapt(String source) {
  6. return source;
  7. }
  8. }
  9. // Fastjson转换
  10. @RequiredArgsConstructor(access = AccessLevel.PACKAGE, staticName = "of")
  11. class FastJsonSourceAdapter<T> implements SourceAdapter<String, T> {
  12. private final Class<T> klass;
  13. @Override
  14. public T adapt(String source) {
  15. if (StringUtils.isEmpty(source)) {
  16. return null;
  17. }
  18. return JSON.parseObject(source, klass);
  19. }
  20. }
  21. // Facade
  22. public enum SourceAdapterFacade {
  23. /**
  24. * 单例
  25. */
  26. X;
  27. private static final SourceAdapter<String, String> I_S_A = RawStringSourceAdapter.of();
  28. @SuppressWarnings("unchecked")
  29. public <T> T adapt(Class<T> klass, String source) {
  30. if (klass.isAssignableFrom(String.class)) {
  31. return (T) I_S_A.adapt(source);
  32. }
  33. return FastJsonSourceAdapter.of(klass).adapt(source);
  34. }
  35. }

最终直接使用SourceAdapterFacade#adapt()方法即可,因为实际上绝大多数情况下只会使用原始字符串和String -> Class实例,适配器层设计可以简单点。

开发转换器和解析器层

对于Canal解析完成的binlog事件,dataold属性是K-V结构,并且KEY都是String类型,需要遍历解析才能推导出完整的目标实例。

转换后的实例的属性类型目前只支持包装类,int等原始类型不支持

为了更好地通过目标实体和实际的数据库、表和列名称、列类型进行映射,引入了两个自定义注解CanalModel@CanalField,它们的定义如下:

  1. // @CanalModel
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Target(ElementType.TYPE)
  4. public @interface CanalModel {
  5. /**
  6. * 目标数据库
  7. */
  8. String database();
  9. /**
  10. * 目标表
  11. */
  12. String table();
  13. /**
  14. * 属性名 -> 列名命名转换策略,可选值有:DEFAULT(原始)、UPPER_UNDERSCORE(驼峰转下划线大写)和LOWER_UNDERSCORE(驼峰转下划线小写)
  15. */
  16. FieldNamingPolicy fieldNamingPolicy() default FieldNamingPolicy.DEFAULT;
  17. }
  18. // @CanalField
  19. @Retention(RetentionPolicy.RUNTIME)
  20. @Target(ElementType.FIELD)
  21. public @interface CanalField {
  22. /**
  23. * 行名称
  24. *
  25. * @return columnName
  26. */
  27. String columnName() default "";
  28. /**
  29. * sql字段类型
  30. *
  31. * @return JDBCType
  32. */
  33. JDBCType sqlType() default JDBCType.NULL;
  34. /**
  35. * 转换器类型
  36. *
  37. * @return klass
  38. */
  39. Class<? extends BaseCanalFieldConverter<?>> converterKlass() default NullCanalFieldConverter.class;
  40. }

定义顶层转换器接口BinLogFieldConverter

  1. public interface BinLogFieldConverter<SOURCE, TARGET> {
  2. TARGET convert(SOURCE source);
  3. }

目前暂定可以通过目标属性的Class和通过注解指定的SQLType类型进行匹配,所以再定义一个抽象转换器BaseCanalFieldConverter

  1. public abstract class BaseCanalFieldConverter<T> implements BinLogFieldConverter<String, T> {
  2. private final SQLType sqlType;
  3. private final Class<?> klass;
  4. protected BaseCanalFieldConverter(SQLType sqlType, Class<?> klass) {
  5. this.sqlType = sqlType;
  6. this.klass = klass;
  7. }
  8. @Override
  9. public T convert(String source) {
  10. if (StringUtils.isEmpty(source)) {
  11. return null;
  12. }
  13. return convertInternal(source);
  14. }
  15. /**
  16. * 内部转换方法
  17. *
  18. * @param source 源字符串
  19. * @return T
  20. */
  21. protected abstract T convertInternal(String source);
  22. /**
  23. * 返回SQL类型
  24. *
  25. * @return SQLType
  26. */
  27. public SQLType sqlType() {
  28. return sqlType;
  29. }
  30. /**
  31. * 返回类型
  32. *
  33. * @return Class<?>
  34. */
  35. public Class<?> typeKlass() {
  36. return klass;
  37. }
  38. }

BaseCanalFieldConverter是面向目标实例中的单个属性的,例如对于实例中的Long类型的属性,可以实现一个BigIntCanalFieldConverter

  1. public class BigIntCanalFieldConverter extends BaseCanalFieldConverter<Long> {
  2. /**
  3. * 单例
  4. */
  5. public static final BaseCanalFieldConverter<Long> X = new BigIntCanalFieldConverter();
  6. private BigIntCanalFieldConverter() {
  7. super(JDBCType.BIGINT, Long.class);
  8. }
  9. @Override
  10. protected Long convertInternal(String source) {
  11. if (null == source) {
  12. return null;
  13. }
  14. return Long.valueOf(source);
  15. }
  16. }

其他类型以此类推,目前已经开发好的最常用的内建转换器如下:

JDBCType JAVAType 转换器
NULL Void NullCanalFieldConverter
BIGINT Long BigIntCanalFieldConverter
VARCHAR String VarcharCanalFieldConverter
DECIMAL BigDecimal DecimalCanalFieldConverter
INTEGER Integer IntCanalFieldConverter
TINYINT Integer TinyIntCanalFieldConverter
DATE java.time.LocalDate SqlDateCanalFieldConverter0
DATE java.sql.Date SqlDateCanalFieldConverter1
TIMESTAMP java.time.LocalDateTime TimestampCanalFieldConverter0
TIMESTAMP java.util.Date TimestampCanalFieldConverter1
TIMESTAMP java.time.OffsetDateTime TimestampCanalFieldConverter2

所有转换器实现都设计为无状态的单例,方便做动态注册和覆盖。接着定义一个转换器工厂CanalFieldConverterFactory,提供API通过指定参数加载目标转换器实例:

  1. // 入参
  2. @SuppressWarnings("rawtypes")
  3. @Builder
  4. @Data
  5. public class CanalFieldConvertInput {
  6. private Class<?> fieldKlass;
  7. private Class<? extends BaseCanalFieldConverter> converterKlass;
  8. private SQLType sqlType;
  9. @Tolerate
  10. public CanalFieldConvertInput() {
  11. }
  12. }
  13. // 结果
  14. @Builder
  15. @Getter
  16. public class CanalFieldConvertResult {
  17. private final BaseCanalFieldConverter<?> converter;
  18. }
  19. // 接口
  20. public interface CanalFieldConverterFactory {
  21. default void registerConverter(BaseCanalFieldConverter<?> converter) {
  22. registerConverter(converter, true);
  23. }
  24. void registerConverter(BaseCanalFieldConverter<?> converter, boolean replace);
  25. CanalFieldConvertResult load(CanalFieldConvertInput input);
  26. }

CanalFieldConverterFactory提供了可以注册自定义转化器的registerConverter()方法,这样就可以让使用者注册自定义的转换器和覆盖默认的转换器。

至此,可以通过指定的参数,加载实例属性的转换器,拿到转换器实例,就可以针对目标实例,从原始事件中解析对应的K-V结构。接着需要编写最核心的解析器模块,此模块主要包含三个方面:

  • 唯一BIGINT类型主键的解析(这一点是公司技术规范的一条铁规则,MySQL每个表只能定义唯一的BIGINT UNSIGNED自增趋势主键)。
  • 更变前的数据,对应于原始事件中的old属性节点(不一定存在,例如INSERT语句中不存在此属性节点)。
  • 更变后的数据,对应于原始事件中的data属性节点。

定义解析器接口CanalBinLogEventParser如下:

  1. public interface CanalBinLogEventParser {
  2. /**
  3. * 解析binlog事件
  4. *
  5. * @param event 事件
  6. * @param klass 目标类型
  7. * @param primaryKeyFunction 主键映射方法
  8. * @param commonEntryFunction 其他属性映射方法
  9. * @return CanalBinLogResult
  10. */
  11. <T> List<CanalBinLogResult<T>> parse(CanalBinLogEvent event,
  12. Class<T> klass,
  13. BasePrimaryKeyTupleFunction primaryKeyFunction,
  14. BaseCommonEntryFunction<T> commonEntryFunction);
  15. }

解析器的解析方法依赖于:

  • binlog事件实例,这个是上游的适配器组件的结果。
  • 转换的目标类型。
  • BasePrimaryKeyTupleFunction主键映射方法实例,默认使用内建的BigIntPrimaryKeyTupleFunction
  • BaseCommonEntryFunction非主键通用列-属性映射方法实例,默认使用内建的ReflectionBinLogEntryFunction这个是非主键列的转换核心,里面使用到了反射)。

解析返回结果是一个List,原因是FlatMessage在批量写入的时候的数据结构本来就是一个List<Map<String,String>>,这里只是"顺水推舟"。

开发处理器层

处理器是开发者处理最终解析出来的实体的入口,只需要面向不同类型的事件选择对应的处理方法即可,看起来如下:

  1. public abstract class BaseCanalBinlogEventProcessor<T> extends BaseParameterizedTypeReferenceSupport<T> {
  2. protected void processInsertInternal(CanalBinLogResult<T> result) {
  3. }
  4. protected void processUpdateInternal(CanalBinLogResult<T> result) {
  5. }
  6. protected void processDeleteInternal(CanalBinLogResult<T> result) {
  7. }
  8. protected void processDDLInternal(CanalBinLogResult<T> result) {
  9. }
  10. }

例如需要处理Insert事件,则子类继承BaseCanalBinlogEventProcessor,对应的实体类(泛型的替换)使用@CanalModel注解声明,然后覆盖processInsertInternal()方法即可。期间子处理器可以覆盖自定义异常处理器实例,如:

  1. @Override
  2. protected ExceptionHandler exceptionHandler() {
  3. return EXCEPTION_HANDLER;
  4. }
  5. /**
  6. * 覆盖默认的ExceptionHandler.NO_OP
  7. */
  8. private static final ExceptionHandler EXCEPTION_HANDLER = (event, throwable)
  9. -> log.error("解析binlog事件出现异常,事件内容:{}", JSON.toJSONString(event), throwable);

另外,有些场景需要对回调前或者回调后的结果做特化处理,因此引入了解析结果拦截器(链)的实现,对应的类是BaseParseResultInterceptor

  1. public abstract class BaseParseResultInterceptor<T> extends BaseParameterizedTypeReferenceSupport<T> {
  2. public BaseParseResultInterceptor() {
  3. super();
  4. }
  5. public void onParse(ModelTable modelTable) {
  6. }
  7. public void onBeforeInsertProcess(ModelTable modelTable, T beforeData, T afterData) {
  8. }
  9. public void onAfterInsertProcess(ModelTable modelTable, T beforeData, T afterData) {
  10. }
  11. public void onBeforeUpdateProcess(ModelTable modelTable, T beforeData, T afterData) {
  12. }
  13. public void onAfterUpdateProcess(ModelTable modelTable, T beforeData, T afterData) {
  14. }
  15. public void onBeforeDeleteProcess(ModelTable modelTable, T beforeData, T afterData) {
  16. }
  17. public void onAfterDeleteProcess(ModelTable modelTable, T beforeData, T afterData) {
  18. }
  19. public void onBeforeDDLProcess(ModelTable modelTable, T beforeData, T afterData, String sql) {
  20. }
  21. public void onAfterDDLProcess(ModelTable modelTable, T beforeData, T afterData, String sql) {
  22. }
  23. public void onParseFinish(ModelTable modelTable) {
  24. }
  25. public void onParseCompletion(ModelTable modelTable) {
  26. }
  27. }

解析结果拦截器的回调时机可以参看上面的架构图或者BaseCanalBinlogEventProcessor的源代码。

开发全局组件自动配置模块

如果使用了Spring容器,需要添加一个配置类来加载所有既有的组件,添加一个全局配置类CanalGlueAutoConfiguration(这个类可以在项目的spring-boot-starter-canal-glue模块中看到,这个模块就只有一个类):

  1. @Configuration
  2. public class CanalGlueAutoConfiguration implements SmartInitializingSingleton, BeanFactoryAware {
  3. private ConfigurableListableBeanFactory configurableListableBeanFactory;
  4. @Bean
  5. @ConditionalOnMissingBean
  6. public CanalBinlogEventProcessorFactory canalBinlogEventProcessorFactory() {
  7. return InMemoryCanalBinlogEventProcessorFactory.of();
  8. }
  9. @Bean
  10. @ConditionalOnMissingBean
  11. public ModelTableMetadataManager modelTableMetadataManager(CanalFieldConverterFactory canalFieldConverterFactory) {
  12. return InMemoryModelTableMetadataManager.of(canalFieldConverterFactory);
  13. }
  14. @Bean
  15. @ConditionalOnMissingBean
  16. public CanalFieldConverterFactory canalFieldConverterFactory() {
  17. return InMemoryCanalFieldConverterFactory.of();
  18. }
  19. @Bean
  20. @ConditionalOnMissingBean
  21. public CanalBinLogEventParser canalBinLogEventParser() {
  22. return DefaultCanalBinLogEventParser.of();
  23. }
  24. @Bean
  25. @ConditionalOnMissingBean
  26. public ParseResultInterceptorManager parseResultInterceptorManager(ModelTableMetadataManager modelTableMetadataManager) {
  27. return InMemoryParseResultInterceptorManager.of(modelTableMetadataManager);
  28. }
  29. @Bean
  30. @Primary
  31. public CanalGlue canalGlue(CanalBinlogEventProcessorFactory canalBinlogEventProcessorFactory) {
  32. return DefaultCanalGlue.of(canalBinlogEventProcessorFactory);
  33. }
  34. @Override
  35. public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
  36. this.configurableListableBeanFactory = (ConfigurableListableBeanFactory) beanFactory;
  37. }
  38. @SuppressWarnings({"rawtypes", "unchecked"})
  39. @Override
  40. public void afterSingletonsInstantiated() {
  41. ParseResultInterceptorManager parseResultInterceptorManager
  42. = configurableListableBeanFactory.getBean(ParseResultInterceptorManager.class);
  43. ModelTableMetadataManager modelTableMetadataManager
  44. = configurableListableBeanFactory.getBean(ModelTableMetadataManager.class);
  45. CanalBinlogEventProcessorFactory canalBinlogEventProcessorFactory
  46. = configurableListableBeanFactory.getBean(CanalBinlogEventProcessorFactory.class);
  47. CanalBinLogEventParser canalBinLogEventParser
  48. = configurableListableBeanFactory.getBean(CanalBinLogEventParser.class);
  49. Map<String, BaseParseResultInterceptor> interceptors
  50. = configurableListableBeanFactory.getBeansOfType(BaseParseResultInterceptor.class);
  51. interceptors.forEach((k, interceptor) -> parseResultInterceptorManager.registerParseResultInterceptor(interceptor));
  52. Map<String, BaseCanalBinlogEventProcessor> processors
  53. = configurableListableBeanFactory.getBeansOfType(BaseCanalBinlogEventProcessor.class);
  54. processors.forEach((k, processor) -> processor.init(canalBinLogEventParser, modelTableMetadataManager,
  55. canalBinlogEventProcessorFactory, parseResultInterceptorManager));
  56. }
  57. }

为了更好地让其他服务引入此配置类,可以使用spring.factories的特性。新建resources/META-INF/spring.factories文件,内容如下:

  1. org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.throwx.canal.gule.config.CanalGlueAutoConfiguration

这样子通过引入spring-boot-starter-canal-glue就可以激活所有用到的组件并且初始化所有已经添加到Spring容器中的处理器。

CanalGlue开发

CanalGlue其实就是提供binlog事件字符串的处理入口,目前定义为一个接口:

  1. public interface CanalGlue {
  2. void process(String content);
  3. }

此接口的实现DefaultCanalGlue也十分简单:

  1. @RequiredArgsConstructor(access = AccessLevel.PUBLIC, staticName = "of")
  2. public class DefaultCanalGlue implements CanalGlue {
  3. private final CanalBinlogEventProcessorFactory canalBinlogEventProcessorFactory;
  4. @Override
  5. public void process(String content) {
  6. CanalBinLogEvent event = SourceAdapterFacade.X.adapt(CanalBinLogEvent.class, content);
  7. ModelTable modelTable = ModelTable.of(event.getDatabase(), event.getTable());
  8. canalBinlogEventProcessorFactory.get(modelTable).forEach(processor -> processor.process(event));
  9. }
  10. }

使用源适配器把字符串转换为CanalBinLogEvent实例,再委托处理器工厂寻找对应的BaseCanalBinlogEventProcessor列表去处理输入的事件实例。

使用canal-glue

主要包括下面几个维度,都在canal-glue-exampletest包下:

  • [x] 一般情况下使用处理器处理INSERT事件。
  • [x] 自定义针对DDL变更的预警父处理器,实现DDL变更预警。
  • [x] 单表对应多个处理器。
  • [x] 使用解析结果处理器针对特定字段进行AES加解密处理。
  • [x] 非Spring容器下,一般编程式使用。
  • [ ] 使用openjdk-jmh进行Benchmark基准性能测试。

这里简单提一下在Spring体系下的使用方式,引入依赖spring-boot-starter-canal-glue

  1. <dependency>
  2. <groupId>cn.throwx</groupId>
  3. <artifactId>spring-boot-starter-canal-glue</artifactId>
  4. <version>版本号</version>
  5. </dependency>

编写一个实体或者DTOOrderModel

  1. @Data
  2. @CanalModel(database = "db_order_service", table = "t_order", fieldNamingPolicy = FieldNamingPolicy.LOWER_UNDERSCORE)
  3. public static class OrderModel {
  4. private Long id;
  5. private String orderId;
  6. private OffsetDateTime createTime;
  7. private BigDecimal amount;
  8. }

这里使用了@CanalModel注解绑定了数据库db_order_service和表t_order,属性名-列名映射策略为驼峰转小写下划线。接着定义一个处理器OrderProcessor和自定义异常处理器(可选,这里是为了模拟在处理事件的时候抛出自定义异常):

  1. @Component
  2. public class OrderProcessor extends BaseCanalBinlogEventProcessor<OrderModel> {
  3. @Override
  4. protected void processInsertInternal(CanalBinLogResult<OrderModel> result) {
  5. OrderModel orderModel = result.getAfterData();
  6. logger.info("接收到订单保存binlog,主键:{},模拟抛出异常...", orderModel.getId());
  7. throw new RuntimeException(String.format("[id:%d]", orderModel.getId()));
  8. }
  9. @Override
  10. protected ExceptionHandler exceptionHandler() {
  11. return EXCEPTION_HANDLER;
  12. }
  13. /**
  14. * 覆盖默认的ExceptionHandler.NO_OP
  15. */
  16. private static final ExceptionHandler EXCEPTION_HANDLER = (event, throwable)
  17. -> log.error("解析binlog事件出现异常,事件内容:{}", JSON.toJSONString(event), throwable);
  18. }

假设一个写入订单数据的binlog事件如下:

  1. {
  2. "data": [
  3. {
  4. "id": "1",
  5. "order_id": "10086",
  6. "amount": "999.0",
  7. "create_time": "2020-03-02 05:12:49"
  8. }
  9. ],
  10. "database": "db_order_service",
  11. "es": 1583143969000,
  12. "id": 3,
  13. "isDdl": false,
  14. "mysqlType": {
  15. "id": "BIGINT",
  16. "order_id": "VARCHAR(64)",
  17. "amount": "DECIMAL(10,2)",
  18. "create_time": "DATETIME"
  19. },
  20. "old": null,
  21. "pkNames": [
  22. "id"
  23. ],
  24. "sql": "",
  25. "sqlType": {
  26. "id": -5,
  27. "order_id": 12,
  28. "amount": 3,
  29. "create_time": 93
  30. },
  31. "table": "t_order",
  32. "ts": 1583143969460,
  33. "type": "INSERT"
  34. }

执行结果如下:

如果直接对接Canal投放到KafkaTopic也很简单,配合Kafka的消费者使用的示例如下:

  1. @Slf4j
  2. @Component
  3. @RequiredArgsConstructor
  4. public class CanalEventListeners {
  5. private final CanalGlue canalGlue;
  6. @KafkaListener(
  7. id = "${canal.event.order.listener.id:db-order-service-listener}",
  8. topics = "db_order_service",
  9. containerFactory = "kafkaListenerContainerFactory"
  10. )
  11. public void onCrmMessage(String content) {
  12. canalGlue.process(content);
  13. }
  14. }

小结

笔者开发这个canal-glue的初衷是需要做一个极大提升效率的大型字符串转换器,因为刚刚接触到"小数据"领域,而且人手不足,而且需要处理下游大量的报表,因为不可能花大量人力在处理这些不停重复的模板化代码上。虽然整体设计还不是十分优雅,至少在提升开发效率这个点上canal-glue做到了。

项目仓库:

  • Giteehttps://gitee.com/throwableDoge/canal-glue

仓库最新代码暂时放在develop分支

(本文完 c-15-d e-a-20201005 鸽了快一个月)

简化ETL工作,编写一个Canal胶水层的更多相关文章

  1. 用Java语言编写一个简易画板

    讲了三篇概博客的概念,今天,我们来一点实际的东西.我们来探讨一下如何用Java语言,编写一块简易的画图板. 一.需求分析 无论我们使用什么语言,去编写一个什么样的项目,我们的第一步,总是去分析这个项目 ...

  2. 手把手教你编写一个具有基本功能的shell(已开源)

    刚接触Linux时,对shell总有种神秘感:在对shell的工作原理有所了解之后,便尝试着动手写一个shell.下面是一个从最简单的情况开始,一步步完成一个模拟的shell(我命名之为wshell) ...

  3. 如何自己编写一个easyui插件

    本文介绍如何通过参考1.4.2版本的progressbar的源码自己编写一个HelloWorld级别的easyui插件,以及如何拓展插件的功能. 有利于我们理解easyui插件的实现,以及了解如何对e ...

  4. 从头开始编写一个Orchard网上商店模块(6) - 创建购物车服务和控制器

    原文地址: http://skywalkersoftwaredevelopment.net/blog/writing-an-orchard-webshop-module-from-scratch-pa ...

  5. 从头开始编写一个Orchard网上商店模块(5) - 创建和渲染ProductCatalog的内容类型

    原文地址: http://skywalkersoftwaredevelopment.net/blog/writing-an-orchard-webshop-module-from-scratch-pa ...

  6. 如何编写一个JSON解析器

    编写一个JSON解析器实际上就是一个函数,它的输入是一个表示JSON的字符串,输出是结构化的对应到语言本身的数据结构. 和XML相比,JSON本身结构非常简单,并且仅有几种数据类型,以Java为例,对 ...

  7. javascript编写一个简单的编译器(理解抽象语法树AST)

    javascript编写一个简单的编译器(理解抽象语法树AST) 编译器 是一种接收一段代码,然后把它转成一些其他一种机制.我们现在来做一个在一张纸上画出一条线,那么我们画出一条线需要定义的条件如下: ...

  8. 多线程编程学习笔记——编写一个异步的HTTP服务器和客户端

    接上文 多线程编程学习笔记——使用异步IO 二.   编写一个异步的HTTP服务器和客户端 本节展示了如何编写一个简单的异步HTTP服务器. 1.程序代码如下. using System; using ...

  9. 用 Go 编写一个简单的 WebSocket 推送服务

    用 Go 编写一个简单的 WebSocket 推送服务 本文中代码可以在 github.com/alfred-zhong/wserver 获取. 背景 最近拿到需求要在网页上展示报警信息.以往报警信息 ...

随机推荐

  1. Kafka Broker源码:网络层设计

    一.整体架构 1.1 核心逻辑 1个Acceptor线程+N个Processor线程(network.threads)+M个Request Handle线程(io threads) 多线程多React ...

  2. Selenium多浏览器处理

    当我们在执行自动化测试过程中,往往会针对不同的浏览器做兼容性测试,那么我们在代码中,可以针对执行命令传过来的参数,选择对应的浏览器来执行测试用例 代码如下: 在终端中执行命令如上图红框中所示: bro ...

  3. Python技术调查

    1. IDE 2. Local Debugging & Remote Debugging 3. Profiling

  4. C# Beanstalkd Client

    http://bestmike007.com/Beanstalkd.Client/ Other Message Queue http://queues.io

  5. Codeforces Round #571 (Div. 2)-D. Vus the Cossack and Numbers

    Vus the Cossack has nn real numbers aiai. It is known that the sum of all numbers is equal to 00. He ...

  6. web前端笔记(包含php+laravel)

    概况 熟悉HTML5.CSS3.JavaScript.ES6规范 熟悉JQuery框架 熟悉BootStrap 熟悉Less.Sass 熟悉Vue 熟悉Git postman Bootstrap 布局 ...

  7. C001:打印勾

    程序: #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { printf(" *\n"); p ...

  8. 转载:SQL语句执行顺序

    转载地址:https://database.51cto.com/art/202001/609727.htm

  9. [极客大挑战 2019]Secret File wp

    通过标题考虑可能为文件包含漏洞方面 打开网页 从页面并没任何思路,查看源代码 得到有一个跳转到./Archive_room.php的超链接,打开Archive_room.php 中央有一个secret ...

  10. Linux 获取屏幕分辨率与窗口行列数(c/c++)

    获取当前分辨率 #include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<s ...