在web服务接入其他系统的数据视图时,可能会遇到中文显示乱码的问题,如采用了Oracle 10g数据库的系统(老旧而“稳定”的系统,嗯)的编码集为US7ASCII,而客户端的编码集通常可能为UTF-8,大多在plsql这种客户端查询是可以正常显示中文的,而此时程序通过jdbc驱动查询到的中文内容就可能会是乱码,这是因为服务端提供的中文内容是iso-8859-1编码(US7ASCII为iso-8859-1的子集)方式,客户端使用的还是utf-8导致的。解决方法可以是服务端解决,也可以是客户端解决。
然而大多数时候,服务端出于部分原因而无法去主动配合更改其系统或者数据库的编码集,这时候就需要客户端自己来想办法解决。本文是通过客户端来主动转码来解决该问题,因此服务端的解决方式不在赘述。
如果你也使用了mybatis或mybatis plus,那么就可以通过typeHandler来实现统一处理来解决编码集问题。
此处直接贴出实际代码(kotlin):
import com.baomidou.dynamic.datasource.toolkit.*
import com.kamjin.common.ext.*
import org.apache.ibatis.session.*
import org.apache.ibatis.type.*
import org.springframework.beans.factory.annotation.*
import org.springframework.boot.*
import org.springframework.stereotype.*
import java.sql.*
/**
*
* 数据库String的编码集转换handler
*
* @author kam
* @since 2022/10/26
*/
@Component
@MappedTypes(String::class)
class DatabaseStringEncodeHandler : BaseTypeHandler<String>(), CommandLineRunner {
private val log = getLogger<DatabaseStringEncodeHandler>()
/**
* 数据库key,在mp多数据源中的key,注:想要处理主库,加入",master"即可
*/
@Value("\${database.string.encode.dbKeys:_u_n_k_n_o_w_}")
var dbKeys: MutableList<String> = mutableListOf()
/**
* 数据库服务端对应的编码集
*/
@Value("\${database.string.encode.serverCharset:UTF-8}")
var serverCharset: String? = null
/**
* 数据库客户端对应的编码集
*/
@Value("\${database.string.encode.clientCharset:UTF-8}")
var clientCharset: String? = null
@Autowired
lateinit var sqlSessionFactory: SqlSessionFactory
override fun setNonNullParameter(ps: PreparedStatement?, i: Int, parameter: String?, jdbcType: JdbcType?) {
try {
ps?.setString(i, if (needCodec(parameter)) parameter?.encode() else parameter)
} catch (e: Exception) {
ps?.setString(i, parameter)
log.error(
"【数据库编码集转换】setNonNullParameter(ps: PreparedStatement?, i: Int, parameter: String?, jdbcType: JdbcType?)失败,e:",
e
)
}
}
override fun getNullableResult(rs: ResultSet?, columnName: String?): String? {
val value = rs?.getString(columnName)
return try {
if (needCodec(value)) value?.decode() else value
} catch (e: Exception) {
log.error("【数据库编码集转换】getNullableResult(rs: ResultSet?, columnName: String?)失败,e:", e)
value
}
}
override fun getNullableResult(rs: ResultSet?, columnIndex: Int): String? {
val value = rs?.getString(columnIndex)
return try {
if (needCodec(value)) value?.decode() else value
} catch (e: Exception) {
log.error("【数据库编码集转换】getNullableResult(rs: ResultSet?, columnIndex: Int) 失败,e:", e)
value
}
}
override fun getNullableResult(cs: CallableStatement?, columnIndex: Int): String? {
val value = cs?.getString(columnIndex)
return try {
if (needCodec(value)) value?.decode() else value
} catch (e: Exception) {
log.error("【数据库编码集转换】getNullableResult(cs: CallableStatement?, columnIndex: Int) 失败,e:", e)
value
}
}
/**
* 编码
*/
fun String?.encode(): String? {
return this?.toByteArray(charset(clientCharset!!))?.toString(charset(serverCharset!!))
}
/**
* 解码
*/
fun String?.decode(): String? {
return this?.toByteArray(charset(serverCharset!!))?.toString(charset(clientCharset!!))
}
/**
* [value]满足该条件则转码
*/
fun needCodec(value: String?): Boolean {
val currentDbKey = DynamicDataSourceContextHolder.peek()
?: "master" //如果是null,则赋值master,表示主数据源,注意,该master和多数据源的primary无直接关联,此处想要处理主数据源,在dbNames中直接加入,master即可
return currentDbKey.isNotBlank()
&& dbKeys.contains(currentDbKey)
&& serverCharset != clientCharset
&& value?.isNotBlank() == true
}
/**
* 替换默认的StringTypeHandler
*/
override fun run(vararg args: String?) {
val hr = sqlSessionFactory.configuration.typeHandlerRegistry
val javaStringType = String::class.java
hr.register(javaStringType, this)
hr.register(javaStringType, JdbcType.CHAR, this)
hr.register(javaStringType, JdbcType.NCHAR, this)
hr.register(javaStringType, JdbcType.VARCHAR, this)
hr.register(javaStringType, JdbcType.NVARCHAR, this)
hr.register(javaStringType, JdbcType.LONGVARCHAR, this)
hr.register(javaStringType, JdbcType.LONGNVARCHAR, this)
}
}
当前DatabaseStringEncodeHandler 实现了mybatis的TypeHandler,泛型为String,表示可以处理所有的orm映射字段类型为String的字段,换言之,只需要转码字符串类型的字段即可
当前TypeHandler的核心代码为encode方法和decode方法,这里参考了druid的com.alibaba.druid.filter.encoding.EncodingConvertFilter
(这里为什么不使用该filter是因为博主测试过了druid提供的这个filter只会编码参数,而不会解码返回值,就自己参考他部分代码写了这个typeHandler,ps:那块的源码很多很绕,感兴趣的可以去看看。)
encode作用为编码具体的sql参数,decode为解码具体的返回值参数,如果编解码失败了,则走原生处理方式,参考org.apache.ibatis.type.StringTypeHandler
因为博主的项目中存在多数据源情况,所以此处有dbKeys属性,来帮助判断具体是哪几个库需要编解码的。如果项目中只有单数据源,则修改needCodec方法,去掉其中部分条件:
val currentDbKey = DynamicDataSourceContextHolder.peek()
?: "master"
currentDbKey.isNotBlank()
&& dbKeys.contains(currentDbKey)
相应的,也去掉当前类成员变量dbKeys,配置文件中也无需配置该参数
/**
* 用以使自定义TypeHandler替换mybatis默认的基础类型处理TypeHandler
*/
@Component
class MybatisTypeHandlerReplacer : CommandLineRunner {
@Autowired
lateinit var sqlSessionFactory: SqlSessionFactory
@Autowired
lateinit var databaseStringEncodeHandler: DatabaseStringEncodeHandler
override fun run(vararg args: String?) {
val hr = sqlSessionFactory.configuration.typeHandlerRegistry
val javaStringType = String::class.java
hr.register(javaStringType, databaseStringEncodeHandler)
hr.register(javaStringType, JdbcType.CHAR, databaseStringEncodeHandler)
hr.register(javaStringType, JdbcType.NCHAR, databaseStringEncodeHandler)
hr.register(javaStringType, JdbcType.VARCHAR, databaseStringEncodeHandler)
hr.register(javaStringType, JdbcType.NVARCHAR, databaseStringEncodeHandler)
hr.register(javaStringType, JdbcType.LONGVARCHAR, databaseStringEncodeHandler)
hr.register(javaStringType, JdbcType.LONGNVARCHAR, databaseStringEncodeHandler)
}
}
在spring的application.yaml中配置该内容,按照自己的实际情况配置
#数据库string编码集转码配置
database:
string:
encode:
#需要转码的dbkeys(非库名),对应mp多数据源中的key名称,如果是主数据源也需要转码,此处约定为master(与多数据源的primary无直接关系),追加,master即可
dbKeys: test_db,master
#数据库服务端对应的编码集
serverCharset: ISO-8859-1
#数据库客户端对应的编码集
clientCharset: UTF-8
其中配置编码集方面,编码方式的选择参考了文章:
https://blog.csdn.net/Sun_Raiser/article/details/120714761
按照自己的实际情况进行选择即可。
------------------------------------------------更新---------------------------------------------
建议直接把注册代码单独拎出来,原因是上面自注册的代码我在开发环境试过没有任何问题,但部署到测试环境后就出现了循环依赖问题。。这个也好解释,每个环境配置不同,spring加载bean的顺序也不完全相同。且拎出来也好维护。
其他注意点:
1.
oracle驱动的版本很重要!
oracle驱动的版本很重要!
oracle驱动的版本很重要!
一定要对版本,oracle 10g的正确可用ojdbc驱动版本为:ojdbc6-11.2.0.4。我之前用了easyproject的版本,编码集仍有问题,用了这个才解决的
2.
还有一点,如果你用rs.getString()获取了数据,那么好了,这一步已经无法再转码了,要用rs.getObject()(在具体转码时再toString())(具体原因是ResultSet中getString()和getObject()实现行为的不一致),否则仍然无法转码。这问题真的很坑,请诸位一定要有个印象。。
3.
如果说你使用的是mybatis/mybatisplus框架,然后又集成了druid或其他连接池等,此时可能还会有一个坑在等着你,我上一个提示说要使用rs.getObject()是因为oracle.jdbc.driver.OracleResultSetImpl
(ojdbc6)中的getString()和getObject(),确确实实的实现行为不一致。而你使用mybatis的typeHandler,几个回调方法中的rs其实是代理类是:org.apache.ibatis.logging.jdbc.ResultSetLogger
,而通过这个代理类代理后,getObject()内部实现居然还是getString(),这就很恶心了,,解决的方法也比较简单,java.sql.Wrapper
的作者已经贴心的想到了这一点了,引用源代码中的原话:
package java.sql;
/**
* Interface for JDBC classes which provide the ability to retrieve the delegate instance when the instance
* in question is in fact a proxy class.
*
* The wrapper pattern is employed by many JDBC driver implementations to provide extensions beyond
* the traditional JDBC API that are specific to a data source. Developers may wish to gain access to
* these resources that are wrapped (the delegates) as proxy class instances representing the
* the actual resources. This interface describes a standard mechanism to access
* these wrapped resources
* represented by their proxy, to permit direct access to the resource delegates.
*
* @since 1.6
*/
public interface Wrapper {
机翻:
JDBC类的接口,当实例*实际上是一个代理类时,它提供了检索委托实例的能力。
包装器模式被许多JDBC驱动程序实现所采用,以提供超越传统JDBC API的扩展,
这些API是特定于数据源的。开发人员可能希望获得*这些资源的访问权,
这些资源被包装为代表*实际资源的代理类实例(委托)。
这个接口描述了一种标准机制来访问由代理代表的包装资源,以允许直接访问资源委托。
* @自1.6
so,我们直接用这个接口判断并拿到实际代理对象(unwrap)即可,因为我们不希望这个ResultSet
实现类在此处被改变行为:
val realrs =
if (rs?.isWrapperFor(OracleResultSet::class.java) == true) rs.unwrap(OracleResultSet::class.java) else rs
//用实际的rs进行操作
val value = realrs?.getObject(columnName)
...
实际项目中在用的可用代码:
import com.baomidou.dynamic.datasource.toolkit.*
import com.kamjin.common.ext.*
import oracle.jdbc.*
import org.apache.ibatis.session.*
import org.apache.ibatis.type.*
import org.springframework.beans.factory.annotation.*
import org.springframework.boot.*
import org.springframework.stereotype.*
import java.sql.*
/**
*
*
*
*
* @author kam
* @since 2022/10/26
*/
@Component
@MappedTypes(String::class)
class DatabaseStringEncodeHandler : BaseTypeHandler<String?>() {
private val log = getLogger<DatabaseStringEncodeHandler>()
/**
* 数据库key,在mp多数据源中的key,注:想要处理主库,加入",master"即可
*/
@Value("\${database.string.encode.dbKeys:_u_n_k_n_o_w_}")
var dbKeys: MutableList<String> = mutableListOf()
/**
* 数据库服务端对应的编码集
*/
@Value("\${database.string.encode.serverCharset:UTF-8}")
var serverCharset: String? = null
/**
* 数据库客户端对应的编码集
*/
@Value("\${database.string.encode.clientCharset:UTF-8}")
var clientCharset: String? = null
override fun setNonNullParameter(ps: PreparedStatement?, i: Int, parameter: String?, jdbcType: JdbcType?) {
val realps =
if (ps?.isWrapperFor(OraclePreparedStatement::class.java) == true) ps.unwrap(OraclePreparedStatement::class.java) else ps
try {
realps?.setObject(i, if (needCodec(parameter)) parameter?.encode() else parameter)
} catch (e: Exception) {
realps?.setObject(i, parameter)
log.error(
"【数据库编码集转换】setNonNullParameter(ps: PreparedStatement?, i: Int, parameter: String?, jdbcType: JdbcType?)失败,e:",
e
)
}
}
override fun getNullableResult(rs: ResultSet?, columnName: String?): String? {
val realrs =
if (rs?.isWrapperFor(OracleResultSet::class.java) == true) rs.unwrap(OracleResultSet::class.java) else rs
val value = realrs?.getObject(columnName)
return try {
if (needCodec(value)) value?.decode() else value?.toString()
} catch (e: Exception) {
log.error("【数据库编码集转换】getNullableResult(rs: ResultSet?, columnName: String?)失败,e:", e)
value?.toString()
}
}
override fun getNullableResult(rs: ResultSet?, columnIndex: Int): String? {
val realrs =
if (rs?.isWrapperFor(OracleResultSet::class.java) == true) rs.unwrap(OracleResultSet::class.java) else rs
val value = realrs?.getObject(columnIndex)
return try {
if (needCodec(value)) value?.decode() else value?.toString()
} catch (e: Exception) {
log.error("【数据库编码集转换】getNullableResult(rs: ResultSet?, columnIndex: Int) 失败,e:", e)
value?.toString()
}
}
override fun getNullableResult(cs: CallableStatement?, columnIndex: Int): String? {
val realcs =
if (cs?.isWrapperFor(OracleCallableStatement::class.java) == true) cs.unwrap(OracleCallableStatement::class.java) else cs
val value = realcs?.getObject(columnIndex)
return try {
if (needCodec(value)) value?.decode() else value?.toString()
} catch (e: Exception) {
log.error("【数据库编码集转换】getNullableResult(cs: CallableStatement?, columnIndex: Int) 失败,e:", e)
value?.toString()
}
}
/**
* 编码
*/
fun Any?.encode(): String? {
return StrCodec.encode(this.toString(), clientCharset, serverCharset)
}
/**
* 解码
*/
fun Any?.decode(): String? {
return StrCodec.decode(this.toString(), serverCharset, clientCharset)
}
/**
* [value]满足该条件则转码
*/
fun needCodec(value: Any?): Boolean {
val currentDbKey = DynamicDataSourceContextHolder.peek()
?: "master" //如果是null,则赋值master,表示主数据源,注意,该master和多数据源的primary无直接关联,此处想要处理主数据源,在dbNames中直接加入,master即可
return currentDbKey.isNotBlank()
&& dbKeys.contains(currentDbKey)
&& serverCharset != clientCharset
&& value != null
}
}
/**
* 用以使自定义TypeHandler替换mybatis默认的基础类型处理TypeHandler
*/
@Component
class MybatisTypeHandlerReplacer : CommandLineRunner {
@Autowired
lateinit var sqlSessionFactory: SqlSessionFactory
@Autowired
lateinit var databaseStringEncodeHandler: DatabaseStringEncodeHandler
override fun run(vararg args: String?) {
val hr = sqlSessionFactory.configuration.typeHandlerRegistry
val javaStringType = String::class.java
hr.register(javaStringType, databaseStringEncodeHandler)
hr.register(javaStringType, JdbcType.CHAR, databaseStringEncodeHandler)
hr.register(javaStringType, JdbcType.NCHAR, databaseStringEncodeHandler)
hr.register(javaStringType, JdbcType.VARCHAR, databaseStringEncodeHandler)
hr.register(javaStringType, JdbcType.NVARCHAR, databaseStringEncodeHandler)
hr.register(javaStringType, JdbcType.LONGVARCHAR, databaseStringEncodeHandler)
hr.register(javaStringType, JdbcType.LONGNVARCHAR, databaseStringEncodeHandler)
}
}
以上
不正经吐槽:)
oracle数据库可真是个小可爱!
当前文章说了这么多,也算是记录问题解决的过程,也算是备忘,能遇上这样的问题的系统已经不多了,特别是在使用oracle 10g这远古版本的系统,要对接点啥,那本文可能、也许、大概对你是有帮助的。
最后,感谢观读,有疑问或其他建议请在评论区留言,下期再会~