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, поэтому отключил его и перезапустил.
И я нашел несколько новых интересных вещей:
- Реализация кэша с включенным JRebel читается как
com.sun.faces.facelets.impl.DefaultFaceletCache.NoCache
но этоcom.sun.faces.util.ExpiringConcurrentCache
вместо. Вот почему я зашифровал строки исходного кода при отладке. - JSF (и в особенности Mojarra) нуждается в серьезном рефакторинге, если серьезно: в создании / кэшировании фасетов и метаданных участвуют как минимум 5 разных фабрик и 2 разных кэша, большинство из которых выполняет простую шаблонную работу по делегированию.
com.sun.faces.facelets.impl.DefaultFaceletCache._metadataFaceletCache
а такжеcom.sun.faces.application.view.FaceletViewHandlingStrategy.metadataCache
плохо спарены: они содержат одни и те же данные и имеют однонаправленную обработку с синхронизированной зависимостью. Концептуально неправильно и потребляет память.- Период обновления лица по умолчанию отличается от того, что я думал: это 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:(
ЗАКЛЮЧЕНИЕ
Хорошо, я нашел проблему. Это не просто объяснить, и требует воспроизведения особых (хотя и распространенных) условий.
Предположим, у вас есть:
- FACELET_REFRESH_PERIOD = 2
- составной компонент с именем
x:myComp
- страница где
x:myComp
используется 100 раз
Теперь вот что происходит под капотом.
- первый раз
x:myComp
встречается при оценке страницы кешRecord
создан с_creation=System.currentTimeMillis()
- в любое другое время
x:myComp
встречается во время оценки страницы,Record
извлекается из кэша иDefaultFaceletCache.Record.getNextRefreshTime()
вызывается два раза (наget()
а такжеcontainsKey()
) проверить срок действия. - составные компоненты оцениваются 2 раза
- при условии, что полная оценка страницы завершится менее чем за 2 секунды, в конце
DefaultFaceletCache.Record.getNextRefreshTime()
был вызван ((100 * 2) - 1) * 2 = 398 раз - когда
DefaultFaceletCache.Record.getNextRefreshTime()
вызывается, он увеличивает атомную локальную переменную_nextRefreshTime
отFACELET_REFRESH_PERIOD * 1000
= 2000 - так что, в конце концов,
_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();
}
}
}
}