Tags
Introduction
JSF spec allows us to place JSF configuration documents, such as faces-config.xml and *taglib.xml either inside WEB-INF/ of our WAR, or in META-INF/ of JARs included in WEB-INF/lib of our WAR. For JSF annotated classes, they can either be in WEB-INF/classes, or in the included JARs.
But what if we want all these things to work properly without having to package all our JSF dependency projects as jars? Naturally, we never want to deploy like that, but during development it would be really nice, b/c then we could actually make changes to any code inside our JSF dependencies with full hot-swap support, without having to package anything, or to restart the application server! Unfortunately, this is not possible with JSF out-of-the-box…
This article describes a technique I used to work around these limitations of JSF, thus gaining the ability to make direct modifications to my JSF libraries without restarting or repackaging, and achieving the state of coding zen :).
This solution was tested with Mojarra JavaServer Faces 2.1.7, and it is intended to work with Eclipse workspaces. There would probably be small differences in the implementation for other configurations, but the general approach should work everywhere.
Solution
We have 3 problems to solve:
1) Picking up JSF Annotated Classes from other JSF projects in the workspace
This turned out to be the hardest problem to solve.
Normally JSF annotated classes (such as @FacesComponent, @FacesConverter, @FacesRenderer, etc) must be inside a JAR, or in /WEB-INF/classes/. What we need is to pick up annotated classes from other Eclipse projects we depend on, which means that they need to be loaded from our Web Project’s classpath.
There is no way to extend JSF to do this, b/c everything inside AnnotationScanTask and ProvideMetadataToAnnotationScanTask is hard coded. In order to make the necessary changes, we’ll need some AspectJ magic.
The idea is to use Load Time Weaving to advise the call to JavaClassScanningAnnotationScanner.getAnnotatedClasses() and merge results from our own annotation scan with the results coming from the stock JSF implementation.
This can be achieved with a simple aspect, and some code to scan for annotated classes, which is the first part of our solution. I am using Google Reflections here to do the annotation scan inside the packages where I know my JSF libraries will be. Modify this for your own needs.
JsfConfigurationShimForEclipseProjectsAspect.aj:
/** * This is an AspectJ shim used to find more JSF annotated classes during the setup process. * Normally, JSF configuration and JSF annotations are only processed on paths inside our own WAR, and from other jars. * However, in development mode we are interested in linking to DryDock dependencies as local Eclipse projects, rather than jars. * This shim provides a missing extension point, which scans the DryDock project classpath for JSF annotations. * * The other part of this solution is found in <code>EclipseProjectJsfResourceProvider</code> * * Since we are weaving JSF, Load Time Weaving is required, which means that this aspect must be declared in <code>META-INF/aop.xml</code>. * Also, Tomcat must be started with: * <pre> * -javaagent:/fullpath/aspectjweaver-version.jar -classpath /fullpath/aspectjrt-version.jar * </pre> * * @see EclipseProjectJsfResourceProvider * * @author Val Blant */ public aspect JsfConfigurationShimForEclipseProjectsAspect { pointcut sortedFacesDocumentsPointcut() : execution(* ConfigManager.sortDocuments(..)); after() returning (DocumentInfo[] sortedFacesDocuments): sortedFacesDocumentsPointcut() { System.out.println("\n ====== Augmented list of JSF config files detected with JsfConfigurationShimForEclipseProjectsAspect ====== "); for ( DocumentInfo doc : sortedFacesDocuments ) { System.out.println(doc.getSourceURI().toString()); } System.out.println("\n"); } pointcut getAnnotatedClassesPointcut(Set<URI> urls) : execution(* JavaClassScanningAnnotationScanner.getAnnotatedClasses(Set<URI>)) && args(urls); Map<Class<? extends Annotation>, Set<Class<?>>> around(Set<URI> urls): getAnnotatedClassesPointcut(urls) { Map<Class<? extends Annotation>, Set<Class<?>>> oldMap = proceed(urls); Map<Class<? extends Annotation>, Set<Class<?>>> newMap = EclipseJsfDryDockProjectAnnotationScanner.getAnnotatedClasses(); Map<Class<? extends Annotation>, Set<Class<?>>> mergedMap = new AnnotatedJsfClassMerger().merge(oldMap, newMap); return mergedMap; } }
EclipseJsfDryDockProjectAnnotationScanner.java:
/** * Scans DryDock project classpath to find any JSF annotated classes. This scanner is activated by * the <code>JsfConfigurationShimForEclipseProjectsAspect</code>, which requires Load Time Weaving. * * This class should only be used in development! It is part of a solution that allows us to run the app * against locally imported DryDocked projects. * * @see JsfConfigurationShimForEclipseProjectsAspect * @see EclipseProjectJsfResourceProvider * * @author Val Blant */ public class EclipseJsfDryDockProjectAnnotationScanner extends AnnotationScanner { private static final Log log = LogFactory.getLog(EclipseJsfDryDockProjectAnnotationScanner.class); private static Reflections reflections = new Reflections( new ConfigurationBuilder() .addUrls(ClasspathHelper.forPackage("ca.gc.agr.common.web.jsf")) .addUrls(ClasspathHelper.forPackage("ca.ibm.web")) ); public EclipseJsfDryDockProjectAnnotationScanner(ServletContext sc) { super(sc); } public static Map<Class<? extends Annotation>, Set<Class<?>>> getAnnotatedClasses() { Map<Class<? extends Annotation>, Set<Class<?>>> annotatedClassMap = new HashMap<>(); for ( Class<? extends Annotation> annotation : FACES_ANNOTATION_TYPE ) { Set<Class<?>> annotatedClasses = reflections.getTypesAnnotatedWith(annotation); if ( !annotatedClasses.isEmpty() ) { Set<Class<?>> classes = annotatedClassMap.get(annotation); if ( classes == null ) { classes = new HashSet<Class<?>>(); annotatedClassMap.put(annotation, classes); } classes.addAll(annotatedClasses); } } log.info(" ====== Found additional JSF annotated classes from Eclipse classpath ====== \n" + annotatedClassMap); return annotatedClassMap; } @Override public Map<Class<? extends Annotation>, Set<Class<?>>> getAnnotatedClasses(Set<URI> urls) { return getAnnotatedClasses(); } }
AnnotatedJsfClassMerger.java:
/** * Merges 2 maps of JSF annotated classes into one map. * * This class should only be used in development! It is part of a solution that allows us to run the app * against locally imported DryDocked projects. * * @see JsfConfigurationShimForEclipseProjectsAspect * @see EclipseProjectJsfResourceProvider * * @author Val Blant */ public class AnnotatedJsfClassMerger { public Map<Class<? extends Annotation>, Set<Class<?>>> merge( Map<Class<? extends Annotation>, Set<Class<?>>> oldMap, Map<Class<? extends Annotation>, Set<Class<?>>> newMap) { Set<Class<? extends Annotation>> annotations = new HashSet<>(); annotations.addAll(oldMap.keySet()); annotations.addAll(newMap.keySet()); Map<Class<? extends Annotation>, Set<Class<?>>> mergedMap = new HashMap<>(); for ( Class<? extends Annotation> annotation : annotations ) { Set<Class<?>> classes = new HashSet<>(); Set<Class<?>> oldClasses = oldMap.get(annotation); Set<Class<?>> newClasses = newMap.get(annotation); if ( oldClasses != null ) classes.addAll(oldClasses); if ( newClasses != null ) classes.addAll(newClasses); mergedMap.put(annotation, classes); } return mergedMap; } }
Next, we need to properly set up the Load Time Weaver.
First we create src/main/resources/META-INF/aop.xml in our Web Project.
META-INF/aop.xml:
<!-- This file is read by AspectJ weaver java agent. Make sure you specify the following on server startup command line: -javaagent:/fullpath/AgriShare/aspectjweaver-version.jar -classpath /fullpath/AgriShare/aspectjrt-version.jar Also, make sure that you actually compile the aspects specified below. Eclipse can't do it! You'll have to use Gradle for that. --> <aspectj> <aspects> <aspect name="ca.gc.pinss.web.jsf.drydock.eclipse.JsfConfigurationShimForEclipseProjectsAspect"/> </aspects> <weaver options="-verbose -showWeaveInfo -XnoInline"> <include within="com.sun.faces.config.*"/> </weaver> </aspectj>
Now we need to make sure that we start our application with the AspectJ weaver.
- Append the following to your Application Server’s startup JVM parameters:
-javaagent:/home/val/.gradle/caches/modules-2/files-2.1/org.aspectj/aspectjweaver/1.7.4/d9d511e417710492f78bb0fb291a629d56bf4216/aspectjweaver-1.7.4.jar
Note: Use the correct path for your machine!
- Make sure that this jar is first on your Application Server’s classpath:
/home/val/.gradle/caches/modules-2/files-2.1/org.aspectj/aspectjrt/1.7.4/e49a5c0acee8fd66225dc1d031692d132323417f/aspectjrt-1.7.4.jar
Note: Use the correct path for your machine!
And that’s it – now your annotated JSF classes will be picked up directly from project classpath!
To make sure that it is working, look for messages from EclipseJsfDryDockProjectAnnotationScanner in the log. It will have the following heading:
====== Found additional JSF annotated classes from Eclipse classpath ======
You should also see some messages from the AspectJ weaver:
[WebappClassLoader@6426a58b] weaveinfo Join point 'method-execution( com.sun.faces.config.DocumentInfo[] com.sun.faces.config.ConfigManager.sortDocuments(com.sun.faces.config.DocumentInfo[], com.sun.faces.config.FacesConfigInfo))' in Type 'com.sun.faces.config.ConfigManager' (ConfigManager.java:503) advised by afterReturning advice from 'ca.gc.pinss.web.jsf.drydock.eclipse.JsfConfigurationShimForEclipseProjectsAspect' (JsfConfigurationShimForEclipseProjectsAspect.aj:36)
[WebappClassLoader@6426a58b] weaveinfo Join point 'method-execution( java.util.Map com.sun.faces.config.JavaClassScanningAnnotationScanner.getAnnotatedClasses(java.util.Set))' in Type 'com.sun.faces.config.JavaClassScanningAnnotationScanner' (JavaClassScanningAnnotationScanner.java:121) advised by around advice from 'ca.gc.pinss.web.jsf.drydock.eclipse.JsfConfigurationShimForEclipseProjectsAspect' (JsfConfigurationShimForEclipseProjectsAspect.aj:45)
2) Picking up Taglibs from other JSF Projects in the Workspace
This one is easy in comparison.
All we need to do here is to specify an additional custom FacesConfigResourceProvider.
EclipseProjectJsfResourceProvider.java:
/** * This custom resource provider is used for finding JSF Resources located in other Eclipse Projects, rather * than jars. JSF spec does not support this, but it is very useful for running DryDocked projects inside the local Eclipse workspace. * * In order to enable this resource provider, this class's name must be specified in * <code>META-INF/services/com.sun.faces.spi.FacesConfigResourceProvider</code> * * <b>NOTE:</b> The Gradle build will not include the com.sun.faces.spi.FacesConfigResourceProvider file, b/c we never want this * customization to be deployed - it's for development only. * * @see JsfConfigurationShimForEclipseProjectsAspect * * @author Val Blant */ public class EclipseProjectJsfResourceProvider implements FacesConfigResourceProvider { private static final Log log = LogFactory.getLog(EclipseProjectJsfResourceProvider.class); @Override public Collection<URI> getResources(ServletContext context) { List<URI> unsortedResourceList = new ArrayList<URI>(); try { for (URI uri : loadURLs(context)) { if ( !uri.toString().contains(".jar!/") ) { unsortedResourceList.add(0, uri); } } } catch (IOException e) { throw new FacesException(e); } List<URI> result = new ArrayList<>(); // Then load the unsorted resources result.addAll(unsortedResourceList); log.info(" ====== Found additional JSF configuration resources on Eclipse classpath ====== \n" + result); return result; } private Collection<URI> loadURLs(ServletContext context) throws IOException { Set<URI> urls = new HashSet<URI>(); try { // Turns out these are already grabbed by MetaInfFacesConfigResourceProvider, so we don't need to do it again // for (Enumeration<URL> e = Util.getCurrentLoader(this).getResources("META-INF/faces-config.xml"); e.hasMoreElements();) { // urls.add(new URI(e.nextElement().toExternalForm())); // } URL[] urlArray = Classpath.search("META-INF/", ".taglib.xml"); for (URL cur : urlArray) { urls.add(new URI(cur.toExternalForm())); } } catch (URISyntaxException ex) { throw new IOException(ex); } return urls; } }
To register this provider, we add the following into our Web Project:
src/main/resources/META-INF/services/com.sun.faces.spi.FacesConfigResourceProvider:
ca.gc.agr.common.web.jsf.drydock.eclipse.EclipseProjectJsfResourceProvider
Note: Use the correct package name for your project!
3) Picking up Facelet includes and resources from OTHER JSF PROJECTS IN THE WORKSPACE
This one is also easy.
We create a custom Facelets ResourceResolver.
ClasspathResourceResolver.java:
/** * This is a special Facelets ResourceResolver, which allows us to ui:include resources from * the classpath, rather than from jars. This is necessary in for the Incubator to see stuff * in other projects under META-INF/resources/ * * @author Val Blant */ public class ClasspathResourceResolver extends DefaultResourceResolver { /** * First check the context root, then the classpath */ public URL resolveUrl(String path) { URL url = super.resolveUrl(path); if (url == null) { /* classpath resources don't start with /, so this must be a jar include. Convert it to classpath include. */ if (path.startsWith("/")) { path = "META-INF/resources" + path; } url = Thread.currentThread().getContextClassLoader().getResource(path); } return url; } }
Now we register it in our web.xml:
<!-- This allows us to "ui:include" resources from the classpath, rather than from jars, which is important for working with DryDocked projects directly from our Eclipse workspace --> <context-param> <param-name>facelets.RESOURCE_RESOLVER</param-name> <param-value>ca.gc.agr.common.web.jsf.ClasspathResourceResolver</param-value> </context-param>
And that’s it! We now have everything we need to load all JSF resources from Eclipse projects instead of JARs.
Eclipse Project Setup
All that remains is to reconfigure the Eclipse workspace to start using our new capabilities.
- Import your JSF library projects and all their dependencies into your Eclipse workspace together with the Web Application you are working on.
- Go to all projects that have dependencies on common component jars, delete the jar dependencies, and replace them with project dependencies that are now in your workspace.
- Get rid of any test related project exports from the library projects that might interfere with the running of the app. This may not be necessary depending on your configuration.
- Configure your Application Server classpath to use the Eclipse Projects instead of JARs.
- Configure your build scripts to turn off these modifications, so they don’t get deployed anywhere past your development machine. This is as simple as not including META-INF/services/com.sun.faces.spi.FacesConfigResourceProvider and META-INF/aop.xml in your WAR.
And that’s it.