IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 不要再发表情了,程序员绩效都被扣没了 -> 正文阅读

[Java知识库]不要再发表情了,程序员绩效都被扣没了

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/> <!-- lookup parent from repository -->
    </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>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>
        <!--lombok 一款还不错的副主编程工具-->
        <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;

/**
 * @author 发现更多精彩  关注公众号:木子的昼夜编程
 * 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
 */
@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;

/**
 * @author 发现更多精彩  关注公众号:木子的昼夜编程
 * 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
 */
@Service
public class EmojiService {

    @Autowired
    PersonMapper mapper;

    // 新增数据
    public Person save(Person p) {
        mapper.save(p);
        return p;
    }

    // 根据ID更新昵称数据
    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.*;

/**
 * @author 发现更多精彩  关注公众号:木子的昼夜编程
 * 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
 */
@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) ;

    // 根据ID更新昵称
    @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;

/**
 * @author 发现更多精彩  关注公众号:木子的昼夜编程
 * 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
 */
@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"); // isnert方法输出了自增主键ID
        // emoji表情 会报错
	 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

<!--emoji处理-->
<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.*;

/**
 * @author 发现更多精彩  关注公众号:木子的昼夜编程
 * 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
 * @create 2021-09-12 11:26
 */
@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 {
        // 1. 获取请求方式、请求类型
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String method = request.getMethod();
        String contentType = request.getContentType();
        // 2. 只过滤非get请求 并且 contentType类型在mediaTypeList中包含的请求
        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;
        }
        // 3. 处理白名单
        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;
        }
        // 4. 处理表情 咦?怎么就一句话 对喽大部分的Filter只需要这里一句话
        // 重要的代码是怎么包装Request 和 Response  我觉得可以叫他装饰模式也可以叫他代理模式
        // 这里的UTF-8编码模式 也需要写到配置文件或者配置中心 我这里还是为了方便大家看 所以写死在代码
        filterChain.doFilter(new ReqWrapper(request, "UTF-8"), servletResponse);
    }


    @Override
    public void destroy() {
        log.info("destroy...");
    }

    // 包装request 常用套路
    public static class ReqWrapper extends HttpServletRequestWrapper {
        // 编码格式
        private final String charset;
        // 构造
        public ReqWrapper(HttpServletRequest request, String charset) {
            super(request);
            this.charset = charset;
        }

        // 这个方法最重要 大部分的项目都是用@RequestBody接收参数 那时候就会用getInputStream 就是在这个时候进行clean
        @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))));
            }
        }

        // 下边这仨在springboot项目中 很少用 但是必须写 不然容易出问题
        @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());
        }

        // 下边三个clean才是真正的受苦受累的打工人 所有emoji的过滤 都是他们来完成
        // 像极了我们打工人
        private Map<String, String[]> clean(Map<String, String[]> map) {
            if (map == null) {
                return map;
            }
            // req 中的map不可直接修改需要新创建一个map
            // 而且要用LinkedHashMap 因为进来的参数都是有序的 不能随意改顺序 (虽然一般情况可能没啥问题)
            Map<String, String[]> result = new LinkedHashMap<>();
            for (Map.Entry<String, String[]> me : map.entrySet()) {
                result.put(me.getKey(), clean(me.getValue()));
            }
            // 人家本身不可修改 咱也给搞成一个不可修改的map其实很简单 就是在修改的方法里边啥也不干抛异常
            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;
        }

        // 这个是最主要 最后 的一个处理方法 就是他兢兢业业的在处理emoji问题
        private String clean(String val) {
            if (StringUtils.isEmpty(val)) {
                return val;
            }
            // 国外大佬的jar包 包含的方法 删除emoji
            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.*;

/**
 * @author 发现更多精彩  关注公众号:木子的昼夜编程
 * 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
 */
@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);
    }

    // 获取Person对象
    @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;

/**
 * @author 发现更多精彩  关注公众号:木子的昼夜编程
 * 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
 */
@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;
    }

    // 根据ID更新昵称数据
    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());
    }

    // 获取Person对象
    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"); // isnert方法输出了自增主键ID
        // emoji表情 会报错
        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") // 这里如果不设置servletPath Filter获取不到
                .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

有问题还是老样子公众号留言:木子的昼夜编程
在这里插入图片描述

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-09-18 10:00:25  更:2021-09-18 10:00:40 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年1日历 -2025/1/31 11:15:04-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码