🔍 乱码根源

US7ASCII字符集不支持中文:这是导致乱码的根本原因。您需要将数据库字符集更改为支持中文的字符集(如ZHS16GBK或AL32UTF8),或通过其他方式“绕开”这个限制。

💡 解决方案概览

下面的流程图可以帮助您快速了解并选择适合的解决方案:

flowchart TD
    A[Oracle中文乱码问题] --> B{解决方案}
    
    B --> C[方案一:修改数据库字符集]
    B --> D[方案二:配置连接池转码]
    B --> E[方案三:应用层手动转码]
    
    C --> C1[彻底解决]
    D --> D1[无需改动DB]
    E --> E1[代码侵入性强]

🛠️ 方案详解

方案一:修改数据库字符集(推荐)

这是最彻底的解决方案,但操作有风险,请务必在修改前备份数据库

  1. 查询当前字符集

    SELECT * FROM NLS_DATABASE_PARAMETERS where PARAMETER='NLS_CHARACTERSET';
    
  2. 修改为支持中文的字符集,以SYSDBA身份执行以下命令:

    -- 以SYSDBA身份登录
    sqlplus / as sysdba
    
    -- 关闭数据库
    shutdown immediate;
    
    -- 以mount方式启动
    startup mount;
    
    -- 设置会话为受限模式
    ALTER SYSTEM ENABLE RESTRICTED SESSION;
    ALTER SYSTEM SET JOB_QUEUE_PROCESSES=0;
    ALTER SYSTEM SET AQ_TM_PROCESSES=0;
    
    -- 打开数据库
    alter database open;
    
    -- 强制修改字符集为ZHS16GBK
    ALTER DATABASE CHARACTER SET INTERNAL_USE ZHS16GBK;
    -- 或者修改为UTF-8
    -- ALTER DATABASE CHARACTER SET INTERNAL_USE AL32UTF8;
    
    -- 再次关闭数据库
    shutdown immediate;
    
    -- 正常启动数据库
    startup;
    

    注意:直接修改字符集可能存在风险,如果上述方法不成功,可能需要重建数据库并导入数据。

方案二:配置Druid进行连接层转码(临时解决)

如果无法立即修改数据库字符集,可以尝试通过Druid连接池在获取连接时进行编码转换。

  • application.propertiesapplication.yml中配置Druid数据源,利用过滤器进行转码:
    # 数据库连接配置
    spring.datasource.druid.driver-class-name=oracle.jdbc.OracleDriver
    spring.datasource.druid.url=jdbc:oracle:thin:@localhost:1521:orcl
    spring.datasource.druid.username=your_username
    spring.datasource.druid.password=your_password
    
    # 配置监控统计拦截的filters,用于转码
    spring.datasource.druid.filters=stat,wall,encoding
    
    或者在创建连接时指定编码属性:
    Properties props = new Properties();
    props.setProperty("serverEncoding", "ISO-8859-1"); // 假设数据库返回的是ISO-8859-1编码
    props.setProperty("clientEncoding", "GBK"); // 或UTF-8,转换为应用需要的编码
    props.put("user", username);
    props.put("password", password);
    Connection conn = DriverManager.getConnection(url, props);
    
    注意:此方法依赖于Druid的编码过滤器,效果可能因环境而异,属于临时方案。
方案三:应用层手动转码(备选)

在前两种方法都难以实施时,可以在代码层面对乱码数据进行手动转换。

  • 在MyBatis映射器或结果处理中转换:假设数据库返回的乱码字符串是由于将GBK编码的字节流错误地用ISO-8859-1解码所致,可以尝试逆向转换:
    // 示例:将乱码字符串恢复
    public String fixMessyText(String messyText) {
        try {
            if (messyText == null) return null;
            // 假设乱码是因为Oracle US7ASCII环境下的错误解码,尝试逆转换
            byte[] bytes = messyText.getBytes("ISO-8859-1"); 
            return new String(bytes, "GBK"); // 或尝试"UTF-8"
        } catch (java.io.UnsupportedEncodingException e) {
            e.printStackTrace();
            return messyText; // 转换失败返回原字符串
        }
    }
    
    注意:此方法不推荐作为首选,因为它具有很强的侵入性,且转换逻辑依赖于特定的错误编码假设,可能不通用。

💎 总结与建议

  1. 强烈推荐方案一:从根源上解决问题,一劳永逸。务必做好备份。
  2. 临时考虑方案二:如果数据库字符集暂时无法修改,可以尝试配置Druid。
  3. 谨慎使用方案三:作为最后的补救措施。
  4. 检查其他配置:确保您的Spring Boot应用全局使用UTF-8编码。在application.properties中检查:
    spring.http.encoding.force=true
    spring.http.encoding.charset=UTF-8
    spring.http.encoding.enabled=true
    server.tomcat.uri-encoding=UTF-8
    

方案四:自定义MyBatis TypeHandler(推荐)

这是最优雅的解决方案,通过在MyBatis类型处理器层面统一处理编码转换。

1. 创建自定义String类型处理器

@Component
@MappedTypes(String.class)
@MappedJdbcTypes({JdbcType.VARCHAR, JdbcType.CLOB, JdbcType.NVARCHAR})
public class OracleStringTypeHandler extends BaseTypeHandler<String> {
    
    private static final String ORACLE_ENCODING = "ISO-8859-1";
    private static final String TARGET_ENCODING = "GBK"; // 或者 "UTF-8"
    
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, 
                                  String parameter, JdbcType jdbcType) throws SQLException {
        // 写入数据库时转换
        try {
            String converted = convertToOracle(parameter);
            ps.setString(i, converted);
        } catch (UnsupportedEncodingException e) {
            ps.setString(i, parameter); // 转换失败使用原值
        }
    }
    
    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        // 从数据库读取时转换
        String value = rs.getString(columnName);
        return convertFromOracle(value);
    }
    
    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String value = rs.getString(columnIndex);
        return convertFromOracle(value);
    }
    
    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String value = cs.getString(columnIndex);
        return convertFromOracle(value);
    }
    
    private String convertFromOracle(String oracleValue) {
        if (oracleValue == null) return null;
        try {
            // 将Oracle返回的乱码转换回正常中文
            byte[] bytes = oracleValue.getBytes(ORACLE_ENCODING);
            return new String(bytes, TARGET_ENCODING);
        } catch (UnsupportedEncodingException e) {
            return oracleValue;
        }
    }
    
    private String convertToOracle(String normalValue) throws UnsupportedEncodingException {
        if (normalValue == null) return null;
        // 将正常中文转换为Oracle能正确存储的格式
        byte[] bytes = normalValue.getBytes(TARGET_ENCODING);
        return new String(bytes, ORACLE_ENCODING);
    }
}

2. 配置MyBatis使用自定义TypeHandler

方式一:在配置类中注册

@Configuration
@MapperScan(basePackages = "com.yourpackage.mapper")
public class MyBatisConfig {
    
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        
        // 注册自定义TypeHandler
        org.apache.ibatis.session.Configuration configuration = 
            new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.getTypeHandlerRegistry().register(OracleStringTypeHandler.class);
        
        sessionFactory.setConfiguration(configuration);
        return sessionFactory.getObject();
    }
}

方式二:在application.yml中配置

mybatis:
  type-handlers-package: com.yourpackage.handler
  configuration:
    map-underscore-to-camel-case: true

方案五:自定义Druid过滤器

创建一个Druid过滤器,在连接层面处理编码问题:

@Component
public class OracleEncodingFilter extends FilterAdapter {
    
    private static final String ORACLE_ENCODING = "ISO-8859-1";
    private static final String TARGET_ENCODING = "GBK";
    
    @Override
    public ResultSet resultSet_getString(FilterChain chain, ResultProxy result, 
                                       int columnIndex) throws SQLException {
        String value = chain.resultSet_getString(result, columnIndex);
        return convertString(value);
    }
    
    @Override
    public ResultSet resultSet_getString(FilterChain chain, ResultProxy result, 
                                       String columnLabel) throws SQLException {
        String value = chain.resultSet_getString(result, columnLabel);
        return convertString(value);
    }
    
    private ResultSet convertString(String value) {
        if (value == null) return null;
        try {
            byte[] bytes = value.getBytes(ORACLE_ENCODING);
            String converted = new String(bytes, TARGET_ENCODING);
            // 这里需要返回包装后的ResultSet,实际实现会更复杂
            // 简化示例,实际需要完整的ResultSet包装
            return null;
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }
}

方案六:Spring AOP切面处理(侵入性最低)

使用AOP在DAO层方法执行前后自动处理编码:

@Aspect
@Component
public class OracleEncodingAspect {
    
    private static final String ORACLE_ENCODING = "ISO-8859-1";
    private static final String TARGET_ENCODING = "GBK";
    
    // 拦截所有Mapper的查询方法
    @Around("execution(* com.yourpackage.mapper.*.*(..))")
    public Object handleEncoding(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = joinPoint.proceed();
        
        // 处理返回结果中的字符串编码
        return processResultEncoding(result);
    }
    
    private Object processResultEncoding(Object result) {
        if (result == null) return null;
        
        if (result instanceof String) {
            return convertFromOracle((String) result);
        } else if (result instanceof List) {
            return ((List<?>) result).stream()
                    .map(this::processResultEncoding)
                    .collect(Collectors.toList());
        } else if (result instanceof Map) {
            return ((Map<?, ?>) result).entrySet().stream()
                    .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        entry -> processResultEncoding(entry.getValue())
                    ));
        }
        
        // 如果是实体对象,通过反射处理所有String字段
        return processEntityEncoding(result);
    }
    
    private Object processEntityEncoding(Object entity) {
        if (entity == null) return null;
        
        Class<?> clazz = entity.getClass();
        Arrays.stream(clazz.getDeclaredFields())
                .filter(field -> field.getType().equals(String.class))
                .forEach(field -> {
                    try {
                        field.setAccessible(true);
                        String value = (String) field.get(entity);
                        if (value != null) {
                            field.set(entity, convertFromOracle(value));
                        }
                    } catch (IllegalAccessException e) {
                        // 忽略无法访问的字段
                    }
                });
        return entity;
    }
    
    private String convertFromOracle(String oracleValue) {
        try {
            byte[] bytes = oracleValue.getBytes(ORACLE_ENCODING);
            return new String(bytes, TARGET_ENCODING);
        } catch (UnsupportedEncodingException e) {
            return oracleValue;
        }
    }
}

推荐使用方案

强烈推荐方案四(自定义TypeHandler),因为:

  1. 侵入性最低:只需要配置一次,所有String字段自动处理
  2. MyBatis原生支持:符合MyBatis扩展机制
  3. 性能良好:在数据访问层统一处理,避免重复代码
  4. 维护简单:编码逻辑集中在一处

配置步骤:

  1. 创建OracleStringTypeHandler
  2. 在MyBatis配置中注册该处理器
  3. 测试编码转换效果(可能需要调整ORACLE_ENCODINGTARGET_ENCODING
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐