/*
* Javassist, a Java-bytecode translator toolkit.
* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. Alternatively, the contents of this file may be used under
* the terms of the GNU Lesser General Public License Version 2.1 or later,
* or the Apache License Version 2.0.
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*/
package javassist.util;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.sun.jdi.Bootstrap;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.VirtualMachine;
import com.sun.jdi.connect.AttachingConnector;
import com.sun.jdi.connect.Connector;
import com.sun.jdi.connect.IllegalConnectorArgumentsException;
import com.sun.jdi.event.Event;
import com.sun.jdi.event.EventIterator;
import com.sun.jdi.event.EventQueue;
import com.sun.jdi.event.EventSet;
import com.sun.jdi.event.MethodEntryEvent;
import com.sun.jdi.request.EventRequest;
import com.sun.jdi.request.EventRequestManager;
import com.sun.jdi.request.MethodEntryRequest;
class Trigger {
void doSwap() {}
}
/**
* A utility class for dynamically reloading a class by
* the Java Platform Debugger Architecture (JPDA), or HotSwap.
* It works only with JDK 1.4 and later.
*
*
Note: The new definition of the reloaded class must declare
* the same set of methods and fields as the original definition. The
* schema change between the original and new definitions is not allowed
* by the JPDA.
*
*
To use this class, the JVM must be launched with the following
* command line options:
*
*
For Java 1.4,
*
java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000
* For Java 5,
*
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
*
* Note that 8000 is the port number used by HotSwapper
.
* Any port number can be specified. Since HotSwapper
does not
* launch another JVM for running a target application, this port number
* is used only for inter-thread communication.
*
*
Furthermore, JAVA_HOME/lib/tools.jar
must be included
* in the class path.
*
*
Using HotSwapper
is easy. See the following example:
*
*
* CtClass clazz = ...
* byte[] classFile = clazz.toBytecode();
* HotSwapper hs = new HostSwapper(8000); // 8000 is a port number.
* hs.reload("Test", classFile);
*
*
* reload()
* first unload the Test
class and load a new version of
* the Test
class.
* classFile
is a byte array containing the new contents of
* the class file for the Test
class. The developers can
* repatedly call reload()
on the same HotSwapper
* object so that they can reload a number of classes.
*
*
{@code HotSwap} depends on the debug agent to perform hot-swapping
* but it is reported that the debug agent is buggy under massively multithreaded
* environments. If you encounter a problem, try {@link HotSwapAgent}.
*
* @since 3.1
* @see HotSwapAgent
*/
public class HotSwapper {
private VirtualMachine jvm;
private MethodEntryRequest request;
private Map newClassFiles;
private Trigger trigger;
private static final String HOST_NAME = "localhost";
private static final String TRIGGER_NAME = Trigger.class.getName();
/**
* Connects to the JVM.
*
* @param port the port number used for the connection to the JVM.
*/
public HotSwapper(int port)
throws IOException, IllegalConnectorArgumentsException
{
this(Integer.toString(port));
}
/**
* Connects to the JVM.
*
* @param port the port number used for the connection to the JVM.
*/
public HotSwapper(String port)
throws IOException, IllegalConnectorArgumentsException
{
jvm = null;
request = null;
newClassFiles = null;
trigger = new Trigger();
AttachingConnector connector
= (AttachingConnector)findConnector("com.sun.jdi.SocketAttach");
Map arguments = connector.defaultArguments();
arguments.get("hostname").setValue(HOST_NAME);
arguments.get("port").setValue(port);
jvm = connector.attach(arguments);
EventRequestManager manager = jvm.eventRequestManager();
request = methodEntryRequests(manager, TRIGGER_NAME);
}
private Connector findConnector(String connector) throws IOException {
List connectors = Bootstrap.virtualMachineManager().allConnectors();
for (Connector con:connectors)
if (con.name().equals(connector))
return con;
throw new IOException("Not found: " + connector);
}
private static MethodEntryRequest methodEntryRequests(
EventRequestManager manager,
String classpattern) {
MethodEntryRequest mereq = manager.createMethodEntryRequest();
mereq.addClassFilter(classpattern);
mereq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
return mereq;
}
/* Stops triggering a hotswapper when reload() is called.
*/
@SuppressWarnings("unused")
private void deleteEventRequest(EventRequestManager manager,
MethodEntryRequest request) {
manager.deleteEventRequest(request);
}
/**
* Reloads a class.
*
* @param className the fully-qualified class name.
* @param classFile the contents of the class file.
*/
public void reload(String className, byte[] classFile) {
ReferenceType classtype = toRefType(className);
Map map = new HashMap();
map.put(classtype, classFile);
reload2(map, className);
}
/**
* Reloads a class.
*
* @param classFiles a map between fully-qualified class names
* and class files. The type of the class names
* is String
and the type of the
* class files is byte[]
.
*/
public void reload(Map classFiles) {
Map map = new HashMap();
String className = null;
for (Map.Entry e:classFiles.entrySet()) {
className = e.getKey();
map.put(toRefType(className), e.getValue());
}
if (className != null)
reload2(map, className + " etc.");
}
private ReferenceType toRefType(String className) {
List list = jvm.classesByName(className);
if (list == null || list.isEmpty())
throw new RuntimeException("no such class: " + className);
return list.get(0);
}
private void reload2(Map map, String msg) {
synchronized (trigger) {
startDaemon();
newClassFiles = map;
request.enable();
trigger.doSwap();
request.disable();
Map ncf = newClassFiles;
if (ncf != null) {
newClassFiles = null;
throw new RuntimeException("failed to reload: " + msg);
}
}
}
private void startDaemon() {
new Thread() {
private void errorMsg(Throwable e) {
System.err.print("Exception in thread \"HotSwap\" ");
e.printStackTrace(System.err);
}
@Override
public void run() {
EventSet events = null;
try {
events = waitEvent();
EventIterator iter = events.eventIterator();
while (iter.hasNext()) {
Event event = iter.nextEvent();
if (event instanceof MethodEntryEvent) {
hotswap();
break;
}
}
}
catch (Throwable e) {
errorMsg(e);
}
try {
if (events != null)
events.resume();
}
catch (Throwable e) {
errorMsg(e);
}
}
}.start();
}
EventSet waitEvent() throws InterruptedException {
EventQueue queue = jvm.eventQueue();
return queue.remove();
}
void hotswap() {
Map map = newClassFiles;
jvm.redefineClasses(map);
newClassFiles = null;
}
}