在后端服务处理用户敏感数据时,经常需要把特定格式的字符串做部分遮盖,只露出末尾几个字符用于核对。比如内部日志里的银行卡号、提现流水号,如果全量明文打印会有泄露风险,但完全打码又不利于排查问题。用Java正则表达式可以基于模式匹配,精准地把中间一段替换成星号,同时保留末尾N个字符,比手动截取更适应格式变化。

使用捕获组与反向引用实现基础遮盖
核心思路是把原始字符串分成「前面待遮盖部分」和「末尾保留部分」两个捕获组,替换时只把前面组对应的内容换成星号,末尾组原样写回。假设我们要遮盖一个至少八位的数字串,保留最后四位,可以这样写:前面匹配一位或多位数字但不贪婪,末尾四个数字单独成组。
下面示例中,正则表达式 (d+)(d{4}) 把数字拆成两组,替换串用 * 重复前面组的长度并不现实,因此更简单的做法是前面组直接替换为固定数量的星号,或者根据需求用 replaceAll 配合自定义逻辑。若只求视觉遮盖,可用固定星号数。
public class MaskDemo {
public static void main(String[] args) {
String input = "6222021234567890123";
// 保留末尾4位,前面用星号遮盖
String masked = input.replaceAll("(\d+)(\d{4})", "****$2");
System.out.println(masked);
}
}
上面代码里 $2 表示第二个捕获组,也就是末尾四位。运行结果是 ****7890123,达到了保留末四位的效果。这种写法适合长度不固定但末尾位数固定的场景,不需要关心总长多少。
如果希望星号数量和遮盖位数一致,则不能在 replaceAll 的替换串里动态生成星号,需要改用 Matcher 遍历并自己拼装。但多数业务脱敏用固定长度星号已足够,避免过度复杂。
针对复杂格式的模式匹配与分组
真实数据往往带有分隔符或前缀,例如 ORD-20240512-8832 这类订单号,只想遮盖中间日期段而保留前后。此时要用更明确的分组,把不变的前缀、待遮段、保留段分开。通过字符类与量词约束,能防止误伤其他格式字符串。
以下例子保留前缀 ORD- 和末尾四位,中间任意字符遮盖为六个星号。注意正则里用 .*? 非贪婪匹配中间内容,避免跨段吞掉末尾。
public class OrderMask {
public static void main(String[] args) {
String order = "ORD-20240512-8832";
String result = order.replaceAll("(ORD-).*?(-\w{4})", "$1******$2");
System.out.println(result);
}
}
这里第一个组 (ORD-) 锁定前缀,第二个组 (-\w{4}) 捕获连字符加末尾四位。替换串写死六个星号,输出为 ORD-******-8832。如果末尾不是固定四位,可以把 \w{4} 改成 \w+ 并用更严谨的边界控制。
当字符串可能包含多种格式时,建议先判断长度或先用 Matcher.matches 试探模式,再决定遮组策略,防止错误遮盖正常文本。对于批量日志处理,可以把正则编译为静态 Pattern 复用,减少重复编译开销。
动态保留N位与性能注意事项
保留末尾N位如果N是变量,无法写死在正则里,需要把N拼进表达式。可以用 String.format 构造 (\d+)(\d{%d}) 这样的模板,再编译Pattern。这样既灵活又保持正则清晰。
下面的方法接收原串和保留位数,返回遮盖结果。它先构建正则,再用 replaceAll 替换。对于超长文本或高频调用,应把Pattern缓存起来而不是每次新建。
import java.util.regex.Pattern;
public class DynamicMask {
public static String mask(String input, int keepTail) {
if (input == null || input.length() <= keepTail) {
return input;
}
String regex = "(\d+)(\d{" + keepTail + "})";
Pattern p = Pattern.compile(regex);
return p.matcher(input).replaceAll("****$2");
}
public static void main(String[] args) {
System.out.println(mask("6222021234567890123", 4));
System.out.println(mask("123456", 2));
}
}
在性能上,正则引擎的回溯可能在极端长串或病态表达式上变慢。尽量用具体字符类代替 .*,并给分组加边界锚点如 ^ 和 $ 当整串匹配时。若系统每秒处理数十万条日志,建议对超长串先做长度分流,短串走正则,极长串走手动截取。
另外注意Java字符串不可变,replaceAll 每次生成新对象,在循环里大量遮盖要考虑StringBuilder或char数组手动写,避免频繁GC。正则方案胜在可读与格式适配,手动方案胜在极致性能,按业务量级取舍即可。