mysql中Incorrect string value亂碼問題解決方案

你是否遇到過相似如下錯誤?
java

java.sql.SQLException: Incorrect string value: '\xF0\x9F\x92\x9C' for column 'content' at row 1.mysql

產生這種異常的緣由在於,mysql中的utf8編碼最多會用3個字節存儲一個字符,若是一個字符的utf8ios

編碼佔用4個字節(最多見的就是ios中的emoji表情字符),那麼在寫入數據庫時就會報錯。spring

mysql從5.5.3版本開始,才支持4字節的utf8編碼,編碼名稱爲utf8mb4(mb4的意思是max bytes 4),這種編碼方式最多用4個字節存儲一個字符。sql

要想證實這個問題,能夠執行如下sql:數據庫

select * from
information_schema.CHARACTER_SETS
where CHARACTER_SET_NAME like 'utf8%'

結果如圖:apache

所以,要解決上述異常的發生,須要使用utf8mb4編碼。服務器


解決數據庫編碼後,還須要解決客戶端Connection鏈接對象使用的編碼問題。框架

調用建立的Connection對象執行如下sql:jvm

conn.createStatement().execute("SET names 'utf8mb4'");

若是項目中使用了DataSource數據源,只須要對數據源進行相關配置便可,這裏以apache的DBCP數據源爲例講解,在spring框架下配置以下:

<!-- 數據源 -->
	<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
		<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
		<property name="url" value="jdbc:mysql://${${data-source.prefix}.data-source.host-name}:3306/${${data-source.prefix}.data-source.db-name}?characterEncoding=utf8&amp;autoReconnect=true&amp;failOverReadOnly=false&amp;maxReconnects=10&amp;allowMultiQueries=true" />
		<property name="username" value="${${data-source.prefix}.data-source.username}" />
		<property name="password" value="${${data-source.prefix}.data-source.password}" />
		<property name="maxActive" value="150" />
		<property name="maxIdle" value="2" />
		<property name="testOnBorrow" value="true" />
		<property name="testOnReturn" value="true" />
		<property name="testWhileIdle" value="true" />
		<property name="validationQuery" value="select 1" />
		<!-- 此配置用於在建立Connection對象時執行指定的初始化sql -->
		<property name="connectionInitSqls">
			<list>
				<value>set names 'utf8mb4'</value>
			</list>
		</property>
	</bean>

如下解釋引用自mysql參考手冊:

SET NAMES 'charset_name'

SET NAMES顯示客戶端發送的SQL語句中使用什麼字符集。

所以,SET NAMES 'utf8mb4'語句告訴服務器:「未來從這個客戶端傳來的信息採用字符集utf8mb4」。它還爲服務器發送回客戶端的結果指定了字符集。(例如,若是你使用一個SELECT語句,它表示列值使用了什麼字符集。)


SET NAMES 'x'語句與這三個語句等價:


mysql> SET character_set_client = x;

mysql> SET character_set_results = x;

mysql> SET character_set_connection = x;

執行完此sql語句後,經過此鏈接對象後續建立的Statement都會成功地執行了。


講到這裏,問題已經獲得完美解決,可是我又聯想到一個新的問題:

jvm虛擬機運行時,內存中的字符串採用utf-16編碼,對於ios中的emoji表情這種用4字節utf-8編碼存儲的字符,在java運行時又是怎樣存儲的呢?

因而,我找了一個emoji字符(4個字節的值分別爲0xf0,0x9F,0x92,0x9c),作了如下試驗。

byte[] bytes = new byte[] { (byte) 0xf0, (byte) 0x9F, (byte) 0x92, (byte) 0x9c };
		String s = new String(bytes, Charset.forName("utf-8"));
		System.out.println("length:"+s.length());
		for (int i=0;i<s.length();i++) {
			int ch = s.charAt(i);
			System.out.println("0x"+Integer.toHexString(ch));
		}

執行結果以下:

由結果能夠看出,unicode值(也叫codePoint碼點,後面介紹API會用到)大於0xffff的單個字符,jvm內部佔用2個char的長度(也就是4個字節)存儲。

全部大於0xffff的字符,全都在UTF編碼表的輔助平面內(域輔助平臺對應的是基礎平面,簡稱BMP)。所以對於String中的某個char,是基礎平面字符,仍是輔助平面字符的一部分,也很好作出判斷。下面介紹java.lang.Character中的一些API:

如下描述中,碼點便是字符的unicode值

Character中API
描述
isValidCodePoint(int codePoint):boolean 判斷輸入碼點是不是有效的,全部屬於UTF定義平面的碼點都是有效的
isBmpCodePoint(int codePoint):boolean 判斷輸入碼點是否屬於基礎平面,即:0x0000~0xffff
isSupplementaryCodePoint(int codePoint):boolean 判斷輸入碼點是否屬於輔助平面,即:碼點>0xffff
isSurrogate(char ch):boolean 判斷輸入的字符是否輔助平面字符的一部分

獲取String中某個字符的碼點也很容易,調用String.codePointAt(int index):int便可。


最後,關於unicode、UCS-二、UCS-四、UTF-八、UTF-16編碼之間的關係,請讀者自行百度。文章太多了,在此就很少作介紹了。


參考資料:

  • mysql utf8mb4與emoji表情:

    http://my.oschina.net/wingyiu/blog/153357

  • 關於 MySQL UTF8 編碼下生僻字符插入失敗/假死問題的分析

    http://my.oschina.net/leejun2005/blog/343353

相關文章
相關標籤/搜索