diff --git a/build.gradle b/build.gradle index 24d6c40..a5ac047 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { } group "meteordevelopment" -version "0.2.5" +version "0.3.0" sourceCompatibility = targetCompatibility = JavaVersion.VERSION_1_8 diff --git a/src/main/java/meteordevelopment/orbit/EventBus.java b/src/main/java/meteordevelopment/orbit/EventBus.java index ba7b951..61ee6fd 100644 --- a/src/main/java/meteordevelopment/orbit/EventBus.java +++ b/src/main/java/meteordevelopment/orbit/EventBus.java @@ -3,39 +3,40 @@ import meteordevelopment.orbit.listeners.IListener; import meteordevelopment.orbit.listeners.LambdaListener; +import java.lang.invoke.LambdaMetafactory; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.ref.WeakReference; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.lang.reflect.Modifier; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Function; +import java.util.function.Consumer; /** * Default implementation of {@link IEventBus}. */ public class EventBus implements IEventBus { - private static class LambdaFactoryInfo { - public final String packagePrefix; - public final LambdaListener.Factory factory; + private static final Map> LAMBDA_FACTORY_CACHE = Collections.synchronizedMap(new WeakHashMap<>()); - public LambdaFactoryInfo(String packagePrefix, LambdaListener.Factory factory) { - this.packagePrefix = packagePrefix; - this.factory = factory; - } - } - - private final Map> listenerCache = new ConcurrentHashMap<>(); + private final Map> listenerCache = Collections.synchronizedMap(new IdentityHashMap<>()); private final Map, List> staticListenerCache = new ConcurrentHashMap<>(); private final Map, List> listenerMap = new ConcurrentHashMap<>(); - private final List lambdaFactoryInfos = new ArrayList<>(); + private final List lookupInfos = new ArrayList<>(); @Override - public void registerLambdaFactory(String packagePrefix, LambdaListener.Factory factory) { - synchronized (lambdaFactoryInfos) { - lambdaFactoryInfos.add(new LambdaFactoryInfo(packagePrefix, factory)); + public void registerLookup(String packagePrefix, MethodHandles.Lookup lookup) { + synchronized (lookupInfos) { + // to ensure the lookups are used correctly, they are ordered from longest to shortest + int i = 0; + while (i < lookupInfos.size() && lookupInfos.get(i).packagePrefix.length() > packagePrefix.length()) { + i++; + } + lookupInfos.add(i, new LookupInfo(packagePrefix, lookup)); } } @@ -45,6 +46,22 @@ public boolean isListening(Class eventKlass) { return listeners != null && !listeners.isEmpty(); } + @Override + public boolean isSubscribed(Object object) { + return listenerCache.containsKey(object); + } + + @Override + public boolean isSubscribed(Class klass) { + return staticListenerCache.containsKey(klass); + } + + @Override + public boolean isSubscribed(IListener listener) { + List listeners = listenerMap.get(listener.getTarget()); + return listeners != null && listeners.contains(listener); + } + @Override public T post(T event) { List listeners = listenerMap.get(event.getClass()); @@ -74,30 +91,21 @@ public T post(T event) { @Override public void subscribe(Object object) { - subscribe(getListeners(object.getClass(), object), false); + subscribe(listenerCache.computeIfAbsent(object, o -> createListeners(o.getClass(), o))); } @Override public void subscribe(Class klass) { - subscribe(getListeners(klass, null), true); - } - - @Override - public void subscribe(IListener listener) { - subscribe(listener, false); + subscribe(staticListenerCache.computeIfAbsent(klass, k -> createListeners(k, null))); } - private void subscribe(List listeners, boolean onlyStatic) { - for (IListener listener : listeners) subscribe(listener, onlyStatic); + private void subscribe(List listeners) { + for (IListener listener : listeners) subscribe(listener); } - private void subscribe(IListener listener, boolean onlyStatic) { - if (onlyStatic) { - if (listener.isStatic()) insert(listenerMap.computeIfAbsent(listener.getTarget(), aClass -> new CopyOnWriteArrayList<>()), listener); - } - else { - insert(listenerMap.computeIfAbsent(listener.getTarget(), aClass -> new CopyOnWriteArrayList<>()), listener); - } + @Override + public void subscribe(IListener listener) { + insert(listenerMap.computeIfAbsent(listener.getTarget(), aClass -> new CopyOnWriteArrayList<>()), listener); } private void insert(List listeners, IListener listener) { @@ -111,63 +119,80 @@ private void insert(List listeners, IListener listener) { @Override public void unsubscribe(Object object) { - unsubscribe(getListeners(object.getClass(), object), false); + List listeners = listenerCache.remove(object); + if (listeners != null) unsubscribe(listeners); + // for backwards-compatibility + else unsubscribe(object.getClass()); } @Override public void unsubscribe(Class klass) { - unsubscribe(getListeners(klass, null), true); - } - - @Override - public void unsubscribe(IListener listener) { - unsubscribe(listener, false); + List staticListeners = staticListenerCache.remove(klass); + if (staticListeners != null) unsubscribe(staticListeners); } - private void unsubscribe(List listeners, boolean staticOnly) { - for (IListener listener : listeners) unsubscribe(listener, staticOnly); + private void unsubscribe(List listeners) { + for (IListener listener : listeners) unsubscribe(listener); } - private void unsubscribe(IListener listener, boolean staticOnly) { + @Override + public void unsubscribe(IListener listener) { List l = listenerMap.get(listener.getTarget()); - - if (l != null) { - if (staticOnly) { - if (listener.isStatic()) l.remove(listener); + if (l != null) l.remove(listener); + } + + private List createListeners(Class klass, Object object) { + List listeners = new ArrayList<>(); + + while (klass != Object.class) { + MethodHandles.Lookup lookup = null; + + for (Method method : klass.getDeclaredMethods()) { + // skip invalid methods + boolean isStatic = Modifier.isStatic(method.getModifiers()); + if (!isValid(method) || (object == null && !isStatic)) { + continue; + } + + // get or create lambda factory + WeakReference ref = LAMBDA_FACTORY_CACHE.get(method); + MethodHandle lambdaFactory = ref != null ? ref.get() : null; + try { + if (lambdaFactory == null) { + // lazily search for lookup infos + if (lookup == null) { + lookup = getLookupInfo(klass).in(klass); + } + + lambdaFactory = LambdaMetafactory.metafactory( + lookup, "accept", + isStatic ? MethodType.methodType(Consumer.class) : MethodType.methodType(Consumer.class, klass), + MethodType.methodType(void.class, Object.class), + lookup.unreflect(method), + MethodType.methodType(void.class, method.getParameters()[0].getType()) + ).getTarget(); + + if (!isStatic) { + lambdaFactory = lambdaFactory.asType(MethodType.methodType(Consumer.class, Object.class)); + } + + LAMBDA_FACTORY_CACHE.put(method, new WeakReference<>(lambdaFactory)); + } + + listeners.add(new LambdaListener(lambdaFactory, object, method)); + } catch (Throwable throwable) { + String message = String.format( + "Could not create lambda listener for '%s.%s(%s)'.", + klass.getSimpleName(), method.getName(), method.getParameters()[0].getType().getSimpleName() + ); + throw new IllegalStateException(message, throwable); + } } - else l.remove(listener); - } - } - private List getListeners(Class klass, Object object) { - Function> func = o -> { - List listeners = new CopyOnWriteArrayList<>(); - - getListeners(listeners, klass, object); - - return listeners; - }; - - if (object == null) return staticListenerCache.computeIfAbsent(klass, func); - - // We need to check if the instances are the same and avoid using .equals() and .hashCode() - for (Object key : listenerCache.keySet()) { - if (key == object) return listenerCache.get(object); - } - - List listeners = func.apply(object); - listenerCache.put(object, listeners); - return listeners; - } - - private void getListeners(List listeners, Class klass, Object object) { - for (Method method : klass.getDeclaredMethods()) { - if (isValid(method)) { - listeners.add(new LambdaListener(getLambdaFactory(klass), klass, object, method)); - } + klass = klass.getSuperclass(); } - if (klass.getSuperclass() != null) getListeners(listeners, klass.getSuperclass(), object); + return new CopyOnWriteArrayList<>(listeners); } private boolean isValid(Method method) { @@ -178,10 +203,10 @@ private boolean isValid(Method method) { return !method.getParameters()[0].getType().isPrimitive(); } - private LambdaListener.Factory getLambdaFactory(Class klass) { - synchronized (lambdaFactoryInfos) { - for (LambdaFactoryInfo info : lambdaFactoryInfos) { - if (klass.getName().startsWith(info.packagePrefix)) return info.factory; + private MethodHandles.Lookup getLookupInfo(Class klass) { + synchronized (lookupInfos) { + for (LookupInfo info : lookupInfos) { + if (klass.getName().startsWith(info.packagePrefix)) return info.lookup; } } diff --git a/src/main/java/meteordevelopment/orbit/IEventBus.java b/src/main/java/meteordevelopment/orbit/IEventBus.java index d2718de..aaba7df 100644 --- a/src/main/java/meteordevelopment/orbit/IEventBus.java +++ b/src/main/java/meteordevelopment/orbit/IEventBus.java @@ -1,18 +1,20 @@ package meteordevelopment.orbit; import meteordevelopment.orbit.listeners.IListener; -import meteordevelopment.orbit.listeners.LambdaListener; + +import java.lang.invoke.MethodHandles; /** * Manages event listeners. */ public interface IEventBus { /** - * Registers a lambda factory to use with the specified package. + * Registers a lookup allowing orbit to reflect into private members inside the provided package. You can obtain a + * lookup instance by calling {@link MethodHandles#lookup()}. * @param packagePrefix Package prefix that this factory will be used for, eg "meteordevelopment.orbit" - * @param factory The factory to use + * @param lookup The lookup to use. */ - void registerLambdaFactory(String packagePrefix, LambdaListener.Factory factory); + void registerLookup(String packagePrefix, MethodHandles.Lookup lookup); /** * Returns whether at least one event listener is currently registered for this event type. @@ -22,6 +24,30 @@ public interface IEventBus { */ boolean isListening(Class eventClass); + /** + * Returns whether the object is currently subscribed to the event bus. + * @param object The object to query + * @return whether the object is currently subscribed to the event bus + * @since 0.3.0 + */ + boolean isSubscribed(Object object); + + /** + * Returns whether the class is currently subscribed to the event bus. + * @param klass The class to query + * @return whether the class is currently subscribed to the event bus + * @since 0.3.0 + */ + boolean isSubscribed(Class klass); + + /** + * Returns whether the listener is currently subscribed to the event bus. + * @param listener The listener to query + * @return whether the listener is currently subscribed to the event bus + * @since 0.3.0 + */ + boolean isSubscribed(IListener listener); + /** * Posts an event to all subscribed event listeners. * @param event Event to post diff --git a/src/main/java/meteordevelopment/orbit/LookupInfo.java b/src/main/java/meteordevelopment/orbit/LookupInfo.java new file mode 100644 index 0000000..f6ce2d1 --- /dev/null +++ b/src/main/java/meteordevelopment/orbit/LookupInfo.java @@ -0,0 +1,13 @@ +package meteordevelopment.orbit; + +import java.lang.invoke.MethodHandles; + +public class LookupInfo { + public final String packagePrefix; + public final MethodHandles.Lookup lookup; + + public LookupInfo(String packagePrefix, MethodHandles.Lookup lookup) { + this.packagePrefix = packagePrefix; + this.lookup = lookup; + } +} diff --git a/src/main/java/meteordevelopment/orbit/listeners/ConsumerListener.java b/src/main/java/meteordevelopment/orbit/listeners/ConsumerListener.java index c048ed5..ec7e8b3 100644 --- a/src/main/java/meteordevelopment/orbit/listeners/ConsumerListener.java +++ b/src/main/java/meteordevelopment/orbit/listeners/ConsumerListener.java @@ -37,9 +37,4 @@ public Class getTarget() { public int getPriority() { return priority; } - - @Override - public boolean isStatic() { - return false; - } } diff --git a/src/main/java/meteordevelopment/orbit/listeners/IListener.java b/src/main/java/meteordevelopment/orbit/listeners/IListener.java index f08e3f9..fd83bcd 100644 --- a/src/main/java/meteordevelopment/orbit/listeners/IListener.java +++ b/src/main/java/meteordevelopment/orbit/listeners/IListener.java @@ -19,11 +19,4 @@ public interface IListener { * @return The priority for this listener */ int getPriority(); - - /** - * @return True if this listener is for static methods - * @deprecated Will be removed in a future version - */ - @Deprecated - boolean isStatic(); } diff --git a/src/main/java/meteordevelopment/orbit/listeners/LambdaListener.java b/src/main/java/meteordevelopment/orbit/listeners/LambdaListener.java index 0337264..2d57acf 100644 --- a/src/main/java/meteordevelopment/orbit/listeners/LambdaListener.java +++ b/src/main/java/meteordevelopment/orbit/listeners/LambdaListener.java @@ -2,12 +2,7 @@ import meteordevelopment.orbit.EventHandler; -import java.lang.invoke.LambdaMetafactory; import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.function.Consumer; @@ -16,66 +11,26 @@ * Default implementation of a {@link IListener} that creates a lambda at runtime to call the target method. */ public class LambdaListener implements IListener { - public interface Factory { - MethodHandles.Lookup create(Method lookupInMethod, Class klass) throws InvocationTargetException, IllegalAccessException; - } - - private static boolean isJava1dot8; - private static Constructor lookupConstructor; - private static Method privateLookupInMethod; - private final Class target; private final boolean isStatic; private final int priority; - private Consumer executor; + private final Consumer executor; /** * Creates a new lambda listener, can be used for both static and non-static methods. - * @param klass Class of the object - * @param object Object, null if static - * @param method Method to create lambda for + * + * @param lambdaFactory The factory from which the lambda is created + * @param object Object, null if static + * @param method Method to create lambda for */ @SuppressWarnings("unchecked") - public LambdaListener(Factory factory, Class klass, Object object, Method method) { + public LambdaListener(MethodHandle lambdaFactory, Object object, Method method) throws Throwable { this.target = method.getParameters()[0].getType(); this.isStatic = Modifier.isStatic(method.getModifiers()); this.priority = method.getAnnotation(EventHandler.class).priority(); - try { - String name = method.getName(); - MethodHandles.Lookup lookup; - - if (isJava1dot8) { - boolean a = lookupConstructor.isAccessible(); - lookupConstructor.setAccessible(true); - lookup = lookupConstructor.newInstance(klass); - lookupConstructor.setAccessible(a); - } - else { - lookup = factory.create(privateLookupInMethod, klass); - } - - MethodType methodType = MethodType.methodType(void.class, method.getParameters()[0].getType()); - - MethodHandle methodHandle; - MethodType invokedType; - - if (isStatic) { - methodHandle = lookup.findStatic(klass, name, methodType); - invokedType = MethodType.methodType(Consumer.class); - } - else { - methodHandle = lookup.findVirtual(klass, name, methodType); - invokedType = MethodType.methodType(Consumer.class, klass); - } - - MethodHandle lambdaFactory = LambdaMetafactory.metafactory(lookup, "accept", invokedType, MethodType.methodType(void.class, Object.class), methodHandle, methodType).getTarget(); - - if (isStatic) this.executor = (Consumer) lambdaFactory.invoke(); - else this.executor = (Consumer) lambdaFactory.invoke(object); - } catch (Throwable throwable) { - throwable.printStackTrace(); - } + if (isStatic) this.executor = (Consumer) lambdaFactory.invokeExact(); + else this.executor = (Consumer) lambdaFactory.invokeExact(object); } @Override @@ -93,23 +48,10 @@ public int getPriority() { return priority; } - @Override + /** + * @return Whether the method associated with this listener is static + */ public boolean isStatic() { return isStatic; } - - static { - try { - isJava1dot8 = System.getProperty("java.version").startsWith("1.8"); - - if (isJava1dot8) { - lookupConstructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class); - } - else { - privateLookupInMethod = MethodHandles.class.getDeclaredMethod("privateLookupIn", Class.class, MethodHandles.Lookup.class); - } - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } - } } diff --git a/src/test/java/test/Main.java b/src/test/java/test/Main.java index 0c1dd2e..487d74f 100644 --- a/src/test/java/test/Main.java +++ b/src/test/java/test/Main.java @@ -33,7 +33,7 @@ static class Abc2 extends Abc { } public static void main(String[] args) { System.out.println("-- WITHOUT INSTANCE --"); IEventBus bus = new EventBus(); - bus.registerLambdaFactory("test", (lookupInMethod, klass) -> (MethodHandles.Lookup) lookupInMethod.invoke(null, klass, MethodHandles.lookup())); + bus.registerLookup("test", MethodHandles.lookup()); // Subscribes only static methods bus.subscribe(Main.class); @@ -57,7 +57,7 @@ public static void main(String[] args) { public Main() { IEventBus bus = new EventBus(); - bus.registerLambdaFactory("test", (lookupInMethod, klass) -> (MethodHandles.Lookup) lookupInMethod.invoke(null, klass, MethodHandles.lookup())); + bus.registerLookup("test", MethodHandles.lookup()); // Subscribes both static and normal methods bus.subscribe(this);