返回

Java转义字符处理:代码直输与属性文件对比

java

好的,以下是一篇关于在Java代码中直接转义字符与在属性文件中转义字符的技术博客文章:

Java中转义字符处理:代码直输 vs 属性文件

Java 开发者在处理字符串时,会碰到需要使用反斜杠 \ 进行字符转义的情况。通常使用方式有2种,将字符串硬编码到代码中与把字符串写到属性(*.properties)文件中。那这两种处理方式有什么不同,又有什么坑需要避免呢?

问题的产生

当尝试在 Java 代码中定义包含特殊字符(如\W)的正则表达式字符串时,会出现编译错误。这是因为Java 编译器会将反斜杠视为转义字符的开始,试图转义紧跟其后的字符。在正则表达式中,\W表示“非单词字符”。

示例:

testRegex ="[ \t]*(Not\Wknown)[ \\t]*";  // 编译错误

如果把相同字符串存储在属性文件中,然后通过 System.getProperty()读取,则编译正常。

app.properties

regex.expr = [ \t]*(Not\Wknown)[ \\t]*

MyClass.java

testRegex = System.getProperty("regex.expr"); // 编译正常

很自然的会有一个疑问,编译器自动转义了 \W吗?

原理解析:Java字符串与属性文件

理解问题本质,我们需要分别搞清楚这两种处理字符串的内在机制。

Java编译器处理字符串 :

在 Java 源代码中,字符串字面量用双引号括起来,由Java 编译器处理。编译器遇到反斜杠 \ 时,它会根据既定的Java语法规范来检查后面跟着的字符是什么来决定如何转义。

  • 对于 \t\n\r\"\'\\ 等,它会将其替换为制表符、换行符、回车符、双引号、单引号和反斜杠。
  • 对于其他字符,如 \W,编译器不知道要替换成什么。根据Java规范是非法转义组合,会直接抛出编译错误。

属性文件处理 :

属性文件是由java.util.Properties 类来加载和处理。Properties 类不以Java编译器的规则解释转义字符。反斜杠 \ 被视为普通字符,除非它是以下特定转义序列的一部分:

  • \t(制表符)
  • \n(换行符)
  • \f(换页符)
  • \r(回车符)
  • \\(反斜杠)
  • \uxxxx(Unicode字符,其中xxxx是四个十六进制数字)

其他的 \ 不会被解释为任何形式的转义,只会作为字面上的反斜杠 \

因此属性文件中的字符串 regex.expr = [ \t]*(Not\Wknown)[ \\t]* 中的 \W 实际上是两个独立的字符:反斜杠和字母 “W”。这2个字符并不会像Java代码中那样直接解释为 非单词的正则表达式匹配符号,而是作为字面字符串 "\\W" 存储和读取的。在应用中通过正则表达式引擎处理 testRegex 字符串时,"\\W" 才被解释为 非单词

结论 :

Java编译器没有在属性文件场景中自动转义\W。差异来源于Java编译器和 java.util.Properties 类各自处理字符串的规则区别。Java编译器根据Java语言规范严格执行转义,而 Properties 类有自己的转义规则,没有那么严格,更趋向于“逐字”解析字符串。

解决及最佳实践

掌握以上原理后,我们会注意到使用这两种方式存储的字符串,实际上是2种内容,也需要应用不同的应对方案。

方案一:使用转义字符

如果字符串被存储到代码中。需要使用\\ 来正确处理反斜杠 \ 。这向 Java 编译器发出信号,表示应将 \\W 解释为文字反斜杠 \ 后跟字母 “W”,然后才是正则表达式\W

操作步骤:

  1. 将字符串 "\\W" 替换所有单独的 \

代码示例:

String testRegex = "[ \\t]*(Not\\Wknown)[ \\t]*"; // 编译通过,存储的是一个\与W字符,无转义字符干扰
testRegex = testRegex.replace("\\", "\\\\");//运行时才能被正则表达式转义为\W,代表非单词

方案二:修改字符串的存储方式

如果把字符串存入到*.properties中,那必须得要考虑上文中讨论到的两者对\处理的不同机制了。属性文件并不会对\W\做为转义标记,导致直接放入的话会生成与预期不一致的内容。

我们得主动使用\\,在属性文件中获得类似在代码中使用的\字符。

操作步骤:

  1. 修改属性文件中的字符串,将 \ 替换为 \\

app.properties

regex.expr = [ \\t]*(Not\\\\Wknown)[ \\t]*  

在这个情况下,由于properties文件的特殊性,第一个反斜杠会对第二个反斜杠做一次转义,最后存储下类似\的一个字符串内容。当Java的 Properties 类加载并解析 regex.expr 的值时,由于其并不会识别\W\作为转义,其又会将每一个\\解释成单个反斜杠 \,又会在运行时得到一个单个 \W 。这之后又再次被正则表达式引擎读取到,得到非单词符号的含义。

经过3个环节才最终让代码与配置文件里的2种不同存储形式都得到统一正确的结果。

方案三:外部化配置

把经常更改的配置(正则表达式)从源代码中提取出来,转移到外部配置文件。可显著的增强程序灵活性和可维护性。这样每次配置改动不必重新编译代码。如果未来需要处理其他的转义字符串也可以按照上述方案快速得到所需字符串。

操作步骤:

  1. 在项目资源目录下创建一个新的属性文件(例如 config.properties)。
  2. 在属性文件中添加配置项。
  3. 使用 Properties 类加载配置文件。
  4. 通过键名读取配置值。

建议和额外的安全建议

以下提供额外的安全建议和技巧以更好应用与管理配置:

  • 避免存储敏感信息: 不要在属性文件存储密码,密钥等隐私信息。攻击者可能轻易获取到纯文本的信息。如确有需要,考虑加解密方式读取和存储。
  • 规范文件格式和位置:统一将所有的配置文件放置于同一目录夹,不要和 .class 的字节码文件混淆放置,以便管理权限与发布。避免 app.properties 之类无含义名称,并添加必要的注释信息。
  • 代码中校验属性文件值: 当读取*.properties属性后,程序添加对应逻辑校验,防止属性值缺失或者设置错误造成的异常崩溃。

按照这三个方案即可有效的避免上述坑点。