一、前言
由于自己在維護(hù)一個(gè) APP 的后端項(xiàng)目,于是乎就有了這個(gè)坑(數(shù)據(jù)庫不支持 Emoji 表情,需要額外設(shè)置),目前的解決思路是把 Emoji 轉(zhuǎn)化成可以存儲(chǔ)到數(shù)據(jù)庫的字符。
科普一下
- MySQL 的 UTF-8 編碼的一個(gè)字符最多 3 個(gè)字節(jié),但是一個(gè) Emoji 表情為 4 個(gè)字節(jié),所以 UTF-8 不支持存儲(chǔ) Emoji 表情。但是 UTF-8 的超集utf8mb4 一個(gè)字符最多能有 4 字節(jié),所以能支持 Emoji 表情的存儲(chǔ)。
二、版本1:Emoji 轉(zhuǎn)化成 UTF-8 編碼
/**
* 對(duì) emoji 表情編碼轉(zhuǎn)換的工具類
*
* @author ybin
* @since 2017-02-19
*/
public class EmojiUtils {
private static final Logger LOG = LoggerFactory.getLogger(EmojiUtils.class);
/**
* 編碼格式
*/
private static final String ENCODING = "UTF-8";
private EmojiUtils() {
throw new UnsupportedOperationException("u can't instantiate me...");
}
/**
* 將字符串中的emoji表情轉(zhuǎn)換成可以在utf-8字符集數(shù)據(jù)庫中保存的格式(表情占4個(gè)字節(jié),需要utf8mb4字符集)
*
* @param str 待轉(zhuǎn)換字符串
* @return 轉(zhuǎn)換后字符串
*/
public static String emojiConvert(String str) {
if (str == null) return "";
String patternString = "([\\x{10000}-\\x{10ffff}\ud800-\udfff])";
Pattern pattern = Pattern.compile(patternString);
Matcher matcher = pattern.matcher(str);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
try {
matcher.appendReplacement(sb, "[[" + URLEncoder.encode(matcher.group(1), ENCODING) + "]]");
} catch (UnsupportedEncodingException e) {
LOG.error("emoji convert fail", e);
return str;
}
}
matcher.appendTail(sb);
return sb.toString();
}
/**
* 還原utf8數(shù)據(jù)庫中保存的含轉(zhuǎn)換后emoji表情的字符串
*
* @param str 轉(zhuǎn)換后的字符串
* @return 轉(zhuǎn)換前的字符串
*/
public static String emojiRecovery(String str) {
String patternString = "\\[\\[(.*?)\\]\\]";
Pattern pattern = Pattern.compile(patternString);
Matcher matcher = pattern.matcher(str);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
try {
matcher.appendReplacement(sb, URLDecoder.decode(matcher.group(1), ENCODING));
} catch (UnsupportedEncodingException e) {
LOG.error("emoji recovery fail", e);
return str;
}
}
matcher.appendTail(sb);
return sb.toString();
}
}
?????? 經(jīng)處理后:
[[%F0%9F%98%9A]][[%F0%9F%98%9C]][[%F0%9F%98%9F]]
三、版本2:Emoji 轉(zhuǎn)化成字符 :smile:
使用大神的 emoji-java 這個(gè)庫可以在代碼段解決這個(gè)問題,解決思路:
- 頁面有一個(gè)表情??,在經(jīng)過處理之后可以是??,將這個(gè)字符存入數(shù)據(jù)庫
- 讀取的時(shí)候可以將??這個(gè)字符轉(zhuǎn)為??
例如: ?? 我可以存儲(chǔ)為:smile:,??存儲(chǔ)為:cry:,等等,可以這樣映射起來。映射規(guī)則 emojis.json
3.1 編寫工具類
/**
* 對(duì) emoji 表情編碼轉(zhuǎn)換的工具類
*
* @author ybin
* @since 2017-04-14
*/
public class EmojiUtils {
private static final Logger logger = LoggerFactory.getLogger(EmojiUtils.class);
private EmojiUtils() {
throw new UnsupportedOperationException("u can't instantiate me...");
}
/**
* Replaces the emoji's unicode occurrences by one of their alias
* (between 2 ':').
*
* @param input the string to parse
*
* @return the string with the emojis replaced by their alias.
*/
public static String toAliases(String input) {
return EmojiParser.parseToAliases(input);
}
/**
* Replaces the emoji's aliases (between 2 ':') occurrences and the html
* representations by their unicode.
*
* @param input the string to parse
*
* @return the string with the emojis replaced by their alias.
*/
public static String toUnicode(String input) {
return EmojiParser.parseToUnicode(input);
}
}
1、引入 Maven 依賴
<dependency>
<groupId>com.vdurmont</groupId>
<artifactId>emoji-java</artifactId>
<version>3.2.0</version>
</dependency>
2、常用 API 使用
- EmojiParser.parseToAliases(string); 將表情符號(hào)轉(zhuǎn)為字符
- EmojiParser.parseToUnicode(string); 將字符轉(zhuǎn)為表情符號(hào)
工具都好了,接下來就是和 Spring MVC 談一下合作了
四、接入 Spring MVC
目前有了工具還不夠,怎么用呢?每個(gè)地方都調(diào)用一次嗎?APP 請(qǐng)求了后端在 Controller 中調(diào)用 EmojiUtils.toUnicode(string)?然后在調(diào)用查詢業(yè)務(wù)時(shí)再調(diào)用 EmojiUtils.toAliases(string)?這樣感覺好大的代碼量啊,瞬間想回家種田了。夢(mèng)醒了生活還是要繼續(xù)的,到底能不能在某個(gè)地方統(tǒng)一處理一下啊?可以的,Spring MVC 中給我們提供了一系列的方案。
- 數(shù)據(jù)在服務(wù)器玩了一圈,而我們要做的是在它 進(jìn)門的時(shí)候
將表情符號(hào)轉(zhuǎn)為字符,而在它要離開時(shí) 返回?cái)?shù)據(jù)將字符轉(zhuǎn)為表情符號(hào) - 入口:
OncePerRequestFilter過濾器 - 出口:
HttpMessageConverter消息轉(zhuǎn)換器
4.1 自定義過濾器
這里統(tǒng)一處理提交的數(shù)據(jù),如果有 Emoji 就進(jìn)行轉(zhuǎn)換,下一步才交給 Controller 處理。
/**
* Emoji's 編碼過濾器
*
* @author ybin
* @since 2017-04-14
*/
public class EmojiEncodingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request = new HttpServletRequestWrapper(request) {
@Override
public String getParameter(String name) {
// 參數(shù)名
// 返回值之前 先進(jìn)行 Emoji 轉(zhuǎn)化
return EmojiUtils.toAliases(super.getParameter(name));
}
@Override
public String[] getParameterValues(String name) {
// 參數(shù)值
// 返回值之前 先進(jìn)行 Emoji 轉(zhuǎn)化
String[] values = super.getParameterValues(name);
if (values != null) {
for (int i = 0; i < values.length; i++) {
values[i] = EmojiUtils.toAliases(values[i]);
}
}
return values;
}
};
filterChain.doFilter(request, response);
}
}
4.1.1 配置過濾器
web.xml
<!-- Emoji 編碼過濾器 -->
<filter>
<filter-name>emojiEncodingFilter</filter-name>
<filter-class>org.spring.web.filter.EmojiEncodingFilter</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>emojiEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
4.2 自定義消息轉(zhuǎn)換器
Spring 給我們提供了很多處理不同類型數(shù)據(jù)的轉(zhuǎn)換器,思路來自 使用自定義HttpMessageConverter對(duì)返回內(nèi)容進(jìn)行加密

貌似是從 4.1 起才引入的GsonHttpMessageConverter,而剛好這個(gè)項(xiàng)目也是使用 Gson,又省了一步,現(xiàn)在我們要做的如下
/**
* @author ybin
* @since 2017-04-14
*/
public class JSONHttpMessageConverter extends GsonHttpMessageConverter {
public JSONHttpMessageConverter() {
super.setGson(GsonBuilderUtils.create());
}
@Override
protected void writeInternal(Object o, Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
super.writeInternal(o, type, outputMessage);
}
}
主要是要提供一個(gè)按我們風(fēng)格來的 Gson 實(shí)例super.setGson(GsonBuilderUtils.create())(果然是一流的大公司,提供一個(gè)可定制性這么高的 JSON 框架 ??),只有你想不到?jīng)]有你定制不了的,哈哈
/**
* 構(gòu)建自定義 Gson 的工具類
*
* @author ybin
* @since 2017-02-19
*/
public class GsonBuilderUtils {
public static Gson create() {
GsonBuilder gb = new GsonBuilder();
gb.registerTypeAdapter(Date.class, new DateSerializer()).setDateFormat(DateFormat.LONG);
gb.registerTypeAdapter(Date.class, new DateDeserializer()).setDateFormat(DateFormat.LONG);
gb.registerTypeAdapter(String.class, new EmojiSerializer());
gb.registerTypeAdapter(String.class, new EmojiDeserializer());
gb = gb
//.serializeNulls()// 序列化null
//.setDateFormat("yyyy-MM-dd HH:mm:ss")// 設(shè)置日期時(shí)間格式
//.setPrettyPrinting()// 格式化輸出 json 數(shù)據(jù)
;
Gson gson = gb.create();
return gson;
}
}
/* *********************** 把時(shí)間轉(zhuǎn)成最原始的Long型. Gson默認(rèn)的是不支持的, 需要手動(dòng)處理一下. *********************** */
class DateSerializer implements JsonSerializer<Date> {
public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.getTime());
}
}
class DateDeserializer implements JsonDeserializer<Date> {
public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
return new Date(json.getAsJsonPrimitive().getAsLong());
}
}
/* ********************************************* Emoji 表情轉(zhuǎn)化. ********************************************* */
class EmojiSerializer implements JsonSerializer<String> {
public JsonElement serialize(String src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(EmojiUtils.toUnicode(src));
}
}
class EmojiDeserializer implements JsonDeserializer<String> {
public String deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
return EmojiUtils.toAliases(json.getAsString());
}
}
這個(gè)使用 GsonBuilder 定制的 Gson 主要是將 Date 轉(zhuǎn)換成 Long 類型,對(duì)字符串中的已被轉(zhuǎn)換的 Emoji 表情進(jìn)行 將字符轉(zhuǎn)為表情符號(hào)。
4.2.1 配置消息轉(zhuǎn)換器
spring-mvc.xml
<mvc:annotation-driven>
<mvc:message-converters>
<!-- 輸出對(duì)象轉(zhuǎn) JSON 支持 -->
<bean id="jsonHttpMessageConverter" class="org.spring.http.converter.json.JSONHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/json;charset=UTF-8</value>
<value>application/json;charset=UTF-8</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
五、檢驗(yàn)效果
至此,我們?cè)谌肟诤统隹诙冀y(tǒng)一安排了,接下來就讓我們來檢驗(yàn)一下吧



可見 Emoji 已經(jīng)被轉(zhuǎn)換成字符了,這時(shí)還沒有返回?cái)?shù)據(jù),只是在方法里打印了,可以看到這時(shí)日期還是默認(rèn)的方式,還沒被轉(zhuǎn)成 long 類型,下一步讓我們來見證奇跡的發(fā)生吧

成功了,數(shù)據(jù)到我們項(xiàng)目中玩了一圈出來后完好無損(但在這途中,可是讓我們下了藥的??,動(dòng)了手腳,它們卻混然不知,嘿嘿嘿),日期類型也按我們的要求轉(zhuǎn)化成了 long 類型,這樣定義好后,除了在業(yè)務(wù)的特殊處理的要求,基本上我們就不用管了,一位到位,而不是每個(gè)地方都要復(fù)制一邊代碼,哈哈。
參考文獻(xiàn):