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的报错都是由于字符编码的不准确导致的,所以出现此类错误应该重点关注客户端和服务器端的字符集是否一致,以及截取的缓冲区长度是否正确,这样才能保证不会出错。

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