Spring MVC 大战 Emoji 表情

一、前言

由于自己在维护一个 APP 的后端项目,于是乎就有了这个坑(数据库不支持 Emoji 表情,需要额外设置),目前的解决思路是把 Emoji 转化成可以存储到数据库的字符。

科普一下

  • MySQL 的 UTF-8 编码的一个字符最多 3 个字节,但是一个 Emoji 表情为 4 个字节,所以 UTF-8 不支持存储 Emoji 表情。但是 UTF-8 的超集utf8mb4 一个字符最多能有 4 字节,所以能支持 Emoji 表情的存储。

二、版本1:Emoji 转化成 UTF-8 编码

/**
 * 对 emoji 表情编码转换的工具类
 *
 * @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表情转换成可以在utf-8字符集数据库中保存的格式(表情占4个字节,需要utf8mb4字符集)
     *
     * @param str 待转换字符串
     * @return 转换后字符串
     */
    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数据库中保存的含转换后emoji表情的字符串
     *
     * @param str 转换后的字符串
     * @return 转换前的字符串
     */
    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();
    }

}

😁😭😄 经处理后:

[[%F0%9F%98%9A]][[%F0%9F%98%9C]][[%F0%9F%98%9F]]

三、版本2:Emoji 转化成字符 :smile:

使用大神的 emoji-java 这个库可以在代码段解决这个问题,解决思路:

  • 页面有一个表情😁,在经过处理之后可以是😄,将这个字符存入数据库
  • 读取的时候可以将😄这个字符转为😁

例如: 😁 我可以存储为:smile:,😭存储为:cry:,等等,可以这样映射起来。映射规则 emojis.json

3.1 编写工具类

/**
 * 对 emoji 表情编码转换的工具类
 *
 * @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); 将表情符号转为字符
  • EmojiParser.parseToUnicode(string); 将字符转为表情符号

工具都好了,接下来就是和 Spring MVC 谈一下合作了

四、接入 Spring MVC

目前有了工具还不够,怎么用呢?每个地方都调用一次吗?APP 请求了后端在 Controller 中调用 EmojiUtils.toUnicode(string)?然后在调用查询业务时再调用 EmojiUtils.toAliases(string)?这样感觉好大的代码量啊,瞬间想回家种田了。梦醒了生活还是要继续的,到底能不能在某个地方统一处理一下啊?可以的,Spring MVC 中给我们提供了一系列的方案。

  • 数据在服务器玩了一圈,而我们要做的是在它 进门的时候将表情符号转为字符,而在它要离开时 返回数据将字符转为表情符号
  • 入口:OncePerRequestFilter过滤器
  • 出口:HttpMessageConverter消息转换器

4.1 自定义过滤器

这里统一处理提交的数据,如果有 Emoji 就进行转换,下一步才交给 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) {
                // 参数名
                // 返回值之前 先进行 Emoji 转化
                return EmojiUtils.toAliases(super.getParameter(name));
            }

            @Override
            public String[] getParameterValues(String name) {
                // 参数值
                // 返回值之前 先进行 Emoji 转化
                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 自定义消息转换器

Spring 给我们提供了很多处理不同类型数据的转换器,思路来自 使用自定义HttpMessageConverter对返回内容进行加密

貌似是从 4.1 起才引入的GsonHttpMessageConverter,而刚好这个项目也是使用 Gson,又省了一步,现在我们要做的如下

/**
 * @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);
    }
}

主要是要提供一个按我们风格来的 Gson 实例super.setGson(GsonBuilderUtils.create())(果然是一流的大公司,提供一个可定制性这么高的 JSON 框架 👍),只有你想不到没有你定制不了的,哈哈

/**
 * 构建自定义 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")// 设置日期时间格式
        //.setPrettyPrinting()// 格式化输出 json 数据
        ;

        Gson gson = gb.create();

        return gson;
    }

}

/* *********************** 把时间转成最原始的Long型. Gson默认的是不支持的, 需要手动处理一下. *********************** */

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 表情转化. ********************************************* */

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());
    }

}

这个使用 GsonBuilder 定制的 Gson 主要是将 Date 转换成 Long 类型,对字符串中的已被转换的 Emoji 表情进行 将字符转为表情符号

4.2.1 配置消息转换器

spring-mvc.xml

<mvc:annotation-driven>
    <mvc:message-converters>
        <!-- 输出对象转 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>

五、检验效果

至此,我们在入口和出口都统一安排了,接下来就让我们来检验一下吧

Controller
Postman
log

可见 Emoji 已经被转换成字符了,这时还没有返回数据,只是在方法里打印了,可以看到这时日期还是默认的方式,还没被转成 long 类型,下一步让我们来见证奇迹的发生吧

json

成功了,数据到我们项目中玩了一圈出来后完好无损(但在这途中,可是让我们下了药的🚳,动了手脚,它们却混然不知,嘿嘿嘿),日期类型也按我们的要求转化成了 long 类型,这样定义好后,除了在业务的特殊处理的要求,基本上我们就不用管了,一位到位,而不是每个地方都要复制一边代码,哈哈。

参考文献:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,494评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,678评论 6 342
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,493评论 18 399
  • spring官方文档:http://docs.spring.io/spring/docs/current/spri...
    牛马风情阅读 1,635评论 0 3
  • 我今天抽了一支烟。烟是一个男孩离开的那天给我的,他抽很多烟。我酝酿好多天,今天终于得以实施。我没有打火机,...
    Scarlett阅读 483评论 1 1