UnmappableCharacterException: Input length = 2問題解決

最近發現在生產系統上頻繁出現這個java.nio.charset.UnmappableCharacterException: Input length =2 這個報錯,於是查代碼來找出問題所在。

先貼一下關鍵的代碼:

public class MmsProtocalDecoder implements ProtocolDecoder {   

    public void decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
    	
        // 先獲取上次的處理上下文,其中可能有未處理完的數據
        Context ctx = getContext(session);
        // 先把當前buffer中的數據追加到Context的buffer當中
        ctx.append(in);
        // 把position指向0位置,把limit指向原來的position位置
        IoBuffer buf = ctx.getBuffer();
        buf.flip();
        buf.mark();
        int length = -1;
        try {
            buf.position(0);
            buf.limit(length);
            String content = buf.getString(ctx.getDecoder());
            // 寫入輸出流中,以便IoHandler進行處理
            out.write(content);
            buf.clear();
        } catch (NumberFormatException ex) {
            _log.debug("Receive head: " + length, ex);
        }
    }
}

這裏使用SocketClient來接收請求其他系統時返回的報文內容,decode方法用來解析報文。

仔細看代碼會發現,這段代碼有捕捉異常,但是不包括charset的異常,所以異常拋出到外面了。

觀察發現只有String content = buf.getString(ctx.getDeocoder());這行代碼會拋出CharacterCodingException的異常所以斷定就是這行代碼出了問題,百度查詢發現UnmappableCharacterException這個異常是客戶端的字符編碼與服務器端的字符編碼不一致時會報的。但網上搜出來的報錯大多是都是Input length =1沒有2。生產上的日誌報錯也是偶爾出現,並不是一直報錯。於是懷疑客戶端和服務器端的字符編碼是否自己發生了變化導致的。

_log.info("context charset: " + ctx.getDecoder().charset().displayName());

在上面代碼中增加日誌打印字符集的名稱,再在生產上觀察,觀察發現字符集一直是gb2312,查看代碼發現是在外層的SocketClient中寫死的。

public class XmlSocketClientImpl implements SocketClient {

    private String charset ="GB2312";

    public String sendMsg(String msg) {	
        IoConnector connector = null;
        try{
            connector = new NioSocketConnector();	
            connector.setConnectTimeoutMillis(timeOut);
            MmsProtocalCodecFactory ippProtocalCodecFactory = new MmsProtocalCodecFactory(Charset.forName(charset),packHeadLength);
            connector.getFilterChain().addLast("codec",new ProtocolCodecFilter(ippProtocalCodecFactory));
            //省略後面的代碼
            }catch(Exception e){
                _log.error("socket error:", e);
                return null;
            }
        }
    }
}

那就不是客戶端的問題,就需要觀察服務器端了,讓相關同事幫忙查,發現服務器端的字符編碼是GBK,這樣大概就猜到是什麼問題了。先來看看兩個字符集的區別,參考GB2312、GBK、GB18030 這幾種字符集的主要區別是什麼?

GB 2312 標準共收錄 6763 個漢字,其中一級漢字 3755 個,二級漢字 3008 個;同時收錄了包括拉丁字母、希臘字母、日文平假名及片假名字母、俄語西裏爾字母在內的 682 個字符。

GBK 共收入 21886 個漢字和圖形符號,包括:

  • GB 2312 中的全部漢字、非漢字符號。
  • BIG5 中的全部漢字。
  • 與 ISO 10646 相應的國家標準 GB 13000 中的其它 CJK 漢字,以上合計 20902 個漢字。
  • 其它漢字、部首、符號,共計 984 個。

GBK 向下與 GB 2312 完全兼容,向上支持 ISO 10646 國際標準,在前者向後者過渡過程中起到的承上啓下的作用。

可知,GBK是兼容GB2312的,所以大部分的中文字符兩者都有,所以當服務器端使用GBK編碼而客戶端使用GB2312編碼時,一些普通字符能正常通過,但當出現某些複雜的字符時,就會報錯。因爲系統龐大、測試不便的原因,我重新寫了一個測試程序來重現上面的報錯。

import java.io.UnsupportedEncodingException;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import org.apache.mina.core.buffer.IoBuffer;

public class Test {

    public static void main(String[] args) throws CharacterCodingException, UnsupportedEncodingException {
    Charset cs_gbk=Charset.forName("GBK");
    Charset cs_gb2312=Charset.forName("GB2312");
    IoBuffer buffer = IoBuffer.allocate(5120);
    buffer.setAutoShrink(true);
    String str = "<CustName>王大喆</CustName><Gender>M</Gender>";//測試報文爲部分內容
    //使用GBK編碼放入緩衝區
    buffer.putString(str ,cs_gbk.newEncoder());
    buffer.position(0);
    int length=str.length();
    buffer.limit(length);
    //使用GB2312編碼從緩衝區讀取
    String a = buffer.getString(cs_gb2312.newDecoder());
    }
}

執行上面的代碼就會報UnmappableCharacterException: Input length = 2這個錯,其實就是“喆”這個字用GBK編碼正常,但是用GB2312解碼就出現錯誤了。由於近來使用複雜的中文字符命名的人變多了,所以導致生產系統上報錯變多,才引起了我們的關注。一些中文字如“樑”、“禤”等的出現都會引起上面的報錯。

於是,我將客戶端的字符集改爲GBK就正常了。

 

 

 

下面說一下引起報錯的其他可能情況,一些我在重現問題時考慮的情況。

一、如果上面的寫入緩衝區使用GB2312編碼,則會報UnmappableCharacterException: Input length = 1的錯誤。因爲用GB2312沒法對“喆”這個字進行編碼。

buffer.putString(str ,cs_gb2312.newEncoder());

二、如果上面的寫入緩衝區和讀取緩衝區均使用GBK編碼,但是截取緩衝區的長度剛好截在了半個中文字符的情況,也會報錯,報UnmappableCharacterException: Input length = 1的錯誤。

    //使用GBK編碼放入緩衝區
    buffer.putString(str ,cs_gbk.newEncoder());
    buffer.position(0);
    int length=13;
    buffer.limit(length);
    //使用GB2312編碼從緩衝區讀取
    String a = buffer.getString(cs_gbk.newDecoder());

同樣情況使用GB2312編碼也會報上面的錯誤,所以截取緩衝區的長度也是關鍵影響因素。 

三、緩衝區寫入使用UTF-8編碼,讀取緩衝區使用的是GB2312還是GBK都會報UnmappableCharacterException: Input length = 2的錯誤。

 

總結上面,關於CharacterException的報錯都是由於字符編碼的不準確導致的,所以出現此類錯誤應該重點關注客戶端和服務器端的字符集是否一致,以及截取的緩衝區長度是否正確,這樣才能保證不會出錯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章