问题
一个年代久远的项目:将商品库的数据清洗扩展,发送至消息中心,再写入ElasticSearch。某一次新需求开发。看到项目中的POM.xml文件有告警信息:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.RELEASE</version>
</parent>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.0.7</version>
</dependency>
Overriding managed version 5.1.41 for mysql-connector-java
嗯,这个mysql 的JDBC驱动版本为啥用这么低的版本,不科学!去了,让它和SpringBoot保持一致。于是,我去除了version指定,愉快地发版了。结果半夜被同事叫醒,说我负责的ElasticSearch数据有问题。经过调查,发现有问题的地方与这次新需求的改动毫无关系。我坚决及肯定地认为不是我的问题(当然我私下又把自己的代码审视了一番之后保证的)。但问题总是要解决,只好十分不情愿地回退版本。结果神奇地发现,数据在渐渐变得正确。
嗯,看来,确实是这次的改动造成了这次“动荡”。
第二天,再次仔细对比了版本,发现除了新需求改动,我还动了上面的POM文件。经过两次打包文件的对比,发现新版本的依赖包:mysql-connector-java已经是5.1.41。
升级JDBC驱动包会影响业务逻辑?!这很不科学!
经过很长时间的本地DEBUG,发现有一段类似下面的代码:
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.RowMapper;
public class DefaultRowMapper implements RowMapper<Map<String, String>> {
private static final Logger logger = LoggerFactory.getLogger(DefaultRowMapper.class);
private String[] columnNames = null;
private int[] columnTypes = null;
private final SimpleDateFormat dateFromat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private final SimpleDateFormat dateFromat2 = new SimpleDateFormat("yyyy-MM-dd");
public Map<String, String> mapRow(ResultSet rs, int rowNum) throws SQLException {
int size = 0;
if(columnNames == null){
ResultSetMetaData metaData = rs.getMetaData();
size = metaData.getColumnCount();
columnNames = new String[size];
columnTypes = new int[size];
for(int i=0; i<size; i++){
columnNames[i] = metaData.getColumnName(i+1);
columnTypes[i] = metaData.getColumnType(i+1);
}
}
Map<String, String> map = new HashMap<String, String>();
size = columnNames.length;
for(int i=0; i<size; i++){
map.put(columnNames[i], getValue(rs.getObject(i+1), columnTypes[i]));
}
return map;
}
private String getValue(Object fieldValue, int sqlType) {
if(fieldValue == null){
return null;
}
if(fieldValue instanceof String){
return (String)fieldValue;
}
switch(sqlType){
case java.sql.Types.TIMESTAMP:
try {
return dateFromat1.format(dateFromat1.parse(fieldValue.toString()));
} catch (Exception e) {
logger.error("dateFromat1 error"+ExceptionUtils.getFullStackTrace(e));
}
case java.sql.Types.DATE:
try {
return dateFromat2.format(dateFromat2.parse(fieldValue.toString()));
} catch (Exception e) {
logger.error("dateFromat2 error"+ExceptionUtils.getFullStackTrace(e));
}
case java.sql.Types.BOOLEAN:
case java.sql.Types.BIT:
if(Boolean.TRUE.equals(fieldValue)){
return "1";
}else{
return "0";
}
default:
return fieldValue.toString();
}
}
}
(大家看到这样的代码先不要笑!毕竟是十几年前的人写的,可能那时没有ORM,也可能那时作者不知道ORM)
如业务有类似下面的SQL:
SELECT PROVINCE_ID provinceId FROM PRODUCT_RANGE WHERE LIST_ID=?
在驱动版本:5.0.7时,metaData.getColumnName得到的是"provinceId",而当驱动版本升级为5.1.14时,得到的却是"PROVINCE_ID"。这就导致新版本业务数据全部不正常。
按照原编码的意图,应该使用metaData.getColumnLabel取得列名。但为什么原编码不使用metaData.getColumnLabel呢?我将本地环境改为了5.0.7后,发现无论是getColumnName()还是getColumnLabel()方法均返回"provinceId"。这就导致原作者认为无论使用哪个方法都是对的。
结论
经过不断调查,终于在官网上找到比较满意的回答。
MySQL :: MySQL Connector/J 8.0 Developer Guide :: 6.3.17 JDBC compliancehttps://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-jdbc-compliance.html为了防止官网改版移动位置,我在此快照下来:
useColumnNamesInFindColumn
在 JDBC-4.0 之前,JDBC 规范有一个错误,该错误与可以作为列名提供给结果集方法(如“findColumn()”)或采用 String 属性的 getter 相关。JDBC-4.0 将“列名”澄清为表示标签,如“AS”子句中给出并由“ResultSetMetaData.getColumnLabel()”返回,如果未指定“AS”子句,则为列名。将此属性设置为“true”将导致与 JDBC-3.0 和早期版本的 JDBC 规范一致的行为,但可能会产生意外结果。此属性优于“useOldAliasMetadataBehavior”
默认值 | false |
---|---|
自版本 | 5.1.7 |
useOldAliasMetadataBehavior
驱动程序是否应该对列和表上的“AS”子句使用旧行为,并且只返回“ResultSetMetaData.getColumnName()”或“ResultSetMetaData.getTableName()”的别名(如果有)而不是原始列/表名?
默认值 | false |
---|---|
自版本 | 5.0.4 |
简单地说,5.1.7版本时就已经使用JDBC4.0规范作为默认实现了。但为了兼容前版本的调用者,驱动有个开关。
说到底,原编码在使用5.0.7版本时,如果仔细看一下API,就应该使用getColumnLabel()方法来取得结果集列名。而我这个后来者,一不小心就踩到这个坑里去了。悲呼!