SQLServer数据库返回错误的国际化
前段时间测试提了一个BUG,我们繁体环境报错的提示是简体的,经查询之后这个错误是SQLServer抛出的,代码只是进行了异常转换,没有把异常信息换掉,在进行异常信息转换后,我又想,会不会有其他地方SQLServer的异常也没有转换,这个我没有办法去进行代码的全部扫描查询(即使进行了,也很难保证后面的同事不会再写),那我退而求其次,信息没转换我最起码能让用户看到的报错提示信息的语种是没有问题的,不能英语环境看到的是中文的报错。这个问题之前没有处理过也没有想过,于是只能自己摸索,经过查阅资料首先知道了这种情况在生产环境中基本不太可能出现,因为SQLServer默认给出的错误提示信息语言是和SQLServer环境有关的,英文生产环境下不太可能连接一个中文环境的数据库吧。
但是好奇心重的我还是想看一下有没有代码的方式可以解决这个问题,结果还真找到了。首先想到的就是驱动,驱动中有没有这么一个参数可以配置呢?查看Driver类,找到一个类似的
public static final String LANGUAGE = "prop.language";
只有这个常量看着是和语言有关系的,不能确定,也不知道怎么修改,那么第一步,先看下Driver是如何获取connection的,看Driver的connect方法
public Connection connect(String url, Properties info)
throws SQLException {
if (url == null || !url.toLowerCase().startsWith(driverPrefix)) {
return null;
}
Properties props = setupConnectProperties(url, info);
if (JDBC3) {
return new ConnectionJDBC3(url, props);
}
return new ConnectionJDBC2(url, props);
}
而ConnectionJDBC3是继承自ConnectionJDBC2,构造只是调用了ConnectionJDBC2的构造,ConnectionJDBC2的构造代码如下
ConnectionJDBC2(String url, Properties info)
throws SQLException {
this.url = url;
//
// Extract properties into instance variables
//
unpackProperties(info);
this.messages = new SQLDiagnostic(serverType);
//
// Get the instance port, if it is specified.
// Named pipes use instance names differently.
//
if (instanceName.length() > 0 && !namedPipe) {
final MSSqlServerInfo msInfo = new MSSqlServerInfo(serverName);
portNumber = msInfo.getPortForInstance(instanceName);
if (portNumber == -1) {
throw new SQLException(
Messages.get("error.msinfo.badinst", serverName, instanceName),
"08003");
}
}
SharedSocket.setMemoryBudget(bufferMaxMemory * 1024);
SharedSocket.setMinMemPkts(bufferMinPackets);
SQLWarning warn;
try {
Object timer = null;
if (loginTimeout > 0) {
// Start a login timer
timer = TimerThread.getInstance().setTimer(loginTimeout * 1000,
new TimerThread.TimerListener() {
public void timerExpired() {
if (socket != null) {
socket.forceClose();
}
}
});
}
if (namedPipe) {
// Use named pipe
socket = createNamedPipe(this);
} else {
// Use plain TCP/IP socket
socket = new SharedSocket(this);
}
if (timer != null && TimerThread.getInstance().hasExpired(timer)) {
// If the timer has expired during the connection phase, close
// the socket and throw an exception
socket.forceClose();
throw new IOException("Login timed out");
}
if ( charsetSpecified ) {
loadCharset(serverCharset);
} else {
// Need a default charset to process login packets for TDS 4.2/5.0
// Will discover the actual serverCharset later
loadCharset("iso_1");
serverCharset = ""; // But don't send charset name to server!
}
//
// Create TDS protocol object
//
baseTds = new TdsCore(this, messages);
//
// Negotiate SSL connection if required
//
if (tdsVersion >= Driver.TDS80 && !namedPipe) {
baseTds.negotiateSSL(instanceName, ssl);
}
//
// Now try to login
//
baseTds.login(serverName,
databaseName,
user,
password,
domainName,
serverCharset,
appName,
progName,
wsid,
language,
macAddress,
packetSize);
if (timer != null) {
// Cancel loginTimer
TimerThread.getInstance().cancelTimer(timer);
}
//
// Save any login warnings so that they will not be overwritten by
// the internal configuration SQL statements e.g. setCatalog() etc.
//
warn = messages.warnings;
// Update the tdsVersion with the value in baseTds. baseTds sets
// the TDS version for the socket and there are no other objects
// with cached TDS versions at this point.
tdsVersion = baseTds.getTdsVersion();
if (tdsVersion < Driver.TDS70 && databaseName.length() > 0) {
// Need to select the default database
setCatalog(databaseName);
}
} catch (UnknownHostException e) {
throw Support.linkException(
new SQLException(Messages.get("error.connection.badhost",
e.getMessage()), "08S03"), e);
} catch (IOException e) {
if (loginTimeout > 0 && e.getMessage().indexOf("timed out") >= 0) {
throw Support.linkException(
new SQLException(Messages.get("error.connection.timeout"), "HYT01"), e);
}
throw Support.linkException(
new SQLException(Messages.get("error.connection.ioerror",
e.getMessage()), "08S01"), e);
} catch (SQLException e) {
if (loginTimeout > 0 && e.getMessage().indexOf("socket closed") >= 0) {
throw Support.linkException(
new SQLException(Messages.get("error.connection.timeout"), "HYT01"), e);
}
throw e;
}
// If charset is still unknown and the collation is not set either,
// determine the charset by querying (we're using Sybase or SQL Server
// 6.5)
if ((serverCharset == null || serverCharset.length() == 0)
&& collation == null) {
loadCharset(determineServerCharset());
}
// Initial database settings.
// Sets: auto commit mode = true
// transaction isolation = read committed.
if (serverType == Driver.SYBASE) {
baseTds.submitSQL(SYBASE_INITIAL_SQL);
} else {
// Also discover the maximum decimal precision: 28 (default)
// or 38 for MS SQL Server 6.5/7, or 38 for 2000 and later.
Statement stmt = this.createStatement();
ResultSet rs = stmt.executeQuery(SQL_SERVER_INITIAL_SQL);
if (rs.next()) {
maxPrecision = rs.getByte(1);
}
rs.close();
stmt.close();
}
//
// Restore any login warnings so that the user can retrieve them
// by calling Connection.getWarnings()
//
messages.warnings = warn;
}
protected void unpackProperties(Properties info)
throws SQLException {
serverName = info.getProperty(Messages.get(Driver.SERVERNAME));
portNumber = parseIntegerProperty(info, Driver.PORTNUMBER);
serverType = parseIntegerProperty(info, Driver.SERVERTYPE);
databaseName = info.getProperty(Messages.get(Driver.DATABASENAME));
instanceName = info.getProperty(Messages.get(Driver.INSTANCE));
domainName = info.getProperty(Messages.get(Driver.DOMAIN));
user = info.getProperty(Messages.get(Driver.USER));
password = info.getProperty(Messages.get(Driver.PASSWORD));
macAddress = info.getProperty(Messages.get(Driver.MACADDRESS));
appName = info.getProperty(Messages.get(Driver.APPNAME));
progName = info.getProperty(Messages.get(Driver.PROGNAME));
wsid = info.getProperty(Messages.get(Driver.WSID));
serverCharset = info.getProperty(Messages.get(Driver.CHARSET));
language = info.getProperty(Messages.get(Driver.LANGUAGE));
lastUpdateCount = "true".equalsIgnoreCase(
info.getProperty(Messages.get(Driver.LASTUPDATECOUNT)));
useUnicode = "true".equalsIgnoreCase(
info.getProperty(Messages.get(Driver.SENDSTRINGPARAMETERSASUNICODE)));
namedPipe = "true".equalsIgnoreCase(
info.getProperty(Messages.get(Driver.NAMEDPIPE)));
tcpNoDelay = "true".equalsIgnoreCase(
info.getProperty(Messages.get(Driver.TCPNODELAY)));
useCursors = (serverType == Driver.SQLSERVER)
&& "true".equalsIgnoreCase(
info.getProperty(Messages.get(Driver.USECURSORS)));
useLOBs = "true".equalsIgnoreCase(
info.getProperty(Messages.get(Driver.USELOBS)));
useMetadataCache = "true".equalsIgnoreCase(
info.getProperty(Messages.get(Driver.CACHEMETA)));
xaEmulation = "true".equalsIgnoreCase(
info.getProperty(Messages.get(Driver.XAEMULATION)));
charsetSpecified = serverCharset.length() > 0;
Integer parsedTdsVersion =
DefaultProperties.getTdsVersion(info.getProperty(Messages.get(Driver.TDS)));
if (parsedTdsVersion == null) {
throw new SQLException(Messages.get("error.connection.badprop",
Messages.get(Driver.TDS)), "08001");
}
tdsVersion = parsedTdsVersion.intValue();
packetSize = parseIntegerProperty(info, Driver.PACKETSIZE);
if (packetSize < TdsCore.MIN_PKT_SIZE) {
if (tdsVersion >= Driver.TDS70) {
// Default of 0 means let the server specify packet size
packetSize = (packetSize == 0) ? 0 : TdsCore.DEFAULT_MIN_PKT_SIZE_TDS70;
} else if (tdsVersion == Driver.TDS42) {
// Sensible minimum for older versions of TDS
packetSize = TdsCore.MIN_PKT_SIZE;
} // else for TDS 5 can auto negotiate
}
if (packetSize > TdsCore.MAX_PKT_SIZE) {
packetSize = TdsCore.MAX_PKT_SIZE;
}
packetSize = (packetSize / 512) * 512;
loginTimeout = parseIntegerProperty(info, Driver.LOGINTIMEOUT);
socketTimeout = parseIntegerProperty(info, Driver.SOTIMEOUT);
lobBuffer = parseLongProperty(info, Driver.LOBBUFFER);
maxStatements = parseIntegerProperty(info, Driver.MAXSTATEMENTS);
statementCache = new ProcedureCache(maxStatements);
prepareSql = parseIntegerProperty(info, Driver.PREPARESQL);
if (prepareSql < 0) {
prepareSql = 0;
} else if (prepareSql > 3) {
prepareSql = 3;
}
// For Sybase use equivalent of sp_executesql.
if (tdsVersion < Driver.TDS70 && prepareSql == TdsCore.PREPARE) {
prepareSql = TdsCore.EXECUTE_SQL;
}
// For SQL 6.5 sp_executesql not available so use stored procedures.
if (tdsVersion < Driver.TDS50 && prepareSql == TdsCore.EXECUTE_SQL) {
prepareSql = TdsCore.TEMPORARY_STORED_PROCEDURES;
}
ssl = info.getProperty(Messages.get(Driver.SSL));
batchSize = parseIntegerProperty(info, Driver.BATCHSIZE);
if (batchSize < 0) {
throw new SQLException(Messages.get("error.connection.badprop",
Messages.get(Driver.BATCHSIZE)), "08001");
}
bufferMaxMemory = parseIntegerProperty(info, Driver.BUFFERMAXMEMORY);
if (bufferMaxMemory < 0) {
throw new SQLException(Messages.get("error.connection.badprop",
Messages.get(Driver.BUFFERMAXMEMORY)), "08001");
}
bufferMinPackets = parseIntegerProperty(info, Driver.BUFFERMINPACKETS);
if (bufferMinPackets < 1) {
throw new SQLException(Messages.get("error.connection.badprop",
Messages.get(Driver.BUFFERMINPACKETS)), "08001");
}
}
至此,如果所猜测没有错,我们应该给Driver的connetion方法传入Properties,key是Messages.get(Driver.LANGUAGE),value是我们希望SQLServer数据库返回给我们的信息的语种。
但是Driver的方法我们是没有办法直接去调用的,我们能操作的只有数据源。
下一步,查询一下获取连接的代码,我们自定义的dataSource是继承BasicDataSource(org.apache.commons.dbcp)的,查看BasicDataSource的getConnection方法
/** * Create (if necessary) and return a connection to the database. * * @throws SQLException if a database access error occurs * @return a database connection */ public Connection getConnection() throws SQLException { return createDataSource().getConnection(); }
protected synchronized DataSource createDataSource()
throws SQLException {
if (closed) {
throw new SQLException("Data source is closed");
}
// Return the pool if we have already created it
if (dataSource != null) {
return (dataSource);
}
// create factory which returns raw physical connections
ConnectionFactory driverConnectionFactory = createConnectionFactory();
// create a pool for our connections
createConnectionPool();
// Set up statement pool, if desired
GenericKeyedObjectPoolFactory statementPoolFactory = null;
if (isPoolPreparedStatements()) {
statementPoolFactory = new GenericKeyedObjectPoolFactory(null,
-1, // unlimited maxActive (per key)
GenericKeyedObjectPool.WHEN_EXHAUSTED_FAIL,
0, // maxWait
1, // maxIdle (per key)
maxOpenPreparedStatements);
}
// Set up the poolable connection factory
createPoolableConnectionFactory(driverConnectionFactory, statementPoolFactory, abandonedConfig);
// Create and return the pooling data source to manage the connections
createDataSourceInstance();
try {
for (int i = 0 ; i < initialSize ; i++) {
connectionPool.addObject();
}
} catch (Exception e) {
throw new SQLNestedException("Error preloading the connection pool", e);
}
return dataSource;
}
protected ConnectionFactory createConnectionFactory() throws SQLException {
// Load the JDBC driver class
Class driverFromCCL = null;
if (driverClassName != null) {
try {
try {
if (driverClassLoader == null) {
Class.forName(driverClassName);
} else {
Class.forName(driverClassName, true, driverClassLoader);
}
} catch (ClassNotFoundException cnfe) {
driverFromCCL = Thread.currentThread(
).getContextClassLoader().loadClass(
driverClassName);
}
} catch (Throwable t) {
String message = "Cannot load JDBC driver class '" +
driverClassName + "'";
logWriter.println(message);
t.printStackTrace(logWriter);
throw new SQLNestedException(message, t);
}
}
// Create a JDBC driver instance
Driver driver = null;
try {
if (driverFromCCL == null) {
driver = DriverManager.getDriver(url);
} else {
// Usage of DriverManager is not possible, as it does not
// respect the ContextClassLoader
driver = (Driver) driverFromCCL.newInstance();
if (!driver.acceptsURL(url)) {
throw new SQLException("No suitable driver", "08001");
}
}
} catch (Throwable t) {
String message = "Cannot create JDBC driver of class '" +
(driverClassName != null ? driverClassName : "") +
"' for connect URL '" + url + "'";
logWriter.println(message);
t.printStackTrace(logWriter);
throw new SQLNestedException(message, t);
}
// Can't test without a validationQuery
if (validationQuery == null) {
setTestOnBorrow(false);
setTestOnReturn(false);
setTestWhileIdle(false);
}
// Set up the driver connection factory we will use
String user = username;
if (user != null) {
connectionProperties.put("user", user);
} else {
log("DBCP DataSource configured without a 'username'");
}
String pwd = password;
if (pwd != null) {
connectionProperties.put("password", pwd);
} else {
log("DBCP DataSource configured without a 'password'");
}
ConnectionFactory driverConnectionFactory = new DriverConnectionFactory(driver, url, connectionProperties);
return driverConnectionFactory;
}
关注红色代码,发现最终会new 一个DriverConnectionFactory
public class DriverConnectionFactory implements ConnectionFactory { public DriverConnectionFactory(Driver driver, String connectUri, Properties props) { _driver = driver; _connectUri = connectUri; _props = props; } public Connection createConnection() throws SQLException { return _driver.connect(_connectUri,_props); } protected Driver _driver = null; protected String _connectUri = null; protected Properties _props = null; public String toString() { return this.getClass().getName() + " [" + String.valueOf(_driver) + ";" + String.valueOf(_connectUri) + ";" + String.valueOf(_props) + "]"; } }
我们可以看到,其实最终调用的还是Driver的connection方法来产生连接,而这个方法会把props给传进去,所以我们的目标就变为了给connectionProperties给增加键值对,而BasicDataSource中确实有这么个方法
public void addConnectionProperty(String name, String value) { connectionProperties.put(name, value); this.restartNeeded = true; }
接下来就简单了,只需要在继承类中调用这个方法就可以了,KEY值已确定,VALUE值查询SQLServer相关文档很容易获取到。
PS:如果没有写继承类,也可以使用xml定义,例如:<property name=“connectionProperties” value="LANGUAGE=简体中文"/>,如果存在多个需要配置的属性,使用英文分号隔开即可。我使用继承类来写主要是我只希望是SQLServer环境的时候这个参数才起作用,使用代码实现比较方便,XML怎么实现不是特别清楚。
那么只剩下最后一步了,测试我们的猜测是否正确,幸运的是我们的猜测完全正确((〃'▽'〃))。其实不正确也没关系,理论上来说只要驱动提供了这个功能我们应该就是可以这么设置的,只不过KEY可能猜错了,换其他的KEY试一下就好了。