侧边栏壁纸
  • 累计撰写 103 篇文章
  • 累计创建 42 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

HighPerfMaskParser

天明
2026-08-05 / 0 评论 / 0 点赞 / 1 阅读 / 0 字 / 正在检测是否收录...
package com.jnliok.sensitive_log.mask;

/**
 * 高性能状态机解析器:O(n) 单次扫描,零正则,处理任意混合格式。
 *
 * <h3>支持的格式</h3>
 * <ul>
 *   <li>纯文本:{@code "用户张三登录,手机13800138000"}</li>
 *   <li>双引号 JSON:{@code {"name":"张三","phone":"13800138000"}}</li>
 *   <li>单引号 JSON:{@code {'name':'张三','phone':'13800138000'}}</li>
 *   <li>无引号 JSON:{@code {name:张三, phone:13800138000}}</li>
 *   <li>Map.toString():{@code {name=张三, phone=13800138000}}</li>
 *   <li>混合格式:{@code log prefix {"name":"张三"} suffix}</li>
 * </ul>
 *
 * <h3>设计</h3>
 * 核心解析器 {@link #parseObject} 接受显式缓冲区参数,可安全递归。
 * 顶层调用使用 ThreadLocal 缓冲区(零分配),嵌套对象递归时分配临时缓冲区。
 * 消除了 mask/maskObjectInline 之间约 200 行的状态机重复代码。
 */
public final class HighPerfMaskParser {

    // ---- 状态常量 ----
    private static final int S_WAIT_KEY         = 0; // 在 { 内,等待 key 或 }
    private static final int S_IN_KEY_QUOTED    = 1; // 正在读带引号 key
    private static final int S_IN_KEY_UNQUOTED  = 2; // 正在读无引号 key
    private static final int S_AFTER_KEY        = 3; // key 读完,等待 : 或 =
    private static final int S_WAIT_VALUE       = 4; // 等待 value 开始
    private static final int S_IN_VALUE_QUOTED  = 5; // 正在读带引号 value
    private static final int S_IN_VALUE_UNQUOTED= 6; // 正在读无引号 value

    // ---- ThreadLocal 缓冲区(顶层调用零分配) ----
    private static final ThreadLocal<StringBuilder> SB =
            ThreadLocal.withInitial(() -> new StringBuilder(4096));
    private static final ThreadLocal<char[]> KEY_BUF =
            ThreadLocal.withInitial(() -> new char[256]);
    private static final ThreadLocal<char[]> VAL_BUF =
            ThreadLocal.withInitial(() -> new char[4096]);

    private HighPerfMaskParser() {}

    // ==================== 公开接口 ====================

    /**
     * 对输入字符串进行脱敏处理
     */
    public static String mask(String input, MaskingConfig config) {
        if (input == null || input.isEmpty() || config.isEmpty()) return input;

        StringBuilder out = SB.get();
        out.setLength(0);
        int len = input.length();
        int i = 0;

        while (i < len) {
            char c = input.charAt(i);
            if (c == '{') {
                i = parseObject(input, i, out, KEY_BUF.get(), VAL_BUF.get(), config);
            } else if (c == '<' && isXmlTag(input, i)) {
                i = parseXmlElement(input, i, out, config);
            } else {
                out.append(c);
                i++;
            }
        }
        return out.toString();
    }

    // ==================== 核心:对象解析 ====================

    /**
     * 解析一个对象 { ... } 并对其中的敏感字段值进行脱敏。
     * <p>
     * 使用显式缓冲区参数,可安全递归——调用者栈上的缓冲区互不干扰。
     *
     * @param input   完整输入字符串
     * @param start   指向 '{' 的位置
     * @param out     输出缓冲区
     * @param keyBuf  键名缓冲区
     * @param valBuf  值缓冲区
     * @param config  脱敏配置
     * @return 匹配 '}' 之后的位置
     */
    private static int parseObject(String input, int start, StringBuilder out,
                                   char[] keyBuf, char[] valBuf,
                                   MaskingConfig config) {
        int len = input.length();
        int i = start + 1;          // 跳过 '{'
        int depth = 1;              // { } 嵌套深度

        out.append('{');

        int   state          = S_WAIT_KEY;
        int   keyLen         = 0;
        char  keyQuote       = 0;
        MaskType maskType    = null;
        char  valQuote       = 0;
        int   valStart       = -1;
        int   valLen         = 0;

        while (i < len && depth > 0) {
            char c = input.charAt(i);

            switch (state) {

                // ============ S_WAIT_KEY ============
                case S_WAIT_KEY:
                    if (c == '}') {
                        depth--;
                        out.append(c);
                        i++;
                    } else if (c == ',' || isWhitespace(c)) {
                        out.append(c);
                        i++;
                    } else if (c == '\'' || c == '"') {
                        keyQuote = c;
                        keyLen = 0;
                        out.append(c);
                        state = S_IN_KEY_QUOTED;
                        i++;
                    } else if (isKeyStartChar(c)) {
                        keyQuote = 0;
                        keyLen = 0;
                        out.append(c);
                        if (keyLen < keyBuf.length) keyBuf[keyLen++] = c;
                        state = S_IN_KEY_UNQUOTED;
                        i++;
                    } else {
                        out.append(c);
                        i++;
                    }
                    break;

                // ============ S_IN_KEY_QUOTED ============
                case S_IN_KEY_QUOTED:
                    if (c == '\\' && i + 1 < len) {
                        char next = input.charAt(i + 1);
                        out.append(c).append(next);
                        if (keyLen < keyBuf.length) keyBuf[keyLen++] = next;
                        i += 2;
                    } else if (c == keyQuote) {
                        out.append(c);
                        maskType = resolveKey(keyBuf, keyLen, config);
                        state = S_AFTER_KEY;
                        i++;
                    } else {
                        out.append(c);
                        if (keyLen < keyBuf.length) keyBuf[keyLen++] = c;
                        i++;
                    }
                    break;

                // ============ S_IN_KEY_UNQUOTED ============
                case S_IN_KEY_UNQUOTED:
                    if (c == ':' || c == '=' || c == '-') {
                        maskType = resolveKey(keyBuf, keyLen, config);
                        out.append(c);
                        state = S_WAIT_VALUE;
                        i++;
                    } else if (c == '{' || c == '[') {
                        // key 后紧跟 { 或 [,如 data-{...},key 结束,不消费 c
                        maskType = resolveKey(keyBuf, keyLen, config);
                        state = S_WAIT_VALUE;
                        break;
                    } else if (isWhitespace(c)) {
                        maskType = resolveKey(keyBuf, keyLen, config);
                        out.append(c);
                        state = S_AFTER_KEY;
                        i++;
                    } else {
                        out.append(c);
                        if (keyLen < keyBuf.length) keyBuf[keyLen++] = c;
                        i++;
                    }
                    break;

                // ============ S_AFTER_KEY ============
                case S_AFTER_KEY:
                    if (c == ':' || c == '=' || c == '-') {
                        out.append(c);
                        state = S_WAIT_VALUE;
                    } else if (isWhitespace(c)) {
                        out.append(c);
                    } else {
                        // 异常:key 后缺分隔符,回退
                        out.append(c);
                        maskType = null;
                        state = S_WAIT_KEY;
                    }
                    i++;
                    break;

                // ============ S_WAIT_VALUE ============
                case S_WAIT_VALUE:
                    if (isWhitespace(c)) {
                        out.append(c);
                        i++;
                    } else if (c == '\'' || c == '"') {
                        valQuote = c;
                        valStart = out.length();
                        out.append(c);
                        valLen = 0;
                        state = S_IN_VALUE_QUOTED;
                        i++;
                    } else if (c == '{') {
                        // 嵌套对象:分配局部缓冲区后递归
                        int end = parseObject(input, i, out, new char[256], new char[4096], config);
                        i = end;
                        maskType = null;
                        state = S_WAIT_KEY;
                    } else if (c == '[') {
                        // 嵌套数组:递归处理
                        i = parseArray(input, i, out, maskType, config);
                        maskType = null;
                        state = S_WAIT_KEY;
                    } else if (c == ',' || c == '}') {
                        // 空值,如 {key:,}
                        maskType = null;
                        state = S_WAIT_KEY;
                        break;
                    } else {
                        // 无引号 value
                        valStart = out.length();
                        valLen = 0;
                        if (valLen < valBuf.length) valBuf[valLen++] = c;
                        out.append(c);
                        state = S_IN_VALUE_UNQUOTED;
                        i++;
                    }
                    break;

                // ============ S_IN_VALUE_QUOTED ============
                case S_IN_VALUE_QUOTED:
                    if (c == '\\' && i + 1 < len) {
                        char next = input.charAt(i + 1);
                        out.append(c).append(next);
                        if (valLen < valBuf.length) valBuf[valLen++] = next;
                        i += 2;
                    } else if (c == valQuote) {
                        out.append(c);
                        applyMask(out, valStart + 1, out.length() - 1, valBuf, valLen, maskType);
                        maskType = null;
                        valLen = 0;
                        state = S_WAIT_KEY;
                        i++;
                    } else {
                        out.append(c);
                        if (valLen < valBuf.length) valBuf[valLen++] = c;
                        i++;
                    }
                    break;

                // ============ S_IN_VALUE_UNQUOTED ============
                case S_IN_VALUE_UNQUOTED:
                    if (c == ',' || c == '}') {
                        applyMask(out, valStart, out.length(), valBuf, valLen, maskType);
                        maskType = null;
                        valLen = 0;
                        state = S_WAIT_KEY;
                        break; // 不消费 c,让 S_WAIT_KEY 处理
                    } else if (c == ':' || isWhitespace(c)) {
                        applyMask(out, valStart, out.length(), valBuf, valLen, maskType);
                        maskType = null;
                        valLen = 0;
                        out.append(c);
                        state = S_WAIT_KEY;
                        i++;
                    } else {
                        out.append(c);
                        if (valLen < valBuf.length) valBuf[valLen++] = c;
                        i++;
                    }
                    break;

                default:
                    i++;
                    break;
            }
        }

        // 末尾未完成的 unquoted value
        if (state == S_IN_VALUE_UNQUOTED && valLen > 0) {
            applyMask(out, valStart, out.length(), valBuf, valLen, maskType);
        }
        return i;
    }

    // ==================== 核心:数组解析 ====================

    /**
     * 解析数组 [...] 并对其中的敏感字符串元素进行脱敏。
     * 嵌套对象通过 {@link #parseObject} 递归处理。
     *
     * @param input     完整输入字符串
     * @param start     指向 '[' 的位置
     * @param out       输出缓冲区
     * @param maskType  当前 key 对应的脱敏类型(null = 不脱敏)
     * @param config    脱敏配置
     * @return 匹配 ']' 之后的位置
     */
    private static int parseArray(String input, int start, StringBuilder out,
                                  MaskType maskType, MaskingConfig config) {
        int len = input.length();
        int depth = 1;
        int i = start;

        while (i < len && depth > 0) {
            char c = input.charAt(i);

            if (c == '[') {
                depth++;
                out.append(c);
                i++;
            } else if (c == ']') {
                depth--;
                out.append(c);
                i++;
            } else if (c == '{') {
                i = parseObject(input, i, out, new char[256], new char[4096], config);
            } else if (c == '\'' || c == '"') {
                i = scanAndMaskQuotedArrayElement(input, i, out, maskType);
            } else {
                out.append(c);
                i++;
            }
        }
        return i;
    }

    /**
     * 扫描数组中一个带引号的字符串元素,必要时脱敏。
     */
    private static int scanAndMaskQuotedArrayElement(String input, int start,
                                                     StringBuilder out, MaskType maskType) {
        int len = input.length();
        int i = start;
        char q = input.charAt(i);
        out.append(q);
        i++;

        // 收集元素值(处理转义)
        StringBuilder strVal = new StringBuilder();
        while (i < len && input.charAt(i) != q) {
            if (input.charAt(i) == '\\' && i + 1 < len) {
                strVal.append(input.charAt(i + 1));
                out.append(input.charAt(i)).append(input.charAt(i + 1));
                i += 2;
            } else {
                strVal.append(input.charAt(i));
                out.append(input.charAt(i));
                i++;
            }
        }
        if (i < len) {
            out.append(input.charAt(i));
            i++;
        }

        // 脱敏
        if (maskType != null && strVal.length() > 0) {
            String masked = MaskStrategyRegistry.getStrategy(maskType).mask(strVal.toString());
            int valStartInOut = out.length() - strVal.length() - 1;
            out.replace(valStartInOut, valStartInOut + strVal.length(), masked);
        }
        return i;
    }

    // ==================== 辅助方法 ====================

    /**
     * 解析 key 的脱敏类型。
     *
     * @return 脱敏类型,null 表示该 key 无需脱敏
     */
    private static MaskType resolveKey(char[] keyBuf, int keyLen, MaskingConfig config) {
        return config.tryMatch(keyBuf, 0, keyLen) != null
                ? config.getMaskType(new String(keyBuf, 0, keyLen))
                : null;
    }

    /**
     * 对 value 应用脱敏策略,原地替换 output 中的内容。
     */
    private static void applyMask(StringBuilder out, int start, int end,
                                  char[] valBuf, int valLen, MaskType maskType) {
        if (maskType == null || valLen == 0) return;
        String rawValue = new String(valBuf, 0, valLen);
        String masked = MaskStrategyRegistry.getStrategy(maskType).mask(rawValue);
        out.replace(start, end, masked);
    }

    // ==================== XML 解析 ====================

    /**
     * 判断 &lt; 是否是一个 XML 元素的开始(非普通文本中的小于号)。
     */
    private static boolean isXmlTag(String input, int pos) {
        int next = pos + 1;
        if (next >= input.length()) return false;
        char c = input.charAt(next);
        return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
                || c == '_' || c == '/' || c == '?' || c == '!';
    }

    /**
     * 解析一个 XML 元素并对其文本内容脱敏。
     * <p>
     * 支持:&lt;tag&gt;text&lt;/tag&gt; / &lt;tag/&gt; / &lt;?xml...?&gt; / &lt;!--...--&gt; / &lt;![CDATA[...]]&gt;。
     * 嵌套子元素递归处理。
     */
    private static int parseXmlElement(String input, int start, StringBuilder out,
                                       MaskingConfig config) {
        int len = input.length();
        int i = start;

        if (i >= len) return i;
        int next = i + 1;
        if (next >= len) { out.append('<'); return next; }

        char nc = input.charAt(next);

        // ---- <?xml ...?> ----
        if (nc == '?') {
            return seekTo(input, i, out, "?>");
        }

        // ---- <!-- ... --> ----
        if (nc == '!' && next + 2 < len
                && input.charAt(next + 1) == '-' && input.charAt(next + 2) == '-') {
            return seekTo(input, i, out, "-->");
        }

        // ---- <![CDATA[...]]> ----
        if (nc == '!' && next + 8 < len && input.charAt(next + 1) == '[') {
            String cdata = input.substring(next + 2, next + 7);
            if ("CDATA".equalsIgnoreCase(cdata) && input.charAt(next + 7) == '[') {
                return seekTo(input, i, out, "]]>");
            }
        }

        // ---- <!DOCTYPE ...> etc. ----
        if (nc == '!') {
            return seekTo(input, i, out, ">");
        }

        // ---- </closing> pass-through ----
        if (nc == '/') {
            return seekTo(input, i, out, ">");
        }

        // ============ <tagname ...>content</tagname> ============
        i++; // skip '<'

        // 读取标签名
        int tagStart = i;
        while (i < len) {
            char tc = input.charAt(i);
            if (tc == '>' || tc == '/' || tc <= ' ') break;
            i++;
        }
        String tagName = input.substring(tagStart, i);
        if (tagName.isEmpty()) { out.append('<'); return start + 1; }

        // 自闭合 <tagname/>?
        boolean selfClosing = false;
        if (i < len && input.charAt(i) == '/') { selfClosing = true; i++; }

        // 跳过属性直到 >
        while (i < len && input.charAt(i) != '>') i++;
        if (i >= len) { out.append('<').append(tagName); return i; }
        i++; // skip '>'

        // 输出开标签
        out.append('<').append(tagName);
        if (selfClosing) { out.append("/>"); return i; }
        out.append('>');

        // 查询脱敏类型
        MaskType maskType = config.getMaskType(tagName);

        // 读取内容直到 </tagname>
        String closeTag = "</" + tagName + ">";
        if (maskType == null) {
            return scanXmlContent(input, i, out, closeTag, null, config);
        } else {
            return scanXmlContent(input, i, out, closeTag, maskType, config);
        }
    }

    /**
     * 扫描元素内容直到匹配 {@code closeTag},嵌套 XML 递归处理。
     * 若 maskType 非 null 且内容为纯文本(无嵌套子元素),应用脱敏。
     *
     * <p>采用"延迟输出"策略:文本先收集到 textBuf,确定是否需要脱敏后再输出,
     * 避免位置偏移导致的 IndexOutOfBoundsException。
     */
    private static int scanXmlContent(String input, int start, StringBuilder out,
                                      String closeTag, MaskType maskType,
                                      MaskingConfig config) {
        int len = input.length();
        int i = start;
        int clen = closeTag.length();
        StringBuilder textBuf = new StringBuilder();
        boolean hasNested = false;

        while (i < len) {
            if (input.charAt(i) == '<') {
                // 检查是否匹配关闭标签
                if (i + clen <= len) {
                    boolean match = true;
                    for (int j = 0; j < clen; j++) {
                        if (input.charAt(i + j) != closeTag.charAt(j)) { match = false; break; }
                    }
                    if (match) {
                        // 输出文本(必要时脱敏)
                        flushText(out, textBuf, maskType, hasNested);
                        out.append(closeTag);
                        return i + clen;
                    }
                }

                // 嵌套 XML 元素
                if (isXmlTag(input, i)) {
                    flushText(out, textBuf, null, false); // 先输出已收集的文本
                    hasNested = true;
                    i = parseXmlElement(input, i, out, config);
                    continue;
                }
            }
            textBuf.append(input.charAt(i));
            i++;
        }

        // 到达末尾未找到关闭标签,输出剩余文本
        flushText(out, textBuf, null, hasNested);
        return i;
    }

    /**
     * 将 textBuf 中的文本输出到 out,必要时脱敏。
     */
    private static void flushText(StringBuilder out, StringBuilder textBuf,
                                  MaskType maskType, boolean hasNested) {
        if (textBuf.length() == 0) return;
        if (maskType != null && !hasNested) {
            out.append(MaskStrategyRegistry.getStrategy(maskType)
                    .mask(textBuf.toString()));
        } else {
            out.append(textBuf);
        }
        textBuf.setLength(0);
    }

    /**
     * 从 start 输出直到 (并包含) delimiter,返回 delimiter 之后的位置。
     */
    private static int seekTo(String input, int start, StringBuilder out, String delimiter) {
        int dlen = delimiter.length();
        int len = input.length();

        // 找到 delimiter 的起始位置
        int pos = start;
        while (pos + dlen <= len) {
            boolean match = true;
            for (int j = 0; j < dlen; j++) {
                if (input.charAt(pos + j) != delimiter.charAt(j)) { match = false; break; }
            }
            if (match) break;
            pos++;
        }

        if (pos + dlen > len) {
            // delimiter 未找到,输出剩余内容
            out.append(input, start, len);
            return len;
        }

        // 输出 start 到 delimiter 之后(包含 delimiter)
        // Note: StringBuilder.append(s, start, end) 的 end 是绝对索引,不是长度
        out.append(input, start, pos + dlen);
        return pos + dlen;
    }

    // ---- 字符判断 ----

    private static boolean isWhitespace(char c) {
        return c == ' ' || c == '\t' || c == '\n' || c == '\r';
    }

    private static boolean isKeyStartChar(char c) {
        return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == '$';
    }
}


package com.jnliok.sensitive_log.mask;

import java.util.*;

/**
 * 脱敏配置管理器
 * <p>
 * 线程安全:构建后不可变,所有查询方法无副作用。
 * 使用 HashMap 存储 fieldName → MaskType 映射,O(1) 查找。
 */
public final class MaskingConfig {

    /** 空配置单例,避免重复创建 */
    private static final MaskingConfig EMPTY = new MaskingConfig(Collections.emptyMap());

    /** fieldName → MaskType 映射,构建后不可变 */
    private final Map<String, MaskType> fieldMaskMap;

    /** 使用字符数组存储已知字段名,供状态机快速前缀匹配 */
    private final char[][] knownFieldChars;

    /** 已知字段名的最大长度,用于状态机提前终止 */
    private final int maxFieldNameLen;

    /** 已知字段名的最小长度 */
    private final int minFieldNameLen;

    private MaskingConfig(Map<String, MaskType> fieldMaskMap) {
        this.fieldMaskMap = Collections.unmodifiableMap(new HashMap<>(fieldMaskMap));

        // 预计算字段名字符数组,用于快速匹配
        this.knownFieldChars = new char[fieldMaskMap.size()][];
        int i = 0;
        int maxLen = 0;
        int minLen = Integer.MAX_VALUE;
        for (String name : fieldMaskMap.keySet()) {
            knownFieldChars[i] = name.toCharArray();
            i++;
            if (name.length() > maxLen) maxLen = name.length();
            if (name.length() < minLen) minLen = name.length();
        }
        this.maxFieldNameLen = maxLen;
        this.minFieldNameLen = fieldMaskMap.isEmpty() ? 0 : minLen;
    }

    /**
     * 创建构建器
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * 获取空配置
     */
    public static MaskingConfig empty() {
        return EMPTY;
    }

    /**
     * 根据字段名查找脱敏类型,O(1)
     *
     * @param fieldName 字段名
     * @return 对应的脱敏类型,未配置返回 null
     */
    public MaskType getMaskType(String fieldName) {
        return fieldMaskMap.get(fieldName);
    }

    /**
     * 判断字段是否需要脱敏
     */
    public boolean shouldMask(String fieldName) {
        return fieldMaskMap.containsKey(fieldName);
    }

    /**
     * 配置的字段数量
     */
    public int size() {
        return fieldMaskMap.size();
    }

    public boolean isEmpty() {
        return fieldMaskMap.isEmpty();
    }

    // ---- 以下方法供状态机内部使用 ----

    char[][] getKnownFieldChars() {
        return knownFieldChars;
    }

    int getMaxFieldNameLen() {
        return maxFieldNameLen;
    }

    int getMinFieldNameLen() {
        return minFieldNameLen;
    }

    /**
     * 尝试匹配字段名,O(k) 其中 k = 字段名长度
     *
     * @param buf    字符缓冲区
     * @param offset 起始偏移
     * @param length 字段名长度
     * @return 匹配到的脱敏类型,未匹配返回 null
     */
    MaskType tryMatch(char[] buf, int offset, int length) {
        if (length < minFieldNameLen || length > maxFieldNameLen) {
            return null;
        }
        // 遍历所有已知字段进行匹配(字段数量通常很少,线性遍历即可)
        for (int i = 0; i < knownFieldChars.length; i++) {
            char[] known = knownFieldChars[i];
            if (known.length == length && charsEqual(known, buf, offset, length)) {
                // 已知字段匹配成功,查 Map 获取 MaskType
                return fieldMaskMap.get(new String(known));
            }
        }
        return null;
    }

    private static boolean charsEqual(char[] a, char[] b, int bOffset, int len) {
        for (int i = 0; i < len; i++) {
            if (a[i] != b[bOffset + i]) {
                return false;
            }
        }
        return true;
    }

    // ==================== Builder ====================

    public static class Builder {
        private final Map<String, MaskType> fieldMaskMap = new HashMap<>();

        /**
         * 添加字段脱敏规则
         */
        public Builder addField(String fieldName, MaskType maskType) {
            fieldMaskMap.put(fieldName, maskType);
            return this;
        }

        /**
         * 批量添加字段规则
         */
        public Builder addFields(Map<String, MaskType> fields) {
            fieldMaskMap.putAll(fields);
            return this;
        }

        public MaskingConfig build() {
            return new MaskingConfig(new HashMap<>(fieldMaskMap));
        }
    }
}


package com.jnliok.sensitive_log.mask;

import org.apache.commons.lang3.StringUtils;

/**
 * 脱敏策略接口
 * <p>
 * 所有实现必须是无状态的,以支持多线程并发调用。
 * 输入输出均为字符串,内部使用字符数组操作,不使用正则。
 */
@FunctionalInterface
public interface MaskStrategy {

    /**
     * 对原始值进行脱敏处理
     *
     * @param rawValue 原始值(已去除引号和前后空白)
     * @return 脱敏后的值
     */
    String mask(String rawValue);

    /**
     * 全量脱敏:将字符串全部替换为 *
     * <p>
     * Java 17 String.repeat() 比手动 char[] 循环更简洁,JIT 优化后性能等同。
     */
    default String fullMask(String s) {
        return StringUtils.repeat('*',s.length());
    }
}

package com.jnliok.sensitive_log.mask;

/**
 * 脱敏策略注册表:将 MaskType 映射到具体的 MaskStrategy 实现
 * <p>
 * 线程安全:不可变,使用枚举单例策略实例
 */
public final class MaskStrategyRegistry {

    private static final MaskStrategy[] STRATEGIES;

    static {
        MaskType[] types = MaskType.values();
        STRATEGIES = new MaskStrategy[types.length];
        STRATEGIES[MaskType.NAME.ordinal()] = NameMaskStrategy.INSTANCE;
        STRATEGIES[MaskType.PHONE.ordinal()] = PhoneMaskStrategy.INSTANCE;
        STRATEGIES[MaskType.ID_CARD.ordinal()] = IdCardMaskStrategy.INSTANCE;
        STRATEGIES[MaskType.BANK_CARD.ordinal()] = BankCardMaskStrategy.INSTANCE;
        STRATEGIES[MaskType.CVV.ordinal()] = CvvMaskStrategy.INSTANCE;
        STRATEGIES[MaskType.TEL.ordinal()] = TelMaskStrategy.INSTANCE;
        STRATEGIES[MaskType.EMAIL.ordinal()] = EmailMaskStrategy.INSTANCE;
        STRATEGIES[MaskType.FULL_MASK.ordinal()] = CvvMaskStrategy.INSTANCE; // 全量*
        STRATEGIES[MaskType.MIDDLE_MASK.ordinal()] = MiddleMaskStrategy.INSTANCE;
    }

    private MaskStrategyRegistry() {}

    /**
     * 根据 MaskType 获取策略,O(1)
     */
    public static MaskStrategy getStrategy(MaskType type) {
        return STRATEGIES[type.ordinal()];
    }

    // ---- 内部策略 ----

    /**
     * 中间脱敏:保留首尾各1位,中间用*替代
     */
    private enum MiddleMaskStrategy implements MaskStrategy {
        INSTANCE;

        @Override
        public String mask(String rawValue) {
            if (rawValue == null || rawValue.isEmpty()) return rawValue;
            int len = rawValue.length();
            if (len <= 2) {
                char[] r = new char[len];
                for (int i = 0; i < len; i++) r[i] = '*';
                return new String(r);
            }
            StringBuilder sb = new StringBuilder(len);
            sb.append(rawValue.charAt(0));
            for (int i = 1; i < len - 1; i++) sb.append('*');
            sb.append(rawValue.charAt(len - 1));
            return sb.toString();
        }
    }
}

package com.jnliok.sensitive_log.mask;

/**
 * 手机号脱敏:保留前3后4,中间4位用****替代
 * <p>
 * 示例:13812345678 → 138****5678
 * <p>
 * 仅处理11位标准手机号,不足11位使用全量脱敏。
 */
public enum PhoneMaskStrategy implements MaskStrategy {
    INSTANCE;

    private static final int PHONE_LEN = 11;

    @Override
    public String mask(String rawValue) {
        if (rawValue == null || rawValue.isEmpty()) return rawValue;

        // 提取纯数字部分(可能包含空格、横线等)
        char[] chars = rawValue.toCharArray();
        int digitCount = 0;
        for (char c : chars) {
            if (c >= '0' && c <= '9') digitCount++;
        }

        if (digitCount < PHONE_LEN) {
            return fullMask(rawValue);
        }

        // 保留前3后4,中间替换为*
        char[] result = new char[chars.length];
        int digitPos = 0;
        for (int i = 0; i < chars.length; i++) {
            char c = chars[i];
            if (c >= '0' && c <= '9') {
                digitPos++;
                if (digitPos <= 3 || digitPos > digitCount - 4) {
                    result[i] = c;
                } else {
                    result[i] = '*';
                }
            } else {
                result[i] = c;
            }
        }
        return new String(result);
    }
}

0

评论区