diff --git a/annotation/README.md b/annotation/README.md index b5517ab..ceba1ef 100644 --- a/annotation/README.md +++ b/annotation/README.md @@ -50,27 +50,45 @@ The `SimplixSerializerSerializableAutoRegister` annotation is used to mark the c annotation. The `CustomAnnotationProcessor` interface is used to determine the processing method. This is very simple, right? But now that the definition is complete, how does this all work? +What if we only want to process a few specific annotations? ```java - @FairyLaunch -@InjectableComponent -public class AnnotationLauncher extends Plugin { +public class Launcher extends Plugin { @Autowired private AnnotationProcessingServiceInterface annotationProcessingService; @Override public void onPluginEnable() { - String basePackage = this.getClass().getPackageName(); - annotationProcessingService.processAnnotations(basePackage, false, this.getClassLoader()); + List basePackages = List.of( + // Packets expected to be processed + "org.example", + + /* + * The package where the Processor of the annotation to be processed is located + * e.g. "me.qwqdev.library.configuration.serialize.annotation.SimplixSerializerSerializableAutoRegister" + * so the package name is "me.qwqdev.library.configuration.serialize.annotation" + */ + "me.qwqdev.library.configuration.serialize.annotation" + ); + + annotationProcessingService.processAnnotations( + basePackages, + + // should Processors dependency injection by Fairy IoC + false, + + /* + * All ClassLoaders used for basePackages scanning + * Here, we need to pass in not only the ClassLoader of the current class, + * but also the ClassLoader of the Launcher when we need to use the Processor that comes with legacy-lands-library + */ + this.getClassLoader(), + ConfigurationLauncher.class.getClassLoader() // configuration moduel's class loader + // or CommonsLauncher.class.getClassLoader(), its commons module's class loader + ); } } ``` -We only need to get `AnnotationProcessingService` through dependency injection and simply call the method~ -Let's focus on `annotationProcessingService.processAnnotations(basePackage, false, this.getClassLoader())`. - -The first parameter is the package we need to scan. -The second is whether the processed class should be injected into the singleton mode by the Fairy framework. If it is -false, it will be created through parameterless reflection. -The third is the classloader that needs to be scanned. \ No newline at end of file +Let's look at this, we just need to get the `AnnotationProcessingService` through dependency injection and then call `processAnnotations`. \ No newline at end of file diff --git a/annotation/src/main/java/me/qwqdev/library/annotation/service/AnnotationProcessingService.java b/annotation/src/main/java/me/qwqdev/library/annotation/service/AnnotationProcessingService.java index 54cfa5c..b05126b 100644 --- a/annotation/src/main/java/me/qwqdev/library/annotation/service/AnnotationProcessingService.java +++ b/annotation/src/main/java/me/qwqdev/library/annotation/service/AnnotationProcessingService.java @@ -1,5 +1,6 @@ package me.qwqdev.library.annotation.service; +import com.google.common.collect.Sets; import io.fairyproject.container.Containers; import io.fairyproject.container.InjectableComponent; import io.fairyproject.log.Log; @@ -10,6 +11,7 @@ import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.util.Collection; +import java.util.List; import java.util.Set; /** @@ -43,6 +45,20 @@ public void processAnnotations(String basePackage, boolean fromFairyIoCSingleton processAnnotations(ClasspathHelper.forPackage(basePackage, classLoader), fromFairyIoCSingleton); } + /** + * {@inheritDoc} + * + * @param basePackages {@inheritDoc} + * @param fromFairyIoCSingleton {@inheritDoc} + * @param classLoader {@inheritDoc} + */ + @Override + public void processAnnotations(List basePackages, boolean fromFairyIoCSingleton, ClassLoader... classLoader) { + Collection urls = Sets.newHashSet(); + basePackages.forEach(basePackage -> urls.addAll(ClasspathHelper.forPackage(basePackage, classLoader))); + processAnnotations(urls, fromFairyIoCSingleton); + } + /** * {@inheritDoc} * diff --git a/annotation/src/main/java/me/qwqdev/library/annotation/service/AnnotationProcessingServiceInterface.java b/annotation/src/main/java/me/qwqdev/library/annotation/service/AnnotationProcessingServiceInterface.java index fcb5b56..bc13b98 100644 --- a/annotation/src/main/java/me/qwqdev/library/annotation/service/AnnotationProcessingServiceInterface.java +++ b/annotation/src/main/java/me/qwqdev/library/annotation/service/AnnotationProcessingServiceInterface.java @@ -2,6 +2,7 @@ import java.net.URL; import java.util.Collection; +import java.util.List; /** * Service interface for processing annotations using custom annotation processors. @@ -28,6 +29,21 @@ public interface AnnotationProcessingServiceInterface { */ void processAnnotations(String basePackage, boolean fromFairyIoCSingleton, ClassLoader... classLoader); + /** + * Processes annotations within the specified list of base packages using the default annotation processors. + * + *

This method scans the specified base packages and their sub-packages for classes annotated with specific + * annotations, and then processes them using default annotation processors. The method supports scanning + * multiple base packages and allows for optional class loaders to be specified for classpath scanning. + * + * @param basePackages the list of base packages to scan for annotated classes + * @param fromFairyIoCSingleton whether handlerClass should be injected into a singleton by the Fairy framework + * If false, it will be created by reflection without parameters, + * but it still supports setter injection with Fairy {@link io.fairyproject.container.Autowired} annotation + * @param classLoader optional class loaders to use for classpath scanning; if not provided, the default class loader is used + */ + void processAnnotations(List basePackages, boolean fromFairyIoCSingleton, ClassLoader... classLoader); + /** * Processes annotations within the specified collection of URLs using default annotation processors. * diff --git a/commons/README.md b/commons/README.md index 13d3227..e9a9fdc 100644 --- a/commons/README.md +++ b/commons/README.md @@ -53,4 +53,41 @@ public class Test { InjectorFactory.createVarHandleReflectionInjector().inject(Test.class); } } -``` \ No newline at end of file +``` + +### [Task](src/main/java/me/qwqdev/library/commons/task) + +The [TaskInterface](src/main/java/me/qwqdev/library/commons/task/TaskInterface.java) +simplifies task scheduling by providing convenience methods with consistent naming and argument order with the Fairy Framework (MCScheduler)[https://docs.fairyproject.io/core/minecraft/scheduler]. + +```java +public class Example { + public static void main(String[] args) { + TaskInterface taskInterface = new TaskInterface() { + @Override + public ScheduledTask start() { + // This is a simple example of a task that prints "Hello, world!" every second. + return scheduleAtFixedRate(() -> System.out.println("Hello, world!"), 0, 1000); + } + }; + + // start the task + taskInterface.start(); + } +} +``` + +It also provides [TaskAutoStartAnnotation](src/main/java/me/qwqdev/library/commons/task/annotation/TaskAutoStartAnnotation.java) to handle some tasks that need to be automatically started at a specific time. When there are many tasks to start, annotation automation will help us avoid manually managing the creation and calling of these instances, thereby simplifying the code. + +```java +@TaskAutoStartAnnotation(isFromFairyIoC = false) +public class Example implements TaskInterface { + @Override + public ScheduledTask start() { + // This is a simple example of a task that prints "Hello, world!" every second. + return scheduleAtFixedRate(() -> System.out.println("Hello, world!"), 0, 1000); + } +} +``` + +As for how to make annotation processors work on your own plugins, please see the [annotation](../annotation/README.md) module. \ No newline at end of file diff --git a/commons/src/main/java/me/qwqdev/library/commons/injector/VarHandleReflectionInjector.java b/commons/src/main/java/me/qwqdev/library/commons/injector/VarHandleReflectionInjector.java index 21d11fe..5878c15 100644 --- a/commons/src/main/java/me/qwqdev/library/commons/injector/VarHandleReflectionInjector.java +++ b/commons/src/main/java/me/qwqdev/library/commons/injector/VarHandleReflectionInjector.java @@ -9,7 +9,7 @@ import java.lang.reflect.Method; /** - * Utility class for injecting {@link VarHandle} instances into static fields annotated with {@link VarHandleAutoInjection}. + * Injector class for injecting {@link VarHandle} instances into static fields annotated with {@link VarHandleAutoInjection}. * This class automatically injects {@link VarHandle} instances into the static fields of a class based on the annotations * present on those fields. * @@ -46,8 +46,8 @@ public class VarHandleReflectionInjector implements StaticInjectorInterface { /** * {@inheritDoc} - *

- * Injects {@link VarHandle} instances into static fields of the given class that are annotated with + * + *

Injects {@link VarHandle} instances into static fields of the given class that are annotated with * {@link VarHandleAutoInjection}. This method uses reflection to find the appropriate {@link VarHandle} * for each annotated field and assigns it to the field. * diff --git a/commons/src/main/java/me/qwqdev/library/commons/task/TaskInterface.java b/commons/src/main/java/me/qwqdev/library/commons/task/TaskInterface.java new file mode 100644 index 0000000..6188cc9 --- /dev/null +++ b/commons/src/main/java/me/qwqdev/library/commons/task/TaskInterface.java @@ -0,0 +1,264 @@ +package me.qwqdev.library.commons.task; + +import io.fairyproject.mc.scheduler.MCScheduler; +import io.fairyproject.mc.scheduler.MCSchedulers; +import io.fairyproject.scheduler.ScheduledTask; +import io.fairyproject.scheduler.repeat.RepeatPredicate; +import io.fairyproject.scheduler.response.TaskResponse; + +import java.time.Duration; +import java.util.concurrent.Callable; + +/** + * The {@code TaskInterface} provides a high-level abstraction for scheduling and running tasks + * using the {@link MCScheduler} (usually provided by {@link MCSchedulers}). + * + *

This interface simplifies task scheduling by providing convenient methods with consistent + * naming and parameter orders. + * + *

By default, the scheduler returned from {@link #getMCScheduler()} is the asynchronous scheduler + * ({@link MCSchedulers#getAsyncScheduler()}); however, implementations can override this method + * to return other schedulers if needed. + * + *

This interface is intended for classes that encapsulate their own internal scheduling logic. + * For instance, a class implementing this interface might define how and when tasks start by + * overriding the {@link #start()} method. + * + * @author qwq-dev + * @see MCSchedulers + * @see MCScheduler + * @see io.fairyproject.scheduler.Scheduler + * @since 2024-12-14 12:30 + */ +public interface TaskInterface { + /** + * Starts the task. Implementations should define the logic of the task within this method. + * + *

By default, overriding this method will schedule the task asynchronously, depending on the + * {@link MCScheduler} provided by {@link #getMCScheduler()}. This method could, for example, + * schedule periodic tasks or a single one-time task. + * + * @return a {@link ScheduledTask} representing the started task + */ + ScheduledTask start(); + + /** + * Provides the {@link MCScheduler} instance used for scheduling tasks. + * + *

By default, this returns {@code MCSchedulers.getAsyncScheduler()}, but implementations can + * override it to return a different scheduler (e.g., synchronous, or a custom one). + * + * @return the {@link MCScheduler} scheduler instance + */ + default MCScheduler getMCScheduler() { + return MCSchedulers.getAsyncScheduler(); + } + + /** + * Schedule a one-time {@link Runnable} task with a specified delay in ticks. + * + * @param task the task to be executed + * @param delayTicks the delay in ticks before the task is executed + * @return a {@link ScheduledTask} representing the scheduled task + */ + default ScheduledTask schedule(Runnable task, long delayTicks) { + return getMCScheduler().schedule(task, delayTicks); + } + + /** + * Schedule a periodic {@link Runnable} task with a fixed delay and interval in ticks. + * + * @param task the task to be executed + * @param delayTicks the initial delay in ticks before the first execution + * @param intervalTicks the interval in ticks between consecutive executions + * @return a {@link ScheduledTask} representing the scheduled task + */ + default ScheduledTask scheduleAtFixedRate(Runnable task, long delayTicks, long intervalTicks) { + return getMCScheduler().scheduleAtFixedRate(task, delayTicks, intervalTicks); + } + + /** + * Schedule a periodic {@link Runnable} task with a fixed delay and interval in ticks, and a custom + * {@link RepeatPredicate}. + * + *

The {@link RepeatPredicate} can be used to dynamically determine whether the task should continue + * running or be terminated based on the logic you define. + * + * @param task the task to be executed + * @param delayTicks the initial delay in ticks before the first execution + * @param intervalTicks the interval in ticks between consecutive executions + * @param predicate the {@link RepeatPredicate} to control repetition + * @return a {@link ScheduledTask} representing the scheduled task + */ + default ScheduledTask scheduleAtFixedRate(Runnable task, long delayTicks, long intervalTicks, RepeatPredicate predicate) { + return getMCScheduler().scheduleAtFixedRate(task, delayTicks, intervalTicks, predicate); + } + + /** + * Schedule a one-time {@link Callable} task with a specified delay in ticks. + * + *

The returned {@link ScheduledTask} will also carry the result of the callable upon completion. + * + * @param task the callable task to be executed + * @param delayTicks the delay in ticks before the task is executed + * @param the return type of the callable + * @return a {@link ScheduledTask} representing the scheduled task + */ + default ScheduledTask schedule(Callable task, long delayTicks) { + return getMCScheduler().schedule(task, delayTicks); + } + + /** + * Schedule a periodic {@link Callable} task with a fixed delay and interval in ticks. + * + *

The callable is expected to return a {@link TaskResponse} which defines whether the task + * should continue or terminate. + * + * @param task the callable task to be executed + * @param delayTicks the initial delay in ticks before the first execution + * @param intervalTicks the interval in ticks between consecutive executions + * @param the return type wrapped by {@link TaskResponse} + * @return a {@link ScheduledTask} representing the scheduled task + */ + default ScheduledTask scheduleAtFixedRate(Callable> task, long delayTicks, long intervalTicks) { + return getMCScheduler().scheduleAtFixedRate(task, delayTicks, intervalTicks); + } + + /** + * Schedule a periodic {@link Callable} task with a fixed delay and interval in ticks, and a custom + * {@link RepeatPredicate}. + * + *

The {@link RepeatPredicate} will control the repetition logic using the callable's return value. + * + * @param task the callable task to be executed + * @param delayTicks the initial delay in ticks before the first execution + * @param intervalTicks the interval in ticks between consecutive executions + * @param predicate the {@link RepeatPredicate} to control repetition + * @param the return type wrapped by {@link TaskResponse} + * @return a {@link ScheduledTask} representing the scheduled task + */ + default ScheduledTask scheduleAtFixedRate(Callable> task, long delayTicks, long intervalTicks, RepeatPredicate predicate) { + return getMCScheduler().scheduleAtFixedRate(task, delayTicks, intervalTicks, predicate); + } + + /** + * Schedule a one-time {@link Runnable} task without any initial delay (i.e., run as soon as possible). + * + * @param task the task to be executed + * @return a {@link ScheduledTask} representing the scheduled task + */ + default ScheduledTask schedule(Runnable task) { + return getMCScheduler().schedule(task); + } + + /** + * Schedule a one-time {@link Runnable} task with a specified {@link Duration} delay. + * + * @param task the task to be executed + * @param delay the duration before the task is executed + * @return a {@link ScheduledTask} representing the scheduled task + */ + default ScheduledTask schedule(Runnable task, Duration delay) { + return getMCScheduler().schedule(task, delay); + } + + /** + * Schedule a periodic {@link Runnable} task using {@link Duration} based initial delay and interval. + * + * @param task the task to be executed + * @param delay the initial delay before the first execution + * @param interval the interval between consecutive executions + * @return a {@link ScheduledTask} representing the scheduled task + */ + default ScheduledTask scheduleAtFixedRate(Runnable task, Duration delay, Duration interval) { + return getMCScheduler().scheduleAtFixedRate(task, delay, interval); + } + + /** + * Schedule a periodic {@link Runnable} task using {@link Duration} based initial delay and interval, + * with a custom {@link RepeatPredicate}. + * + * @param task the task to be executed + * @param delay the initial delay before the first execution + * @param interval the interval between consecutive executions + * @param predicate the {@link RepeatPredicate} to control repetition + * @return a {@link ScheduledTask} representing the scheduled task + */ + default ScheduledTask scheduleAtFixedRate(Runnable task, Duration delay, Duration interval, RepeatPredicate predicate) { + return getMCScheduler().scheduleAtFixedRate(task, delay, interval, predicate); + } + + /** + * Schedule a one-time {@link Callable} task with no initial delay. + * + * @param task the callable task to be executed + * @param the return type of the callable + * @return a {@link ScheduledTask} representing the scheduled task + */ + default ScheduledTask schedule(Callable task) { + return getMCScheduler().schedule(task); + } + + /** + * Schedule a one-time {@link Callable} task with a specified {@link Duration} delay. + * + * @param task the callable task to be executed + * @param delay the duration before the task is executed + * @param the return type of the callable + * @return a {@link ScheduledTask} representing the scheduled task + */ + default ScheduledTask schedule(Callable task, Duration delay) { + return getMCScheduler().schedule(task, delay); + } + + /** + * Schedule a periodic {@link Callable} task using {@link Duration} based initial delay and interval. + * + *

The callable is expected to return a {@link TaskResponse} that informs whether the task should continue. + * + * @param task the callable task to be executed + * @param delay the initial delay before the first execution + * @param interval the interval between consecutive executions + * @param the return type wrapped by {@link TaskResponse} + * @return a {@link ScheduledTask} representing the scheduled task + */ + default ScheduledTask scheduleAtFixedRate(Callable> task, Duration delay, Duration interval) { + return getMCScheduler().scheduleAtFixedRate(task, delay, interval); + } + + /** + * Schedule a periodic {@link Callable} task using {@link Duration} based initial delay and interval, + * with a custom {@link RepeatPredicate}. + * + * @param task the callable task to be executed + * @param delay the initial delay before the first execution + * @param interval the interval between consecutive executions + * @param predicate the {@link RepeatPredicate} to control repetition + * @param the return type wrapped by {@link TaskResponse} + * @return a {@link ScheduledTask} representing the scheduled task + */ + default ScheduledTask scheduleAtFixedRate(Callable> task, Duration delay, Duration interval, RepeatPredicate predicate) { + return getMCScheduler().scheduleAtFixedRate(task, delay, interval, predicate); + } + + /** + * Check if the current thread is the same thread used by the scheduler returned + * by {@link #getMCScheduler()}. + * + * @return {@code true} if the current thread is the scheduler's thread, {@code false} otherwise + */ + default boolean isCurrentThread() { + return getMCScheduler().isCurrentThread(); + } + + /** + * Execute a {@link Runnable} task immediately, typically using the {@link MCScheduler#execute(Runnable)} + * method of the configured scheduler. This is useful for tasks you want to run as soon as possible, + * without any delay or periodic repetition. + * + * @param task the task to be executed + */ + default void execute(Runnable task) { + getMCScheduler().execute(task); + } +} diff --git a/commons/src/main/java/me/qwqdev/library/commons/task/annotation/TaskAutoStartAnnotation.java b/commons/src/main/java/me/qwqdev/library/commons/task/annotation/TaskAutoStartAnnotation.java new file mode 100644 index 0000000..4d8ce4b --- /dev/null +++ b/commons/src/main/java/me/qwqdev/library/commons/task/annotation/TaskAutoStartAnnotation.java @@ -0,0 +1,33 @@ +package me.qwqdev.library.commons.task.annotation; + +import me.qwqdev.library.commons.task.TaskInterface; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *

The {@link TaskAutoStartAnnotation} is a custom annotation that marks a class for + * auto-start behavior within the task scheduling system. + * + *

When a class annotated with {@link TaskAutoStartAnnotation} is detected, + * the {@link TaskAutoStartAnnotationProcessor} will automatically call its {@link TaskInterface#start()} method + * (assuming it implements {@link me.qwqdev.library.commons.task.TaskInterface}). + * + *

Once the annotation processor detects the above class, it will instantiate + * and invoke its {@link TaskInterface#start()} method, ensuring tasks are automatically initialized. + * + * @author qwq-dev + * @since 2024-12-24 11:47 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface TaskAutoStartAnnotation { + /** + * Determines whether the annotated class is created by @code Fairy IoC}. + * + * @return is created by @code Fairy IoC} or not + */ + boolean isFromFairyIoC() default true; +} diff --git a/commons/src/main/java/me/qwqdev/library/commons/task/annotation/TaskAutoStartAnnotationProcessor.java b/commons/src/main/java/me/qwqdev/library/commons/task/annotation/TaskAutoStartAnnotationProcessor.java new file mode 100644 index 0000000..39a2990 --- /dev/null +++ b/commons/src/main/java/me/qwqdev/library/commons/task/annotation/TaskAutoStartAnnotationProcessor.java @@ -0,0 +1,65 @@ +package me.qwqdev.library.commons.task.annotation; + +import io.fairyproject.container.Containers; +import io.fairyproject.log.Log; +import me.qwqdev.library.annotation.service.AnnotationProcessor; +import me.qwqdev.library.annotation.service.CustomAnnotationProcessor; +import me.qwqdev.library.commons.task.TaskInterface; + +/** + * The {@link TaskAutoStartAnnotationProcessor} is responsible for processing + * all classes annotated with {@link TaskAutoStartAnnotation}. Upon processing, it attempts + * to retrieve an instance of the annotated class from a dependency container (via {@link Containers#get(Class)}), + * or read the {@link TaskAutoStartAnnotation#isFromFairyIoC()} property from the annotation and + * create the instance directly using reflection, then calls the {@link TaskInterface#start()} method on that instance + * (if it implements {@link TaskInterface}). + * + *

When {@link TaskAutoStartAnnotation#isFromFairyIoC()} is false, it must contain a no-argument constructor. + * If we do not want it to be managed by {@code Fairy IoC} but still need to inject dependencies, + * we need to use the {@link io.fairyproject.container.Autowired} annotation. + * + * @author qwq-dev + * @since 2024-12-24 11:47 + */ +@AnnotationProcessor(TaskAutoStartAnnotation.class) +public class TaskAutoStartAnnotationProcessor implements CustomAnnotationProcessor { + /** + * Processes a class annotated with {@link TaskAutoStartAnnotation}. + * + *

Casts the retrieved object from the container to {@link TaskInterface}, + * then calls its {@link TaskInterface#start()} method to initiate the task. + * + * @param clazz the annotated class to be processed + */ + @Override + public void process(Class clazz) throws Exception { + TaskAutoStartAnnotation taskAutoStartAnnotation = + clazz.getAnnotation(TaskAutoStartAnnotation.class); + + if (taskAutoStartAnnotation == null) { + return; + } + + TaskInterface taskInterface; + + if (taskAutoStartAnnotation.isFromFairyIoC()) { + taskInterface = ((TaskInterface) Containers.get(clazz)); + } else { + taskInterface = (TaskInterface) clazz.getDeclaredConstructor().newInstance(); + } + + taskInterface.start(); + Log.info("[AnnotationProcessor] %s task started.", clazz.getName()); + } + + /** + * Handles exceptions that occur during the annotation processing phase. + * + * @param clazz the annotated class where the exception occurred + * @param exception the exception thrown + */ + @Override + public void exception(Class clazz, Exception exception) { + Log.error("An exception occurred while processing the task", exception); + } +} diff --git a/configuration/src/main/java/me/qwqdev/library/configuration/ConfigurationLauncher.java b/configuration/src/main/java/me/qwqdev/library/configuration/ConfigurationLauncher.java index ebd6da7..7ae1485 100644 --- a/configuration/src/main/java/me/qwqdev/library/configuration/ConfigurationLauncher.java +++ b/configuration/src/main/java/me/qwqdev/library/configuration/ConfigurationLauncher.java @@ -1,10 +1,8 @@ package me.qwqdev.library.configuration; import io.fairyproject.FairyLaunch; -import io.fairyproject.container.Autowired; import io.fairyproject.container.InjectableComponent; import io.fairyproject.plugin.Plugin; -import me.qwqdev.library.annotation.service.AnnotationProcessingServiceInterface; /** * The type Configuration launcher. @@ -15,13 +13,4 @@ @FairyLaunch @InjectableComponent public class ConfigurationLauncher extends Plugin { - @Autowired - private AnnotationProcessingServiceInterface annotationProcessingServiceInterface; - - @Override - public void onPluginEnable() { - // We need to process serializable annotations - String basePackage = this.getClass().getPackageName(); - annotationProcessingServiceInterface.processAnnotations(basePackage, false, this.getClassLoader()); - } }