前言
Thymeleaf 丰富的扩展性为我们实现自定义的标签实现了可能。这里以创建数据脱敏标签这个需求为例,讲解下如何实现自定义的dialect。

需求描述:
controller的model中我们有客户的手机号信息:"phone": "13001234567"。
按照默认的写法,要在页面中展示手机号,HTML模板为:

<p th:text="${phone}"></p>
如果不使用自定义dialect,这里会将完整的手机号展示出来:

<p>13001234567</p>
接下来我们打算打造一个自定义方言:

<p masking:text="${phone}"></p>
实现如下的效果:

<p>13*67</p>
除了首位2个字符全部替换为星号是自定义dialect的默认行为。除此之外,我们还可以使用正则表达式来定义替换规则。比如,除了前两个字符,其余的全替换为星号:

<p masking:text="${phone}" masking:pattern="^.{2}(.*)$"></p>
下面一起来实现这个Thymeleaf自定义方言。

实现自定义标签处理器
对于Thymeleaf方言,自定义标签的处理逻辑是在标签处理器定义的。
自定义标签处理器需要实现AbstractAttributeTagProcessor 接口,标签的处理逻辑在doProcess 方法中编写。

数据脱敏标签的处理器代码如下所示:

public class DataMaskingDialectTagProcessor extends AbstractAttributeTagProcessor {

    private static final String TEXT_ATTRIBUTE = "text";

    private static final String PATTERN_ATTRIBUTE = "pattern";

    private static final String DEFAULT_PATTERN = "^.{2}(.*).{2}$";

    public DataMaskingDialectTagProcessor(String prefix) {
        // (1)
        super(TemplateMode.HTML, prefix, null, false, TEXT_ATTRIBUTE, true, 1000, true);
    }

    @Override
    protected void doProcess(ITemplateContext iTemplateContext, IProcessableElementTag iProcessableElementTag, AttributeName attributeName, String s, IElementTagStructureHandler iElementTagStructureHandler) {
        //s为自定义属性text的内容,如果s为表达式,该函数可以获取表达式的值
        final Object value = getExpressionValue(iTemplateContext, s);

        IAttribute patternAttribute = iProcessableElementTag.getAttribute(PATTERN_ATTRIBUTE);
        if (null == patternAttribute) {
            // 设置标签的内容
            iElementTagStructureHandler.setBody(StringUtil.doMasking(value.toString(), DEFAULT_PATTERN), false);
        } else {
            String patternValue = iProcessableElementTag.getAttribute(PATTERN_ATTRIBUTE).getValue();
            iElementTagStructureHandler.setBody(StringUtil.doMasking(value.toString(), patternValue), false);
        }
    }
}

(1)处所示的构造函数的参数比较多,下面为大家列出各个参数的具体含义:

templateMode: 模板模式,这里使用HTML模板。
prefix: 标签前缀。即xxx:text中的xxx。在此例子中prefix为masking。
elementName:匹配标签元素名。举例来说如果是div,则我们的自定义标签只能用在div标签中。为null能够匹配所有的标签。
prefixElementName: 标签名是否要求前缀。
attributeName: 自定义标签属性名。这里为text。
prefixAttributeName:属性名是否要求前缀,如果为true,Thymeeleaf会要求使用text属性时必须加上前缀,即masking:text。
precedence:标签处理的优先级,此处使用和Thymeleaf标准方言相同的优先级。
removeAttribute:标签处理后是否移除自定义属性。
StringUtil类用来进行字符替换。它保留前后n位字符不变,中间部分替换为字符个数相同的星号。

StringUtil.java中的方法doMasking的代码:

public static String doMasking(String target, String patternString) {
    Pattern pattern = Pattern.compile(patternString);
    Matcher matcher = pattern.matcher(target);
    if (matcher.matches()) {
        if (matcher.groupCount() < 1) {
            return target;
        }
        String group = matcher.group(1);
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < group.length(); i++) {
            stringBuilder.append("*");
        }
        return target.replace(group, stringBuilder.toString());
    }
    return target;
}

如果标签中masking:text的属性值是普通字符串的话,这个 自定义方言已经能够满足要求了。但是,text属性值在实际应用时大多数是表达式,比如用来接收model的对象:

masking:text="${phone}"
如何将${phone}解析为phone具体的值呢?请看下面代码:

private Object getExpressionValue(ITemplateContext iTemplateContext, String expressionString) {
    final IEngineConfiguration configuration = iTemplateContext.getConfiguration();
    final IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration);
    // 解析expression
    final IStandardExpression expression = parser.parseExpression(iTemplateContext, expressionString);
    // 获取expression的执行结果
    return expression.execute(iTemplateContext);
}

定义方言类
编写好之定义标签的处理器之后,别忘了定义一个方言类。在方言类中,我们需要给出方言的名称,前缀,处理优先级和涉及到的一系列自定义标签处理器。代码如下所示:

public class DataMaskingDialect extends AbstractProcessorDialect {
    private static final String PREFIX = "masking";
    public DataMaskingDialect() {
        // 方言名称,前缀,处理优先级
        super("Data Masking Dialect", "masking", StandardDialect.PROCESSOR_PRECEDENCE);
    }

    @Override
    public Set<IProcessor> getProcessors(String s) {
        // 把所有的自定义tag处理器加入处理器集,这个例子中我们只有这一个自定义处理器
        final Set<IProcessor> processorSet = new HashSet<>();
        DataMaskingDialectTagProcessor dataMaskingDialectTagProcessor = new DataMaskingDialectTagProcessor(PREFIX);
        processorSet.add(dataMaskingDialectTagProcessor);
        return processorSet;
    }
}

在SpringBoot中加载自定义方言
即将大功告成了。在SpringBoot中使用自定义方言之前,务必要加载自定义的方言类,否则masking:text标签将不会被解析。

@Configuration
public class DialectConfig() {
    @Bean
    public DataMaskingDialect dataMaskingDialect() {
        return new DataMaskingDialect();
    }
}

作者:AlienPaul
链接:https://www.jianshu.com/p/df9c27fc97f7
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Last modification:April 21, 2022
如果觉得我的文章对你有用,请随意赞赏