org.osgl.inject.Genie Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of genie Show documentation
Show all versions of genie Show documentation
A JSR330 style dependency injection solution
package org.osgl.inject;
/*-
* #%L
* OSGL Genie
* %%
* Copyright (C) 2017 OSGL (Open Source General Library)
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.osgl.$;
import org.osgl.exception.UnexpectedException;
import org.osgl.inject.annotation.*;
import org.osgl.inject.provider.*;
import org.osgl.inject.util.AnnotationUtil;
import org.osgl.inject.util.SimpleSingletonScope;
import org.osgl.logging.LogManager;
import org.osgl.logging.Logger;
import org.osgl.util.C;
import org.osgl.util.E;
import org.osgl.util.S;
import osgl.version.Version;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.inject.*;
public final class Genie implements Injector {
/**
* Describe the version of genie library.
*/
public static final Version VERSION = Version.of(Genie.class);
/**
* The logger used by genie library.
*/
static final Logger logger = LogManager.get(Genie.class);
private static final ThreadLocal TGT_SPEC = new ThreadLocal();
private static final ThreadLocal AFFINITY = new ThreadLocal();
private static final Provider BEAN_SPEC_PROVIDER = new Provider() {
@Override
public BeanSpec get() {
return TGT_SPEC.get();
}
};
/**
* A `Binder` is used in {@link Module#configure() module configure method}
* to define a custom binding.
*
* @param
* A generic type of binding object
*/
public static class Binder {
private Class type;
private String name;
private Provider extends T> provider;
private List annotations = C.newList();
private Class extends Annotation> scope;
private boolean forceFireEvent;
private boolean fireEvent;
private Constructor extends T> constructor;
private Class extends T> impl;
/**
* Create a `Binder` for specified class
*
* @param type
* the type to be bound
*/
public Binder(Class type) {
this.type = type;
this.fireEvent = true;
}
/**
* Bind this `Binder` to a specific implementation.
*
* If there is another binding already specified then it
* will throw out an {@link IllegalStateException}
*
* @param impl
* the implementation class
* @return this binder instance
* @throws IllegalStateException
* if another binding exists
*/
public Binder to(final Class extends T> impl) {
ensureNoBinding();
this.impl = $.requireNotNull(impl);
return this;
}
/**
* Bind this `Binder` to a specific instance
*
* If there is another binding already specified then it
* will throw out an {@link IllegalStateException}
*
* @param instance
* the instance to which the Binder is bound
* @return this binder instance
* @throws IllegalStateException
* if another binding exists
*/
public Binder to(final T instance) {
ensureNoBinding();
this.provider = new Provider() {
@Override
public T get() {
return instance;
}
};
return this;
}
/**
* Bind this `Binder` to a provider.
*
* If there is another binding already specified then it
* will throw out an {@link IllegalStateException}
*
* @param provider
* the provider that provides the instance to which this binder is bound
* @return this binder instance
* @throws IllegalStateException
* if another binding exists
*/
public Binder to(Provider extends T> provider) {
ensureNoBinding();
this.provider = provider;
return this;
}
/**
* Bind this binder to a constructor
*
* If there is another binding already specified then it
* will throw out an {@link IllegalStateException}
*
* @param constructor
* the constructor that generate the instance to which this binder is bound
* @return this binder instance
* @throws IllegalStateException
* if another binding exists
*/
public Binder to(final Constructor extends T> constructor) {
ensureNoBinding();
this.constructor = constructor;
return this;
}
/**
* Bind this instance to a constructor specified by class and constructor arguments.
*
* This is a convenient method for {@link #to(Constructor) constructor binding} as
* the developer does not need to provides a {@link Constructor} instance
*
* If no constructor found with the class and argument types then an
* {@link InjectException} will be thrown out
*
* If there is another binding already specified then it
* will throw out an {@link IllegalStateException}
*
* @param implement
* the class of the target instance
* @param args
* the constructor argument types
* @return this binder instance
* @throws InjectException
* if no constructor found by the spec
* @throws IllegalStateException
* if another binding exists
*/
public Binder toConstructor(Class extends T> implement, Class>... args) {
ensureNoBinding();
try {
return to(implement.getConstructor(args));
} catch (NoSuchMethodException e) {
throw new InjectException(e,
"cannot find constructor for %s with arguments: %s", implement.getName(), $.toString2(args));
}
}
private void ensureNoBinding() {
E.illegalStateIf(bound(), "binding has already been specified");
}
/**
* Constraint the binding with a name.
*
* A name is usually specified via {@link Named} annotation when declaring
* an injection. When genie looking for a provider for a named injection,
* it will check if the supplied binding matches the name specified.
*
* If there is a name already registered it will throw out an
* {@link IllegalArgumentException}
*
* @param name
* the name of the binding
* @return this `Binder` instance
* @throws IllegalArgumentException
* if there is name already registered
*/
public Binder named(String name) {
E.illegalStateIf(null != this.name, "name has already been specified");
this.name = name;
this.fireEvent = false;
return this;
}
/**
* Constraint the binder with a scope annotation class.
*
* Once the constraint is added to this binder the binding will search
* for candidates within the scope constraint only
*
* The scope annotation class must be tagged with {@link Scope} annotation.
* Otherwise a {@link InjectException} will be thrown out.
*
* @param scope
* the scope annotation class
* @return this binder instance
* @throws InjectException
* if the scope class is not annotated with {@link Scope}
* @throws IllegalStateException
* if there is already a scope annotation constraint put on this binder
* @see Scope
*/
public Binder in(Class extends Annotation> scope) {
if (!scope.isAnnotationPresent(Scope.class)) {
throw new InjectException(
"the scope annotation type must have @Scope annotation presented: " + scope.getName());
}
E.illegalStateIf(null != this.scope, "Scope has already been specified");
this.scope = scope;
this.fireEvent = false;
return this;
}
/**
* Add annotation constraints to this binder.
*
* Usually the annotation specified in the parameter should be a valid {@link Qualifier qualifiers}
*
* This method is deprecated. Please use {@link #qualifiedWith(Class[])} instead
*
* @param annotations
* an array of annotation classes
* @return this binder instance
* @see Qualifier
*/
@Deprecated
public Binder withAnnotation(Class extends Annotation>... annotations) {
for (Class extends Annotation> annotation : annotations) {
this.annotations.add(AnnotationUtil.createAnnotation(annotation));
}
this.fireEvent = false;
return this;
}
/**
* Add annotation constraints to this binder.
*
* Usually the type of the annotation specified in the parameter should be
* a valid {@link Qualifier qualifiers}
*
* This method is deprecated. Please use {@link #qualifiedWith(Annotation...)} instead
*
* @param annotations
* an array of annotation classes
* @return this binder instance
* @see Qualifier
*/
@Deprecated
public Binder withAnnotation(Annotation... annotations) {
this.annotations.addAll(C.listOf(annotations));
this.fireEvent = false;
return this;
}
/**
* Add qualifier annotation constraints to this binder
*
* Each qualifier annotation type must be tagged with {@link Qualifier} annotation.
* Otherwise an {@link InjectException} will be thrown out
*
* @param qualifiers
* an array of qualifier annotation types
* @return this binder instance
* @throws InjectException
* if the any qualifier class is not tagged with {@link Qualifier}
* @see Qualifier
*/
public Binder qualifiedWith(Class extends Annotation>... qualifiers) {
for (Class extends Annotation> qualifier : qualifiers) {
if (!qualifier.isAnnotationPresent(Qualifier.class)) {
throw new InjectException(
"Qualifier annotation type must have \"@Qualifier\" annotation presented: " +
qualifier.getName());
}
this.annotations.add(AnnotationUtil.createAnnotation(qualifier));
}
this.fireEvent = false;
return this;
}
/**
* Add qualifier annotation constraints to this binder
*
* The type of each qualifier annotation must be tagged with {@link Qualifier}
* annotation. Otherwise an {@link InjectException} will be thrown out
*
* @param qualifiers
* an array of qualifier annotations
* @return this binder instance
* @throws InjectException
* if the any qualifier's class is not tagged with {@link Qualifier}
* @see Qualifier
*/
public Binder qualifiedWith(Annotation... qualifiers) {
for (Annotation qualifier : qualifiers) {
Class extends Annotation> qulifierType = qualifier.annotationType();
if (!qulifierType.isAnnotationPresent(Qualifier.class)) {
throw new InjectException(
"Qualifier annotation type must have \"@Qualifier\" annotation presented: " +
qulifierType.getName());
}
this.annotations.add(AnnotationUtil.createAnnotation(qulifierType));
}
this.fireEvent = false;
return this;
}
/**
* Turn on `forceFireEvent` flag.
*
* Once force fire event flag is turned on, when it calls
* {@link #register(Genie)} method the
* {@link Genie#fireProviderRegisteredEvent(Class)} method
* will be called
*
* @return this binder instance
*/
public Binder forceFireEvent() {
this.forceFireEvent = true;
this.fireEvent = true;
return this;
}
/**
* Turn off the `forceFireEvent` flag.
*
* If this flag is turned off, no {@link Genie#fireProviderRegisteredEvent(Class)}
* call will happen upon {@link #register(Genie)} method is called
*
* @return this binder instance
*/
public Binder doNotFireEvent() {
this.forceFireEvent = false;
this.fireEvent = false;
return this;
}
/**
* Check if binding is setup
*
* @return `true` if binding is setup
*/
boolean bound() {
return null != provider || null != constructor;
}
/**
* Register this binder to `Genie`
*
* @param genie
* the dependency injector
*/
public void register(Genie genie) {
if (null == provider) {
if (null != constructor) {
provider = genie.buildConstructor(
constructor,
BeanSpec.of(constructor.getDeclaringClass(), null, genie),
new HashSet());
} else if (null != impl) {
provider = new LazyProvider<>(impl, genie);
}
}
if (!bound()) {
throw new InjectException("Cannot register without binding specified");
}
BeanSpec spec = beanSpec(genie);
genie.addIntoRegistry(spec, genie.decorate(spec, provider, true), annotations.isEmpty() && S.blank(name));
if (fireEvent || forceFireEvent) {
genie.fireProviderRegisteredEvent(type);
}
}
BeanSpec beanSpec(Genie genie) {
BeanSpec spec = BeanSpec.of(type, annotations.toArray(new Annotation[annotations.size()]), name, genie);
if (scope != null) {
spec.scope(scope);
}
return spec;
}
}
private static class WeightedProvider implements Provider, Comparable> {
private Provider realProvider;
private int affinity;
WeightedProvider(Provider provider) {
realProvider = provider;
affinity = AFFINITY.get();
}
@Override
public T get() {
return realProvider.get();
}
@Override
public int compareTo(WeightedProvider o) {
return this.affinity - o.affinity;
}
static WeightedProvider decorate(Provider provider) {
return provider instanceof WeightedProvider ? (WeightedProvider) provider : new WeightedProvider(provider);
}
}
private static class FieldInjector {
private final BeanSpec fieldSpec;
private final Provider provider;
FieldInjector(BeanSpec fieldSpec, Provider provider) {
this.fieldSpec = fieldSpec;
this.provider = provider;
}
void applyTo(Object bean) {
Object obj = provider.get();
if (null == obj) {
return;
}
fieldSpec.setField(bean, obj);
}
@Override
public String toString() {
return $.fmt("Field injector for: " + fieldSpec);
}
}
private static class MethodInjector {
private final Method method;
private final Provider[] providers;
MethodInjector(Method method, Provider[] providers) {
this.method = method;
this.providers = providers;
}
Object applyTo(Object bean) {
try {
return method.invoke(bean, params(providers));
} catch (Exception e) {
throw new InjectException(e, "Unable to invoke method[%s] on %s", method.getName(), bean.getClass());
}
}
@Override
public String toString() {
return $.fmt("MethodInjector for %s", method);
}
}
private ConcurrentMap> registry = new ConcurrentHashMap<>();
private ConcurrentMap, NamedProvider>> namedRegistry = new ConcurrentHashMap<>();
private ConcurrentMap expressRegistry = new ConcurrentHashMap<>();
private Set> qualifierRegistry = new HashSet<>();
private Set> injectTagRegistry = new HashSet<>();
private Map, Class extends Annotation>> scopeAliases = new HashMap<>();
private Map, ScopeCache> scopeProviders = new HashMap<>();
private ConcurrentMap, PostConstructProcessor>> postConstructProcessors =
new ConcurrentHashMap, PostConstructProcessor>>();
private ConcurrentMap beanSpecLookup = new ConcurrentHashMap<>();
private ConcurrentMap genericTypedBeanLoaders = new ConcurrentHashMap<>();
private List listeners = new ArrayList<>();
private boolean supportInjectionPoint = false;
Genie(Object... modules) {
init(false, modules);
}
Genie(boolean noPlugin, Object... modules) {
init(noPlugin, modules);
}
private Genie(InjectListener listener, Object... modules) {
this.listeners.add(listener);
init(false, modules);
}
private void init(boolean noPlugin, Object... modules) {
registerBuiltInProviders();
if (!noPlugin) {
registerBuiltInPlugins();
}
if (modules.length > 0) {
List list = new ArrayList();
for (Object module : modules) {
if (module instanceof InjectListener) {
listeners.add((InjectListener) module);
} else if (module instanceof Class) {
Class moduleClass = (Class) module;
if (InjectListener.class.isAssignableFrom(moduleClass)) {
listeners.add((InjectListener) $.newInstance(moduleClass));
}
}
list.add(module);
}
// register real modules after listener get registered
for (Object module : list) {
registerModule(module);
}
} else {
registerModule(new Module() {
@Override
protected void configure() {
bind(ScopeCache.SingletonScope.class).to(new SimpleSingletonScope());
}
});
}
initScopeAliases();
}
public void supportInjectionPoint(boolean enabled) {
this.supportInjectionPoint = enabled;
}
@Override
public T get(Class type) {
return getProvider(type).get();
}
public T get(BeanSpec beanSpec) {
Provider provider = findProvider(beanSpec, C.empty());
return (T) provider.get();
}
/**
* Check if a type has already been registered with a binding already
*
* @param type
* the class
* @return `true` if the type has already been registered to Genie with a binding
*/
public boolean hasProvider(Class> type) {
return expressRegistry.containsKey(type);
}
@Override
public Provider getProvider(Class type) {
Provider provider = expressRegistry.get(type);
if (null == provider) {
if (type.isArray()) {
provider = ArrayProvider.of(type, this);
expressRegistry.putIfAbsent(type, provider);
return (Provider) provider;
}
BeanSpec spec = beanSpecOf(type);
provider = findProvider(spec, C.empty());
if (!$.isSimpleType(type)) {
expressRegistry.putIfAbsent(type, provider);
}
}
return (Provider) provider;
}
public void registerProvider(Class super T> type, Provider extends T> provider) {
registerProvider(type, provider, true);
}
public void registerNamedProvider(Class super T> type, NamedProvider extends T> provider) {
namedRegistry.put(type, provider);
}
public void registerQualifiers(Class extends Annotation>... qualifiers) {
this.qualifierRegistry.addAll(C.listOf(qualifiers));
}
public void registerQualifiers(Collection> qualifiers) {
this.qualifierRegistry.addAll(qualifiers);
}
public void registerInjectTag(Class extends Annotation>... injectTags) {
this.injectTagRegistry.addAll(C.listOf(injectTags));
}
public void registerScopeAlias(Class extends Annotation> scopeAnnotation, Class extends Annotation> scopeAlias) {
scopeAliases.put(scopeAlias, scopeAnnotation);
}
public void registerScopeProvider(Class extends Annotation> scopeAnnotation, ScopeCache scopeCache) {
scopeProviders.put(scopeAnnotation, scopeCache);
}
public void registerScopeProvider(Class extends Annotation> scopeAnnotation, Class extends ScopeCache> scopeCacheClass) {
scopeProviders.put(scopeAnnotation, get(scopeCacheClass));
}
public void registerPostConstructProcessor(
Class extends Annotation> annoClass,
PostConstructProcessor> processor
) {
postConstructProcessors.put(annoClass, processor);
}
public void registerGenericTypedBeanLoader(Class type, GenericTypedBeanLoader loader) {
genericTypedBeanLoaders.put(type, loader);
}
@Override
public boolean isScope(Class extends Annotation> annoClass) {
if (Singleton.class == annoClass || SessionScoped.class == annoClass || RequestScoped.class == annoClass) {
return true;
}
Class extends Annotation> mapped = scopeAliases.get(annoClass);
return ((null != mapped && StopInheritedScope.class != mapped)) || scopeProviders.containsKey(annoClass);
}
@Override
public boolean isInheritedScopeStopper(Class extends Annotation> annoClass) {
if (StopInheritedScope.class == annoClass) {
return true;
}
Class extends Annotation> mapped = scopeAliases.get(annoClass);
return StopInheritedScope.class == mapped;
}
@Override
public boolean isQualifier(Class extends Annotation> annoClass) {
return qualifierRegistry.contains(annoClass) || annoClass.isAnnotationPresent(Qualifier.class);
}
@Override
public boolean isPostConstructProcessor(Class extends Annotation> annoClass) {
return postConstructProcessors.containsKey(annoClass) || annoClass.isAnnotationPresent(PostConstructProcess.class);
}
@Override
public Class extends Annotation> scopeByAlias(Class extends Annotation> alias) {
Class extends Annotation> annoType = scopeAliases.get(alias);
return null == annoType ? alias : annoType;
}
ScopeCache scopeCache(Class extends Annotation> scope) {
return scopeProviders.get(scope);
}
PostConstructProcessor postConstructProcessor(Annotation annotation) {
Class extends Annotation> annoClass = annotation.annotationType();
PostConstructProcessor processor = postConstructProcessors.get(annoClass);
if (null == processor) {
if (!annoClass.isAnnotationPresent(PostConstructProcess.class)) {
throw new UnexpectedException("Cannot find PostConstructProcessor on %s", annoClass);
}
PostConstructProcess pcp = annoClass.getAnnotation(PostConstructProcess.class);
Class extends PostConstructProcessor> cls = pcp.value();
processor = get(cls);
PostConstructProcessor p2 = postConstructProcessors.putIfAbsent(annoClass, processor);
if (null != p2) {
processor = p2;
}
}
return processor;
}
private BeanSpec beanSpecOf(Class type) {
BeanSpec spec = beanSpecLookup.get(type);
if (null == spec) {
spec = BeanSpec.of(type, this);
beanSpecLookup.putIfAbsent(type, spec);
}
return spec;
}
private void registerProvider(Class type, Provider extends T> provider, boolean fireEvent) {
AFFINITY.set(0);
bindProviderToClass(type, provider, fireEvent);
}
private void bindProviderToClass(Class> target, Provider> provider, boolean fireEvent) {
addIntoRegistry(target, provider);
AFFINITY.set(AFFINITY.get() + 1);
Class dad = target.getSuperclass();
if (null != dad && Object.class != dad) {
bindProviderToClass(dad, provider, fireEvent);
}
Class[] roles = target.getInterfaces();
if (null == roles) {
return;
}
for (Class role : roles) {
bindProviderToClass(role, provider, fireEvent);
}
if (fireEvent) {
fireProviderRegisteredEvent(target);
}
}
private void addIntoRegistry(BeanSpec spec, Provider> val, boolean addIntoExpressRegistry) {
WeightedProvider current = WeightedProvider.decorate(val);
Provider> old = registry.get(spec);
if (null == old) {
registry.put(spec, current);
if (addIntoExpressRegistry) {
expressRegistry.put(spec.rawType(), current);
}
return;
}
String newName = providerName(current.realProvider);
if (old instanceof WeightedProvider) {
WeightedProvider weightedOld = (WeightedProvider) old;
String oldName = providerName(weightedOld.realProvider);
if (weightedOld.affinity > current.affinity) {
registry.put(spec, current);
if (addIntoExpressRegistry) {
expressRegistry.put(spec.rawType(), current);
}
if (logger.isTraceEnabled()) {
logger.trace("Provider %s \n\tfor [%s] \n\tis replaced with: %s", oldName, spec, newName);
}
} else {
if (weightedOld.affinity == 0 && current.affinity == 0) {
throw new InjectException("Provider has already registered for spec: %s", spec);
} else {
if (logger.isTraceEnabled()) {
logger.trace("Provider %s \n\t for [%s] \n\t cannot be replaced with: %s", oldName, spec, newName);
}
}
}
} else {
throw E.unexpected("Provider has already registered for spec: %s", spec);
}
}
private void addIntoRegistry(Class> type, Provider> val) {
addIntoRegistry(BeanSpec.of(type, this), val, true);
}
private void registerBuiltInProviders() {
registerProvider(Collection.class, OsglListProvider.INSTANCE, false);
registerProvider(Deque.class, DequeProvider.INSTANCE, false);
registerProvider(ArrayList.class, ArrayListProvider.INSTANCE, false);
registerProvider(LinkedList.class, LinkedListProvider.INSTANCE, false);
registerProvider(C.List.class, OsglListProvider.INSTANCE, false);
registerProvider(C.Set.class, OsglSetProvider.INSTANCE, false);
registerProvider(C.Map.class, OsglMapProvider.INSTANCE, false);
registerProvider(LinkedHashMap.class, LinkedHashMapProvider.INSTANCE, false);
registerProvider(ConcurrentMap.class, ConcurrentMapProvider.INSTANCE, false);
registerProvider(SortedMap.class, SortedMapProvider.INSTANCE, false);
registerProvider(SortedSet.class, SortedSetProvider.INSTANCE, false);
}
private void registerBuiltInPlugins() {
tryRegisterPlugin("org.osgl.inject.CDIAdaptor");
tryRegisterPlugin("org.osgl.inject.GuiceAdaptor");
}
private void tryRegisterPlugin(String pluginClass) {
try {
GeniePlugin plugin = $.newInstance(pluginClass);
plugin.register(this);
} catch (Exception e) {
logger.warn(e, "error registering plug: %s", pluginClass);
} catch (NoClassDefFoundError e) {
// plugin dependency not provided, ignore it
}
}
private void initScopeAliases() {
scopeAliases.put(Singleton.class, Singleton.class);
scopeAliases.put(SessionScoped.class, SessionScoped.class);
scopeAliases.put(RequestScoped.class, RequestScoped.class);
}
private void registerModule(Object module) {
boolean isClass = module instanceof Class;
Class moduleClass = isClass ? (Class) module : module.getClass();
if (Module.class.isAssignableFrom(moduleClass)) {
if (isClass) {
module = $.newInstance(moduleClass);
isClass = false;
}
((Module) module).applyTo(this);
}
for (Method method : moduleClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(Provides.class)) {
method.setAccessible(true);
boolean isStatic = Modifier.isStatic(method.getModifiers());
if (isClass && !isStatic) {
module = $.newInstance(moduleClass);
isClass = false;
}
registerFactoryMethod(isStatic ? null : module, method);
}
}
}
private void registerFactoryMethod(final Object instance, final Method factory) {
Type retType = factory.getGenericReturnType();
Annotation[] factoryAnnotations = factory.getAnnotations();
final BeanSpec spec = BeanSpec.of(retType, factoryAnnotations, this);
final MethodInjector methodInjector = methodInjector(factory, C.empty());
addIntoRegistry(spec, decorate(spec, new Provider() {
@Override
public Object get() {
return methodInjector.applyTo(instance);
}
@Override
public String toString() {
return S.fmt("%s::%s", instance.getClass().getName(), methodInjector.method.getName());
}
}, true), factoryAnnotations.length == 0);
fireProviderRegisteredEvent(spec.rawType());
}
private Provider> findProvider(final BeanSpec spec, final Set chain) {
// try named registry
Class> rawType = spec.rawType();
final NamedProvider namedProvider = namedRegistry.get(rawType);
if (null != namedProvider && spec.hasAnnotation(Named.class)) {
final String name = spec.name();
return new Provider