一个emoji表情包处理工具类
尊重他人成果、转载请注明出处
参考
https://github.com/vdurmont/emoji-java
业务场景
在常规如APP推送、BBS、论坛等类型的业务系统中,消息内容中包含emoji表情包(😁、💣、🦢)由于其本身的趣味性,在消息内容中引入的场景越来越普遍。
但是我们发现,若果直接存储emoji表情包,数据库通常会直接报如下异常:
Caused by: java.sql.SQLException: Incorrect string value: ‘\xF6\x8D\x78\x84’ for column ‘comment’ at row 1
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1074)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4096)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4028)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2490)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2651)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2734)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:2155)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2458)
这是因为本身并不是直接支持emoji表情包,需要通过配置字符编码方式或者其它方案才能实现emoji包存储。
目前业界有两种解决方案:一、配置mysql字符集成utf8mb4;二、对emoji表情包进行转码存储。
两种方案各有优势吧,utf8mb4方案对数据库版本有要求,仅需修改配置,无需修改代码。从根源上解决emoji表情包存储问题。emoji表情包转码方案则对数据库版本没啥要求,但是需要对表情包内容进行编码、解码操作,因此对代码侵入性高。
这两种方案根据业务场景取舍吧。
解决方案
方案一(mysql字符集)
mysql5.5版本之前,数据库中的utf-8编码只支持1~3个字节(注:数据库中的utf-8并非广义上讲的utf-8,仅是utf-8的精简版本)。从mysql5.5开始支持4个字节的utf8mb4。
emoji表情包占用4个字节,因此要求mysql支持emoji表情包,则需要配置utf8mb4编码方式。目前网上有相当丰富的资源介绍,因此本文仅简单介绍。
查看mysql当前的编码方式:
show variables like ‘%character%’;
方案二(emoji表情包转码)
针对已经成熟的线上系统或者mysql版本较老,基于数据安全及系统稳定性考虑,建议采用emoji表情包转码方案,无需修改线上数据库配置,仅对emoji表情包转码存储。
本方案对开源emoji表情包类库进行二次封装、自定义注解EmojiField、工具类EmojiUtil实现,emoji表情包快速转码处理。
工具类包结构如下图:
包括一个注解类@EmojiField、一个工具类EmojiUtil。EmojiUtil对emoji工具类库( https://github.com/vdurmont/emoji-java)进行二次封装,利用java反射特性对POJO对象中的某个string字段进行emoji表情包转码。
@EmojiFiled:
/**
* @description: 指定指端做emoji字符转换
* @author: luoping
* @date: 2022/10/21
**/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EmojiField {
}
POJO对象:
@ApiModel("资源基本信息")
@Data
public class TResourceBaseInfoVO {
@ApiModelProperty("自增id")
private Long id;
@ApiModelProperty("资源名称")
private String name;
@ApiModelProperty("广告代码")
@EmojiField
private String codes;
@ApiModelProperty("模板内容")
@EmojiField
private String templateContent;
}
EmojiUtil:
package xxx.xxx;
import com.alibaba.fastjson.JSONObject;
import com.vdurmont.emoji.EmojiManager;
import com.vdurmont.emoji.EmojiParser;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jetty.util.StringUtil;
import java.lang.reflect.Field;
/**
* @description: 表情包工具类
* @author: luoping
* @date: 2022/10/21
**/
@Slf4j
public class EmojiUtil {
/**
* 将文本中的emoji表情包转码
* @param emojiStr
* @return
*/
public static String encode(String emojiStr) {
if(StringUtil.isBlank(emojiStr)) {
return null;
}
if(!EmojiManager.containsEmoji(emojiStr)) {
return emojiStr;
}
return EmojiParser.parseToAliases(emojiStr);
}
/**
* 将文本中的emoji表情(转码后)恢复成表情包
* @param deEmojiStr
* @return
*/
public static String decode(String deEmojiStr) {
if(StringUtil.isBlank(deEmojiStr)) {
return null;
}
// if(!EmojiManager.containsEmoji(deEmojiStr)) {
// return deEmojiStr;
// }
return EmojiParser.parseToUnicode(deEmojiStr);
}
/**
* 将对象中@EmojiField修饰的字符串进行emoji表情包转码
* @param t
* @param <T>
*/
public static <T> void encode(T t) {
Field[] fields = t.getClass().getDeclaredFields();
for (Field field : fields) {
// 判断字段注解是否存在
boolean annotationPresent = field.isAnnotationPresent(EmojiField.class);
if (annotationPresent) {
if(!StringUtils.equals(field.getType().getName(), "java.lang.String")) {
// 只处理String类型
continue;
}
// 将字段中包含emoji表情包的内容进行转码赋值
field.setAccessible(true);
try {
String srcStr = (String)field.get(t);
field.set(t, encode(srcStr));
}catch (Exception e) {
log.error("EmojiUtil encode error: {}", JSONObject.toJSONString(t), e);
}
field.setAccessible(false);
}
}
}
/**
* 将对象中@EmojiField修饰的字符串进行emoji表情包解码
* @param t
* @param <T>
*/
public static <T> void decode(T t) {
Field[] fields = t.getClass().getDeclaredFields();
for (Field field : fields) {
// 判断字段注解是否存在
boolean annotationPresent = field.isAnnotationPresent(EmojiField.class);
if (annotationPresent) {
if(!StringUtils.equals(field.getType().getName(), "java.lang.String")) {
// 只处理String类型
continue;
}
// 将字段中包含emoji表情包的内容进行转码赋值
field.setAccessible(true);
try {
String srcStr = (String)field.get(t);
field.set(t, decode(srcStr));
}catch (Exception e) {
log.error("EmojiUtil decode error: {}", JSONObject.toJSONString(t), e);
}
field.setAccessible(false);
}
}
}
/**
* 是否包含emoji表情包
* @param str
* @return
*/
public static boolean containEmoji(String str) {
return EmojiManager.containsEmoji(str);
}
}
单元测试:
@Test
public void testEmojiUtil() {
TResourceBaseInfoAddReqVO vo = new TResourceBaseInfoAddReqVO();
vo.setId(1000l);
vo.setCodes("哈喽emoji😁");
vo.setTemplateContent("😁💣");
System.out.println("编码前:");
System.out.println(JSONObject.toJSONString(vo));
EmojiUtil.encode(vo);
System.out.println("编码后:");
System.out.println(JSONObject.toJSONString(vo));
EmojiUtil.decode(vo);
System.out.println("解码后:");
System.out.println(JSONObject.toJSONString(vo));
}
运行结果: