事情是这样的,一个夏日的午后,我先是提交了代码到我自己的开发分支,然后将开发分支合并到一个test分支,准备发到测试环境。先用Jenkins构建,正常,然后测试同学更新到test环境,十分钟过去了,更新仍未完成,哟嚯,多半是要凉了。

打开日志,一看,果然报错了。不对呀,我的分支启动是正常的啊,本地重新启动试试,我的分支可以正常启动,于是切到test分支重新启动,果然启动失败,错误信息如下:

mysql 启动 执行函数 mysql启动程序_java


从最下方的信息来看,大致是连接数据的时候失败了,抛了空指针,难道是从配置中心没有读取到配置mysql配置吗?

先看看代码

JDBCTemplate.java:49对应的queryDB方法

public void queryDB(String sql, DBCallback callback) throws Exception {
        Connection conn = null;
        Statement stmt = null;
        try {
            Class.forName("com.mysql.jdbc.Driver");
   			// 获取连接,调用的是DriverManager的方法
            conn = getConnection(dbUrl, dbUsername, dbPassword);
			。。。
        } catch (SQLException se) {
           。。。
        }
        } finally {
            。。。
        }
    }

DriverManager.java

@CallerSensitive
    public static Connection getConnection(String url,
        String user, String password) throws SQLException {
        // Properties实际是一个Hashtable
        java.util.Properties info = new java.util.Properties();

		// 此处做了判空处理,因此不可能是没有获取到用户名,密码同理
        if (user != null) {
            info.put("user", user);
        }
        if (password != null) {
            info.put("password", password);
        }
		
		// 继续获取connection
        return (getConnection(url, info, Reflection.getCallerClass()));
    }

DriverManager.java

private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        。。。
		// url有判空处理
        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }
        。。。
		// 遍历每个注册的Driver,如果有一个能获取到connection就返回
        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    // 操蛋就操蛋在这里,每个Driver都会去尝试获取connection,竟然还有Driver会更改info
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }
        }
        
       // 没有找到Driver抛异常
        throw new SQLException("No suitable driver found for "+ url, "08001");
    }

debug时截的图,已经加载的Drivers

mysql 启动 执行函数 mysql启动程序_mysql 启动 执行函数_02


通过调试,发现org.apache.phoenix.queryserver.client.Driver 更改了info

mysql 启动 执行函数 mysql启动程序_mysql_03

public Connection connect(String url, Properties info) throws SQLException {
        String prefix = this.getConnectStringPrefix();
        String urlSuffix = url.toLowerCase().substring(prefix.length());
        Properties connectionInfo = ConnectStringParser.parse(urlSuffix, info);
        // 当没有配置serialization时,就会set一个Serialization.PROTOBUF,WTF(注意,它不是String类型)
        if (!connectionInfo.containsKey("serialization") && !connectionInfo.containsKey("serialization".toUpperCase())) {
            info.put("serialization", Serialization.PROTOBUF);
        }

        this.setConnectionInfo(connectionInfo);
        return super.connect(url, info);
    }

接着往下看,进入到com.mysql.jdbc.NonRegisteringDriver#connect 方法

public java.sql.Connection connect(String url, Properties info) throws SQLException {
        if (url == null) {
            throw SQLError.createSQLException(Messages.getString("NonRegisteringDriver.1"), SQLError.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE, null);
        }

		。。。 

        Properties props = null;

		// 解析url
        if ((props = parseURL(url, info)) == null) {
            return null;
        }

        if (!"1".equals(props.getProperty(NUM_HOSTS_PROPERTY_KEY))) {
            return connectFailover(url, info);
        }

        try {
            Connection newConn = com.mysql.jdbc.ConnectionImpl.getInstance(host(props), port(props), props, database(props), url);

            return newConn;
        } catch (SQLException sqlEx) {
            。。。
        } catch (Exception ex) {
            。。。
        }
    }

这里是mysql-connector-java:5.1.46 中的com.mysql.jdbc.NonRegisteringDriver#parseURL(String url, Properties defaults) 方法的一个片段

defaults对应的就是info,原本info中只有用户名和密码,但是经过org.apache.phoenix.queryserver.client.Driver 处理之后,里面加入了一个serialization,value值是Serialization.PROTOBUF,这个时候defaults.getProperty("serialization") 取出的就不是一个String,而是nullurlProps.setProperty(key, property); 就会抛出NullPointerException,urlProps是一个Hashtable

mysql 启动 执行函数 mysql启动程序_java_04

// 获取String类型的值,如果取出的非String类型,则返回null
	public String getProperty(String key) {
        Object oval = super.get(key);
        String sval = (oval instanceof String) ? (String)oval : null;
        return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;
    }

urlProps是一个Properties对象

Properties urlProps = (defaults != null) ? new Properties(defaults) : new Properties();

而 Properties 是Hashtable

public class Properties extends Hashtable<Object,Object>

OK,发现罪魁祸首了,org.apache.phoenix.queryserver.client.Driver,这个是哪里引入的呢?我们需要它吗?

后续通过mvn依赖树,查到是有有其他团队的jar包传递依赖过来的,exclude之后程序启动正常。