【问题标题】:ResourceBundle with ClassLoader带有 ClassLoader 的 ResourceBundle
【发布时间】:2021-03-12 11:38:57
【问题描述】:

我想构建仅包含部分键/值的 ResourceBundles 属性文件中的对,从而将大量文件保存为 与将这些部分或部分中的每一个存储在单个文件中相比。 这些部分由以“#”开头的标题行标记并与 由一个空行彼此。下面代码之后的示例属性文件包含两个部分:

  • #FileChooser
  • #OptionPane

  我试图通过传递一个自定义的 ClassLoader 来实现这一点,它只读 所需的部分,在 getBundle(...) 方法中。 CustomClassLoader 工作 很好地减少了键定义,但 ResourceBundle 仍然包含属性文件的所有键。

import java.io.*;
import java.util.*;

public class ResourceReader {

  public ResourceReader() {
    Locale locale= Locale.getDefault();
    ResourceBundle i18n= ResourceBundle.getBundle("ComponentBundle", locale,
                    new CustomClassLoader("#FileChooser"));
    Enumeration<String> enu= i18n.getKeys();
    System.out.println("Keys of ResourceBundle");
    printEnumeration(enu);
  }


  public static void main(String args[]) {
    new ResourceReader();
  }

  public void printEnumeration(Enumeration<String> enu) {
    int i= 1;
    while (enu.hasMoreElements()) {
      System.out.println(i+".: "+enu.nextElement());
      i++;
    }
  }


//////////////////////////////////////////////////////////////////////////////

  public class CustomClassLoader extends ClassLoader {
    String section;

    public CustomClassLoader(String section) {
      this.section= section;
    }

    @Override
    public Class findClass(String name) throws ClassNotFoundException {
      byte[] b = loadClassFromFile(name);
//System.out.writeBytes(b); // OK.
      return defineClass(name, b, 0, b.length);
    }
 
    private byte[] loadClassFromFile(String fileName)  {
      InputStream inputStream = getClass().getClassLoader().getResourceAsStream(
                      fileName.replace('.', File.separatorChar) + ".properties");
      byte[] buffer;
      ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
      int nextValue = 0;
      try {
        while ( (nextValue = inputStream.read()) != -1 ) {
          byteStream.write(nextValue);
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
      buffer = extractSection(byteStream.toString(), section);
      return buffer;
    }

    private byte[] extractSection(String stream, String caption)  {
      final String LINE_SEP= System.getProperty("line.separator", "\n");
      String[] lines= stream.split(LINE_SEP);
//    Detect first and last line (exclusive) of section.
      int iEnd= 0, iStart= -1;
      for (int i=0; i<lines.length; i++) {
        lines[i]= lines[i].trim();
        if (iStart==-1) {
          if (!lines[i].equals(caption)) continue;
          iStart= i+1;
          i++;
        }
        else if (lines[i].isEmpty()) {
          iEnd= i;
          break;
        }
      }
      if (iEnd==0) iEnd= lines.length+1;
      StringBuilder sb= new StringBuilder();
      for (int i=iStart; i<iEnd; i++)
        sb.append(lines[i]+LINE_SEP);
      return sb.toString().getBytes();
    }
  }

}
//////////////////////////////////////////////////////////////////////////////

/* // 文件 ComponentBundle.properties

#FileChooser
acceptAllFileFilterText= All files (*.*)
cancelButtonText= Cancel
cancelButtonToolTipText= Cancel

#OptionPane
Cancel= Cancel
Input= Input
Message= Message
No= No
// End of ComponentBundle.properties

*/

【问题讨论】:

    标签: java classloader resourcebundle


    【解决方案1】:

    您不会以这种方式获取受限资源包,因为您已覆盖 findClass 以将属性文件的字节作为类返回。要了解发生了什么,请添加以下代码:

    public URL getResource(String name)
    {
        var url = super.getResource(name);
        System.out.println("getResource "+name+" -> "+url);
        return url;
    }
    public Class findClass(String name) throws ClassNotFoundException {
        System.out.println("findClass "+name);
    ...
    

    然后您可以看到 ResourceBundle 的内部工作原理,它会找到所有键,因为它正在加载文件 url 的内容 - 并且您的代码 被使用:

    findClass ComponentBundle
    getResource ComponentBundle.properties -> file:/C:/some/path/to/ComponentBundle.properties
    findClass ComponentBundle_en
    getResource ComponentBundle_en.properties -> null
    findClass ComponentBundle_en_GB
    getResource ComponentBundle_en_GB.properties -> null
    

    如果您覆盖 getResource(String name) 并使其生成适合密钥子集的文件,并将 URL 传递回子集文件,则可以让您的捆绑器工作。

    如果您可以为所有应用程序定义一个资源包文件,或者为每个子组件定义一个资源包文件,这似乎需要做很多工作。

    【讨论】:

    • 感谢 DuncG 的澄清演示。由于我对 ClassLoader 类完全没有经验,您能否确认 findClass(String name) 方法仅用于查找已编译的类?因为我想知道该方法没有在我的代码中引发异常,实际上确实读取了 System.out.writeBytes(b); // OK. 行所证明的属性文件。再次感谢。
    • 对于大多数正常情况,您不需要定义自己的 ClassLoader。 ResourceBundle.getBundle 正在为 (default, lang, lang_locale) 的每个排列搜索 [Class, properties] 的定义三次。 findClass 调用将返回一个 Class 实例。在您的情况下,您的 findClass 发回由属性流的 byte[] 制成的损坏的类。将 getResource() return url 更改为 return null,您将看到已被忽略的异常(类具有错误的幻数)。
    • 返回 null 会给出明确的错误消息:java.util.MissingResourceException: Can't find bundle for base name ComponentBundle, locale de_DE。您知道getBundle(... ClassLoader loader) 方法有用的场景吗?
    • 使用getBundle(String baseName, Locale locale) 更为常见。 loader 参数允许类或资源文件的非标准位置,但它似乎不太有用,因为getBundle 调用ClassLoader.getResource 而不是getResourceAsStream 用于基础数据。
    • 标准方法是为自定义格式实现ResourceBundle.Control。我的建议是使用 XML 格式,因为它免费提供这样的逻辑;每个部分都可以是一个 XML 元素,每个部分的属性都可以是 XML 属性。
    【解决方案2】:

    如果一个人绝对想使用 ResourceBundle 的一个子集,这可能是一种方法 - 虽然不是很优雅,因为需要读取和过滤完整的包。

    int componentFlag= ...;
    final int FILE_CHOOSER= 1, OPTION_PANE= 2;
    
    ResourceBundle i18n= ResourceBundle.getBundle("ComponentBundle", locale);
    String prefix;
    Set<String> set= i18n.keySet();
    if ((componentFlag&FILE_CHOOSER)>0)
      prefix= "FileChooser.";
    else if ((componentFlag&OPTION_PANE)>0)
      prefix= "OptionPane.";
    set.stream().filter(s -> s.startsWith(prefix))
        .forEach(s -> UIManager.put(s, i18n.getString(s)));
    

    【讨论】:

      猜你喜欢
      • 2023-03-24
      • 2012-03-30
      • 2017-03-22
      • 2023-04-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-09-27
      • 1970-01-01
      相关资源
      最近更新 更多