ORACLE驅動thin方式動態切換客戶端字符集實現

 

表裏明明有這條數據,SQL語句卻查不到?
A客戶端寫入,B查詢出來的內容倒是亂碼?
在未知數據編碼的狀況下如何作到動態的切換客戶端字符集去適應變化?

    這兩個問題你們應該都遇到過,一般此類問題的根本緣由是讀寫或數據轉換中的字符集不一致,致使不一致的緣由卻有多種。數據庫字符集、客戶端字符集、session字符集,結合Java應用之上的輸入字符集,任何一個轉換操做有誤均可能致使上面的問題。下面咱們經過一些概念和知識儲備來完成1和2,從而進一步在解決3.java

    Java方式下MySQL能夠經過驅動地址上的參數以及set names來解決。那麼ORACLE是如何處理的,先來了解一下它的NLS。ORACLE做爲很是成熟的數據庫系統,它對全球化(Globalization Support)有着全面的支持,NLS包含了字符集、時區、貨幣、日期數字格式顯示, NLS運行庫提供了一套與語言無關的函數,可以正確的處理文本和字符以及語言約定操做。 這些函數在特定於語言環境下對語言和區域的行爲進行標識和加載。下圖所示服務器根據客戶端NLS設置(french和Japanese)進行加載對應的運行庫。web

這意味着客戶端的設置在此過程當中很是重要。在Java下,oracle驅動提供了兩種方式去訪問oracle數據庫:OCI和Thin。OCI須要客戶端安裝oracle客戶端,而thin的方式比較簡單,只須要依賴驅動便可使用,在此咱們僅關注thin下它的行爲。sql

其餘信息如編碼和字符集、java web和應用編碼轉換這類知識網上已經有不少透徹的信息可供查閱。數據庫

下面作一個測試來複現下上面的問題:apache

SELECT * FROM NLS_DATABASE_PARAMETERS;  //查看數據庫端NLS相關設置

   

SELECT USERENV('language') FROM DUAL;//查看用戶session級別的設置

SELECT * FROM V$NLS_PARAMETERS;

服務器端的爲US7ASCII,ascii編碼。客戶端也爲ascii編碼,即ISO8859-1。數組

這裏建立一張表,以GBK的編碼插入一條數據「白癡3號」,查詢整張表能夠看到這條數據的確存在。(正常狀況下此處查出的結果應該爲亂碼,這裏的客戶端是已經解決過編碼問題的,在確保表中數據正確的狀況下以原文本展現)服務器

可是當咱們指定該值的時候居然什麼也沒查到。session

這種狀況基本能夠肯定一件事情,就是客戶端編碼有問題!oracle的NLS不是可以自適應的麼,且上面客戶端和服務端的NLS字符集是一致的,查閱ORACLE文檔JDBC and NLS得知,thin客戶端的NLS設置很是有限,僅參考系統參數user.language來設置NLS的NLS_LANGUAGE 和NLS_TERRITORY,其餘能想到的一點就是應用編碼,經過系統參數file.encoding看到個人應用編碼是UTF8的,那麼差別就出來了,原來還和客戶端應用的字符集有關?先驗證下是否正確,使用CONVERT函數轉換字符編碼查詢,的確如此!可看結果:數據結構

那麼這個問題有解了,只須要在JVM參數上設置-Dfile.encoding便可。上圖亂碼字符的問題對應該文章第二個,如何將這個亂碼轉換爲正常字符?在thin下,oracle有單獨的處理方式,當你的數據庫編碼爲US7ASCII 或者 WE8ISO8859P1時,ORACLE不作任何轉換直接返回UCS-2數據給客戶端,若是不是這兩種,那麼在服務器端將轉換爲UTF8,而後在客戶端由thin驅動轉換爲UCS-2給應用使用,UCS-2即爲UTF-16。因此這個亂碼仍是客戶端搗鬼,原GBK的bit數據轉換爲了UTF8因而乎就亂了。這個能夠再使用CONVERT將txt轉換爲UTF8數據便可正常顯示,其實jvm參數設置好之後,這兩個問題是一塊解決掉的。如圖:oracle客戶端字符集轉換流程:oracle

Text description of nls81009.gif follows.

 

    那麼接下來咱們如何動態設置這個字符集?有這樣一種場景,咱們的應用須要鏈接的數據源編碼千奇百怪,應用設置了UTF8,GBK的數據卻不能正常查詢和使用了,難道要動態設置file.encoding。。。系統參數都是一次性初始化沒法設置。

    回過頭來想,爲何在NLS設置都已經初始化了的狀況下,數據庫依然收到錯誤的數據?爲何會和file.encoding有關?帶着這兩個問題一塊來看ORACLE的驅動究竟是怎麼作的。ORACLE的真實CONNECTION是T4CConnection,它的sql都放在一個叫作OracleSql的類中,在Statement#execute的時候,它的一個方法getSqlBytes直接推送給了數據庫,那麼這裏是如何實現的?接着往下看發現,T4CConnection的初始化中還包含了另一個很重要的數據結構:DBConversion,它包含了客戶端和服務器端的NLS配置

    

OracleSql#getSqlBytes正是由該類中的參數組合而來,當客戶端爲ASCII時,getSqlBytes的邏輯爲

public static final byte[] stringToASCII(String var0) {
        byte[] var1 = new byte[var0.length()];
        var1 = var0.getBytes();
        return var1;
    }

var0.getBytes()! 默認實現使用的就是file.encoding,因此這裏返回的就是file.encoding對應的bytes,這樣就能夠解釋通了,其實上面轉換流程這張圖中「JDBC-Thin(Calling Java socket in Java)」以前還和平臺自己的編碼有關。

       瞭解到了這裏咱們就很清楚的知道如何動態設置了,目標就是oracle.jdbc.driver.DBConversion#clientCharSet

/**
* oracle.jdbc.driver.DBConversion 轉換bytes的入口 
**/    
public byte[] StringToCharBytes(String var1) throws SQLException {
        if(var1.length() == 0) {
            return null;
        } else {
            switch(this.clientCharSetId) {
            case -1:
                return this.serverCharSet.convertWithReplacement(var1);
            case 2:
            case 31:
            case 178:
                return this.clientCharSet.convertWithReplacement(var1);
            default:
                return stringToDriverCharBytes(var1, this.clientCharSetId);
            }
        }
    }

在ACSII下走的是default路徑,即由應用自己的編碼控制,這樣有很大的不肯定性。那麼咱們要動態設置解就要執行case 2&31&178,同時設置clientCharSet。還有一些要準備的環境數據:

    客戶端全球化支持須要依賴orai18n.jar,pom依賴以下

<dependency>
            <groupId>oracle.i18n</groupId>
            <artifactId>orai18n</artifactId>
            <version>12.1.0</version>
        </dependency>

準備好了就開搞了。這時另一個問題來了,oracle的驅動非開源額,怎麼辦,對外也沒有這些數據的設置入口,只能使用「黑科技」:Java proxy了。

    梳理下流程:

            應用層提供設置目標數據庫編碼入口。

            開啓客戶端編碼模式。

            設置客戶端編碼。

            執行。

調用代碼:  

//設置prepareStatement代理                
OraclePreparedStatementProxy proxy=new OraclePreparedStatementProxy(statement,targetCharacterType);
PreparedStatement proxyStatement = (PreparedStatement) Proxy.newProxyInstance(statement.getClass().getClassLoader(), new Class[]{PreparedStatement.class}, proxy);
//使用代理獲取結果
ResultSet resultSet = proxyStatement.executeQuery();

代理實現:

import com.xxx.CharacterType;
import oracle.jdbc.OraclePreparedStatement;
import oracle.sql.CharacterSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.sql.PreparedStatement;

/**
 * Created by.
 * 類說明:代理
 */
public class OraclePreparedStatementProxy implements InvocationHandler {

    private static final Log logger = LogFactory.getLog(OraclePreparedStatementProxy.class);

    private PreparedStatement target;

    private CharacterType characterType;

    private Object clientCharSetIdTmp;

    private Object clientCharSetTmp;

    private Object conversionField;



    public OraclePreparedStatementProxy(PreparedStatement target, CharacterType characterType) {
        this.target = target;
        this.characterType = characterType;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            if (method.getName().equals("executeQuery")||method.getName().equals("executeUpdate")||method.getName().equals("close")) {
                if (target instanceof OraclePreparedStatement){
                    if (method.getName().equals("close")){//上下文依賴 清除設置
                        filterStatementCharacterSet(target,characterType,Boolean.FALSE);
                    }else {
                        filterStatementCharacterSet(target,characterType,Boolean.TRUE);
                    }
                    return method.invoke(target, args);

                }
            }
            return method.invoke(target, args);
        } catch (Exception e) {
            if (e.getCause() == null) {
                throw e;
            } else {
                throw e.getCause();
            }
        }
    }


    /**
     * 因爲oracle驅動沒法直接設置編碼 且底層使用getBytes()將數據流傳給db,嚴重依賴運行環境的編碼
     * 在這裏指定客戶端編碼來解決
     * @param oraclePreparedStatement
     */
    private void filterStatementCharacterSet(Object oraclePreparedStatement, CharacterType characterType,boolean isGet){
        try {
            if (null==characterType||characterType==CharacterType.UNKNOWN){
                return;
            }
            CharacterSet characterSet;
            switch (characterType){
                case UTF8:
                case UTF_8:
                    characterSet=CharacterSet.make(CharacterSet.UTF8_CHARSET);
                    break;
                case GB2312:
                case ISO8859_1:
                case ISO_8859_1:
                case GBK:
                    characterSet=CharacterSet.make(CharacterSet.ZHS16GBK_CHARSET);
                    break;
                default:
                    return;
            }
            Class<?> c;
            if (isGet){
                c = Class.forName("oracle.jdbc.driver.OraclePreparedStatementWrapper");
                Field preparedStatement = c.getDeclaredField("preparedStatement");
                preparedStatement.setAccessible(true);
                Object t4CPreparedStatement = preparedStatement.get(oraclePreparedStatement);

                c = Class.forName("oracle.jdbc.driver.OracleStatement");
                Field sqlObject = c.getDeclaredField("sqlObject");
                sqlObject.setAccessible(true);
                Object sqlObjectField = sqlObject.get(t4CPreparedStatement);

                c = Class.forName("oracle.jdbc.driver.OracleSql");
                Field conversion = c.getDeclaredField("conversion");
                conversion.setAccessible(true);
                conversionField = conversion.get(sqlObjectField);
            }
            c = Class.forName("oracle.jdbc.driver.DBConversion");
            Field clientCharSetId = c.getDeclaredField("clientCharSetId");
            clientCharSetId.setAccessible(true);

            if (isGet){
                //設置開啓客戶端編碼
                clientCharSetIdTmp=clientCharSetId.get(conversionField);
                clientCharSetId.setShort(conversionField, (short) 2);
            }else {
                clientCharSetId.set(conversionField, clientCharSetIdTmp);
            }

            Field clientCharSet = c.getDeclaredField("clientCharSet");
            clientCharSet.setAccessible(true);
            if (isGet){
                //設置客戶端編碼
                clientCharSetTmp=clientCharSet.get(conversionField);
                clientCharSet.set(conversionField, characterSet);
            }else {
                clientCharSet.set(conversionField, clientCharSetTmp);
            }
        } catch (Exception e) {
            logger.error("filterStatementCharacterSet error!",e);
        }

    }

}

大功告成,解決了第三個問題。

         ORACLE的NLS是一套覆蓋面很是廣、內容龐雜的知識體系,想完整的瞭解它不太容易,因此它的解決方案異常強大。字符集的問題也是老生常談,自由切換字符集是個比較新穎的操做,彷佛沒有看到有相似的解決方案,但願這篇須要相似操做的同窗。

相關文章
相關標籤/搜索