使用javaagent替换nacos数据库MySQL到Postgresql

背景

nacos 默认支持的数据库是MySQL. 不支持Postgresql或其他数据库. 如果想使用其他数据有几种方式

  • 修改源码. 缺点是如果nacos升级则还需要同步代码做升级
  • 从jdbc层面考虑. 可以使用javaagent替换MySQL驱动为postgresql从而替换到底层的数据库.

实现方式

  • JavaAgent是一种探针技术,它可以通过 premain 方法,在类加载的过程中给指定的方法进行字节码增强。因为类最终都是以字节码指令执行的,通过这种增强后的方法就可以输出我们想要的信息
  • 入口类
import cn.agent.infrastructure.mysql2postgresql.driver.MysqlToPostgresDriver;
import javassist.*;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.util.HashMap;
import java.util.Map;

/**
 * Mysql驱动替换
 * 在驱动类没有提供配置参数的情况下使用agent替换
 *
 * @author wxl
 */
public class JdbcDriverAgent {

    private static final byte[] EMPTY_BYTE_ARRAY = {};

    private static final String DEFAULT_MYSQL_JDBC_DRIVER_NAME = "com.mysql.cj.jdbc.Driver";
    private static final String MYSQL_DRIVER_CLASS_NAME_KEY = "driverClassName";

    private static final String MTO_DRIVER_NAME = MysqlToPostgresDriver.class.getName();

    public static void premain(String agentArgs, Instrumentation inst) {
        final ClassFileTransformer transformer = (loader, classFile, classBeingRedefined, protectionDomain, classfileBuffer) -> {

            //Lambda
            if (classFile == null) {
                return EMPTY_BYTE_ARRAY;
            }

            final String className = toClassName(classFile);

            final String mysqlDriverClassName = splitCommaColonStringToKv(agentArgs).getOrDefault(MYSQL_DRIVER_CLASS_NAME_KEY, DEFAULT_MYSQL_JDBC_DRIVER_NAME);

            if (mysqlDriverClassName.equals(className)) {

                ClassLoader classLoader = loader == null ? ClassLoader.getSystemClassLoader() : loader;

                final ClassPool classPool = new ClassPool(true);
                if (classLoader == null) {
                    classPool.appendClassPath(new LoaderClassPath(ClassLoader.getSystemClassLoader()));
                } else {
                    classPool.appendClassPath(new LoaderClassPath(classLoader));
                }

                try {
                    CtClass ctClass = classPool.get(MTO_DRIVER_NAME);
                    ctClass.setName(mysqlDriverClassName);
                    return ctClass.toBytecode();
                } catch (NotFoundException | CannotCompileException | IOException e) {
                    throw new IllegalStateException(e);
                }
            }

            return EMPTY_BYTE_ARRAY;
        };

        inst.addTransformer(transformer, true);
    }

    private static String toClassName(final String classFile) {
        return classFile.replace('/', '.');
    }

    /**
     * Split to {@code json} like String({@code "k1:v1,k2:v2"}) to KV map({@code "k1"->"v1", "k2"->"v2"}).
     */
    static Map<String, String> splitCommaColonStringToKv(String commaColonString) {
        Map<String, String> ret = new HashMap<>(6);
        if (commaColonString == null || commaColonString.trim().isEmpty()) {
            return ret;
        }

        final String[] splitKvArray = commaColonString.trim().split("\\s*,\\s*");

        for (String kvString : splitKvArray) {
            final String[] kv = kvString.trim().split("\\s*:\\s*");
            if (kv.length == 0) {
                continue;
            }

            if (kv.length == 1) {
                ret.put(kv[0], "");
            } else {
                ret.put(kv[0], kv[1]);
            }
        }

        return ret;
    }
}
  • 自定义Driver
import cn.agent.infrastructure.mysql2postgresql.wrapper.ConnectionWrapper;
import org.postgresql.Driver;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

/**
 * @author wxl
 */
public class MysqlToPostgresDriver extends Driver {
    private static final java.sql.Driver INSTANCE = new MysqlToPostgresDriver();

    private static final String MYSQL = "mysql";

    private static final String PG = "postgresql";

    private static final String DEF_MYSQL_PORT = "3306";

    private static final String DEF_PG_PORT = "5432";

    static {
        try {
            DriverManager.registerDriver(MysqlToPostgresDriver.INSTANCE);
        } catch (SQLException e) {
            throw new IllegalStateException("Could not register MysqlToOracleDriver with DriverManager.", e);
        }
    }

    @Override
    public Connection connect(String url, Properties info) throws SQLException {
        String replaceURL = replaceURL(url);
        return ConnectionWrapper.wrap(super.connect(replaceURL, info));
    }

    @Override
    public boolean acceptsURL(String url) {
        String replaceURL = replaceURL(url);
        return super.acceptsURL(replaceURL);
    }

    public String replaceURL(String url) {
        if (url.contains(MYSQL)) {
            url = url.replaceAll(MYSQL, PG);
        }
        if (url.contains(DEF_MYSQL_PORT)) {
            url = url.replaceAll(DEF_MYSQL_PORT, DEF_PG_PORT);
        }
        return url;
    }
}

SQL 转换

  • 目前使用的是sqlparser来做的SQL转换https://github.com/JSQLParser/JSqlParser.也可以试试JOOQ
  • 一些特殊的SQL直接替换即可
  • 其中对与MySQL自增序列在pg里需要特殊处理下. 具体在
    @Override
    public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
        //TODO PG 中 autoGeneratedKeys 与 bigserial 不兼容
        // 改成 cn.agent.infrastructure.mysql2postgresql.wrapper.ConnectionWrapper.prepareStatement(java.lang.String, java.lang.String[])
        return delegate.prepareStatement(SqlHelper.mysql2postgresql(sql), new String[]{"id"});
//        return PreparedStatementWrapper.wrap(delegate.prepareStatement(SqlHelper.mysql2postgresql(sql), autoGeneratedKeys), sql);
    }

github 源码.欢迎starred

  • https://github.com/siaron/mysql2postgresql-jdbc-agent

你可能感兴趣的:(postgres,spring,mysql,postgresql,nacos)