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

标签 jsf jsf-2 internationalization external resourcebundle

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

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

两个可选要求是更改文本和刷新应用程序中的消息而不必重新启动应用程序(优先级 2),以及在 WAR 中拥有一个默认包,该包被外部包覆盖(优先级 3)。

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

是否有人以这种方式使用了这种机制,并且可以向我指出一些关于细节的示例/描述,或者有更好的想法来实现列出的要求?

最佳答案

How to put JSF message bundle outside of WAR?



两种方式:
  • Add its path to the runtime classpath of the server .
  • 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 本身,那么您可以简单地在 save 方法中执行缓存清理。但是,如果更改文本可以在外部发生,那么您需要注册 file system watch service监听更改( tutorial here ),然后对于方式 1 清除包缓存,或者对于方式 2 在 handleGetObject() 内部重新加载.

    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>
    

    关于jsf - 如何将 JSF 消息包放在 WAR 之外,以便无需重新部署即可对其进行编辑?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35086113/

    相关文章:

    java - 如何获取与按钮相同的数据表行中的输入组件的值?

    javascript - 使用 JSF 1.1 从 WAS 6.1 迁移到 8.5

    java - Primefaces/JSF 按钮的操作处理程序不会触发相关 bean 的方法

    angular - 我如何在 Angular 中重复使用 i18n 翻译键?

    java - 参数化 h :commandButton

    validation - 验证错误时未突出显示 PrimeFaces 输入组件

    java - Servlet 过滤器不工作

    jsf - 为什么 mojarra 2.1 在每个模块启动时扫描所有其他 war ?

    java - h :commandButton multiple actions: download file and render ajax table

    php - 进行多种语言的翻译