diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/InjectTestProcessor.java b/inject-generator/src/main/java/io/avaje/inject/generator/InjectTestProcessor.java new file mode 100644 index 00000000..70e86298 --- /dev/null +++ b/inject-generator/src/main/java/io/avaje/inject/generator/InjectTestProcessor.java @@ -0,0 +1,73 @@ +package io.avaje.inject.generator; + +import static io.avaje.inject.generator.APContext.typeElement; + +import java.io.IOException; +import java.util.Optional; +import java.util.Set; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.TypeElement; + +@SupportedAnnotationTypes({"io.avaje.inject.test.InjectTest"}) +public final class InjectTestProcessor extends AbstractProcessor { + + private boolean wroteLookup; + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latest(); + } + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + if (!wroteLookup + && !Optional.ofNullable(typeElement("io.avaje.inject.test.InjectTest")) + .map(roundEnv::getElementsAnnotatedWith) + .orElse(Set.of()) + .isEmpty()) { + wroteLookup = true; + writeLookup(); + } + return false; + } + + private void writeLookup() { + var template = + "package io.avaje.inject.test.lookup;\n" + + "\n" + + "import java.lang.invoke.MethodHandles;\n" + + "import java.lang.invoke.MethodHandles.Lookup;\n" + + "\n" + + "import io.avaje.inject.test.LookupProvider;\n" + + "\n" + + "public class TestLookup implements LookupProvider {\n" + + "\n" + + " @Override\n" + + " public Lookup provideLookup() {\n" + + " return MethodHandles.lookup();\n" + + " }\n" + + "}"; + + try (var writer = + APContext.createSourceFile("io.avaje.inject.test.lookup.TestLookup").openWriter(); + var services = + ProcessingContext.createMetaInfWriterFor( + "META-INF/services/io.avaje.inject.test.LookupProvider") + .openWriter()) { + writer.append(template); + services.append("io.avaje.inject.test.lookup.TestLookup"); + } catch (IOException e) { + APContext.logWarn("failed to write lookup"); + } + } +} diff --git a/inject-generator/src/main/java/module-info.java b/inject-generator/src/main/java/module-info.java index 8d0464df..02a487fc 100644 --- a/inject-generator/src/main/java/module-info.java +++ b/inject-generator/src/main/java/module-info.java @@ -1,3 +1,6 @@ +import io.avaje.inject.generator.InjectProcessor; +import io.avaje.inject.generator.InjectTestProcessor; + module io.avaje.inject.generator { requires java.compiler; @@ -11,5 +14,5 @@ uses io.avaje.inject.spi.InjectExtension; - provides javax.annotation.processing.Processor with io.avaje.inject.generator.InjectProcessor; + provides javax.annotation.processing.Processor with InjectProcessor, InjectTestProcessor; } diff --git a/inject-test/pom.xml b/inject-test/pom.xml index 9486328f..69301acd 100644 --- a/inject-test/pom.xml +++ b/inject-test/pom.xml @@ -12,7 +12,7 @@ 5.12.0 - 5.15.2 + 5.16.0 @@ -45,12 +45,12 @@ net.bytebuddy byte-buddy - 1.17.1 + 1.17.2 net.bytebuddy byte-buddy-agent - 1.17.1 + 1.17.2 diff --git a/inject-test/src/main/java/io/avaje/inject/test/LookupProvider.java b/inject-test/src/main/java/io/avaje/inject/test/LookupProvider.java new file mode 100644 index 00000000..e42acc7c --- /dev/null +++ b/inject-test/src/main/java/io/avaje/inject/test/LookupProvider.java @@ -0,0 +1,10 @@ +package io.avaje.inject.test; + +import java.lang.invoke.MethodHandles.Lookup; + +/** Provides a Lookup instance for accessing test fields. */ +public interface LookupProvider { + + /** Return the Lookup. */ + Lookup provideLookup(); +} diff --git a/inject-test/src/main/java/io/avaje/inject/test/Lookups.java b/inject-test/src/main/java/io/avaje/inject/test/Lookups.java new file mode 100644 index 00000000..e3f5aaaf --- /dev/null +++ b/inject-test/src/main/java/io/avaje/inject/test/Lookups.java @@ -0,0 +1,57 @@ +package io.avaje.inject.test; + +import static java.util.stream.Collectors.toMap; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.VarHandle; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Map; +import java.util.ServiceLoader; + +/** Provides Lookup instances using potentially module specific Lookups. */ +final class Lookups { + + private static final Map MODULE_LOOKUP_MAP = + ServiceLoader.load(LookupProvider.class).stream() + .collect(toMap(p -> p.type().getModule().getName(), p -> p.get().provideLookup())); + + private static final Lookup DEFAULT_LOOKUP = MethodHandles.publicLookup(); + + /** Return a Lookup ideally for the module associated with the given type. */ + static Lookup getLookup(Class type) { + return MODULE_LOOKUP_MAP.getOrDefault(type.getModule().getName(), DEFAULT_LOOKUP); + } + + static VarHandle getVarhandle(Class testClass, Field field) { + try { + var lookup = getLookup(testClass); + lookup = + lookup.hasPrivateAccess() + ? MethodHandles.privateLookupIn(testClass, getLookup(testClass)) + : lookup; + + return lookup.unreflectVarHandle(field); + } catch (Exception e) { + throw new IllegalStateException("Can't access field " + field, e); + } + } + + static Class getClassFromType(Type generic) { + if (generic instanceof Class) { + return (Class) generic; + } + if (generic instanceof ParameterizedType) { + Type actual = ((ParameterizedType) generic).getActualTypeArguments()[0]; + if (actual instanceof Class) { + return (Class) actual; + } + if (actual instanceof ParameterizedType) { + return (Class) ((ParameterizedType) actual).getRawType(); + } + } + return Object.class; + } +} diff --git a/inject-test/src/main/java/io/avaje/inject/test/MetaReader.java b/inject-test/src/main/java/io/avaje/inject/test/MetaReader.java index ac02315c..5ab35916 100644 --- a/inject-test/src/main/java/io/avaje/inject/test/MetaReader.java +++ b/inject-test/src/main/java/io/avaje/inject/test/MetaReader.java @@ -1,6 +1,7 @@ package io.avaje.inject.test; import java.lang.annotation.Annotation; +import java.lang.invoke.VarHandle; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; @@ -14,8 +15,6 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Spy; -import org.mockito.internal.configuration.plugins.Plugins; -import org.mockito.internal.util.reflection.GenericMaster; import io.avaje.inject.BeanScope; import io.avaje.inject.BeanScopeBuilder; @@ -26,6 +25,7 @@ final class MetaReader { private final SetupMethods methodFinder; + final Class testClass; final List captors = new ArrayList<>(); final List mocks = new ArrayList<>(); final List spies = new ArrayList<>(); @@ -41,6 +41,7 @@ final class MetaReader { boolean instancePlugin; MetaReader(Class testClass, Plugin plugin) { + this.testClass = testClass; this.plugin = plugin; final var hierarchy = typeHierarchy(testClass); this.methodFinder = new SetupMethods(hierarchy); @@ -54,9 +55,8 @@ final class MetaReader { boolean hasMocksOrSpies(Object testInstance) { if (testInstance == null) { return hasStaticMocksOrSpies() || methodFinder.hasStaticMethods(); - } else { - return hasInstanceMocksOrSpies(testInstance) || methodFinder.hasInstanceMethods(); } + return hasInstanceMocksOrSpies(testInstance) || methodFinder.hasInstanceMethods(); } private boolean hasInstanceMocksOrSpies(Object testInstance) { @@ -154,7 +154,7 @@ private void add(FieldTarget target, List instanceList, List type = field.getType(); if (!ArgumentCaptor.class.isAssignableFrom(type)) { - throw new IllegalStateException("@Captor field must be of the type ArgumentCaptor.\n Field: '" + field.getName() + "' has wrong type"); + throw new IllegalStateException( + "@Captor field must be of the type ArgumentCaptor.\n Field: '" + + field.getName() + + "' has wrong type"); } - Class cls = new GenericMaster().getGenericType(field); + Class cls = Lookups.getClassFromType(field.getGenericType()); return ArgumentCaptor.forClass(cls); } @@ -308,8 +314,12 @@ private static void registerAsTestDouble(BeanScopeBuilder builder, FieldTarget t builder.bean(target.name(), target.type(), value); } - void set(Field field, Object val, Object testInstance) throws IllegalAccessException { - Plugins.getMemberAccessor().set(field, testInstance, val); + void set(boolean isStatic, VarHandle fieldHandle, Object val, Object testInstance) { + if (isStatic) { + fieldHandle.set(val); + } else { + fieldHandle.set(testInstance, val); + } } class FieldTarget { @@ -319,11 +329,13 @@ class FieldTarget { private final boolean isStatic; private boolean pluginInjection; private boolean valueAlreadyProvided; + private final VarHandle fieldHandle; - FieldTarget(Field field, String name) { + FieldTarget(Field field, String name, VarHandle fieldHandle) { this.field = field; this.isStatic = Modifier.isStatic(field.getModifiers()); this.name = name; + this.fieldHandle = fieldHandle; } @Override @@ -344,11 +356,7 @@ boolean isStatic() { } Object get(Object instance) { - try { - return Plugins.getMemberAccessor().get(field, instance); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } + return isStatic ? fieldHandle.get() : fieldHandle.get(instance); } void setFromScope(BeanScope beanScope, Object testInstance) throws IllegalAccessException { @@ -356,28 +364,27 @@ void setFromScope(BeanScope beanScope, Object testInstance) throws IllegalAccess return; } final var type = type(); - if (type instanceof ParameterizedType) { final var parameterizedType = (ParameterizedType) type; final var rawType = parameterizedType.getRawType(); final var typeArguments = parameterizedType.getActualTypeArguments(); if (rawType.equals(List.class)) { - set(field, beanScope.list(typeArguments[0]), testInstance); + set(isStatic, fieldHandle, beanScope.list(typeArguments[0]), testInstance); return; } if (rawType.equals(Optional.class)) { - set(field, beanScope.getOptional(typeArguments[0], name), testInstance); + set(isStatic, fieldHandle, beanScope.getOptional(typeArguments[0], name), testInstance); return; } } - set(field, beanScope.get(type, name), testInstance); + set(isStatic, fieldHandle, beanScope.get(type, name), testInstance); } void setFromPlugin(Object value, Object testInstance) throws IllegalAccessException { - set(field, value, testInstance); + set(isStatic, fieldHandle, value, testInstance); } void markForPluginInjection() { diff --git a/inject-test/src/main/java/module-info.java b/inject-test/src/main/java/module-info.java index 956aef2c..6b4dbba6 100644 --- a/inject-test/src/main/java/module-info.java +++ b/inject-test/src/main/java/module-info.java @@ -16,4 +16,5 @@ uses io.avaje.inject.test.TestModule; uses io.avaje.inject.test.Plugin; + uses io.avaje.inject.test.LookupProvider; } diff --git a/inject/pom.xml b/inject/pom.xml index 32bfe740..a0a3e0ac 100644 --- a/inject/pom.xml +++ b/inject/pom.xml @@ -53,7 +53,7 @@ org.mockito mockito-core - 5.15.2 + 5.16.0 true diff --git a/pom.xml b/pom.xml index 48c99a82..0476e854 100644 --- a/pom.xml +++ b/pom.xml @@ -61,19 +61,19 @@ net.bytebuddy byte-buddy - 1.17.1 + 1.17.2 test net.bytebuddy byte-buddy-agent - 1.17.1 + 1.17.2 test org.mockito mockito-core - 5.15.2 + 5.16.0 test