Improve event system and reflection

This commit is contained in:
JeremyStar™ 2024-07-21 21:10:49 +02:00
parent 350b18219b
commit 2469e124bd
Signed by: JeremyStarTM
GPG key ID: E366BAEF67E4704D
10 changed files with 188 additions and 84 deletions

View file

@ -22,7 +22,16 @@ package de.staropensource.sosengine.base.classes.helpers;
import de.staropensource.sosengine.base.EngineConfiguration; import de.staropensource.sosengine.base.EngineConfiguration;
import de.staropensource.sosengine.base.annotations.EventListener; import de.staropensource.sosengine.base.annotations.EventListener;
import de.staropensource.sosengine.base.classes.Event; import de.staropensource.sosengine.base.classes.Event;
import de.staropensource.sosengine.base.exceptions.UnexpectedThrowableException;
import de.staropensource.sosengine.base.exceptions.reflection.InstanceMethodFromStaticContextException;
import de.staropensource.sosengine.base.exceptions.reflection.InvalidMethodSignature;
import de.staropensource.sosengine.base.exceptions.reflection.NoAccessException;
import de.staropensource.sosengine.base.exceptions.reflection.StaticInitializerException;
import de.staropensource.sosengine.base.logging.Logger; import de.staropensource.sosengine.base.logging.Logger;
import de.staropensource.sosengine.base.logging.LoggerInstance;
import de.staropensource.sosengine.base.reflection.Reflect;
import de.staropensource.sosengine.base.reflection.ReflectionMethod;
import de.staropensource.sosengine.base.types.CodePart;
import de.staropensource.sosengine.base.types.logging.LogIssuer; import de.staropensource.sosengine.base.types.logging.LogIssuer;
import de.staropensource.sosengine.base.utility.ListFormatter; import de.staropensource.sosengine.base.utility.ListFormatter;
import lombok.Getter; import lombok.Getter;
@ -53,7 +62,7 @@ public class EventHelper {
* *
* @since v1-alpha0 * @since v1-alpha0
*/ */
private static final HashMap<@NotNull Class<? extends Event>, LinkedList<@NotNull Method>> cachedEventListeners = new HashMap<>(); private static final HashMap<@NotNull Class<? extends Event>, LinkedList<@NotNull ReflectionMethod>> cachedEventListeners = new HashMap<>();
/** /**
* Constructs this class. * Constructs this class.
@ -85,8 +94,8 @@ public class EventHelper {
* @since v1-alpha0 * @since v1-alpha0
*/ */
@NotNull @NotNull
public static LinkedList<Method> getAnnotatedMethods(@NotNull Class<? extends Event> clazz, boolean forceScanning) { public static LinkedList<ReflectionMethod> getAnnotatedMethods(@NotNull Class<? extends Event> clazz, boolean forceScanning) {
LinkedList<Method> methods = new LinkedList<>(); LinkedList<ReflectionMethod> methods = new LinkedList<>();
if (forceScanning || !cachedEventListeners.containsKey(clazz)) { if (forceScanning || !cachedEventListeners.containsKey(clazz)) {
// Scan entire classpath through Reflections library // Scan entire classpath through Reflections library
@ -103,10 +112,10 @@ public class EventHelper {
// Sort event listeners not listening for this event out // Sort event listeners not listening for this event out
for (Method method : annotatedMethods) for (Method method : annotatedMethods)
if (method.getAnnotation(EventListener.class).event() == clazz) if (method.getAnnotation(EventListener.class).event() == clazz)
methods.add(method); methods.add(Reflect.reflectOn(method));
// Sort 'methods' linked list after event priority // Sort 'methods' linked list after event priority
methods.sort(Comparator.comparing(method0 -> method0.getAnnotation(EventListener.class).priority())); methods.sort(Comparator.comparing(method -> method.getAnnotation(EventListener.class).priority()));
} else } else
// 'forcedScanning' is false and matching event listeners are cached // 'forcedScanning' is false and matching event listeners are cached
methods = cachedEventListeners.get(clazz); methods = cachedEventListeners.get(clazz);
@ -122,25 +131,36 @@ public class EventHelper {
* @since v1-alpha0 * @since v1-alpha0
*/ */
@NotNull @NotNull
public static LinkedList<Method> getAnnotatedMethods(@NotNull Class<? extends Event> clazz) { public static LinkedList<ReflectionMethod> getAnnotatedMethods(@NotNull Class<? extends Event> clazz) {
return getAnnotatedMethods(clazz, false); return getAnnotatedMethods(clazz, false);
} }
/** /**
* Invokes all matching event listeners without any arguments. * Invokes all matching event listeners without any arguments.
* *
* @param clazz event class * @param event event class
* @since v1-alpha0 * @since v1-alpha0
*/ */
public static void invokeAnnotatedMethods(@NotNull Class<? extends Event> clazz) { public static void invokeAnnotatedMethods(@NotNull Class<? extends Event> event, Object... arguments) {
Runnable eventCode = () -> { Runnable eventCode = () -> {
logCall(clazz); LoggerInstance logger = new LoggerInstance(new LogIssuer(EventHelper.class, event.getName(), CodePart.ENGINE));
logCall(event);
for (Method method : getAnnotatedMethods(clazz)) { for (ReflectionMethod method : getAnnotatedMethods(event)) {
try { try {
method.invoke(null); method.invoke(arguments);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | } catch (UnexpectedThrowableException exception) {
NullPointerException | ExceptionInInitializerError ignored) { logger.crash("Event listener method " + method.getName() + " could not be called as an error occured during reflection", exception, true);
} catch (NoAccessException exception) {
logger.warn("Event listener method " + method.getName() + " could not be called as the method could not be accessed");
} catch (InvalidMethodSignature exception) {
logger.warn("Event listener method " + method.getName() + " has an invalid method signature");
} catch (InvocationTargetException exception) {
logger.crash("Event listener method " + method.getName() + " threw a throwable", exception.getTargetException(), true);
} catch (InstanceMethodFromStaticContextException exception) {
logger.warn("Event listener method " + method.getName() + " is not static. Event listener methods must be static for them to be called.");
} catch (StaticInitializerException exception) {
logger.crash("Event listener method " + method.getName() + " could not be called as the static initializer failed", exception.getThrowable(), true);
} }
} }
}; };
@ -163,7 +183,7 @@ public class EventHelper {
for (Class<? extends Event> event : cachedEventListeners.keySet()) for (Class<? extends Event> event : cachedEventListeners.keySet())
precomputeEventListeners(event); precomputeEventListeners(event);
else { else {
LinkedList<@NotNull Method> annotatedMethods = getAnnotatedMethods(clazz); LinkedList<@NotNull ReflectionMethod> annotatedMethods = getAnnotatedMethods(clazz);
if (cachedEventListeners.containsKey(clazz)) if (cachedEventListeners.containsKey(clazz))
cachedEventListeners.replace(clazz, annotatedMethods); cachedEventListeners.replace(clazz, annotatedMethods);

View file

@ -19,16 +19,12 @@
package de.staropensource.sosengine.base.events; package de.staropensource.sosengine.base.events;
import de.staropensource.sosengine.base.EngineConfiguration;
import de.staropensource.sosengine.base.classes.Event; import de.staropensource.sosengine.base.classes.Event;
import de.staropensource.sosengine.base.classes.helpers.EventHelper; import de.staropensource.sosengine.base.classes.helpers.EventHelper;
import de.staropensource.sosengine.base.types.logging.LogIssuer; import de.staropensource.sosengine.base.types.logging.LogIssuer;
import de.staropensource.sosengine.base.types.logging.LogLevel; import de.staropensource.sosengine.base.types.logging.LogLevel;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/** /**
* Called before a new log message is printed. * Called before a new log message is printed.
* *
@ -59,20 +55,6 @@ public final class LogEvent implements Event {
* @since v1-alpha0 * @since v1-alpha0
*/ */
public void callEvent(@NotNull LogLevel level, @NotNull LogIssuer logIssuer, @NotNull String message) { public void callEvent(@NotNull LogLevel level, @NotNull LogIssuer logIssuer, @NotNull String message) {
Runnable eventCode = Thread.ofVirtual().start(() -> { EventHelper.invokeAnnotatedMethods(getClass(), level, logIssuer, message);
// Uncommenting this would be a great way to cause a StackOverflowException!
//EventHelper.logCall(getClass(), level, logIssuer, message);
for (Method method : EventHelper.getAnnotatedMethods(getClass())) {
try {
method.invoke(null, level, logIssuer, message);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NullPointerException | ExceptionInInitializerError ignored) {}
}
});
if (EngineConfiguration.getInstance().isOptimizeEvents())
Thread.ofVirtual().start(eventCode);
else
eventCode.run();
} }
} }

View file

@ -19,15 +19,11 @@
package de.staropensource.sosengine.base.events; package de.staropensource.sosengine.base.events;
import de.staropensource.sosengine.base.EngineConfiguration;
import de.staropensource.sosengine.base.classes.Event; import de.staropensource.sosengine.base.classes.Event;
import de.staropensource.sosengine.base.classes.helpers.EventHelper; import de.staropensource.sosengine.base.classes.helpers.EventHelper;
import de.staropensource.sosengine.base.utility.Miscellaneous; import de.staropensource.sosengine.base.utility.Miscellaneous;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/** /**
* Called when an exception is caught. * Called when an exception is caught.
* *
@ -57,19 +53,6 @@ public final class ThrowableCatchEvent implements Event {
* @since v1-alpha0 * @since v1-alpha0
*/ */
public void callEvent(@NotNull Throwable throwable, @NotNull String identifier) { public void callEvent(@NotNull Throwable throwable, @NotNull String identifier) {
Runnable eventCode = Thread.ofVirtual().start(() -> { EventHelper.invokeAnnotatedMethods(getClass(), throwable, identifier);
EventHelper.logCall(getClass(), throwable, identifier);
for (Method method : EventHelper.getAnnotatedMethods(getClass())) {
try {
method.invoke(null, throwable, identifier);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NullPointerException | ExceptionInInitializerError ignored) {}
}
});
if (EngineConfiguration.getInstance().isOptimizeEvents())
Thread.ofVirtual().start(eventCode);
else
eventCode.run();
} }
} }

View file

@ -0,0 +1,39 @@
/*
* STAROPENSOURCE ENGINE SOURCE FILE
* Copyright (c) 2024 The StarOpenSource Engine Contributors
* Licensed under the GNU Affero General Public License v3
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.staropensource.sosengine.base.exceptions.reflection;
import org.jetbrains.annotations.NotNull;
/**
* Thrown when trying to call a non-static method from a static context.
*
* @since v1-alpha2
*/
public class InstanceMethodFromStaticContextException extends Exception {
/**
* Constructs this exception.
*
* @param methodName name of the method
* @since v1-alpha2
*/
public InstanceMethodFromStaticContextException(@NotNull String methodName) {
super("Method " + methodName + " could not be called as the method is non-static and was called from a static context");
}
}

View file

@ -0,0 +1,28 @@
/*
* STAROPENSOURCE ENGINE SOURCE FILE
* Copyright (c) 2024 The StarOpenSource Engine Contributors
* Licensed under the GNU Affero General Public License v3
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.staropensource.sosengine.base.exceptions.reflection;
import org.jetbrains.annotations.NotNull;
public class InvalidMethodSignature extends Exception {
public InvalidMethodSignature(@NotNull String methodName) {
super("Method " + methodName + " has a different method signature");
}
}

View file

@ -0,0 +1,56 @@
/*
* STAROPENSOURCE ENGINE SOURCE FILE
* Copyright (c) 2024 The StarOpenSource Engine Contributors
* Licensed under the GNU Affero General Public License v3
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.staropensource.sosengine.base.exceptions.reflection;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
/**
* Thrown when an exception occurs
* while executing a static initializer.
*
* @since v1-alpha2
*/
@Getter
@SuppressWarnings({ "unused", "JavadocDeclaration", "JavadocBlankLines" })
public class StaticInitializerException extends Exception {
/**
* Contains the throwable supplied to the constructor.
*
* @since v1-alpha2
*
* -- GETTER --
* Returns the throwable supplied to the constructor.
*
* @return throwable thrown by the static initializer
* @since v1-alpha2
*/
private final Throwable throwable;
/**
* Constructs this exception.
*
* @param throwable throwable thrown by the static initializer
* @since v1-alpha2
*/
public StaticInitializerException(@NotNull Throwable throwable) {
this.throwable = throwable;
}
}

View file

@ -23,6 +23,7 @@ import de.staropensource.sosengine.base.Engine;
import de.staropensource.sosengine.base.EngineConfiguration; import de.staropensource.sosengine.base.EngineConfiguration;
import de.staropensource.sosengine.base.classes.LoggerImpl; import de.staropensource.sosengine.base.classes.LoggerImpl;
import de.staropensource.sosengine.base.classes.Placeholder; import de.staropensource.sosengine.base.classes.Placeholder;
import de.staropensource.sosengine.base.classes.helpers.EventHelper;
import de.staropensource.sosengine.base.events.LogEvent; import de.staropensource.sosengine.base.events.LogEvent;
import de.staropensource.sosengine.base.internal.placeholders.logger.*; import de.staropensource.sosengine.base.internal.placeholders.logger.*;
import de.staropensource.sosengine.base.internal.types.QueuedLogMessage; import de.staropensource.sosengine.base.internal.types.QueuedLogMessage;
@ -214,7 +215,7 @@ public final class Logger {
base = loggerImplementation.postPlaceholder(level, issuer, base); base = loggerImplementation.postPlaceholder(level, issuer, base);
// Call event // Call event
if (!issuer.getClazz().getName().equals("de.staropensource.sosengine.slf4j_compat.CompatibilityLogger")) if (!(issuer.getClazz().getName().equals("de.staropensource.sosengine.slf4j_compat.CompatibilityLogger") || issuer.getClazz().equals(EventHelper.class)))
new LogEvent().callEvent(level, issuer, message); new LogEvent().callEvent(level, issuer, message);
// Print log message by invoking LoggerImpl#print // Print log message by invoking LoggerImpl#print

View file

@ -21,7 +21,10 @@ package de.staropensource.sosengine.base.reflection;
import de.staropensource.sosengine.base.exceptions.UnexpectedCheckEndException; import de.staropensource.sosengine.base.exceptions.UnexpectedCheckEndException;
import de.staropensource.sosengine.base.exceptions.UnexpectedThrowableException; import de.staropensource.sosengine.base.exceptions.UnexpectedThrowableException;
import de.staropensource.sosengine.base.exceptions.reflection.InstanceMethodFromStaticContextException;
import de.staropensource.sosengine.base.exceptions.reflection.InvalidMethodSignature;
import de.staropensource.sosengine.base.exceptions.reflection.NoAccessException; import de.staropensource.sosengine.base.exceptions.reflection.NoAccessException;
import de.staropensource.sosengine.base.exceptions.reflection.StaticInitializerException;
import de.staropensource.sosengine.base.internal.reflection.ReflectionAccessWidener; import de.staropensource.sosengine.base.internal.reflection.ReflectionAccessWidener;
import de.staropensource.sosengine.base.types.reflection.VisibilityModifier; import de.staropensource.sosengine.base.types.reflection.VisibilityModifier;
import lombok.Getter; import lombok.Getter;
@ -310,11 +313,12 @@ public final class ReflectionMethod {
/** /**
* Returns the specified annotation or {@code null} if not found. * Returns the specified annotation or {@code null} if not found.
* *
* @param annotation class * @param <T> annotation class
* @param annotation annotation class
* @return annotation or {@code null} * @return annotation or {@code null}
* @since v1-alpha2 * @since v1-alpha2
*/ */
public Annotation getAnnotation(@NotNull Class<Annotation> annotation) { public <T extends Annotation> T getAnnotation(@NotNull Class<T> annotation) {
return method.getAnnotation(annotation); return method.getAnnotation(annotation);
} }
@ -345,13 +349,16 @@ public final class ReflectionMethod {
* Invokes the method. * Invokes the method.
* *
* @return method return value * @return method return value
* @throws NoAccessException if access to the method has been denied
* @throws InvocationTargetException covers exceptions thrown by the method
* @throws UnexpectedThrowableException if the {@code modifiers} field could not be found * @throws UnexpectedThrowableException if the {@code modifiers} field could not be found
* @throws NoAccessException if access to the method has been denied
* @throws InvalidMethodSignature if the method signature is incorrect
* @throws InvocationTargetException covers exceptions thrown by the method
* @throws InstanceMethodFromStaticContextException when the target method is non-static and called from a static context
* @throws StaticInitializerException when an the static initializer fails
* @since v1-alpha2 * @since v1-alpha2
*/ */
@Nullable @Nullable
public Object invoke() throws NoAccessException, InvocationTargetException, UnexpectedThrowableException { public Object invoke() throws UnexpectedThrowableException, NoAccessException, InvalidMethodSignature, InvocationTargetException, InstanceMethodFromStaticContextException, StaticInitializerException {
return invoke(new Object[0]); return invoke(new Object[0]);
} }
@ -359,13 +366,16 @@ public final class ReflectionMethod {
* Invokes the method. * Invokes the method.
* *
* @return method return value * @return method return value
* @throws NoAccessException if access to the method has been denied
* @throws InvocationTargetException covers exceptions thrown by the method
* @throws UnexpectedThrowableException if the {@code modifiers} field could not be found * @throws UnexpectedThrowableException if the {@code modifiers} field could not be found
* @throws NoAccessException if access to the method has been denied
* @throws InvalidMethodSignature if the method signature is incorrect
* @throws InvocationTargetException covers exceptions thrown by the method
* @throws InstanceMethodFromStaticContextException when the target method is non-static and called from a static context
* @throws StaticInitializerException when an the static initializer fails
* @since v1-alpha2 * @since v1-alpha2
*/ */
@Nullable @Nullable
public Object invoke(Object... args) throws NoAccessException, InvocationTargetException, UnexpectedThrowableException { public Object invoke(Object... args) throws UnexpectedThrowableException, NoAccessException, InvalidMethodSignature, InvocationTargetException, InstanceMethodFromStaticContextException, StaticInitializerException {
Object returnValue; Object returnValue;
// Allow access to method // Allow access to method
@ -377,6 +387,12 @@ public final class ReflectionMethod {
} catch (IllegalAccessException exception) { } catch (IllegalAccessException exception) {
//ReflectionAccessWidener.lockModifications(this, updatedModifiers); // Lock method before throwing exception //ReflectionAccessWidener.lockModifications(this, updatedModifiers); // Lock method before throwing exception
throw new NoAccessException("method", getName()); throw new NoAccessException("method", getName());
} catch (IllegalArgumentException exception) {
throw new InvalidMethodSignature(getName());
} catch (NullPointerException exception) {
throw new InstanceMethodFromStaticContextException(getName());
} catch (ExceptionInInitializerError exception) {
throw new StaticInitializerException(exception.getCause());
} }
// Return return value from method // Return return value from method

View file

@ -21,14 +21,8 @@ package de.staropensource.sosengine.graphics.events;
import de.staropensource.sosengine.base.classes.Event; import de.staropensource.sosengine.base.classes.Event;
import de.staropensource.sosengine.base.classes.helpers.EventHelper; import de.staropensource.sosengine.base.classes.helpers.EventHelper;
import de.staropensource.sosengine.base.events.LogEvent;
import de.staropensource.sosengine.base.types.logging.LogIssuer;
import de.staropensource.sosengine.base.types.logging.LogLevel;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/** /**
* Called when a graphics error occurs. * Called when a graphics error occurs.
* *
@ -44,7 +38,7 @@ public class GraphicsApiErrorEvent implements Event {
/** /**
* {@inheritDoc} * {@inheritDoc}
* @deprecated use the {@code callEvent} method with arguments * @deprecated use the {@code callEvent} method with arguments
* @see LogEvent#callEvent(LogLevel, LogIssuer, String) * @see #callEvent(String)
*/ */
@Deprecated @Deprecated
@Override @Override
@ -57,12 +51,6 @@ public class GraphicsApiErrorEvent implements Event {
* @since v1-alpha0 * @since v1-alpha0
*/ */
public void callEvent(@NotNull String error) { public void callEvent(@NotNull String error) {
EventHelper.logCall(getClass()); EventHelper.invokeAnnotatedMethods(getClass(), error);
for (Method method : EventHelper.getAnnotatedMethods(getClass())) {
try {
method.invoke(null, error);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NullPointerException | ExceptionInInitializerError ignored) {}
}
} }
} }

View file

@ -26,9 +26,6 @@ import de.staropensource.sosengine.base.types.logging.LogIssuer;
import de.staropensource.sosengine.base.types.logging.LogLevel; import de.staropensource.sosengine.base.types.logging.LogLevel;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/** /**
* Called when a graphics error occurs. * Called when a graphics error occurs.
* *
@ -57,12 +54,6 @@ public class GraphicsErrorEvent implements Event {
* @since v1-alpha0 * @since v1-alpha0
*/ */
public void callEvent(@NotNull String error) { public void callEvent(@NotNull String error) {
EventHelper.logCall(getClass()); EventHelper.invokeAnnotatedMethods(getClass(), error);
for (Method method : EventHelper.getAnnotatedMethods(getClass())) {
try {
method.invoke(null, error);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NullPointerException | ExceptionInInitializerError ignored) {}
}
} }
} }