下午接着检查王泽佑完成的作业情况,王泽佑的任务是使用Hibernate中的DriverManagerConnectionProvider来获得数据库连接,由于DriverManagerConnectionProvider没有提供构造方法或setter方法来接收配置信息,而是只能调用configure(Properties)方法来设置其配置信息,王泽佑为了能够利用起Spring,他将confiure方法接受的参数Properties对象作为Spring的一个bean对象(这有点过度使用Spring了,他们的理由是好玩和加深对spring的应用),没想到这一用却用出了一个很怪的问题:
下面是spring配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="providerProps" class="java.util.Properties">
<constructor-arg>
<props>
<prop key="hibernate.connection.driver_class">com.mysql.jdbc.Driver</prop>
<prop key="hibernate.connection.url">jdbc:mysql:///itcast</prop>
<prop key="hibernate.connection.username">root</prop>
<prop key="hibernate.connection.password">1234</prop>
</props>
</constructor-arg>
</bean>
</beans>
上面的配置相当于如下的一段源程序代码:
Properties providerProps = new Properties();
providerProps.setProperty("hibernate.connection.driver_class","com.mysql.jdbc.Driver");
providerProps.setProperty("hibernate.connection.url","jdbc:mysql:///itcast");
providerProps.setProperty("hibernate.connection.username","root");
providerProps.setProperty("hibernate.connection.password","1234");
Properties props = new Properties(providerProps);
即先创建出一个Properties对象,然后将这个Properties对象作为另外一个Properties对象的构造方法的参数去初试化另外那个Properties对象。
源程序中的调用代码片段如下:
DriverManagerConnectionProvider dmcp = new DriverManagerConnectionProvider();
ApplicationContext context = new ClassPathXmlApplicationContext("/applicationContext.xml");
props = (Properties)context.getBean("providerProps");
dmcp.configure(props);
Connection cn = dmcp.getConnection();
....
程序运行时,总是报告连接数据库失败,MySQl报告的失败原因是登陆帐户的密码为空。他们将上面的程序代码改成如下形式:
DriverManagerConnectionProvider dmcp = new DriverManagerConnectionProvider();
ApplicationContext context = new ClassPathXmlApplicationContext("/applicationContext.xml");
props = (Properties)context.getBean("providerProps");
dmcp.configure(props);
//增加了下面这条语句
props.setProperty("hibernate.connection.password",props.getProperty("hibernate.connection.password"));
Connection cn = dmcp.getConnection();
....
再次运行,结果就正常了。修改后的代码仅仅是再次设置了password属性,并且设置值就是从原来的props对象中提取出来的,加不加这条语句,似乎没有什么不同啊?在新增的这条语句前后分别打印出props对象中的内容,看到的结果也完全一样。
DriverManagerConnectionProvider dmcp = new DriverManagerConnectionProvider();
ApplicationContext context = new ClassPathXmlApplicationContext("/applicationContext.xml");
props = (Properties)context.getBean("providerProps");
dmcp.configure(props);
props.list(System.out); //与下面的list显示结果完全一样
//增加了下面这条语句
props.setProperty("hibernate.connection.password",props.getProperty("hibernate.connection.password"));
props.list(System.out); //与上面的list显示结果完全一样
Connection cn = dmcp.getConnection();
....
这个问题实在太怪异了!!!我怎么也想不明白!
于是,我打开JDK帮助文档,仔细阅读Properties类的帮助信息,注意到了如下一段描述:
A property list can contain another property list as its "defaults"; this second property list is searched if the property key is not found in the original property list.
其大概意思是说:一个Properties对象内部可以包含另外一个Properties对象作为其缺省Properties对象,当我们从Properties对象中检索一个属性时,先从这个Properties对象自身中查找,当找不着时,再从它内部包含的另外那个缺省Properties对象中查找。这是我以前不曾注意的一个细节,而我们上面的应用看起来正好与这个细节有些关系,我们也是先构造出一个Properties对象,然后将它设置成了另外一个Properties对象的内部缺省Properties对象,尽管内部的缺省Properties对象中包含了一个名为“hibernate.connection.password”的属性,但它毕竟不是外面的Properties对象自己的属性,我们对外面的Properties对象调用setProperty("hibernate.connection.password",...)方法,导致外面的Properties对象与它内部的缺省Properties对象中都有了一个名为“hibernate.connection.password”的属性,显然,我们调用了setProperty方法后确实改变了外面的Properties对象的内部数据,而不是我们以前认为的没有变化,看来问题可能就出现在这个细节上了。于是,我在eclipse下开始跟踪调试这个程序,跟踪到ConnectionProviderFacatory.getConnectionProperties方法时,看到该方法的代码如下:
public static Properties getConnectionProperties(Properties properties) {
Iterator iter = properties.keySet().iterator();
Properties result = new Properties();
while ( iter.hasNext() ) {
String prop = (String) iter.next();
if ( prop.indexOf(Environment.CONNECTION_PREFIX) > -1 && !SPECIAL_PROPERTIES.contains(prop) ) {
result.setProperty(
prop.substring( Environment.CONNECTION_PREFIX.length()+1 ),
properties.getProperty(prop)
);
}
}
String userName = properties.getProperty(Environment.USER);
if (userName!=null) result.setProperty( "user", userName );
return result;
}
该方法的作用是从一个Properties对象中提取与数据库连接信息相关的属性,并将这些连接属性生成一个新的Properties对象返回,这个新生成的properties对象中确实没有包含“password”属性,问题就出现在这个方法上。结合前面的Properties对象的重新认识,分析上面的代码,猜想是Properties.keySet()方法返回的关键字集合中只包含了它自己的关键字,而没有包含它内部的缺省Properties对象的关键字,为此,我写了如下一段代码来证实自己的猜想:
Properties props1 = new Properties();
props1.setProperty("hibernate.connection.username","root");
props1.setProperty("hibernate.connection.password","1234");
Properties props = new Properties(props1);
props.setProperty("hibernate.connection.username","root");
Enumeration e = props.keys();
while(e.hasMoreElements())
{
System.out.println((String)e.nextElement());
}
运行的结果,果然是只打印出了props.setProperty方法设置的关键字,而没有打印出props1中包含的关键字。至此,我完全确定了问题的原因,看来,Hibernate的开发者可能也没有注意Properties类的这个细节,因而给Hibernate留下了这样的一个Bug。