emoji表情的处理与存储
一、故事开始
在一个安静的下午,小明在公司值班,监控项目是否稳定运行。突然报警群里来了一条报警提示
这条提示最主要的内容是: Cause: java.sql.SQLException: #HY000
经过排查是因为修改昵称的时候传入了特殊字符(emoji表情)
😁😊🙂🙂🙂👦🏿
对应的编码:\uD83D\uDE01\uD83D\uDE0A\uD83D\uDE42\uD83D\uDE42\uD83D\uDE42\uD83D\uDC66\uD83C\uDFFF
怎么办,报错了,影响客户使用了,要扣绩效了,说时迟那时快,聪明的小明想到了,直接修改昵称字段的编码,从utf8改成utf8mb4 ,utf8mb4可以承载emoji表情,抓紧写了修改编码语句,提交了工单,联系数据库管理员审批通过,问题暂时解决。
二、 复盘
异常发生当天晚上,领导假总,召集他的得利干将小亮、小明开复盘会。
经过大家激烈的讨论,出了一个方案
第一条:禁止所有接口传递emoji表情 需要Filter实现
第二条:需要emoji表情的接口Filter放行,不用utf8mb4来解决这个问题,用转义来解决,存储的时候进行转义,读取的时候进行反转义。
这个艰巨的任务领导分配给了小组灵魂人物小亮。
三、修改前代码
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>emoji</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>emoji</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml
server:
port: 8888
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf-8&useSSL=false
username: root
password: 123456
mybatis:
type-aliases-package: com.example.dao.entity
configuration:
map-underscore-to-camel-case: true
EmojiController
package com.example.controller;
import com.example.entity.Person;
import com.example.service.EmojiService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class EmojiController {
@Autowired
EmojiService service;
@PostMapping(value = "/person/save")
Person Person(@RequestBody Person p){
return service.save(p);
}
@PostMapping(value = "/person/updateNickName")
void updateNickName(@RequestBody Person p){
service.updateNickName(p);
}
}
EmojiService
package com.example.service;
import com.example.dao.PersonMapper;
import com.example.entity.Person;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class EmojiService {
@Autowired
PersonMapper mapper;
public Person save(Person p) {
mapper.save(p);
return p;
}
public void updateNickName(Person p) {
mapper.updateNickName(p.getNickname(), p.getId());
}
}
PersonMapper
package com.example.dao;
import com.example.entity.Person;
import org.apache.ibatis.annotations.*;
@Mapper
public interface PersonMapper {
@Insert(value = "INSERT INTO person " +
"(name,nickname,age,gender) " +
"VALUES (#{name},#{nickname},#{age},#{gender} )")
@SelectKey(statement = "select last_insert_id()",
keyProperty = "id",
before = false,
resultType = Long.class)
void save(Person p) ;
@Update(value = "update person set nickname=#{nickname} where id = #{id}")
void updateNickName(@Param("nickname") String nickname, @Param("id") Long id);
}
Person
package com.example.entity;
import lombok.Data;
@Data
public class Person {
private Long id;
private String name;
private String nickname;
private Integer age;
private String gender;
}
EmojiApplicationTests 测试
package com.example.emoji;
import com.alibaba.fastjson.JSONObject;
import org.apache.ibatis.annotations.Param;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
class EmojiApplicationTests {
@Autowired
protected MockMvc mockMvc;
@Test
void insert() throws Exception {
JSONObject params = new JSONObject();
params.put("name", "刘强国");
params.put("nickname", "小锅锅");
params.put("age", "18");
params.put("gender", "男");
String res = mockMvc.perform(post("/person/save")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(params.toString()))
.andReturn().getResponse().getContentAsString();
JSONObject parse = JSONObject.parseObject(res);
System.out.println("插入数据自增主键ID=" + parse.get("id"));
}
@Test
void updateNickName() throws Exception {
JSONObject params = new JSONObject();
params.put("id", "15");
params.put("nickname","\uD83D\uDE01\uD83D\uDE0A\uD83D\uDE42\uD83D\uDE42\uD83D\uDE42\uD83D\uDC66\uD83C\uDFFF");
mockMvc.perform(post("/person/updateNickName")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(params.toString()))
.andExpect(status().isOk());
}
}
四、 添加过滤器
大概流程是这样的:
pom.xml添加 感谢我们的老外朋友开发的这个实用、高级、牛逼的API
<dependency>
<groupId>com.vdurmont</groupId>
<artifactId>emoji-java</artifactId>
<version>5.1.1</version>
</dependency>
Filter
有些配置,比如白名单、过滤类型等开发的时候一般会写成配置文件的方式,我这里为了能够一目了然的看到,所以写了个类变量来实用。
package com.example.filter;
import com.vdurmont.emoji.EmojiParser;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;
import java.util.*;
@Component
@Slf4j
public class EmojiFilter implements Filter {
List<String> urls = Arrays.asList(
"/person/save",
"/person/updateNickName"
);
List<String> mediaTypeList = Arrays.asList(
"application/x-www-form-urlencoded",
"application/json",
"application/xml"
);
@Override
public void init(FilterConfig filterConfig) {
log.info("init...");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String method = request.getMethod();
String contentType = request.getContentType();
boolean flag = !HttpMethod.GET.name().equals(method.toUpperCase())
&& mediaTypeList.contains(contentType == null ? null : contentType.toLowerCase());
if (!flag) {
log.debug("无需处理 method={} contentType={}", method, contentType);
filterChain.doFilter(servletRequest, servletResponse);
return;
}
String srvPath = request.getServletPath();
boolean count = urls.stream().filter(e -> srvPath.startsWith(e)).findAny().isPresent();
if (count) {
log.debug("在白名单中 直接放行 srvPath={}", srvPath);
filterChain.doFilter(servletRequest, servletResponse);
return;
}
filterChain.doFilter(new ReqWrapper(request, "UTF-8"), servletResponse);
}
@Override
public void destroy() {
log.info("destroy...");
}
public static class ReqWrapper extends HttpServletRequestWrapper {
private final String charset;
public ReqWrapper(HttpServletRequest request, String charset) {
super(request);
this.charset = charset;
}
@Override
public ServletInputStream getInputStream() throws IOException {
ServletInputStream inputStream = super.getInputStream();
try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, charset))) {
String line = null;
StringBuilder result = new StringBuilder();
while ((line = br.readLine()) != null) {
result.append(clean(line));
}
return new ServletInputStreamWrapper(new ByteArrayInputStream(result.toString()
.getBytes(Charset.forName(charset))));
}
}
@Override
public String getParameter(String name) {
return clean(super.getParameter(name));
}
@Override
public String[] getParameterValues(String name) {
return clean(super.getParameterValues(name));
}
@Override
public Map<String, String[]> getParameterMap() {
return clean(super.getParameterMap());
}
private Map<String, String[]> clean(Map<String, String[]> map) {
if (map == null) {
return map;
}
Map<String, String[]> result = new LinkedHashMap<>();
for (Map.Entry<String, String[]> me : map.entrySet()) {
result.put(me.getKey(), clean(me.getValue()));
}
return Collections.unmodifiableMap(result);
}
private String[] clean(String[] arr) {
if (arr != null) {
for (int i = 0; i < arr.length; i++) {
arr[i] = clean(arr[i]);
}
}
return arr;
}
private String clean(String val) {
if (StringUtils.isEmpty(val)) {
return val;
}
return EmojiParser.removeAllEmojis(val);
}
}
public static class ServletInputStreamWrapper extends ServletInputStream {
public ServletInputStreamWrapper(ByteArrayInputStream stream) {
this.stream = stream;
}
private InputStream stream;
@Override
public boolean isFinished() {
return true;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() throws IOException {
return stream.read();
}
}
}
EmojiController
package com.example.controller;
import com.example.entity.Person;
import com.example.service.EmojiService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
public class EmojiController {
@Autowired
EmojiService service;
@PostMapping(value = "/person/save")
Person Person(@RequestBody Person p){
return service.save(p);
}
@PostMapping(value = "/person/updateNickName")
void updateNickName(@RequestBody Person p){
service.updateNickName(p);
}
@GetMapping(value = "/person/{id}" ,produces = "application/json; charset=utf-8")
Person get(@PathVariable(value = "id") Long id){
return service.get(id);
}
@PostMapping(value = "/person/test")
void PersonTest(@RequestBody Person p){
System.out.println(p.getNickname());
}
}
EmojiService
业务代码中如果需要转码就用EmojiParser.parseToAliases
反转码就用EmojiParser.parseToUnicode
package com.example.service;
import com.example.dao.PersonMapper;
import com.example.entity.Person;
import com.vdurmont.emoji.EmojiManager;
import com.vdurmont.emoji.EmojiParser;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class EmojiService {
@Autowired
PersonMapper mapper;
public Person save(Person p) {
if (StringUtils.isNoneBlank(p.getNickname())){
System.out.println("昵称转换前:"+p.getNickname());
p.setNickname(EmojiParser.parseToAliases(p.getNickname()));
System.out.println("昵称转换前:"+p.getNickname());
}
mapper.save(p);
return p;
}
public void updateNickName(Person p) {
if (StringUtils.isNoneBlank(p.getNickname())){
System.out.println("昵称转换前:"+p.getNickname());
p.setNickname(EmojiParser.parseToAliases(p.getNickname()));
System.out.println("昵称转换前:"+p.getNickname());
}
mapper.updateNickName(p.getNickname(), p.getId());
}
public Person get(Long id) {
Person p = mapper.get(id);
if (StringUtils.isNoneBlank(p.getNickname())) {
System.out.println("昵称转换前:"+p.getNickname());
p.setNickname(EmojiParser.parseToUnicode(p.getNickname()));
System.out.println("昵称转换后:"+p.getNickname());
}
return p;
}
}
EmojiApplicationTests
package com.example.emoji;
import com.alibaba.fastjson.JSONObject;
import com.example.entity.Person;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
class EmojiApplicationTests {
@Autowired
protected MockMvc mockMvc;
@Test
void insert() throws Exception {
JSONObject params = new JSONObject();
params.put("name", "刘强国");
params.put("nickname", "小锅锅");
params.put("age", "18");
params.put("gender", "男");
String res = mockMvc.perform(post("/person/save")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(params.toString()))
.andReturn().getResponse().getContentAsString();
JSONObject parse = JSONObject.parseObject(res);
System.out.println("插入数据自增主键ID=" + parse.get("id"));
}
@Test
void updateNickName() throws Exception {
JSONObject params = new JSONObject();
params.put("id", "15");
params.put("nickname","\uD83D\uDE01\uD83D\uDE0A\uD83D\uDE42\uD83D\uDE42\uD83D\uDE42\uD83D\uDC66\uD83C\uDFFF");
mockMvc.perform(post("/person/updateNickName")
.servletPath("/person/updateNickName")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(params.toString()))
.andExpect(status().isOk());
}
@Test
void getPerson() throws Exception {
String contentAsString = mockMvc.perform(
get("/person/15")
).andReturn().getResponse().getContentAsString();
Person p = JSONObject.parseObject(contentAsString, Person.class);
System.out.println("昵称:"+p.getNickname());
}
}
再次测试发现可以更新表情,测试/person/test 表情也会被过滤掉
主要涉及的知识是:SpringBoot 怎么使用Filter 、EmojiParser的使用
代码同样放到了github:
https://github.com/githubforliming/emoji
有问题还是老样子公众号留言:木子的昼夜编程
|