Spring AOP 修改请求体和响应体数据解决方案

 

Spring AOP 修改请求体和响应体数据解决方案

0X01 问题背景

最近在开发时,有这样一个场景简单描述就是:前台客户端需要向后端提交数据,后端服务器在接收到数据后需要将其进行持久化,也就是存储到数据库中进行保存,后端持久层使用的是Mysql数据库,且与之对应库的字符集为utf8,后来当前台用户提交的数据含有Emoji表情时,后端在进行持久化执行相应的SQL语句时,报了如下异常:

### Error updating database.  Cause: java.sql.SQLException: Incorrect string value: '\xF0\x9F\x91\xB1\xE2\x80...' for column 'title' at row 1

很显然,是执行SQL语句时出错了,不难发现应该是有什么非法字符导致的,其实单纯就这个问题,是非常容易解决的。即只需要知道MySQL如何存储emoji表情即可。

其实emoji表情其实跟我们使用的其他文字一样,都可以用Unicode编码。那么,可以发现只要MySQL支持Unicode就可以存储emoji表情了。所以,如果将MySQL的字符集设置为utf8,是不支持插入emoji表情的。为了解决这个问题,MySQL在5.5.3之后增加了新的utf8mb4的编码,mb4就是most bytes 4的意思,专门用来兼容四字节的unicode。所以,如果需要在MySQL中存储emoji表情,需要选定utf8mb4字符集。

以上解决办法参考自网络,这里我并没有尝试!

好了,到这里整个问题其实就解决了,但是如果我们不去修改数据库使用的字符集,通过后端服务器该如何解决呢?

0X02 问题分析

OK,通过如上的背景描述,下面进入正题!

解决问题要从根源入手,既然报错的异常是无法存储对应的Unicode字符编码,那么首先要做的就是将其含有Emoji的数据进行转码,以保证可以成功存储到数据库进行持久化,并且需要提供将其取出来返回前端时进行转换为Emoji的功能。为了达到如上需求,我们可以封装一个Util工具类,该类至少须提供的方法有:判断字符串中是否含有表情、将emojiStr转为 带有表情的字符、有表情的字符串转换为编码。有关这个工具类以及使用具体可以看这里:Java 存储mysql数据库时如何进行Emoji表情转换和处理 - 编程那点事儿 (imyjs.cn)

有了上述的工具类,其实已经可以解决问题了,只需要在每一处需要进行过滤的数据进行判断并处理即可!显然,这个工作虽不复杂但是很麻烦,那么,此时就想到了使用Spring的AOP特性来解决它。

大概思路就是首先在执行Controller对应的方法前进行截取请求体数据,并对其使用Emoji工具类进行过滤是否含有Emoji,如果有就进行替换并将新的请求体进行发送至方法,如果没有就直接使用原请求体;然后对于方法返回的响应体同样进行转换编码以实现在前端显示Emoji表情即可。

0X03 问题解决

首先创建切面类EmojiAOP.java

@Component
@Aspect
@Slf4j
public class EmojiAOP {
}

在类中,定义切点表达式

/**
 * 定义切点表达式
 */
@Pointcut("execution(* cn.imyjs.controller..*.*(..))")
public void myPoint(){}

定义环绕执行增强方法,过滤请求体数据

/**
     * 环绕操作
     *
     * @param point 切入点
     * @return 原方法返回值
     * @throws Throwable 异常信息
     */
     @Around("myPoint()")
    public Object aroundLog(ProceedingJoinPoint point) throws Throwable {
        Object[] args = point.getArgs();  // 获取请求体参数
         // 如果请求体不为空,防止空指针异常
        if (args != null) {
            // 依次过滤每一个请求体数据
            for (int i = 0; i < args.length; i++) {
                // 注意 : 这里一定要进行如下判断,否则报下面的错误!暂不清楚原因。
                // It is illegal to call this method if the current request is not in asynchronous mode
                if (args[i] instanceof ServletRequest || args[i] instanceof ServletResponse || args[i] instanceof MultipartFile){
                   continue;
                }
                // 使用工具类进行具体过滤Emoji
                if (EmojiFilterUtil.containsEmoji(JSON.toJSONString(args[i]))){  // 注意将json数据转换为String
                    // System.out.println("第" + i + "个请求体中含有Emoji表情哦!");
                    // System.out.println(args[i].toString());
                    // 如果含有Emoji,则进行转换编码以保证可以成功保存到数据库
                    String strMsg = EmojiFilterUtil.emojiConverterToAlias(JSON.toJSONString(args[i]));
                    // 因为请求体中需要一个对象,而不是String,所以需要再转换为具体的Object
                    Object o1 = null;
                    // 这里需要进行判断对象类型,防止强转报错!嗯、这里就会比较麻烦一点,需要写出所有可能传递的对象类型
                    if (args[i] instanceof LoginModel){
                        o1 = JSON.parseObject(strMsg,LoginModel.class);
                    } else if (args[i] instanceof RemindModel){
                        o1 = JSON.parseObject(strMsg,RemindModel.class);
                    }else if (args[i] instanceof DaKaDataModel) {
                        o1 = JSON.parseObject(strMsg, DaKaDataModel.class);
                    }else if (args[i] instanceof Record) {
                        o1 = JSON.parseObject(strMsg, Record.class);
                    }else if (args[i] instanceof Card) {
                        o1 = JSON.parseObject(strMsg, Card.class);
                    }
                    // 最后,将这个对象再替换原请求体中的数据对象
                    args[i] = o1;
                    // System.out.println("修改后的请求体:" + args[i].toString());
                }
            }
        }

        return point.proceed(args);// 注意:这里返回修改参数值
    }

定义后置增强方法,过滤返回的响应体数据

/**
     * 后置操作
     * @param response 具体方法返回数据
     * @return 
     */
    @AfterReturning(pointcut = "myPoint()", returning = "response")
    public Object  afterReturning(Object response) {
        // System.out.println(response + "========================");
        // 判断response不为null 防止空指针异常
        if (response != null){
            // 强转为返回数据类型,由于整个项目的响应数据类型是一致的,可以放心的进行强转
            R r = (R)response;
            // 获取返回数据
            Object data = r.getData();
            // 进行Emoji过滤并转换
            String str = JSON.toJSONString(data);
            String emojiConverterUnicodeStr = EmojiFilterUtil.emojiConverterUnicodeStr(str);
            // 然后进行转换为Object
            Object o1 = JSON.parse(emojiConverterUnicodeStr);
            // 重新设置新的数据进行返回
            r.setData(o1);
            return r;
        }
        return null;
    }

0X04 拓展

常见AOP使用:

@Component
@Aspect
@Slf4j
public class CommonAOP {
	private static final String START_TIME = "request-start";

    /**
     * 定义切点表达式
     */
    @Pointcut("execution(* cn.imyjs.controller..*.*(..))")

/**
     * 前置增强方法
     * @param joinPoint
     */
    @Before("myPoint()")
    public void doBefore(JoinPoint joinPoint)  {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
        log.info("【请求 URL】:{}", request.getRequestURL());
        log.info("【请求 IP】:{}", GetIPAddressUtil.getIpAddress(request));
        log.info("【请求类名】:{},【请求方法名】:{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
        log.info("【body】:{},", JSONUtil.toJsonStr(joinPoint.getArgs()));
        Map<String, String[]> parameterMap = request.getParameterMap();
        log.info("【请求参数】:{},", JSONUtil.toJsonStr(parameterMap));
        Long start = System.currentTimeMillis();
        request.setAttribute(START_TIME, start);
    }

/**
     * 后置操作
     * @param response 具体方法返回数据
     * @return
     */
    @AfterReturning(pointcut = "myPoint()", returning = "response")
    public Object  afterReturning(Object response) {
         ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
         HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
         HttpServletResponse response = Objects.requireNonNull(attributes).getResponse();

         Long start = (Long) request.getAttribute(START_TIME);
         Long end = System.currentTimeMillis();
         log.info("【请求耗时】:{}毫秒", end - start);

         String header = request.getHeader("User-Agent");
         UserAgent userAgent = UserAgent.parseUserAgentString(header);
         log.info("【浏览器类型】:{},【操作系统】:{},【原始User-Agent】:{}", userAgent.getBrowser().toString(), userAgent.getOperatingSystem().toString(), header);
    }
}

0X05 补充

以上全部代码可能用到的pom坐标

		<!--emoji转换编码-->
        <dependency>
            <groupId>com.github.binarywang</groupId>
            <artifactId>java-emoji-converter</artifactId>
            <version>0.1.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!--获取浏览器信息-->
        <dependency>
            <groupId>eu.bitwalker</groupId>
            <artifactId>UserAgentUtils</artifactId>
            <version>1.21</version>
        </dependency>

        <!-- fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.72</version>
        </dependency>

0X06 总结

实践出真知!对于技术类相关的学习唯有自己具体落实到项目中去使用,才会有深刻的体会与理解!否则只是纸上谈兵,很难体会真正的实用价值!学习过的技术是用来使用的而不是用来背得、记得!

0X07 微信关注

编程那点事儿

阅读剩余
THE END