It seems I can not properly debug the entire stack trace via com.sun.faces.facelets.impl.DefaultFaceletFactory.createFacelet(URL)
, the source code is not aligned with the compiled classes for jsf-impl-2.2.12-jbossorg-2.jar
.
To shorten the long story, I rewrote the cache.
With this new cache, createFacelet(URL)
now called one time for facelet per request, effectively reloading component changes to composite components.
This cache implementation is not fully tested and absolutely not ready for production, but this is the beginning.
However, it must be thread safe because the internal half-case has a request scope.
Please note that I used only the import API ( javax.faces.*
) And not com.sun.faces.*
, So this should work with any version of 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); } } }
and it is activated through 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>
Happy combined coding;)
UPDATE
I found that JRebel was interfering with the eclipse debugger, so I disabled it and restarted.
And I found some new interesting things:
- A cache implementation with JRebel enabled is read as
com.sun.faces.facelets.impl.DefaultFaceletCache.NoCache
, but instead it is com.sun.faces.util.ExpiringConcurrentCache
. This is why I had scrambled lines of source code during debugging. - JSF (and especially Mojarra) needs deep refactoring, seriously: at least 5 different factories and 2 different caches involved in creating / caching masks and metadata, most of which do the simple job of delegating templates.
com.sun.faces.facelets.impl.DefaultFaceletCache._metadataFaceletCache
and com.sun.faces.application.view.FaceletViewHandlingStrategy.metadataCache
poorly paired : they contain the same data and they have synchronization-dependent unidirectional processing. Conceptually wrong and consumed memory.- The period for updating the background in the default mode is different from what I thought: it is 2000 instead of 0.
So another workaround is to install:
<context-param> <param-name>javax.faces.FACELETS_REFRESH_PERIOD</param-name> <param-value>0</param-value> </context-param>
in web.xml, but to be honest, it is much less efficient than my simple cache implementation, because it creates layouts and metadata twice per instance of a composite component ...
Finally, in this debugging session, I never came across a situation where the modified dummy is not updated, and even if the implementation is monstrously inefficient and schizophrenic, this version (2.2.12) seems to work.
In my case, I think this is a JRebel problem.
However, now I can finally start by rebooting JRebel and translating the masks.
If I delete a hidden case (for example, eclipse does not copy / update the checkboxes to the target folder and / or does not set the date of the last modified file when saving from the editor), I will update this answer.
PS
In some cases, they use abstract classes because interfaces are stateless and not suitable for all conceptual templates. Unambiguous inheritance is IMO's most serious Java issue. However, with Java 8, we have default / defender methods that help mitigate the problem. However, they cannot be called by JSF ExpressionLanguage 3.0 :(
Conclusion
Ok, I found a problem. This is not easy to explain and requires special (albeit general) conditions for reproduction.
Suppose you have:
- FACELET_REFRESH_PERIOD = 2
- component named
x:myComp
- page where
x:myComp
used 100 times
Now here's what happens under the hood.
- The first time a
x:myComp
is encountered during page evaluation, a Record
cache is created using _creation=System.currentTimeMillis()
- during each other time
x:myComp
occurs during page evaluation, Record
retrieved from the cache, and DefaultFaceletCache.Record.getNextRefreshTime()
is called twice (in get()
and containsKey()
) to check the expiration date. - components are evaluated 2 times
- Assuming that a full page score is completed in less than 2 seconds,
DefaultFaceletCache.Record.getNextRefreshTime()
((100 * 2) - 1) * 2 = 398 times was called at the end - when
DefaultFaceletCache.Record.getNextRefreshTime()
is called, it increments the local atom variable _nextRefreshTime
by FACELET_REFRESH_PERIOD * 1000
= 2000 - therefore, in the end,
_nextRefreshTime = initial System.currentTimeMillis() + (398 * 2000 = 796 s)
, now this facelet will expire in 796 seconds from the moment of its creation. Each access to this page before the expiration date adds another 796 seconds!
the problem is that checking the cache is (2 ^ 2 times!) related to extending the life span.
See JAVASERVERFACES-4107 and JAVASERVERFACES-4176 (and now basically JAVASERVERFACES-4178 ) for more details.
While waiting for the problem to be resolved, I use my own cache code ( Java 8 is required ), maybe it is also useful for you to use / adapt (manually condensed in one large class, maybe there is some "copy'n'paste" error):
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(); } } } }