如何将 JSF 消息包放在 WAR 之外,以便无需重新部署即可对其进行编辑?

How to put JSF message bundle outside of WAR so it can be edited without redeployment?

我们在 WildFly 8 上有一个 JSF 应用程序,它使用国际化文本的传统机制,方法是在 WAR 的 WEB-INF\classes 文件夹中包含德语和英语的消息包,并在 faces-config.xml 将名称映射到它并列出语言环境。该应用程序没有数据库连接,但使用 REST 服务与第二个应用程序通信。

现在我们需要能够更轻松地更改文本,这意味着在更改文本时不必构建新的 WAR 文件和进行部署。所以我需要一种机制来将消息捆绑在 WAR 之外,同时能够像以前一样在 XHTML 页面中使用它。

两个可选要求是更改文本并刷新应用程序中的消息而无需重新启动应用程序(优先级 2),并在 WAR 中有一个默认包,它被外部包(优先级 3)。

我的想法是使用类似于 Apache 公共配置的东西来读取应用程序范围 bean 中的 属性 文件,并在之前使用的 EL 名称下公开 getter。但不知何故,感觉就像必须重新实现现有机制,而且这应该更容易,甚至可能仅使用 Java EE 核心。

是否有人以这种方式使用过此机制并且可以向我指出一些 example/description 的细节或有更好的想法来实现列出的要求?

How to put JSF message bundle outside of WAR?

两种方式:

  1. Add its path to the runtime classpath of the server.

  2. Create a custom ResourceBundle implementation with a Control.


change the text and refresh the messages in the application without having to restart the application

更改文本将是微不足道的。然而,刷新不是微不足道的。 Mojarra 在内部积极缓存它。如果您想选择方法 1,则必须考虑到这一点。Arjan Tijms 在这个相关问题中发布了一个 Mojarra 特定技巧来清除其内部资源包缓存:How to reload resource bundle in web application?

如果更改文本发生在 webapp 本身,那么您可以简单地在保存方法中执行缓存清理。但是,如果更改文本可以在外部发生,那么您需要注册一个 file system watch service to listen on changes (tutorial here),然后对于方式 1 清除包缓存,或者对于方式 2 在 handleGetObject().[=25 内部重新加载=]


have a default bundle within the WAR, which is overwritten by the external bundle

当从类路径加载它们时,默认行为是相反的(WAR 中的资源具有更高的类加载优先级),所以这肯定会破坏方式 1 并留给我们方式 2。

下面是方式 2 的启动示例。这假设您正在使用 属性 基本名称为 text 的资源包(即没有包)并且外部路径位于在 /var/webapp/i18n.

public class YourBundle extends ResourceBundle {

    protected static final Path EXTERNAL_PATH = Paths.get("/var/webapp/i18n");
    protected static final String BASE_NAME = "text";
    protected static final Control CONTROL = new YourControl();

    private static final WatchKey watcher;

    static {
        try {
            watcher = EXTERNAL_PATH.register(FileSystems.getDefault().newWatchService(), StandardWatchEventKinds.ENTRY_MODIFY);
        } catch (IOException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    private Path externalResource;
    private Properties properties;

    public YourBundle() {
        Locale locale = FacesContext.getCurrentInstance().getViewRoot().getLocale();
        setParent(ResourceBundle.getBundle(BASE_NAME, locale, CONTROL));
    }

    private YourBundle(Path externalResource, Properties properties) {
        this.externalResource = externalResource;
        this.properties = properties;
    }

    @Override
    protected Object handleGetObject(String key) {
        if (properties != null) {
            if (!watcher.pollEvents().isEmpty()) { // TODO: this is naive, you'd better check resource name if you've multiple files in the folder and keep track of others.
                synchronized(properties) {
                    try (InputStream input = new FileInputStream(externalResource.toFile())) {
                        properties.load(input);
                    } catch (IOException e) {
                        throw new IllegalStateException(e);
                    }
                }
            }

            return properties.get(key);
        }

        return parent.getObject(key);
    }

    @Override
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public Enumeration<String> getKeys() {
        if (properties != null) {
            Set keys = properties.keySet();
            return Collections.enumeration(keys);
        }

        return parent.getKeys();
    }

    protected static class YourControl extends Control {

        @Override
        public ResourceBundle newBundle
            (String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
                throws IllegalAccessException, InstantiationException, IOException
        {
            String resourceName = toResourceName(toBundleName(baseName, locale), "properties");
            Path externalResource = EXTERNAL_PATH.resolve(resourceName);
            Properties properties = new Properties();

            try (InputStream input = loader.getResourceAsStream(resourceName)) {
                properties.load(input); // Default (internal) bundle.
            }

            try (InputStream input = new FileInputStream(externalResource.toFile())) {
                properties.load(input); // External bundle (will overwrite same keys).
            }

            return new YourBundle(externalResource, properties);
        }

    }

}

为了获得运行,请在faces-config.xml中进行如下注册。

<application>
    <resource-bundle>
        <base-name>com.example.YourBundle</base-name>
        <var>i18n</var>
    </resource-bundle>
</application>