Java はクロスプラットフォーム言語であり、異なるプラットフォームでのコーディングの切り替えが頻繁に行われるため、特に Java ではコーディングの問題が常にプログラム開発者を悩ませてきました。次に、Java エンコードの問題の根本的な原因、Java でよく発生するいくつかのエンコード形式の違い、Java Web の開発で発生する可能性のあるエンコードの問題の原因の分析、 HTTP リクエストのエンコード形式を制御する方法、中国語エンコードの問題を回避する方法など。
コンピュータに情報を保存する最小単位は 1 バイト、つまり 8 ビットなので、表現できる文字の範囲は 0 ~ 255 です。
表現するにはシンボルが多すぎるため、1 バイトで完全に表現することはできません。
コンピュータはさまざまな変換方式を提供しており、一般的な変換方式には ASCII、ISO-8859-1、GB2312、GBK、UTF-8、UTF-16 などが含まれます。これらはすべて変換ルールを規定しており、これらのルールに従ってコンピュータは文字を正しく表現できます。これらのエンコード形式を以下に紹介します:
ASCIIコード
合計128個あり、1バイトの下位7ビットで表され、0~31は改行、復帰、削除などの制御文字、32~126はキーボードから入力できる印刷文字であり、表示されます。
ISO-8859-1
128 文字では明らかに不十分であるため、ISO 組織は ASCII に基づいて拡張され、ISO-8859-1 から ISO-8859-15 までがカバーされ、最も広く使用されています。 ISO-8859-1 は依然としてシングルバイトエンコーディングであり、合計 256 文字を表現できます。
GB2312
これは 2 バイト エンコードであり、合計のエンコード範囲は A1 ~ F7 です。A1 ~ A9 はシンボル領域で、合計 682 個のシンボルが含まれます。B0 ~ F7 は漢字領域で、6763 個の漢字が含まれます。
GBk
GBK は GB2312 を拡張した「漢字内部コード拡張仕様」であり、そのエンコード範囲は 8140 ~ FEFE (XX7F を除く) で、合計 21003 個の漢字を表現できます。 GB2312のエンコーディングで文字化けしません。
UTF-16
これは、コンピュータ内で Unicode 文字にアクセスする方法を具体的に定義します。 UTF-16 は、Unicode 変換形式を表すために 2 バイトを使用します。つまり、どの文字であっても 2 バイトで表されます。 2 バイトが 16 ビットであるため、UTF-16 と呼ばれます。文字を表すのに非常に便利です。2 バイトで 1 文字を表すことができないため、文字列操作が大幅に簡素化されます。
UTF-8
UTF-16 では一律 2 バイトで文字を表現するのが簡単で便利ですが、大部分の文字は 1 バイトで表現できるため、2 バイトで表現すると記憶容量が 2 倍になります。ネットワーク帯域幅が制限されている場合、ネットワーク送信のトラフィックが増加するという問題があります。 UTF-8 は可変長テクノロジーを使用しており、各エンコード領域は異なる文字長を 1 ~ 6 バイトで構成できます。
UTF-8 には次のエンコード規則があります:
1バイトで最上位ビット(8ビット目)が0ならASCII文字(00~7F)ということになります
11から始まる1バイトの場合、連続する1の数がその文字のバイト数を示します
10 から始まる 1 バイトの場合、それは最初のバイトではないことを意味し、現在の文字の最初のバイトを取得するまで待つ必要があります
上に示すように、Reader クラスは Java I/O で文字を読み取るための親クラスであり、InputStream クラスはバイトを読み取るための親クラスであり、バイトを文字に関連付けるブリッジです。 I/O プロセス中に、読み取られたバイトの文字への変換を処理し、StreamDecoder のデコード プロセス中に、特定のバイトの文字へのデコードの実装を StreamDecoder に委託します。ユーザーは Charset エンコード形式を指定する必要があります。 Charset を指定しない場合、ローカル環境のデフォルトの文字セットが使用されることに注意してください。たとえば、中国語環境では GBK エンコーディングが使用されます。
たとえば、次のコードはファイルの読み取りおよび書き込み関数を実装します:
String file = "c:/stream.txt"; String charset = "UTF-8"; // 写字符换转成字节流 FileOutputStream outputStream = new FileOutputStream(file); OutputStreamWriter writer = new OutputStreamWriter( outputStream, charset); try { writer.write("这是要保存的中文字符"); } finally { writer.close(); } // 读取字节转换成字符 FileInputStream inputStream = new FileInputStream(file); InputStreamReader reader = new InputStreamReader( inputStream, charset); StringBuffer buffer = new StringBuffer(); char[] buf = new char[64]; int count = 0; try { while ((count = reader.read(buf)) != -1) { buffer.append(buffer, 0, count); } } finally { reader.close(); }
アプリケーションに I/O 操作が含まれる場合、統一されたエンコードおよびデコード Charset 文字セットの指定に注意を払っている限り、通常はコードの文字化けの問題は発生しません。
メモリ内で文字からバイトへのデータ型変換を実行します。
1、String 类提供字符串转换到字节的方法,也支持将字节转换成字符串的构造函数。
String s = "字符串"; byte[] b = s.getBytes("UTF-8"); String n = new String(b, "UTF-8");
2、Charset 提供 encode 与 decode,分别对应 char[] 到 byte[] 的编码 和 byte[] 到 char[] 的解码。
Charset charset = Charset.forName("UTF-8"); ByteBuffer byteBuffer = charset.encode(string); CharBuffer charBuffer = charset.decode(byteBuffer);
...
Java 编码类图
首先根据指定的 charsetName 通过 Charset.forName(charsetName) 设置 Charset 类,然后根据 Charset 创建 CharsetEncoder 对象,再调用 CharsetEncoder.encode 对字符串进行编码,不同的编码类型都会对应到一个类中,实际的编码过程是在这些类中完成的。下面是 String. getBytes(charsetName) 编码过程的时序图
Java 编码时序图
从上图可以看出根据 charsetName 找到 Charset 类,然后根据这个字符集编码生成 CharsetEncoder,这个类是所有字符编码的父类,针对不同的字符编码集在其子类中定义了如何实现编码,有了 CharsetEncoder 对象后就可以调用 encode 方法去实现编码了。这个是 String.getBytes 编码方法,其它的如 StreamEncoder 中也是类似的方式。
经常会出现中文变成“?”很可能就是错误的使用了 ISO-8859-1 这个编码导致的。中文字符经过 ISO-8859-1 编码会丢失信息,通常我们称之为“黑洞”,它会把不认识的字符吸收掉。由于现在大部分基础的 Java 框架或系统默认的字符集编码都是 ISO-8859-1,所以很容易出现乱码问题,后面将会分析不同的乱码形式是怎么出现的。
对中文字符后面四种编码格式都能处理,GB2312 与 GBK 编码规则类似,但是 GBK 范围更大,它能处理所有汉字字符,所以 GB2312 与 GBK 比较应该选择 GBK。UTF-16 与 UTF-8 都是处理 Unicode 编码,它们的编码规则不太相同,相对来说 UTF-16 编码效率最高,字符到字节相互转换更简单,进行字符串操作也更好。它适合在本地磁盘和内存之间使用,可以进行字符和字节之间快速切换,如 Java 的内存编码就是采用 UTF-16 编码。但是它不适合在网络之间传输,因为网络传输容易损坏字节流,一旦字节流损坏将很难恢复,想比较而言 UTF-8 更适合网络传输,对 ASCII 字符采用单字节存储,另外单个字符损坏也不会影响后面其它字符,在编码效率上介于 GBK 和 UTF-16 之间,所以 UTF-8 在编码效率上和编码安全性上做了平衡,是理想的中文编码方式。
对于使用中文来说,有 I/O 的地方就会涉及到编码,前面已经提到了 I/O 操作会引起编码,而大部分 I/O 引起的乱码都是网络 I/O,因为现在几乎所有的应用程序都涉及到网络操作,而数据经过网络传输都是以字节为单位的,所以所有的数据都必须能够被序列化为字节。在 Java 中数据被序列化必须继承 Serializable 接口。
一段文本它的实际大小应该怎么计算,我曾经碰到过一个问题:就是要想办法压缩 Cookie 大小,减少网络传输量,当时有选择不同的压缩算法,发现压缩后字符数是减少了,但是并没有减少字节数。所谓的压缩只是将多个单字节字符通过编码转变成一个多字节字符。减少的是 String.length(),而并没有减少最终的字节数。例如将“ab”两个字符通过某种编码转变成一个奇怪的字符,虽然字符数从两个变成一个,但是如果采用 UTF-8 编码这个奇怪的字符最后经过编码可能又会变成三个或更多的字节。同样的道理比如整型数字 1234567 如果当成字符来存储,采用 UTF-8 来编码占用 7 个 byte,采用 UTF-16 编码将会占用 14 个 byte,但是把它当成 int 型数字来存储只需要 4 个 byte 来存储。所以看一段文本的大小,看字符本身的长度是没有意义的,即使是一样的字符采用不同的编码最终存储的大小也会不同,所以从字符到字节一定要看编码类型。
我们能够看到的汉字都是以字符形式出现的,例如在 Java 中“淘宝”两个字符,它在计算机中的数值 10 进制是 28120 和 23453,16 进制是 6bd8 和 5d9d,也就是这两个字符是由这两个数字唯一表示的。Java 中一个 char 是 16 个 bit 相当于两个字节,所以两个汉字用 char 表示在内存中占用相当于四个字节的空间。
这两个问题搞清楚后,我们看一下 Java Web 中那些地方可能会存在编码转换?
用户从浏览器端发起一个 HTTP 请求,需要存在编码的地方是 URL、Cookie、Parameter。服务器端接受到 HTTP 请求后要解析 HTTP 协议,其中 URI、Cookie 和 POST 表单参数需要解码,服务器端可能还需要读取数据库中的数据,本地或网络中其它地方的文本文件,这些数据都可能存在编码问题,当 Servlet 处理完所有请求的数据后,需要将这些数据再编码通过 Socket 发送到用户请求的浏览器里,再经过浏览器解码成为文本。这些过程如下图所示:
一次 HTTP 请求的编码示例
用户提交一个 URL,这个 URL 中可能存在中文,因此需要编码,如何对这个 URL 进行编码?根据什么规则来编码?有如何来解码?如下图一个 URL:
上图中以 Tomcat 作为 Servlet Engine 为例,它们分别对应到下面这些配置文件中:
Port 对应在 Tomcat 的
<servlet-mapping> <servlet-name>junshanExample</servlet-name> <url-pattern>/servlets/servlet/*</url-pattern> </servlet-mapping>
上图中 PathInfo 和 QueryString 出现了中文,当我们在浏览器中直接输入这个 URL 时,在浏览器端和服务端会如何编码和解析这个 URL 呢?为了验证浏览器是怎么编码 URL 的我选择的是360极速浏览器并通过 Postman 插件观察我们请求的 URL 的实际的内容,以下是 URL:
HTTP://localhost:8080/examples/servlets/servlet/君山?author=君山
君山的编码结果是:e5 90 9b e5 b1 b1,和《深入分析 Java Web 技术内幕》中的结果不一样,这是因为我使用的浏览器和插件和原作者是有区别的,那么这些浏览器之间的默认编码是不一样的,原文中的结果是:
君山的编码结果分别是:e5 90 9b e5 b1 b1,be fd c9 bd,查阅上一届的编码可知,PathInfo 是 UTF-8 编码而 QueryString 是经过 GBK 编码,至于为什么会有“%”?查阅 URL 的编码规范 RFC3986 可知浏览器编码 URL 是将非 ASCII 字符按照某种编码格式编码成 16 进制数字然后将每个 16 进制表示的字节前加上“%”,所以最终的 URL 就成了上图的格式了。
从上面测试结果可知浏览器对 PathInfo 和 QueryString 的编码是不一样的,不同浏览器对 PathInfo 也可能不一样,这就对服务器的解码造成很大的困难,下面我们以 Tomcat 为例看一下,Tomcat 接受到这个 URL 是如何解码的。
解析请求的 URL 是在 org.apache.coyote.HTTP11.InternalInputBuffer 的 parseRequestLine 方法中,这个方法把传过来的 URL 的 byte[] 设置到 org.apache.coyote.Request 的相应的属性中。这里的 URL 仍然是 byte 格式,转成 char 是在 org.apache.catalina.connector.CoyoteAdapter 的 convertURI 方法中完成的:
protected void convertURI(MessageBytes uri, Request request) throws Exception { ByteChunk bc = uri.getByteChunk(); int length = bc.getLength(); CharChunk cc = uri.getCharChunk(); cc.allocate(length, -1); String enc = connector.getURIEncoding(); if (enc != null) { B2CConverter conv = request.getURIConverter(); try { if (conv == null) { conv = new B2CConverter(enc); request.setURIConverter(conv); } } catch (IOException e) {...} if (conv != null) { try { conv.convert(bc, cc, cc.getBuffer().length - cc.getEnd()); uri.setChars(cc.getBuffer(), cc.getStart(), cc.getLength()); return; } catch (IOException e) {...} } } // Default encoding: fast conversion byte[] bbuf = bc.getBuffer(); char[] cbuf = cc.getBuffer(); int start = bc.getStart(); for (int i = 0; i < length; i++) { cbuf[i] = (char) (bbuf[i + start] & 0xff); } uri.setChars(cbuf, 0, length); }
从上面的代码中可以知道对 URL 的 URI 部分进行解码的字符集是在 connector 的
QueryStringを解析するにはどうすればよいですか? GET HTTP リクエストの QueryString と POST HTTP リクエストのフォームパラメータは Parameters として保存され、パラメータ値は request.getParameter を通じて取得されます。これらは、request.getParameter メソッドが初めて呼び出されたときにデコードされます。 request.getParameter メソッドが呼び出されると、org.apache.catalina.connector.Request の parseParameters メソッドが呼び出されます。このメソッドは GET および POST によって渡されたパラメータをデコードしますが、それらのデコード文字セットは異なる場合があります。 POST フォームのデコードについては後で紹介します。QueryString のデコード文字セットはどこで定義されていますか?これは HTTP ヘッダーを介してサーバーに送信され、URL にも含まれます。これは URI のデコード文字セットと同じですか?以前のブラウザでは PathInfo と QueryString に異なるエンコード形式が使用されていたことから、デコードされた文字セットは確実に一貫性がないことが推測できます。実際、QueryString のデコード文字セットは、ヘッダーの ContentType で定義された Charset またはデフォルトの ISO-8859-1 のいずれかです。ContentType で定義されたエンコーディングを使用するには、コネクタの
上記の URL のエンコードとデコードのプロセスから判断すると、これは比較的複雑であり、アプリケーションでエンコードとデコードを完全に制御できるわけではありません。そのため、アプリケーションの URL で非 ASCII 文字を使用することは避ける必要があります。もちろん、文字化けが発生する可能性があります。
クライアントが HTTP リクエストを開始するとき、上記の URL に加えて、Cookie や redirectPath などの他のパラメータもヘッダーで渡すことがあります。これらのユーザー設定値には、Tomcat がどのようにデコードするかというエンコードの問題も発生する可能性があります。 ?
ヘッダー内の項目のデコードは、request.getHeader を呼び出すことによっても実行されます。要求されたヘッダー項目がデコードされない場合は、MessageBytes の toString メソッドが呼び出され、バイトから文字への変換に使用されるデフォルトのエンコードも ISO-8859 です。 -1. であり、ヘッダーの他のデコード形式を設定することはできないため、ヘッダーに非 ASCII 文字のデコードを設定すると、確実に文字化けが発生します。
ヘッダーを追加する場合も同様です。ヘッダーに非 ASCII 文字を渡さないでください。渡す必要がある場合は、まず org.apache.catalina.util.URLEncoder でこれらの文字をエンコードしてからヘッダーに追加します。この方法では、ブラウザからサーバーへの転送中に情報が失われることはありません。これらの項目にアクセスするときに、対応する文字セットに従って情報をデコードできれば便利です。
前述したように、POST フォームによって送信されたパラメーターのデコードは、request.getParameter が初めて呼び出されたときに行われます。POST フォームのパラメーター転送メソッドは、HTTP の BODY を通じてサーバーに渡されます。ページ上の送信ボタンをクリックすると、ブラウザはまず ContentType の Charset エンコード形式に従ってフォームに入力されたパラメータをエンコードし、次にサーバーもデコードに ContentType の文字セットを使用します。したがって、POST フォームを通じて送信されたパラメータには通常問題はなく、文字セットのエンコーディングは独自に設定され、request.setCharacterEncoding(charset) を通じて設定できます。
さらに、multipart/form-data タイプのパラメーターの場合、つまり、アップロードされたファイルのエンコーディングも、ContentType で定義された文字セット エンコーディングを使用します。アップロードされたファイルは、バイト単位でサーバーのローカル一時ディレクトリに送信されることに注意してください。このプロセスには文字エンコーディングは含まれませんが、実際のエンコーディングでは、このエンコーディングを使用してエンコードできない場合は、デフォルトのエンコーディング ISO-8859-1 が使用されます。
ユーザーが要求したリソースが正常に取得されると、コンテンツは応答を通じてクライアント ブラウザーに返されます。このプロセスは、ブラウザーによって最初にエンコードされ、次にデコードされる必要があります。このプロセスのエンコードおよびデコード文字セットは、response.setCharacterEncoding を通じて設定できます。これは request.getCharacterEncoding の値をオーバーライドし、ブラウザが返されたソケット ストリームを受信すると、ヘッダーの Content-Type を通じてクライアントに返します。返された HTTP ヘッダーの Content-Type に文字セットが設定されていない場合、ブラウザは
除了 URL 和参数编码问题外,在服务端还有很多地方可能存在编码,如可能需要读取 xml、velocity 模版引擎、JSP 或者从数据库读取数据等。
xml 文件可以通过设置头来制定编码格式
<?xml version="1.0" encoding="UTF-8"?>
Velocity 模版设置编码格式:
services.VelocityService.input.encoding=UTF-8
JSP 设置编码格式:
<%@page contentType="text/html; charset=UTF-8"%>
访问数据库都是通过客户端 JDBC 驱动来完成,用 JDBC 来存取数据要和数据的内置编码保持一致,可以通过设置 JDBC URL 来制定如 MySQL:url="jdbc:mysql://localhost:3306/DB?useUnicode=true&characterEncoding=GBK"。
下面看一下,当我们碰到一些乱码时,应该怎么处理这些问题?出现乱码问题唯一的原因都是在 char 到 byte 或 byte 到 char 转换中编码和解码的字符集不一致导致的,由于往往一次操作涉及到多次编解码,所以出现乱码时很难查找到底是哪个环节出现了问题,下面就几种常见的现象进行分析。
例如,字符串“淘!我喜欢!”变成了“Ì Ô £ ¡Î Ò Ï²»¶ £ ¡”编码过程如下图所示:
字符串在解码时所用的字符集与编码字符集不一致导致汉字变成了看不懂的乱码,而且是一个汉字字符变成两个乱码字符。
例如,字符串“淘!我喜欢!”变成了“??????”编码过程如下图所示:
将中文和中文符号经过不支持中文的 ISO-8859-1 编码后,所有字符变成了“?”,这是因为用 ISO-8859-1 进行编解码时遇到不在码值范围内的字符时统一用 3f 表示,这也就是通常所说的“黑洞”,所有 ISO-8859-1 不认识的字符都变成了“?”。
例如,字符串“淘!我喜欢!”变成了“????????????”编码过程如下图所示:
这种情况比较复杂,中文经过多次编码,但是其中有一次编码或者解码不对仍然会出现中文字符变成“?”现象,出现这种情况要仔细查看中间的编码环节,找出出现编码错误的地方。
还有一种情况是在我们通过 request.getParameter 获取参数值时,当我们直接调用
String value = request.getParameter(name); 会出现乱码,但是如果用下面的方式
String value = String(request.getParameter(name).getBytes(" ISO-8859-1"), "GBK");
解析时取得的 value 会是正确的汉字字符,这种情况是怎么造成的呢?
看下如所示:
这种情况是这样的,ISO-8859-1 字符集的编码范围是 0000-00FF,正好和一个字节的编码范围相对应。这种特性保证了使用 ISO-8859-1 进行编码和解码可以保持编码数值“不变”。虽然中文字符在经过网络传输时,被错误地“拆”成了两个欧洲字符,但由于输出时也是用 ISO-8859-1,结果被“拆”开的中文字的两半又被合并在一起,从而又刚好组成了一个正确的汉字。虽然最终能取得正确的汉字,但是还是不建议用这种不正常的方式取得参数值,因为这中间增加了一次额外的编码与解码,这种情况出现乱码时因为 Tomcat 的配置文件中 useBodyEncodingForURI 配置项没有设置为”true”,从而造成第一次解析式用 ISO-8859-1 来解析才造成乱码的。
本文首先总结了几种常见编码格式的区别,然后介绍了支持中文的几种编码格式,并比较了它们的使用场景。接着介绍了 Java 那些地方会涉及到编码问题,已经 Java 中如何对编码的支持。并以网络 I/O 为例重点介绍了 HTTP 请求中的存在编码的地方,以及 Tomcat 对 HTTP 协议的解析,最后分析了我们平常遇到的乱码问题出现的原因。
要約すると、中国語の問題を解決するには、まず、文字からバイトへのエンコードとバイトから文字へのデコードがどこで行われるか、最も一般的な場所はデータの読み取りとディスクへの保存、またはネットワークを通過するデータを把握する必要があります。 。 伝染 ; 感染。次に、これらの場所については、これらのデータを操作するフレームワークまたはシステムがエンコードをどのように制御するかを理解し、エンコード形式を正しく設定し、ソフトウェアまたはオペレーティング システム プラットフォームのデフォルトのエンコード形式の使用を避けます。
注: 記事のほとんどは書籍「Insider Java Web Technology」の第 3 章に言及しています。転載する場合は必ず出典を明記してください。
以上がJava Web における中国語エンコーディングの問題の詳細な分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。