From 22b500a4a412e42c5fe5939056e2b177c52fa4e3 Mon Sep 17 00:00:00 2001 From: QwQ-dev Date: Mon, 23 Dec 2024 18:46:33 +0800 Subject: [PATCH] feat: VarHandleReflectionInjector. --- annotation/README.md | 3 +- cache/README.md | 8 +- .../cache/factory/CacheServiceFactory.java | 23 +++- commons/README.md | 51 +++++++++ commons/build.gradle.kts | 2 + .../library/commons/CommonsLauncher.java | 16 +++ .../commons/factory/InjectorFactory.java | 25 +++++ .../injector/ObjectInjectorInterface.java | 16 +++ .../injector/StaticInjectorInterface.java | 16 +++ .../injector/VarHandleReflectionInjector.java | 100 ++++++++++++++++++ .../annotation/VarHandleAutoInjection.java | 42 ++++++++ .../factory/SimplixBuilderFactory.java | 9 +- mongodb/README.md | 8 +- .../MongoDBConnectionConfigFactory.java | 7 +- 14 files changed, 313 insertions(+), 13 deletions(-) create mode 100644 commons/README.md create mode 100644 commons/src/main/java/me/qwqdev/library/commons/CommonsLauncher.java create mode 100644 commons/src/main/java/me/qwqdev/library/commons/factory/InjectorFactory.java create mode 100644 commons/src/main/java/me/qwqdev/library/commons/injector/ObjectInjectorInterface.java create mode 100644 commons/src/main/java/me/qwqdev/library/commons/injector/StaticInjectorInterface.java create mode 100644 commons/src/main/java/me/qwqdev/library/commons/injector/VarHandleReflectionInjector.java create mode 100644 commons/src/main/java/me/qwqdev/library/commons/injector/annotation/VarHandleAutoInjection.java diff --git a/annotation/README.md b/annotation/README.md index ec832b8..b5517ab 100644 --- a/annotation/README.md +++ b/annotation/README.md @@ -71,5 +71,6 @@ We only need to get `AnnotationProcessingService` through dependency injection a 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 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 diff --git a/cache/README.md b/cache/README.md index 800456c..58012db 100644 --- a/cache/README.md +++ b/cache/README.md @@ -1,9 +1,11 @@ ### cache -The module is a caching solution that supports `Caffeine`'s `Cache` and `AsyncCache`, -as well as an in-memory `Redis` database implementation, offering functional programming support and automatic lock handling. +The module is a caching solution that supports `Caffeine`'s `Cache` and `AsyncCache`, +as well as an in-memory `Redis` database implementation, offering functional programming support and automatic lock +handling. -The original purpose of this module was to design a L1 cache for the `mongodb` module, but now it is **general-purpose**. +The original purpose of this module was to design a L1 cache for the `mongodb` module, but now it is **general-purpose +**. ### usage diff --git a/cache/src/main/java/me/qwqdev/library/cache/factory/CacheServiceFactory.java b/cache/src/main/java/me/qwqdev/library/cache/factory/CacheServiceFactory.java index a948e0e..09a6cb9 100644 --- a/cache/src/main/java/me/qwqdev/library/cache/factory/CacheServiceFactory.java +++ b/cache/src/main/java/me/qwqdev/library/cache/factory/CacheServiceFactory.java @@ -22,22 +22,28 @@ @UtilityClass public final class CacheServiceFactory { /** - * Creates a Redis cache service with the specified configuration. + * Creates a {@link RedisCacheService} with the specified configuration. * * @param config the Redis configuration - * @return a new Redis cache service instance + * @return a new {@link RedisCacheService} instance + * @see RedisCacheService + * @see RedisCacheServiceInterface + * @see Config */ public static RedisCacheServiceInterface createRedisCache(Config config) { return new RedisCacheService(config); } /** - * Creates a Caffeine synchronous cache service with custom configuration. + * Creates a {@link CaffeineCacheService} with custom configuration. * * @param cache the Caffeine cache * @param the cache key type * @param the cache value type - * @return a new Caffeine cache service instance + * @return a new {@link CaffeineCacheService} instance + * @see CacheServiceInterface + * @see Cache + * @see CaffeineCacheService */ public static CacheServiceInterface, V> createCaffeineCache(Cache cache) { return new CaffeineCacheService<>(cache); @@ -49,6 +55,9 @@ public static CacheServiceInterface, V> createCaffeineCache(C * @param the cache key type * @param the cache value type * @return a new Caffeine cache service instance + * @see CacheServiceInterface + * @see Cache + * @see CaffeineCacheService */ public static CacheServiceInterface, V> createCaffeineCache() { return new CaffeineCacheService<>(); @@ -61,6 +70,9 @@ public static CacheServiceInterface, V> createCaffeineCache() * @param the cache key type * @param the cache value type * @return a new async Caffeine cache service instance + * @see CacheServiceInterface + * @see AsyncCache + * @see CaffeineAsyncCacheService */ public static CacheServiceInterface, V> createCaffeineAsyncCache(AsyncCache asyncCache) { return new CaffeineAsyncCacheService<>(asyncCache); @@ -72,6 +84,9 @@ public static CacheServiceInterface, V> createCaffeineAs * @param the cache key type * @param the cache value type * @return a new async Caffeine cache service instance + * @see CacheServiceInterface + * @see AsyncCache + * @see CaffeineAsyncCacheService */ public static CacheServiceInterface, V> createCaffeineAsyncCache() { return new CaffeineAsyncCacheService<>(); diff --git a/commons/README.md b/commons/README.md new file mode 100644 index 0000000..d52bc6c --- /dev/null +++ b/commons/README.md @@ -0,0 +1,51 @@ +### commons + +This is a module full of good stuff that is useful in every way, so it's a bit of a mixed bag, but I'll always update +this document when there's new content. + +### usage + +```kotlin +// Dependencies +dependencies { + // commons module + compileOnly("me.qwqdev.library:commons:1.0-SNAPSHOT") +} +``` + +### [VarHandleReflectionInjector](src/main/java/me/qwqdev/library/commons/injector/VarHandleReflectionInjector.java) + +This is an `injector`, and its main use is to be used +with [VarHandleAutoInjection](src/main/java/me/qwqdev/library/commons/injector/annotation/VarHandleAutoInjection.java). + +Yes, just like its name, we don't have to write a bunch of ugly code to assign `VarHandle`, let it all disappear, Amen. + +```java +public class Example { + public static void main(String[] args) { + Test test = new Test(); + + // we can use TField_HANDLE + Test.TField_HANDLE.set(test, 2); + + // prints 2 + System.out.println(test.getTField()); + } +} +``` + +```java + +@Getter +@Setter +public class Test { + private volatile int tField = 100; + + @VarHandleAutoInjection(fieldName = "tField") + public static VarHandle TField_HANDLE; + + static { + new VarHandleReflectionInjector().inject(Test.class); + } +} +``` \ No newline at end of file diff --git a/commons/build.gradle.kts b/commons/build.gradle.kts index f1bb3e8..8f37427 100644 --- a/commons/build.gradle.kts +++ b/commons/build.gradle.kts @@ -24,4 +24,6 @@ fairy { // Dependencies dependencies { + // Annotation module + compileOnly(project(":annotation")) } \ No newline at end of file diff --git a/commons/src/main/java/me/qwqdev/library/commons/CommonsLauncher.java b/commons/src/main/java/me/qwqdev/library/commons/CommonsLauncher.java new file mode 100644 index 0000000..dc5b43d --- /dev/null +++ b/commons/src/main/java/me/qwqdev/library/commons/CommonsLauncher.java @@ -0,0 +1,16 @@ +package me.qwqdev.library.commons; + +import io.fairyproject.FairyLaunch; +import io.fairyproject.container.InjectableComponent; +import io.fairyproject.plugin.Plugin; + +/** + * The type Commons launcher. + * + * @author qwq-dev + * @since 2024-12-23 18:32 + */ +@FairyLaunch +@InjectableComponent +public class CommonsLauncher extends Plugin { +} diff --git a/commons/src/main/java/me/qwqdev/library/commons/factory/InjectorFactory.java b/commons/src/main/java/me/qwqdev/library/commons/factory/InjectorFactory.java new file mode 100644 index 0000000..746ed18 --- /dev/null +++ b/commons/src/main/java/me/qwqdev/library/commons/factory/InjectorFactory.java @@ -0,0 +1,25 @@ +package me.qwqdev.library.commons.factory; + +import lombok.experimental.UtilityClass; +import me.qwqdev.library.commons.injector.StaticInjectorInterface; +import me.qwqdev.library.commons.injector.VarHandleReflectionInjector; + +/** + * Factory for creating injector instances. + * + * @author qwq-dev + * @since 2024-12-23 18:38 + */ +@UtilityClass +public final class InjectorFactory { + /** + * Create a {@link VarHandleReflectionInjector} instance. + * + * @return a {@link VarHandleReflectionInjector} instance + * @see StaticInjectorInterface + * @see VarHandleReflectionInjector + */ + public static StaticInjectorInterface createVarHandleReflectionInjector() { + return new VarHandleReflectionInjector(); + } +} diff --git a/commons/src/main/java/me/qwqdev/library/commons/injector/ObjectInjectorInterface.java b/commons/src/main/java/me/qwqdev/library/commons/injector/ObjectInjectorInterface.java new file mode 100644 index 0000000..cf43b67 --- /dev/null +++ b/commons/src/main/java/me/qwqdev/library/commons/injector/ObjectInjectorInterface.java @@ -0,0 +1,16 @@ +package me.qwqdev.library.commons.injector; + +/** + * Interface for object injectors. + * + * @author qwq-dev + * @since 2024-12-23 16:44 + */ +public interface ObjectInjectorInterface { + /** + * Process the given object. + * + * @param object given object + */ + void inject(Object object); +} diff --git a/commons/src/main/java/me/qwqdev/library/commons/injector/StaticInjectorInterface.java b/commons/src/main/java/me/qwqdev/library/commons/injector/StaticInjectorInterface.java new file mode 100644 index 0000000..acfc093 --- /dev/null +++ b/commons/src/main/java/me/qwqdev/library/commons/injector/StaticInjectorInterface.java @@ -0,0 +1,16 @@ +package me.qwqdev.library.commons.injector; + +/** + * Interface for static injectors. + * + * @author qwq-dev + * @since 2024-12-23 17:07 + */ +public interface StaticInjectorInterface { + /** + * Process the given class. + * + * @param clazz given class + */ + void inject(Class clazz); +} 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 new file mode 100644 index 0000000..8a81c7d --- /dev/null +++ b/commons/src/main/java/me/qwqdev/library/commons/injector/VarHandleReflectionInjector.java @@ -0,0 +1,100 @@ +package me.qwqdev.library.commons.injector; + +import me.qwqdev.library.commons.injector.annotation.VarHandleAutoInjection; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * Utility 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. + * + *

The injection is done via reflection, which allows this class to work without explicitly hardcoding the + * dependencies. However, this approach introduces some overhead and may not be suitable for high-performance use cases. + *

+ * + *

The injection process works in two ways: + *

    + *
  • Default: Uses {@link MethodHandles#lookup()} to locate the {@link VarHandle} for the specified static field. + * This is the most common use case when static methods aren't needed.
  • + *
  • Static Method: If specified in the {@link VarHandleAutoInjection} annotation, a static method can be used + * to retrieve the {@link VarHandle} for the field. This allows for more customized handling of the {@link VarHandle} + * injection.
  • + *
+ *

+ * + *

If using a static method for injection, the class containing the static method must be loadable by the current + * thread's context {@link ClassLoader}. The method should be accessible and match the required signature for retrieving + * the {@link VarHandle}.

+ * + *

Due to the use of reflection, the fields will be made accessible even if they are private, ensuring that injection + * can occur regardless of visibility modifiers.

+ * + *

This class implements the {@link StaticInjectorInterface} interface, which defines the contract for injecting + * dependencies (in this case, {@link VarHandle} instances) into static fields of a class.

+ * + * @author qwq-dev + * @since 2024-12-23 16:15 + */ +public class VarHandleReflectionInjector implements StaticInjectorInterface { + /** + * {@inheritDoc} + *

+ * 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. + * + *

The method performs the injection by either using the default method of + * {@link MethodHandles#privateLookupIn(Class, MethodHandles.Lookup)} to + * locate the {@link VarHandle} for the specified static field or by using a static method if defined in the + * annotation {@link VarHandleAutoInjection}. + * + *

The specified static method must contain two parameters: {@link String} and {@link Class} + * + *

If a static method is specified in the annotation, it will be called to retrieve the {@link VarHandle} + * for the field. The class containing the static method must be loadable by the current thread's context + * {@link ClassLoader}.

+ * + * @param clazz the class into which {@link VarHandle} instances will be injected into its static fields + * @throws IllegalStateException if the injection fails due to reflection issues or invalid annotations + */ + @Override + public void inject(Class clazz) { + Field[] fields = clazz.getDeclaredFields(); + + for (Field field : fields) { + VarHandleAutoInjection annotation = field.getAnnotation(VarHandleAutoInjection.class); + + if (annotation == null) { + continue; + } + + String targetFieldName = annotation.fieldName(); + String staticMethodName = annotation.staticMethodName(); + String staticMethodPackage = annotation.staticMethodPackage(); + boolean useStaticMethod = !staticMethodName.isEmpty() && !staticMethodPackage.isEmpty(); + + try { + VarHandle varHandle; + + if (useStaticMethod) { + Class staticClass = Class.forName(staticMethodPackage); + Method method = staticClass.getDeclaredMethod(staticMethodName, String.class, Class.class); + method.setAccessible(true); + varHandle = (VarHandle) method.invoke(null, targetFieldName, field.getType()); + } else { + varHandle = MethodHandles.privateLookupIn(clazz, MethodHandles.lookup()) + .unreflectVarHandle(clazz.getDeclaredField(targetFieldName)); + } + + field.setAccessible(true); + field.set(null, varHandle); + } catch (Exception exception) { + throw new IllegalStateException("Failed to inject VarHandle for field: " + field.getName(), exception); + } + } + } +} diff --git a/commons/src/main/java/me/qwqdev/library/commons/injector/annotation/VarHandleAutoInjection.java b/commons/src/main/java/me/qwqdev/library/commons/injector/annotation/VarHandleAutoInjection.java new file mode 100644 index 0000000..bc2423b --- /dev/null +++ b/commons/src/main/java/me/qwqdev/library/commons/injector/annotation/VarHandleAutoInjection.java @@ -0,0 +1,42 @@ +package me.qwqdev.library.commons.injector.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.invoke.VarHandle; + +/** + * Annotation to mark a field for automatic {@link VarHandle} injection. + * + * @author qwq-dev + * @since 2024-12-23 16:15 + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface VarHandleAutoInjection { + /** + * The name of the target field for which the {@link VarHandle} is being generated. + * + * @return the field name + */ + String fieldName(); + + /** + * The name of the static method used to generate the {@link VarHandle}. + * + *

The specified static method must contain two parameters: {@link String} and {@link Class} + * + * @return the static method name, or an empty string if not specified + */ + String staticMethodName() default ""; + + /** + * The fully qualified name of the class containing the static method used to generate the {@link VarHandle}. + * + *

The class must be loadable by the current thread's context {@link ClassLoader}. + * + * @return the fully qualified class name, or an empty string if not specified + */ + String staticMethodPackage() default ""; +} diff --git a/configuration/src/main/java/me/qwqdev/library/configuration/factory/SimplixBuilderFactory.java b/configuration/src/main/java/me/qwqdev/library/configuration/factory/SimplixBuilderFactory.java index 228d760..873a802 100644 --- a/configuration/src/main/java/me/qwqdev/library/configuration/factory/SimplixBuilderFactory.java +++ b/configuration/src/main/java/me/qwqdev/library/configuration/factory/SimplixBuilderFactory.java @@ -20,7 +20,7 @@ * @since 2024-12-18 13:48 */ @UtilityClass -public class SimplixBuilderFactory { +public final class SimplixBuilderFactory { /** * Creates a {@link SimplixBuilder} instance from a file. * @@ -28,6 +28,8 @@ public class SimplixBuilderFactory { * * @param file the file to create the {@link SimplixBuilder} from * @return a configured {@link SimplixBuilder} instance + * @see File + * @see SimplixBuilder */ public static SimplixBuilder createSimplixBuilderFromFile(File file) { return configureSimplixBuilder(SimplixBuilder.fromFile(file)); @@ -40,6 +42,8 @@ public static SimplixBuilder createSimplixBuilderFromFile(File file) { * * @param file the directory to create the {@link SimplixBuilder} from * @return a configured {@link SimplixBuilder} instance + * @see File + * @see SimplixBuilder */ public static SimplixBuilder createSimplixBuilderFromDirectory(File file) { return configureSimplixBuilder(SimplixBuilder.fromDirectory(file)); @@ -52,6 +56,8 @@ public static SimplixBuilder createSimplixBuilderFromDirectory(File file) { * * @param path the path to create the {@link SimplixBuilder} from * @return a configured {@link SimplixBuilder} instance + * @see Path + * @see SimplixBuilder */ public static SimplixBuilder createSimplixBuilderFromPath(Path path) { return configureSimplixBuilder(SimplixBuilder.fromPath(path)); @@ -65,6 +71,7 @@ public static SimplixBuilder createSimplixBuilderFromPath(Path path) { * @param name the name of the file * @param path the path to the file * @return a configured {@link SimplixBuilder} instance + * @see SimplixBuilder */ public static SimplixBuilder createSimplixBuilder(String name, String path) { return configureSimplixBuilder(SimplixBuilder.fromPath(name, path)); diff --git a/mongodb/README.md b/mongodb/README.md index eca85ca..944e7d9 100644 --- a/mongodb/README.md +++ b/mongodb/README.md @@ -1,8 +1,10 @@ ### mongodb -This module only encapsulates a more convenient MongoConfig based on [Morphia](https://morphia.dev/landing/index.html), and its implementation is very simple. +This module only encapsulates a more convenient MongoConfig based on [Morphia](https://morphia.dev/landing/index.html), +and its implementation is very simple. -We recommend using Datastore directly for any CRUD operations, using [Morphia](https://morphia.dev/landing/index.html) for index creation or using the [aggregation](https://morphia.dev/morphia/2.4/aggregations.html). +We recommend using Datastore directly for any CRUD operations, using [Morphia](https://morphia.dev/landing/index.html) +for index creation or using the [aggregation](https://morphia.dev/morphia/2.4/aggregations.html). ### cache @@ -57,7 +59,7 @@ public class Example { Filters.eq("name", "Alice"), // name eq Filters.gt("age", 35) // age > 35, it's time to lay off employees lol )) - + // It should be noted that iterators are not thread-safe .iterator(); // get iterator diff --git a/mongodb/src/main/java/me/qwqdev/library/mongodb/factory/MongoDBConnectionConfigFactory.java b/mongodb/src/main/java/me/qwqdev/library/mongodb/factory/MongoDBConnectionConfigFactory.java index b6ecb54..2c0c4ed 100644 --- a/mongodb/src/main/java/me/qwqdev/library/mongodb/factory/MongoDBConnectionConfigFactory.java +++ b/mongodb/src/main/java/me/qwqdev/library/mongodb/factory/MongoDBConnectionConfigFactory.java @@ -13,7 +13,7 @@ * @since 2024-12-20 12:18 */ @UtilityClass -public class MongoDBConnectionConfigFactory { +public final class MongoDBConnectionConfigFactory { /** * Creates a default {@link MongoDBConnectionConfig} instance using the standard UUID representation. * @@ -21,6 +21,7 @@ public class MongoDBConnectionConfigFactory { * @param mongoURL the MongoDB connection URL, e.g., "mongodb://localhost:27017" * @return a configured {@link MongoDBConnectionConfig} instance * @throws IllegalArgumentException if either the database name or MongoDB URL is null or empty + * @see MongoDBConnectionConfig */ public static MongoDBConnectionConfig create(String databaseName, String mongoURL) { return new MongoDBConnectionConfig(databaseName, mongoURL); @@ -34,6 +35,8 @@ public static MongoDBConnectionConfig create(String databaseName, String mongoUR * @param uuidRepresentation the UUID representation to be used in MongoDB documents * @return a configured {@link MongoDBConnectionConfig} instance * @throws IllegalArgumentException if either the database name or MongoDB URL is null or empty + * @see MongoDBConnectionConfig + * @see UuidRepresentation */ public static MongoDBConnectionConfig create(String databaseName, String mongoURL, UuidRepresentation uuidRepresentation) { return new MongoDBConnectionConfig(databaseName, mongoURL, uuidRepresentation); @@ -45,6 +48,8 @@ public static MongoDBConnectionConfig create(String databaseName, String mongoUR * @param mongoClientSettings the custom {@link MongoClientSettings} to configure the MongoClient * @return a configured {@link MongoDBConnectionConfig} instance * @throws IllegalArgumentException if {@link MongoClientSettings} is null + * @see MongoDBConnectionConfig + * @see MongoClientSettings */ public static MongoDBConnectionConfig create(MongoClientSettings mongoClientSettings) { return new MongoDBConnectionConfig(mongoClientSettings);