guava EventBus learning two source codes

Keywords: Programming snapshot Database JDK Attribute

Part I This paper introduces the use of event bus in guava package, and deeply understands the implementation details in the source code

EventBus

understand

First of all, we need to look at the EventBus class. There are many comments on the class, which are summarized as follows:
Eventbus ensures that subscribers will not be accessed at the same time in a multithreaded environment, unless the subscriber has the allowcurrentevents annotation. If this annotation is not used, subscribers do not need to worry about message re-entry

The message receiving method can only receive one parameter, and will route messages to the specified subscriber according to the message type. If there is a parent-child relationship in the message type, then send the child message, and the parent subscriber will also receive it

3 when the call method is executed, all subscribers of the event will execute in sequence. If a subscriber takes a long time to process (such as database operation), the whole thread will block. To solve this problem, we can reduce the time-consuming operation of the subscriber as much as possible, such as asynchronous operation. The other is to use AsyncEventBus, AsyncEventBus uses multithreading to execute subscriber method

After sending a message, if there is no consumer consumption, the message is considered as "dead". We have defined the DeadEvent class to specifically handle these messages, and we can listen to such messages. If the message the subscriber listens to is Object, all the messages will be received, and there is no "dead" message

The EventBus properties are as follows

// event bus ID is a name
private final String identifier;
// jdk executor, used to execute threads
private final Executor executor;
// Exception handler
private final SubscriberExceptionHandler exceptionHandler;
// For registering subscribers
private final SubscriberRegistry subscribers = new SubscriberRegistry(this);
// Message distributor
private final Dispatcher dispatcher;

Let's start with a simple example

@Test
public void Section 1() {

    AwesomeEventBusDriver.register(new AwesomeStudent());
    AwesomeEventBusDriver.publishAnything("adopt~");
}

One step of EventBus creation is omitted here EventBus eventBus = new EventBus(); The source code is as follows. It's very simple. Set the name of EventBus and create some default classes. We'll see where these classes are used

/** Creates a new EventBus named "default". */
public EventBus() {
this("default");
}

public EventBus(String identifier) {
    this(
        identifier,
        MoreExecutors.directExecutor(),
        Dispatcher.perThreadDispatchQueue(),
        LoggingHandler.INSTANCE);
  }

register

Start event registration after creating EventBus

public void register(Object object) {
    subscribers.register(object);
}

Registration is handled by the SubscriberRegistry. The principle is to scan the listener, and then store the qualified (with subscription annotation) methods into the collection. The key of the collection is the input parameter type of the listener method, which is the basis of message routing later. Each key (message type) corresponds to a list (multiple subscribers) Let's take a look at the SubscriberRegistry class first. Note that the class is used to register subscribers to an event bus. The event bus specifies when instantiating the SubscriberRegistry class

 SubscriberRegistry(EventBus bus) {
    this.bus = checkNotNull(bus);
  }

The SubscriberRegistry class property, in addition to EventBus, has a collection class for storing registered subscribers

/**
* All registered subscribers, indexed by event type.
*
* <p>The {@link CopyOnWriteArraySet} values make it easy and relatively lightweight to get an
* immutable snapshot of all current subscribers to an event without any locking.
*/
private final ConcurrentMap<Class<?>, CopyOnWriteArraySet<Subscriber>> subscribers =
  Maps.newConcurrentMap();

For thread safety, the collection uses ConcurrentMap, and internally uses CopyOnWriteArraySet to store subscribers of the same route. The note explains that the CopyOnWriteArraySet can be used without locks, It is very simple and relatively lightweight to obtain an immutable snapshot of all subscribers subscribing to an event (route). The read of COW(Copy On Write) does not need to be locked because when modifying data, one copy will be copied, one copy will be read, one copy will be modified, and the previous content will be replaced after modification, so the read content will not be affected by the modified data In the registration phase, the Subscriber registry encapsulates the Subscriber as a Subscriber object. Let's look at the abstract Subscriber class of the Subscriber
According to the Convention, read the notes first. There are two sentences in total 1 this is a subscriber class (...) If two subscribers are the same method of the same object, then the two subscribers are equal. That is to say, the coordinates of each subscriber are the @ Subscribe decoration method and the object of the method. Each coordinate uniquely determines a subscriber. This can ensure that the subscriber will receive only one message if he or she repeatedly registers
OK, next is the attribute. It's very simple

/** Subscriber registered event bus */
@Weak private EventBus bus;

/** Subscriber method object */
@VisibleForTesting final Object target;

/** Subscriber method */
private final Method method;

/** Executor to use for dispatching events to this subscriber. */
private final Executor executor;

What's more interesting is the creation of subscribers. The code is as follows (how can my code be public class... Just look at it and low)

class Subscriber {

  /** Creates a {@code Subscriber} for {@code method} on {@code listener}. */
  static Subscriber create(EventBus bus, Object listener, Method method) {
    return isDeclaredThreadSafe(method)
        ? new Subscriber(bus, listener, method)
        : new SynchronizedSubscriber(bus, listener, method);
  }
  ...
  private Subscriber(EventBus bus, Object target, Method method) {
    this.bus = bus;
    this.target = checkNotNull(target);
    this.method = method;
    method.setAccessible(true);
    this.executor = bus.executor();
  }
  ...
  static final class SynchronizedSubscriber extends Subscriber {

    private SynchronizedSubscriber(EventBus bus, Object target, Method method) {
      super(bus, target, method);
    }
    ...
  }
}

The subscriber class provides a static creation method, in which two types of objects are created according to conditions. We can see from the name that one is common and the other is synchronous. I wonder if you remember a paragraph in the opening comment of EventBus class EventBus guarantees that the subscriber will not be accessed at the same time in the multithreaded environment unless the subscriber has marked the allowcurrentevents annotation. If this annotation is not used, the subscriber does not need to worry about the problem of message re-entry The principle is as follows: first, two different objects are generated according to @ allowcurrentevents annotation. The annotation modifies ordinary subscribers, and the annotation does not modify synchronous subscribers. That is to say, by default, subscribers are thread safe

private static boolean isDeclaredThreadSafe(Method method) {
    return method.getAnnotation(AllowConcurrentEvents.class) != null;
}

Synchronization subscribers are implemented by an internal class. The only special thing is that synchronization keywords are used when calling subscriber methods

@Override
void invokeSubscriberMethod(Object event) throws InvocationTargetException {
  synchronized (this) { // What's in it is still the calling parent method
    super.invokeSubscriberMethod(event);
  }
}

The test code is as follows

@Test
public void Multithread start() throws InterruptedException {
    MultiThreadSub threadSub = new MultiThreadSub();

    AwesomeEventBusDriver.register(threadSub);
    int count = 10000;
    Thread t1 = new Thread(() -> {

        int c1 = count;
        while (c1 > 0) {
            c1--;
            AwesomeEventBusDriver.publishAnything(1);
        }
    });

    Thread t2 = new Thread(() -> {

        int c1 = count;
        while (c1 > 0) {
            c1--;
            AwesomeEventBusDriver.publishAnything(1);
        }
    });
    t1.start();
    t2.start();

    t1.join();
    t2.join();

    threadSub.print();
}


public class MultiThreadSub {

    private int allowConcurrentSum = 0;
    private int noConcurrentSum = 0;

    @Subscribe
    @AllowConcurrentEvents
    public void addAllow(Integer i) {
        allowConcurrentSum += i;
    }

    @Subscribe
    public void addNo(Integer i) {
        noConcurrentSum += i;
    }

    public void print() {
        System.out.println("allowConcurrentSum: " + allowConcurrentSum);
        System.out.println("noConcurrentSum: " + noConcurrentSum);
    }
}

result

Then scan the registration class, and get all the subscription methods of the registration class through the following methods

private static ImmutableList<Method> getAnnotatedMethods(Class<?> clazz) {
    return subscriberMethodsCache.getUnchecked(clazz);
}

The call level of getUnchecked method is relatively deep. The details need not be concerned at first. Finally, the following method will be called

private static ImmutableList<Method> getAnnotatedMethodsNotCached(Class<?> clazz) {
    Set<? extends Class<?>> supertypes = TypeToken.of(clazz).getTypes().rawTypes();
    Map<MethodIdentifier, Method> identifiers = Maps.newHashMap();
    for (Class<?> supertype : supertypes) {
      for (Method method : supertype.getDeclaredMethods()) {
        if (method.isAnnotationPresent(Subscribe.class) && !method.isSynthetic()) {
          // TODO(cgdecker): Should check for a generic parameter type and error out
          Class<?>[] parameterTypes = method.getParameterTypes();
          // This is the number of parameters being checked here
          checkArgument(
              parameterTypes.length == 1,
              "Method %s has @Subscribe annotation but has %s parameters."
                  + "Subscriber methods must have exactly 1 parameter.",
              method,
              parameterTypes.length);

          MethodIdentifier ident = new MethodIdentifier(method);
          if (!identifiers.containsKey(ident)) {
            identifiers.put(ident, method);
          }
        }
      }
    }
    return ImmutableList.copyOf(identifiers.values());
  }
// Method entry
/**
* A thread-safe cache that contains the mapping from each class to all methods in that class and
* all super-classes, that are annotated with {@code @Subscribe}. The cache is shared across all
* instances of this class; this greatly improves performance if multiple EventBus instances are
* created and objects of the same class are registered on all of them.
*/
private static final LoadingCache<Class<?>, ImmutableList<Method>> subscriberMethodsCache =
  CacheBuilder.newBuilder()
      .weakKeys()
      .build(
          new CacheLoader<Class<?>, ImmutableList<Method>>() {
            @Override
            public ImmutableList<Method> load(Class<?> concreteClass) throws Exception {
              return getAnnotatedMethodsNotCached(concreteClass);
            }
          });

This method will scan the registered class and all the parent classes of the registered class, and cache the @ Subscribe method. key is the parameter type of the method. This advantage is that the same class will not scan multiple times
There is a comment todo (cgdecker) in the method: should check for a generic parameter type and error out, which roughly means that later versions should check the generic parameters and throw exceptions. Let's see how to handle the generic parameters

public class GenericObject<T> {

    @Subscribe
    public void TestGeneric(T obj) {

        System.out.println("generic " + obj);
    }
}

@Test
public void generic paradigm() {

    AwesomeEventBusDriver.register(new GenericObject<String>());
    AwesomeEventBusDriver.publishAnything("string");
    AwesomeEventBusDriver.publishAnything(90);
    AwesomeEventBusDriver.publishAnything(100L);
}

// results of enforcement
generic string
generic 90
generic 100

It can be seen from the results that generics don't work. In fact, it's easy to understand that generics will be erased after compilation. At runtime, generics will be considered as objects. After testing generics, you immediately think of the basic type. debug to the above code, you can see that the registered method parameters (method parameter type is int) are int.class . but if we send a number like 5, It will be automatically boxed to Integer type, so the corresponding subscriber cannot be found when sending. However, if the int type is changed to Integer, the message can be received

Registration stage is almost here, the principle is very simple, you can see the details of the code

send out

The code is as follows

/**
* Posts an event to all registered subscribers. This method will return successfully after the
* event has been posted to all subscribers, and regardless of any exceptions thrown by
* subscribers.
*
* <p>If no subscribers have been subscribed for {@code event}'s class, and {@code event} is not
* already a {@link DeadEvent}, it will be wrapped in a DeadEvent and reposted.
*
* @param event event to post.
*/
public void post(Object event) {
    Iterator<Subscriber> eventSubscribers = subscribers.getSubscribers(event);
    if (eventSubscribers.hasNext()) {
      dispatcher.dispatch(event, eventSubscribers);
    } else if (!(event instanceof DeadEvent)) {
      // the event had no subscribers and was not itself a DeadEvent
      post(new DeadEvent(this, event));
    }
}

First look at the notes, two sentences 1. The sending method will send the event to all subscribers. As long as the subscriber receives the event, the sending method will be put back successfully, regardless of whether the error is reported after receiving the event. That is to say, if the subscriber reports the error, the event message will be lost. When using the event, you should consider the scenario If the event has no subscribers and is not a DeadEvent class, the event will be wrapped as a DeadEvent class and sent again

If the registration phase is clear, the sending logic is very simple
First, get the corresponding subscriber according to the type of message sent, then call the dispatch method of Dispatcher class. Dispatcher instance is created when EventBus is instantiated, PerThreadQueuedDispatcher is used synchronously, and LegacyAsyncDispatcher. is used asynchronously to see the synchronization.

/** Per-thread queue of events to dispatch. */
private final ThreadLocal<Queue<Event>> queue =
    new ThreadLocal<Queue<Event>>() {
      @Override
      protected Queue<Event> initialValue() {
        return Queues.newArrayDeque();
      }
    };

/** Per-thread dispatch state, used to avoid reentrant event dispatching. */
private final ThreadLocal<Boolean> dispatching =
    new ThreadLocal<Boolean>() {
      @Override
      protected Boolean initialValue() {
        return false;
      }
    };
@Override
void dispatch(Object event, Iterator<Subscriber> subscribers) {
  checkNotNull(event);
  checkNotNull(subscribers);
  // A queue saved by the current thread, which is the subsequent send queue
  Queue<Event> queueForThread = queue.get();
  // Queue events to be distributed
  queueForThread.offer(new Event(event, subscribers));

  if (!dispatching.get()) { // If it's not being sent, send it
    dispatching.set(true); // Set sending (? Multithreading threshold)
    try {
      Event nextEvent;
      while ((nextEvent = queueForThread.poll()) != null) {
        while (nextEvent.subscribers.hasNext()) {
        // Get Subscriber class Subscriber and execute sending method
          nextEvent.subscribers.next().dispatchEvent(nextEvent.event);
        }
      }
    } finally {
      dispatching.remove();
      queue.remove();
    }
  }
}

Through the class name PerThreadQueuedDispatcher, I know that this is the sender of "one thread and one queue" (...). Seeing the two ThreadLocal properties of this class, I can understand the meaning of PreThread. When sending, I first get the queue from ThreadLocal, and then insert the content to be sent into the queue, Next, judge whether it is currently being sent according to another ThreadLocal object dispatching. If it is being sent, because the message has already entered the queue, it is not necessary to worry about it. If it is not sent, execute the sending logic. Take the data out of the queue, and then execute the dispatchEvent method of the data subscriber. The synchronous sending method of the subscriber has been mentioned before, and the last is a reflection Then look at asynchronous sending. When asynchronous EventBus is instantiated, the only difference is that it enters a thread pool and specifies the Dispatcher as LegacyAsyncDispatcher. In terms of sending logic, it is also very simple. Unlike synchronization, the queue uses thread safe classes to save. There is no big difference
If you are a sensitive bank, you may have found that there is another Dispatcher class, ImmediateDispatcher, but the author has not found that it has been used in the package. This class is simpler. From the name, it does not need to queue to store messages, but directly sends messages to subscribers

summary

At this point, the learning of EventBus is over. This paper starts with the source code and goes through the execution process of event bus from the beginning. The author does not explain the source code line by line, but just explains the general process, and then takes out the more interesting part and tells his own opinions. If there are interesting points later, continue to add

Posted by porta325 on Mon, 25 May 2020 18:25:41 -0700