Mac: JSF: почему веб-приложения JSF на стадии разработки не всегда отслеживают изменения составных компонентов?

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 с той же версией OS X].

Краткое описание проблемы: Несколько дней назад, когда я с удовольствием разрабатывал большое веб-приложение JSF / Primefaces, я обнаружил, что после нескольких перезагрузок сложных страниц JSF / Primefaces, над которыми я работал, перестало обновляться / отражать сделанные мной изменения. (и сохраняется) в составных компонентах. Однако я обнаружил, что если я подожду несколько минут, то смогу снова выполнить перезагрузку, в течение нескольких раз, отражая изменения CC, пока он снова не "застрянет".

Это происходит, насколько я могу судить, только на моей основной машине разработки который является MacBook Pro 15 "(macbookpro11,3 Mid2014.).

[EDIT: 2016-08-14 Теперь воспроизведено также на macmini4,1 Mid2010, работающем в той же версии OS X и работающем (слегка) адаптированном * скопированном * варианте всей той же установки NetBeans / GlassFish NB8.1Beta / GF4.1, и с JSF 2.2.8-17]

Похоже, не имеет значения:

  • Я использую NetBeans-8.1beta / Glassfish4.1 или NetBeans8.1 / Glassfish4.1.1 [ВНЕ: причина, по которой я в основном использую NB8.1beta / GF4.1, а не NB8.1 / GF4.1.1, поясняется по адресу: /questions/12307368/glassfishpayara-pochemu-razvertyivanie-bolshogo-veb-prilozheniya-na-glassfish-411-ili-payara-server-411163-mozhet-zanyat-v-5-raz-bolshe-vremeni-chem-na-glassfish-41

  • Я использую полностью свежую или установленную версию NetBeans + Glassfish.

  • Я использую JDK1.7 (jdk1.7.0_51.jdk) или JDK1.8 (jdk1.8.0_60.jdk) (в том числе для NetBeans / Glassfish и / или для компиляции и выполнения исходного кода).

  • Я использую проект, в котором используется Git (проблема впервые возникла в большом проекте, но с тех пор я воспроизводил его в простейших проектах без Git, то есть он имеет отношение только к тому, что происходит, обнаруживая изменения граней в /build/web /).

  • Я использую Primefaces или нет (я могу добиться этого в очень простом приложении JSF).

  • Я использую чистую перезагрузку GET или перезагрузку команды браузера.

Но это не происходит, насколько я могу судить, с почти идентичной установкой на более старом MacMini (macmini4,1 Mid2010).

[EDIT: 2016-08-14 Да, на MacMini это действительно происходит, если я достаточно часто перезагружаю страницы JSF в полном, большом веб-приложении, которое я разрабатываю, а не просто в мини-тестовом приложении]

Некоторые другие вещи, которые я думаю, я знаю об этом:

  • Это во всех случаях отключено для функции "Развернуть при сохранении".

  • Кажется, что он не затрагивает шаблоны JSF или включает в себя, а только затрагивает составные компоненты.

  • Это не проблема с javax.faces.FACELETS_REFRESH_PERIOD (который по умолчанию для мохарры равен 2). Если я изменяю его на 0, проблема исчезает (кэширование отсутствует), но время загрузки / перезагрузки для больших сложных страниц JSF становится болезненным, в некоторых случаях минуты, а не секунды.

  • Просто переход с одной страницы JSF на другую не помогает.

  • Не имеет значения, какую область применения JSF я использую.

  • Это происходит с приложением, развернутым через /build/web.

  • Временные метки измененных файлов XHTML для составных компонентов определенно меняются, поскольку я сохраняю их в NetBeans (они правильно копируются в /build/web/resources/...).

  • Я не делал никаких обновлений программного обеспечения ОС или установки в течение многих дней.

Я сделал скриншоты (не доступны здесь) всей проблемы, как описано ниже.

Опыт работы с оригинальным очень большим веб-приложением

Когда я впервые столкнулся с проблемой, это было в очень большом веб-приложении. Я заметил это с крошечным составным компонентом, который генерирует некоторый текст с классом стиля (для иконки), который CC использовался внутри a p:accordionPanel и p:tab. Я обнаружил, что после перезагрузки изменений пару раз он перестал ловить изменения. Только случайно я обнаружил, что если я подожду много минут, иногда до 10 минут, это "поймает" изменения.

Затем я вернулся к своим коммитам на несколько дней, в то время, когда я явно мог развиваться без каких-либо проблем, и проблема снова возникла! Я проверял это много раз, независимо от того, в чем проблема, это не в коммите.git (который включает / nbproject / private, но не все подпапки в / nbproject / private).

Опыт работы с небольшим тестовым веб-приложением Primefaces

Затем я попробовал его с гораздо меньшим тестовым веб-приложением с некоторыми тестовыми страницами Primefaces. Я смог воспроизвести проблему, если несколько раз перезагрузил страницу index.xhtml, при этом изменив крошечный составной компонент, состоящий из одной реализации, используемый на странице index.html. Затем я обнаружил, что мне нужно подождать около 10 секунд, а иногда и целую минуту, и тогда изменение снова "зацепится".

Опыт работы с крошечным базовым веб-приложением JSF

С одним index.xhtml и одним составным компонентом с одним словом h:outputText я мог бы решить эту проблему, если бы сохранил CC, а затем очень быстро перезагрузил index.xhtml. Я не говорю о том, что это не похоже на изменение (потому что один "бьет" javax.faces.FACELETS_REFRESH_PERIOD) Я говорю о том, что он "блокируется", чтобы он вообще не улавливал изменения в CC после этого, независимо от того, как часто вы перезагружаете страницу, пока Призрак в Машине не решит сам "разблокировать".

Обычно я действительно приводил бы пример или "Шаги по воспроизведению проблемы", но в этом нет особого смысла; когда я перемещаю тестовый проект с одной машины (мой MacBook Pro) на другую (MacMini работает под управлением той же версии ОС), проблема исчезает. И я могу добиться этого (на своем главном компьютере для разработки MacBook Pro) с помощью простейшего веб-приложения JSF NetBeans с index.xhtml, включающим одну CC.

[EDIT: 2016-08-14 Я действительно могу воспроизвести его на том MacMini, работающем под той же версией ОС, но пока я мог воспроизвести его только с очень большим веб-приложением, которое я разрабатываю, которое не может быть легко предоставлено другим для тестирование (и мне нужно, например, чтобы удалить зависимость от базы данных ObjectDB и предоставить фиктивные данные)]

Я понимаю, что обычно задают один вопрос о Stackru, но ответы на любой из них, которые могут помочь мне двигаться вперед, будут оценены по достоинству:

Q0: Кто-нибудь испытывал что-нибудь подобное (на Mac)?

Q1: Что еще я могу попытаться диагностировать? У меня нет идей.

В2: Кто-нибудь знает что-то специфическое для MacBook Pro, которое может повлиять на опрос / обнаружение изменений в папках сборки / веб-сайта, которые могут это объяснить?

В3: Есть ли что-нибудь о том, как Facelets и / или Glassfish работают вместе с приложением, развернутым через /build/web, что могло бы объяснить это?

1 ответ

Решение

Кажется, я не могу правильно отладить все трассировки стека com.sun.faces.facelets.impl.DefaultFaceletFactory.createFacelet(URL) исходный код не выровнен с скомпилированными классами для jsf-impl-2.2.12-jbossorg-2.jar,

Короче говоря, я переписал кеш.

С этим новым кешем createFacelet(URL) теперь вызывается один раз для facelet при каждом запросе, что эффективно перезагружает изменения фасетов составного компонента.

Эта реализация кеша не полностью протестирована и абсолютно не готова к производству, но это только начало.

Тем не менее, он должен быть ориентирован на многопотоковое исполнение, потому что внутренний полешак является областью запроса

Обратите внимание, что я использовал только импорт 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 разных кэша, большинство из которых выполняет простую шаблонную работу по делегированию.
  3. com.sun.faces.facelets.impl.DefaultFaceletCache._metadataFaceletCache а также com.sun.faces.application.view.FaceletViewHandlingStrategy.metadataCache плохо спарены: они содержат одни и те же данные и имеют однонаправленную обработку с синхронизированной зависимостью. Концептуально неправильно и потребляет память.
  4. Период обновления лица по умолчанию отличается от того, что я думал: это 2000 вместо 0.

Таким образом, другой обходной путь должен установить:

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

в web.xml, но, честно говоря, это гораздо менее эффективно, чем моя простая реализация кэша, потому что он создает лицевые стороны и метаданные два раза для экземпляра составного компонента...

Наконец, в этом сеансе отладки я никогда не сталкивался со случаем, когда модифицированный фасет не обновляется, и даже если реализация чудовищно неэффективна и шизофренична, эта версия (2.2.12), похоже, работает.

В моем случае, я думаю, что это проблема JRebel.

Тем не менее, теперь я могу наконец-то развиваться с включенным JRebel и перезагрузкой Facelets.

Если я обнаружу скрытый случай (например, затмение, не копирующее / не обновляющее фасетки в целевую папку и / или не устанавливающее дату последнего изменения файла, при сохранении из редактора), я обновлю этот ответ.


PS
В некоторых случаях они используют абстрактные классы, потому что интерфейсы не сохраняют состояния и не подходят для всех концептуальных шаблонов. Одноклассное наследование является IMO наиболее серьезной проблемой Java. Однако в Java 8 у нас есть методы по умолчанию /defender, которые помогают решить проблему. Тем не менее, они не могут быть вызваны JSF ExpressionLanguage 3.0:(


ЗАКЛЮЧЕНИЕ

Хорошо, я нашел проблему. Это не просто объяснить, и требует воспроизведения особых (хотя и распространенных) условий.

Предположим, у вас есть:

  1. FACELET_REFRESH_PERIOD = 2
  2. составной компонент с именем x:myComp
  3. страница где x:myComp используется 100 раз

Теперь вот что происходит под капотом.

  1. первый раз x:myComp встречается при оценке страницы кеш Record создан с _creation=System.currentTimeMillis()
  2. в любое другое время x:myComp встречается во время оценки страницы, Record извлекается из кэша и DefaultFaceletCache.Record.getNextRefreshTime() вызывается два раза (на get() а также containsKey()) проверить срок действия.
  3. составные компоненты оцениваются 2 раза
  4. при условии, что полная оценка страницы завершится менее чем за 2 секунды, в конце DefaultFaceletCache.Record.getNextRefreshTime() был вызван ((100 * 2) - 1) * 2 = 398 раз
  5. когда DefaultFaceletCache.Record.getNextRefreshTime() вызывается, он увеличивает атомную локальную переменную _nextRefreshTime от FACELET_REFRESH_PERIOD * 1000 = 2000
  6. так что, в конце концов, _nextRefreshTime = initial System.currentTimeMillis() + (398 * 2000 = 796 s)

Теперь срок действия этого фейслета истекает через 796 секунд с момента его создания. Каждый доступ к этой странице до истечения срока действия добавляет еще 796 секунд!

проблема в том, что проверка кеша связана (2^2 раза!!) с продлением жизни.

См. JAVASERVERFACES-4107 и JAVASERVERFACES-4176 (и теперь в первую очередь JAVASERVERFACES-4178) для получения дополнительной информации.


В ожидании решения проблемы, я использую свой собственный кэш impl (требуется Java 8), возможно, вам также будет полезно использовать / адаптировать (вручную сжатый в один большой класс, возможно, есть какая-то ошибка copy'n'paste):

/**
 * 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();
            }
        }
    }
}
Другие вопросы по тегам