mysql中Incorrect string value亂碼問題解決方案
你是否遇到過類似以下錯誤?
java.sql.SQLException: Incorrect string value: '\xF0\x9F\x92\x9C' for column 'content' at row 1.
產生這種異常的原因在於,mysql中的utf8編碼最多會用3個字節存儲一個字符,如果一個字符的utf8
編碼占用4個字節(最常見的就是ios中的emoji表情字符),那么在寫入數據庫時就會報錯。
mysql從5.5.3版本開始,才支持4字節的utf8編碼,編碼名稱為utf8mb4(mb4的意思是max bytes 4),這種編碼方式最多用4個字節存儲一個字符。
要想證明這個問題,可以執行以下sql:
select * from information_schema.CHARACTER_SETS where CHARACTER_SET_NAME like 'utf8%'
結果如圖:
因此,要解決上述異常的發生,需要使用utf8mb4編碼。
解決數據庫編碼后,還需要解決客戶端Connection連接對象使用的編碼問題。
調用創建的Connection對象執行以下sql:
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&autoReconnect=true&failOverReadOnly=false&maxReconnects=10&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-2、UCS-4、UTF-8、UTF-16編碼之間的關系,請讀者自行百度。文章太多了,在此就不多做介紹了。
參考資料:
-
mysql utf8mb4與emoji表情:
http://my.oschina.net/wingyiu/blog/153357
-
關於 MySQL UTF8 編碼下生僻字符插入失敗/假死問題的分析