From 995deaf7fd8aa7e68a17a2d95cde3b97aeb20fb8 Mon Sep 17 00:00:00 2001 From: Hannes Wellmann Date: Mon, 23 Sep 2024 19:19:08 +0200 Subject: [PATCH] Support multiple versions of annotation classes in E4 Injector and specifically support jakarta.annotation version 3.0. Fixes https://github.com/eclipse-platform/eclipse.platform/issues/1565 --- .../e4/core/internal/di/AnnotationLookup.java | 196 ++++++++++-------- .../e4/core/internal/di/InjectorImpl.java | 51 +---- .../e4/core/internal/di/ProviderImpl.java | 43 ---- 3 files changed, 112 insertions(+), 178 deletions(-) delete mode 100644 runtime/bundles/org.eclipse.e4.core.di/src/org/eclipse/e4/core/internal/di/ProviderImpl.java diff --git a/runtime/bundles/org.eclipse.e4.core.di/src/org/eclipse/e4/core/internal/di/AnnotationLookup.java b/runtime/bundles/org.eclipse.e4.core.di/src/org/eclipse/e4/core/internal/di/AnnotationLookup.java index d6793c24280..07f3954a855 100644 --- a/runtime/bundles/org.eclipse.e4.core.di/src/org/eclipse/e4/core/internal/di/AnnotationLookup.java +++ b/runtime/bundles/org.eclipse.e4.core.di/src/org/eclipse/e4/core/internal/di/AnnotationLookup.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023, 2023 Hannes Wellmann and others. + * Copyright (c) 2023, 2024 Hannes Wellmann and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -10,18 +10,24 @@ * * Contributors: * Hannes Wellmann - initial API and implementation + * Hannes Wellmann - support multiple versions of one annotation class *******************************************************************************/ package org.eclipse.e4.core.internal.di; import java.lang.annotation.Annotation; +import java.lang.invoke.CallSite; +import java.lang.invoke.LambdaMetafactory; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Supplier; import org.eclipse.e4.core.di.IInjector; @@ -42,14 +48,15 @@ public class AnnotationLookup { private AnnotationLookup() { } - public static record AnnotationProxy(List> classes) { + public static record AnnotationProxy(List classes) { + public AnnotationProxy { classes = List.copyOf(classes); } public boolean isPresent(AnnotatedElement element) { - for (Class annotationClass : classes) { - if (element.isAnnotationPresent(annotationClass)) { + for (Annotation annotation : element.getAnnotations()) { + if (classes.contains(annotation.annotationType().getName())) { return true; } } @@ -57,114 +64,119 @@ public boolean isPresent(AnnotatedElement element) { } } - static final AnnotationProxy INJECT = createProxyForClasses(jakarta.inject.Inject.class, - () -> javax.inject.Inject.class); - static final AnnotationProxy SINGLETON = createProxyForClasses(jakarta.inject.Singleton.class, - () -> javax.inject.Singleton.class); - static final AnnotationProxy QUALIFIER = createProxyForClasses(jakarta.inject.Qualifier.class, - () -> javax.inject.Qualifier.class); - - static final AnnotationProxy PRE_DESTROY = createProxyForClasses(jakarta.annotation.PreDestroy.class, - () -> javax.annotation.PreDestroy.class); - public static final AnnotationProxy POST_CONSTRUCT = createProxyForClasses(jakarta.annotation.PostConstruct.class, - () -> javax.annotation.PostConstruct.class); - - static final AnnotationProxy OPTIONAL = createProxyForClasses(org.eclipse.e4.core.di.annotations.Optional.class, - null); - - private static AnnotationProxy createProxyForClasses(Class jakartaAnnotationClass, - Supplier> javaxAnnotationClass) { - List> classes = getAvailableClasses(jakartaAnnotationClass, javaxAnnotationClass); - @SuppressWarnings({ "rawtypes", "unchecked" }) - List> annotationClasses = (List) classes; - return new AnnotationProxy(annotationClasses); + static final AnnotationProxy INJECT = createProxyForClasses("jakarta.inject.Inject", //$NON-NLS-1$ + "javax.inject.Inject"); //$NON-NLS-1$ + static final AnnotationProxy SINGLETON = createProxyForClasses("jakarta.inject.Singleton", //$NON-NLS-1$ + "javax.inject.Singleton"); //$NON-NLS-1$ + static final AnnotationProxy QUALIFIER = createProxyForClasses("jakarta.inject.Qualifier", //$NON-NLS-1$ + "javax.inject.Qualifier"); //$NON-NLS-1$ + + static final AnnotationProxy PRE_DESTROY = createProxyForClasses("jakarta.annotation.PreDestroy", //$NON-NLS-1$ + "javax.annotation.PreDestroy"); //$NON-NLS-1$ + public static final AnnotationProxy POST_CONSTRUCT = createProxyForClasses("jakarta.annotation.PostConstruct", //$NON-NLS-1$ + "javax.annotation.PostConstruct"); //$NON-NLS-1$ + + static final AnnotationProxy OPTIONAL = createProxyForClasses("org.eclipse.e4.core.di.annotations.Optional", null); //$NON-NLS-1$ + + private static AnnotationProxy createProxyForClasses(String jakartaAnnotationClass, + String javaxAnnotationClass) { + return new AnnotationProxy(getAvailableClasses(jakartaAnnotationClass, javaxAnnotationClass)); } - private static final List> PROVIDER_TYPES = getAvailableClasses(jakarta.inject.Provider.class, - () -> javax.inject.Provider.class); + private static final Set PROVIDER_TYPES = Set + .copyOf(getAvailableClasses("jakarta.inject.Provider", "javax.inject.Provider")); //$NON-NLS-1$//$NON-NLS-2$ static boolean isProvider(Type type) { - for (Class clazz : PROVIDER_TYPES) { - if (clazz.equals(type)) { - return true; - } - } - return false; + return PROVIDER_TYPES.contains(type.getTypeName()); } - @FunctionalInterface - private interface ProviderFactory { - Object create(IObjectDescriptor descriptor, IInjector injector, PrimaryObjectSupplier provider); - } + private static final Map, MethodHandle> PROVIDER_FACTORYS = new ConcurrentHashMap<>(); - private static final ProviderFactory PROVIDER_FACTORY; - static { - ProviderFactory factory; - try { - /** - * This subclass solely exists for the purpose to not require the presence of - * the javax.inject.Provider interface in the runtime when the base-class is - * loaded. This can be deleted when support for javax is removed form the - * E4-injector. - */ - class JavaxCompatibilityProviderImpl extends ProviderImpl implements javax.inject.Provider { - public JavaxCompatibilityProviderImpl(IObjectDescriptor descriptor, IInjector injector, - PrimaryObjectSupplier provider) { - super(descriptor, injector, provider); - } + public static Object getProvider(IObjectDescriptor descriptor, IInjector injector, PrimaryObjectSupplier provider) { + + Supplier genericProvider = () -> ((InjectorImpl) injector).makeFromProvider(descriptor, provider); + + Class providerClass; + if ((descriptor.getDesiredType() instanceof ParameterizedType parameterizedType + && parameterizedType.getRawType() instanceof Class clazz)) { + providerClass = clazz; + } else { + throw new IllegalStateException(); // The caller must ensure the providerClass can be extracted + } + // At runtime dynamically create a method-reference that implements the specific + // providerClass 'foo.bar.Provider': + // (foo.bar.Provider) genericProvider::get + MethodHandle factory = PROVIDER_FACTORYS.computeIfAbsent(providerClass, providerType -> { + try { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + MethodType suppliedType = MethodType.methodType(Object.class); + CallSite callSite = LambdaMetafactory.metafactory(lookup, "get", //$NON-NLS-1$ + MethodType.methodType(providerClass, Supplier.class), suppliedType.erase(), // + lookup.findVirtual(Supplier.class, "get", MethodType.methodType(Object.class)), //$NON-NLS-1$ + suppliedType); + return callSite.getTarget(); + } catch (Exception e) { + throw new IllegalStateException(e); } - factory = JavaxCompatibilityProviderImpl::new; - // Attempt to load the class early in order to enforce an early class-loading - // and to be able to handle the NoClassDefFoundError below in case - // javax-Provider is not available in the runtime: - factory.create(null, null, null); - } catch (NoClassDefFoundError e) { - factory = ProviderImpl::new; + }); + try { + Object providerImpl = factory.bindTo(genericProvider).invoke(); + return providerClass.cast(providerImpl); + } catch (Throwable e) { + throw new IllegalStateException(e); } - PROVIDER_FACTORY = factory; - } - - public static Object getProvider(IObjectDescriptor descriptor, IInjector injector, PrimaryObjectSupplier provider) { - return PROVIDER_FACTORY.create(descriptor, injector, provider); } public static String getQualifierValue(IObjectDescriptor descriptor) { - var annotations = NAMED_ANNOTATION2VALUE_GETTER.entrySet(); - for (Entry, Function> entry : annotations) { - Class annotationClass = entry.getKey(); - if (descriptor.hasQualifier(annotationClass)) { - Annotation namedAnnotation = descriptor.getQualifier(annotationClass); - return entry.getValue().apply(namedAnnotation); + Annotation[] qualifiers = descriptor.getQualifiers(); + if (qualifiers != null) { + for (Annotation namedAnnotation : qualifiers) { + Class annotationType = namedAnnotation.annotationType(); + if (NAMED_ANNOTATION_CLASSES.contains(annotationType.getName())) { + return namedAnnotationValueGetter(annotationType).apply(namedAnnotation); + } } } return null; } - private static final Map, Function> NAMED_ANNOTATION2VALUE_GETTER; - - static { - Map, Function> annotation2valueGetter = new HashMap<>(); - annotation2valueGetter.put(jakarta.inject.Named.class, a -> ((jakarta.inject.Named) a).value()); - loadJavaxClass( - () -> annotation2valueGetter.put(javax.inject.Named.class, a -> ((javax.inject.Named) a).value())); - NAMED_ANNOTATION2VALUE_GETTER = Map.copyOf(annotation2valueGetter); + private static Function namedAnnotationValueGetter( + Class annotationType) { + return NAMED_ANNOTATION2VALUE_GETTER2.computeIfAbsent(annotationType, type -> { + try { + // At runtime dynamically create the method-reference: 'foo.bar.Named::value' + // where 'foo.bar.Named' is the passed specific annotationType. Invoking the + // returned Function built from the method reference is much faster than using + // reflection. + MethodHandles.Lookup lookup = MethodHandles.lookup(); + MethodType functionApplySignature = MethodType.methodType(String.class, type); + CallSite site = LambdaMetafactory.metafactory(lookup, "apply", //$NON-NLS-1$ + MethodType.methodType(Function.class), functionApplySignature.erase(), + lookup.findVirtual(type, "value", MethodType.methodType(String.class)), //$NON-NLS-1$ + functionApplySignature); + return (Function) site.getTarget().invokeExact(); + } catch (Throwable e) { + throw new IllegalStateException(e); + } + }); } - private static List> getAvailableClasses(Class jakartaClass, Supplier> javaxClass) { - List> classes = new ArrayList<>(); - classes.add(jakartaClass); - if (javaxClass != null) { - loadJavaxClass(() -> classes.add(javaxClass.get())); - } - return classes; + private static final Map, Function> NAMED_ANNOTATION2VALUE_GETTER2 = new ConcurrentHashMap<>(); + private static final Set NAMED_ANNOTATION_CLASSES = Set.of("jakarta.inject.Named", "javax.inject.Named"); //$NON-NLS-1$//$NON-NLS-2$ + // TODO: warn about the javax-class? + + private static List getAvailableClasses(String jakartaClass, String javaxClass) { + return javaxClass != null && canLoadJavaxClass(javaxClass) // + ? List.of(jakartaClass, javaxClass) + : List.of(jakartaClass); } private static boolean javaxWarningPrinted = false; - private static void loadJavaxClass(Runnable run) { + private static boolean canLoadJavaxClass(String className) { try { if (!getSystemPropertyFlag("eclipse.e4.inject.javax.disabled", false)) { //$NON-NLS-1$ - run.run(); + Class.forName(className); // fails if absent if (!javaxWarningPrinted) { if (getSystemPropertyFlag("eclipse.e4.inject.javax.warning", true)) { //$NON-NLS-1$ @SuppressWarnings("nls") @@ -179,10 +191,12 @@ private static void loadJavaxClass(Runnable run) { } javaxWarningPrinted = true; } + return true; } - } catch (NoClassDefFoundError e) { + } catch (NoClassDefFoundError | ClassNotFoundException e) { // Ignore exception: javax-annotation seems to be unavailable in the runtime } + return false; } private static boolean getSystemPropertyFlag(String key, boolean defaultValue) { diff --git a/runtime/bundles/org.eclipse.e4.core.di/src/org/eclipse/e4/core/internal/di/InjectorImpl.java b/runtime/bundles/org.eclipse.e4.core.di/src/org/eclipse/e4/core/internal/di/InjectorImpl.java index 9ea475e1ee6..b56c5937549 100644 --- a/runtime/bundles/org.eclipse.e4.core.di/src/org/eclipse/e4/core/internal/di/InjectorImpl.java +++ b/runtime/bundles/org.eclipse.e4.core.di/src/org/eclipse/e4/core/internal/di/InjectorImpl.java @@ -24,7 +24,6 @@ import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.security.CodeSource; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -37,7 +36,6 @@ import java.util.Map; import java.util.Set; import java.util.WeakHashMap; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.e4.core.di.IBinding; import org.eclipse.e4.core.di.IInjector; @@ -50,8 +48,6 @@ import org.eclipse.e4.core.di.suppliers.PrimaryObjectSupplier; import org.eclipse.e4.core.internal.di.AnnotationLookup.AnnotationProxy; import org.eclipse.e4.core.internal.di.osgi.LogHelper; -import org.osgi.framework.Bundle; -import org.osgi.framework.FrameworkUtil; /** * Reflection-based dependency injector. @@ -330,7 +326,7 @@ public Object makeFromProvider(IObjectDescriptor descriptor, PrimaryObjectSuppli Binding binding = findBinding(descriptor); Class implementationClass; if (binding == null) - implementationClass = getProviderType(descriptor.getDesiredType()); + implementationClass = getProvidedType(descriptor.getDesiredType()); else implementationClass = binding.getImplementationClass(); if (objectSupplier != null) { @@ -486,7 +482,7 @@ private Object[] resolveArgs(Requestor requestor, PrimaryObjectSupplier objec // 1) check if we have a Provider for (int i = 0; i < actualArgs.length; i++) { - Class providerClass = getProviderType(descriptors[i].getDesiredType()); + Class providerClass = getProvidedType(descriptors[i].getDesiredType()); if (providerClass == null) { continue; } @@ -837,14 +833,14 @@ private Class getDesiredClass(Type desiredType) { /** * Returns null if not a provider */ - private Class getProviderType(Type type) { - if (!(type instanceof ParameterizedType)) + private Class getProvidedType(Type type) { + if (!(type instanceof ParameterizedType parameterizedType)) return null; - Type rawType = ((ParameterizedType) type).getRawType(); + Type rawType = parameterizedType.getRawType(); if (!AnnotationLookup.isProvider(rawType)) { return null; } - Type[] actualTypes = ((ParameterizedType) type).getActualTypeArguments(); + Type[] actualTypes = parameterizedType.getActualTypeArguments(); if (actualTypes.length != 1) return null; if (!(actualTypes[0] instanceof Class)) @@ -883,7 +879,7 @@ public IBinding addBinding(IBinding binding) { } private Binding findBinding(IObjectDescriptor descriptor) { - Class desiredClass = getProviderType(descriptor.getDesiredType()); + Class desiredClass = getProvidedType(descriptor.getDesiredType()); if (desiredClass == null) desiredClass = getDesiredClass(descriptor.getDesiredType()); synchronized (bindings) { @@ -942,23 +938,6 @@ private void processAnnotated(AnnotationProxy annotation, Object userObject, Cla Method[] methods = getDeclaredMethods(objectClass); for (Method method : methods) { if (!isAnnotationPresent(method, annotation)) { - if (shouldDebug) { - for (Annotation a : method.getAnnotations()) { - if (annotation.classes().stream().map(Class::getName) - .anyMatch(a.annotationType().getName()::equals)) { - StringBuilder tmp = new StringBuilder(); - tmp.append("Possbible annotation mismatch: method \""); //$NON-NLS-1$ - tmp.append(method.toString()); - tmp.append("\" annotated with \""); //$NON-NLS-1$ - tmp.append(describeClass(a.annotationType())); - tmp.append("\" but was looking for \""); //$NON-NLS-1$ - tmp.append(annotation.classes().stream().map(InjectorImpl::describeClass) - .collect(Collectors.joining(System.lineSeparator() + " or "))); //$NON-NLS-1$ - tmp.append("\""); //$NON-NLS-1$ - LogHelper.logWarning(tmp.toString(), null); - } - } - } continue; } if (isOverridden(method, classHierarchy)) @@ -978,22 +957,6 @@ private void processAnnotated(AnnotationProxy annotation, Object userObject, Cla } } - /** Provide a human-meaningful description of the provided class */ - private static String describeClass(Class cl) { - Bundle b = FrameworkUtil.getBundle(cl); - if (b != null) { - return b.getSymbolicName() + ":" + b.getVersion() + ":" + cl.getName(); //$NON-NLS-1$ //$NON-NLS-2$ - } - CodeSource clazzCS = cl.getProtectionDomain().getCodeSource(); - if (clazzCS != null) { - return clazzCS.getLocation() + ">" + cl.getName(); //$NON-NLS-1$ - } - if (cl.getClassLoader() == null) { - return cl.getName() + " [via bootstrap classloader]"; //$NON-NLS-1$ - } - return cl.getName(); - } - @Override public void setDefaultSupplier(PrimaryObjectSupplier objectSupplier) { defaultSupplier = objectSupplier; diff --git a/runtime/bundles/org.eclipse.e4.core.di/src/org/eclipse/e4/core/internal/di/ProviderImpl.java b/runtime/bundles/org.eclipse.e4.core.di/src/org/eclipse/e4/core/internal/di/ProviderImpl.java deleted file mode 100644 index d137cec1c7e..00000000000 --- a/runtime/bundles/org.eclipse.e4.core.di/src/org/eclipse/e4/core/internal/di/ProviderImpl.java +++ /dev/null @@ -1,43 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2009, 2015 IBM Corporation and others. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 2.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * IBM Corporation - initial API and implementation - *******************************************************************************/ -package org.eclipse.e4.core.internal.di; - -import jakarta.inject.Provider; -import org.eclipse.e4.core.di.IInjector; -import org.eclipse.e4.core.di.suppliers.IObjectDescriptor; -import org.eclipse.e4.core.di.suppliers.PrimaryObjectSupplier; - -public class ProviderImpl implements Provider { - - final private PrimaryObjectSupplier objectProvider; - final private IObjectDescriptor objectDescriptor; - final private IInjector injector; - - public ProviderImpl(IObjectDescriptor descriptor, IInjector injector, PrimaryObjectSupplier provider) { - objectDescriptor = descriptor; - objectProvider = provider; - this.injector = injector; - } - - @Override - @SuppressWarnings("unchecked") - public T get() { - try { - return (T) ((InjectorImpl) injector).makeFromProvider(objectDescriptor, objectProvider); - } catch (ClassCastException e) { - return null; - } - } - -}