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