Generate actual classes for LP events at runtime instead of using proxies + InvocationHandlers

This commit is contained in:
Luck 2020-03-30 19:18:34 +01:00
parent b65639cd76
commit c3128dec0d
No known key found for this signature in database
GPG Key ID: EFA9B3EC5FD90F8B
13 changed files with 266 additions and 232 deletions

View File

@ -55,6 +55,7 @@ shadowJar {
relocate 'com.github.benmanes.caffeine', 'me.lucko.luckperms.lib.caffeine'
relocate 'okio', 'me.lucko.luckperms.lib.okio'
relocate 'okhttp3', 'me.lucko.luckperms.lib.okhttp3'
relocate 'net.bytebuddy', 'me.lucko.luckperms.lib.bytebuddy'
relocate 'me.lucko.commodore', 'me.lucko.luckperms.lib.commodore'
relocate 'org.mariadb.jdbc', 'me.lucko.luckperms.lib.mariadb'
relocate 'com.mysql', 'me.lucko.luckperms.lib.mysql'

View File

@ -41,6 +41,7 @@ shadowJar {
relocate 'com.github.benmanes.caffeine', 'me.lucko.luckperms.lib.caffeine'
relocate 'okio', 'me.lucko.luckperms.lib.okio'
relocate 'okhttp3', 'me.lucko.luckperms.lib.okhttp3'
relocate 'net.bytebuddy', 'me.lucko.luckperms.lib.bytebuddy'
relocate 'me.lucko.commodore', 'me.lucko.luckperms.lib.commodore'
relocate 'org.mariadb.jdbc', 'me.lucko.luckperms.lib.mariadb'
relocate 'com.mysql', 'me.lucko.luckperms.lib.mysql'

View File

@ -21,6 +21,7 @@ dependencies {
compile 'com.github.ben-manes.caffeine:caffeine:2.8.0'
compile 'com.squareup.okhttp3:okhttp:3.14.4'
compile 'com.squareup.okio:okio:1.17.4'
compile 'net.bytebuddy:byte-buddy:1.10.9'
compile('me.lucko.configurate:configurate-core:3.5') {
exclude(module: 'guava')
}

View File

@ -127,6 +127,13 @@ public enum Dependency {
Relocation.of(RelocationHelper.OKHTTP3_STRING, RelocationHelper.OKHTTP3_STRING),
Relocation.of(RelocationHelper.OKIO_STRING, RelocationHelper.OKIO_STRING)
),
BYTEBUDDY(
"net{}bytebuddy",
"byte-buddy",
"1.10.9",
"B7nKbi+XDLA/SyVlHfHy/OJx1JG0TgQJgniHeG9pLU0=",
Relocation.of("bytebuddy", "net{}bytebuddy")
),
COMMODORE(
"me{}lucko",
"commodore",

View File

@ -32,7 +32,7 @@ import me.lucko.luckperms.common.api.implementation.ApiPermissionHolder;
import me.lucko.luckperms.common.api.implementation.ApiUser;
import me.lucko.luckperms.common.cacheddata.GroupCachedDataManager;
import me.lucko.luckperms.common.cacheddata.UserCachedDataManager;
import me.lucko.luckperms.common.event.gen.GeneratedEventSpec;
import me.lucko.luckperms.common.event.gen.GeneratedEventClass;
import me.lucko.luckperms.common.event.model.EntitySourceImpl;
import me.lucko.luckperms.common.event.model.SenderPlatformEntity;
import me.lucko.luckperms.common.event.model.UnknownSource;
@ -132,7 +132,11 @@ public final class EventDispatcher {
@SuppressWarnings("unchecked")
private <T extends LuckPermsEvent> T generate(Class<T> eventClass, Object... params) {
return (T) GeneratedEventSpec.lookup(eventClass).newInstance(this.eventBus.getApiProvider(), params);
try {
return (T) GeneratedEventClass.generate(eventClass).newInstance(this.eventBus.getApiProvider(), params);
} catch (Throwable e) {
throw new RuntimeException("Exception occurred whilst generating event instance", e);
}
}
public void dispatchExtensionLoad(Extension extension) {

View File

@ -0,0 +1,54 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package me.lucko.luckperms.common.event.gen;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.event.LuckPermsEvent;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.lang.invoke.MethodHandles;
/**
* Abstract implementation of {@link LuckPermsEvent}.
*/
public abstract class AbstractEvent implements LuckPermsEvent {
private final LuckPerms api;
protected AbstractEvent(LuckPerms api) {
this.api = api;
}
@Override
public @NonNull LuckPerms getLuckPerms() {
return this.api;
}
// Overridden by the subclass. Used by GeneratedEventClass to get setter method handles.
public MethodHandles.Lookup mhl() {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,192 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package me.lucko.luckperms.common.event.gen;
import me.lucko.luckperms.common.cache.LoadingMap;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.ClassFileVersion;
import net.bytebuddy.description.NamedElement;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.FixedValue;
import net.bytebuddy.implementation.MethodCall;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.event.LuckPermsEvent;
import net.luckperms.api.event.util.Param;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Map;
import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.returns;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
/**
* Holds the generated event class for a given type of {@link LuckPermsEvent}.
*/
public class GeneratedEventClass {
/** The MethodHandles.lookup() method */
private static final Method METHOD_HANDLES_LOOKUP;
static {
try {
METHOD_HANDLES_LOOKUP = MethodHandles.class.getMethod("lookup");
} catch (NoSuchMethodException e) {
throw new ExceptionInInitializerError(e);
}
}
/**
* A loading cache of event types to {@link GeneratedEventClass}es.
*/
private static final Map<Class<? extends LuckPermsEvent>, GeneratedEventClass> CACHE = LoadingMap.of(clazz -> {
try {
return new GeneratedEventClass(clazz);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
});
/**
* Generate a {@link GeneratedEventClass} for the given {@code event} type.
*
* @param event the event type
* @return the generated class
*/
public static GeneratedEventClass generate(Class<? extends LuckPermsEvent> event) {
return CACHE.get(event);
}
/**
* The constructor used to create instances of the generated class
*/
private final Constructor<? extends AbstractEvent> constructor;
/**
* An array of {@link MethodHandle}s, which can set values for each of the properties in the event class.
*/
private final MethodHandle[] setters;
private GeneratedEventClass(Class<? extends LuckPermsEvent> eventClass) throws ReflectiveOperationException {
// get a TypeDescription for the event class
TypeDescription eventClassType = new TypeDescription.ForLoadedType(eventClass);
// determine a generated class name of the event
String eventClassSuffix = eventClass.getName().substring(LuckPermsEvent.class.getPackage().getName().length());
String generatedClassName = GeneratedEventClass.class.getPackage().getName() + eventClassSuffix;
DynamicType.Builder<AbstractEvent> builder = new ByteBuddy(ClassFileVersion.JAVA_V8)
// create a subclass of AbstractEvent
.subclass(AbstractEvent.class, ConstructorStrategy.Default.IMITATE_SUPER_CLASS_OPENING)
// using the predetermined generated class name
.name(generatedClassName)
// implement the event interface
.implement(eventClassType)
// implement all methods annotated with Param by simply returning the value from the corresponding field with the same name
.method(isAnnotatedWith(Param.class))
.intercept(FieldAccessor.of(NamedElement.WithRuntimeName::getInternalName))
// implement LuckPermsEvent#getEventType by returning the event class type
.method(named("getEventType").and(returns(Class.class)).and(takesArguments(0)))
.intercept(FixedValue.value(eventClassType))
// implement AbstractEvent#mh by calling & returning the value of MethodHandles.lookup()
.method(named("mhl").and(returns(MethodHandles.Lookup.class)).and(takesArguments(0)))
.intercept(MethodCall.invoke(METHOD_HANDLES_LOOKUP))
// implement a toString method
.withToString();
// get a sorted array of all methods on the event interface annotated with @Param
Method[] properties = Arrays.stream(eventClass.getMethods())
.filter(m -> m.isAnnotationPresent(Param.class))
.sorted(Comparator.comparingInt(o -> o.getAnnotation(Param.class).value()))
.toArray(Method[]::new);
// for each method on the event interface annotated with @Param
for (Method method : properties) {
if (!method.isAnnotationPresent(Param.class)) {
continue;
}
// define a field on the generated class to hold the value
builder = builder.defineField(method.getName(), method.getReturnType(), Visibility.PRIVATE);
}
// finish building, load the class, get a constructor
Class<? extends AbstractEvent> generatedClass = builder.make().load(GeneratedEventClass.class.getClassLoader()).getLoaded();
this.constructor = generatedClass.getDeclaredConstructor(LuckPerms.class);
// create a dummy instance of the generated class & get the method handle lookup instance
MethodHandles.Lookup lookup = this.constructor.newInstance(new Object[]{null}).mhl();
// get 'setter' MethodHandles
this.setters = new MethodHandle[properties.length];
for (int i = 0; i < properties.length; i++) {
Method method = properties[i];
// obtain a setter MethodHandle for the property
this.setters[i] = lookup.findSetter(generatedClass, method.getName(), method.getReturnType())
.asType(MethodType.methodType(void.class, new Class[]{AbstractEvent.class, Object.class}));
}
}
/**
* Creates a new instance of the event class.
*
* @param api an instance of the LuckPerms API
* @param properties the event properties
* @return the event instance
* @throws Throwable if something goes wrong
*/
public LuckPermsEvent newInstance(LuckPerms api, Object... properties) throws Throwable {
if (properties.length != this.setters.length) {
throw new IllegalStateException("Unexpected number of properties. given: " + properties.length + ", expected: " + this.setters.length);
}
// create a new instance of the event
final AbstractEvent event = this.constructor.newInstance(api);
// set the properties onto the event instance
for (int i = 0; i < this.setters.length; i++) {
MethodHandle setter = this.setters[i];
Object value = properties[i];
setter.invokeExact(event, value);
}
return event;
}
}

View File

@ -1,167 +0,0 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package me.lucko.luckperms.common.event.gen;
import com.google.common.collect.ImmutableList;
import me.lucko.luckperms.common.cache.LoadingMap;
import me.lucko.luckperms.common.util.ImmutableCollectors;
import me.lucko.luckperms.common.util.PrivateMethodHandles;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.event.LuckPermsEvent;
import net.luckperms.api.event.util.Param;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
/**
* Represents the generated specification for an instance of a given {@link LuckPermsEvent}.
*/
public class GeneratedEventSpec {
private static final Method TO_STRING_METHOD;
private static final Method EQUALS_METHOD;
private static final Method HASHCODE_METHOD;
private static final Method GET_LUCKPERMS_METHOD;
private static final Method GET_EVENT_TYPE_METHOD;
static {
try {
TO_STRING_METHOD = Object.class.getMethod("toString");
EQUALS_METHOD = Object.class.getMethod("equals", Object.class);
HASHCODE_METHOD = Object.class.getMethod("hashCode");
GET_LUCKPERMS_METHOD = LuckPermsEvent.class.getMethod("getLuckPerms");
GET_EVENT_TYPE_METHOD = LuckPermsEvent.class.getMethod("getEventType");
} catch (NoSuchMethodException e) {
throw new ExceptionInInitializerError(e);
}
}
private static final Map<Class<? extends LuckPermsEvent>, GeneratedEventSpec> CACHE = LoadingMap.of(GeneratedEventSpec::new);
public static GeneratedEventSpec lookup(Class<? extends LuckPermsEvent> event) {
return CACHE.get(event);
}
private final Class<? extends LuckPermsEvent> eventClass;
private final List<Method> methods;
private final List<Class<?>> returnTypes;
private GeneratedEventSpec(Class<? extends LuckPermsEvent> eventClass) {
this.eventClass = eventClass;
List<Method> methods = new ArrayList<>();
for (Method method : eventClass.getMethods()) {
if (method.isDefault()) {
continue;
}
if (GET_LUCKPERMS_METHOD.equals(method) || GET_EVENT_TYPE_METHOD.equals(method)) {
continue;
}
methods.add(method);
}
methods.sort(Comparator.comparingInt(o -> o.isAnnotationPresent(Param.class) ? o.getAnnotation(Param.class).value() : 0));
this.methods = ImmutableList.copyOf(methods);
this.returnTypes = this.methods.stream()
.map(Method::getReturnType)
.collect(ImmutableCollectors.toList());
}
public LuckPermsEvent newInstance(LuckPerms api, Object... params) {
if (params.length != this.methods.size()) {
throw new IllegalStateException("param length differs from number of methods. expected " + this.methods.size() + " - " + this.methods);
}
for (int i = 0; i < params.length; i++) {
Object param = params[i];
Class<?> expectedType = this.returnTypes.get(i);
if (!expectedType.isInstance(param)) {
throw new IllegalArgumentException("Parameter at index " + i + " cannot be assigned to " + expectedType);
}
}
EventInvocationHandler eventInvocationHandler = new EventInvocationHandler(api, params);
return (LuckPermsEvent) Proxy.newProxyInstance(GeneratedEventSpec.class.getClassLoader(), new Class[]{this.eventClass}, eventInvocationHandler);
}
/**
* An invocation handler bound to a set of parameters, used to implement the event as a proxy.
*/
private final class EventInvocationHandler implements InvocationHandler {
private final LuckPerms api;
private final Object[] fields;
EventInvocationHandler(LuckPerms api, Object[] fields) {
this.api = api;
this.fields = fields;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (TO_STRING_METHOD.equals(method)) {
return "GeneratedEvent(" +
"proxy=" + proxy.getClass().getName() + "@" + Integer.toHexString(proxy.hashCode()) + ", " +
"class=" + GeneratedEventSpec.this.eventClass.getName() + ", " +
"fields=" + Arrays.toString(this.fields) + ")";
}
if (EQUALS_METHOD.equals(method)) {
return proxy == args[0];
}
if (HASHCODE_METHOD.equals(method)) {
return System.identityHashCode(proxy);
}
if (GET_LUCKPERMS_METHOD.equals(method)) {
return this.api;
}
if (GET_EVENT_TYPE_METHOD.equals(method)) {
return GeneratedEventSpec.this.eventClass;
}
if (method.getDeclaringClass() == Object.class || method.isDefault()) {
return PrivateMethodHandles.privateLookup(method.getDeclaringClass())
.unreflectSpecial(method, method.getDeclaringClass())
.bindTo(proxy)
.invokeWithArguments(args);
}
int methodIndex = GeneratedEventSpec.this.methods.indexOf(method);
if (methodIndex == -1) {
throw new UnsupportedOperationException("Method not supported: " + method);
}
return this.fields[methodIndex];
}
}
}

View File

@ -250,6 +250,7 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin {
Dependency.CAFFEINE,
Dependency.OKIO,
Dependency.OKHTTP,
Dependency.BYTEBUDDY,
Dependency.EVENT
);
}

View File

@ -1,63 +0,0 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package me.lucko.luckperms.common.util;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public final class PrivateMethodHandles {
private PrivateMethodHandles() {}
private static final Constructor<MethodHandles.Lookup> LOOKUP_CONSTRUCTOR;
static {
try {
LOOKUP_CONSTRUCTOR = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
LOOKUP_CONSTRUCTOR.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new ExceptionInInitializerError(e);
}
}
// MethodHandles.Lookup.PUBLIC | MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED | MethodHandles.Lookup.PACKAGE
private static final int TRUSTED_FLAG = -1;
/**
* Returns a {@link MethodHandles.Lookup lookup object} with full capabilities to emulate all
* supported bytecode behaviors, including private access, on a target class.
*
* @param targetClass the target class
* @return a lookup object for the target class, with private access
*/
public static MethodHandles.Lookup privateLookup(Class<?> targetClass) {
try {
return LOOKUP_CONSTRUCTOR.newInstance(targetClass, TRUSTED_FLAG);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -33,6 +33,7 @@ shadowJar {
relocate 'com.github.benmanes.caffeine', 'me.lucko.luckperms.lib.caffeine'
relocate 'okio', 'me.lucko.luckperms.lib.okio'
relocate 'okhttp3', 'me.lucko.luckperms.lib.okhttp3'
relocate 'net.bytebuddy', 'me.lucko.luckperms.lib.bytebuddy'
relocate 'me.lucko.commodore', 'me.lucko.luckperms.lib.commodore'
relocate 'org.mariadb.jdbc', 'me.lucko.luckperms.lib.mariadb'
relocate 'com.mysql', 'me.lucko.luckperms.lib.mysql'

View File

@ -46,6 +46,7 @@ shadowJar {
relocate 'com.github.benmanes.caffeine', 'me.lucko.luckperms.lib.caffeine'
relocate 'okio', 'me.lucko.luckperms.lib.okio'
relocate 'okhttp3', 'me.lucko.luckperms.lib.okhttp3'
relocate 'net.bytebuddy', 'me.lucko.luckperms.lib.bytebuddy'
relocate 'me.lucko.commodore', 'me.lucko.luckperms.lib.commodore'
relocate 'org.mariadb.jdbc', 'me.lucko.luckperms.lib.mariadb'
relocate 'com.mysql', 'me.lucko.luckperms.lib.mysql'

View File

@ -32,6 +32,7 @@ shadowJar {
relocate 'com.github.benmanes.caffeine', 'me.lucko.luckperms.lib.caffeine'
relocate 'okio', 'me.lucko.luckperms.lib.okio'
relocate 'okhttp3', 'me.lucko.luckperms.lib.okhttp3'
relocate 'net.bytebuddy', 'me.lucko.luckperms.lib.bytebuddy'
relocate 'me.lucko.commodore', 'me.lucko.luckperms.lib.commodore'
relocate 'org.mariadb.jdbc', 'me.lucko.luckperms.lib.mariadb'
relocate 'com.mysql', 'me.lucko.luckperms.lib.mysql'