Browse Source

Merge pull request #865 from rewerma/master

fix #849: HBase数据同步适配
agapple 6 years ago
parent
commit
3c80b6c1f7
41 changed files with 4975 additions and 1 deletions
  1. 33 0
      client-adapter/common/pom.xml
  2. 34 0
      client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/CanalOuterAdapter.java
  3. 181 0
      client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/support/CanalClientConfig.java
  4. 52 0
      client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/support/CanalOuterAdapterConfiguration.java
  5. 96 0
      client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/support/Dml.java
  6. 563 0
      client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/support/ExtensionLoader.java
  7. 103 0
      client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/support/JdbcTypeUtil.java
  8. 113 0
      client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/support/MessageUtil.java
  9. 12 0
      client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/support/SPI.java
  10. 101 0
      client-adapter/hbase/pom.xml
  11. 115 0
      client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/HbaseAdapter.java
  12. 337 0
      client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/config/MappingConfig.java
  13. 149 0
      client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/config/MappingConfigLoader.java
  14. 418 0
      client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/service/HbaseSyncService.java
  15. 84 0
      client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/support/HRow.java
  16. 159 0
      client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/support/HbaseTemplate.java
  17. 150 0
      client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/support/PhType.java
  18. 614 0
      client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/support/PhTypeUtil.java
  19. 104 0
      client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/support/Type.java
  20. 188 0
      client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/support/TypeUtil.java
  21. 1 0
      client-adapter/hbase/src/main/resources/META-INF/canal/com.alibaba.otter.canal.client.adapter.CanalOuterAdapter
  22. 5 0
      client-adapter/hbase/src/main/resources/hbase-mapping/configs.conf
  23. 56 0
      client-adapter/hbase/src/main/resources/hbase-mapping/mytest_person2.yml
  24. 47 0
      client-adapter/logger/pom.xml
  25. 45 0
      client-adapter/logger/src/main/java/com/alibaba/otter/canal/client/adapter/logger/LoggerAdapterExample.java
  26. 1 0
      client-adapter/logger/src/main/resources/META-INF/canal/com.alibaba.otter.canal.client.adapter.CanalOuterAdapter
  27. 21 0
      client-adapter/pom.xml
  28. 150 0
      client-launcher/pom.xml
  29. 57 0
      client-launcher/src/main/assembly/dev.xml
  30. 57 0
      client-launcher/src/main/assembly/release.xml
  31. 25 0
      client-launcher/src/main/bin/startup.bat
  32. 100 0
      client-launcher/src/main/bin/startup.sh
  33. 65 0
      client-launcher/src/main/bin/stop.sh
  34. 65 0
      client-launcher/src/main/java/com/alibaba/otter/canal/client/ClientLauncher.java
  35. 83 0
      client-launcher/src/main/java/com/alibaba/otter/canal/client/adapter/loader/AbstractCanalAdapterWorker.java
  36. 168 0
      client-launcher/src/main/java/com/alibaba/otter/canal/client/adapter/loader/CanalAdapterKafkaWorker.java
  37. 164 0
      client-launcher/src/main/java/com/alibaba/otter/canal/client/adapter/loader/CanalAdapterLoader.java
  38. 187 0
      client-launcher/src/main/java/com/alibaba/otter/canal/client/adapter/loader/CanalAdapterWorker.java
  39. 22 0
      client-launcher/src/main/resources/canal-client.yml
  40. 47 0
      client-launcher/src/main/resources/logback.xml
  41. 3 1
      pom.xml

+ 33 - 0
client-adapter/common/pom.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>canal.client-adapter</artifactId>
+        <groupId>com.alibaba.otter</groupId>
+        <version>1.1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>com.alibaba.otter</groupId>
+    <artifactId>client-adapter.common</artifactId>
+    <packaging>jar</packaging>
+    <name>canal client adapter common module for otter ${project.version}</name>
+    <dependencies>
+        <dependency>
+            <groupId>com.alibaba.otter</groupId>
+            <artifactId>canal.protocol</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.7.12</version>
+        </dependency>
+        <dependency>
+            <groupId>joda-time</groupId>
+            <artifactId>joda-time</artifactId>
+            <version>2.9.4</version>
+        </dependency>
+    </dependencies>
+
+</project>

+ 34 - 0
client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/CanalOuterAdapter.java

@@ -0,0 +1,34 @@
+package com.alibaba.otter.canal.client.adapter;
+
+import com.alibaba.otter.canal.client.adapter.support.CanalOuterAdapterConfiguration;
+import com.alibaba.otter.canal.client.adapter.support.SPI;
+import com.alibaba.otter.canal.protocol.Message;
+
+/**
+ * 外部适配器接口
+ *
+ * @author machengyuan 2018-8-18 下午10:14:02
+ * @version 1.0.0
+ */
+@SPI("logger")
+public interface CanalOuterAdapter {
+
+    /**
+     * 外部适配器初始化接口
+     *
+     * @param configuration 外部适配器配置信息
+     */
+    void init(CanalOuterAdapterConfiguration configuration);
+
+    /**
+     * 往适配器中写入数据
+     *
+     * @param message message数据包
+     */
+    void writeOut(Message message);
+
+    /**
+     * 外部适配器销毁接口
+     */
+    void destroy();
+}

+ 181 - 0
client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/support/CanalClientConfig.java

@@ -0,0 +1,181 @@
+package com.alibaba.otter.canal.client.adapter.support;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+
+/**
+ * 配置信息类
+ *
+ * @author machengyuan 2018-8-18 下午10:40:12
+ * @version 1.0.0
+ */
+public class CanalClientConfig {
+
+    private String              canalServerHost;
+
+    private String              zookeeperHosts;
+
+    private Properties          properties;
+
+    private String              bootstrapServers;
+
+    private List<KafkaTopic>    kafkaTopics    = new ArrayList<>();
+
+    private List<CanalInstance> canalInstances = new ArrayList<>();
+
+    public String getCanalServerHost() {
+        return canalServerHost;
+    }
+
+    public void setCanalServerHost(String canalServerHost) {
+        this.canalServerHost = canalServerHost;
+    }
+
+    public String getZookeeperHosts() {
+        return zookeeperHosts;
+    }
+
+    public void setZookeeperHosts(String zookeeperHosts) {
+        this.zookeeperHosts = zookeeperHosts;
+    }
+
+    public Properties getProperties() {
+        return properties;
+    }
+
+    public void setProperties(Properties properties) {
+        this.properties = properties;
+    }
+
+    public String getBootstrapServers() {
+        return bootstrapServers;
+    }
+
+    public void setBootstrapServers(String bootstrapServers) {
+        this.bootstrapServers = bootstrapServers;
+    }
+
+    public List<KafkaTopic> getKafkaTopics() {
+        return kafkaTopics;
+    }
+
+    public void setKafkaTopics(List<KafkaTopic> kafkaTopics) {
+        this.kafkaTopics = kafkaTopics;
+    }
+
+    public List<CanalInstance> getCanalInstances() {
+        return canalInstances;
+    }
+
+    public void setCanalInstances(List<CanalInstance> canalInstances) {
+        this.canalInstances = canalInstances;
+    }
+
+    public static class CanalInstance {
+
+        private String             instance;
+
+        private List<AdapterGroup> adapterGroups;
+
+        public String getInstance() {
+            return instance;
+        }
+
+        public void setInstance(String instance) {
+            if (instance != null) {
+                this.instance = instance.trim();
+            }
+        }
+
+        public List<AdapterGroup> getAdapterGroups() {
+            return adapterGroups;
+        }
+
+        public void setAdapterGroups(List<AdapterGroup> adapterGroups) {
+            this.adapterGroups = adapterGroups;
+        }
+    }
+
+    public static class AdapterGroup {
+
+        private List<CanalOuterAdapterConfiguration> outAdapters;
+
+        public List<CanalOuterAdapterConfiguration> getOutAdapters() {
+            return outAdapters;
+        }
+
+        public void setOutAdapters(List<CanalOuterAdapterConfiguration> outAdapters) {
+            this.outAdapters = outAdapters;
+        }
+    }
+
+    public static class KafkaTopic {
+
+        private String      topic;
+
+        private List<Group> groups = new ArrayList<>();
+
+        public String getTopic() {
+            return topic;
+        }
+
+        public void setTopic(String topic) {
+            this.topic = topic;
+        }
+
+        public List<Group> getGroups() {
+            return groups;
+        }
+
+        public void setGroups(List<Group> groups) {
+            this.groups = groups;
+        }
+    }
+
+    public static class Group {
+
+        private String                               groupId;
+
+        // private List<Adaptor> adapters = new ArrayList<>();
+
+        private List<CanalOuterAdapterConfiguration> outAdapters;
+
+        public String getGroupId() {
+            return groupId;
+        }
+
+        public void setGroupId(String groupId) {
+            this.groupId = groupId;
+        }
+
+        public List<CanalOuterAdapterConfiguration> getOutAdapters() {
+            return outAdapters;
+        }
+
+        public void setOutAdapters(List<CanalOuterAdapterConfiguration> outAdapters) {
+            this.outAdapters = outAdapters;
+        }
+
+        // public List<Adaptor> getAdapters() {
+        // return adapters;
+        // }
+        //
+        // public void setAdapters(List<Adaptor> adapters) {
+        // this.adapters = adapters;
+        // }
+    }
+
+    // public static class Adaptor {
+    // private List<CanalOuterAdapterConfiguration> outAdapters;
+    //
+    // public List<CanalOuterAdapterConfiguration> getOutAdapters() {
+    // return outAdapters;
+    // }
+    //
+    // public void setOutAdapters(List<CanalOuterAdapterConfiguration> outAdapters)
+    // {
+    // this.outAdapters = outAdapters;
+    // }
+    // }
+}

+ 52 - 0
client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/support/CanalOuterAdapterConfiguration.java

@@ -0,0 +1,52 @@
+package com.alibaba.otter.canal.client.adapter.support;
+
+import java.util.Properties;
+
+/**
+ * 外部适配器配置信息类
+ *
+ * @author machengyuan 2018-8-18 下午10:15:12
+ * @version 1.0.0
+ */
+public class CanalOuterAdapterConfiguration {
+
+    private String     name;       // 适配器名称, 如: logger, hbase, es
+
+    private String     hosts;      // 适配器内部的地址, 比如对应es该参数可以填写es的server地址
+
+    private String     zkHosts;    // 适配器内部的ZK地址, 比如对应HBase该参数可以填写HBase对应的ZK地址
+
+    private Properties properties; // 其余参数, 可填写适配器中的所需的配置信息
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getHosts() {
+        return hosts;
+    }
+
+    public void setHosts(String hosts) {
+        this.hosts = hosts;
+    }
+
+    public Properties getProperties() {
+        return properties;
+    }
+
+    public void setProperties(Properties properties) {
+        this.properties = properties;
+    }
+
+    public String getZkHosts() {
+        return zkHosts;
+    }
+
+    public void setZkHosts(String zkHosts) {
+        this.zkHosts = zkHosts;
+    }
+}

+ 96 - 0
client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/support/Dml.java

@@ -0,0 +1,96 @@
+package com.alibaba.otter.canal.client.adapter.support;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * DML操作转换对象
+ *
+ * @author machengyuan 2018-8-19 下午11:30:49
+ * @version 1.0.0
+ */
+public class Dml implements Serializable {
+
+    private static final long         serialVersionUID = 2611556444074013268L;
+
+    private String                    database;
+    private String                    table;
+    private String                    type;
+    private Long                      ts;
+    private String                    sql;
+    private List<Map<String, Object>> data;
+    private List<Map<String, Object>> old;
+
+    public String getDatabase() {
+        return database;
+    }
+
+    public void setDatabase(String database) {
+        this.database = database;
+    }
+
+    public String getTable() {
+        return table;
+    }
+
+    public void setTable(String table) {
+        this.table = table;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public Long getTs() {
+        return ts;
+    }
+
+    public void setTs(Long ts) {
+        this.ts = ts;
+    }
+
+    public String getSql() {
+        return sql;
+    }
+
+    public void setSql(String sql) {
+        this.sql = sql;
+    }
+
+    public List<Map<String, Object>> getData() {
+        return data;
+    }
+
+    public void setData(List<Map<String, Object>> data) {
+        this.data = data;
+    }
+
+    public List<Map<String, Object>> getOld() {
+        return old;
+    }
+
+    public void setOld(List<Map<String, Object>> old) {
+        this.old = old;
+    }
+
+    public void clear() {
+        database = null;
+        table = null;
+        type = null;
+        ts = null;
+        data = null;
+        old = null;
+        sql = null;
+    }
+
+    @Override
+    public String toString() {
+        return "Dml{" + "database='" + database + '\'' + ", table='" + table + '\'' + ", type='" + type + '\'' + ", ts="
+               + ts + ", sql='" + sql + '\'' + ", data=" + data + ", old=" + old + '}';
+    }
+}

+ 563 - 0
client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/support/ExtensionLoader.java

@@ -0,0 +1,563 @@
+package com.alibaba.otter.canal.client.adapter.support;
+
+import java.io.*;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.regex.Pattern;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * SPI 类加载器
+ *
+ * @author machengyuan 2018-8-19 下午11:30:49
+ * @version 1.0.0
+ */
+public class ExtensionLoader<T> {
+
+    private static final Logger                                      logger                     = LoggerFactory
+        .getLogger(ExtensionLoader.class);
+
+    private static final String                                      SERVICES_DIRECTORY         = "META-INF/services/";
+
+    private static final String                                      CANAL_DIRECTORY            = "META-INF/canal/";
+
+    private static final String                                      DEFAULT_CLASSLOADER_POLICY = "internal";
+
+    private static final Pattern                                     NAME_SEPARATOR             = Pattern
+        .compile("\\s*[,]+\\s*");
+
+    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS          = new ConcurrentHashMap<>();
+
+    private static final ConcurrentMap<Class<?>, Object>             EXTENSION_INSTANCES        = new ConcurrentHashMap<>();
+
+    private final Class<?>                                           type;
+
+    private final String                                             classLoaderPolicy;
+
+    private final ConcurrentMap<Class<?>, String>                    cachedNames                = new ConcurrentHashMap<>();
+
+    private final Holder<Map<String, Class<?>>>                      cachedClasses              = new Holder<>();
+
+    private final ConcurrentMap<String, Holder<Object>>              cachedInstances            = new ConcurrentHashMap<>();
+
+    private String                                                   cachedDefaultName;
+
+    private ConcurrentHashMap<String, IllegalStateException>         exceptions                 = new ConcurrentHashMap<>();
+
+    private static <T> boolean withExtensionAnnotation(Class<T> type) {
+        return type.isAnnotationPresent(SPI.class);
+    }
+
+    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
+        return getExtensionLoader(type, DEFAULT_CLASSLOADER_POLICY);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type, String classLoaderPolicy) {
+        if (type == null) throw new IllegalArgumentException("Extension type == null");
+        if (!type.isInterface()) {
+            throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
+        }
+        if (!withExtensionAnnotation(type)) {
+            throw new IllegalArgumentException("Extension type(" + type + ") is not extension, because WITHOUT @"
+                                               + SPI.class.getSimpleName() + " Annotation!");
+        }
+
+        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
+        if (loader == null) {
+            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type, classLoaderPolicy));
+            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
+        }
+        return loader;
+    }
+
+    private ExtensionLoader(Class<?> type, String classLoaderPolicy){
+        this.type = type;
+        this.classLoaderPolicy = classLoaderPolicy;
+    }
+
+    public String getExtensionName(T extensionInstance) {
+        return getExtensionName(extensionInstance.getClass());
+    }
+
+    public String getExtensionName(Class<?> extensionClass) {
+        return cachedNames.get(extensionClass);
+    }
+
+    public ConcurrentHashMap<String, IllegalStateException> getExceptions() {
+        return exceptions;
+    }
+
+    /**
+     * 返回扩展点实例,如果没有指定的扩展点或是还没加载(即实例化)则返回<code>null</code>注意:此方法不会触发扩展点的加载
+     * <p/>
+     * 一般应该调用{@link #getExtension(String)}方法获得扩展,这个方法会触发扩展点加载
+     *
+     * @see #getExtension(String)
+     */
+    @SuppressWarnings("unchecked")
+    public T getLoadedExtension(String name) {
+        if (name == null || name.length() == 0) throw new IllegalArgumentException("Extension name == null");
+        Holder<Object> holder = cachedInstances.get(name);
+        if (holder == null) {
+            cachedInstances.putIfAbsent(name, new Holder<>());
+            holder = cachedInstances.get(name);
+        }
+        return (T) holder.get();
+    }
+
+    /**
+     * 返回已经加载的扩展点的名字
+     * <p/>
+     * 一般应该调用{@link #getSupportedExtensions()}方法获得扩展,这个方法会返回所有的扩展点
+     *
+     * @see #getSupportedExtensions()
+     */
+    public Set<String> getLoadedExtensions() {
+        return Collections.unmodifiableSet(new TreeSet<>(cachedInstances.keySet()));
+    }
+
+    /**
+     * 返回指定名字的扩展
+     *
+     * @param name
+     * @return
+     */
+    @SuppressWarnings("unchecked")
+    public T getExtension(String name) {
+        if (name == null || name.length() == 0) throw new IllegalArgumentException("Extension name == null");
+        if ("true".equals(name)) {
+            return getDefaultExtension();
+        }
+        Holder<Object> holder = cachedInstances.get(name);
+        if (holder == null) {
+            cachedInstances.putIfAbsent(name, new Holder<>());
+            holder = cachedInstances.get(name);
+        }
+        Object instance = holder.get();
+        if (instance == null) {
+            synchronized (holder) {
+                instance = holder.get();
+                if (instance == null) {
+                    instance = createExtension(name);
+                    holder.set(instance);
+                }
+            }
+        }
+        return (T) instance;
+    }
+
+    /**
+     * 返回缺省的扩展,如果没有设置则返回<code>null</code>
+     */
+    public T getDefaultExtension() {
+        getExtensionClasses();
+        if (null == cachedDefaultName || cachedDefaultName.length() == 0 || "true".equals(cachedDefaultName)) {
+            return null;
+        }
+        return getExtension(cachedDefaultName);
+    }
+
+    public boolean hasExtension(String name) {
+        if (name == null || name.length() == 0) throw new IllegalArgumentException("Extension name == null");
+        try {
+            return getExtensionClass(name) != null;
+        } catch (Throwable t) {
+            return false;
+        }
+    }
+
+    public Set<String> getSupportedExtensions() {
+        Map<String, Class<?>> clazzes = getExtensionClasses();
+        return Collections.unmodifiableSet(new TreeSet<String>(clazzes.keySet()));
+    }
+
+    /**
+     * 返回缺省的扩展点名,如果没有设置缺省则返回<code>null</code>
+     */
+    public String getDefaultExtensionName() {
+        getExtensionClasses();
+        return cachedDefaultName;
+    }
+
+    /**
+     * 编程方式添加新扩展点
+     *
+     * @param name 扩展点名
+     * @param clazz 扩展点类
+     * @throws IllegalStateException 要添加扩展点名已经存在
+     */
+    public void addExtension(String name, Class<?> clazz) {
+        getExtensionClasses(); // load classes
+
+        if (!type.isAssignableFrom(clazz)) {
+            throw new IllegalStateException("Input type " + clazz + "not implement Extension " + type);
+        }
+        if (clazz.isInterface()) {
+            throw new IllegalStateException("Input type " + clazz + "can not be interface!");
+        }
+
+        if (name == null || "".equals(name)) {
+            throw new IllegalStateException("Extension name is blank (Extension " + type + ")!");
+        }
+        if (cachedClasses.get().containsKey(name)) {
+            throw new IllegalStateException("Extension name " + name + " already existed(Extension " + type + ")!");
+        }
+
+        cachedNames.put(clazz, name);
+        cachedClasses.get().put(name, clazz);
+    }
+
+    /**
+     * 编程方式添加替换已有扩展点
+     *
+     * @param name 扩展点名
+     * @param clazz 扩展点类
+     * @throws IllegalStateException 要添加扩展点名已经存在
+     * @deprecated 不推荐应用使用,一般只在测试时可以使用
+     */
+    @Deprecated
+    public void replaceExtension(String name, Class<?> clazz) {
+        getExtensionClasses(); // load classes
+
+        if (!type.isAssignableFrom(clazz)) {
+            throw new IllegalStateException("Input type " + clazz + "not implement Extension " + type);
+        }
+        if (clazz.isInterface()) {
+            throw new IllegalStateException("Input type " + clazz + "can not be interface!");
+        }
+
+        if (name == null || "".equals(name)) {
+            throw new IllegalStateException("Extension name is blank (Extension " + type + ")!");
+        }
+        if (!cachedClasses.get().containsKey(name)) {
+            throw new IllegalStateException("Extension name " + name + " not existed(Extension " + type + ")!");
+        }
+
+        cachedNames.put(clazz, name);
+        cachedClasses.get().put(name, clazz);
+        cachedInstances.remove(name);
+    }
+
+    @SuppressWarnings("unchecked")
+    private T createExtension(String name) {
+        Class<?> clazz = getExtensionClasses().get(name);
+        if (clazz == null) {
+            throw new IllegalStateException("Extension instance(name: " + name + ", class: " + type
+                                            + ")  could not be instantiated: class could not be found");
+        }
+        try {
+            T instance = (T) EXTENSION_INSTANCES.get(clazz);
+            if (instance == null) {
+                EXTENSION_INSTANCES.putIfAbsent(clazz, (T) clazz.newInstance());
+                instance = (T) EXTENSION_INSTANCES.get(clazz);
+            }
+            return instance;
+        } catch (Throwable t) {
+            throw new IllegalStateException("Extension instance(name: " + name + ", class: " + type
+                                            + ")  could not be instantiated: " + t.getMessage(),
+                t);
+        }
+    }
+
+    private Class<?> getExtensionClass(String name) {
+        if (type == null) throw new IllegalArgumentException("Extension type == null");
+        if (name == null) throw new IllegalArgumentException("Extension name == null");
+        Class<?> clazz = getExtensionClasses().get(name);
+        if (clazz == null)
+            throw new IllegalStateException("No such extension \"" + name + "\" for " + type.getName() + "!");
+        return clazz;
+    }
+
+    private Map<String, Class<?>> getExtensionClasses() {
+        Map<String, Class<?>> classes = cachedClasses.get();
+        if (classes == null) {
+            synchronized (cachedClasses) {
+                classes = cachedClasses.get();
+                if (classes == null) {
+                    classes = loadExtensionClasses();
+                    cachedClasses.set(classes);
+                }
+            }
+        }
+        return classes;
+    }
+
+    private String getJarDirectoryPath() {
+        URL url = Thread.currentThread().getContextClassLoader().getResource("");
+        if (url == null) {
+            throw new IllegalStateException("failed to get class loader resource");
+        }
+        String dirtyPath = url.toString();
+        String jarPath = dirtyPath.replaceAll("^.*file:/", ""); // removes file:/ and everything before it
+        jarPath = jarPath.replaceAll("jar!.*", "jar"); // removes everything after .jar, if .jar exists in dirtyPath
+        jarPath = jarPath.replaceAll("%20", " "); // necessary if path has spaces within
+        if (!jarPath.endsWith(".jar")) { // this is needed if you plan to run the app using Spring Tools Suit play
+                                         // button.
+            jarPath = jarPath.replaceAll("/classes/.*", "/classes/");
+        }
+        return Paths.get(jarPath).getParent().toString(); // Paths - from java 8
+    }
+
+    private Map<String, Class<?>> loadExtensionClasses() {
+        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
+        if (defaultAnnotation != null) {
+            String value = defaultAnnotation.value();
+            if ((value = value.trim()).length() > 0) {
+                String[] names = NAME_SEPARATOR.split(value);
+                if (names.length > 1) {
+                    throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
+                                                    + ": " + Arrays.toString(names));
+                }
+                if (names.length == 1) cachedDefaultName = names[0];
+            }
+        }
+
+        Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
+
+        // 1. lib folder,customized extension classLoader (jar_dir/lib)
+        String dir = File.separator + this.getJarDirectoryPath() + File.separator + "lib";
+        logger.info("extension classpath dir: " + dir);
+        File externalLibDir = new File(dir);
+        if (!externalLibDir.exists()) {
+            externalLibDir = new File(
+                File.separator + this.getJarDirectoryPath() + File.separator + "canal_client" + File.separator + "lib");
+        }
+        if (externalLibDir.exists()) {
+            File[] files = externalLibDir.listFiles(new FilenameFilter() {
+
+                @Override
+                public boolean accept(File dir, String name) {
+                    return name.endsWith(".jar");
+                }
+            });
+            if (files != null) {
+                for (File f : files) {
+                    URL url = null;
+                    try {
+                        url = f.toURI().toURL();
+                    } catch (MalformedURLException e) {
+                        throw new RuntimeException("load extension jar failed!", e);
+                    }
+
+                    ClassLoader parent = Thread.currentThread().getContextClassLoader();
+                    URLClassLoader localClassLoader;
+                    if (classLoaderPolicy == null || "".equals(classLoaderPolicy)
+                        || DEFAULT_CLASSLOADER_POLICY.equalsIgnoreCase(classLoaderPolicy)) {
+                        localClassLoader = new URLClassLoader(new URL[] { url }, parent) {
+
+                            @Override
+                            public Class<?> loadClass(String name) throws ClassNotFoundException {
+                                Class<?> c = findLoadedClass(name);
+                                if (c != null) {
+                                    return c;
+                                }
+
+                                if (name.startsWith("java.") || name.startsWith("org.slf4j.")
+                                    || name.startsWith("org.apache.logging")
+                                    || name.startsWith("org.apache.commons.logging.")) {
+                                    // || name.startsWith("org.apache.hadoop.")) {
+                                    c = super.loadClass(name);
+                                }
+                                if (c != null) return c;
+
+                                try {
+                                    // 先加载jar内的class,可避免jar冲突
+                                    c = findClass(name);
+                                } catch (ClassNotFoundException e) {
+                                    c = null;
+                                }
+                                if (c != null) {
+                                    return c;
+                                }
+
+                                return super.loadClass(name);
+                            }
+
+                            @Override
+                            public Enumeration<URL> getResources(String name) throws IOException {
+                                @SuppressWarnings("unchecked")
+                                Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
+
+                                tmp[0] = findResources(name); // local class path first
+                                // tmp[1] = super.getResources(name);
+
+                                return new CompoundEnumeration<>(tmp);
+                            }
+                        };
+                    } else {
+                        localClassLoader = new URLClassLoader(new URL[] { url }, parent);
+                    }
+
+                    loadFile(extensionClasses, CANAL_DIRECTORY, localClassLoader);
+                    loadFile(extensionClasses, SERVICES_DIRECTORY, localClassLoader);
+                }
+            }
+        }
+        // 2. load inner extension class with default classLoader
+        ClassLoader classLoader = findClassLoader();
+        loadFile(extensionClasses, CANAL_DIRECTORY, classLoader);
+        loadFile(extensionClasses, SERVICES_DIRECTORY, classLoader);
+
+        return extensionClasses;
+    }
+
+    public static class CompoundEnumeration<E> implements Enumeration<E> {
+
+        private Enumeration<E>[] enums;
+        private int              index = 0;
+
+        public CompoundEnumeration(Enumeration<E>[] enums){
+            this.enums = enums;
+        }
+
+        private boolean next() {
+            while (this.index < this.enums.length) {
+                if (this.enums[this.index] != null && this.enums[this.index].hasMoreElements()) {
+                    return true;
+                }
+
+                ++this.index;
+            }
+
+            return false;
+        }
+
+        public boolean hasMoreElements() {
+            return this.next();
+        }
+
+        public E nextElement() {
+            if (!this.next()) {
+                throw new NoSuchElementException();
+            } else {
+                return this.enums[this.index].nextElement();
+            }
+        }
+    }
+
+    private void loadFile(Map<String, Class<?>> extensionClasses, String dir, ClassLoader classLoader) {
+        String fileName = dir + type.getName();
+        try {
+            Enumeration<URL> urls;
+            if (classLoader != null) {
+                urls = classLoader.getResources(fileName);
+            } else {
+                urls = ClassLoader.getSystemResources(fileName);
+            }
+            if (urls != null) {
+                while (urls.hasMoreElements()) {
+                    URL url = urls.nextElement();
+                    try {
+                        BufferedReader reader = null;
+                        try {
+                            reader = new BufferedReader(new InputStreamReader(url.openStream(), "utf-8"));
+                            String line = null;
+                            while ((line = reader.readLine()) != null) {
+                                final int ci = line.indexOf('#');
+                                if (ci >= 0) line = line.substring(0, ci);
+                                line = line.trim();
+                                if (line.length() > 0) {
+                                    try {
+                                        String name = null;
+                                        int i = line.indexOf('=');
+                                        if (i > 0) {
+                                            name = line.substring(0, i).trim();
+                                            line = line.substring(i + 1).trim();
+                                        }
+                                        if (line.length() > 0) {
+                                            Class<?> clazz = classLoader.loadClass(line);
+                                            // Class<?> clazz = Class.forName(line, true, classLoader);
+                                            if (!type.isAssignableFrom(clazz)) {
+                                                throw new IllegalStateException(
+                                                    "Error when load extension class(interface: " + type
+                                                                                + ", class line: " + clazz.getName()
+                                                                                + "), class " + clazz.getName()
+                                                                                + "is not subtype of interface.");
+                                            } else {
+                                                try {
+                                                    clazz.getConstructor(type);
+                                                } catch (NoSuchMethodException e) {
+                                                    clazz.getConstructor();
+                                                    String[] names = NAME_SEPARATOR.split(name);
+                                                    if (names != null && names.length > 0) {
+                                                        for (String n : names) {
+                                                            if (!cachedNames.containsKey(clazz)) {
+                                                                cachedNames.put(clazz, n);
+                                                            }
+                                                            Class<?> c = extensionClasses.get(n);
+                                                            if (c == null) {
+                                                                extensionClasses.put(n, clazz);
+                                                            } else if (c != clazz) {
+                                                                cachedNames.remove(clazz);
+                                                                throw new IllegalStateException(
+                                                                    "Duplicate extension " + type.getName() + " name "
+                                                                                                + n + " on "
+                                                                                                + c.getName() + " and "
+                                                                                                + clazz.getName());
+                                                            }
+                                                        }
+                                                    }
+                                                }
+                                            }
+                                        }
+                                    } catch (Throwable t) {
+                                        IllegalStateException e = new IllegalStateException(
+                                            "Failed to load extension class(interface: " + type + ", class line: "
+                                                                                            + line + ") in " + url
+                                                                                            + ", cause: "
+                                                                                            + t.getMessage(),
+                                            t);
+                                        exceptions.put(line, e);
+                                    }
+                                }
+                            } // end of while read lines
+                        } finally {
+                            if (reader != null) {
+                                reader.close();
+                            }
+                        }
+                    } catch (Throwable t) {
+                        logger.error("Exception when load extension class(interface: " + type + ", class file: " + url
+                                     + ") in " + url,
+                            t);
+                    }
+                } // end of while urls
+            }
+        } catch (Throwable t) {
+            logger.error(
+                "Exception when load extension class(interface: " + type + ", description file: " + fileName + ").",
+                t);
+        }
+    }
+
+    private static ClassLoader findClassLoader() {
+        return ExtensionLoader.class.getClassLoader();
+    }
+
+    @Override
+    public String toString() {
+        return this.getClass().getName() + "[" + type.getName() + "]";
+    }
+
+    private static class Holder<T> {
+
+        private volatile T value;
+
+        private void set(T value) {
+            this.value = value;
+        }
+
+        private T get() {
+            return value;
+        }
+
+    }
+}

+ 103 - 0
client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/support/JdbcTypeUtil.java

@@ -0,0 +1,103 @@
+package com.alibaba.otter.canal.client.adapter.support;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.sql.Types;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 类型转换工具类
+ *
+ * @author machengyuan 2018-8-19 下午06:14:23
+ * @version 1.0.0
+ */
+public class JdbcTypeUtil {
+
+    private static Logger logger = LoggerFactory.getLogger(JdbcTypeUtil.class);
+
+    public static Object typeConvert(String tableName, String columnName, String value, int sqlType, String mysqlType) {
+        if (value == null || value.equals("")) {
+            return null;
+        }
+
+        try {
+            Object res;
+            switch (sqlType) {
+                case Types.INTEGER:
+                    res = Integer.parseInt(value);
+                    break;
+                case Types.SMALLINT:
+                    res = Short.parseShort(value);
+                    break;
+                case Types.BIT:
+                case Types.TINYINT:
+                    res = Byte.parseByte(value);
+                    break;
+                case Types.BIGINT:
+                    if (mysqlType.startsWith("bigint") && mysqlType.endsWith("unsigned")) {
+                        res = new BigInteger(value);
+                    } else {
+                        res = Long.parseLong(value);
+                    }
+                    break;
+                // case Types.BIT:
+                case Types.BOOLEAN:
+                    res = !"0".equals(value);
+                    break;
+                case Types.DOUBLE:
+                case Types.FLOAT:
+                    res = Double.parseDouble(value);
+                    break;
+                case Types.REAL:
+                    res = Float.parseFloat(value);
+                    break;
+                case Types.DECIMAL:
+                case Types.NUMERIC:
+                    res = new BigDecimal(value);
+                    break;
+                case Types.BINARY:
+                case Types.VARBINARY:
+                case Types.LONGVARBINARY:
+                case Types.BLOB:
+                    res = value.getBytes("ISO-8859-1");
+                    break;
+                case Types.DATE:
+                    if (!value.startsWith("0000-00-00")) {
+                        value = value.trim().replace(" ", "T");
+                        DateTime dt = new DateTime(value);
+                        res = new Date(dt.toDate().getTime());
+                    } else {
+                        res = null;
+                    }
+                    break;
+                case Types.TIME:
+                    value = "T" + value;
+                    DateTime dt = new DateTime(value);
+                    res = new Time(dt.toDate().getTime());
+                    break;
+                case Types.TIMESTAMP:
+                    if (!value.startsWith("0000-00-00")) {
+                        value = value.trim().replace(" ", "T");
+                        dt = new DateTime(value);
+                        res = new Timestamp(dt.toDate().getTime());
+                    } else {
+                        res = null;
+                    }
+                    break;
+                case Types.CLOB:
+                default:
+                    res = value;
+            }
+            return res;
+        } catch (Exception e) {
+            logger.error("table: {} column: {}, failed convert type {} to {}", tableName, columnName, value, sqlType);
+            return value;
+        }
+    }
+}

+ 113 - 0
client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/support/MessageUtil.java

@@ -0,0 +1,113 @@
+package com.alibaba.otter.canal.client.adapter.support;
+
+import java.util.*;
+
+import com.alibaba.otter.canal.protocol.CanalEntry;
+import com.alibaba.otter.canal.protocol.Message;
+
+/**
+ * Message对象解析工具类
+ *
+ * @author machengyuan 2018-8-19 下午06:14:23
+ * @version 1.0.0
+ */
+public class MessageUtil {
+
+    public static void parse4Dml(Message message, Consumer<Dml> consumer) {
+        if (message == null) {
+            return;
+        }
+        List<CanalEntry.Entry> entries = message.getEntries();
+
+        for (CanalEntry.Entry entry : entries) {
+            if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN
+                || entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
+                continue;
+            }
+
+            CanalEntry.RowChange rowChange;
+            try {
+                rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
+            } catch (Exception e) {
+                throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(),
+                    e);
+            }
+
+            CanalEntry.EventType eventType = rowChange.getEventType();
+
+            final Dml dml = new Dml();
+            dml.setDatabase(entry.getHeader().getSchemaName());
+            dml.setTable(entry.getHeader().getTableName());
+            dml.setType(eventType.toString());
+            dml.setTs(System.currentTimeMillis());
+            dml.setSql(rowChange.getSql());
+            List<Map<String, Object>> data = new ArrayList<>();
+            List<Map<String, Object>> old = new ArrayList<>();
+
+            if (!rowChange.getIsDdl()) {
+                Set<String> updateSet = new HashSet<>();
+                for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
+                    if (eventType != CanalEntry.EventType.INSERT && eventType != CanalEntry.EventType.UPDATE
+                        && eventType != CanalEntry.EventType.DELETE) {
+                        continue;
+                    }
+
+                    Map<String, Object> row = new LinkedHashMap<>();
+                    List<CanalEntry.Column> columns;
+
+                    if (eventType == CanalEntry.EventType.DELETE) {
+                        columns = rowData.getBeforeColumnsList();
+                    } else {
+                        columns = rowData.getAfterColumnsList();
+                    }
+
+                    for (CanalEntry.Column column : columns) {
+                        row.put(column.getName(),
+                            JdbcTypeUtil.typeConvert(dml.getTable(),
+                                column.getName(),
+                                column.getValue(),
+                                column.getSqlType(),
+                                column.getMysqlType()));
+                        // 获取update为true的字段
+                        if (column.getUpdated()) {
+                            updateSet.add(column.getName());
+                        }
+                    }
+                    if (!row.isEmpty()) {
+                        data.add(row);
+                    }
+
+                    if (eventType == CanalEntry.EventType.UPDATE) {
+                        Map<String, Object> rowOld = new LinkedHashMap<>();
+                        for (CanalEntry.Column column : rowData.getBeforeColumnsList()) {
+                            if (updateSet.contains(column.getName())) {
+                                rowOld.put(column.getName(),
+                                    JdbcTypeUtil.typeConvert(dml.getTable(),
+                                        column.getName(),
+                                        column.getValue(),
+                                        column.getSqlType(),
+                                        column.getMysqlType()));
+                            }
+                        }
+                        // update操作将记录修改前的值
+                        if (!rowOld.isEmpty()) {
+                            old.add(rowOld);
+                        }
+                    }
+                }
+                if (!data.isEmpty()) {
+                    dml.setData(data);
+                }
+                if (!old.isEmpty()) {
+                    dml.setOld(old);
+                }
+                consumer.accept(dml);
+            }
+        }
+    }
+
+    public interface Consumer<T> {
+
+        void accept(T t);
+    }
+}

+ 12 - 0
client-adapter/common/src/main/java/com/alibaba/otter/canal/client/adapter/support/SPI.java

@@ -0,0 +1,12 @@
+package com.alibaba.otter.canal.client.adapter.support;
+
+import java.lang.annotation.*;
+
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.TYPE })
+public @interface SPI {
+
+    // Default SPI name
+    String value() default "";
+}

+ 101 - 0
client-adapter/hbase/pom.xml

@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>canal.client-adapter</artifactId>
+        <groupId>com.alibaba.otter</groupId>
+        <version>1.1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>com.alibaba.otter</groupId>
+    <artifactId>client-adapter.hbase</artifactId>
+    <packaging>jar</packaging>
+    <name>canal client adapter hbase module for otter ${project.version}</name>
+    <dependencies>
+        <dependency>
+            <groupId>com.alibaba.otter</groupId>
+            <artifactId>client-adapter.common</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.yaml</groupId>
+            <artifactId>snakeyaml</artifactId>
+            <version>1.17</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.hbase</groupId>
+            <artifactId>hbase-client</artifactId>
+            <version>1.1.2</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-log4j12</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>12.0.1</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <artifactId>maven-jar-plugin</artifactId>
+                <configuration>
+                    <archive>
+                        <addMavenDescriptor>true</addMavenDescriptor>
+                    </archive>
+                    <excludes>
+                        <exclude>**/hbase-mapping/**</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+            <plugin>
+                <artifactId>maven-antrun-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>run</goal>
+                        </goals>
+                        <configuration>
+                            <tasks>
+                                <copy todir="${project.basedir}/../../client-launcher/target/canal_client/conf/hbase-mapping" overwrite="true" >
+                                    <fileset dir="${project.basedir}/src/main/resources/hbase-mapping" erroronmissingdir="true">
+                                        <include name="*.conf"/>
+                                        <include name="*.yml"/>
+                                    </fileset>
+                                </copy>
+                            </tasks>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <version>2.4</version>
+                <configuration>
+                    <descriptorRefs>
+                        <descriptorRef>jar-with-dependencies</descriptorRef>
+                    </descriptorRefs>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>make-assembly</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 115 - 0
client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/HbaseAdapter.java

@@ -0,0 +1,115 @@
+package com.alibaba.otter.canal.client.adapter.hbase;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.HBaseConfiguration;
+import org.apache.hadoop.hbase.client.Connection;
+import org.apache.hadoop.hbase.client.ConnectionFactory;
+
+import com.alibaba.otter.canal.client.adapter.CanalOuterAdapter;
+import com.alibaba.otter.canal.client.adapter.hbase.config.MappingConfig;
+import com.alibaba.otter.canal.client.adapter.hbase.config.MappingConfigLoader;
+import com.alibaba.otter.canal.client.adapter.hbase.service.HbaseSyncService;
+import com.alibaba.otter.canal.client.adapter.support.CanalOuterAdapterConfiguration;
+import com.alibaba.otter.canal.client.adapter.support.Dml;
+import com.alibaba.otter.canal.client.adapter.support.MessageUtil;
+import com.alibaba.otter.canal.client.adapter.support.SPI;
+import com.alibaba.otter.canal.protocol.Message;
+
+/**
+ * HBase外部适配器
+ *
+ * @author machengyuan 2018-8-21 下午8:45:38
+ * @version 1.0.0
+ */
+@SPI("hbase")
+public class HbaseAdapter implements CanalOuterAdapter {
+
+    private static volatile Map<String, MappingConfig> mappingConfigCache = null;
+
+    private Connection                                 conn;
+    private HbaseSyncService                           hbaseSyncService;
+
+    @Override
+    public void init(CanalOuterAdapterConfiguration configuration) {
+        try {
+            if (mappingConfigCache == null) {
+                synchronized (MappingConfig.class) {
+                    if (mappingConfigCache == null) {
+                        Map<String, MappingConfig> hbaseMapping = MappingConfigLoader.load();
+                        mappingConfigCache = new HashMap<>();
+                        for (MappingConfig mappingConfig : hbaseMapping.values()) {
+                            mappingConfigCache.put(mappingConfig.getHbaseOrm().getDatabase() + "-"
+                                                   + mappingConfig.getHbaseOrm().getTable(),
+                                mappingConfig);
+                        }
+                    }
+                }
+            }
+
+            String hosts = configuration.getZkHosts();
+            if (StringUtils.isEmpty(hosts)) {
+                hosts = configuration.getHosts();
+            }
+            if (StringUtils.isEmpty(hosts)) {
+                throw new RuntimeException("Empty zookeeper hosts");
+            }
+            String[] zkHosts = StringUtils.split(hosts, ",");
+            int zkPort = 0;
+            StringBuilder hostsWithoutPort = new StringBuilder();
+            for (String host : zkHosts) {
+                int i = host.indexOf(":");
+                hostsWithoutPort.append(host, 0, i);
+                hostsWithoutPort.append(",");
+                if (zkPort == 0) zkPort = Integer.parseInt(host.substring(i + 1));
+            }
+            hostsWithoutPort.deleteCharAt(hostsWithoutPort.length() - 1);
+
+            String znode = configuration.getProperties().getProperty("znodeParent");
+            if (StringUtils.isEmpty(znode)) {
+                znode = "/hbase";
+            }
+
+            Configuration hbaseConfig = HBaseConfiguration.create();
+            hbaseConfig.set("hbase.zookeeper.quorum", hostsWithoutPort.toString());
+            hbaseConfig.set("hbase.zookeeper.property.clientPort", Integer.toString(zkPort));
+            hbaseConfig.set("zookeeper.znode.parent", znode);
+            conn = ConnectionFactory.createConnection(hbaseConfig);
+            hbaseSyncService = new HbaseSyncService(conn);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public void writeOut(Message message) {
+        MessageUtil.parse4Dml(message, new MessageUtil.Consumer<Dml>() {
+
+            @Override
+            public void accept(Dml dml) {
+                if (dml == null) {
+                    return;
+                }
+                String database = dml.getDatabase();
+                String table = dml.getTable();
+                MappingConfig config = mappingConfigCache.get(database + "-" + table);
+                hbaseSyncService.sync(config, dml);
+            }
+        });
+    }
+
+    @Override
+    public void destroy() {
+        if (conn != null) {
+            try {
+                conn.close();
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+}

+ 337 - 0
client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/config/MappingConfig.java

@@ -0,0 +1,337 @@
+package com.alibaba.otter.canal.client.adapter.hbase.config;
+
+import java.util.*;
+
+/**
+ * HBase表映射配置
+ *
+ * @author machengyuan 2018-8-21 下午06:45:49
+ * @version 1.0.0
+ */
+public class MappingConfig {
+
+    private HbaseOrm hbaseOrm;
+
+    public HbaseOrm getHbaseOrm() {
+        return hbaseOrm;
+    }
+
+    public void setHbaseOrm(HbaseOrm hbaseOrm) {
+        this.hbaseOrm = hbaseOrm;
+    }
+
+    public void validate() {
+        if (hbaseOrm.database == null || hbaseOrm.database.isEmpty()) {
+            throw new NullPointerException("hbaseOrm.database");
+        }
+        if (hbaseOrm.table == null || hbaseOrm.table.isEmpty()) {
+            throw new NullPointerException("hbaseOrm.table");
+        }
+        if (hbaseOrm.hbaseTable == null || hbaseOrm.hbaseTable.isEmpty()) {
+            throw new NullPointerException("hbaseOrm.hbaseTable");
+        }
+        if (hbaseOrm.mode == null) {
+            throw new NullPointerException("hbaseOrm.mode");
+        }
+        if (hbaseOrm.rowKey != null && hbaseOrm.rowKeyColumn != null) {
+            throw new RuntimeException("已配置了复合主键作为RowKey,无需再指定RowKey列");
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        MappingConfig config = (MappingConfig) o;
+
+        return hbaseOrm != null ? hbaseOrm.equals(config.hbaseOrm) : config.hbaseOrm == null;
+    }
+
+    @Override
+    public int hashCode() {
+        return hbaseOrm != null ? hbaseOrm.hashCode() : 0;
+    }
+
+    public static class ColumnItem {
+
+        private boolean isRowKey = false;
+        private String  column;
+        private String  family;
+        private String  qualifier;
+        private String  type;
+
+        public boolean isRowKey() {
+            return isRowKey;
+        }
+
+        public void setRowKey(boolean rowKey) {
+            isRowKey = rowKey;
+        }
+
+        public String getColumn() {
+            return column;
+        }
+
+        public void setColumn(String column) {
+            this.column = column;
+        }
+
+        public String getFamily() {
+            return family;
+        }
+
+        public void setFamily(String family) {
+            this.family = family;
+        }
+
+        public String getQualifier() {
+            return qualifier;
+        }
+
+        public void setQualifier(String qualifier) {
+            this.qualifier = qualifier;
+        }
+
+        public String getType() {
+            return type;
+        }
+
+        public void setType(String type) {
+            this.type = type;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            ColumnItem that = (ColumnItem) o;
+            return Objects.equals(column, that.column);
+        }
+
+        @Override
+        public int hashCode() {
+
+            return Objects.hash(column);
+        }
+    }
+
+    public enum Mode {
+                      STRING("STRING"), NATIVE("NATIVE"), PHOENIX("PHOENIX");
+
+        private String type;
+
+        public String getType() {
+            return type;
+        }
+
+        Mode(String type){
+            this.type = type;
+        }
+    }
+
+    public static class HbaseOrm {
+
+        private Mode                    mode               = Mode.STRING;
+        private String                  database;
+        private String                  table;
+        private String                  hbaseTable;
+        private String                  family             = "CF";
+        private boolean                 uppercaseQualifier = true;
+        private boolean                 autoCreateTable    = false;                // 同步时HBase中表不存在的情况下自动建表
+        private String                  rowKey;                                    // 指定复合主键为rowKey
+        private Map<String, String>     columns;
+        private ColumnItem              rowKeyColumn;
+        private String                  etlCondition;
+
+        private Map<String, ColumnItem> columnItems        = new LinkedHashMap<>();
+        private Set<String>             families           = new LinkedHashSet<>();
+        private int                     readBatch          = 5000;
+        private int                     commitBatch        = 5000;
+
+        public Mode getMode() {
+            return mode;
+        }
+
+        public void setMode(Mode mode) {
+            this.mode = mode;
+        }
+
+        public String getDatabase() {
+            return database;
+        }
+
+        public void setDatabase(String database) {
+            this.database = database;
+        }
+
+        public String getTable() {
+            return table;
+        }
+
+        public void setTable(String table) {
+            this.table = table;
+        }
+
+        public String getHbaseTable() {
+            return hbaseTable;
+        }
+
+        public void setHbaseTable(String hbaseTable) {
+            this.hbaseTable = hbaseTable;
+        }
+
+        public Map<String, String> getColumns() {
+            return columns;
+        }
+
+        public boolean isAutoCreateTable() {
+            return autoCreateTable;
+        }
+
+        public void setAutoCreateTable(boolean autoCreateTable) {
+            this.autoCreateTable = autoCreateTable;
+        }
+
+        public int getReadBatch() {
+            return readBatch;
+        }
+
+        public void setReadBatch(int readBatch) {
+            this.readBatch = readBatch;
+        }
+
+        public int getCommitBatch() {
+            return commitBatch;
+        }
+
+        public void setCommitBatch(int commitBatch) {
+            this.commitBatch = commitBatch;
+        }
+
+        public String getRowKey() {
+            return rowKey;
+        }
+
+        public void setRowKey(String rowKey) {
+            this.rowKey = rowKey;
+        }
+
+        public String getEtlCondition() {
+            return etlCondition;
+        }
+
+        public void setEtlCondition(String etlCondition) {
+            this.etlCondition = etlCondition;
+        }
+
+        public void setColumns(Map<String, String> columns) {
+            this.columns = columns;
+
+            if (columns != null) {
+                for (Map.Entry<String, String> columnField : columns.entrySet()) {
+                    String field = columnField.getValue();
+                    String type = null;
+                    if (field != null) {
+                        // 解析类型
+                        int i = field.indexOf("$");
+                        if (i > -1) {
+                            type = field.substring(i + 1);
+                            field = field.substring(0, i);
+                        }
+                    }
+                    ColumnItem columnItem = new ColumnItem();
+                    columnItem.setColumn(columnField.getKey());
+                    columnItem.setType(type);
+                    if ("rowKey".equalsIgnoreCase(field)) {
+                        columnItem.setRowKey(true);
+                        rowKeyColumn = columnItem;
+                    } else {
+                        if (field == null || field.equals("")) {
+                            columnItem.setFamily(family);
+                            columnItem.setQualifier(columnField.getKey());
+                        } else {
+                            int len = field.indexOf(":");
+                            if (len > -1) {
+                                columnItem.setFamily(field.substring(0, len));
+                                columnItem.setQualifier(field.substring(len + 1));
+                            } else {
+                                columnItem.setFamily(family);
+                                columnItem.setQualifier(field);
+                            }
+                        }
+                        if (uppercaseQualifier) {
+                            columnItem.setQualifier(columnItem.getQualifier().toUpperCase());
+                        }
+                        families.add(columnItem.getFamily());
+                    }
+
+                    columnItems.put(columnField.getKey(), columnItem);
+                }
+            } else {
+                this.columns = new LinkedHashMap<>();
+            }
+        }
+
+        public String getFamily() {
+            return family;
+        }
+
+        public void setFamily(String family) {
+            this.family = family;
+            if (family == null) {
+                this.family = "CF";
+            }
+        }
+
+        public boolean isUppercaseQualifier() {
+            return uppercaseQualifier;
+        }
+
+        public void setUppercaseQualifier(boolean uppercaseQualifier) {
+            this.uppercaseQualifier = uppercaseQualifier;
+        }
+
+        public ColumnItem getRowKeyColumn() {
+            return rowKeyColumn;
+        }
+
+        public void setRowKeyColumn(ColumnItem rowKeyColumn) {
+            this.rowKeyColumn = rowKeyColumn;
+        }
+
+        public Map<String, ColumnItem> getColumnItems() {
+            return columnItems;
+        }
+
+        public void setColumnItems(Map<String, ColumnItem> columnItems) {
+            this.columnItems = columnItems;
+        }
+
+        public Set<String> getFamilies() {
+            return families;
+        }
+
+        public void setFamilies(Set<String> families) {
+            this.families = families;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+
+            HbaseOrm hbaseOrm = (HbaseOrm) o;
+
+            if (table != null ? !table.equals(hbaseOrm.table) : hbaseOrm.table != null) return false;
+            return hbaseTable != null ? hbaseTable.equals(hbaseOrm.hbaseTable) : hbaseOrm.hbaseTable == null;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = table != null ? table.hashCode() : 0;
+            result = 31 * result + (hbaseTable != null ? hbaseTable.hashCode() : 0);
+            return result;
+        }
+    }
+}

+ 149 - 0
client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/config/MappingConfigLoader.java

@@ -0,0 +1,149 @@
+package com.alibaba.otter.canal.client.adapter.hbase.config;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.yaml.snakeyaml.Yaml;
+
+/**
+ * HBase表映射配置加载器
+ * <p>
+ * 配置统一从hbase-mapping/configs.conf文件作为入口, 该文件包含所有表映射配置的名称或者文件名列表。
+ * 每个对应的表配置可以yml配置文件或者以database.table为配置名的简化形式
+ * </p>
+ *
+ * @author machengyuan 2018-8-21 下午06:45:49
+ * @version 1.0.0
+ */
+public class MappingConfigLoader {
+
+    private static Logger       logger    = LoggerFactory.getLogger(MappingConfigLoader.class);
+
+    private static final String BASE_PATH = "hbase-mapping/";
+
+    /**
+     * 加载HBase表映射配置
+     * 
+     * @return 配置名/配置文件名--对象
+     */
+    public static Map<String, MappingConfig> load() {
+        logger.info("## Start loading mapping config ... ");
+        String mappingConfigContent = readConfigContent(BASE_PATH + "configs.conf");
+
+        Map<String, MappingConfig> result = new LinkedHashMap<>();
+
+        String[] configLines = mappingConfigContent.split("\n");
+        for (String c : configLines) {
+            if (c == null) {
+                continue;
+            }
+            c = c.trim();
+            if (c.startsWith("#")) {
+                continue;
+            }
+
+            MappingConfig config;
+            String configContent = null;
+
+            if (c.endsWith(".yml")) {
+                configContent = readConfigContent(BASE_PATH + "/" + c);
+            }
+
+            // 简单配置database.table@datasourcekey?rowKey=key1,key2
+            if (StringUtils.isEmpty(configContent)) {
+                String[] mapping = c.split("\\?");
+                String params = mapping.length == 2 ? mapping[1] : null;
+                String rowKey = null;
+                String srcMeta = mapping[0];
+                //
+                if (params != null) {
+                    for (String entry : params.split("&")) {
+                        if ("rowKey".equals(entry.split("=")[0])) {
+                            rowKey = entry.split("=")[1];
+                        }
+                    }
+                }
+                String dsKey = srcMeta.split("@").length == 2 ? srcMeta.split("@")[1] : null;
+                String[] dbTable;
+                if (dsKey == null) {
+                    dbTable = srcMeta.split("\\.");
+                } else {
+                    dbTable = srcMeta.split("@")[0].split("\\.");
+                }
+
+                if (dbTable.length == 2) {
+                    config = new MappingConfig();
+
+                    MappingConfig.HbaseOrm hbaseOrm = new MappingConfig.HbaseOrm();
+                    hbaseOrm.setHbaseTable(dbTable[0].toUpperCase() + "." + dbTable[1].toUpperCase());
+                    hbaseOrm.setAutoCreateTable(true);
+                    hbaseOrm.setDatabase(dbTable[0]);
+                    hbaseOrm.setTable(dbTable[1]);
+                    hbaseOrm.setMode(MappingConfig.Mode.STRING);
+                    hbaseOrm.setRowKey(rowKey);
+                    // 有定义rowKey
+                    if (rowKey != null) {
+                        MappingConfig.ColumnItem columnItem = new MappingConfig.ColumnItem();
+                        columnItem.setRowKey(true);
+                        columnItem.setColumn(rowKey);
+                        hbaseOrm.setRowKeyColumn(columnItem);
+                    }
+                    config.setHbaseOrm(hbaseOrm);
+
+                } else {
+                    throw new RuntimeException(String.format("配置项[%s]内容为空, 或格式不符合database.table", c));
+                }
+
+            } else { // 配置文件配置
+                config = new Yaml().loadAs(configContent, MappingConfig.class);
+            }
+
+            try {
+                config.validate();
+            } catch (Exception e) {
+                throw new RuntimeException("ERROR Config: " + c + " " + e.getMessage(), e);
+            }
+            result.put(c, config);
+        }
+
+        logger.info("## Mapping config loaded");
+        return result;
+    }
+
+    public static String readConfigContent(String config) {
+        InputStream in = null;
+        try {
+            // 先取本地文件,再取类路径
+            File configFile = new File("config/" + config);
+            if (configFile.exists()) {
+                in = new FileInputStream(configFile);
+            } else {
+                in = MappingConfigLoader.class.getClassLoader().getResourceAsStream(config);
+            }
+            if (in == null) {
+                throw new RuntimeException("Config file not found.");
+            }
+
+            byte[] bytes = new byte[in.available()];
+            in.read(bytes);
+            return new String(bytes, "UTF-8");
+        } catch (IOException e) {
+            throw new RuntimeException("Read ds-config.yml or hbase-mappings.conf error. ", e);
+        } finally {
+            try {
+                if (in != null) {
+                    in.close();
+                }
+            } catch (IOException e) {
+                // ignore
+            }
+        }
+    }
+}

+ 418 - 0
client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/service/HbaseSyncService.java

@@ -0,0 +1,418 @@
+package com.alibaba.otter.canal.client.adapter.hbase.service;
+
+import java.util.*;
+
+import org.apache.hadoop.hbase.client.Connection;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.alibaba.otter.canal.client.adapter.hbase.config.MappingConfig;
+import com.alibaba.otter.canal.client.adapter.hbase.support.*;
+import com.alibaba.otter.canal.client.adapter.support.Dml;
+
+/**
+ * HBase同步操作业务
+ *
+ * @author machengyuan 2018-8-21 下午06:45:49
+ * @version 1.0.0
+ */
+public class HbaseSyncService {
+
+    private Logger        logger = LoggerFactory.getLogger(this.getClass());
+
+    private HbaseTemplate hbaseTemplate;                                    // HBase操作模板
+
+    public HbaseSyncService(Connection conn){
+        hbaseTemplate = new HbaseTemplate(conn);
+    }
+
+    public void sync(MappingConfig config, Dml dml) {
+        try {
+            if (config != null) {
+                String type = dml.getType();
+                if (type != null && type.equalsIgnoreCase("INSERT")) {
+                    insert(config, dml);
+                } else if (type != null && type.equalsIgnoreCase("UPDATE")) {
+                    update(config, dml);
+                } else if (type != null && type.equalsIgnoreCase("DELETE")) {
+                    delete(config, dml);
+                }
+                if (logger.isDebugEnabled()) {
+                    String res = dml.toString();
+                    logger.debug(res);
+                }
+            }
+        } catch (Exception e) {
+            logger.error(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 插入操作
+     * 
+     * @param config 配置项
+     * @param dml DML数据
+     */
+    private void insert(MappingConfig config, Dml dml) {
+        List<Map<String, Object>> data = dml.getData();
+        if (data == null || data.isEmpty()) {
+            return;
+        }
+
+        MappingConfig.HbaseOrm hbaseOrm = config.getHbaseOrm();
+
+        // if (!validHTable(config)) {
+        // logger.error("HBase table '{}' not exists", hbaseOrm.getHbaseTable());
+        // return;
+        // }
+        int i = 1;
+        boolean complete = false;
+        List<HRow> rows = new ArrayList<>();
+        for (Map<String, Object> r : data) {
+            HRow hRow = new HRow();
+
+            // 拼接复合rowKey
+            if (hbaseOrm.getRowKey() != null) {
+                String[] rowKeyColumns = hbaseOrm.getRowKey().trim().split(",");
+                String rowKeyVale = getRowKeys(rowKeyColumns, r);
+                // params.put("rowKey", Bytes.toBytes(rowKeyVale));
+                hRow.setRowKey(Bytes.toBytes(rowKeyVale));
+            }
+
+            convertData2Row(hbaseOrm, hRow, r);
+            if (hRow.getRowKey() == null) {
+                throw new RuntimeException("empty rowKey");
+            }
+            rows.add(hRow);
+            complete = false;
+            if (i % config.getHbaseOrm().getCommitBatch() == 0 && !rows.isEmpty()) {
+                hbaseTemplate.puts(hbaseOrm.getHbaseTable(), rows);
+                rows.clear();
+                complete = true;
+            }
+            i++;
+        }
+        if (!complete && !rows.isEmpty()) {
+            hbaseTemplate.puts(hbaseOrm.getHbaseTable(), rows);
+        }
+
+    }
+
+    /**
+     * 将Map数据转换为HRow行数据
+     * 
+     * @param hbaseOrm hbase映射配置
+     * @param hRow 行对象
+     * @param data Map数据
+     */
+    private static void convertData2Row(MappingConfig.HbaseOrm hbaseOrm, HRow hRow, Map<String, Object> data) {
+        Map<String, MappingConfig.ColumnItem> columnItems = hbaseOrm.getColumnItems();
+        int i = 0;
+        for (Map.Entry<String, Object> entry : data.entrySet()) {
+            if (entry.getValue() != null) {
+                MappingConfig.ColumnItem columnItem = columnItems.get(entry.getKey());
+
+                byte[] bytes = typeConvert(columnItem, hbaseOrm, entry.getValue());
+
+                if (columnItem == null) {
+                    String familyName = hbaseOrm.getFamily();
+                    String qualifier = entry.getKey();
+                    if (hbaseOrm.isUppercaseQualifier()) {
+                        qualifier = qualifier.toUpperCase();
+                    }
+
+                    if (hbaseOrm.getRowKey() == null && i == 0) {
+                        hRow.setRowKey(bytes);
+                    } else {
+                        hRow.addCell(familyName, qualifier, bytes);
+                    }
+                } else {
+                    if (columnItem.isRowKey()) {
+                        // row.put("rowKey", bytes);
+                        hRow.setRowKey(bytes);
+                    } else {
+                        hRow.addCell(columnItem.getFamily(), columnItem.getQualifier(), bytes);
+                    }
+                }
+            }
+            i++;
+        }
+    }
+
+    /**
+     * 更新操作
+     * 
+     * @param config
+     * @param dml
+     */
+    private void update(MappingConfig config, Dml dml) {
+        List<Map<String, Object>> data = dml.getData();
+        List<Map<String, Object>> old = dml.getOld();
+        if (old == null || old.isEmpty() || data == null || data.isEmpty()) {
+            return;
+        }
+
+        MappingConfig.HbaseOrm hbaseOrm = config.getHbaseOrm();
+
+        // if (!validHTable(config)) {
+        // logger.error("HBase table '{}' not exists", hbaseOrm.getHbaseTable());
+        // return;
+        // }
+
+        MappingConfig.ColumnItem rowKeyColumn = hbaseOrm.getRowKeyColumn();
+        int index = 0;
+        int i = 1;
+        boolean complete = false;
+        List<HRow> rows = new ArrayList<>();
+        out: for (Map<String, Object> r : data) {
+            byte[] rowKeyBytes;
+
+            if (hbaseOrm.getRowKey() != null) {
+                String[] rowKeyColumns = hbaseOrm.getRowKey().trim().split(",");
+
+                // 判断是否有复合主键修改
+                for (String updateColumn : old.get(index).keySet()) {
+                    for (String rowKeyColumnName : rowKeyColumns) {
+                        if (rowKeyColumnName.equalsIgnoreCase(updateColumn)) {
+                            // 调用删除插入操作
+                            deleteAndInsert(config, dml);
+                            continue out;
+                        }
+                    }
+                }
+
+                String rowKeyVale = getRowKeys(rowKeyColumns, r);
+                rowKeyBytes = Bytes.toBytes(rowKeyVale);
+            } else if (rowKeyColumn == null) {
+                Map<String, Object> rowKey = data.get(0);
+                rowKeyBytes = typeConvert(null, hbaseOrm, rowKey.values().iterator().next());
+            } else {
+                rowKeyBytes = typeConvert(rowKeyColumn, hbaseOrm, r.get(rowKeyColumn.getColumn()));
+            }
+            if (rowKeyBytes == null) throw new RuntimeException("rowKey值为空");
+
+            Map<String, MappingConfig.ColumnItem> columnItems = hbaseOrm.getColumnItems();
+            HRow hRow = new HRow(rowKeyBytes);
+            for (String updateColumn : old.get(index).keySet()) {
+                MappingConfig.ColumnItem columnItem = columnItems.get(updateColumn);
+                if (columnItem == null) {
+                    String family = hbaseOrm.getFamily();
+                    String qualifier = updateColumn;
+                    if (hbaseOrm.isUppercaseQualifier()) {
+                        qualifier = qualifier.toUpperCase();
+                    }
+
+                    Object newVal = r.get(updateColumn);
+
+                    if (newVal == null) {
+                        hRow.addCell(family, qualifier, null);
+                    } else {
+                        hRow.addCell(family, qualifier, typeConvert(null, hbaseOrm, newVal));
+                    }
+                } else {
+                    // 排除修改id的情况
+                    if (columnItem.isRowKey()) continue;
+
+                    Object newVal = r.get(updateColumn);
+                    if (newVal == null) {
+                        hRow.addCell(columnItem.getFamily(), columnItem.getQualifier(), null);
+                    } else {
+                        hRow.addCell(columnItem.getFamily(),
+                            columnItem.getQualifier(),
+                            typeConvert(columnItem, hbaseOrm, newVal));
+                    }
+                }
+            }
+            rows.add(hRow);
+            complete = false;
+            if (i % config.getHbaseOrm().getCommitBatch() == 0 && !rows.isEmpty()) {
+                hbaseTemplate.puts(hbaseOrm.getHbaseTable(), rows);
+                rows.clear();
+                complete = true;
+            }
+            i++;
+            index++;
+        }
+        if (!complete && !rows.isEmpty()) {
+            hbaseTemplate.puts(hbaseOrm.getHbaseTable(), rows);
+        }
+    }
+
+    private void delete(MappingConfig config, Dml dml) {
+        List<Map<String, Object>> data = dml.getData();
+        if (data == null || data.isEmpty()) {
+            return;
+        }
+
+        MappingConfig.HbaseOrm hbaseOrm = config.getHbaseOrm();
+
+        // if (!validHTable(config)) {
+        // logger.error("HBase table '{}' not exists", hbaseOrm.getHbaseTable());
+        // return;
+        // }
+
+        MappingConfig.ColumnItem rowKeyColumn = hbaseOrm.getRowKeyColumn();
+        boolean complete = false;
+        int i = 1;
+        Set<byte[]> rowKeys = new HashSet<>();
+        for (Map<String, Object> r : data) {
+            byte[] rowKeyBytes;
+
+            if (hbaseOrm.getRowKey() != null) {
+                String[] rowKeyColumns = hbaseOrm.getRowKey().trim().split(",");
+                String rowKeyVale = getRowKeys(rowKeyColumns, r);
+                rowKeyBytes = Bytes.toBytes(rowKeyVale);
+            } else if (rowKeyColumn == null) {
+                // 如果不需要类型转换
+                Map<String, Object> rowKey = data.get(0);
+                rowKeyBytes = typeConvert(null, hbaseOrm, rowKey.values().iterator().next());
+            } else {
+                Object val = r.get(rowKeyColumn.getColumn());
+                rowKeyBytes = typeConvert(rowKeyColumn, hbaseOrm, val);
+            }
+            if (rowKeyBytes == null) throw new RuntimeException("rowKey值为空");
+            rowKeys.add(rowKeyBytes);
+            complete = false;
+            if (i % config.getHbaseOrm().getCommitBatch() == 0 && !rowKeys.isEmpty()) {
+                hbaseTemplate.deletes(hbaseOrm.getHbaseTable(), rowKeys);
+                rowKeys.clear();
+                complete = true;
+            }
+            i++;
+        }
+        if (!complete && !rowKeys.isEmpty()) {
+            hbaseTemplate.deletes(hbaseOrm.getHbaseTable(), rowKeys);
+        }
+    }
+
+    private void deleteAndInsert(MappingConfig config, Dml dml) {
+        List<Map<String, Object>> data = dml.getData();
+        List<Map<String, Object>> old = dml.getOld();
+        if (old == null || old.isEmpty() || data == null || data.isEmpty()) {
+            return;
+        }
+        MappingConfig.HbaseOrm hbaseOrm = config.getHbaseOrm();
+
+        String[] rowKeyColumns = hbaseOrm.getRowKey().trim().split(",");
+
+        int index = 0;
+        int i = 1;
+        boolean complete = false;
+        Set<byte[]> rowKeys = new HashSet<>();
+        List<HRow> rows = new ArrayList<>();
+        for (Map<String, Object> r : data) {
+            // 拼接老的rowKey
+            List<String> updateSubRowKey = new ArrayList<>();
+            for (String rowKeyColumnName : rowKeyColumns) {
+                for (String updateColumn : old.get(index).keySet()) {
+                    if (rowKeyColumnName.equalsIgnoreCase(updateColumn)) {
+                        updateSubRowKey.add(rowKeyColumnName);
+                    }
+                }
+            }
+            if (updateSubRowKey.isEmpty()) {
+                throw new RuntimeException("没有更新复合主键的RowKey");
+            }
+            StringBuilder oldRowKey = new StringBuilder();
+            StringBuilder newRowKey = new StringBuilder();
+            for (String rowKeyColumnName : rowKeyColumns) {
+                newRowKey.append(r.get(rowKeyColumnName).toString()).append("|");
+                if (!updateSubRowKey.contains(rowKeyColumnName)) {
+                    // 从data取
+                    oldRowKey.append(r.get(rowKeyColumnName).toString()).append("|");
+                } else {
+                    // 从old取
+                    oldRowKey.append(old.get(index).get(rowKeyColumnName).toString()).append("|");
+                }
+            }
+            int len = newRowKey.length();
+            newRowKey.delete(len - 1, len);
+            len = oldRowKey.length();
+            oldRowKey.delete(len - 1, len);
+            byte[] newRowKeyBytes = Bytes.toBytes(newRowKey.toString());
+            byte[] oldRowKeyBytes = Bytes.toBytes(oldRowKey.toString());
+
+            rowKeys.add(oldRowKeyBytes);
+            HRow row = new HRow(newRowKeyBytes);
+            convertData2Row(hbaseOrm, row, r);
+            rows.add(row);
+            complete = false;
+            if (i % config.getHbaseOrm().getCommitBatch() == 0 && !rows.isEmpty()) {
+                hbaseTemplate.deletes(hbaseOrm.getHbaseTable(), rowKeys);
+
+                hbaseTemplate.puts(hbaseOrm.getHbaseTable(), rows);
+                rowKeys.clear();
+                rows.clear();
+                complete = true;
+            }
+            i++;
+            index++;
+        }
+        if (!complete && !rows.isEmpty()) {
+            hbaseTemplate.deletes(hbaseOrm.getHbaseTable(), rowKeys);
+            hbaseTemplate.puts(hbaseOrm.getHbaseTable(), rows);
+        }
+    }
+
+    /**
+     * 根据对应的类型进行转换
+     * 
+     * @param columnItem 列项配置
+     * @param hbaseOrm hbase映射配置
+     * @param value 值
+     * @return 复合字段rowKey
+     */
+    private static byte[] typeConvert(MappingConfig.ColumnItem columnItem, MappingConfig.HbaseOrm hbaseOrm,
+                                      Object value) {
+        if (value == null) {
+            return null;
+        }
+        byte[] bytes = null;
+        if (columnItem == null || columnItem.getType() == null || "".equals(columnItem.getType())) {
+            if (MappingConfig.Mode.STRING == hbaseOrm.getMode()) {
+                bytes = Bytes.toBytes(value.toString());
+            } else if (MappingConfig.Mode.NATIVE == hbaseOrm.getMode()) {
+                bytes = TypeUtil.toBytes(value);
+            } else if (MappingConfig.Mode.PHOENIX == hbaseOrm.getMode()) {
+                PhType phType = PhType.getType(value.getClass());
+                bytes = PhTypeUtil.toBytes(value, phType);
+            }
+        } else {
+            if (hbaseOrm.getMode() == MappingConfig.Mode.STRING) {
+                bytes = Bytes.toBytes(value.toString());
+            } else if (hbaseOrm.getMode() == MappingConfig.Mode.NATIVE) {
+                Type type = Type.getType(columnItem.getType());
+                bytes = TypeUtil.toBytes(value, type);
+            } else if (hbaseOrm.getMode() == MappingConfig.Mode.PHOENIX) {
+                PhType phType = PhType.getType(columnItem.getType());
+                bytes = PhTypeUtil.toBytes(value, phType);
+            }
+        }
+        return bytes;
+    }
+
+    /**
+     * 获取复合字段作为rowKey的拼接
+     *
+     * @param rowKeyColumns 复合rowK对应的字段
+     * @param data 数据
+     * @return
+     */
+    private static String getRowKeys(String[] rowKeyColumns, Map<String, Object> data) {
+        StringBuilder rowKeyValue = new StringBuilder();
+        for (String rowKeyColumnName : rowKeyColumns) {
+            Object obj = data.get(rowKeyColumnName);
+            if (obj != null) {
+                rowKeyValue.append(obj.toString());
+            }
+            rowKeyValue.append("|");
+        }
+        int len = rowKeyValue.length();
+        if (len > 0) {
+            rowKeyValue.delete(len - 1, len);
+        }
+        return rowKeyValue.toString();
+    }
+
+}

+ 84 - 0
client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/support/HRow.java

@@ -0,0 +1,84 @@
+package com.alibaba.otter.canal.client.adapter.hbase.support;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * HBase操作对象类
+ *
+ * @author machengyuan 2018-8-21 下午10:12:34
+ * @version 1.0.0
+ */
+public class HRow {
+
+    private byte[]      rowKey;
+    private List<HCell> cells = new ArrayList<>();
+
+    public HRow(){
+    }
+
+    public HRow(byte[] rowKey){
+        this.rowKey = rowKey;
+    }
+
+    public byte[] getRowKey() {
+        return rowKey;
+    }
+
+    public void setRowKey(byte[] rowKey) {
+        this.rowKey = rowKey;
+    }
+
+    public List<HCell> getCells() {
+        return cells;
+    }
+
+    public void setCells(List<HCell> cells) {
+        this.cells = cells;
+    }
+
+    public void addCell(String family, String qualifier, byte[] value) {
+        HCell hCell = new HCell(family, qualifier, value);
+        cells.add(hCell);
+    }
+
+    public static class HCell {
+
+        private String family;
+        private String qualifier;
+        private byte[] value;
+
+        public HCell(){
+        }
+
+        public HCell(String family, String qualifier, byte[] value){
+            this.family = family;
+            this.qualifier = qualifier;
+            this.value = value;
+        }
+
+        public String getFamily() {
+            return family;
+        }
+
+        public void setFamily(String family) {
+            this.family = family;
+        }
+
+        public String getQualifier() {
+            return qualifier;
+        }
+
+        public void setQualifier(String qualifier) {
+            this.qualifier = qualifier;
+        }
+
+        public byte[] getValue() {
+            return value;
+        }
+
+        public void setValue(byte[] value) {
+            this.value = value;
+        }
+    }
+}

+ 159 - 0
client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/support/HbaseTemplate.java

@@ -0,0 +1,159 @@
+package com.alibaba.otter.canal.client.adapter.hbase.support;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.hadoop.hbase.HColumnDescriptor;
+import org.apache.hadoop.hbase.HTableDescriptor;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.*;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * HBase操作模板
+ *
+ * @author machengyuan 2018-8-21 下午10:12:34
+ * @version 1.0.0
+ */
+public class HbaseTemplate {
+
+    private Logger     logger = LoggerFactory.getLogger(this.getClass());
+
+    private Connection conn;
+
+    public HbaseTemplate(Connection conn){
+        this.conn = conn;
+    }
+
+    public boolean tableExists(String tableName) {
+        try (HBaseAdmin admin = (HBaseAdmin) conn.getAdmin()) {
+
+            return admin.tableExists(TableName.valueOf(tableName));
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public void createTable(String tableName, String... familyNames) {
+        try (HBaseAdmin admin = (HBaseAdmin) conn.getAdmin()) {
+
+            HTableDescriptor desc = new HTableDescriptor(TableName.valueOf(tableName));
+            // 添加列簇
+            if (familyNames != null) {
+                for (String familyName : familyNames) {
+                    HColumnDescriptor hcd = new HColumnDescriptor(familyName);
+                    desc.addFamily(hcd);
+                }
+            }
+            admin.createTable(desc);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public void disableTable(String tableName) {
+        try (HBaseAdmin admin = (HBaseAdmin) conn.getAdmin()) {
+            admin.disableTable(tableName);
+        } catch (IOException e) {
+            logger.error(e.getMessage(), e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public void deleteTable(String tableName) {
+        try (HBaseAdmin admin = (HBaseAdmin) conn.getAdmin()) {
+            if (admin.isTableEnabled(tableName)) {
+                disableTable(tableName);
+            }
+            admin.deleteTable(tableName);
+        } catch (IOException e) {
+            logger.error(e.getMessage(), e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 插入一行数据
+     * 
+     * @param tableName 表名
+     * @param hRow 行数据对象
+     * @return 是否成功
+     */
+    public Boolean put(String tableName, HRow hRow) {
+        boolean flag = false;
+        try {
+            HTable table = (HTable) conn.getTable(TableName.valueOf(tableName));
+            Put put = new Put(hRow.getRowKey());
+            for (HRow.HCell hCell : hRow.getCells()) {
+                put.addColumn(Bytes.toBytes(hCell.getFamily()), Bytes.toBytes(hCell.getQualifier()), hCell.getValue());
+            }
+            table.put(put);
+            flag = true;
+        } catch (Exception e) {
+            logger.error(e.getMessage(), e);
+        }
+        return flag;
+
+    }
+
+    /**
+     * 批量插入
+     * 
+     * @param tableName 表名
+     * @param rows 行数据对象集合
+     * @return 是否成功
+     */
+    public Boolean puts(String tableName, List<HRow> rows) {
+        boolean flag = false;
+        try {
+            HTable table = (HTable) conn.getTable(TableName.valueOf(tableName));
+            List<Put> puts = new ArrayList<>();
+            for (HRow hRow : rows) {
+                Put put = new Put(hRow.getRowKey());
+                for (HRow.HCell hCell : hRow.getCells()) {
+                    put.addColumn(Bytes.toBytes(hCell.getFamily()),
+                        Bytes.toBytes(hCell.getQualifier()),
+                        hCell.getValue());
+                }
+                puts.add(put);
+            }
+            if (!puts.isEmpty()) {
+                table.put(puts);
+            }
+            flag = true;
+        } catch (Exception e) {
+            logger.error(e.getMessage(), e);
+        }
+        return flag;
+    }
+
+    /**
+     * 批量删除数据
+     * 
+     * @param tableName 表名
+     * @param rowKeys rowKey集合
+     * @return 是否成功
+     */
+    public Boolean deletes(String tableName, Set<byte[]> rowKeys) {
+        boolean flag = false;
+        try {
+            HTable table = (HTable) conn.getTable(TableName.valueOf(tableName));
+            List<Delete> deletes = new ArrayList<>();
+            for (byte[] rowKey : rowKeys) {
+                Delete delete = new Delete(rowKey);
+                deletes.add(delete);
+            }
+            if (!deletes.isEmpty()) {
+                table.delete(deletes);
+            }
+            flag = true;
+        } catch (Exception e) {
+            logger.error(e.getMessage(), e);
+        }
+        return flag;
+    }
+}

+ 150 - 0
client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/support/PhType.java

@@ -0,0 +1,150 @@
+package com.alibaba.otter.canal.client.adapter.hbase.support;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Date;
+
+/**
+ * Phoenix类型
+ *
+ * @author machengyuan 2018-8-21 下午06:12:34
+ * @version 1.0.0
+ */
+public enum PhType {
+    DEFAULT(-1, "VARCHAR"),
+    UNSIGNED_INT(4, "UNSIGNED_INT"),
+    UNSIGNED_LONG(8, "UNSIGNED_LONG"),
+    UNSIGNED_TINYINT(1, "UNSIGNED_TINYINT"),
+    UNSIGNED_SMALLINT(2, "UNSIGNED_SMALLINT"),
+    UNSIGNED_FLOAT(4, "UNSIGNED_FLOAT"),
+    UNSIGNED_DOUBLE(8, "UNSIGNED_DOUBLE"),
+    INTEGER(4, "INTEGER"),
+    BIGINT(8, "BIGINT"),
+    TINYINT(1, "TINYINT"),
+    SMALLINT(2, "SMALLINT"),
+    FLOAT(4, "FLOAT"),
+    DOUBLE(8, "DOUBLE"),
+    DECIMAL(-1, "DECIMAL"),
+    BOOLEAN(1, "BOOLEAN"),
+    UNSIGNED_TIME(8, "UNSIGNED_TIME"),
+    UNSIGNED_DATE(8, "UNSIGNED_DATE"),
+    UNSIGNED_TIMESTAMP(12, "UNSIGNED_TIMESTAMP"),
+    TIME(8, "TIME"),
+    DATE(8, "DATE"),
+    TIMESTAMP(12, "TIMESTAMP"),
+    VARCHAR(-1, "VARCHAR"),
+    VARBINARY(-1, "VARBINARY");
+
+    /**
+     * -1:长度可变
+     */
+    private int len;
+    private String type;
+
+    PhType(int len, String type) {
+        this.len = len;
+        this.type = type;
+    }
+
+    public int getLen() {
+        return len;
+    }
+
+    public String getType() {
+        return this.type;
+    }
+
+    public static PhType getType(Class<?> javaType) {
+        if (javaType == null) return DEFAULT;
+        PhType phType;
+        if (Integer.class.isAssignableFrom(javaType) || int.class.isAssignableFrom(javaType)) {
+            phType = INTEGER;
+        } else if (Long.class.isAssignableFrom(javaType) || long.class.isAssignableFrom(javaType)) {
+            phType = BIGINT;
+        } else if (Byte.class.isAssignableFrom(javaType) || byte.class.isAssignableFrom(javaType)) {
+            phType = TINYINT;
+        } else if (Short.class.isAssignableFrom(javaType) || short.class.isAssignableFrom(javaType)) {
+            phType = SMALLINT;
+        } else if (Float.class.isAssignableFrom(javaType) || float.class.isAssignableFrom(javaType)) {
+            phType = FLOAT;
+        } else if (Double.class.isAssignableFrom(javaType) || double.class.isAssignableFrom(javaType)) {
+            phType = DOUBLE;
+        } else if (Boolean.class.isAssignableFrom(javaType) || boolean.class.isAssignableFrom(javaType)) {
+            phType = BOOLEAN;
+        } else if (java.sql.Date.class.isAssignableFrom(javaType)) {
+            phType = DATE;
+        } else if (Time.class.isAssignableFrom(javaType)) {
+            phType = DATE;
+        } else if (Timestamp.class.isAssignableFrom(javaType)) {
+            phType = TIMESTAMP;
+        } else if (Date.class.isAssignableFrom(javaType)) {
+            phType = DATE;
+        } else if (byte[].class.isAssignableFrom(javaType)) {
+            phType = VARBINARY;
+        } else if (String.class.isAssignableFrom(javaType)) {
+            phType = VARCHAR;
+        } else if (BigDecimal.class.isAssignableFrom(javaType)) {
+            phType = DECIMAL;
+        }  else if (BigInteger.class.isAssignableFrom(javaType)) {
+            phType = UNSIGNED_LONG;
+        } else {
+            phType = DEFAULT;
+        }
+        return phType;
+    }
+
+    public static PhType getType(String type) {
+        if (type == null) return DEFAULT;
+        PhType phType;
+        if (type.equalsIgnoreCase(UNSIGNED_INT.type)) {
+            phType = UNSIGNED_INT;
+        } else if (type.equalsIgnoreCase(UNSIGNED_LONG.type)) {
+            phType = UNSIGNED_LONG;
+        } else if (type.equalsIgnoreCase(UNSIGNED_TINYINT.type)) {
+            phType = UNSIGNED_TINYINT;
+        } else if (type.equalsIgnoreCase(UNSIGNED_SMALLINT.type)) {
+            phType = UNSIGNED_SMALLINT;
+        } else if (type.equalsIgnoreCase(UNSIGNED_FLOAT.type)) {
+            phType = UNSIGNED_FLOAT;
+        } else if (type.equalsIgnoreCase(UNSIGNED_DOUBLE.type)) {
+            phType = UNSIGNED_DOUBLE;
+        } else if (type.equalsIgnoreCase(INTEGER.type)) {
+            phType = INTEGER;
+        } else if (type.equalsIgnoreCase(BIGINT.type)) {
+            phType = BIGINT;
+        } else if (type.equalsIgnoreCase(TINYINT.type)) {
+            phType = TINYINT;
+        } else if (type.equalsIgnoreCase(SMALLINT.type)) {
+            phType = SMALLINT;
+        } else if (type.equalsIgnoreCase(FLOAT.type)) {
+            phType = FLOAT;
+        } else if (type.equalsIgnoreCase(DOUBLE.type)) {
+            phType = DOUBLE;
+        } else if (type.equalsIgnoreCase(BOOLEAN.type)) {
+            phType = BOOLEAN;
+        } else if (type.equalsIgnoreCase(UNSIGNED_TIME.type)) {
+            phType = UNSIGNED_TIME;
+        } else if (type.equalsIgnoreCase(UNSIGNED_DATE.type)) {
+            phType = UNSIGNED_DATE;
+        } else if (type.equalsIgnoreCase(UNSIGNED_TIMESTAMP.type)) {
+            phType = UNSIGNED_TIMESTAMP;
+        } else if (type.equalsIgnoreCase(TIME.type)) {
+            phType = TIME;
+        } else if (type.equalsIgnoreCase(DATE.type)) {
+            phType = DATE;
+        } else if (type.equalsIgnoreCase(TIMESTAMP.type)) {
+            phType = TIMESTAMP;
+        } else if (type.equalsIgnoreCase(VARCHAR.type)) {
+            phType = VARCHAR;
+        } else if (type.equalsIgnoreCase(VARBINARY.type)) {
+            phType = VARBINARY;
+        } else if (type.equalsIgnoreCase(DECIMAL.type)) {
+            phType = DECIMAL;
+        } else {
+            phType = DEFAULT;
+        }
+        return phType;
+    }
+}

+ 614 - 0
client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/support/PhTypeUtil.java

@@ -0,0 +1,614 @@
+package com.alibaba.otter.canal.client.adapter.hbase.support;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.sql.Timestamp;
+import java.util.Date;
+
+import org.apache.hadoop.hbase.util.Bytes;
+import org.joda.time.DateTime;
+
+import com.google.common.math.LongMath;
+
+/**
+ * Phoenix类型转换工具类
+ *
+ * @author machengyuan 2018-8-21 下午06:14:26
+ * @version 1.0.0
+ */
+public class PhTypeUtil {
+
+    public static byte[] toBytes(Object v, PhType phType) {
+        if (v == null) return null;
+        byte[] b = null;
+        if (phType == PhType.DEFAULT) {
+            PhType phType1 = PhType.getType(v.getClass());
+            if (phType1 != null && phType1 != PhType.DEFAULT) {
+                toBytes(v, phType1);
+            }
+        } else if (phType == PhType.INTEGER) {
+            b = new byte[Bytes.SIZEOF_INT];
+            encodeInt(((Number) v).intValue(), b, 0);
+        } else if (phType == PhType.UNSIGNED_INT) {
+            b = new byte[Bytes.SIZEOF_INT];
+            encodeUnsignedInt(((Number) v).intValue(), b, 0);
+        } else if (phType == PhType.BIGINT) {
+            b = new byte[Bytes.SIZEOF_LONG];
+            encodeLong(((Number) v).longValue(), b, 0);
+        } else if (phType == PhType.UNSIGNED_LONG) {
+            b = new byte[Bytes.SIZEOF_LONG];
+            encodeUnsignedLong(((Number) v).longValue(), b, 0);
+        } else if (phType == PhType.SMALLINT) {
+            b = new byte[Bytes.SIZEOF_SHORT];
+            encodeShort(((Number) v).shortValue(), b, 0);
+        } else if (phType == PhType.UNSIGNED_SMALLINT) {
+            b = new byte[Bytes.SIZEOF_SHORT];
+            encodeUnsignedShort(((Number) v).shortValue(), b, 0);
+        } else if (phType == PhType.TINYINT) {
+            b = new byte[Bytes.SIZEOF_BYTE];
+            encodeByte(((Number) v).byteValue(), b, 0);
+        } else if (phType == PhType.UNSIGNED_TINYINT) {
+            b = new byte[Bytes.SIZEOF_BYTE];
+            encodeUnsignedByte(((Number) v).byteValue(), b, 0);
+        } else if (phType == PhType.FLOAT) {
+            b = new byte[Bytes.SIZEOF_FLOAT];
+            encodeFloat(((Number) v).floatValue(), b, 0);
+        } else if (phType == PhType.UNSIGNED_FLOAT) {
+            b = new byte[Bytes.SIZEOF_FLOAT];
+            encodeUnsignedFloat(((Number) v).floatValue(), b, 0);
+        } else if (phType == PhType.DOUBLE) {
+            b = new byte[Bytes.SIZEOF_DOUBLE];
+            encodeDouble(((Number) v).doubleValue(), b, 0);
+        } else if (phType == PhType.UNSIGNED_DOUBLE) {
+            b = new byte[Bytes.SIZEOF_DOUBLE];
+            encodeUnsignedDouble(((Number) v).doubleValue(), b, 0);
+        } else if (phType == PhType.BOOLEAN) {
+            if ((Boolean) v) {
+                b = new byte[] { 1 };
+            } else {
+                b = new byte[] { 0 };
+            }
+        } else if (phType == PhType.TIME || phType == PhType.DATE) {
+            b = new byte[Bytes.SIZEOF_LONG];
+            encodeDate(v, b, 0);
+        } else if (phType == PhType.TIMESTAMP) {
+            b = new byte[Bytes.SIZEOF_LONG + Bytes.SIZEOF_INT];
+            encodeTimestamp(v, b, 0);
+        } else if (phType == PhType.UNSIGNED_TIME || phType == PhType.UNSIGNED_DATE) {
+            b = new byte[Bytes.SIZEOF_LONG];
+            encodeUnsignedDate(v, b, 0);
+        } else if (phType == PhType.UNSIGNED_TIMESTAMP) {
+            b = new byte[Bytes.SIZEOF_LONG + Bytes.SIZEOF_INT];
+            encodeUnsignedTimestamp(v, b, 0);
+        } else if (phType == PhType.VARBINARY) {
+            b = (byte[]) v;
+        } else if (phType == PhType.VARCHAR) {
+            b = Bytes.toBytes(v.toString());
+        } else if (phType == PhType.DECIMAL) {
+            if (v instanceof BigDecimal) {
+                b = encodeDecimal(v);
+            } else if (v instanceof Number) {
+                b = encodeDecimal(new BigDecimal(v.toString()));
+            }
+        }
+        return b;
+    }
+
+    public static Object toObject(byte[] b, PhType phType) {
+        if (b == null) return null;
+        Object v = null;
+        if (phType == PhType.INTEGER) {
+            v = decodeInt(b, 0);
+        } else if (phType == PhType.UNSIGNED_INT) {
+            v = decodeUnsignedInt(b, 0);
+        } else if (phType == PhType.BIGINT) {
+            v = decodeLong(b, 0);
+        } else if (phType == PhType.UNSIGNED_LONG) {
+            v = decodeUnsignedLong(b, 0);
+        } else if (phType == PhType.SMALLINT) {
+            v = decodeShort(b, 0);
+        } else if (phType == PhType.UNSIGNED_SMALLINT) {
+            v = decodeUnsignedShort(b, 0);
+        } else if (phType == PhType.TINYINT) {
+            v = decodeByte(b, 0);
+        } else if (phType == PhType.UNSIGNED_TINYINT) {
+            v = decodeUnsignedByte(b, 0);
+        } else if (phType == PhType.FLOAT) {
+            v = decodeFloat(b, 0);
+        } else if (phType == PhType.UNSIGNED_FLOAT) {
+            v = decodeUnsignedFloat(b, 0);
+        } else if (phType == PhType.DOUBLE) {
+            v = decodeDouble(b, 0);
+        } else if (phType == PhType.UNSIGNED_DOUBLE) {
+            v = decodeUnsignedDouble(b, 0);
+        } else if (phType == PhType.BOOLEAN) {
+            checkForSufficientLength(b, 0, Bytes.SIZEOF_BOOLEAN);
+            if (b[0] == 1) {
+                v = true;
+            } else if (b[0] == 0) {
+                v = false;
+            }
+        } else if (phType == PhType.TIME || phType == PhType.DATE) {
+            v = new Date(decodeLong(b, 0));
+        } else if (phType == PhType.TIMESTAMP) {
+            long millisDeserialized = decodeLong(b, 0);
+            Timestamp ts = new Timestamp(millisDeserialized);
+            int nanosDeserialized = decodeUnsignedInt(b, Bytes.SIZEOF_LONG);
+            ts.setNanos(nanosDeserialized < 1000000 ? ts.getNanos() + nanosDeserialized : nanosDeserialized);
+            v = ts;
+        } else if (phType == PhType.UNSIGNED_TIME || phType == PhType.UNSIGNED_DATE) {
+            v = new Date(decodeUnsignedLong(b, 0));
+        } else if (phType == PhType.UNSIGNED_TIMESTAMP) {
+            long millisDeserialized = decodeUnsignedLong(b, 0);
+            Timestamp ts = new Timestamp(millisDeserialized);
+            int nanosDeserialized = decodeUnsignedInt(b, Bytes.SIZEOF_LONG);
+            ts.setNanos(nanosDeserialized < 1000000 ? ts.getNanos() + nanosDeserialized : nanosDeserialized);
+            v = ts;
+        } else if (phType == PhType.VARBINARY) {
+            v = b;
+        } else if (phType == PhType.VARCHAR || phType == PhType.DEFAULT) {
+            v = Bytes.toString(b);
+        } else if (phType == PhType.DECIMAL) {
+            v = decodeDecimal(b, 0, b.length);
+        }
+
+        return v;
+    }
+
+    private static int decodeInt(byte[] bytes, int o) {
+        checkForSufficientLength(bytes, o, Bytes.SIZEOF_INT);
+        int v;
+        v = bytes[o] ^ 0x80; // Flip sign bit back
+        for (int i = 1; i < Bytes.SIZEOF_INT; i++) {
+            v = (v << 8) + (bytes[o + i] & 0xff);
+        }
+        return v;
+    }
+
+    private static int encodeInt(int v, byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_INT);
+        b[o + 0] = (byte) ((v >> 24) ^ 0x80); // Flip sign bit so that INTEGER is binary comparable
+        b[o + 1] = (byte) (v >> 16);
+        b[o + 2] = (byte) (v >> 8);
+        b[o + 3] = (byte) v;
+        return Bytes.SIZEOF_INT;
+    }
+
+    private static int decodeUnsignedInt(byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_INT);
+
+        int v = Bytes.toInt(b, o);
+        if (v < 0) {
+            throw new RuntimeException();
+        }
+        return v;
+    }
+
+    private static int encodeUnsignedInt(int v, byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_INT);
+        if (v < 0) {
+            throw new RuntimeException();
+        }
+        Bytes.putInt(b, o, v);
+        return Bytes.SIZEOF_INT;
+    }
+
+    private static long decodeLong(byte[] bytes, int o) {
+        checkForSufficientLength(bytes, o, Bytes.SIZEOF_LONG);
+        long v;
+        byte b = bytes[o];
+        v = b ^ 0x80; // Flip sign bit back
+        for (int i = 1; i < Bytes.SIZEOF_LONG; i++) {
+            b = bytes[o + i];
+            v = (v << 8) + (b & 0xff);
+        }
+        return v;
+    }
+
+    private static int encodeLong(long v, byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_LONG);
+        b[o + 0] = (byte) ((v >> 56) ^ 0x80); // Flip sign bit so that INTEGER is binary comparable
+        b[o + 1] = (byte) (v >> 48);
+        b[o + 2] = (byte) (v >> 40);
+        b[o + 3] = (byte) (v >> 32);
+        b[o + 4] = (byte) (v >> 24);
+        b[o + 5] = (byte) (v >> 16);
+        b[o + 6] = (byte) (v >> 8);
+        b[o + 7] = (byte) v;
+        return Bytes.SIZEOF_LONG;
+    }
+
+    private static long decodeUnsignedLong(byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_LONG);
+        long v = 0;
+        for (int i = o; i < o + Bytes.SIZEOF_LONG; i++) {
+            v <<= 8;
+            v ^= b[i] & 0xFF;
+        }
+
+        if (v < 0) {
+            throw new RuntimeException();
+        }
+        return v;
+    }
+
+    private static int encodeUnsignedLong(long v, byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_LONG);
+        if (v < 0) {
+            throw new RuntimeException();
+        }
+        Bytes.putLong(b, o, v);
+        return Bytes.SIZEOF_LONG;
+    }
+
+    private static short decodeShort(byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_SHORT);
+        int v;
+        v = b[o] ^ 0x80; // Flip sign bit back
+        for (int i = 1; i < Bytes.SIZEOF_SHORT; i++) {
+            v = (v << 8) + (b[o + i] & 0xff);
+        }
+        return (short) v;
+    }
+
+    private static int encodeShort(short v, byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_SHORT);
+        b[o + 0] = (byte) ((v >> 8) ^ 0x80); // Flip sign bit so that Short is binary comparable
+        b[o + 1] = (byte) v;
+        return Bytes.SIZEOF_SHORT;
+    }
+
+    private static short decodeUnsignedShort(byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_SHORT);
+        short v = Bytes.toShort(b, o);
+        if (v < 0) {
+            throw new RuntimeException();
+        }
+        return v;
+    }
+
+    private static int encodeUnsignedShort(short v, byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_SHORT);
+        if (v < 0) {
+            throw new RuntimeException();
+        }
+        Bytes.putShort(b, o, v);
+        return Bytes.SIZEOF_SHORT;
+    }
+
+    private static byte decodeByte(byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_BYTE);
+        int v;
+        v = b[o] ^ 0x80; // Flip sign bit back
+        return (byte) v;
+    }
+
+    private static int encodeByte(byte v, byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_BYTE);
+        b[o] = (byte) (v ^ 0x80); // Flip sign bit so that Short is binary comparable
+        return Bytes.SIZEOF_BYTE;
+    }
+
+    private static byte decodeUnsignedByte(byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_BYTE);
+        byte v = b[o];
+        if (v < 0) {
+            throw new RuntimeException();
+        }
+        return v;
+    }
+
+    private static int encodeUnsignedByte(byte v, byte[] b, int o) {
+        if (v < 0) {
+            throw new RuntimeException();
+        }
+        Bytes.putByte(b, o, v);
+        return Bytes.SIZEOF_BYTE;
+    }
+
+    private static float decodeFloat(byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_INT);
+        int value;
+        value = Bytes.toInt(b, o);
+        value--;
+        value ^= (~value >> Integer.SIZE - 1) | Integer.MIN_VALUE;
+        return Float.intBitsToFloat(value);
+    }
+
+    private static int encodeFloat(float v, byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_FLOAT);
+        int i = Float.floatToIntBits(v);
+        i = (i ^ ((i >> Integer.SIZE - 1) | Integer.MIN_VALUE)) + 1;
+        Bytes.putInt(b, o, i);
+        return Bytes.SIZEOF_FLOAT;
+    }
+
+    private static float decodeUnsignedFloat(byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_FLOAT);
+        float v = Bytes.toFloat(b, o);
+        if (v < 0) {
+            throw new RuntimeException();
+        }
+        return v;
+    }
+
+    private static int encodeUnsignedFloat(float v, byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_FLOAT);
+        if (v < 0) {
+            throw new RuntimeException();
+        }
+        Bytes.putFloat(b, o, v);
+        return Bytes.SIZEOF_FLOAT;
+    }
+
+    private static double decodeDouble(byte[] bytes, int o) {
+        checkForSufficientLength(bytes, o, Bytes.SIZEOF_LONG);
+        long l;
+        l = Bytes.toLong(bytes, o);
+        l--;
+        l ^= (~l >> Long.SIZE - 1) | Long.MIN_VALUE;
+        return Double.longBitsToDouble(l);
+    }
+
+    private static int encodeDouble(double v, byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_LONG);
+        long l = Double.doubleToLongBits(v);
+        l = (l ^ ((l >> Long.SIZE - 1) | Long.MIN_VALUE)) + 1;
+        Bytes.putLong(b, o, l);
+        return Bytes.SIZEOF_LONG;
+    }
+
+    private static double decodeUnsignedDouble(byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_DOUBLE);
+        double v = Bytes.toDouble(b, o);
+        if (v < 0) {
+            throw new RuntimeException();
+        }
+        return v;
+    }
+
+    private static int encodeUnsignedDouble(double v, byte[] b, int o) {
+        checkForSufficientLength(b, o, Bytes.SIZEOF_DOUBLE);
+        if (v < 0) {
+            throw new RuntimeException();
+        }
+        Bytes.putDouble(b, o, v);
+        return Bytes.SIZEOF_DOUBLE;
+    }
+
+    private static int encodeDate(Object v, byte[] b, int o) {
+        if (v instanceof Date) {
+            encodeLong(((Date) v).getTime(), b, 0);
+        } else if (v instanceof String) {
+            String dateStr = (String) v;
+            Date date;
+            try {
+                date = parseDatetime(dateStr);
+                if (date != null) {
+                    encodeLong(date.getTime(), b, 0);
+                }
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        }
+        return Bytes.SIZEOF_LONG;
+    }
+
+    private static int encodeTimestamp(Object v, byte[] b, int o) {
+        if (v instanceof Timestamp) {
+            Timestamp ts = (Timestamp) v;
+            encodeLong(ts.getTime(), b, o);
+            Bytes.putInt(b, Bytes.SIZEOF_LONG, ts.getNanos() % 1000000);
+        } else {
+            encodeDate(v, b, o);
+        }
+        return Bytes.SIZEOF_LONG + Bytes.SIZEOF_INT;
+    }
+
+    private static int encodeUnsignedDate(Object v, byte[] b, int o) {
+        if (v instanceof Date) {
+            encodeUnsignedLong(((Date) v).getTime(), b, 0);
+        } else if (v instanceof String) {
+            String dateStr = (String) v;
+            Date date;
+            try {
+                date = parseDatetime(dateStr);
+                if (date != null) {
+                    encodeUnsignedLong(date.getTime(), b, 0);
+                }
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        }
+        return Bytes.SIZEOF_LONG;
+    }
+
+    private static int encodeUnsignedTimestamp(Object v, byte[] b, int o) {
+        if (v instanceof Timestamp) {
+            Timestamp ts = (Timestamp) v;
+            encodeUnsignedLong(ts.getTime(), b, o);
+            Bytes.putInt(b, Bytes.SIZEOF_LONG, ts.getNanos() % 1000000);
+        } else {
+            encodeUnsignedDate(v, b, o);
+        }
+        return Bytes.SIZEOF_LONG + Bytes.SIZEOF_INT;
+    }
+
+    private static byte[] encodeDecimal(Object object) {
+        if (object == null) {
+            return new byte[0];
+        }
+        BigDecimal v = (BigDecimal) object;
+        v = v.round(DEFAULT_MATH_CONTEXT).stripTrailingZeros();
+        int len = getLength(v);
+        byte[] result = new byte[Math.min(len, 21)];
+        decimalToBytes(v, result, 0, len);
+        return result;
+    }
+
+    private static BigDecimal decodeDecimal(byte[] bytes, int offset, int length) {
+        if (length == 1 && bytes[offset] == ZERO_BYTE) {
+            return BigDecimal.ZERO;
+        }
+        int signum = ((bytes[offset] & 0x80) == 0) ? -1 : 1;
+        int scale;
+        int index;
+        int digitOffset;
+        long multiplier = 100L;
+        int begIndex = offset + 1;
+        if (signum == 1) {
+            scale = (byte) (((bytes[offset] & 0x7F) - 65) * -2);
+            index = offset + length;
+            digitOffset = POS_DIGIT_OFFSET;
+        } else {
+            scale = (byte) ((~bytes[offset] - 65 - 128) * -2);
+            index = offset + length - (bytes[offset + length - 1] == NEG_TERMINAL_BYTE ? 1 : 0);
+            digitOffset = -NEG_DIGIT_OFFSET;
+        }
+        length = index - offset;
+        long l = signum * bytes[--index] - digitOffset;
+        if (l % 10 == 0) { // trailing zero
+            scale--; // drop trailing zero and compensate in the scale
+            l /= 10;
+            multiplier = 10;
+        }
+        // Use long arithmetic for as long as we can
+        while (index > begIndex) {
+            if (l >= MAX_LONG_FOR_DESERIALIZE || multiplier >= Long.MAX_VALUE / 100) {
+                multiplier = LongMath.divide(multiplier, 100L, RoundingMode.UNNECESSARY);
+                break; // Exit loop early so we don't overflow our multiplier
+            }
+            int digit100 = signum * bytes[--index] - digitOffset;
+            l += digit100 * multiplier;
+            multiplier = LongMath.checkedMultiply(multiplier, 100);
+        }
+
+        BigInteger bi;
+        // If still more digits, switch to BigInteger arithmetic
+        if (index > begIndex) {
+            bi = BigInteger.valueOf(l);
+            BigInteger biMultiplier = BigInteger.valueOf(multiplier).multiply(ONE_HUNDRED);
+            do {
+                int digit100 = signum * bytes[--index] - digitOffset;
+                bi = bi.add(biMultiplier.multiply(BigInteger.valueOf(digit100)));
+                biMultiplier = biMultiplier.multiply(ONE_HUNDRED);
+            } while (index > begIndex);
+            if (signum == -1) {
+                bi = bi.negate();
+            }
+        } else {
+            bi = BigInteger.valueOf(l * signum);
+        }
+        // Update the scale based on the precision
+        scale += (length - 2) * 2;
+        BigDecimal v = new BigDecimal(bi, scale);
+        return v;
+    }
+
+    private static int getLength(BigDecimal v) {
+        int signum = v.signum();
+        if (signum == 0) { // Special case for zero
+            return 1;
+        }
+        return (signum < 0 ? 2 : 1) + (v.precision() + 1 + (v.scale() % 2 == 0 ? 0 : 1)) / 2;
+    }
+
+    private static final int         MAX_PRECISION            = 38;
+    private static final MathContext DEFAULT_MATH_CONTEXT     = new MathContext(MAX_PRECISION, RoundingMode.HALF_UP);
+    private static final Integer     MAX_BIG_DECIMAL_BYTES    = 21;
+    private static final byte        ZERO_BYTE                = (byte) 0x80;
+    private static final byte        NEG_TERMINAL_BYTE        = (byte) 102;
+    private static final int         EXP_BYTE_OFFSET          = 65;
+    private static final int         POS_DIGIT_OFFSET         = 1;
+    private static final int         NEG_DIGIT_OFFSET         = 101;
+    private static final BigInteger  MAX_LONG                 = BigInteger.valueOf(Long.MAX_VALUE);
+    private static final BigInteger  MIN_LONG                 = BigInteger.valueOf(Long.MIN_VALUE);
+    private static final BigInteger  ONE_HUNDRED              = BigInteger.valueOf(100);
+    private static final long        MAX_LONG_FOR_DESERIALIZE = Long.MAX_VALUE / 1000;
+
+    private static int decimalToBytes(BigDecimal v, byte[] result, final int offset, int length) {
+        int signum = v.signum();
+        if (signum == 0) {
+            result[offset] = ZERO_BYTE;
+            return 1;
+        }
+        int index = offset + length;
+        int scale = v.scale();
+        int expOffset = scale % 2 * (scale < 0 ? -1 : 1);
+        int multiplyBy;
+        BigInteger divideBy;
+        if (expOffset == 0) {
+            multiplyBy = 1;
+            divideBy = ONE_HUNDRED;
+        } else {
+            multiplyBy = 10;
+            divideBy = BigInteger.TEN;
+        }
+        // Normalize the scale based on what is necessary to end up with a base 100
+        // decimal (i.e. 10.123e3)
+        int digitOffset;
+        BigInteger compareAgainst;
+        if (signum == 1) {
+            digitOffset = POS_DIGIT_OFFSET;
+            compareAgainst = MAX_LONG;
+            scale -= (length - 2) * 2;
+            result[offset] = (byte) ((-(scale + expOffset) / 2 + EXP_BYTE_OFFSET) | 0x80);
+        } else {
+            digitOffset = NEG_DIGIT_OFFSET;
+            compareAgainst = MIN_LONG;
+            // Scale adjustment shouldn't include terminal byte in length
+            scale -= (length - 2 - 1) * 2;
+            result[offset] = (byte) (~(-(scale + expOffset) / 2 + EXP_BYTE_OFFSET + 128) & 0x7F);
+            if (length <= MAX_BIG_DECIMAL_BYTES) {
+                result[--index] = NEG_TERMINAL_BYTE;
+            } else {
+                // Adjust length and offset down because we don't have enough room
+                length = MAX_BIG_DECIMAL_BYTES;
+                index = offset + length;
+            }
+        }
+        BigInteger bi = v.unscaledValue();
+        // Use BigDecimal arithmetic until we can fit into a long
+        while (bi.compareTo(compareAgainst) * signum > 0) {
+            BigInteger[] dandr = bi.divideAndRemainder(divideBy);
+            bi = dandr[0];
+            int digit = dandr[1].intValue();
+            result[--index] = (byte) (digit * multiplyBy + digitOffset);
+            multiplyBy = 1;
+            divideBy = ONE_HUNDRED;
+        }
+        long l = bi.longValue();
+        do {
+            long divBy = 100 / multiplyBy;
+            long digit = l % divBy;
+            l /= divBy;
+            result[--index] = (byte) (digit * multiplyBy + digitOffset);
+            multiplyBy = 1;
+        } while (l != 0);
+
+        return length;
+    }
+
+    private static void checkForSufficientLength(byte[] b, int offset, int requiredLength) {
+        if (b.length < offset + requiredLength) {
+            throw new RuntimeException(
+                "Expected length of at least " + requiredLength + " bytes, but had " + (b.length - offset));
+        }
+    }
+
+    private static Date parseDatetime(String dateStr) {
+        Date date = null;
+        int len = dateStr.length();
+        if (len == 10 && dateStr.charAt(4) == '-' && dateStr.charAt(7) == '-') {
+            date = new DateTime(dateStr).toDate();
+        } else if (len == 8 && dateStr.charAt(2) == ':' && dateStr.charAt(5) == ':') {
+            date = new DateTime("T" + dateStr).toDate();
+        } else if (len >= 19 && dateStr.charAt(4) == '-' && dateStr.charAt(7) == '-' && dateStr.charAt(13) == ':'
+                   && dateStr.charAt(16) == ':') {
+            date = new DateTime(dateStr.replace(" ", "T")).toDate();
+        }
+        return date;
+    }
+}

+ 104 - 0
client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/support/Type.java

@@ -0,0 +1,104 @@
+package com.alibaba.otter.canal.client.adapter.hbase.support;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Date;
+
+/**
+ * Java类型
+ *
+ * @author machengyuan 2018-8-21 下午06:11:36
+ * @version 1.0.0
+ */
+public enum Type {
+    DEFAULT("STRING"),
+    STRING("STRING"),
+    INTEGER("INTEGER"),
+    LONG("LONG"),
+    SHORT("SHORT"),
+    BOOLEAN("BOOLEAN"),
+    FLOAT("FLOAT"),
+    DOUBLE("DOUBLE"),
+    BIGDECIMAL("BIGDECIMAL"),
+    DATE("DATE"),
+    BYTE("BYTE"),
+    BYTES("BYTES");
+
+    private String type;
+
+    Type(String type) {
+        this.type = type;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public static Type getType(String type) {
+        if (type == null) {
+            return DEFAULT;
+        }
+        Type res;
+        if (type.equalsIgnoreCase("STRING")) {
+            res = STRING;
+        } else if (type.equalsIgnoreCase("INTEGER")) {
+            res = INTEGER;
+        } else if (type.equalsIgnoreCase("LONG")) {
+            res = LONG;
+        } else if (type.equalsIgnoreCase("SHORT")) {
+            res = SHORT;
+        } else if (type.equalsIgnoreCase("BOOLEAN")) {
+            res = BOOLEAN;
+        } else if (type.equalsIgnoreCase("FLOAT")) {
+            res = FLOAT;
+        } else if (type.equalsIgnoreCase("DOUBLE")) {
+            res = DOUBLE;
+        } else if (type.equalsIgnoreCase("BIGDECIMAL")) {
+            res = BIGDECIMAL;
+        } else if (type.equalsIgnoreCase("DATE")) {
+            res = DATE;
+        } else if (type.equalsIgnoreCase("BYTE")) {
+            res = BYTE;
+        } else if (type.equalsIgnoreCase("BYTES")) {
+            res = BYTES;
+        } else {
+            res = DEFAULT;
+        }
+        return res;
+    }
+
+    public static Type getType(Class<?> javaType) {
+        if (javaType == null) {
+            return DEFAULT;
+        }
+        Type type;
+        if (Integer.class.isAssignableFrom(javaType) || int.class.isAssignableFrom(javaType)) {
+            type = INTEGER;
+        } else if (Long.class.isAssignableFrom(javaType) || long.class.isAssignableFrom(javaType)) {
+            type = LONG;
+        } else if (Byte.class.isAssignableFrom(javaType) || byte.class.isAssignableFrom(javaType)) {
+            type = BYTE;
+        } else if (Short.class.isAssignableFrom(javaType) || short.class.isAssignableFrom(javaType)) {
+            type = SHORT;
+        } else if (Float.class.isAssignableFrom(javaType) || float.class.isAssignableFrom(javaType)) {
+            type = FLOAT;
+        } else if (Double.class.isAssignableFrom(javaType) || double.class.isAssignableFrom(javaType)) {
+            type = DOUBLE;
+        } else if (Boolean.class.isAssignableFrom(javaType) || boolean.class.isAssignableFrom(javaType)) {
+            type = BOOLEAN;
+        } else if (Date.class.isAssignableFrom(javaType)) {
+            type = DATE;
+        } else if (byte[].class.isAssignableFrom(javaType)) {
+            type = BYTES;
+        } else if (String.class.isAssignableFrom(javaType)) {
+            type = STRING;
+        } else if (BigDecimal.class.isAssignableFrom(javaType)) {
+            type = BIGDECIMAL;
+        } else if (BigInteger.class.isAssignableFrom(javaType)) {
+            type = LONG;
+        } else {
+            type = DEFAULT;
+        }
+        return type;
+    }
+}

+ 188 - 0
client-adapter/hbase/src/main/java/com/alibaba/otter/canal/client/adapter/hbase/support/TypeUtil.java

@@ -0,0 +1,188 @@
+package com.alibaba.otter.canal.client.adapter.hbase.support;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Date;
+
+import org.apache.hadoop.hbase.util.Bytes;
+
+/**
+ * Java类型转换工具类
+ *
+ * @author machengyuan 2018-8-21 下午06:12:34
+ * @version 1.0.0
+ */
+public class TypeUtil {
+
+    public static byte[] toBytes(Object obj) {
+        if (obj == null) {
+            return null;
+        }
+        byte[] bytes;
+        if (obj instanceof String) {
+            bytes = Bytes.toBytes((String) obj);
+        } else if (obj instanceof Integer) {
+            bytes = Bytes.toBytes((Integer) obj);
+        } else if (obj instanceof Long) {
+            bytes = Bytes.toBytes((Long) obj);
+        } else if (obj instanceof Short) {
+            bytes = Bytes.toBytes((Short) obj);
+        } else if (obj instanceof Boolean) {
+            bytes = Bytes.toBytes((Boolean) obj);
+        } else if (obj instanceof Float) {
+            bytes = Bytes.toBytes((Float) obj);
+        } else if (obj instanceof Double) {
+            bytes = Bytes.toBytes((Double) obj);
+        } else if (obj instanceof BigDecimal) {
+            bytes = Bytes.toBytes((BigDecimal) obj);
+        } else if (obj instanceof BigInteger) {
+            bytes = Bytes.toBytes(((BigInteger) obj).longValue());
+        } else if (obj instanceof Date) {
+            bytes = Bytes.toBytes(((Date) obj).getTime());
+        } else if (obj instanceof Byte) {
+            bytes = new byte[] { (byte) obj };
+        } else if (obj instanceof byte[]) {
+            bytes = (byte[]) obj;
+        } else {
+            // 其余类型统一转换为string
+            bytes = Bytes.toBytes(obj.toString());
+        }
+        return bytes;
+    }
+
+    public static byte[] toBytes(Object v, Type type) {
+        if (v == null) {
+            return null;
+        }
+        byte[] b = null;
+        if (type == Type.DEFAULT) {
+            Type type1 = Type.getType(v.getClass());
+            if (type1 != null && type1 != Type.DEFAULT) {
+                b = toBytes(v, type1);
+            }
+        } else if (type == Type.STRING) {
+            b = Bytes.toBytes(v.toString());
+        } else if (type == Type.INTEGER) {
+            b = Bytes.toBytes(((Number) v).intValue());
+        } else if (type == Type.LONG) {
+            b = Bytes.toBytes(((Number) v).longValue());
+        } else if (type == Type.SHORT) {
+            b = Bytes.toBytes(((Number) v).shortValue());
+        } else if (type == Type.BYTE) {
+            b = Bytes.toBytes(((Number) v).byteValue());
+        } else if (type == Type.FLOAT) {
+            b = Bytes.toBytes(((Number) v).floatValue());
+        } else if (type == Type.DOUBLE) {
+            b = Bytes.toBytes(((Number) v).doubleValue());
+        } else if (type == Type.BOOLEAN) {
+            b = Bytes.toBytes(((Boolean) v));
+        } else if (type == Type.DATE) {
+            b = Bytes.toBytes(((Date) v).getTime());
+        } else if (type == Type.BYTES) {
+            b = (byte[]) v;
+        } else if (type == Type.BIGDECIMAL) {
+            if (v instanceof BigDecimal) {
+                b = Bytes.toBytes((BigDecimal) v);
+            } else {
+                b = Bytes.toBytes(new BigDecimal(v.toString()));
+            }
+        }
+        return b;
+    }
+
+    public static <T> T toObject(byte[] bytes, Class<T> clazz) {
+        if (bytes == null) {
+            return null;
+        }
+        Object res;
+        if (String.class.isAssignableFrom(clazz)) {
+            res = Bytes.toString(bytes);
+        } else if (Integer.class.isAssignableFrom(clazz)) {
+            res = Bytes.toInt(bytes);
+        } else if (Long.class.isAssignableFrom(clazz)) {
+            res = Bytes.toLong(bytes);
+        } else if (Short.class.isAssignableFrom(clazz)) {
+            res = Bytes.toShort(bytes);
+        } else if (Boolean.class.isAssignableFrom(clazz)) {
+            res = Bytes.toBoolean(bytes);
+        } else if (Float.class.isAssignableFrom(clazz)) {
+            res = Bytes.toFloat(bytes);
+        } else if (Double.class.isAssignableFrom(clazz)) {
+            res = Bytes.toDouble(bytes);
+        } else if (BigDecimal.class.isAssignableFrom(clazz)) {
+            res = Bytes.toBigDecimal(bytes);
+        } else if (BigInteger.class.isAssignableFrom(clazz)) {
+            res = Bytes.toLong(bytes);
+        } else if (java.sql.Date.class.isAssignableFrom(clazz)) {
+            long ts = Bytes.toLong(bytes);
+            res = new java.sql.Date(ts);
+        } else if (Time.class.isAssignableFrom(clazz)) {
+            long ts = Bytes.toLong(bytes);
+            res = new Time(ts);
+        } else if (Timestamp.class.isAssignableFrom(clazz)) {
+            long ts = Bytes.toLong(bytes);
+            res = new Timestamp(ts);
+        } else if (Date.class.isAssignableFrom(clazz)) {
+            long ts = Bytes.toLong(bytes);
+            res = new Date(ts);
+        } else if (Byte.class.isAssignableFrom(clazz)) {
+            res = bytes[0];
+        } else {
+            throw new IllegalArgumentException("mismatch class type");
+        }
+        // noinspection unchecked
+        return (T) res;
+    }
+
+    public static <T> T toObject(byte[] bytes, Type type) {
+        if (bytes == null) {
+            return null;
+        }
+        Object res = null;
+        if (type == Type.STRING || type == Type.DEFAULT) {
+            res = Bytes.toString(bytes);
+        } else if (type == Type.INTEGER) {
+            if (bytes.length == Bytes.SIZEOF_INT) {
+                res = Bytes.toInt(bytes);
+            }
+        } else if (type == Type.LONG) {
+            if (bytes.length == Bytes.SIZEOF_LONG) {
+                res = Bytes.toLong(bytes);
+            }
+        } else if (type == Type.SHORT) {
+            if (bytes.length == Bytes.SIZEOF_SHORT) {
+                res = Bytes.toShort(bytes);
+            }
+        } else if (type == Type.BYTE) {
+            if (bytes.length == Bytes.SIZEOF_BYTE) {
+                res = bytes[0];
+            }
+        } else if (type == Type.FLOAT) {
+            if (bytes.length == Bytes.SIZEOF_FLOAT) {
+                res = Bytes.toFloat(bytes);
+            }
+        } else if (type == Type.DOUBLE) {
+            if (bytes.length == Bytes.SIZEOF_DOUBLE) {
+                res = Bytes.toDouble(bytes);
+            }
+        } else if (type == Type.BOOLEAN) {
+            if (bytes.length == Bytes.SIZEOF_BOOLEAN) {
+                res = Bytes.toBoolean(bytes);
+            }
+        } else if (type == Type.DATE) {
+            if (bytes.length == Bytes.SIZEOF_LONG) {
+                res = new Date(Bytes.toLong(bytes));
+            }
+        } else if (type == Type.BYTES) {
+            res = bytes;
+        } else if (type == Type.BIGDECIMAL) {
+            res = Bytes.toBigDecimal(bytes);
+        } else {
+            throw new IllegalArgumentException("mismatch class type");
+        }
+        // noinspection unchecked
+        return (T) res;
+    }
+}

+ 1 - 0
client-adapter/hbase/src/main/resources/META-INF/canal/com.alibaba.otter.canal.client.adapter.CanalOuterAdapter

@@ -0,0 +1 @@
+hbase=com.alibaba.otter.canal.client.adapter.hbase.HbaseAdapter

+ 5 - 0
client-adapter/hbase/src/main/resources/hbase-mapping/configs.conf

@@ -0,0 +1,5 @@
+# 详细映射配置
+mytest_person2.yml
+
+# 简易配置, 只用指定数据库名.表名, 详细配置全部使用默认
+mytest.person

+ 56 - 0
client-adapter/hbase/src/main/resources/hbase-mapping/mytest_person2.yml

@@ -0,0 +1,56 @@
+hbaseOrm:
+  mode: PHOENIX  #NATIVE   #STRING
+  database: mytest  # 数据库名
+  table: person2     # 数据库表名
+  hbaseTable: MYTEST.PERSON2   # HBase表名
+  family: CF  # 默认统一Family名称
+  uppercaseQualifier: true  # 字段名转大写, 默认为true
+  commitBatch: 3000 # 批量提交的大小
+  #rowKey: id,type  # 复合字段rowKey不能和columns中的rowKey重复
+  columns:
+    # 数据库字段:HBase对应字段
+    id: ROWKEY$UNSIGNED_LONG
+    name: NAME
+    email: EMAIL
+    type: $DECIMAL
+    c_time: C_TIME$UNSIGNED_TIMESTAMP
+    birthday: BIRTHDAY$DATE
+
+# -- NATIVE类型
+# $DEFAULT
+# $STRING
+# $INTEGER
+# $LONG
+# $SHORT
+# $BOOLEAN
+# $FLOAT
+# $DOUBLE
+# $BIGDECIMAL
+# $DATE
+# $BYTE
+# $BYTES
+
+# -- PHOENIX类型
+# $DEFAULT                  对应PHOENIX里的VARCHAR
+# $UNSIGNED_INT             对应PHOENIX里的UNSIGNED_INT           4字节
+# $UNSIGNED_LONG            对应PHOENIX里的UNSIGNED_LONG          8字节
+# $UNSIGNED_TINYINT         对应PHOENIX里的UNSIGNED_TINYINT       1字节
+# $UNSIGNED_SMALLINT        对应PHOENIX里的UNSIGNED_SMALLINT      2字节
+# $UNSIGNED_FLOAT           对应PHOENIX里的UNSIGNED_FLOAT         4字节
+# $UNSIGNED_DOUBLE          对应PHOENIX里的UNSIGNED_DOUBLE        8字节
+# $INTEGER                  对应PHOENIX里的INTEGER                4字节
+# $BIGINT                   对应PHOENIX里的BIGINT                 8字节
+# $TINYINT                  对应PHOENIX里的TINYINT                1字节
+# $SMALLINT                 对应PHOENIX里的SMALLINT               2字节
+# $FLOAT                    对应PHOENIX里的FLOAT                  4字节
+# DOUBLE                    对应PHOENIX里的DOUBLE                 8字节
+# $BOOLEAN                  对应PHOENIX里的BOOLEAN                1字节
+# $TIME                     对应PHOENIX里的TIME                   8字节
+# $DATE                     对应PHOENIX里的DATE                   8字节
+# $TIMESTAMP                对应PHOENIX里的TIMESTAMP              12字节
+# $UNSIGNED_TIME            对应PHOENIX里的UNSIGNED_TIME          8字节
+# $UNSIGNED_DATE            对应PHOENIX里的UNSIGNED_DATE          8字节
+# $UNSIGNED_TIMESTAMP       对应PHOENIX里的UNSIGNED_TIMESTAMP     12字节
+# $VARCHAR                  对应PHOENIX里的VARCHAR                动态长度
+# $VARBINARY                对应PHOENIX里的VARBINARY              动态长度
+# $DECIMAL                  对应PHOENIX里的DECIMAL                动态长度

+ 47 - 0
client-adapter/logger/pom.xml

@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>canal.client-adapter</artifactId>
+        <groupId>com.alibaba.otter</groupId>
+        <version>1.1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>com.alibaba.otter</groupId>
+    <artifactId>client-adapter.logger</artifactId>
+    <packaging>jar</packaging>
+    <name>canal client adapter logger example module for otter ${project.version}</name>
+    <dependencies>
+        <dependency>
+            <groupId>com.alibaba.otter</groupId>
+            <artifactId>client-adapter.common</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <version>2.4</version>
+                <configuration>
+                    <descriptorRefs>
+                        <descriptorRef>jar-with-dependencies</descriptorRef>
+                    </descriptorRefs>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>make-assembly</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 45 - 0
client-adapter/logger/src/main/java/com/alibaba/otter/canal/client/adapter/logger/LoggerAdapterExample.java

@@ -0,0 +1,45 @@
+package com.alibaba.otter.canal.client.adapter.logger;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.alibaba.otter.canal.client.adapter.CanalOuterAdapter;
+import com.alibaba.otter.canal.client.adapter.support.CanalOuterAdapterConfiguration;
+import com.alibaba.otter.canal.client.adapter.support.Dml;
+import com.alibaba.otter.canal.client.adapter.support.MessageUtil;
+import com.alibaba.otter.canal.client.adapter.support.SPI;
+import com.alibaba.otter.canal.protocol.Message;
+
+/**
+ * 外部适配器示例
+ *
+ * @author machengyuan 2018-8-19 下午11:45:38
+ * @version 1.0.0
+ */
+@SPI("logger") // logger参数对应CanalOuterAdapterConfiguration配置中的name
+public class LoggerAdapterExample implements CanalOuterAdapter {
+
+    private Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    @Override
+    public void writeOut(Message message) {
+        // 直接输出日志信息
+        MessageUtil.parse4Dml(message, new MessageUtil.Consumer<Dml>() {
+
+            @Override
+            public void accept(Dml dml) {
+                logger.info(dml.toString());
+            }
+        });
+    }
+
+    @Override
+    public void init(CanalOuterAdapterConfiguration configuration) {
+
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+}

+ 1 - 0
client-adapter/logger/src/main/resources/META-INF/canal/com.alibaba.otter.canal.client.adapter.CanalOuterAdapter

@@ -0,0 +1 @@
+logger=com.alibaba.otter.canal.client.adapter.logger.LoggerAdapterExample

+ 21 - 0
client-adapter/pom.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>canal</artifactId>
+        <groupId>com.alibaba.otter</groupId>
+        <version>1.1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>com.alibaba.otter</groupId>
+    <artifactId>canal.client-adapter</artifactId>
+    <packaging>pom</packaging>
+    <name>canal client adapter module for otter ${project.version}</name>
+    <modules>
+        <module>common</module>
+        <module>logger</module>
+        <module>hbase</module>
+    </modules>
+
+</project>

+ 150 - 0
client-launcher/pom.xml

@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>canal</artifactId>
+        <groupId>com.alibaba.otter</groupId>
+        <version>1.1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>com.alibaba.otter</groupId>
+    <artifactId>canal.client-launcher</artifactId>
+    <packaging>jar</packaging>
+    <name>canal client launcher module for otter ${project.version}</name>
+    <dependencies>
+        <dependency>
+            <groupId>com.alibaba.otter</groupId>
+            <artifactId>client-adapter.common</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba.otter</groupId>
+            <artifactId>canal.client</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba.otter</groupId>
+            <artifactId>canal.kafka.client</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.yaml</groupId>
+            <artifactId>snakeyaml</artifactId>
+            <version>1.17</version>
+        </dependency>
+
+        <!-- outer adapter -->
+        <dependency>
+            <groupId>com.alibaba.otter</groupId>
+            <artifactId>client-adapter.logger</artifactId>
+            <version>${project.version}</version>
+            <classifier>jar-with-dependencies</classifier>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba.otter</groupId>
+            <artifactId>client-adapter.hbase</artifactId>
+            <version>${project.version}</version>
+            <classifier>jar-with-dependencies</classifier>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <!-- deploy模块的packaging通常是jar,如果项目中没有java 源代码或资源文件,加上这一段配置使项目能通过构建 -->
+            <plugin>
+                <artifactId>maven-jar-plugin</artifactId>
+                <configuration>
+                    <archive>
+                        <addMavenDescriptor>true</addMavenDescriptor>
+                    </archive>
+                    <excludes>
+                        <exclude>**/logback.xml</exclude>
+                        <exclude>**/canal-client.yml</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <!-- 这是最新版本,推荐使用这个版本 -->
+                <version>2.2.1</version>
+                <executions>
+                    <execution>
+                        <id>assemble</id>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                        <phase>package</phase>
+                    </execution>
+                </executions>
+                <configuration>
+                    <appendAssemblyId>false</appendAssemblyId>
+                    <attach>false</attach>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <profiles>
+        <profile>
+            <id>dev</id>
+            <activation>
+                <activeByDefault>true</activeByDefault>
+                <property>
+                    <name>env</name>
+                    <value>!release</value>
+                </property>
+            </activation>
+
+            <build>
+                <plugins>
+                    <plugin>
+                        <artifactId>maven-assembly-plugin</artifactId>
+                        <configuration>
+                            <!-- maven assembly插件需要一个描述文件 来告诉插件包的结构以及打包所需的文件来自哪里 -->
+                            <descriptors>
+                                <descriptor>${basedir}/src/main/assembly/dev.xml</descriptor>
+                            </descriptors>
+                            <finalName>canal_client</finalName>
+                            <outputDirectory>${project.build.directory}</outputDirectory>
+                        </configuration>
+                    </plugin>
+                </plugins>
+            </build>
+
+        </profile>
+
+        <profile>
+            <id>release</id>
+            <activation>
+                <property>
+                    <name>env</name>
+                    <value>release</value>
+                </property>
+            </activation>
+
+            <build>
+                <plugins>
+                    <plugin>
+                        <artifactId>maven-assembly-plugin</artifactId>
+                        <configuration>
+                            <!-- 发布模式使用的maven assembly插件描述文件 -->
+                            <descriptors>
+                                <descriptor>${basedir}/src/main/assembly/release.xml</descriptor>
+                            </descriptors>
+                            <!-- 如果一个应用的包含多个deploy模块,如果使用同样的包名, 如果把它们复制的一个目录中可能会失败,所以包名加了 artifactId以示区分 -->
+                            <finalName>${project.artifactId}-${project.version}</finalName>
+                            <!-- scm 要求 release 模式打出的包放到顶级目录下的target子目录中 -->
+                            <outputDirectory>${project.parent.build.directory}</outputDirectory>
+                        </configuration>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
+</project>

+ 57 - 0
client-launcher/src/main/assembly/dev.xml

@@ -0,0 +1,57 @@
+<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
+	<id>dist</id>
+	<formats>
+		<format>dir</format>
+	</formats>
+	<includeBaseDirectory>false</includeBaseDirectory>
+	<fileSets>
+		<fileSet>
+			<directory>.</directory>
+			<outputDirectory>/</outputDirectory>
+			<includes>
+				<include>README*</include>
+			</includes>
+		</fileSet>
+		<fileSet>
+			<directory>./src/main/bin</directory>
+			<outputDirectory>bin</outputDirectory>
+			<includes>
+				<include>**/*</include>
+			</includes>
+			<fileMode>0755</fileMode>
+		</fileSet>
+		<fileSet>
+			<directory>./src/main/conf</directory>
+			<outputDirectory>/conf</outputDirectory>
+			<includes>
+				<include>**/*</include>
+			</includes>
+		</fileSet>
+		<fileSet>
+			<directory>./src/main/resources</directory>
+			<outputDirectory>/conf</outputDirectory>
+			<includes>
+				<include>**/*</include>
+			</includes>
+			<excludes>
+				<exclude>META-INF/**</exclude>
+			</excludes>
+		</fileSet>
+		<fileSet>
+			<directory>target</directory>
+			<outputDirectory>logs</outputDirectory>
+			<excludes>
+				<exclude>**/*</exclude>
+			</excludes>
+		</fileSet>
+	</fileSets>
+	<dependencySets>
+		<dependencySet>
+			<outputDirectory>lib</outputDirectory>
+			<excludes>
+				<exclude>junit:junit</exclude>
+			</excludes>
+		</dependencySet>
+	</dependencySets>
+</assembly>

+ 57 - 0
client-launcher/src/main/assembly/release.xml

@@ -0,0 +1,57 @@
+<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
+	<id>dist</id>
+	<formats>
+		<format>tar.gz</format>
+	</formats>
+	<includeBaseDirectory>false</includeBaseDirectory>
+	<fileSets>
+		<fileSet>
+			<directory>.</directory>
+			<outputDirectory>/</outputDirectory>
+			<includes>
+				<include>README*</include>
+			</includes>
+		</fileSet>
+		<fileSet>
+			<directory>./src/main/bin</directory>
+			<outputDirectory>bin</outputDirectory>
+			<includes>
+				<include>**/*</include>
+			</includes>
+			<fileMode>0755</fileMode>
+		</fileSet>
+		<fileSet>
+			<directory>./src/main/conf</directory>
+			<outputDirectory>/conf</outputDirectory>
+			<includes>
+				<include>**/*</include>
+			</includes>
+		</fileSet>
+		<fileSet>
+			<directory>./src/main/resources</directory>
+			<outputDirectory>/conf</outputDirectory>
+			<includes>
+				<include>**/*</include>
+			</includes>
+			<excludes>
+				<exclude>META-INF/**</exclude>
+			</excludes>
+		</fileSet>
+		<fileSet>
+			<directory>target</directory>
+			<outputDirectory>logs</outputDirectory>
+			<excludes>
+				<exclude>**/*</exclude>
+			</excludes>
+		</fileSet>
+	</fileSets>
+	<dependencySets>
+		<dependencySet>
+			<outputDirectory>lib</outputDirectory>
+			<excludes>
+				<exclude>junit:junit</exclude>
+			</excludes>
+		</dependencySet>
+	</dependencySets>
+</assembly>

+ 25 - 0
client-launcher/src/main/bin/startup.bat

@@ -0,0 +1,25 @@
+@echo off
+@if not "%ECHO%" == ""  echo %ECHO%
+@if "%OS%" == "Windows_NT"  setlocal
+
+set ENV_PATH=.\
+if "%OS%" == "Windows_NT" set ENV_PATH=%~dp0%
+
+set conf_dir=%ENV_PATH%\..\conf
+set canal_conf=%conf_dir%\canal-client.yml
+set logback_configurationFile=%conf_dir%\logback.xml
+
+set CLASSPATH=%conf_dir%
+set CLASSPATH=%conf_dir%\..\lib\*;%CLASSPATH%
+
+set JAVA_MEM_OPTS= -Xms128m -Xmx512m -XX:PermSize=128m
+set JAVA_OPTS_EXT= -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -Dapplication.codeset=UTF-8 -Dfile.encoding=UTF-8
+set JAVA_DEBUG_OPT= -server -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=9099,server=y,suspend=n
+set CANAL_OPTS= -DappName=otter-canal -Dlogback.configurationFile="%logback_configurationFile%" -Dcanal.conf="%canal_conf%"
+
+set JAVA_OPTS= %JAVA_MEM_OPTS% %JAVA_OPTS_EXT% %JAVA_DEBUG_OPT% %CANAL_OPTS%
+
+set CMD_STR= java %JAVA_OPTS% -classpath "%CLASSPATH%" java %JAVA_OPTS% -classpath "%CLASSPATH%" com.alibaba.otter.canal.client.ClientLauncher
+echo start cmd : %CMD_STR%
+
+java %JAVA_OPTS% -classpath "%CLASSPATH%" com.alibaba.otter.canal.client.ClientLauncher

+ 100 - 0
client-launcher/src/main/bin/startup.sh

@@ -0,0 +1,100 @@
+#!/bin/bash 
+
+current_path=`pwd`
+case "`uname`" in
+    Linux)
+		bin_abs_path=$(readlink -f $(dirname $0))
+		;;
+	*)
+		bin_abs_path=`cd $(dirname $0); pwd`
+		;;
+esac
+base=${bin_abs_path}/..
+canal_conf=$base/conf/canal-client.yml
+logback_configurationFile=$base/conf/logback.xml
+export LANG=en_US.UTF-8
+export BASE=$base
+
+if [ -f $base/bin/canal_client.pid ] ; then
+	echo "found canal_client.pid , Please run stop.sh first ,then startup.sh" 2>&2
+    exit 1
+fi
+
+## set java path
+if [ -z "$JAVA" ] ; then
+  JAVA=$(which java)
+fi
+
+ALIBABA_JAVA="/usr/alibaba/java/bin/java"
+TAOBAO_JAVA="/opt/taobao/java/bin/java"
+if [ -z "$JAVA" ]; then
+  if [ -f $ALIBABA_JAVA ] ; then
+  	JAVA=$ALIBABA_JAVA
+  elif [ -f $TAOBAO_JAVA ] ; then
+  	JAVA=$TAOBAO_JAVA
+  else
+  	echo "Cannot find a Java JDK. Please set either set JAVA or put java (>=1.5) in your PATH." 2>&2
+    exit 1
+  fi
+fi
+
+case "$#" 
+in
+0 ) 
+	;;
+1 )	
+	var=$*
+	if [ -f $var ] ; then 
+		canal_conf=$var
+	else
+		echo "THE PARAMETER IS NOT CORRECT.PLEASE CHECK AGAIN."
+        exit
+	fi;;
+2 )	
+	var=$1
+	if [ -f $var ] ; then
+		canal_conf=$var
+	else 
+		if [ "$1" = "debug" ]; then
+			DEBUG_PORT=$2
+			DEBUG_SUSPEND="n"
+			JAVA_DEBUG_OPT="-Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=$DEBUG_PORT,server=y,suspend=$DEBUG_SUSPEND"
+		fi
+     fi;;
+* )
+	echo "THE PARAMETERS MUST BE TWO OR LESS.PLEASE CHECK AGAIN."
+	exit;;
+esac
+
+str=`file -L $JAVA | grep 64-bit`
+if [ -n "$str" ]; then
+	JAVA_OPTS="-server -Xms2048m -Xmx3072m -Xmn1024m -XX:SurvivorRatio=2 -XX:PermSize=96m -XX:MaxPermSize=256m -Xss256k -XX:-UseAdaptiveSizePolicy -XX:MaxTenuringThreshold=15 -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:+HeapDumpOnOutOfMemoryError"
+else
+	JAVA_OPTS="-server -Xms1024m -Xmx1024m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:MaxPermSize=128m "
+fi
+
+JAVA_OPTS=" $JAVA_OPTS -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -Dfile.encoding=UTF-8"
+CANAL_OPTS="-DappName=otter-canal -Dlogback.configurationFile=$logback_configurationFile -Dcanal.conf=$canal_conf"
+
+if [ -e $canal_conf -a -e $logback_configurationFile ]
+then 
+	
+	for i in $base/lib/*;
+		do CLASSPATH=$i:"$CLASSPATH";
+	done
+ 	CLASSPATH="$base/conf:$CLASSPATH";
+ 	
+ 	echo "cd to $bin_abs_path for workaround relative path"
+  	cd $bin_abs_path
+ 	
+	echo LOG CONFIGURATION : $logback_configurationFile
+	echo canal conf : $canal_conf 
+	echo CLASSPATH :$CLASSPATH
+	$JAVA $JAVA_OPTS $JAVA_DEBUG_OPT $CANAL_OPTS -classpath .:$CLASSPATH com.alibaba.otter.canal.client.ClientLauncher 1>>$base/logs/canal_client.log 2>&1 &
+	echo $! > $base/bin/canal_client.pid
+	
+	echo "cd to $current_path for continue"
+  	cd $current_path
+else 
+	echo "canal client conf("$canal_conf") OR log configration file($logback_configurationFile) is not exist,please create then first!"
+fi

+ 65 - 0
client-launcher/src/main/bin/stop.sh

@@ -0,0 +1,65 @@
+#!/bin/bash
+
+cygwin=false;
+linux=false;
+case "`uname`" in
+    CYGWIN*)
+        cygwin=true
+        ;;
+    Linux*)
+    	linux=true
+    	;;
+esac
+
+get_pid() {	
+	STR=$1
+	PID=$2
+    if $cygwin; then
+        JAVA_CMD="$JAVA_HOME\bin\java"
+        JAVA_CMD=`cygpath --path --unix $JAVA_CMD`
+        JAVA_PID=`ps |grep $JAVA_CMD |awk '{print $1}'`
+    else
+    	if $linux; then
+	        if [ ! -z "$PID" ]; then
+	        	JAVA_PID=`ps -C java -f --width 1000|grep "$STR"|grep "$PID"|grep -v grep|awk '{print $2}'`
+		    else 
+		        JAVA_PID=`ps -C java -f --width 1000|grep "$STR"|grep -v grep|awk '{print $2}'`
+	        fi
+	    else
+	    	if [ ! -z "$PID" ]; then
+	        	JAVA_PID=`ps aux |grep "$STR"|grep "$PID"|grep -v grep|awk '{print $2}'`
+		    else 
+		        JAVA_PID=`ps aux |grep "$STR"|grep -v grep|awk '{print $2}'`
+	        fi
+	    fi
+    fi
+    echo $JAVA_PID;
+}
+
+base=`dirname $0`/..
+pidfile=$base/bin/canal_client.pid
+if [ ! -f "$pidfile" ];then
+	echo "canal client is not running. exists"
+	exit
+fi
+
+pid=`cat $pidfile`
+if [ "$pid" == "" ] ; then
+	pid=`get_pid "appName=otter-canal"`
+fi
+
+echo -e "`hostname`: stopping canal $pid ... "
+kill $pid
+
+LOOPS=0
+while (true); 
+do 
+	gpid=`get_pid "appName=otter-canal" "$pid"`
+    if [ "$gpid" == "" ] ; then
+    	echo "Oook! cost:$LOOPS"
+    	`rm $pidfile`
+    	break;
+    fi
+    let LOOPS=LOOPS+1
+    sleep 1
+done

+ 65 - 0
client-launcher/src/main/java/com/alibaba/otter/canal/client/ClientLauncher.java

@@ -0,0 +1,65 @@
+package com.alibaba.otter.canal.client;
+
+import com.alibaba.otter.canal.client.adapter.loader.CanalAdapterLoader;
+import com.alibaba.otter.canal.client.adapter.support.CanalClientConfig;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.yaml.snakeyaml.Yaml;
+
+import java.io.FileInputStream;
+
+public class ClientLauncher {
+
+    private static final String CLASSPATH_URL_PREFIX = "classpath:";
+    private static final Logger logger               = LoggerFactory.getLogger(ClientLauncher.class);
+
+    public static void main(String[] args) {
+        try {
+            logger.info("## set default uncaught exception handler");
+            setGlobalUncaughtExceptionHandler();
+
+            logger.info("## load canal client configurations");
+            String conf = System.getProperty("client.conf", "classpath:canal-client.yml");
+            CanalClientConfig canalClientConfig;
+            if (conf.startsWith(CLASSPATH_URL_PREFIX)) {
+                conf = StringUtils.substringAfter(conf, CLASSPATH_URL_PREFIX);
+                canalClientConfig = new Yaml().loadAs(ClientLauncher.class.getClassLoader().getResourceAsStream(conf),
+                    CanalClientConfig.class);
+            } else {
+                canalClientConfig = new Yaml().loadAs(new FileInputStream(conf), CanalClientConfig.class);
+            }
+            logger.info("## start the canal client adapters.");
+            final CanalAdapterLoader adapterLoader = new CanalAdapterLoader(canalClientConfig);
+            adapterLoader.init();
+            logger.info("## the canal client adapters are running now ......");
+            Runtime.getRuntime().addShutdownHook(new Thread() {
+
+                public void run() {
+                    try {
+                        logger.info("## stop the canal client adapters");
+                        adapterLoader.destroy();
+                    } catch (Throwable e) {
+                        logger.warn("## something goes wrong when stopping canal client adapters:", e);
+                    } finally {
+                        logger.info("## canal client adapters are down.");
+                    }
+                }
+
+            });
+        } catch (Throwable e) {
+            logger.error("## something goes wrong when starting up the canal client adapters:", e);
+            System.exit(0);
+        }
+    }
+
+    private static void setGlobalUncaughtExceptionHandler() {
+        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
+
+            @Override
+            public void uncaughtException(Thread t, Throwable e) {
+                logger.error("UnCaughtException", e);
+            }
+        });
+    }
+}

+ 83 - 0
client-launcher/src/main/java/com/alibaba/otter/canal/client/adapter/loader/AbstractCanalAdapterWorker.java

@@ -0,0 +1,83 @@
+package com.alibaba.otter.canal.client.adapter.loader;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+import com.alibaba.otter.canal.client.adapter.CanalOuterAdapter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.alibaba.otter.canal.protocol.Message;
+
+/**
+ * 适配器工作线程抽象类
+ *
+ * @author machengyuan 2018-8-19 下午11:30:49
+ * @version 1.0.0
+ */
+public abstract class AbstractCanalAdapterWorker {
+
+    protected final Logger                    logger  = LoggerFactory.getLogger(this.getClass());
+
+    protected String                          canalDestination;                                                 // canal实例
+    protected List<List<CanalOuterAdapter>>   canalOuterAdapters;                                               // 外部适配器
+    protected ExecutorService                 groupInnerExecutorService;                                        // 组内工作线程池
+    protected volatile boolean                running = false;                                                  // 是否运行中
+    protected Thread                          thread  = null;
+    protected Thread.UncaughtExceptionHandler handler = new Thread.UncaughtExceptionHandler() {
+
+                                                          @Override
+                                                          public void uncaughtException(Thread t, Throwable e) {
+                                                              logger.error("parse events has an error", e);
+                                                          }
+                                                      };
+
+    protected void writeOut(final Message message) {
+        List<Future<Boolean>> futures = new ArrayList<>();
+        // 组间适配器并行运行
+        for (List<CanalOuterAdapter> outerAdapters : canalOuterAdapters) {
+            final List<CanalOuterAdapter> adapters = outerAdapters;
+            futures.add(groupInnerExecutorService.submit(new Callable<Boolean>() {
+
+                @Override
+                public Boolean call() {
+                    try {
+                        // 组内适配器穿行运行,尽量不要配置组内适配器
+                        for (CanalOuterAdapter c : adapters) {
+                            long begin = System.currentTimeMillis();
+                            c.writeOut(message);
+                            if (logger.isDebugEnabled()) {
+                                logger.debug("{} elapsed time: {}",
+                                    c.getClass().getName(),
+                                    (System.currentTimeMillis() - begin));
+                            }
+                        }
+                        return true;
+                    } catch (Exception e) {
+                        return false;
+                    }
+                }
+            }));
+
+            // 等待所有适配器写入完成
+            // 由于是组间并发操作,所以将阻塞直到耗时最久的工作组操作完成
+            for (Future<Boolean> f : futures) {
+                try {
+                    if (!f.get()) {
+                        logger.error("Outer adapter write failed");
+                    }
+                } catch (InterruptedException | ExecutionException e) {
+                    logger.error(e.getMessage(), e);
+                }
+            }
+        }
+    }
+
+    public abstract void start();
+
+    public abstract void stop();
+}

+ 168 - 0
client-launcher/src/main/java/com/alibaba/otter/canal/client/adapter/loader/CanalAdapterKafkaWorker.java

@@ -0,0 +1,168 @@
+package com.alibaba.otter.canal.client.adapter.loader;
+
+import com.alibaba.otter.canal.client.adapter.CanalOuterAdapter;
+import com.alibaba.otter.canal.client.adapter.loader.AbstractCanalAdapterWorker;
+import com.alibaba.otter.canal.kafka.client.KafkaCanalConnector;
+import com.alibaba.otter.canal.kafka.client.KafkaCanalConnectors;
+import com.alibaba.otter.canal.protocol.Message;
+import org.apache.kafka.clients.consumer.CommitFailedException;
+import org.apache.kafka.common.errors.WakeupException;
+
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class CanalAdapterKafkaWorker extends AbstractCanalAdapterWorker {
+
+    private KafkaCanalConnector connector;
+
+    private String              topic;
+
+    public CanalAdapterKafkaWorker(String zkServers, String bootstrapServers, String topic, String groupId,
+                                   List<List<CanalOuterAdapter>> canalOuterAdapters){
+        this.canalOuterAdapters = canalOuterAdapters;
+        this.groupInnerExecutorService = Executors.newFixedThreadPool(canalOuterAdapters.size());
+        this.topic = topic;
+        this.canalDestination = topic;
+        connector = KafkaCanalConnectors.newKafkaConnector(zkServers, bootstrapServers, topic, null, groupId);
+        // connector.setSessionTimeout(5L, TimeUnit.MINUTES);
+
+        // super.initSwitcher(topic);
+    }
+
+    @Override
+    public void start() {
+        if (!running) {
+            thread = new Thread(new Runnable() {
+
+                @Override
+                public void run() {
+                    process();
+                }
+            });
+            thread.setUncaughtExceptionHandler(handler);
+            running = true;
+            thread.start();
+        }
+    }
+
+    @Override
+    public void stop() {
+        try {
+            if (!running) {
+                return;
+            }
+
+            connector.stopRunning();
+            running = false;
+
+            // if (switcher != null && !switcher.state()) {
+            // switcher.set(true);
+            // }
+
+            if (thread != null) {
+                try {
+                    thread.join();
+                } catch (InterruptedException e) {
+                    // ignore
+                }
+            }
+            groupInnerExecutorService.shutdown();
+            logger.info("topic {} connectors' worker thread dead!", this.topic);
+            for (List<CanalOuterAdapter> outerAdapters : canalOuterAdapters) {
+                for (CanalOuterAdapter adapter : outerAdapters) {
+                    adapter.destroy();
+                }
+            }
+            logger.info("topic {} all connectors destroyed!", this.topic);
+        } catch (Exception e) {
+            logger.error(e.getMessage(), e);
+        }
+    }
+
+    private void process() {
+        while (!running)
+            ;
+        ExecutorService executor = Executors.newFixedThreadPool(1);
+        final AtomicBoolean executing = new AtomicBoolean(true);
+        while (running) {
+            try {
+                logger.info("=============> Start to connect topic: {} <=============", this.topic);
+                connector.connect();
+                logger.info("=============> Start to subscribe topic: {}<=============", this.topic);
+                connector.subscribe();
+                logger.info("=============> Subscribe topic: {} succeed<=============", this.topic);
+                while (running) {
+                    try {
+                        // switcher.get(); //等待开关开启
+
+                        final Message message = connector.getWithoutAck();
+
+                        executing.set(true);
+                        if (message != null) {
+                            executor.submit(new Runnable() {
+
+                                @Override
+                                public void run() {
+                                    try {
+                                        if (logger.isDebugEnabled()) {
+                                            logger.debug("topic: {} batchId: {} batchSize: {} ",
+                                                topic,
+                                                message.getId(),
+                                                message.getEntries().size());
+                                        }
+                                        long begin = System.currentTimeMillis();
+                                        writeOut(message);
+                                        long now = System.currentTimeMillis();
+                                        if ((System.currentTimeMillis() - begin) > 5 * 60 * 1000) {
+                                            logger.error("topic: {} batchId {} elapsed time: {} ms",
+                                                topic,
+                                                message.getId(),
+                                                now - begin);
+                                        }
+                                        if (logger.isDebugEnabled()) {
+                                            logger.debug("topic: {} batchId {} elapsed time: {} ms",
+                                                topic,
+                                                message.getId(),
+                                                now - begin);
+                                        }
+                                    } catch (Exception e) {
+                                        logger.error(e.getMessage(), e);
+                                    } finally {
+                                        executing.compareAndSet(true, false);
+                                    }
+                                }
+                            });
+
+                            while (executing.get()) { // keeping kafka client active
+                                connector.ack();
+                                Thread.sleep(500);
+                            }
+                        } else {
+                            connector.ack();
+                        }
+                    } catch (CommitFailedException e) {
+                        logger.warn(e.getMessage());
+                    } catch (Exception e) {
+                        logger.error(e.getMessage(), e);
+                        TimeUnit.SECONDS.sleep(1L);
+                    }
+                }
+            } catch (Exception e) {
+                logger.error(e.getMessage(), e);
+            }
+        }
+
+        executor.shutdown();
+
+        try {
+            connector.unsubscribe();
+        } catch (WakeupException e) {
+            // No-op. Continue process
+        }
+        connector.disconnnect();
+        logger.info("=============> Disconnect topic: {} <=============", this.topic);
+    }
+}

+ 164 - 0
client-launcher/src/main/java/com/alibaba/otter/canal/client/adapter/loader/CanalAdapterLoader.java

@@ -0,0 +1,164 @@
+package com.alibaba.otter.canal.client.adapter.loader;
+
+import com.alibaba.otter.canal.client.adapter.CanalOuterAdapter;
+import com.alibaba.otter.canal.client.adapter.support.CanalClientConfig;
+import com.alibaba.otter.canal.client.adapter.support.CanalOuterAdapterConfiguration;
+import com.alibaba.otter.canal.client.adapter.support.ExtensionLoader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * 外部适配器的加载器
+ *
+ * @author machengyuan 2018-8-19 下午11:45:49
+ * @version 1.0.0
+ */
+public class CanalAdapterLoader {
+
+    private static final Logger                  logger            = LoggerFactory.getLogger(CanalAdapterLoader.class);
+
+    private CanalClientConfig canalClientConfig;
+
+    private Map<String, CanalAdapterWorker>      canalWorkers      = new HashMap<>();
+
+    private Map<String, CanalAdapterKafkaWorker> canalKafkaWorkers = new HashMap<>();
+
+    private ExtensionLoader<CanalOuterAdapter> loader;
+
+    public CanalAdapterLoader(CanalClientConfig canalClientConfig){
+        this.canalClientConfig = canalClientConfig;
+    }
+
+    /**
+     * 初始化canal-client、 canal-client-kafka的适配器
+     */
+    public void init() {
+        // canal instances 和 kafka topics 配置不能同时为空
+        if (canalClientConfig.getCanalInstances().isEmpty() && canalClientConfig.getKafkaTopics().isEmpty()) {
+            throw new RuntimeException("Blank config property: canalInstances or canalKafkaTopics");
+        }
+
+        loader = ExtensionLoader.getExtensionLoader(CanalOuterAdapter.class,
+            "" /* TODO canalClientConfig.getClassloaderPolicy() */);
+
+        String canalServerHost = this.canalClientConfig.getCanalServerHost();
+        SocketAddress sa = null;
+        if (canalServerHost != null) {
+            String[] ipPort = canalServerHost.split(":");
+            sa = new InetSocketAddress(ipPort[0], Integer.parseInt(ipPort[1]));
+        }
+        String zkHosts = this.canalClientConfig.getZookeeperHosts();
+
+        if (zkHosts == null && sa == null) {
+            throw new RuntimeException("Blank config property: canalServerHost or zookeeperHosts");
+        }
+
+        // 初始化canal-client的适配器
+        for (CanalClientConfig.CanalInstance instance : canalClientConfig.getCanalInstances()) {
+            List<List<CanalOuterAdapter>> canalOuterAdapterGroups = new ArrayList<>();
+
+            for (CanalClientConfig.AdapterGroup connectorGroup : instance.getAdapterGroups()) {
+                List<CanalOuterAdapter> canalOutConnectors = new ArrayList<>();
+                for (CanalOuterAdapterConfiguration c : connectorGroup.getOutAdapters()) {
+                    loadConnector(c, canalOutConnectors);
+                }
+                canalOuterAdapterGroups.add(canalOutConnectors);
+            }
+            CanalAdapterWorker worker;
+            if (zkHosts != null) {
+                worker = new CanalAdapterWorker(instance.getInstance(), zkHosts, canalOuterAdapterGroups);
+            } else {
+                worker = new CanalAdapterWorker(instance.getInstance(), sa, canalOuterAdapterGroups);
+            }
+            canalWorkers.put(instance.getInstance(), worker);
+            worker.start();
+            logger.info("Start adapter for canal instance: {} succeed", instance.getInstance());
+        }
+
+        // 初始化canal-client-kafka的适配器
+        for (CanalClientConfig.KafkaTopic kafkaTopic : canalClientConfig.getKafkaTopics()) {
+            for (CanalClientConfig.Group group : kafkaTopic.getGroups()) {
+                List<List<CanalOuterAdapter>> canalOuterAdapterGroups = new ArrayList<>();
+
+                List<CanalOuterAdapter> canalOuterAdapters = new ArrayList<>();
+
+                for (CanalOuterAdapterConfiguration config : group.getOutAdapters()) {
+                    // for (CanalOuterAdapterConfiguration config : adaptor.getOutAdapters()) {
+                    loadConnector(config, canalOuterAdapters);
+                    // }
+                }
+                canalOuterAdapterGroups.add(canalOuterAdapters);
+
+                String zkServers = canalClientConfig.getZookeeperHosts();
+                CanalAdapterKafkaWorker canalKafkaWorker = new CanalAdapterKafkaWorker(zkServers,
+                    canalClientConfig.getBootstrapServers(),
+                    kafkaTopic.getTopic(),
+                    group.getGroupId(),
+                    canalOuterAdapterGroups);
+                canalKafkaWorkers.put(kafkaTopic.getTopic() + "-" + group.getGroupId(), canalKafkaWorker);
+                canalKafkaWorker.start();
+                logger.info("Start adapter for canal-client kafka topic: {} succeed",
+                    kafkaTopic.getTopic() + "-" + group.getGroupId());
+            }
+        }
+    }
+
+    private void loadConnector(CanalOuterAdapterConfiguration config, List<CanalOuterAdapter> canalOutConnectors) {
+        try {
+            CanalOuterAdapter adapter = loader.getExtension(config.getName());
+            ClassLoader cl = Thread.currentThread().getContextClassLoader();
+            // 替换ClassLoader
+            Thread.currentThread().setContextClassLoader(adapter.getClass().getClassLoader());
+            adapter.init(config);
+            Thread.currentThread().setContextClassLoader(cl);
+            canalOutConnectors.add(adapter);
+            logger.info("Load canal adapter: {} succeed", config.getName());
+        } catch (Exception e) {
+            logger.error("Load canal adapter: {} failed", config.getName(), e);
+        }
+    }
+
+    /**
+     * 销毁所有适配器 为防止canal实例太多造成销毁阻塞, 并行销毁
+     */
+    public void destroy() {
+        if (canalWorkers.size() > 0) {
+            ExecutorService stopExecutorService = Executors.newFixedThreadPool(canalWorkers.size());
+            for (CanalAdapterWorker v : canalWorkers.values()) {
+                final CanalAdapterWorker caw = v;
+                stopExecutorService.submit(new Runnable() {
+
+                    @Override
+                    public void run() {
+                        caw.stop();
+                    }
+                });
+            }
+            stopExecutorService.shutdown();
+        }
+        if (canalKafkaWorkers.size() > 0) {
+            ExecutorService stopKafkaExecutorService = Executors.newFixedThreadPool(canalKafkaWorkers.size());
+            for (CanalAdapterKafkaWorker v : canalKafkaWorkers.values()) {
+                final CanalAdapterKafkaWorker cakw = v;
+                stopKafkaExecutorService.submit(new Runnable() {
+
+                    @Override
+                    public void run() {
+                        cakw.stop();
+                    }
+                });
+            }
+            stopKafkaExecutorService.shutdown();
+        }
+        logger.info("All canal adapters destroyed");
+    }
+}

+ 187 - 0
client-launcher/src/main/java/com/alibaba/otter/canal/client/adapter/loader/CanalAdapterWorker.java

@@ -0,0 +1,187 @@
+package com.alibaba.otter.canal.client.adapter.loader;
+
+import com.alibaba.otter.canal.client.CanalConnector;
+import com.alibaba.otter.canal.client.CanalConnectors;
+import com.alibaba.otter.canal.client.adapter.CanalOuterAdapter;
+import com.alibaba.otter.canal.client.adapter.loader.AbstractCanalAdapterWorker;
+import com.alibaba.otter.canal.client.impl.ClusterCanalConnector;
+import com.alibaba.otter.canal.protocol.Message;
+
+import java.net.SocketAddress;
+import java.util.List;
+import java.util.concurrent.Executors;
+
+/**
+ * 原生canal-server对应的client适配器工作线程
+ *
+ * @author machengyuan 2018-8-19 下午11:30:49
+ * @version 1.0.0
+ */
+public class CanalAdapterWorker extends AbstractCanalAdapterWorker {
+
+    private static final int BATCH_SIZE = 50;
+    private static final int SO_TIMEOUT = 0;
+
+    private CanalConnector   connector;
+
+    /**
+     * 单台client适配器worker的构造方法
+     *
+     * @param canalDestination canal实例名
+     * @param address canal-server地址
+     * @param canalOuterAdapters 外部适配器组
+     */
+    public CanalAdapterWorker(String canalDestination, SocketAddress address,
+                              List<List<CanalOuterAdapter>> canalOuterAdapters){
+        this.canalOuterAdapters = canalOuterAdapters;
+        this.canalDestination = canalDestination;
+        groupInnerExecutorService = Executors.newFixedThreadPool(canalOuterAdapters.size());
+        connector = CanalConnectors.newSingleConnector(address, canalDestination, "", "");
+    }
+
+    /**
+     * HA模式下client适配器worker的构造方法
+     *
+     * @param canalDestination canal实例名
+     * @param zookeeperHosts zookeeper地址
+     * @param canalOuterAdapters 外部适配器组
+     */
+    public CanalAdapterWorker(String canalDestination, String zookeeperHosts,
+                              List<List<CanalOuterAdapter>> canalOuterAdapters){
+        this.canalOuterAdapters = canalOuterAdapters;
+        this.canalDestination = canalDestination;
+        groupInnerExecutorService = Executors.newFixedThreadPool(canalOuterAdapters.size());
+        connector = CanalConnectors.newClusterConnector(zookeeperHosts, canalDestination, "", "");
+        ((ClusterCanalConnector) connector).setSoTimeout(SO_TIMEOUT);
+
+        // super.initSwitcher(canalDestination);
+    }
+
+    @Override
+    public void start() {
+        if (!running) {
+            thread = new Thread(new Runnable() {
+
+                @Override
+                public void run() {
+                    process();
+                }
+            });
+            thread.setUncaughtExceptionHandler(handler);
+            thread.start();
+            running = true;
+        }
+    }
+
+    @Override
+    public void stop() {
+        try {
+            if (!running) {
+                return;
+            }
+
+            // if (switcher != null && !switcher.state()) {
+            // switcher.set(true);
+            // }
+
+            connector.stopRunning();
+            running = false;
+
+            logger.info("destination {} is waiting for adapters' worker thread die!", canalDestination);
+            if (thread != null) {
+                try {
+                    thread.join();
+                } catch (InterruptedException e) {
+                    // ignore
+                }
+            }
+            groupInnerExecutorService.shutdown();
+            logger.info("destination {} adapters' worker thread dead!", canalDestination);
+            for (List<CanalOuterAdapter> outerAdapters : canalOuterAdapters) {
+                for (CanalOuterAdapter adapter : outerAdapters) {
+                    adapter.destroy();
+                }
+            }
+            logger.info("destination {} all adapters destroyed!", canalDestination);
+        } catch (Exception e) {
+            logger.error(e.getMessage(), e);
+        }
+    }
+
+    private void process() {
+        while (!running)
+            ; // waiting until running == true
+        while (running) {
+            try {
+                // if (switcher != null) {
+                // switcher.get();
+                // }
+                logger.info("=============> Start to connect destination: {} <=============", this.canalDestination);
+                connector.connect();
+                logger.info("=============> Start to subscribe destination: {} <=============", this.canalDestination);
+                connector.subscribe();
+                logger.info("=============> Subscribe destination: {} succeed <=============", this.canalDestination);
+                while (running) {
+                    // try {
+                    // if (switcher != null) {
+                    // switcher.get();
+                    // }
+                    // } catch (TimeoutException e) {
+                    // break;
+                    // }
+
+                    // server配置canal.instance.network.soTimeout(默认: 30s)
+                    // 范围内未与server交互,server将关闭本次socket连接
+                    Message message = connector.getWithoutAck(BATCH_SIZE); // 获取指定数量的数据
+                    long batchId = message.getId();
+                    try {
+                        int size = message.getEntries().size();
+
+                        if (batchId == -1 || size == 0) {
+                            try {
+                                Thread.sleep(1000);
+                            } catch (InterruptedException e) {
+                                // ignore
+                            }
+                        } else {
+                            if (logger.isDebugEnabled()) {
+                                logger.debug("destination: {} batchId: {} batchSize: {} ",
+                                    this.canalDestination,
+                                    batchId,
+                                    size);
+                            }
+                            long begin = System.currentTimeMillis();
+                            writeOut(message);
+                            long now = System.currentTimeMillis();
+                            if (logger.isDebugEnabled()) {
+                                logger.debug("destination: {} batchId: {} elapsed time: {} ms",
+                                    this.canalDestination,
+                                    batchId,
+                                    now - begin);
+                            }
+                        }
+                        connector.ack(batchId); // 提交确认
+                    } catch (Exception e) {
+                        connector.rollback(batchId); // 处理失败, 回滚数据
+                        throw e;
+                    }
+                }
+
+            } catch (Exception e) {
+                logger.error("process error!", e);
+            } finally {
+                connector.disconnect();
+                logger.info("=============> Disconnect destination: {} <=============", this.canalDestination);
+            }
+
+            if (running) { // is reconnect
+                try {
+                    Thread.sleep(1000);
+                } catch (InterruptedException e) {
+                    // ignore
+                }
+            }
+
+        }
+    }
+}

+ 22 - 0
client-launcher/src/main/resources/canal-client.yml

@@ -0,0 +1,22 @@
+canalServerHost: 127.0.0.1:11111
+#zookeeperHosts: 127.0.0.1:2181
+#bootstrapServers: kafka1.mytest.com:9092,kafka2.mytest.com:9092
+
+canalInstances:
+- instance: example
+  adapterGroups:
+  - outAdapters:
+    - name: logger
+    - name: hbase
+      hosts: slave1:2181
+      properties: {znodeParent: "/hbase-unsecure"}
+
+#kafkaTopics:
+#- topic: devmysql4308
+#  groups:
+#  - groupId: devmysql4308_es
+#    adapters:
+#    - name: es
+#      hosts:
+#      zkHosts:
+#      properties: {clusterName: es-service-test}

+ 47 - 0
client-launcher/src/main/resources/logback.xml

@@ -0,0 +1,47 @@
+<configuration scan="true" scanPeriod=" 5 seconds">
+	<jmxConfigurator />
+	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder>
+			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{56} - %msg%n
+			</pattern>
+		</encoder>
+	</appender>
+	
+	<appender name="CANAL-ROOT" class="ch.qos.logback.classic.sift.SiftingAppender">
+		<discriminator>
+			<Key>destination</Key>
+			<DefaultValue>canal_client</DefaultValue>
+		</discriminator>
+		<sift>
+			<appender name="FILE-${destination}" class="ch.qos.logback.core.rolling.RollingFileAppender">
+				<File>../logs/${destination}.log</File>
+				<rollingPolicy
+					class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+					<!-- rollover daily -->
+					<fileNamePattern>../logs/%d{yyyy-MM-dd}/${destination}-%d{yyyy-MM-dd}-%i.log.gz</fileNamePattern>
+					<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+						<!-- or whenever the file size reaches 100MB -->
+						<maxFileSize>512MB</maxFileSize>
+					</timeBasedFileNamingAndTriggeringPolicy>
+					<maxHistory>60</maxHistory>
+				</rollingPolicy>
+				<encoder>
+					<pattern>
+						%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{56} - %msg%n
+					</pattern>
+				</encoder>
+			</appender>
+		</sift>
+	</appender>
+
+	<logger name="com.alibaba.otter.canal.client" additivity="false">
+		<level value="INFO" />
+		<!--<appender-ref ref="STDOUT"/>-->
+		<appender-ref ref="CANAL-ROOT" />
+	</logger>
+    
+	<root level="WARN">
+		<!--<appender-ref ref="STDOUT"/>-->
+		<appender-ref ref="CANAL-ROOT" />
+	</root>
+</configuration>

+ 3 - 1
pom.xml

@@ -120,6 +120,8 @@
         <module>kafka</module>
         <module>kafka-client</module>
         <module>prometheus</module>
+        <module>client-adapter</module>
+        <module>client-launcher</module>
     </modules>
 
     <dependencyManagement>
@@ -332,7 +334,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
-                <version>3.8.0</version>
+                <version>3.7.0</version>
                 <configuration>
                     <source>${java_source_version}</source>
                     <target>${java_target_version}</target>