It seems I can't debug correctly all the stack trace through com.sun.faces.facelets.impl.DefaultFaceletFactory.createFacelet(URL)
, the source code is not aligned with compiled classes for jsf-impl-2.2.12-jbossorg-2.jar
.
To cut a long story short, I rewrote the cache.
With this new cache, createFacelet(URL)
is now called one time for facelet on each request, effectively reloading composite component facelets changes.
This cache implementation it's not fully tested and absolutely it's not production-ready, but it's a start.
Nevertheless it should be thread-safe, because the internal semi-cache is request scoped.
Note that I've used only API imports (javax.faces.*
) and no com.sun.faces.*
, so this should work with any Mojarra/MyFaces 2.2.x implementation.
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's 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 composite coding ;)
UPDATE
I found JRebel interfering with eclipse debugger, so I disabled it and restarted.
And I found some new intersting things:
- The cache implementation with JRebel enabled is read as
com.sun.faces.facelets.impl.DefaultFaceletCache.NoCache
but it is com.sun.faces.util.ExpiringConcurrentCache
instead. That's why I had scrambled source code lines while debugging.
- JSF (and specifically Mojarra) needs a deep refactoring, seriously: there are at least 5 different factories and 2 different caches involved in the creation/caching of facelets and metadata, most doing simple boilerplate delegation job.
com.sun.faces.facelets.impl.DefaultFaceletCache._metadataFaceletCache
and com.sun.faces.application.view.FaceletViewHandlingStrategy.metadataCache
are poorly paired: they contain the very same data and they have dependant-synced unidirectional handling. Conceptually wrong and memory consuming.
- The default facelet refresh period is different from what I thought: it is 2000 instead of 0.
So another workaround is to set:
<context-param>
<param-name>javax.faces.FACELETS_REFRESH_PERIOD</param-name>
<param-value>0</param-value>
</context-param>
in web.xml, but honestly this is much less efficient than my simple cache implementation, because it creates facelets and metadata two times per composite component instance...
Finally, in this debugging session, I've never hit a case where the modified facelet doesn't get refreshed and even if the implementation is monstrously inefficient and schizofrenic, this version (2.2.12) seems to work.
In my case, I think it's a JRebel issue.
However, now I can finally develop with JRebel enabled and facelets reloading.
If I'll hit a hidden case (such as eclipse not copying/updating facelets to target folder and/or not setting last modified file date, on saving from editor) I'll update this answer.
P.S.
They use abstract classes in some case because interfaces are stateless and are not suitable for all conceptual patterns. Single class inheritance is IMO the most serious Java issue. However, with Java 8, we have default/defender methods, which help mitigating the problem.
Nevertheless, they can't be called by JSF ExpressionLanguage 3.0 :(
CONCLUSION
Ok I found the issue.
It's not simple to explain, and requires special (although common) conditions to be reproduced.
Suppose you have:
- FACELET_REFRESH_PERIOD=2
- a composite component named
x:myComp
- a page where
x:myComp
is used 100 times
Now here's what's going on under the hood.
- the first time a
x:myComp
is encountered during page evaluation a cache Record
is created with _creation=System.currentTimeMillis()
- for every other time
x:myComp
is encountered during page evaluation, the Record
retrieved from cache and DefaultFaceletCache.Record.getNextRefreshTime()
is called two times (on get()
and containsKey()
) to verify expiration.
- composite components get evaluated 2 times
- assuming that full page evaluation completes in less than 2 seconds, in the end
DefaultFaceletCache.Record.getNextRefreshTime()
has been called ((100 * 2) - 1) * 2 = 398 times
- when
DefaultFaceletCache.Record.getNextRefreshTime()
is called, it increments an atomic local variable _nextRefreshTime
by FACELET_REFRESH_PERIOD * 1000
= 2000
- so, in the end,
_nextRefreshTime = initial System.currentTimeMillis() + (398 * 2000 = 796 s)
now this facelet will expire in 796 seconds since it has been created. Each access to this page before expiration adds another 796 seconds!
the problem is that cache checking is coupled (2^2 times!!) with life extension.
See JAVASERVERFACES-4107 and JAVASERVERFACES-4176 (and now primarily JAVASERVERFACES-4178) for further details.
Waiting for the issue resolution, I'm using my own cache impl (Java 8 required), maybe it's also useful for you to use/adapt (manually condensed in one single big class, maybe there's some copy'n'paste mistake):
/**
* 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