Skip to content

[inject-test] Generate Method Handle Lookups #775

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<? extends TypeElement> 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");
}
}
}
5 changes: 4 additions & 1 deletion inject-generator/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import io.avaje.inject.generator.InjectProcessor;
import io.avaje.inject.generator.InjectTestProcessor;

module io.avaje.inject.generator {

requires java.compiler;
Expand All @@ -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;
}
6 changes: 3 additions & 3 deletions inject-test/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

<properties>
<jupiter.version>5.12.0</jupiter.version>
<mockito.version>5.15.2</mockito.version>
<mockito.version>5.16.0</mockito.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -45,12 +45,12 @@
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.17.1</version>
<version>1.17.2</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.17.1</version>
<version>1.17.2</version>
</dependency>

<dependency>
Expand Down
10 changes: 10 additions & 0 deletions inject-test/src/main/java/io/avaje/inject/test/LookupProvider.java
Original file line number Diff line number Diff line change
@@ -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();
}
57 changes: 57 additions & 0 deletions inject-test/src/main/java/io/avaje/inject/test/Lookups.java
Original file line number Diff line number Diff line change
@@ -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<String, Lookup> 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;
}
}
53 changes: 30 additions & 23 deletions inject-test/src/main/java/io/avaje/inject/test/MetaReader.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -26,6 +25,7 @@
final class MetaReader {

private final SetupMethods methodFinder;
final Class<?> testClass;
final List<Field> captors = new ArrayList<>();
final List<FieldTarget> mocks = new ArrayList<>();
final List<FieldTarget> spies = new ArrayList<>();
Expand All @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -154,7 +154,7 @@ private void add(FieldTarget target, List<FieldTarget> instanceList, List<FieldT
}

private FieldTarget newTarget(Field field) {
return new FieldTarget(field, name(field));
return new FieldTarget(field, name(field), Lookups.getVarhandle(testClass, field));
}

private String name(Field field) {
Expand All @@ -178,9 +178,8 @@ private String name(Field field) {
TestBeans setFromScope(TestBeans metaScope, Object testInstance) {
if (testInstance != null) {
return setForInstance(metaScope, testInstance);
} else {
return setForStatics(metaScope);
}
return setForStatics(metaScope);
}

private TestBeans setForInstance(TestBeans metaScope, Object testInstance) {
Expand All @@ -189,7 +188,11 @@ private TestBeans setForInstance(TestBeans metaScope, Object testInstance) {
BeanScope beanScope = metaScope.beanScope();

for (Field field : captors) {
set(field, captorFor(field), testInstance);
set(
Modifier.isStatic(field.getModifiers()),
Lookups.getVarhandle(testClass, field),
captorFor(field),
testInstance);
}
for (FieldTarget target : mocks) {
target.setFromScope(beanScope, testInstance);
Expand Down Expand Up @@ -239,9 +242,12 @@ private TestBeans setForStatics(TestBeans metaScope) {
private Object captorFor(Field field) {
Class<?> 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);
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -344,40 +356,35 @@ 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 {
if (valueAlreadyProvided) {
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() {
Expand Down
1 change: 1 addition & 0 deletions inject-test/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@

uses io.avaje.inject.test.TestModule;
uses io.avaje.inject.test.Plugin;
uses io.avaje.inject.test.LookupProvider;
}
2 changes: 1 addition & 1 deletion inject/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.15.2</version>
<version>5.16.0</version>
<optional>true</optional>
</dependency>

Expand Down
6 changes: 3 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,19 +61,19 @@
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.17.1</version>
<version>1.17.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.17.1</version>
<version>1.17.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.15.2</version>
<version>5.16.0</version>
<scope>test</scope>
</dependency>
</dependencies>
Expand Down