I'm trying to integrate my application with Bukkit - A Minecraft Server API.
Basically, I'm trying to figure out how could I get functionality like this to work:
Reflections reflections = new Reflections("com.mycompany");
Set<Class<? extends BlockEvent>> classes = reflections.getSubTypesOf(BlockEvent.class);
for (Class<? extends BlockEvent> clazz : classes) {
getServer().getPluginManager().registerEvents(new BlockListener<clazz>(), this);
}
I want to register a listener for all event types that extend BlockEvent. Events are registered by passing an implemention of Listener into the registerEvents(Listener, Plugin) method exposed by the Bukkit API's PluginManager. Obviously clazz is not a type and cannot be used as such.
Here is the generic class:
public class BlockListener<T extends BlockEvent> implements Listener {
#EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
void onBlock(T event) {
System.out.println("Block Event");
Bukkit.getPluginManager().callEvent(new BlockChangeEvent(event.getBlock()));
}
}
Update
public Map<Class<? extends Event>, Set<RegisteredListener>> createRegisteredListeners(#NotNull Listener listener, #NotNull final Plugin plugin) {
Validate.notNull(plugin, "Plugin can not be null");
Validate.notNull(listener, "Listener can not be null");
boolean useTimings = server.getPluginManager().useTimings();
Map<Class<? extends Event>, Set<RegisteredListener>> ret = new HashMap<Class<? extends Event>, Set<RegisteredListener>>();
Set<Method> methods;
try {
Method[] publicMethods = listener.getClass().getMethods();
Method[] privateMethods = listener.getClass().getDeclaredMethods();
methods = new HashSet<Method>(publicMethods.length + privateMethods.length, 1.0f);
for (Method method : publicMethods) {
methods.add(method);
}
for (Method method : privateMethods) {
methods.add(method);
}
} catch (NoClassDefFoundError e) {
plugin.getLogger().severe("Plugin " + plugin.getDescription().getFullName() + " has failed to register events for " + listener.getClass() + " because " + e.getMessage() + " does not exist.");
return ret;
}
for (final Method method : methods) {
final EventHandler eh = method.getAnnotation(EventHandler.class);
if (eh == null) continue;
// Do not register bridge or synthetic methods to avoid event duplication
// Fixes SPIGOT-893
if (method.isBridge() || method.isSynthetic()) {
continue;
}
final Class<?> checkClass;
if (method.getParameterTypes().length != 1 || !Event.class.isAssignableFrom(checkClass = method.getParameterTypes()[0])) {
plugin.getLogger().severe(plugin.getDescription().getFullName() + " attempted to register an invalid EventHandler method signature \"" + method.toGenericString() + "\" in " + listener.getClass());
continue;
}
final Class<? extends Event> eventClass = checkClass.asSubclass(Event.class);
method.setAccessible(true);
Set<RegisteredListener> eventSet = ret.get(eventClass);
if (eventSet == null) {
eventSet = new HashSet<RegisteredListener>();
ret.put(eventClass, eventSet);
}
for (Class<?> clazz = eventClass; Event.class.isAssignableFrom(clazz); clazz = clazz.getSuperclass()) {
// This loop checks for extending deprecated events
if (clazz.getAnnotation(Deprecated.class) != null) {
Warning warning = clazz.getAnnotation(Warning.class);
WarningState warningState = server.getWarningState();
if (!warningState.printFor(warning)) {
break;
}
plugin.getLogger().log(
Level.WARNING,
String.format(
"\"%s\" has registered a listener for %s on method \"%s\", but the event is Deprecated. \"%s\"; please notify the authors %s.",
plugin.getDescription().getFullName(),
clazz.getName(),
method.toGenericString(),
(warning != null && warning.reason().length() != 0) ? warning.reason() : "Server performance will be affected",
Arrays.toString(plugin.getDescription().getAuthors().toArray())),
warningState == WarningState.ON ? new AuthorNagException(null) : null);
break;
}
}
final CustomTimingsHandler timings = new CustomTimingsHandler("Plugin: " + plugin.getDescription().getFullName() + " Event: " + listener.getClass().getName() + "::" + method.getName() + "(" + eventClass.getSimpleName() + ")", pluginParentTimer); // Spigot
EventExecutor executor = new EventExecutor() {
#Override
public void execute(#NotNull Listener listener, #NotNull Event event) throws EventException {
try {
if (!eventClass.isAssignableFrom(event.getClass())) {
return;
}
// Spigot start
boolean isAsync = event.isAsynchronous();
if (!isAsync) timings.startTiming();
method.invoke(listener, event);
if (!isAsync) timings.stopTiming();
// Spigot end
} catch (InvocationTargetException ex) {
throw new EventException(ex.getCause());
} catch (Throwable t) {
throw new EventException(t);
}
}
};
if (false) { // Spigot - RL handles useTimings check now
eventSet.add(new TimedRegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled()));
} else {
eventSet.add(new RegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled()));
}
}
return ret;
}
Specifically
method.getParameterTypes()[0]
Does not work with generics.
You can create a generic method which returns you the BlockEventListenerObject, something like this:
private static <T extends BlockEvent> BlockEventListener<T> getBlockEventListener(Class<T> clazz) {
return new BlockEventListener<T>();
}
Then you can call it from your loop,
getServer().getPluginManager().registerEvents(getBlockEventListener(clazz), this);
„…Basically how could I get functionality like this to work…“
Here is one way…
...
public void use( final Set< Class < ? extends BlockEvent > > classes, PluginManager pluginMgr ) throws ReflectiveOperationException {
for( Class < ? extends BlockEvent > clazz : ( classes ) ){
pluginMgr.registerEvents( new BlockListener< >( clazz.newInstance( ) ), this );
}
}
...
I've implemented some stand-in classes to make the experiment testable. I used the experimental classes like this…
...
final Set< Class < ? extends BlockEvent > > classes = new HashSet< >( );
classes.add( BlockParty.class );
final PluginManager pluginMgr = new PluginManager( );
final Deduper experimental = new DeduperAnswer( );
experimental.use( classes, pluginMgr );
...
Click the green Start button at the top of the page and observe this output…
BlockChangeEvent [ block: Block#404b9385 ]
Havana Block Party!
EXPERIMENT SUCCESSFUL
Related
I'm trying to back-port to J1.8 an application written for J9 (Update4j); it uses ServiceLoader.Provider class and its methods.
The original code is:
public static <T extends Service> T loadService(ModuleLayer layer, ClassLoader classLoader, Class<T> type,
String classname) {
if (classname != null && !StringUtils.isClassName(classname)) {
throw new IllegalArgumentException(classname + " is not a valid Java class name.");
}
if (classLoader == null) {
classLoader = Thread.currentThread().getContextClassLoader();
}
ServiceLoader<T> loader;
List<Provider<T>> providers = new ArrayList<>();
if (layer != null) {
loader = ServiceLoader.load(layer, type);
providers.addAll(loader.stream().collect(Collectors.toList()));
}
loader = ServiceLoader.load(type, classLoader);
providers.addAll(loader.stream().collect(Collectors.toList()));
if (classname != null) {
// an explicit class name is used
// first lets look at providers, to locate in closed modules
for (Provider<T> p : providers) {
if (p.type().getName().equals(classname))
return p.get();
}
// nothing found, lets load with reflection
try {
Class<?> clazz = classLoader.loadClass(classname);
if (type.isAssignableFrom(clazz)) {
// What do you mean?? look 1 line above
#SuppressWarnings("unchecked")
T value = (T) clazz.getConstructor().newInstance();
return value;
} else {
// wrong type
throw new IllegalArgumentException(classname + " is not of type " + type.getCanonicalName());
}
} catch (RuntimeException e) {
throw e; // avoid unnecessary wrapping
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
if (providers.isEmpty()) {
throw new IllegalStateException("No provider found for " + type.getCanonicalName());
}
List<T> values = providers.stream().map(Provider::get).collect(Collectors.toList());
long maxVersion = Long.MIN_VALUE;
T maxValue = null;
for (T t : values) {
long version = t.version();
if (maxVersion <= version) {
maxVersion = version;
maxValue = t;
}
}
return maxValue;
}
}
How can you achieve the same result in J1.8? Is there a best-practice?
Unfortunately J1.8 does not have ServiceLoader.Provider and its utility methods. Should I iterate and select? Is there a reference I can study?
Thanks
There’s something you can improve or simplify. I wouldn’t waste resources to check the validity of the class name argument but postpone it until the class truly hasn’t found. Or, since the restrictions on class names at JVM level are much less than in Java source code, I wouldn’t check the name at all, as checking whether a matching class exist is already enough.
There is no point in iterating over all providers and adding them to a List, just to iterate over the list and search for a maximum version, when we could search for the maximum version right in the first iteration, without the List.
Note that for both cases, searching for a service with a given name and for searching for the maximum version, we can use the Stream API.
For the reflection fallback, catching ReflectiveOperationException instead of Exception removes the need to catch and re-throw RuntimeException. Also, knowing about Class.cast(…) and Class.asSubclass(…) helps avoiding unchecked casts.
public static <T extends Service> T loadService(
Object layer, ClassLoader classLoader, Class<T> type, String classname) {
if (classLoader == null) {
classLoader = Thread.currentThread().getContextClassLoader();
}
Stream<T> s = StreamSupport.stream(
ServiceLoader.load(type, classLoader).spliterator(), false);
if(classname == null)
return s.max(Comparator.comparingLong(T::version))
.orElseThrow(() -> new IllegalStateException(
"No provider found for " + type.getCanonicalName()));
Optional<T> o = s.filter(t -> t.getClass().getName().equals(classname)).findAny();
if(o.isPresent()) return o.get();
try {
// nothing found, lets load with reflection
Class<?> clazz = classLoader.loadClass(classname);
// we could also use a single
// return clazz.asSubclass(type).getConstructor().newInstance();
// if we can live with a ClassCastException instead of IllegalArgumentException
if(!type.isAssignableFrom(clazz)) {
throw new IllegalArgumentException(
classname + " is not of type " + type.getCanonicalName());
}
return type.cast(clazz.getConstructor().newInstance());
} catch(ReflectiveOperationException e) {
if(!StringUtils.isClassName(classname)) { // debatable whether needed
throw new IllegalArgumentException(
classname + " is not a valid Java class name.");
}
throw new RuntimeException(e);
}
}
Well, I do not know if this is the best practice but this solves:
public static <T extends Service> T loadService(Object layer, ClassLoader classLoader, Class<T> type, String classname) {
if (classname != null && !StringUtils.isClassName(classname)) {
throw new IllegalArgumentException(classname + " is not a valid Java class name.");
}
if (classLoader == null) {
classLoader = Thread.currentThread().getContextClassLoader();
}
ServiceLoader<T> loader;
loader = ServiceLoader.load(type, classLoader);
Iterator<T> iterator = loader.iterator();
if (classname != null) {
// an explicit class name is used
// first lets iterate on providers, to locate in closed modules
while (iterator.hasNext()) {
T p = iterator.next();
if (p.getClass().getName().equals(classname))
return p;
}
// nothing found, lets load with reflection
try {
Class<?> clazz = classLoader.loadClass(classname);
if (type.isAssignableFrom(clazz)) {
// What do you mean?? look 1 line above
#SuppressWarnings("unchecked")
T value = (T) clazz.getConstructor().newInstance();
return value;
} else {
// wrong type
throw new IllegalArgumentException(classname + " is not of type " + type.getCanonicalName());
}
} catch (RuntimeException e) {
throw e; // avoid unnecessary wrapping
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
if (!iterator.hasNext()) {
throw new IllegalStateException("No provider found for " + type.getCanonicalName());
}
List<T> values = new ArrayList();
while (iterator.hasNext()) {
T p = iterator.next();
values.add(p);
}
long maxVersion = Long.MIN_VALUE;
T maxValue = null;
for (T t : values) {
long version = t.version();
if (maxVersion <= version) {
maxVersion = version;
maxValue = t;
}
}
return maxValue;
}
}
Hoping this could help someone downgrading Update4j to J1.8.
I forked the original project and may be I'll release my public fork for J1.8 later, when it will be completely working.
I have an class with "T extends StorageClass". You can extend this class and put another class instend of the "T" for example "GroupStorage extends Storage. In the "Storage" class is an method called "get". Now i want that method to convert json to the "T" class, in my case to the "Group" class. Maybe you will understand when you look to the code below.
public abstract class Storage<T extends StorageClass> {
// This should return whatever T is.
public T get(String groupName) {
T t = null;
File file = new File(this.groupFolderPath, groupName + ".json");
if (file.exists()) {
try {
FileReader reader = new FileReader(file);
// 'T.class' is not possible
t = Storage.GSON.fromJson(reader, T.class);
} catch (Exception e) {
e.printStackTrace();
Bukkit.getLogger().warning("Failed to read " + groupName + ".json!");
}
} else {
Bukkit.getLogger().warning("The group " + groupName + " does not exists!");
}
return t;
}
}
You would have to pass down the type of the class to your method as :
public T get(String groupName, Class<T> type)
and then use it as :
t = Storage.GSON.fromJson(reader, type);
I have a very strange error, occurring only in chrome and safari.
I get an UmbrellaException when calling GWT.Create(foo.class); (Deferred binding)
Anyone any idea?
I figuerd out that java script throws an umbrella exception with the following javascript:
function entry_0(jsFunction){
return function(){
try {
return entry0(jsFunction, this, arguments);
}
catch (e) {
throw e;
Uncaught com.google.web.bindery.event.shared.UmbrellaException: Exception caught: Exception caught: null
}
}
;
}
In gwt its on the specific java code
IAmsPresenterFactory factory = (IAmsPresenterFactory) GWT.create(IReflectiveAmsFactory.class);
where IReflectiveAmsFactory is to create instances by class name (string)... (class for name gwt)
public class IReflectiveAmsFactory implements IAmsFactoryWrapper {
}
public interface IAmsPresenterFactory {
IDynamicAmsPresenter newInstance(String className, AmsClientFactory clientfactory, String id);
}
<generate-with class="ch.zhaw.ams.server.ams.AmsPresenterGenerator">
<when-type-assignable class="ch.zhaw.ams.client.ams.IAmsFactoryWrapper" />
</generate-with>
Full code snippet:
Request<IModuleBaseProxy> req = clientfactory.getRequestFactory().moduleRequest().findModuleBase(modtoken);
req.fire(new Receiver<IModuleBaseProxy>() {
#Override
public void onSuccess(IModuleBaseProxy response) {
System.out.println("found");
if (response != null) {
---> HERE
IAmsPresenterFactory factory = (IAmsPresenterFactory) GWT
---> ERROR .create(IReflectiveAmsFactory.class);
String clazz = response.getConfigSite();
AmsClientFactory apcf = null;
if (clientfactory == null) {
apcf = new AmsClientFactory();
} else {
apcf = clientfactory;
}
IDynamicAmsPresenter p = factory.newInstance(clazz, apcf, modtoken);
HasWidgets content = view.getContent();
// ContentPresenter contentPresenter = new
// ContentPresenter(apcf, p, new
// ContentView(modtoken));
p.go(content);
}
}
});
My Generator Class:
import java.io.PrintWriter;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
public class AmsPresenterGenerator extends Generator {
#Override
public String generate(TreeLogger logger, GeneratorContext context, String typeName)
throws UnableToCompleteException {
logger.log(TreeLogger.INFO, "Generating source for " + typeName, null);
TypeOracle typeOracle = context.getTypeOracle();
JClassType clazz = typeOracle.findType(typeName);
if (clazz == null) {
logger.log(TreeLogger.ERROR, "Unable to find metadata for type '" + typeName + "'", null);
throw new UnableToCompleteException();
}
try {
logger.log(TreeLogger.INFO, "Generating source for " + clazz.getQualifiedSourceName(), null);
JClassType reflectableType = typeOracle.getType("ch.zhaw.ams.client.ams.IDynamicAmsPresenter");
SourceWriter sourceWriter = getSourceWriter(clazz, context, logger);
if (sourceWriter != null) {
sourceWriter.println("public " + reflectableType.getQualifiedSourceName()
+ " newInstance(String className, AmsClientFactory clientfactory, String id) {");
JClassType[] types = typeOracle.getTypes();
int count = 0;
for (int i = 0; i < types.length; i++) {
// System.out.println(""+types[i].getName());
if (types[i].isInterface() == null && types[i].isAssignableTo(reflectableType)) {
System.out.println("Done:" + types[i].getName());
if (count == 0) {
sourceWriter.println(" if(\"" + types[i].getQualifiedSourceName()
+ "\".equals(className)) {" + " return new " + types[i].getQualifiedSourceName()
+ "(clientfactory, id);" + "}");
} else {
sourceWriter.println(" else if(\"" + types[i].getQualifiedSourceName()
+ "\".equals(className)) {" + " return new " + types[i].getQualifiedSourceName()
+ "(clientfactory, id);" + "}");
}
count++;
}
}
sourceWriter.println("return null;");
sourceWriter.println("}");
sourceWriter.commit(logger);
logger.log(TreeLogger.INFO, "Done Generating source for " + clazz.getName(), null);
return clazz.getQualifiedSourceName() + "Wrapper";
}
} catch (NotFoundException e) {
e.printStackTrace();
}
return null;
}
public SourceWriter getSourceWriter(JClassType classType, GeneratorContext context, TreeLogger logger) {
String packageName = classType.getPackage().getName();
String simpleName = classType.getSimpleSourceName() + "Wrapper";
ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory(packageName, simpleName);
composer.addImplementedInterface("ch.zhaw.ams.client.ams.IAmsPresenterFactory");
PrintWriter printWriter = context.tryCreate(logger, packageName, simpleName);
if (printWriter == null) {
return null;
} else {
SourceWriter sw = composer.createSourceWriter(context, printWriter);
return sw;
}
}
}
I solved the issue with upgrading to GWT 2.5.1
Having Compiler Arguments
-XdisableCastChecking
And VM Arguments
-Xmx512m -Xss16M
I think it is problem with Deferred Binding. it is very much browser dependent. As I know if you want to write any browser dependent or locale dependent code like GUI change, then you have to create instance of it instead of use different Deferred Binding.
As you say it works in firefox because it might compile with firefox and generating firefox versions of code at compile time. and loaded by a particular firefox during bootstrapping at runtime and not for others client.
Deferred binding is a feature of the GWT compiler that works by generating many versions of code at compile time, only one of which needs to be loaded by a particular client during bootstrapping at runtime.
Mostly internationalized applications or GWT RPC calls you will be using deferred binding
For more information check the GWT page on Deferred binding:
Coding Basics - Deferred Binding
Are there any way not to define all Places in the PlaceHistoryMapper?
At this moment I am using Generator in order to generate list of all places automatically, but I am not sure that this is a correct way.
public class AppPlaceHistoryMapper extends AbstractPlaceHistoryMapper<Object> {
#Override
protected PrefixAndToken getPrefixAndToken(Place place) {
if (place instanceof AbstractPlace) {
return new PrefixAndToken(((AbstractPlace) place).getName(), ((AbstractPlace) place).getTokenizer()
.getToken((AbstractPlace) place));
}
throw new RuntimeException("Invalid place: " + place);
}
/**
* This method will be overrided by the gwt-generated class, so any changes made in it will not be executed
*
* #see PlaceMapperGenerator
*/
#Override
protected PlaceTokenizer<?> getTokenizer(String prefix) {
AbstractPlace[] places = {/* List of places generated by PlaceMapperGenerator */};
for (AbstractPlace p : places) {
if (p.getName().equals(prefix)) {
return p.getTokenizer();
}
}
throw new RuntimeException("Unable to find place for provided prefix: " + prefix);
}
}
Generator:
public class PlaceMapperGenerator extends Generator {
// #formatter:off
private static final String GENERATED_METHOD_TEMPLATE =
"protected com.google.gwt.place.shared.PlaceTokenizer<?> getTokenizer(String prefix) {" +
"AbstractPlace[] places = { %s };" +
"for (AbstractPlace p : places) {" +
"if (p.getName().equals(prefix)) {" +
"return p.getTokenizer();" +
"}" +
"}" +
"throw new RuntimeException(\"Unable to find place for provided prefix: \" + prefix);" +
"}"
; // #formatter:on
#Override
public String generate(TreeLogger logger, GeneratorContext context, String typeName) {
JClassType type;
try {
type = context.getTypeOracle().getType(typeName);
} catch (NotFoundException e) {
throw new RuntimeException(e);
}
String implTypeName = type.getSimpleSourceName() + "Impl";
String implPackageName = type.getPackage().getName();
ClassSourceFileComposerFactory composerFactory = new ClassSourceFileComposerFactory(implPackageName,
implTypeName);
composerFactory.setSuperclass(AppPlaceHistoryMapper.class.getName());
#SuppressWarnings("resource")
PrintWriter printWriter = context.tryCreate(logger, implPackageName, implTypeName);
if (printWriter != null) {
SourceWriter sourceWriter = composerFactory.createSourceWriter(context, printWriter);
sourceWriter.print(GENERATED_METHOD_TEMPLATE, getPlaces(context));
sourceWriter.commit(logger);
printWriter.close();
}
return composerFactory.getCreatedClassName();
}
private static String getPlaces(GeneratorContext context) {
JPackage[] packages = context.getTypeOracle().getPackages();
List<String> places = new ArrayList<String>();
for (JPackage p : packages) {
if (p.getName().startsWith(AbstractPlace.class.getPackage().getName())) {
JClassType[] types = p.getTypes();
for (JClassType type : types) {
if (type.getSuperclass() != null
&& type.getSuperclass().getQualifiedSourceName().equals(AbstractPlace.class.getName())) {
places.add("new " + type.getQualifiedSourceName() + "()");
}
}
}
}
return places.toString().replaceAll("^\\[|\\]$", "");
}
}
I'm afraid that the only way to figure out what Places and Tokenizers are in your application, without maintaining a list with them, is with a generator like you are doing.
Anyway instead of maintaining a generator I would use the #WithTokenizers annotation and let GWT generate your PlaceHistoryMapper take a look to the GWT MVP dev-guide
#WithTokenizers({HelloPlace.Tokenizer.class, GoodbyePlace.Tokenizer.class})
public interface AppPlaceHistoryMapper extends PlaceHistoryMapper {}
What I do in my applications is to use a script to generate activities, views, places and update gin modules and mappers based on a template.
For my instrumentation tool, I want to provide a wrapping ClassLoader that is used to start a main method after specific classes have been instrumented. My ClassLoader should load instrumented versions of certain classes. But for Jetty and JUnit, this approach is severly limited because they build their own classloading hierarchy.
I don't want to pass VM arguments, so I can't change the SystemClassLoader. But I can force-feed it with my classes by using reflection to make ClassLoader.defineClass(String, byte[], int, int) public.
ClassLoader scl = ClassLoader.getSystemClassLoader();
Method defineClass = ClassLoader.class.getDeclaredMethod(
"defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
for (String binaryName : classNamesToLoad) {
byte[] bytecode = this.declaredClasses.get(binaryName);
defineClass.invoke(scl, binaryName, bytecode, 0, bytecode.length);
}
defineClass.setAccessible(false);
This is just great - but there's one problem left: If some of my classes inherit from or contain other classes, they have to be loaded in the right order because the SystemClassLoader loads all classes the current one depends on - and would load the uninstrumented version.
Here is an example with some (poorly named) classes and the order they would have to be loaded in:
A
A.A extends B.A
B
B.A extends B.C
B.C
would have to be loaded in order
B
B.C
B.A
A
A.A
if I want to load only the instrumented version.
Is there an easy way out - e.g. a "setSystemClassLoader" method I didn't spot yet?
A workaround by which I wouldn't need to manipulate the SystemClassLoader?
Or do I really have to do a full transitive dependency analysis starting on the classes I want to load to determine the right order (and in this case: is there any "prior art" I can work with)?
Thanks!
Looks like there's no way around the transitive dependency analysis.
I solved it this way, and I really hope someone can profit from this implementation:
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.tree.ClassNode;
public class DependencyDetector {
private static class Node implements Comparable<Node> {
private final String binaryName;
private final Node[] imports;
private final int score;
private Node(String binaryName, Node...imports) {
this.binaryName = binaryName;
this.imports = imports;
this.score = calculateScore(imports);
}
public int compareTo(Node o) {
return score - o.score;
}
private int calculateScore(Node...imports) {
int newScore = 0;
for (Node n : imports) {
if (n.score >= newScore) {
newScore = n.score + 1;
}
}
return newScore;
}
}
private Map<String, Node> nodes = new HashMap<String, Node>();
public DependencyDetector add(ClassNode node) {
Node n = nodes.get(node.name);
if (n == null) {
n = createNode(node);
}
return this;
}
private Node createNode(ClassNode node) {
String binaryName = node.name;
String[] importNames = extractImportedBinaryNames(node);
Node[] imports = new Node[importNames.length];
for (int i = 0; i < imports.length; i++) {
String importName = importNames[i];
Node imp = nodes.get(importName);
if (imp == null) {
ClassNode cn = new ClassNode();
String path = importName.replace('.', '/') + ".class";
try {
new ClassReader(
ClassLoader.getSystemResourceAsStream(path)
).accept(cn, ClassReader.SKIP_CODE);
} catch (IOException e) {
throw new RuntimeException(
"could not read class " + importName);
}
imp = createNode(cn);
nodes.put(importName, imp);
}
imports[i] = imp;
}
Node result = new Node(binaryName, imports);
nodes.put(binaryName, result);
return result;
}
private String[] extractImportedBinaryNames(ClassNode node) {
String binaryName = node.name;
ArrayList<String> nodesToAdd = new ArrayList<String>();
int endOfOuter = binaryName.lastIndexOf('$');
if (endOfOuter >= 0) {
nodesToAdd.add(binaryName.substring(0, endOfOuter));
}
if (node.superName != null) {
nodesToAdd.add(node.superName);
}
if (node.interfaces != null) {
for (String interf : (List<String>) node.interfaces) {
if (interf != null) {
nodesToAdd.add(interf);
}
}
}
return nodesToAdd.toArray(new String[nodesToAdd.size()]);
}
public String[] getClassesToLoad(String...binaryNames) {
String[] classNames = binaryNames != null && binaryNames.length > 0
? binaryNames.clone()
: nodes.keySet().toArray(new String[nodes.size()]);
ArrayDeque<Node> dependencyQueue = new ArrayDeque<Node>();
for (String className : classNames) {
Node node = nodes.get(className.replace('.', '/'));
dependencyQueue.add(node);
if (node == null) {
throw new RuntimeException(
"Class " + className + " was not registered");
}
}
HashMap<String, Node> dependencyMap = new HashMap<String, Node>();
while (!dependencyQueue.isEmpty()) {
Node node = dependencyQueue.removeFirst();
dependencyMap.put(node.binaryName, node);
for (Node i : node.imports) {
dependencyQueue.addLast(i);
}
}
ArrayList<Node> usedNodes =
new ArrayList<Node>(dependencyMap.values());
Collections.sort(usedNodes);
String[] result = new String[usedNodes.size()];
int i = 0;
for (Node n : usedNodes) {
result[i++] = n.binaryName.replace('/', '.');
}
return result;
}
public boolean contains(String binaryName) {
return nodes.containsKey(binaryName.replace('.', '/'));
}
}
It's used like this: on a DependencyDetector, you call add(ClassNode) to add a ClassNode and all its dependencies (all classes it extends or implements or is contained by). When you are done building the dependency tree, you call getClassesToLoad() to retrieve all dependencies as a String[] containing the binary names in the necessary order. You can also just ask for a subset of all added classes and their dependencies by specifying the binary names as a parameter of getClassesToLoad(...).
Now, when I instrument classes, I also add the ClassNode to the DependencyDetector and can retrieve everything I need to pass it into a method like this:
/**
* load the specified classes (or all instrumented classes)
* and all their dependencies with the specified ClassLoader.
* #param loader
* #param binaryNames binary names of all classes you want to load
* - none loads all instrumented classes
*/
public void loadIntoClassLoader(ClassLoader loader, String...binaryNames) {
final String[] classNamesToLoad =
dependencies.getClassesToLoad(binaryNames);
Method defineClass = null;
Method findLoadedClass = null;
try {
// crack ClassLoader wide open and force-feed it with our classes
defineClass = ClassLoader.class.getDeclaredMethod(
"defineClass", String.class, byte[].class,
int.class, int.class);
defineClass.setAccessible(true);
findLoadedClass = ClassLoader.class.getDeclaredMethod(
"findLoadedClass", String.class);
findLoadedClass.setAccessible(true);
for (String binaryName : classNamesToLoad) {
if (!binaryName.startsWith("java.")) {
if (findLoadedClass.invoke(loader, binaryName) == null) {
byte[] bytecode = getBytecode(binaryName);
defineClass.invoke(loader, binaryName, bytecode,
0, bytecode.length);
} else if (declaredClasses.containsKey(binaryName)) {
throw new RuntimeException(
"Class " + binaryName + " was already loaded, " +
"it must not be redeclared");
}
}
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(
"could not load classes into ClassLoader", e);
} finally {
rehideMethod(findLoadedClass);
rehideMethod(defineClass);
}
}
private void rehideMethod(Method m) {
if (m != null) {
try {
m.setAccessible(false);
} catch (Exception e) {
}
}
}
which relies on
private final DependencyDetector dependencies = new DependencyDetector();
private final Map<String, byte[]> declaredClasses = new HashMap<String, byte[]>();
private byte[] getBytecode(String binaryName) {
byte[] bytecode = declaredClasses.get(binaryName);
if (bytecode == null) {
// asBytes loads the class as byte[]
bytecode =
asBytes(binaryName.replace('.', '/') + ".class");
}
return bytecode;
}
That's pretty much it and it works great in every situation I encountered so far.
use instance of to check the object is whether belongs to the class.
if (aAnimal instanceof Fish){
Fish fish = (Fish)aAnimal;
fish.swim();
}
else if (aAnimal instanceof Spider){
Spider spider = (Spider)aAnimal;
spider.crawl();
}