Live Class and Template Reloading
One of the best features of Tapestry is automatic reloading of changed classes and templates. Page and component classes will automatically reload when changed. Likewise, changes to component templates and other related resources will also be picked up immediately. In addition, starting in version 5.2, your service classes will also be reloaded automatically after changes (if you're using Tapestry IoC).
- Template Reloading
- Class Reloading
- Packages Scanned
- File System Only
- Class Loader Issues
- Handling Reloads in your Code
- Checking For Updates
- Troubleshooting Live Class Reloading
When a template changes, all page instances (as well as the hierarchy of components below them) are discarded and reconstructed with the new template. However, classes are not reloaded in this case.
On a change to any loaded class from inside a controlled package (or any sub-package of a controlled package), Tapestry will discard all page instances, and discard the class loader.
Persistent field data on the pages will usually not be affected (as it is stored separately, usually in the session). This allows you to make fairly significant changes to a component class even while the application continues to run.
Only certain classes are subject to reload. Reloading is based on package name; the packages that are reloaded are derived from the application configuration.
If your root package is
org.example.myapp, then only classes in the following packages (and their sub-packages) will be scanned for automatic reloads:
- org.example.myapp.services (Tapestry 5.2 and later, with restrictions)
Starting in Tapestry 5.2, live class reloading includes service implementation classes. There are some limitations to this. See Service Implementation Reloading for more details.
File System Only
Reloading of classes and other files applies only to files that are actually on the file system, and not files obtained from JAR files. This is perfect during development, where the files in question are in your local workspace. In a deployed application, you are somewhat subject to the implementation of your servlet container or application server.
Class Loader Issues
Tapestry uses an extra class loader to load page and component classes.
When a change to an underlying Java class file is detected, Tapestry discards the class loader and any pooled page instances.
You should be careful to not hold any references to Tapestry pages or components in other code, such as Tapestry IoC services. Holding such references can cause significant memory leaks, as they can prevent the class loader from being reclaimed by the garbage collector.
Tapestry's class loader architecture can cause minor headaches when you make use of a services layer, or any time that you pass component instances to objects that are not themselves components.
In such cases you may see ClassCastException errors. This is because the same class name, say org.example.myapp.pages.Start, exists as two different class instances. One class instance is loaded by the web application's default class loader. A second class instance has been loaded and transformed by Tapestry's reloading class loader.
Ordinary classes, such as Tapestry IoC Services, will be loaded by the default class loader and expect instances to be loaded by the same class loader (or a parent).
The solution to this problem is to introduce an interface; the component class should implement the interface, and the service should expect an instance of the interface, rather than a specific type.
It is important that the interface be loaded by the default class loader. It should not be in the pages or components package, but instead be in another package, such as services.
Handling Reloads in your Code
On occasion, you may need to know when invalidations occur, to clear your own cache. For example, if you have a binding that creates new classes, the way PropertyConduitSource does, you need to discard any cached classes or instances when a change is detected in component classes.
You do this by registering a listener with the correct InvalidationEventHub service.
For example, your service may be in the business of creating new classes based on component classes, and keep a cache of those classes:
Here, the service implementation implements the InvalidationEventListener interface, as well as its service interface. The question is: how does it register for notifications?
In your module, you will want to use a service builder method, such as:
This is the intent of service builder methods; to do more than just injecting dependencies.
Checking For Updates
The built in InvalidationEventHub services provide notifications of changes to component classes, to component templates, and to component message catalogs. If you wish to check some other resources (for example, files in a directory of the file system or rows in a database table), you should register as an UpdateListener with the UpdateListenerHub service.
Periodically (the frequency is configurable), UpdateListeners are notified that they should check for updates. Typically, UpdateListeners are also InvalidationEventHubs (or provide InvalidationEventHubs), so that other interested parties can be alerted when underlying data changes.
Troubleshooting Live Class Reloading
- "Production Mode" must be false (in Tapestry 5.3 and later)
- The class must be one that Tapestry instantiates (a page, component, or mixin class, or a Tapestry IOC service implementation that implements an interface)
- Turn on "Build Automatically" in your IDE, or remember to build manually.
- Turn off JVM hot code swapping, if your servlet container supports it.
- Eclipse: Uncheck the "derived" checkbox for the Target dir (in the Project Explorer view, right click on "target", select properties, uncheck "derived" on the Resource tab)
Some of these issues are described in more detail below.
If Live Class Reloading doesn't work
Starting with Tapestry 5.3, Live Class Reloading only works when not in "Production Mode". Check your application module (usually AppModule.java) to be sure you have:
and that this isn't being overridden to "true" on your application's startup command line.
Build Path Issues
Live Class Reloading can fail if your build path isn't set correctly, and the exact configuration may differ between Maven plugin versions and Eclipse versions. The build process must be set to create classes in a folder which is in the servlet container's classpath.
Live Class Reloading won't work correctly with vanilla Tomcat without some tweaks (see below).
Non-Tapestry filters can interfere with LCR. Try disabling other filters in your web.xml file to see if that helps.
Although LCR allows you to see changes without restarting your app, you still need to "build" your project (to compile the Java source into byte code). Your IDE can be set to do this automatically every time you save a file. (In Eclipse, this is done using
Project > Build Automatically.) Alternatively, you can manually trigger a build after you save a file. (In Eclipse, this is done using
Project > Build, or by pressing
Turn off JVM hot code swapping & automatic restarts
Many servlet containers, including Tomcat and Jetty, support various forms of hot code swapping and/or automatic restarts when file changes are detected. These are generally much slower than LCR and usually should be turned off with Tapestry applications. If you're using RunJettyRun plugin for Eclipse, for example, edit your Run Configuration, and on the Jetty tab, click Show Advanced Options and uncheck the Enable Scanner checkbox.
If Live Class Reloading works but is slow
If LCR works for you but is slow (more than a second or two), consider the following.
- Be sure your project source files (your workspace in Eclipse, for example), are on a local drive, NOT a network location. Network drives are always slower, and the file system scanning needed for LCR can add a noticable lag if I/O is slow. If you use Maven, be sure to put your local repository (e.g. ~/.m2/repository) on a local drive for similar reasons.
Java 7 and below: Since LCR adds classes to your PermGen space, you may be running low on PermGen memory (and may eventually get a "java.lang.OutOfMemoryError: PermGen space" error). Try increasing PermGen size with a JVM argument of something like -XX:MaxPermSize=400m. (PermGen settings are not relevant for Java 8 and above.)