Skip to content

Commit

Permalink
#38 - Automate tests for different Java a framework versions
Browse files Browse the repository at this point in the history
#34 -  webappDir configuration property
#11 - Tomcat 6 support
  • Loading branch information
Jiří Bubník committed Jul 28, 2014
1 parent c01cf10 commit 238b4f5
Show file tree
Hide file tree
Showing 26 changed files with 886 additions and 142 deletions.
43 changes: 26 additions & 17 deletions README.md
Expand Up @@ -28,15 +28,14 @@ Quick start:
===========
### Install
1. download [latest release of DCEVM Java patch](https://github.com/dcevm/dcevm/releases) and launch the installer
(e.g. `java -jar installer-light.jar`)
1. select java installation directory on your disc and press "Install DCEVM as altjvm" button. Java 1.6+ versions are supported.
(e.g. `java -jar installer-light.jar`). Currently you need to select correct installer for Java major version (7/8).
1. select java installation directory on your disc and press "Install DCEVM as altjvm" button. Java 1.7+ versions are supported.
1. download [latest release of Hotswap agent jar](https://github.com/HotswapProjects/HotswapAgent/releases),
unpack `hotswap-agent.jar` and put it anywhere on your disc. For example: `C:\java\hotswap-agent.jar`

### Run your application
1. add following command line java attributes:
<pre>-XXaltjvm=dcevm -javaagent:PATH_TO_AGENT\hotswap-agent.jar</pre> You need to replace PATH_TO_AGENT with an actual
directory. For example `java -XXaltjvm=dcevm -javaagent:c:\java\hotswap-agent.jar YourApp`.
1. add following command line java attributes: `-XXaltjvm=dcevm -javaagent:PATH_TO_AGENT\hotswap-agent.jar` (you
need to replace PATH_TO_AGENT with an actual) directory. For example `java -XXaltjvm=dcevm -javaagent:c:\java\hotswap-agent.jar YourApp`.
See [IntelliJ IDEA](https://groups.google.com/forum/#!topic/hotswapagent/BxAK_Clniss)
and [Netbeans](https://groups.google.com/forum/#!topic/hotswapagent/ydW5bQMwQqU) forum threads for IDE specific setup guides.
1. (optional) create a file named "hotswap-agent.properties" inside your resources directory, see available properties and
Expand All @@ -47,34 +46,41 @@ unpack `hotswap-agent.jar` and put it anywhere on your disc. For example: `C:\ja
HOTSWAP AGENT: 9:49:29.725 INFO (org.hotswap.agent.config.PluginRegistry) - Discovered plugins: [org.hotswap.agent.plugin.hotswapper.HotswapperPlugin, org.hotswap.agent.plugin.jvm.AnonymousClassPatchPlugin, org.hotswap.agent.plugin.hibernate.HibernatePlugin, org.hotswap.agent.plugin.spring.SpringPlugin, org.hotswap.agent.plugin.jetty.JettyPlugin, org.hotswap.agent.plugin.tomcat.TomcatPlugin, org.hotswap.agent.plugin.zk.ZkPlugin, org.hotswap.agent.plugin.logback.LogbackPlugin]
...
HOTSWAP AGENT: 9:49:38.700 INFO (org.hotswap.agent.plugin.spring.SpringPlugin) - Spring plugin initialized - Spring core version '3.2.3.RELEASE'
1. save a resource and/or use the HotSwap feature of your IDE to reload changes
1. save a changed resource and/or use the HotSwap feature of your IDE to reload changes

### What is available?
* Enhanced Java Hotswap - change method body, add/rename a method, field, ... The only unsupported operation
is hierarchy change (change the superclass or remove an interface).
* Reload resource - resources from webapp directory are usually reloaded by application server. But what about
* You can use standard Java Hotswap from IDE in debug mode to reload changed class
* or set autoHotswap property `-XXaltjvm=dcevm -javaagent:PATH_TO_AGENT\hotswap-agent.jar=autoHotswap=true` to reload
changed classes after compilation. This setup allows even reload on production system without restart.
* Automatic configuration - all local classes and resources known to the running Java application are automatically
discovered and watched for reload (all files on local filesystem, not inside JAR file).
* Extra classpath - Need change a runtime class inside dependent JAR? Use extraClasspath property to add any directory
as a classpath to watch for class files.
* Reload resource after a change - resources from webapp directory are usually reloaded by application server. But what about
other resources like src/main/resources? Use watchResources property to add any directory to watch for a resource change.
* Extra classpath - Need change of a class inside dependent jar? Use extraClasspath property to add any directory as
a classpath to watch for class files
* Framework support - through plugin system, many frameworks are supported. New plugins can be easily added.
* Reload without IDE - you can configure the agent to automatically reload changed class file automatically (without IDE).
This may be used to upload changed classes even on a production system without restart (note, that the agent is not stable
enough yet, use at your own risk).
* Fast - until the plugin is initialized, it does not consume any resources or slow down the application (see Runtime overhead for more information)

Should you have any problems or questions, ask at [HotswapAgent forum](https://groups.google.com/forum/#!forum/hotswapagent).

This project is similar to [JRebel](http://zeroturnaround.com/software/jrebel/). Main differences are:

* HotswapAgent (DCEVM) supports Java8!
* HotswapAgent does not need any additional configuration for basic project setup.
* JRebel is currently more mature and contains more plugins.
* JRebel is neither open source nor free.
* JRebel modifies bytecode of all classes on reload. You need special IDE plugin to fix debugging.
* HotswapAgent extraClasspath is similar to JRebel <classpath> configuration
* HotswapAgent adds watchResources configuration

### Examples
See [HotswapAgentExamples](https://github.com/HotswapProjects/HotswapAgentExamples) GitHub project.
The purpose of an example application is:

* to check "real world" plugin usage during plugin development
* complex automate integration tests (check various configurations before a release, see `run-tests.sh` script)
* to check "real world" plugin usage during plugin development (i.e. inside container)
* to provide working solution for typical application setups
* sandbox to simulate issues for existing or new setups

Expand All @@ -83,7 +89,7 @@ General setups will be merged into the master.

Configuration
=============
The basic configuration is configured reload classes and resources from classpath known to the running application
The basic configuration set to reload classes and resources from classpath known to the running application
(classloader). If you need a different configuration, add hotswap-agent.properties file to the classpath root
(e.g. `src/main/resources/hotswap-agent.properties`).

Expand All @@ -95,9 +101,12 @@ Full syntax of command line options is:
-javaagent:[yourpath/]hotswap-agent.jar=[option1]=[value1],[option2]=[value2]

Hotswap agent accepts following options:
* disablePlugin - plugin name to disable. Not that this will compleatly forbid the plugin to load (opposite to disablePlugin
option in hotswap-agent.properties, which will only disable the plugin for a classloader. You can repeat this
option for every plugin to disable.

* autoHotswap=true - watch all .class files for change and automatically Hotswap the class in the running application
(instead of running Hotswap from your IDE debugging session)
* disablePlugin=[pluginName] - disable a plugin. Note that this will completely forbid the plugin to load
(opposite to disablePlugin option in hotswap-agent.properties, which will only disable the plugin for a classloader.
You can repeat this option for every plugin to disable.


How does it work?
Expand Down
Expand Up @@ -57,7 +57,7 @@ public static void parseArgs(String args) {
String option = val[0];
String optionValue = val[1];
if ("disablePlugin".equals(option)) {
disabledPlugins.add(optionValue);
disabledPlugins.add(optionValue.toLowerCase());
} else if ("autoHotswap".equals(option)) {
autoHotswap = Boolean.valueOf(optionValue);
} else {
Expand All @@ -74,7 +74,7 @@ public static void parseArgs(String args) {
* @return true if the plugin is disabled
*/
public static boolean isPluginDisabled(String pluginName) {
return disabledPlugins.contains(pluginName);
return disabledPlugins.contains(pluginName.toLowerCase());
}

/**
Expand All @@ -100,16 +100,18 @@ public static boolean isAutoHotswap() {
*/
private static void fixJboss7Modules() {
String JBOSS_SYSTEM_MODULES_KEY = "jboss.modules.system.pkgs";
String HOTSWAP_AGENT_EXPORT_PACKAGES = "org.hotswap.agent.annotation," +
"org.hotswap.agent.annotation," +
"org.hotswap.agent.command," +
"org.hotswap.agent.config," +
"org.hotswap.agent.logging," +
"org.hotswap.agent.plugin," +
"org.hotswap.agent.util," +
"org.hotswap.agent.watch";


String oldValue = System.getProperty(JBOSS_SYSTEM_MODULES_KEY, null);
System.setProperty(JBOSS_SYSTEM_MODULES_KEY, oldValue == null ? HOTSWAP_AGENT_EXPORT_PACKAGES : oldValue + "," + HOTSWAP_AGENT_EXPORT_PACKAGES);
}

public static final String HOTSWAP_AGENT_EXPORT_PACKAGES =
"org.hotswap.agent.annotation," +
"org.hotswap.agent.command," +
"org.hotswap.agent.config," +
"org.hotswap.agent.logging," +
"org.hotswap.agent.plugin," +
"org.hotswap.agent.util," +
"org.hotswap.agent.watch";
}
Expand Up @@ -7,6 +7,7 @@
import org.hotswap.agent.watch.WatchEvent;
import org.hotswap.agent.watch.WatchEventListener;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
Expand Down Expand Up @@ -75,6 +76,16 @@ private void registerResources(final PluginAnnotation<Watch> pluginAnnotation, f
while (en.hasMoreElements()) {
try {
URI uri = en.nextElement().toURI();

// check that this is a local accessible file (not vfs inside JAR etc.)
try {
new File(uri);
} catch (Exception e) {
LOGGER.trace("Skipping uri {}, not a local file.", uri);
continue;
}


LOGGER.debug("Registering resource listener on classpath URI {}", uri);
registerResourceListener(pluginAnnotation, classLoader, uri);
} catch (URISyntaxException e) {
Expand Down
Expand Up @@ -129,7 +129,8 @@ private void initExtraClassPath() {
URLClassLoaderHelper.prependClassPath((URLClassLoader) classLoader, extraClassPath);
} else {
LOGGER.warning("Unable to set extraClasspath to {} on classLoader {}. " +
"Only URLClassLoader is supported.", Arrays.toString(extraClassPath), classLoader);
"Only URLClassLoader is supported.\n" +
"*** extraClasspath configuration property will not be handled on JVM level ***", Arrays.toString(extraClassPath), classLoader);
}
}
}
Expand Down Expand Up @@ -192,6 +193,23 @@ public URL[] getWatchResources() {
return convertToURL(getProperty("watchResources"));
}

/**
* Return configuration property webappDir as URL.
*/
public URL getWebappDir() {
try {
String webappDir = getProperty("webappDir");
if (webappDir != null && webappDir.length() > 0) {
return resourceNameToURL(webappDir);
} else {
return null;
}
} catch (Exception e) {
LOGGER.error("Invalid configuration value for webappDir: '{}' is not a valid URL or path and will be skipped.", getProperty("webappDir"), e);
return null;
}
}

/**
* List of disabled plugin names
*/
Expand Down
Expand Up @@ -130,13 +130,14 @@ public void initClassLoader(ClassLoader classLoader) {
}

public void initClassLoader(ClassLoader classLoader, ProtectionDomain protectionDomain) {
// parent of current classloader (system/bootstrap)
if (classLoader.equals(getClass().getClassLoader().getParent()))
return;

if (classLoaderConfigurations.containsKey(classLoader))
return;

// parent of current classloader (system/bootstrap)
if (classLoader.equals(getClass().getClassLoader().getParent()))
return;


// transformation
if (classLoaderPatcher.isPatchAvailable(classLoader)) {
Expand Down
Expand Up @@ -141,12 +141,16 @@ public Object initializePlugin(String pluginClass, ClassLoader appClassLoader) {
Class<Object> clazz = getPluginClass(pluginClass);

// skip if the plugin is disabled
if (pluginManager.getPluginConfiguration(appClassLoader).isDisabledPlugin(clazz))
if (pluginManager.getPluginConfiguration(appClassLoader).isDisabledPlugin(clazz)) {
LOGGER.debug("Plugin {} disabled in classloader {}.", clazz, appClassLoader );
return null;
}

// already initialized in this or parent classloader
if (hasPlugin(clazz, appClassLoader))
if (hasPlugin(clazz, appClassLoader, false)) {
LOGGER.debug("Plugin {} already initialized in parent classloader of {}.", clazz, appClassLoader );
return getPlugin(clazz, appClassLoader);
}

Object pluginInstance = instantiate(clazz);
registeredPlugins.get(clazz).put(appClassLoader, pluginInstance);
Expand Down Expand Up @@ -196,14 +200,17 @@ public <T> T getPlugin(Class<T> pluginClass, ClassLoader classLoader) {
*
* @param pluginClass type of the plugin
* @param classLoader classloader of the plugin
* @param checkParent for parent classloaders as well?
* @return true/false
*/
public boolean hasPlugin(Class<?> pluginClass, ClassLoader classLoader) {
public boolean hasPlugin(Class<?> pluginClass, ClassLoader classLoader, boolean checkParent) {
if (!registeredPlugins.containsKey(pluginClass))
return false;

for (Map.Entry<ClassLoader, Object> registeredClassLoaderEntry : registeredPlugins.get(pluginClass).entrySet()) {
if (isParentClassLoader(registeredClassLoaderEntry.getKey(), classLoader)) {
if (checkParent && isParentClassLoader(registeredClassLoaderEntry.getKey(), classLoader)) {
return true;
} else if (registeredClassLoaderEntry.getKey().equals(classLoader)) {
return true;
}
}
Expand Down
Expand Up @@ -108,7 +108,7 @@ public static void init(PluginConfiguration pluginConfiguration, ClassLoader app
LOGGER.debug("Init plugin at classLoader {}", appClassLoader);

// init only if the classloader contains directly the property file (not in parent classloader)
if (!pluginConfiguration.containsPropertyFile()) {
if (!HotswapAgent.isAutoHotswap() && !pluginConfiguration.containsPropertyFile()) {
LOGGER.debug("ClassLoader {} does not contain hotswap-agent.properties file, hotswapper skipped.", appClassLoader);
return;
}
Expand Down
Expand Up @@ -44,7 +44,7 @@ public static void init(PluginManager pluginManager, PluginConfiguration pluginC
LOGGER.debug("Init plugin at classLoader {}", appClassLoader);

// synthetic classloader, skip
if (appClassLoader instanceof WatchResourcesClassLoader.WatchResourcesUrlClassLoader)
if (appClassLoader instanceof WatchResourcesClassLoader.UrlOnlyClassLoader)
return;

// init only if the classloader contains directly the property file (not in parent classloader)
Expand Down Expand Up @@ -84,7 +84,7 @@ public static void init(PluginManager pluginManager, PluginConfiguration pluginC
*/
private void init(URL[] watchResources) {
// configure the classloader to return only changed resources on watchResources path
watchResourcesClassLoader.init(watchResources, watcher);
watchResourcesClassLoader.initWatchResources(watchResources, watcher);

// modify the application classloader to look for resources first in watchResourcesClassLoader
URLClassLoaderHelper.setWatchResourceLoader((URLClassLoader)appClassLoader, watchResourcesClassLoader);
Expand Down
@@ -1,5 +1,7 @@
package org.hotswap.agent.util;

import org.hotswap.agent.command.Command;
import org.hotswap.agent.config.PluginManager;
import org.hotswap.agent.logging.AgentLogger;

import java.lang.instrument.ClassFileTransformer;
Expand All @@ -20,11 +22,19 @@ public class HotswapTransformer implements ClassFileTransformer {

private static AgentLogger LOGGER = AgentLogger.getLogger(HotswapTransformer.class);

/**
* Exclude these classLoaders from initialization (system classloaders). Note that
*/
private static final Set<String> excludedClassLoaders = new HashSet<String>(Arrays.asList(
"sun.reflect.DelegatingClassLoader"
));

protected Map<Pattern, List<ClassFileTransformer>> registeredTransformers = new HashMap<Pattern, List<ClassFileTransformer>>();

// keep track about which classloader requested which transformer
protected Map<ClassFileTransformer, ClassLoader> classLoaderTransformers = new HashMap<ClassFileTransformer, ClassLoader>();

protected Map<ClassLoader, Object> seenClassLoaders = new WeakHashMap<ClassLoader, Object>();

/**
* Register a transformer for a regexp matching class names.
Expand Down Expand Up @@ -100,10 +110,13 @@ public void closeClassLoader(ClassLoader classLoader) {
* @see ClassFileTransformer#transform(ClassLoader, String, Class, java.security.ProtectionDomain, byte[])
*/
@Override
public byte[] transform(ClassLoader classLoader, String className, Class<?> redefiningClass,
ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException {
public byte[] transform(final ClassLoader classLoader, String className, Class<?> redefiningClass,
final ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException {
LOGGER.trace("Transform on class '{}' @{} redefiningClass '{}'.", className, classLoader, redefiningClass);

// ensure classloader initialized
ensureClassLoaderInitialized(classLoader, protectionDomain);

byte[] result = bytes;
try {
// call transform on all registered transformers
Expand All @@ -121,9 +134,44 @@ public byte[] transform(ClassLoader classLoader, String className, Class<?> rede
LOGGER.error("Error transforming class '" + className + "'.", t);
}



return result;
}

/**
* Every classloader should be initialized. Usually if anything interesting happens,
* it is initialized during plugin initialization process. However, some plugins (e.g. Hotswapper)
* are triggered during classloader initialization process itself (@Init on static method). In this case,
* the plugin will be never invoked, until the classloader initialization is invoked from here.
*
* Schedule with some timeout to allow standard plugin initialization process to precede.
*
* @param classLoader the classloader to which this transformation is associated
* @param protectionDomain associated protection domain (if any)
*/
protected void ensureClassLoaderInitialized(final ClassLoader classLoader, final ProtectionDomain protectionDomain) {
if (!seenClassLoaders.containsKey(classLoader)) {
seenClassLoaders.put(classLoader, null);

// ensure the classloader should not be excluded
if (!excludedClassLoaders.contains(classLoader.getClass().getName())) {
// schedule the excecution
PluginManager.getInstance().getScheduler().scheduleCommand(new Command() {
@Override
public void executeCommand() {
PluginManager.getInstance().initClassLoader(classLoader, protectionDomain);
}

@Override
public String toString() {
return "executeCommand: initClassLoader(" + classLoader + ")";
}
}, 1000);
}
}
}


/**
* Transform type to ^regexp$ form - match only whole pattern.
Expand Down

0 comments on commit 238b4f5

Please sign in to comment.