Mac: JSF:为什么开发阶段的 JSF web 应用程序并不总是捕捉复合组件的变化?

Mac: JSF: why are development stage JSF web apps not always catching composite component changes?

Mac OS X: Yosemite 10.10.5
NetBeans8.1beta or NetBeans8.1
Glassfish4.1 or Glassfish4.1.1
Mojarra 2.2.7 or 2.2.12 [2016-08-14 EDIT: or 2.2.8-17]
[EDIT: Primefaces 5.3]

我是一名经验丰富的 NetBeans + JSF 开发人员,也就是说我知道它应该如何工作,而且通常可以工作,但由于某种原因,这不再正常工作,在一个 (据我所知只有一个) MacBook Pro 机器 [编辑:2016-08-14 并且还在 MacMini 上使用相同的 OSX版本].

问题的简短描述:几天前,当我愉快地开发一个大型JSF/Primefaces web应用程序时,我发现在重新加载了几次之后我正在处理的复杂 JSF/Primefaces 页面停止了我在复合组件中所做(并保存)的 updating/reflecting 更改。但是我发现,如果我等待几分钟,然后我可以再次执行重新加载,几次,反映 CC 更改,直到它再次 "got stuck"。

据我所知,它只发生在我的主要开发机器上,它是 MacBook Pro 15" (macbookpro11,3 Mid2014.) .

[编辑:2016-08-14 现在也在 macmini4,1 Mid2010 运行 相同的 OS X 版本和 运行 a(略) 改编 *copied* 完全相同 NetBeans/GlassFish 设置 NB8.1Beta/GF4.1 的版本,以及 JSF 2.2.8-17]

似乎并不重要:

但据我所知,在较旧的 MacMini (macmini4,1 Mid2010) 上几乎相同的设置并没有发生。

[编辑:2016-08-14 是的,它确实发生在 MacMini 上,如果我在我正在开发的完整的大型 Web 应用程序中足够频繁地重新加载 JSF 页面,而不是只是一个迷你测试应用程序]

我想我知道的其他一些事情:

  • 在所有情况下都关闭“保存时部署”功能。

  • 它似乎不影响 JSF 模板或包含,它似乎只影响复合组件。

  • 这不是 javax.faces.FACELETS_REFRESH_PERIOD 的问题(mojarra 默认为 2)。如果我将其更改为 0,问题就会消失(没有缓存),但是大型复杂 JSF 页面的 load/reload 时间变得很痛苦,在某些情况下是几分钟而不是几秒钟。

  • 只是从一个 JSF 页面移动到另一个没有帮助。

  • 我使用的 JSF 作用域没有区别。

  • 部署在 /build/web 上的应用程序会发生这种情况。

  • 当我将它们保存在 NetBeans 中时,复合组件的已更改 XHTML 文件的时间戳肯定会发生变化(它们被正确复制到 /build/web/resources/...) .

  • 我已经很多天没有进行任何 OS 软件更新或安装了。

我制作了整个问题的截屏视频(此处不可用),如下所述。

使用原始超大型 Web 应用程序的经验

当我第一次遇到这个问题时,它是在一个非常大的网络应用程序中。我注意到它有一个很小的复合组件,它生成一些样式为 class(对于图标)的文本,CC 在 p:accordionPanel 和 p:tab 中使用。我发现在重新加载更改几次后,它会停止捕获更改。我只是偶然发现,如果我等待很多分钟,有时长达 10 分钟,它就会 "catch" 更改。

然后我又回去提交了几天,明明可以毫无问题地开发的时候,问题又来了!我已经测试了很多次,无论问题是什么,它都不在 .git 提交中(包括 /nbproject/private 但不是 /nbproject/private 的所有子文件夹)。

使用较小的 Primefaces 测试网络应用程序的经验

然后我用一个小得多的测试网络应用程序和一些 Primefaces 测试页面进行了尝试。如果我重新加载 index.xhtml 页面几次,同时更改 index.html 页面中使用的一个微小的单实现行复合组件,我就能够重现该问题。然后我发现我必须等待大约 10 秒或有时整整一分钟,然后更改将再次 "catch"。

使用小型 JSF 核心 Web 应用程序的经验

使用一个 index.xhtml 和一个带有单个 h:outputText 单词的复合组件,如果我保存 CC 然后重新加载 index.xhtml 非常可能会出现问题迅速地。我不是在谈论它似乎没有改变(因为一个 "beat" javax.faces.FACELETS_REFRESH_PERIOD)我在谈论它 "locking up" 这样它就不会在那之后完全捕捉到 CC 中的变化, 无论重新加载页面的频率如何,直到 Machine 中的 Ghost 决定 "unlock" 自己。

通常我确实会提供一个示例或 'Steps to reproduce the problem' 但这样做没有什么意义; 当我将测试项目从一台机器(我的 MacBook Pro)移动到另一台机器(MacMini 运行 相同的 OS 版本)时出现问题 我可以使用最简单的 NetBeans JSF Web 应用程序实现它(在我的主要 MacBook Pro 开发机器上),其中 index.xhtml 包含一个 CC。

[编辑:2016-08-14 我确实可以在 MacMini 运行 相同的 OS 版本上重现它,但我只能重现到目前为止,我正在开发的非常大的网络应用程序无法轻易提供给其他人进行测试(例如,我需要去除 ObjectDB 数据库依赖性并提供虚拟数据)]

我知道通常人们会在 Whosebug 上问一个问题,但如果能回答其中任何一个,这可能有助于我前进,我将不胜感激:

Q0:有没有人经历过类似的事情(在 Mac 上)?

Q1:我还能尝试诊断什么?我没主意了。

问题 2:是否有人知道 MacBook Pro 的任何特定内容可能会影响 build/web 文件夹中的 polling/detection 更改,从而解释它?

问题 3:关于 Facelets and/or Glassfish 如何与部署在 /build/web 上的应用程序一起工作,有什么可以解释的吗?

看来我无法通过 com.sun.faces.facelets.impl.DefaultFaceletFactory.createFacelet(URL) 正确调试所有堆栈跟踪,源代码与编译的 classes 不一致 jsf-impl-2.2.12-jbossorg-2.jar.

长话短说,我重写了缓存。

有了这个新缓存,createFacelet(URL) 现在在每次请求时为 facelet 调用一次,有效地重新加载复合组件 facelets 更改。

这个缓存实现还没有经过全面测试,绝对不是生产就绪的,但它是一个开始。

然而它应该是线程安全的,因为内部半缓存是请求范围的。

请注意,我只使用了 API 导入 (javax.faces.*) 而没有使用 com.sun.faces.*,因此这应该适用于任何 Mojarra/MyFaces 2.2.x 实施。

public class DebugFaceletCacheFactory extends FaceletCacheFactory
{
    protected final FaceletCacheFactory wrapped;

    public DebugFaceletCacheFactory(FaceletCacheFactory wrapped)
    {
        this.wrapped = wrapped;
    }

    @Override
    public FaceletCacheFactory getWrapped()
    {
        return wrapped;
    }

    @Override
    public FaceletCache<?> getFaceletCache()
    {
        return new DebugFaceletCache();
    }

    public static class DebugFaceletCache extends FaceletCache<Facelet>
    {
        protected static final String MEMBER_CACHE_KEY = DebugFaceletCache.class.getName() + "#MEMBER_CACHE";

        protected static final String METADATA_CACHE_KEY = DebugFaceletCache.class.getName() + "#METADATA_CACHE";

        protected Map<URL, Facelet> getCache(String key)
        {
            Map<String, Object> requestMap = FacesContext.getCurrentInstance().getExternalContext().getRequestMap();

            Map<URL, Facelet> cache = (Map<URL, Facelet>) requestMap.get(key);
            if(cache == null)
            {
                cache = new HashMap<>();
                requestMap.put(key, cache);
            }

            return cache;
        }

        protected MemberFactory<Facelet> getFactory(String key)
        {
            if(MEMBER_CACHE_KEY.equals(key))
            {
                return getMemberFactory();
            }

            if(METADATA_CACHE_KEY.equals(key))
            {
                return getMetadataMemberFactory();
            }

            throw new IllegalArgumentException();
        }

        protected Facelet getFacelet(String key, URL url) throws IOException
        {
            Map<URL, Facelet> cache = getCache(key);
            Facelet facelet = cache.get(url);
            if(facelet == null)
            {
                MemberFactory<Facelet> factory = getFactory(key);
                facelet = factory.newInstance(url);

                cache.put(url, facelet);
            }

            return facelet;
        }

        @Override
        public Facelet getFacelet(URL url) throws IOException
        {
            return getFacelet(MEMBER_CACHE_KEY, url);
        }

        @Override
        public boolean isFaceletCached(URL url)
        {
            return getCache(MEMBER_CACHE_KEY).containsKey(url);
        }

        @Override
        public Facelet getViewMetadataFacelet(URL url) throws IOException
        {
            return getFacelet(METADATA_CACHE_KEY, url);
        }

        @Override
        public boolean isViewMetadataFaceletCached(URL url)
        {
            return getCache(METADATA_CACHE_KEY).containsKey(url);
        }
    }
}

并通过faces-config.xml激活:

<?xml version="1.0" encoding="utf-8"?>
<faces-config version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd">

    ...
    
    <factory>
        <facelet-cache-factory>it.shape.core.jsf.factory.DebugFaceletCacheFactory</facelet-cache-factory>
    </factory>
</faces-config>

快乐的复合编码;)


更新

我发现 JRebel 干扰了 eclipse 调试器,所以我禁用它并重新启动。

而且我发现了一些有趣的新东西:

  1. 启​​用 JRebel 的缓存实现被读取为 com.sun.faces.facelets.impl.DefaultFaceletCache.NoCache,但它是 com.sun.faces.util.ExpiringConcurrentCache。这就是为什么我在调试时打乱了源代码行。
  2. JSF(特别是 Mojarra)需要深度重构,严重的是:至少涉及 5 个不同的工厂 2 个不同的缓存 在 facelets 和元数据的 creation/caching 中,大多数都在做简单的样板委托工作。
  3. com.sun.faces.facelets.impl.DefaultFaceletCache._metadataFaceletCachecom.sun.faces.application.view.FaceletViewHandlingStrategy.metadataCache 配对不佳 :它们包含完全相同的数据,并且具有依赖同步的单向处理。概念错误且占用内存。
  4. 默认的facelet刷新周期和我想的不一样:是2000而不是0。

所以另一个解决方法是设置:

<context-param>
    <param-name>javax.faces.FACELETS_REFRESH_PERIOD</param-name>
    <param-value>0</param-value>
</context-param>

在 web.xml 中,但老实说,这比我的简单缓存实现效率低得多,因为它为每个复合组件实例创建两次 facelets 和元数据...

最后,在这个调试会话中,我从来没有遇到过修改后的 facelet 没有得到刷新的情况,即使实现效率低得惊人且精神分裂,这个版本 (2.2.12) 似乎可以工作。

就我而言,我认为这是一个 JRebel 问题。

但是,现在我终于可以在启用 JRebel 和重新加载 facelets 的情况下进行开发了。

如果我遇到一个隐藏的案例(例如 eclipse 不是 copying/updating facelets 到目标文件夹 and/or 没有设置最后修改文件日期,从编辑器保存)我会更新这个答案.


P.S.
他们在某些情况下使用抽象 classes,因为接口是无状态的并且不适合所有概念模式。单一 class 继承是 IMO 最严重的 Java 问题。然而,对于 Java 8,我们有 default/defender 方法,这有助于缓解问题。 然而,它们不能被 JSF ExpressionLanguage 3.0 调用 :(


结论

好的,我发现了问题。 解释起来并不简单,需要特殊(虽然常见)的条件才能重现。

假设你有:

  1. FACELET_REFRESH_PERIOD=2
  2. 一个名为 x:myComp
  3. 的复合组件
  4. x:myComp 被使用 100 次的页面

下面是幕后发生的事情。

  1. 在页面评估期间第一次遇到 x:myComp 时,使用 _creation=System.currentTimeMillis()
  2. 创建缓存 Record
  3. 在页面评估期间每隔一次遇到 x:myComp,从缓存中检索的 RecordDefaultFaceletCache.Record.getNextRefreshTime() 被调用两次(在 get()containsKey()) 验证过期。
  4. 复合组件被评估 2 次
  5. 假设整页评估在不到2秒内完成,最后DefaultFaceletCache.Record.getNextRefreshTime()被调用了((100 * 2) - 1) * 2 = 398次
  6. 当调用 DefaultFaceletCache.Record.getNextRefreshTime() 时,它将原子局部变量 _nextRefreshTime 递增 FACELET_REFRESH_PERIOD * 1000 = 2000
  7. 所以,最后,_nextRefreshTime = initial System.currentTimeMillis() + (398 * 2000 = 796 s)

现在这个 facelet 将在创建后的 796 秒后过期。在到期前每次访问此页面都会增加 796 秒!

问题是缓存检查与寿命延长有关(2^2 次!!)。

有关详细信息,请参阅 JAVASERVERFACES-4107 and JAVASERVERFACES-4176 (and now primarily JAVASERVERFACES-4178)。


等待问题解决,我正在使用自己的缓存实现(Java 8 required),也许对你也有用use/adapt(手动压缩成一个大的class,可能有一些复制粘贴错误):

/**
 * A factory for creating ShapeFaceletCache objects.
 *
 * @author Michele Mariotti
 */
public class ShapeFaceletCacheFactory extends FaceletCacheFactory
{
    protected FaceletCacheFactory wrapped;

    public ShapeFaceletCacheFactory(FaceletCacheFactory wrapped)
    {
        this.wrapped = wrapped;
    }

    @Override
    public FaceletCacheFactory getWrapped()
    {
        return wrapped;
    }

    @Override
    public ShapeFaceletCache getFaceletCache()
    {
        String param = FacesContext.getCurrentInstance()
            .getExternalContext()
            .getInitParameter(ViewHandler.FACELETS_REFRESH_PERIOD_PARAM_NAME);

        long period = NumberUtils.toLong(param, 2) * 1000;

        if(period < 0)
        {
            return new UnlimitedFaceletCache();
        }

        if(period == 0)
        {
            return new DevelopmentFaceletCache();
        }

        return new ExpiringFaceletCache(period);
    }

    public static abstract class ShapeFaceletCache extends FaceletCache<Facelet>
    {
        protected static volatile ShapeFaceletCache INSTANCE;

        protected Map<URL, FaceletRecord> memberCache = new ConcurrentHashMap<>();

        protected Map<URL, FaceletRecord> metadataCache = new ConcurrentHashMap<>();

        protected ShapeFaceletCache()
        {
            INSTANCE = this;
        }

        public static ShapeFaceletCache getInstance()
        {
            return INSTANCE;
        }

        protected Facelet getFacelet(FaceletCacheKey key, URL url)
        {
            Map<URL, FaceletRecord> cache = getLocalCache(key);
            FaceletRecord record = cache.compute(url, (u, r) -> computeFaceletRecord(key, u, r));
            Facelet facelet = record.getFacelet();
            return facelet;
        }

        protected boolean isCached(FaceletCacheKey key, URL url)
        {
            Map<URL, FaceletRecord> cache = getLocalCache(key);
            FaceletRecord record = cache.computeIfPresent(url, (u, r) -> checkFaceletRecord(key, u, r));
            return record != null;
        }

        protected FaceletRecord computeFaceletRecord(FaceletCacheKey key, URL url, FaceletRecord record)
        {
            if(record == null || checkFaceletRecord(key, url, record) == null)
            {
                return buildFaceletRecord(key, url);
            }

            return record;
        }

        protected FaceletRecord buildFaceletRecord(FaceletCacheKey key, URL url)
        {
            try
            {
                MemberFactory<Facelet> factory = getFactory(key);
                Facelet facelet = factory.newInstance(url);
                long lastModified = URLUtils.getLastModified(url);
                FaceletRecord record = new FaceletRecord(facelet, lastModified);
                return record;
            }
            catch(IOException e)
            {
                throw new FacesException(e.getMessage(), e);
            }
        }

        protected FaceletRecord checkFaceletRecord(FaceletCacheKey key, URL url, FaceletRecord record)
        {
            return record;
        }

        protected Map<URL, FaceletRecord> getLocalCache(FaceletCacheKey key)
        {
            if(key == FaceletCacheKey.MEMBER)
            {
                return memberCache;
            }

            if(key == FaceletCacheKey.METADATA)
            {
                return metadataCache;
            }

            throw new IllegalArgumentException();
        }

        protected MemberFactory<Facelet> getFactory(FaceletCacheKey key)
        {
            if(key == FaceletCacheKey.MEMBER)
            {
                return getMemberFactory();
            }

            if(key == FaceletCacheKey.METADATA)
            {
                return getMetadataMemberFactory();
            }

            throw new IllegalArgumentException();
        }

        @Override
        public Facelet getFacelet(URL url) throws IOException
        {
            return getFacelet(FaceletCacheKey.MEMBER, url);
        }

        @Override
        public Facelet getViewMetadataFacelet(URL url) throws IOException
        {
            return getFacelet(FaceletCacheKey.METADATA, url);
        }

        @Override
        public boolean isFaceletCached(URL url)
        {
            return isCached(FaceletCacheKey.MEMBER, url);
        }

        @Override
        public boolean isViewMetadataFaceletCached(URL url)
        {
            return isCached(FaceletCacheKey.METADATA, url);
        }

        public void clearFacelets()
        {
            getLocalCache(FaceletCacheKey.MEMBER).clear();
        }

        public void clearViewMetadataFacelets()
        {
            getLocalCache(FaceletCacheKey.METADATA).clear();
        }

        public void clearAll()
        {
            clearViewMetadataFacelets();
            clearFacelets();
        }
    }

    public static class UnlimitedFaceletCache extends ShapeFaceletCache
    {
        public UnlimitedFaceletCache()
        {
            super();
        }
    }

    public static class DevelopmentFaceletCache extends ShapeFaceletCache
    {
        public DevelopmentFaceletCache()
        {
            super();
        }

        @Override
        protected FaceletRecord checkFaceletRecord(FaceletCacheKey key, URL url, FaceletRecord record)
        {
            try
            {
                Set<URL> urls = (Set<URL>) FacesContext.getCurrentInstance()
                    .getAttributes()
                    .computeIfAbsent(key, x -> new HashSet<>());

                if(urls.add(url))
                {
                    long lastModified = URLUtils.getLastModified(url);
                    if(lastModified != record.getLastModified())
                    {
                        return null;
                    }
                }

                return record;
            }
            catch(IOException e)
            {
                throw new FacesException(e.getMessage(), e);
            }
        }
    }

    public static class ExpiringFaceletCache extends ShapeFaceletCache
    {
        protected final long period;

        public ExpiringFaceletCache(long period)
        {
            super();
            this.period = period;
        }

        @Override
        protected FaceletRecord checkFaceletRecord(FaceletCacheKey key, URL url, FaceletRecord record)
        {
            try
            {
                long now = System.currentTimeMillis();
                if(now > record.getLastChecked() + period)
                {
                    long lastModified = URLUtils.getLastModified(url);
                    if(lastModified != record.getLastModified())
                    {
                        return null;
                    }

                    record.setLastChecked(now);
                }

                return record;
            }
            catch(IOException e)
            {
                throw new FacesException(e.getMessage(), e);
            }
        }
    }

    public static class FaceletRecord
    {
        protected final Facelet facelet;

        protected final long lastModified;

        protected long lastChecked;

        public FaceletRecord(Facelet facelet, long lastModified)
        {
            this.facelet = facelet;
            this.lastModified = lastModified;
            lastChecked = System.currentTimeMillis();
        }

        public long getLastModified()
        {
            return lastModified;
        }

        public Facelet getFacelet()
        {
            return facelet;
        }
        
        public long getLastChecked()
        {
            return lastChecked;
        }

        public void setLastChecked(long lastChecked)
        {
            this.lastChecked = lastChecked;
        }
    }

    public static enum FaceletCacheKey
    {
        MEMBER,
        METADATA;

        @Override
        public String toString()
        {
            return getClass().getName() + "." + name();
        }
    }

    public static class URLUtils
    {
        public static long getLastModified(URL url) throws IOException
        {
            URLConnection urlConnection = url.openConnection();

            if(urlConnection instanceof JarURLConnection)
            {
                JarURLConnection jarUrlConnection = (JarURLConnection) urlConnection;
                URL jarFileUrl = jarUrlConnection.getJarFileURL();

                return getLastModified(jarFileUrl);
            }

            try(InputStream input = urlConnection.getInputStream())
            {
                return urlConnection.getLastModified();
            }
        }
    }
}