client: LambdaMetaFactory for subscribe annotation, java target… (#2128)

client: LambdaMetaFactory for subscribe annotation, java target level 11
This commit is contained in:
Owain van Brakel
2019-12-17 18:02:00 +01:00
committed by GitHub
23 changed files with 221 additions and 319 deletions

View File

@@ -53,7 +53,7 @@ public class ClientSessionManager
{
sessionClient.openSession()
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.from(clientThread))
.observeOn(Schedulers.single())
.subscribe(this::setUuid, this::error);
}
@@ -68,7 +68,7 @@ public class ClientSessionManager
sessionClient.pingSession(sessionId)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.from(clientThread))
.observeOn(Schedulers.single())
.doOnError(this::error)
.subscribe();
}
@@ -79,7 +79,7 @@ public class ClientSessionManager
{
sessionClient.delete(sessionId)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.from(clientThread))
.observeOn(Schedulers.single())
.doOnError(this::error)
.subscribe();

View File

@@ -258,7 +258,7 @@ public class RuneLite
final ClientLoader clientLoader = new ClientLoader(options.valueOf(updateMode));
Completable.fromAction(clientLoader::get)
.subscribeOn(Schedulers.single())
.subscribeOn(Schedulers.computation())
.subscribe();
Completable.fromAction(ClassPreloader::preload)

View File

@@ -25,6 +25,8 @@
package net.runelite.client.callback;
import com.google.inject.Inject;
import io.reactivex.plugins.RxJavaPlugins;
import io.reactivex.schedulers.Schedulers;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
@@ -45,6 +47,12 @@ public class ClientThread implements Executor
@Nullable
private Client client;
@Inject
private ClientThread()
{
RxJavaPlugins.setSingleSchedulerHandler(old -> Schedulers.from(this));
}
public void invoke(Runnable r)
{
invoke(() ->

View File

@@ -26,11 +26,11 @@ package net.runelite.client.config;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import net.runelite.client.util.ReflectUtil;
@Slf4j
class ConfigInvocationHandler implements InvocationHandler
@@ -169,7 +169,7 @@ class ConfigInvocationHandler implements InvocationHandler
static Object callDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable
{
Class<?> declaringClass = method.getDeclaringClass();
return ReflectUtil.privateLookupIn(declaringClass)
return MethodHandles.privateLookupIn(declaringClass, MethodHandles.lookup())
.unreflectSpecial(method, declaringClass)
.bindTo(proxy)
.invokeWithArguments(args);

View File

@@ -449,6 +449,10 @@ public class ConfigManager
{
return Integer.parseInt(str);
}
if (type == long.class)
{
return Long.parseLong(str);
}
if (type == Color.class)
{
return ColorUtil.fromString(str);
@@ -615,6 +619,10 @@ public class ConfigManager
{
return ((EnumSet) object).toArray()[0].getClass().getCanonicalName() + "{" + object.toString() + "}";
}
if (object instanceof Number)
{
return String.valueOf(object);
}
return object.toString();
}

View File

@@ -0,0 +1,85 @@
package net.runelite.client.eventbus;
import com.google.common.collect.ImmutableSet;
import io.reactivex.functions.Consumer;
import java.lang.invoke.CallSite;
import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
import static java.lang.invoke.MethodType.methodType;
import java.lang.reflect.Method;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.events.Event;
@Slf4j
public class AccessorGenerator
{
public static Set<Subscription> scanSubscribes(Lookup caller, Object ref)
{
ImmutableSet.Builder<Subscription> builder = ImmutableSet.builder();
final Class<?> refClass = ref.getClass();
caller = getPrivateAccess(refClass, caller).in(refClass);
for (Method method : refClass.getDeclaredMethods())
{
Subscribe sub = method.getAnnotation(Subscribe.class);
if (sub != null)
{
final Consumer accessor;
final Class paramType = method.getParameterTypes()[0];
try
{
accessor = getConsumerFor(caller, ref, method);
}
catch (Throwable t)
{
log.warn("Creating consumer lambda for {} failed!", method, t);
continue;
}
builder.add(new Subscription(paramType, accessor, sub.takeUntil(), sub.subscribe(), sub.observe()));
}
}
return builder.build();
}
@SuppressWarnings("unchecked")
private static <EVENT extends Event> Consumer<EVENT> getConsumerFor(Lookup caller, Object ref, Method method) throws Throwable
{
final MethodHandle methodHandle = caller.unreflect(method);
final MethodType actualConsumer = methodHandle.type().dropParameterTypes(0, 1);
final MethodType eventsConsumer = actualConsumer.erase();
final MethodType factoryType = methodType(Consumer.class, ref.getClass());
final CallSite callsite = LambdaMetafactory.metafactory(
caller, // To get past security checks
"accept", // The name of the method to implement inside the lambda type
factoryType, // Signature of the factory method
eventsConsumer, // Signature of function implementation
methodHandle, // Target method
actualConsumer // Target method signature
);
final MethodHandle factory = callsite.getTarget();
return (Consumer<EVENT>) factory.invoke(ref);
}
private static Lookup getPrivateAccess(Class<?> into, Lookup from)
{
try
{
return MethodHandles.privateLookupIn(into, from);
}
catch (IllegalAccessException a)
{
log.warn("Failed to get private access in {} from {}", into, from, a);
return from;
}
}
}

View File

@@ -9,7 +9,6 @@ import io.reactivex.annotations.Nullable;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
import io.sentry.Sentry;
import java.util.HashMap;
import java.util.Map;
@@ -19,7 +18,6 @@ import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.events.Event;
import net.runelite.client.RuneLiteProperties;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.config.OpenOSRSConfig;
@Slf4j
@@ -33,9 +31,6 @@ public class EventBus implements EventBusInterface
@Inject
private OpenOSRSConfig openOSRSConfig;
@Inject
private ClientThread clientThread;
@NonNull
private <T extends Event> Relay<Object> getSubject(Class<T> eventClass)
{
@@ -60,41 +55,9 @@ public class EventBus implements EventBusInterface
return observable -> until > 0 ? observable.take(until) : observable;
}
private Scheduler getScheduler(EventScheduler scheduler)
{
Scheduler subscribeScheduler;
switch (scheduler)
{
case COMPUTATION:
subscribeScheduler = Schedulers.computation();
break;
case IO:
subscribeScheduler = Schedulers.io();
break;
case NEWTHREAD:
subscribeScheduler = Schedulers.newThread();
break;
case SINGLE:
subscribeScheduler = Schedulers.single();
break;
case TRAMPOLINE:
subscribeScheduler = Schedulers.trampoline();
break;
case CLIENT:
subscribeScheduler = Schedulers.from(clientThread);
break;
case DEFAULT:
default:
subscribeScheduler = null;
break;
}
return subscribeScheduler;
}
private <T> ObservableTransformer<T, T> applyScheduler(EventScheduler eventScheduler, boolean subscribe)
{
Scheduler scheduler = getScheduler(eventScheduler);
Scheduler scheduler = eventScheduler.get();
return observable -> scheduler == null ? observable : subscribe ? observable.subscribeOn(scheduler) : observable.observeOn(scheduler);
}

View File

@@ -1,21 +1,27 @@
package net.runelite.client.eventbus;
import io.reactivex.Scheduler;
import io.reactivex.annotations.Nullable;
import io.reactivex.schedulers.Schedulers;
import java.util.function.Supplier;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public enum EventScheduler
{
DEFAULT(null),
COMPUTATION("computation"),
IO("io"),
NEWTHREAD("newThread"),
SINGLE("single"),
TRAMPOLINE("trampoline"),
CLIENT("client");
DEFAULT(() -> null),
COMPUTATION(Schedulers::computation),
IO(Schedulers::io),
NEWTHREAD(Schedulers::newThread),
SINGLE(Schedulers::single),
TRAMPOLINE(Schedulers::trampoline),
CLIENT(Schedulers::single);
public final String scheduler;
private Supplier<Scheduler> scheduler;
EventScheduler(@Nullable String scheduler)
@Nullable
public Scheduler get()
{
this.scheduler = scheduler;
return scheduler.get();
}
}

View File

@@ -0,0 +1,20 @@
package net.runelite.client.eventbus;
import io.reactivex.functions.Consumer;
import lombok.Value;
@Value
public class Subscription
{
private final Class type;
private final Consumer method;
private final int takeUntil;
private final EventScheduler subscribe;
private final EventScheduler observe;
@SuppressWarnings("unchecked")
public void subscribe(EventBus eventBus, Object lifecycle)
{
eventBus.subscribe(type, lifecycle, method, takeUntil, subscribe, observe);
}
}

View File

@@ -24,25 +24,23 @@
*/
package net.runelite.client.plugins;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Binder;
import com.google.inject.Injector;
import com.google.inject.Module;
import io.reactivex.functions.Consumer;
import java.lang.reflect.Method;
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import java.lang.invoke.MethodHandles;
import java.util.Collection;
import java.util.Set;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Value;
import net.runelite.api.events.Event;
import net.runelite.client.eventbus.AccessorGenerator;
import net.runelite.client.eventbus.EventBus;
import net.runelite.client.eventbus.EventScheduler;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.eventbus.Subscription;
public abstract class Plugin implements Module
{
private final Set<Subscription> annotatedSubscriptions = findSubscriptions();
private final Object annotatedSubsLock = new Object();
private Set<Subscription> annotatedSubscriptions = null;
@Getter(AccessLevel.PROTECTED)
protected Injector injector;
@@ -60,53 +58,33 @@ public abstract class Plugin implements Module
{
}
@SuppressWarnings("unchecked")
final void addAnnotatedSubscriptions(EventBus eventBus)
{
annotatedSubscriptions.forEach(sub -> eventBus.subscribe(sub.type, annotatedSubsLock, sub.method, sub.takeUntil, sub.subscribe, sub.observe));
if (annotatedSubscriptions == null)
{
Observable.fromCallable(this::findSubscriptions)
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.single())
.subscribe(subs -> addSubs(eventBus, (annotatedSubscriptions = subs)));
}
else
{
addSubs(eventBus, annotatedSubscriptions);
}
}
final void removeAnnotatedSubscriptions(EventBus eventBus)
{
eventBus.unregister(annotatedSubsLock);
eventBus.unregister(this);
}
private Set<Subscription> findSubscriptions()
{
ImmutableSet.Builder<Subscription> builder = ImmutableSet.builder();
for (Method method : this.getClass().getDeclaredMethods())
{
Subscribe annotation = method.getAnnotation(Subscribe.class);
if (annotation == null)
{
continue;
}
assert method.getParameterCount() == 1 : "Methods annotated with @Subscribe should have only one parameter";
Class<?> type = method.getParameterTypes()[0];
assert Event.class.isAssignableFrom(type) : "Parameters of methods annotated with @Subscribe should implement net.runelite.api.events.Event";
assert method.getReturnType() == void.class : "Methods annotated with @Subscribe should have a void return type";
method.setAccessible(true);
Subscription sub = new Subscription(type.asSubclass(Event.class), event -> method.invoke(this, event), annotation.takeUntil(), annotation.subscribe(), annotation.observe());
builder.add(sub);
}
return builder.build();
return AccessorGenerator.scanSubscribes(MethodHandles.lookup(), this);
}
@Value
private static class Subscription
private void addSubs(EventBus eventBus, Collection<Subscription> subs)
{
private final Class type;
private final Consumer method;
private final int takeUntil;
private final EventScheduler subscribe;
private final EventScheduler observe;
subs.forEach(s -> s.subscribe(eventBus, this));
}
}

View File

@@ -25,6 +25,7 @@
package net.runelite.client.plugins;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.graph.Graph;
@@ -218,13 +219,17 @@ public class PluginManager
public void startCorePlugins()
{
List<Plugin> scannedPlugins = new ArrayList<>(plugins);
int loaded = 0;
int loaded = 0, started = 0;
final Stopwatch timer = Stopwatch.createStarted();
for (Plugin plugin : scannedPlugins)
{
try
{
startPlugin(plugin);
if (startPlugin(plugin))
{
++started;
}
}
catch (PluginInstantiationException ex)
{
@@ -236,6 +241,8 @@ public class PluginManager
RuneLiteSplashScreen.stage(.80, 1, "Starting plugins", loaded, scannedPlugins.size());
}
log.debug("Started {}/{} plugins in {}", started, loaded, timer);
}
@SuppressWarnings("unchecked")

View File

@@ -93,7 +93,7 @@ public class HydraPlugin extends Plugin
private boolean inHydraInstance;
private int lastAttackTick;
@Inject
private Client client;

View File

@@ -185,9 +185,8 @@ public class BlackjackPlugin extends Plugin
@Override
public boolean matches(MenuEntry entry)
{
return
Text.removeTags(entry.getTarget(), true).equalsIgnoreCase(this.getTarget()) &&
entry.getOption().equalsIgnoreCase(this.getOption());
return entry.getOption().equalsIgnoreCase(this.getOption()) &&
Text.removeTags(entry.getTarget(), true).equalsIgnoreCase(this.getTarget());
}
}
}

View File

@@ -1025,7 +1025,7 @@ public class ChatCommandsPlugin extends Plugin
ItemPrice item = retrieveFromList(results, search);
CLIENT.lookupItem(item.getId())
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.from(clientThread))
.observeOn(Schedulers.single())
.subscribe(
(osbresult) ->
{

View File

@@ -362,7 +362,7 @@ public class ExaminePlugin extends Plugin
int finalQuantity = quantity;
CLIENT.lookupItem(id)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.from(clientThread))
.observeOn(Schedulers.single())
.subscribe(
(osbresult) ->
{

View File

@@ -605,7 +605,7 @@ public class GrandExchangePlugin extends Plugin
{
CLIENT.lookupItem(itemId)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.from(clientThread))
.observeOn(Schedulers.single())
.subscribe(
(osbresult) ->
{

View File

@@ -1,72 +0,0 @@
/*
* Copyright (c) 2018, Tomas Slusny <slusnucky@gmail.com>
* Copyright (c) 2018, Abex
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.util;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ReflectUtil
{
private ReflectUtil()
{
}
public static MethodHandles.Lookup privateLookupIn(Class clazz)
{
try
{
// Java 9+ has privateLookupIn method on MethodHandles, but since we are shipping and using Java 8
// we need to access it via reflection. This is preferred way because it's Java 9+ public api and is
// likely to not change
final Method privateLookupIn = MethodHandles.class.getMethod("privateLookupIn", Class.class, MethodHandles.Lookup.class);
return (MethodHandles.Lookup) privateLookupIn.invoke(null, clazz, MethodHandles.lookup());
}
catch (InvocationTargetException | IllegalAccessException e)
{
throw new RuntimeException(e);
}
catch (NoSuchMethodException e)
{
try
{
// In Java 8 we first do standard lookupIn class
final MethodHandles.Lookup lookupIn = MethodHandles.lookup().in(clazz);
// and then we mark it as trusted for private lookup via reflection on private field
final Field modes = MethodHandles.Lookup.class.getDeclaredField("allowedModes");
modes.setAccessible(true);
modes.setInt(lookupIn, -1); // -1 == TRUSTED
return lookupIn;
}
catch (ReflectiveOperationException ex)
{
throw new RuntimeException(ex);
}
}
}
}