All Downloads are FREE. Search and download functionalities are using the official Maven repository.

daggerok.context.DaggerokContext Maven / Gradle / Ivy

The newest version!
package daggerok.context;

import daggerok.context.Exceptions.BeanNotFoundException;
import daggerok.context.Exceptions.CreateNewInstanceException;
import daggerok.context.Finders.FinderBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

import static daggerok.context.Requires.requireNonNull;

/**
 * High overview of DaggerokContext public API:
 * 

* * Entry point: create uninitialized context using: * {@link DaggerokContext#create()} * {@link DaggerokContext#create(Class...)} * {@link DaggerokContext#create(Package...)} * {@link DaggerokContext#create(String...)} *

* * User configurations: * {@link DaggerokContext#withBasePackageClasses(Class...)} * {@link DaggerokContext#withBasePackageNames(String...)} * {@link DaggerokContext#withBasePackages(Package...)} * {@link DaggerokContext#withComponents(Class)} * {@link DaggerokContext#withInjectors(Class)} * {@link DaggerokContext#failOnInjectNullRef(boolean)} * {@link DaggerokContext#failOnBeanCreationError(boolean)} * {@link DaggerokContext#failOnUnknownReflectionsErrors(boolean)} *

* * Manual beans registration: * {@link DaggerokContext#register(String, Object)} * {@link DaggerokContext#register(Class, Object)} *

* * Search, create and inject everything we can: * {@link DaggerokContext#initialize()} *

* * Get bean from context - could be used before initialize() if bean was previously manually added: * {@link DaggerokContext#getBean(Class)} * {@link DaggerokContext#getBean(String, Class)} * {@link DaggerokContext#getBean(String)} */ public class DaggerokContext extends ConcurrentHashMap> { private static final Logger log = LoggerFactory.getLogger(DaggerokContext.class); private final List basePackages = new ArrayList(); private final ConcurrentHashMap beans = new ConcurrentHashMap(); private Class injectAnnotation = Inject.class; private Class componentAnnotation = Singleton.class; private boolean failOnInjectNullRef = false; private boolean failOnBeanCreationError = false; private boolean failOnUnknownReflectionsErrors = false; /* public API */ /* context creation */ /** * Step 1: Initialize default application context. * * final DaggerokContext applicationContext = DaggerokContext.create(); * * This will create empty application context with only 1 Bean: itself registered DaggerokContext * * @return context initialization. */ public static DaggerokContext create() { return create(DaggerokContext.class); } /** * Step 1: Initialize application context by application classes. * * final DaggerokContext applicationContext = DaggerokContext.create(App.class); * * @param baseClasses optional additional sources. * @return context initialization. */ public static DaggerokContext create(final Class... baseClasses) { return new DaggerokContext(baseClasses); } /** * Step 1: Initialize application context by packages. * * final DaggerokContext applicationContext = DaggerokContext.create( * App1.class.getPackage(), App2.class.getPackage() * ); * * @param basePackages base packages for annotation / beans scan. see Class.getPackage(), * usually it's package of application class with main method. * @return context initialization. */ public static DaggerokContext create(final Package... basePackages) { return new DaggerokContext(basePackages); } /** * Step 1: Initialize application context by package names. * * final DaggerokContext applicationContext = DaggerokContext.create( * "my.app.package1", "my.app.package2", "my.app.package3" * ); * * @param basePackages base packages for annotation / beans scan. * @return context initialization. */ public static DaggerokContext create(final String... basePackages) { return new DaggerokContext(basePackages); } /* context configuration */ /** * Step 2: (Optional / if created default context) Add base ckasses to get packages for injections scan. * * @param baseClasses base classes to get it's packages to components and injections scan. * @return context initialization. */ public DaggerokContext withBasePackageClasses(final Class... baseClasses) { requireNonNull(baseClasses, "base package classes"); if (0 == baseClasses.length) return this; final Package[] packages = new Package[baseClasses.length]; for (int i = 0; i < baseClasses.length; i++) { final Class aClass = baseClasses[i]; requireNonNull(aClass, "a package class"); packages[i] = aClass.getPackage(); } withBasePackages(packages); return this; } /** * Step 2: (Optional / if created default context) Add base package names for injections scan. * * @param basePackageNames base packages to components and injections scan. * @return context initialization. */ public DaggerokContext withBasePackageNames(final String... basePackageNames) { requireNonNull(basePackageNames, "base package"); if (0 == basePackageNames.length) return this; final ArrayList packages = new ArrayList(); for (final String basePackage : basePackageNames) { if ("".equals(basePackage)) log.warn( "Detected empty base package! We don't recommend use all included empty packages. It can decrease " + "performance dramatically and cause some unknown errors depends on 3rd party libraries your are " + "using. Please, create context with proper base package for scan including your application " + "components only" ); if (null != basePackage) packages.add(basePackage); } if (packages.size() > 0) this.basePackages.addAll(packages); return this; } /** * Step 2: (Optional / if created default context) Add packages for injections scan. * * @param basePackages base packages to components and injections scan. * @return context initialization. */ public DaggerokContext withBasePackages(final Package... basePackages) { requireNonNull(basePackages, "base packages"); if (0 == basePackages.length) return this; final String[] packageNames = new String[basePackages.length]; for (int i = 0; i < basePackages.length; i++) { final Package aPackage = basePackages[i]; requireNonNull(aPackage, "a package"); packageNames[i] = aPackage.getName(); } withBasePackageNames(packageNames); return this; } /** * Step 2: Optionally configure component annotation. Default: {@link Singleton} * * Must be performed before applicationContext.initialize() * * applicationContext.withComponents(Singleton.class); * * @param componentAnnotation user specific components annotation. * @param could be any annotation. * @return context initialization. Throw {@link NullPointerException} if passed component annotation is null. */ public DaggerokContext withComponents(final Class componentAnnotation) { requireNonNull(componentAnnotation, "component annotation"); this.componentAnnotation = componentAnnotation; return this; } /** * Step 2: Optionally configure injector annotation. Default: {@link Inject} * * Must be performed before applicationContext.initialize() * * applicationContext.withInjectors(Inject.class); * * @param injectAnnotation replacements for {@link Inject} annotation sor injections scan. * @param could be any annotation. * @return context initialization. Throw {@link NullPointerException} if passed injector annotation is null. */ public DaggerokContext withInjectors(final Class injectAnnotation) { requireNonNull(injectAnnotation, "inject annotation"); this.injectAnnotation = injectAnnotation; return this; } /** * Step 2: Optionally configure if application context bootstrap must fail on any null injection. * * Must be performed before applicationContext.initialize(), * Can be useful for debug. * * @param failOnInjectNullRef if set to true, application will fail on any null @{@link Inject}s. Default: false. * @return context configuration. */ public DaggerokContext failOnInjectNullRef(final boolean failOnInjectNullRef) { this.failOnInjectNullRef = failOnInjectNullRef; return this; } /** * Step 2: Optionally configure if application context bootstrap must fail on bean creation new instance errors. * Default: false. * * Must be performed before applicationContext.initialize(), * We believe you don't need enable it, unless debug purposes. * * @param failOnBeanCreationError if set to true, application will fail on any new beans creations errors. * Default: false. * @return context configuration. */ public DaggerokContext failOnBeanCreationError(final boolean failOnBeanCreationError) { this.failOnBeanCreationError = failOnBeanCreationError; return this; } /** * Step 2: Optionally configure if application context bootstrap must fail on unknown possible exceptions catches. * Default: false. * * Must be performed before applicationContext.initialize(), * Use it only if you know what you are doing and you believe you configured out everything. * * @param failOnUnknownErrors if set to true, application will fail on any null @{@link Inject}s. Default: false. * @return context configuration. */ public DaggerokContext failOnUnknownReflectionsErrors(final boolean failOnUnknownErrors) { this.failOnUnknownReflectionsErrors = failOnUnknownErrors; return this; } /* manual context registration */ /** * Step 3: Optionally in addition manually register bean by it's class. * * final MyBean myBean = new Bean("bean initialization..."); * * applicationContext.register(MyBean.class, myBean); * applicationContext.register(MyService.class, new MyService(myBean)); * * @param beanType bean class. * @param instance bean instance. * @param can any bean instance. * @return context configuration. */ public DaggerokContext register(final Class beanType, final T instance) { requireNonNull(beanType, "bean type"); register(beanType.getName(), instance); return this; } /** * Step 3: Optionally in addition manually register bean by it's full (FQDN) class name. * * package my.app; * // .... * * final MyBean myBean = new Bean("bean initialization..."); * final MyOtherBean myOtherBean = new MyOtherBean(myBean); * * applicationContext.register("my.app.MyBean", myBean); * applicationContext.register(myOtherBean.getClass().getName(), myOtherBean); * applicationContext.register(MyService.class.getClass().getName(), new MyService(myOtherBean)); * * @param beanName FQDN class name. * @param instance bean instance. * @param can any bean instance. * @return context configuration. */ public DaggerokContext register(final String beanName, final T instance) { requireNonNull(beanName, "bean name type"); requireNonNull(instance, "instance"); beans.put(beanName, instance); return this; } /* context initialization */ /** * Step 4: Context initialization. * * Scanning for components and registering beans for application context will be returned. * * @return context configuration. */ public DaggerokContext initialize() { return findAndRegisterAllBeans().register(DaggerokContext.class, this); } /* context usage */ /** * Step 5: Gets bean instance by it's class type. * * Can be used before applicationContext.initialize() only if bean you are looking for previously was manually added. * * final MyBean myBean = applicationContext.getBean(MyBean.class); * * @param type bean class type. * @param can be any registered bean: * - manually (explicitly); * - automatically (implicitly) by scanning @{@link Singleton}s with default * or public no-arg constructor in basePackages; * @return bean from context if registered otherwise null. */ public T getBean(final Class type) { requireNonNull(type, "bean type"); return getBean(type.getName()); } /** * Step 5: Gets named bean instance by it's type. * * Can be used before applicationContext.initialize() only if bean you are looking for previously was manually added. * * final MyBean myBean = applicationContext.getBean(MyBean.class); * * @param bean type * @param type bean class * @param name full (FQDN) class name. Can be any registered bean: * - manually (explicitly); * - automatically (implicitly) by scanning @{@link Singleton}s with default * or public no-arg constructor in basePackages; * @return bean from context if registered otherwise null. */ public T getBean(final String name, final Class type) { requireNonNull(name, "bean name"); return type.cast(beans.get(name)); } /** * Step 5: Gets named bean instance (unsafe ). * * Can be used before applicationContext.initialize() only if bean you are looking for previously was manually added. * * final MyBean myBean = applicationContext.getBean(MyBean.class); * * @param bean type * @param typeName full (FQDN) class name. Can be any registered bean: * - manually (explicitly); * - automatically (implicitly) by scanning @{@link Singleton}s with default * or public no-arg constructor in basePackages; * @return bean from context if registered otherwise null. */ @SuppressWarnings("unchecked") public T getBean(final String typeName) { requireNonNull(typeName, "bean name"); return (T) beans.get(typeName); } /* overrides */ @Override public int size() { return beans.size(); } /* private API */ /* construct context required components base package scan initialization */ private DaggerokContext(final C... sources) { withBasePackageClasses(sources); } private

DaggerokContext(final P... packages) { withBasePackages(packages); } private DaggerokContext(final String... packageNames) { withBasePackageNames(packageNames); } /* components base package scan initialization */ /* helpers and DRY methods */ /** * Scan for component classes and context initialization. * * @return context initialization. */ private DaggerokContext findAndRegisterAllBeans() { createNoArgComponents(); injectConstructorsInstances(); return this; } /** * Try to find and initialize all no-arg components and injector parameters into context. * * flow: * * - validate base package for beans scan * - find all no arg components annotated with @{@link Singleton} * as well as all no-arg parameters of constructor annotated with @{@link Inject} * - create new instance and put it into context * * @return DaggerokContext */ private DaggerokContext createNoArgComponents() { final List constructors = FinderBuilder .builder() .basePackages(basePackages) .componentAnnotation(componentAnnotation) .injectAnnotation(injectAnnotation) .failOnUnknownReflectionsErrors(failOnUnknownReflectionsErrors) .build() .findAllComponentsConstructorsByParameterCountAndEqual(0, true); for (final Constructor constructor : constructors) { final Class type = constructor.getDeclaringClass(); if (log.isDebugEnabled()) log.debug("injecting {}...", type); injectAndRegister(type, constructor); } return this; } /** * Try to find and initialize all beans and injectors with retry = O(n^2). * * flow: * * - find all constructors annotated with @Inject with args > 0 * - create tree map with key: nr of constructor arguments and value: non resolved beans classes set * - for each entry in map get key (nr of constructor args) and unresolved set * - create injectorsLeft counter equal to unresolved set size and loopedCounter equal to injectorsLeft * - for each unresolved set item try: * - find bean in context * - if bean vas found: * - remove class from unresolved set * - decrease injectorsLeft counter * - otherwise try get all parameters for newInstance call * - if all parameters exists (non null): * - create instance with constructor injection * - put it in context * - remove class from unresolved set * - decrease injectorsLeft counter * - if injectorsLeft zero break to next entry * - else if injectorsLeft == loopedCounter: * - throw error and stop context bootstrap * - or skip to next entry depends on fail-on condition configurations * * @return context initialization. */ private DaggerokContext injectConstructorsInstances() { final List injects = findParametrizedInjectConstructors(); final TreeMap> toBeInitialized = getInjectorsMap(injects); final AtomicInteger beansLeft = new AtomicInteger(countTotalItemsValues(toBeInitialized)); final AtomicInteger retry = new AtomicInteger(beansLeft.get()); while (beansLeft.get() > 0 && retry.get() > 0) { for (final Map.Entry> parametersCountToInjectors : toBeInitialized.entrySet()) { final AtomicInteger beforeCounter = new AtomicInteger(beansLeft.get()); final HashSet constructors = parametersCountToInjectors.getValue(); for (final Constructor constructor : constructors) { final Class type = constructor.getDeclaringClass(); if (null != getBean(type)) // bean already exists in context decrementIfValid(parametersCountToInjectors, constructors, constructor, beansLeft); else { final Class[] parameterTypes = constructor.getParameterTypes(); final ArrayList params = parseParams(parameterTypes); if (params.size() != parameterTypes.length) continue; if (null != injectAndRegister(type, constructor, params.toArray())) // bean was created with injections decrementIfValid(parametersCountToInjectors, constructors, constructor, beansLeft); } } if (beansLeft.get() < 1) continue; // there are no unresolved beans left for initialization, bye-bye... if (beansLeft.get() == beforeCounter.get()) retry.decrementAndGet(); // nothing changes sins last iteration, retry-- beforeCounter.set(beansLeft.get()); } } return this; } /** * @return list of classes injectors with more than zero arguments. */ private List findParametrizedInjectConstructors() { final List injects = FinderBuilder .builder() .basePackages(basePackages) .componentAnnotation(componentAnnotation) .injectAnnotation(injectAnnotation) .failOnUnknownReflectionsErrors(failOnUnknownReflectionsErrors) .build() .findAllInjects(); final Set parametrizedConstructors = new HashSet(); for (final Constructor constructor : injects) { if (constructor.getParameterTypes().length > 0) parametrizedConstructors.add(constructor); } return new ArrayList(parametrizedConstructors); } /** * Beans dependency tree resolution. * * @param injects inject to be parsed for getting constructors. * @return sorted map by argument count to constructors set. */ private TreeMap> getInjectorsMap(final List injects) { requireNonNull(injects, "injects"); final TreeMap> unresolved = new TreeMap>(); for (final Constructor constructor : injects) { final int count = constructor.getParameterTypes().length; final HashSet container = unresolved.get(count); final HashSet constructors = null == container ? new HashSet() : container; constructors.add(constructor); unresolved.put(count, constructors); } return unresolved; } private int countTotalItemsValues(final TreeMap> map) { int total = 0; for (final Entry> item : map.entrySet()) { total += item.getValue().size(); } return total; } private void decrementIfValid(final Map.Entry> entry, final HashSet constructors, final Constructor constructor, final AtomicInteger counter) { final HashSet updated = new HashSet(constructors); updated.remove(constructor); entry.setValue(updated); counter.decrementAndGet(); } /** * @param parameterTypes bean types. * @return beans from application context according to it's type. */ private ArrayList parseParams(final Class[] parameterTypes) { final ArrayList params = new ArrayList(); for (final Class type : parameterTypes) { final Object bean = getBean(type); if (null != bean) params.add(bean); else for (final Constructor constructor : type.getConstructors()) { if (0 != constructor.getParameterTypes().length) continue; final Object instance = injectAndRegister(type, constructor); if (null == instance) continue; params.add(getBean(type)); break; } } return params; } /** * Internal: create bean instance. * Will throw {@link NullPointerException} if bean is null and failOnInjectNullRef enabled. * * @param type bean type. * @param constructor constructor to be used for bean creation. * @param params parameters to be pass in constructor. * @param could be any class. * @return registered bean or throw exception according to failOnInjectNullRef and failOnUnknownReflectionsErrors * configurations. */ private T injectAndRegister(final Class type, final Constructor constructor, final Object... params) { final Object instance = newInstance(constructor, params); final Object bean = type.cast(instance); if (null == bean) { if (failOnInjectNullRef) { final BeanNotFoundException exception = new BeanNotFoundException(type); log.error(exception.getLocalizedMessage(), exception); throw exception; } if (log.isDebugEnabled()) log.debug("Injecting bean {} and was resulted in null.", type.getName()); } register(type.getName(), bean); return getBean(type); } /** * Creates new instance using constructor and reflection. * * @param constructor constructor to be used for bean instantiation. * @param parameters constructor parameters. If not present or null - NoArgConstructor will be used. * @return new bean instance. */ @SuppressWarnings("unchecked") private Object newInstance(final Constructor constructor, final Object... parameters) { try { return constructor.newInstance(parameters); } catch (final Throwable e) { final Class type = constructor.getDeclaringClass(); if (log.isDebugEnabled()) log.debug("Creation bean {} failed: {}", type.getName(), e.getLocalizedMessage()); if (!failOnBeanCreationError) return null; final CreateNewInstanceException error = new CreateNewInstanceException(type, e.getLocalizedMessage()); log.error("Bean instance '{}' creation with parameters '{}' failed.", type.getName(), parameters, error); throw error; } } }