本文分享在 Springboot 中解决 Emoji 存储的几个办法。其中常见方式为很多人互相借鉴参考的方案,其他方式为博主脑抽后给出的诡异方案,其中 ObjectMapper方案主要参考Springmvc请求参数的优雅处方式

另外,另一篇博客 中的两幅图对这个问题有一定帮助

常见方式

以下两种为通过搜索可以很容易找到的解决办法

  1. 直接修改数据库配置,通过修改字符集编码支持 Emoji 编码。
  • 优势:对业务代码没有影响
  • 劣势:开销大
  1. 在过滤器上使用第三方类包提供的转换方法,每一次请求中对请求进行转化
  • 优势:对业务代码影响较小,只需编写过滤器定义过滤规则即可
  • 因为是对请求(响应)全文进行过滤,所以支持对字段名的转化

其他方式

以下方式为另辟蹊径的两种方式。

包装字符串类

  1. 包装一个字符串类,替换 String 存储可能含有 Emoji 的字段
  2. 为其建立转换函数,在存储前转化为 unicode,读取后转回 Emoji。

  • 优势:开销较小,只在需要的字段上进行转化,无关内容一律不管。
  • 劣势:下文给出的实现比较弱智,代码耦合度较大,程序中需要用到该字段的地方都会受到影响,暂未想到针对这种办法的优化。
/********** 字符串类 (EmojiSupportedString.java) ***********/
import lombok.AllArgsConstructor;
import lombok.Data;

// 支持 Emoji 的字符串,String 不可继承,通过包装类实现
@Data
@AllArgsConstructor
public class EmojiSupportedString {

    /**
     * 可能包含 Emoji 等四字节字符的字符串
     */
    private String value;

    @Override
    public String toString() {
        return value;
    }
}

/********** 转化器类 (EmojiConverter.java) ***********/

import cn.hutool.extra.emoji.EmojiUtil;

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter(autoApply = true)
public class EmojiConverter implements AttributeConverter<EmojiSupportedString, String> {
    @Override
    public String convertToDatabaseColumn(EmojiSupportedString attribute) {
        // 使用 Hutool 实现转化
        return EmojiUtil.toAlias(attribute.getValue());
    }

    @Override
    public EmojiSupportedString convertToEntityAttribute(String dbData) {
        return new EmojiSupportedString(EmojiUtil.toUnicode(dbData));
    }
}

/************** 实体类举例 **********/
@Entity
@Data
@Table(name="comment")
public class Comment {
    @Column(name = "content")
    EmojiSupportedString content;
}

自定义 ObjectMapper 实现转化

折腾了一段时间没有得到想要的实现,准备继续使用 Filter 方案时,一篇文章让我有了另一种思路,即使用 ObjectMapper 针对请求体 Json 实现转化。

但是下文的实现不支持对参数中字符串的转化,对非接口式(如直接返回页面)的请求也不支持 那TM还支持啥 适合用在提供纯接口服务的后端程序。

如果需要,也可以通过扩展 WebBindingInitializer 的方式实现针对参数的支持。(下文思路基于文章开头给出的参考实现,如有需要,可以参考)

  • 优势:只针对请求中的字段值进行转化,开销较全文转化更小
  • 优势:需要将自定义 ObjectMapper 配置到 Spring 中,其他业务代码中,无需关心 Emoji
  • 劣势:支持的范围受限,仅在发生 Json 转化的过程中有效

实现如下:

/******* 配置类,关键部分,WebMvcAutoConfiguration.java *********/
@Configuration
public class WebMvcAutoConfiguration extends WebMvcConfigurationSupport {

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.stream()
                .filter(c -> c instanceof MappingJackson2HttpMessageConverter)
                .findFirst()
                .ifPresent(converter -> {
                    // 添加自定义序列化模块,使用类似的思想可以定义其他行为,比如自定义分页字段等
                    MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = (MappingJackson2HttpMessageConverter) converter;
                    Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
                    JsonComponentModule module = new JsonComponentModule();
                    module.addSerializer(new PageJacksonSerializer());  // 定制分页的 json 序列化行为
                    ObjectMapper objectMapper = builder.modules(module, new EmojiSupportSimpleModule()).build();
                    mappingJackson2HttpMessageConverter.setObjectMapper(objectMapper);
                });
    }
}

/******* 自定义 ObjectMapper 转化模块 ***************/


import cn.hutool.extra.emoji.EmojiUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;

import java.io.IOException;

public class EmojiSupportSimpleModule extends SimpleModule {
    {
        // Emoji 转化为别名
        addDeserializer(String.class, new StdDeserializer<>(String.class) {
            @Override
            public String deserialize(JsonParser p, DeserializationContext
                    ctxt) throws IOException, JsonProcessingException {
                return EmojiUtil.toAlias(p.getValueAsString());
            }
        });
        // 别名转回 Emoji
        addSerializer(String.class, new StdSerializer<>(String.class) {
            @Override
            public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
                gen.writeString(EmojiUtil.toUnicode(value));
            }
        });
    }
}