问题简述

说白了,Java根据指定分隔符分割字符串,忽略在引号(单引号和双引号)里面的分隔符; oom压测的时候,正则匹配"(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)(?=(?:[^']*'[^']*')*[^']*$)" 挂掉了,栈溢出了.
压测使用了200k的sql字符串,也就是200*1024Byte的字符串,单层时间复杂度就有2*10^5,不说时间的问题,正则匹配的迭代量太大,往往2*10^5中首次就可以匹配到上千个分隔符,上千1个再向后迭代,云云.

本地复现,debug一遍找到漏洞点

使用正则,200K的字符串扛不住;量小的话,运算时间也挺长的

    /**
* 根据指定分隔符分割字符串---忽略在引号里面的分隔符
* @param str
* @param delimter
* @Deprecated Reason : 针对200K大小的sql任务,会存在OOM的问题
* @return
*/
public static String[] splitIgnoreQuota(String str, String delimter){
String splitPatternStr = delimter + "(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)(?=(?:[^']*'[^']*')*[^']*$)";
return str.split(splitPatternStr);
}

不使用正则, 完全通过使用单层for循环完全重写String的split方法, 废弃正则表达式, OOM的问题得到解决,秒出结果!

    /**
* 使用非正则表达式的方法来实现 `根据指定分隔符分割字符串---忽略在引号里面的分隔符`
* @param str
* @param delimiter 分隔符
* @return
*/
public static String[] splitIgnoreQuotaNotUsingRegex(String str, String delimiter) {
// trim
str = str.trim();
// 遍历出成对的双引号的位置区间,排除转义的双引号
List<Pair<Integer, Integer>> doubleQuotas = getQuotaIndexPairs(str, '\"');
// 遍历出成对的单引号的位置区间,排除转义的单引号
List<Pair<Integer, Integer>> singleQuotas = getQuotaIndexPairs(str, '\''); // 遍历出所有的delimiter的位置,排除掉在上述两个区间中的,排除掉转义的,按该delimiter位置拆分字符串
List<String> splitList = new ArrayList<>(128);
// index 表示目前搜索指针下标
// beforeIndex 表示目前已经成功匹配到的指针下标
int index = 0, beforeIndex = -1;
while ((index = str.indexOf(delimiter, Math.max(beforeIndex + 1, index))) != -1) {
// 排除转义
if (index == 0 || str.charAt(index - 1) != '\\') {
boolean flag = false;
// 排除双引号内的
for (Pair<Integer, Integer> p : doubleQuotas) {
if (p.getKey() <= index && p.getValue() >= index) {
flag = true;
break;
}
}
// 排除单引号内的
for (int i = 0; !flag && i < singleQuotas.size(); i++) {
Pair<Integer, Integer> p = singleQuotas.get(i);
if (p.getKey() <= index && p.getValue() >= index) {
flag = true;
break;
}
}
// flag = true, 表示该字符串在匹配的成对引号,跳过
if(flag){
index++;
continue;
}
// 这里的substring只取到分隔符的前一位,分隔符不加进来
splitList.add(str.substring(beforeIndex + 1, index));
beforeIndex = index;
} else {
index++;
}
}
// 收尾串
if (beforeIndex != str.length()) {
splitList.add(str.substring(beforeIndex + 1, str.length()));
}
return splitList.toArray(new String[0]);
} /**
* 遍历出成对的双/单引号的位置区间,排除转义的双引号
* @param str
* @param quotaChar
* @return
*/
private static List<Pair<Integer, Integer>> getQuotaIndexPairs(String str, char quotaChar) {
List<Pair<Integer, Integer>> quotaPairs = new ArrayList<>(64);
List<Integer> posList = new ArrayList<>(128);
for (int idx = 0; idx < str.length(); idx++) {
if (str.charAt(idx) == quotaChar) {
if (idx == 0 || str.charAt(idx - 1) != '\\') {
posList.add(idx);
}
}
}
// 每两个装进Pair中,总数为奇数的话最后一个舍掉
for (int idx = 0; idx <= posList.size() - 2; idx += 2) {
quotaPairs.add(new Pair<>(posList.get(idx), posList.get(idx + 1)));
}
return quotaPairs;
}

样例输入 简单单测

 @Test
public void test02() throws Exception {
String builder = "create table if not exists exam_ads_sales_all_d (\n" +
" stat_date string comment '统计日期'\n" +
" ,ord_quantity bigint comment '订单数量'\n" +
" ,ord_amount double comment '订单金额'\n" +
" ,pay_quantity bigint comment '付款数量'\n" +
" ,pay_amount double comment '付款金额'\n" +
" ,shop_cnt bigint comment '有交易的店铺数量'\n" +
")comment '测试;订单交易总表'\n" +
"PARTITIONED BY (ds string) lifecycle 7;select * from exam_ads_sales_all_d"; String[] splits = MyFormatter.splitIgnoreQuota(builder.toString(), ";");
System.out.println("================splitIgnoreQuota分割后行数: " + splits.length);
for (int i = 0; i < splits.length; i++) {
System.out.println(splits[i]+"\n");
} String[] splits2 = DtStringUtil.splitIgnoreQuotaNotUsingRegex(builder.toString(), ";");
System.out.println("================splitIgnoreQuotaNotUsingRegex分割后行数: " + splits2.length);
for (int i = 0; i < splits2.length; i++) {
System.out.println(splits2[i]+"\n");
}
Assert.assertEquals(splits.length, splits2.length);
}

样例输出

================splitIgnoreQuotaNotUsingRegex分割后行数:  2
create table if not exists exam_ads_sales_all_d (
stat_date string comment '统计日期'
,ord_quantity bigint comment '订单数量'
,ord_amount double comment '订单金额'
,pay_quantity bigint comment '付款数量'
,pay_amount double comment '付款金额'
,shop_cnt bigint comment '有交易的店铺数量'
)comment '测试;订单交易总表'
PARTITIONED BY (ds string) lifecycle 7 select * from exam_ads_sales_all_d

- 为了后续的业务需求,算法中去掉了分隔符(有注释);更多问题,欢迎指正!

- class Pair 引用自package org.apache.commons.math3.util; 自行添加maven依赖

码字不易啊~~

BUGFIX 09 - 记一次Java中String的split正则表达式匹配 - 引发`OutOfMemoryError: Java heap space`的oom异常 排查及解决 -Java根据指定分隔符分割字符串,忽略在引号里面的分隔符的更多相关文章

  1. Java中String的split()方法的一些需要注意的地方

    public String[] split(String regex, int limit) split函数是用于使用特定的切割符(regex)来分隔字符串成一个字符串数组,这里我就不讨论第二个参数( ...

  2. Java中String的split()方法的一些疑问和试验

    http://tjuking.iteye.com/blog/1507855 和我想的还是不大一样,因为不知道源码也不知道具体是怎么实现的,我的理解如下: 当字符串只包含分隔符时,返回数组没有元素:当字 ...

  3. Java中String和byte[]间的 转换

    数据库的字段中使用了blob类型时,在entity中此字段可以对应为byte[] 类型,保存到数据库中时需要把传入的参数转为byte[]类型,读取的时候再通过将byte[]类型转换为String类型. ...

  4. Java中String类的方法及说明

    String : 字符串类型 一.      String sc_sub = new String(c,3,2);    //      String sb_copy = new String(sb) ...

  5. 【转载】Java中String类的方法及说明

    转载自:http://www.cnblogs.com/YSO1983/archive/2009/12/07/1618564.html String : 字符串类型 一.      String sc_ ...

  6. 【转载】 Java中String类型的两种创建方式

    本文转载自 https://www.cnblogs.com/fguozhu/articles/2661055.html Java中String是一个特殊的包装类数据有两种创建形式: String s ...

  7. java成神之——java中string的用法

    java中String的用法 String基本用法 String分割 String拼接 String截取 String换行符和format格式化 String反转字符串和去除空白字符 String获取 ...

  8. Java中String类的特殊性

    java中特殊的String类型 Java中String是一个特殊的包装类数据有两种创建形式: String s = "abc"; String s = new String(&q ...

  9. java中string内存的相关知识点

    (一):区别java内存中堆和栈: 1.栈:数据可以共享,存放基本数据类型和对象的引用,其中对象存放在堆中,对象的引用存放在栈中: 当在一段代码块定义一个变量时,就在栈中 为这个变量分配内存空间,当该 ...

随机推荐

  1. php--->php 缓冲区 buffer 原理

    php 缓冲区 buffer 原理 1.缓冲流程 从php脚本echo(print.print_r...)内容之后,是如何显示给用户的呢,下面看看流程 echo.print => php out ...

  2. SpringBoot系列专栏

    学会使用SpringBoot能够极大地提升Spring应用的开发效率,可以说是目前开发应用Java必需掌握的工具之一,而且SpringBoot也是微服务应用的基础,只有学会了SpringBoot,你才 ...

  3. springIOC源码接口分析(八):AutowireCapableBeanFactory

    参考博文: https://blog.csdn.net/f641385712/article/details/88651128 一 接口规范 从宏观上看,AutowireCapableBeanFact ...

  4. kubernetes secret 和 serviceaccount删除

    背景 今天通过配置创建了一个serviceaccounts和secret,后面由于某种原因想再次创建发现已存在一个serviceaccounts和rolebindings.rbac.authoriza ...

  5. vue2.x中子组件修改父组件通过pops传递过来的值

    首先,父组件向子组件传值 这里面主要是在传值的时候,加上.sync 然后子组件通过 $emit 修改 如此即可完成对父组件的数据操作

  6. 死磕mysql(5)

    高性能mysql --查看隐藏的库//use mysql --创建新用户//create user 'new'@'localhost' identified by ''; --创建用户 '用户名'@' ...

  7. 死磕java(4)

    数组 public void int4() {  int[] int2 = {1,2,3,4};  System.out.print(int2[2]); } 输出:3 另一种数组的初始化 public ...

  8. tf识别非固定长度图片ocr(数字+字母 n位长度可变)- CNN+RNN+CTC

    先安装必须的库 tensorflow_gpu==1.15.0 numpy opencv_python github: https://github.com/bai-shang/crnn_ctc_ocr ...

  9. ubuntu 如何添加alias

    公司的nx 上面一般使用gvim 编辑文件.并且为gvim 增加了alias,只要敲 g 就是gvim 的意思,这样编辑一个文件只需要 g xxx.v 就可以了.非常方便. 在自己电脑上安装了ubun ...

  10. Vsftpd: 基于PAM认证的虚拟用户和匿名用户

    目录 环境说明效果说明及截图①. 安装组件②. 系统账户建立③. 编辑vsftpd的配置文件④. 生成虚拟用户的数据库文件⑤. 生成一个使用vsftpd_login.db数据文件的PAM认证文件⑥. ...