/* * Copyright 2012 Jason Miller * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jj.event; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import javassist.ClassPool; import javassist.CtClass; import javassist.CtField; import javassist.CtMethod; import javassist.CtNewConstructor; import javassist.CtNewMethod; import javassist.NotFoundException; import jj.execution.ServerTask; import jj.execution.TaskRunner; import jj.util.CodeGenHelper; import com.google.inject.TypeLiteral; import com.google.inject.spi.InjectionListener; import com.google.inject.spi.TypeEncounter; import com.google.inject.spi.TypeListener; /** * <p> * The heart of the event system. Basically, * <ul> * <li>All bindings are introspected for a Subscriber annotation * <li>if one is found, all non-static, non-private methods of * the bound class that take a single parameter are checked * for Listener annotations. * <li>if one or more are found, then when instances of the class * are created, they are bound up as listeners so the EventManager * can publish events to them * </ul> * * That's a little light on the details but substantially correct. * </p> * * <p> * Breaking this up might pay some dividends? it's pretty tricky to do so, * however. Guice doesn't really have a reliable way of letting TypeListener * instances participate with the injector and still get all the notifications, * so it becomes a dance of figuring out how to manually instantiate stuff * which seems painful. A quick google suggests this exact thing has been a * problem for others. * * Possibly moving all of the management of invoker creation to a separate object * could work, if the creation of the structures can be rearranged so that the AOT * creation of the instance queues is just done at the point of injection rather * than the binding time. That's the kind of sentence that will stop making * sense to me in the next hour. * @author jason * */ class EventConfiguringTypeListener implements TypeListener { /** * mapped from subscriber class name -> the methodinfos for the listener methods of the class. * since this list stored as the value is only ever manipulated when a new type is encountered for * the first time, and is read-only after that, it doesn't need to be synchronized */ private final ConcurrentHashMap<String, List<MethodInfo>> subscribers = new ConcurrentHashMap<>(); /** * mapped from the class name of an invoker to the invoker class. */ private final ConcurrentHashMap<String, Class<? extends Invoker>> invokerClasses = new ConcurrentHashMap<>(); /** * mapped from the event type -> the set of listener invokers for that event * the set stored as the value can be manipulated from multiple threads, so it needs * to also be concurrent, but the value itself will only ever be set once on first encounter * for a given event type and never removed after so that should do it */ private final ConcurrentHashMap<Class<?>, ConcurrentLinkedQueue<Invoker>> invokers = new ConcurrentHashMap<>(); /** mapped from the weak reference to the instance invoked -> invoker */ private final ConcurrentHashMap<WeakReference<Object>, Invoker> cleanupMap = new ConcurrentHashMap<>(); private final ReferenceQueue<Object> invokerInstanceQueue = new ReferenceQueue<>(); private final ClassPool classPool = CodeGenHelper.classPool(); private final CtClass invokerClass; private final CtMethod invokeMethod; private final CtMethod targetMethod; private final CtClass weakReferenceClass; EventConfiguringTypeListener() { try { invokerClass = classPool.get(Invoker.class.getName()); invokeMethod = invokerClass.getDeclaredMethod("invoke"); targetMethod = invokerClass.getDeclaredMethod("target"); weakReferenceClass = classPool.get(WeakReference.class.getName()); } catch (NotFoundException e) { // this can't happen throw new AssertionError(e); } } /** * Cleans up invoker instances when their object being invoked * is eligible for garbage collection * * @author jason * */ private final class ListenerCleaner extends ServerTask { /** * @param targetName */ public ListenerCleaner() { super("Event System cleanup"); } @Override public void run() throws Exception { while (true) { Reference<?> reference = invokerInstanceQueue.remove(); Invoker instance = cleanupMap.remove(reference); if (instance != null) { for (ConcurrentLinkedQueue<Invoker> invokerQueue : invokers.values()) { invokerQueue.remove(instance); } } } } } private static final class MethodInfo { private final Method method; private final String className; private final String parameterType; MethodInfo(final Method method) throws Exception { this.method = method; className = method.getDeclaringClass().getPackage().getName() + ".InvokerFor" + method.getDeclaringClass().getSimpleName() + "$" + method.getName() + method.getParameterTypes()[0].getSimpleName() + "$" + Integer.toHexString(method.hashCode()); // hanging onto event types is okay, they'll get reused parameterType = method.getParameterTypes()[0].getName(); } @Override public String toString() { return method.toString(); } } /** * Wires event listeners to the publisher when an instance is being injected. * @author jason * * @param <I> */ private final class EventWiringInjectionListener<I> implements InjectionListener<I> { @Override public void afterInjection(final I injectee) { if (injectee instanceof TaskRunner) { ((TaskRunner)injectee).execute(new ListenerCleaner()); } else if (injectee instanceof PublisherImpl) { // share state with the publisher - but it can't have events. boo ((PublisherImpl)injectee).listenerMap(invokers); } else { possiblySubscribeToEvents(injectee); } } } private void possiblySubscribeToEvents(final Object subscriber) { ArrayList<MethodInfo> methods = new ArrayList<>(); Class<?> startingClass = subscriber.getClass(); while (startingClass != Object.class) { String name = startingClass.getName(); if (subscribers.containsKey(name)) { methods.addAll(subscribers.get(name)); } startingClass = startingClass.getSuperclass(); } for (final MethodInfo invoked : methods) { cleanupMap.computeIfAbsent( new WeakReference<Object>(subscriber, invokerInstanceQueue), reference -> { try { Invoker invoker = invokerClasses.computeIfAbsent(invoked.className, className -> { try { return makeInvokerClass(className, subscriber, invoked.method); } catch (Exception e) { throw new AssertionError(e); } }).getConstructor(WeakReference.class).newInstance(reference); invokers.get(Class.forName(invoked.parameterType)).offer(invoker); return invoker; } catch (Exception e) { throw new AssertionError(e); } } ); } } @SuppressWarnings("unchecked") private Class<? extends Invoker> makeInvokerClass(String className, Object injectee, Method invoked) throws Exception { // might be defined already try { return (Class<? extends Invoker>)Class.forName(className); } catch (ClassNotFoundException e) {} CtClass newClass = classPool.makeClass(className); newClass.addInterface(invokerClass); newClass.addField(CtField.make("private final java.lang.ref.WeakReference instance;", newClass)); newClass.addConstructor( CtNewConstructor.make( new CtClass[] { weakReferenceClass }, null, "{this.instance = $1;}", newClass ) ); final String invokedClassName = invoked.getDeclaringClass().getName(); final String invokedMethodName = invoked.getName(); final String eventClassName = invoked.getParameterTypes()[0].getName(); CtMethod target = CtNewMethod.copy(targetMethod, newClass, null); target.setBody( "{" + "return \"" + invokedClassName + "." + invokedMethodName + "(" + eventClassName + ")\";" + "}" ); newClass.addMethod(target); CtMethod invoke = CtNewMethod.copy(invokeMethod, newClass, null); invoke.setBody( "{" + invokedClassName + " invokee = (" + invokedClassName + ")instance.get();" + // if the invokee goes out of scope, we can stop sending events to it "if (invokee != null) invokee." + invokedMethodName + "((" + eventClassName + ")$1);" + "}" ); newClass.addMethod(invoke); Class<? extends Invoker> invokerClass = classPool.toClass( newClass, getClass().getClassLoader(), getClass().getProtectionDomain() ); newClass.detach(); return invokerClass; } private List<Method> resolveAllListenerMethods(Class<?> c) { List<Method> result = new ArrayList<>(); do { for (Method m : c.getDeclaredMethods()) { if ((m.getAnnotation(Listener.class) != null) && !Modifier.isStatic(m.getModifiers()) && !Modifier.isPrivate(m.getModifiers()) && m.getParameterTypes().length == 1) { result.add(m); } } } while ((c = c.getSuperclass()) != Object.class); return result; } @Override public <I> void hear(TypeLiteral<I> type, final TypeEncounter<I> encounter) { // anything we heard here, we also want to wire encounter.register(new EventWiringInjectionListener<I>()); final Class<?> clazz = type.getRawType(); if (!TaskRunner.class.isAssignableFrom(clazz) && !Publisher.class.isAssignableFrom(clazz)) { String name = type.toString(); subscribers.computeIfAbsent(name, className -> { ArrayList<MethodInfo> result = new ArrayList<>(); try { for (Method method : resolveAllListenerMethods(clazz)) { result.add(new MethodInfo(method)); invokers.computeIfAbsent( Class.forName(method.getParameterTypes()[0].getName()), a -> new ConcurrentLinkedQueue<>() ); } } catch (NotFoundException nfe) { encounter.addError("unable to load " + className + " for subscription"); } catch (Exception e) { encounter.addError(e); } if (result.isEmpty()) { encounter.addError("%s is annotated as a @Subscriber but has no @Listener methods", className); } return Collections.unmodifiableList(result); }); } } }