项目作者: ryeash

项目描述 :
Dependency injection framework based on javax.inject
高级语言: Java
项目地址: git://github.com/ryeash/assist.git
创建时间: 2018-02-28T02:38:42Z
项目社区:https://github.com/ryeash/assist

开源协议:Apache License 2.0

下载


Build Status
Coverage Status

assist

Lightweight dependency injection framework based on javax.inject.

Basics

In its simplest form:

  1. Assist assist = new Assist();

Assist can be used to inject instances of qualifying classes. For instance:

  1. public class FrenchPress {
  2. public void brew(){
  3. System.out.println("french press brewing");
  4. }
  5. }

It is possible to get an instance of the FrenchPress class from Assist:

  1. FrenchPress fp = assist.instance(FrenchPress.class);

The Assist object will automatically create a javax.inject.Provider for FrenchPress based on the default constructor.

The @Singleton annotation can be added to FrenchPress to indicate the desired scope:

  1. @Singleton
  2. public class FrenchPress ...

In this case Assist will ensure it only ever instantiates one instance of FrenchPress:

  1. FrenchPress fp1 = assist.instance(FrenchPress.class);
  2. FrenchPress fp2 = assist.instance(FrenchPress.class);
  3. assert fp1 == fp2 // true

This is the basic function of Assist: create and inject scoped instances of classes (while adhering to the
specifications defined in the javax.inject documentation).

Application Configuration

Any real application is going to require much more complexity which means a whole bunch of configuration. Assist tries
to simplify the config down to a single Application Configuration class with annotated factory methods.

Let’s assume we have a very stupid coffee app, with the CoffeeMaker interface:

  1. public interface CoffeeMaker {
  2. void brew();
  3. }

Coincidentally the FrenchPress class from above already implements the brew() method, so let’s update it to:

  1. public class FrenchPress implements CoffeeMaker {
  2. public void brew(){
  3. System.out.println("french press brewing");
  4. }
  5. }

And now our Application Configuration class would look something like this:

  1. public class AppConfig {
  2. @Factory // makes this method eligible to be turned into a Provider
  3. @Singleton // indicates that the Provider should be Singleton scope
  4. public CoffeMaker coffeeMakerFactory(){
  5. return new FrenchPress();
  6. }
  7. }

Now we can put the pieces together:

  1. Assist assist = new Assist();
  2. assist.addConfig(AppConfig.class);
  3. CoffeeMaker cm = assist.instance(CoffeeMaker.class);
  4. // cm is the singleton instance of the FrenchPress class created by
  5. // the coffeeMakerFactory() method
  6. cm.brew(); // --> 'french press brewing'

If you want to support multiple different coffee makers you’ll have to use qualifiers:

  1. public class AppConfig {
  2. @Factory
  3. @Singleton
  4. @Named("his") // Qualifier that attaches a name to a Provider
  5. public CoffeMaker frenchPressFactory(){
  6. return new FrenchPress();
  7. }
  8. @Factory
  9. @Singleton
  10. @Named("hers")
  11. public CoffeMaker pourOverFactory(){
  12. // assuming you have another implementation of the CoffeeMaker
  13. return new PourOver();
  14. }
  15. }

Let’s say we now wanted to inject our Application class:

  1. @Singleton
  2. public class Application {
  3. @Inject // indicates Assist should set the field value
  4. @Named("his")
  5. private CoffeeMaker his;
  6. @Inject
  7. @Named("hers")
  8. private CoffeeMaker hers;
  9. @Inject // indicates Assist should call this method after instantiation
  10. public void makeEveryonesCoffee() {
  11. hers.brew(); // ladies first
  12. his.brew();
  13. }
  14. }

Just ask Assist:

  1. Assist assist = new Assist();
  2. assist.addConfig(AppConfig.class);
  3. Application app = assist.instance(Application.class);
  4. // both his and hers coffee makers will be set in app,
  5. // additionally the makeEveryonesCoffee() method will be called
  6. // because it is annotated with @Inject, resulting in an output like:
  7. // > keuring brewing
  8. // > french press brewing

@Primary

In case you have multiple qualified factory methods returning the same type,
you can mark one of them as primary:

  1. public class AppConfig {
  2. @Factory
  3. @Primary
  4. @Singleton
  5. @Named("his")
  6. public CoffeMaker coffeeMakerFactory(){
  7. return new FrenchPress();
  8. }
  9. ...
  10. }

This tells Assist that when an unqualified Provider of the type is requested,
the primary provider should be returned.

  1. CoffeeMaker cm = assist.instance(CoffeeMaker.class);
  2. assert cm instanceof FrenchPress // true

Implementation note: internally, marking a qualified provider with @Primary will cause
two providers to be registered for the factory, one with the qualifier, one without.

@Eager

Sometimes you want your @Singletons to not be so lazy. Mark the factory method with @Eager to force creation of the
instance during configuration processing.

  1. public class AppConfig {
  2. @Factory
  3. @Eager
  4. @Singleton
  5. public DAO daoFactory(){
  6. return new MySqlDAOImpl();
  7. }
  8. }

When this AppConfig is handed to an addConfig method of Assist, the provider for DAO will automatically
be called once to force creation of the singleton.

It is technically possible to mark any @Factory eager, but it probably only makes sense for @Singleton scope.

@SkipInjection

It may happen that you create a factory that returns an instance that you don’t want to implicitly inject. In this case
you can mark a factory method with @SkipInjection to prevent any @Inject marked fields and methods from being injected.

  1. @Factory
  2. @SkipInjection
  3. public CoffeeMaker skipInjectionCoffeeMakerFactory() {
  4. return new IndependentDripper();
  5. }

In this case the provider created for this factory method will not perform any injection for the returned instance.

@Lazy

In rare cases it may be necessary to inject a handle to an object before assist is ready to properly wire it; possibly
to avoid dependency or inheritance issues. To solve this, Assist can inject lazy handles to objects:

  1. public class LazilyInjected {
  2. @Inject // lazy fields still must be marked with @Inject in order to be injected
  3. @Lazy
  4. private Provider<Dog> lazyDog; // lazy injection is only allowed for Provider types
  5. ...
  6. public void wakeUp(){
  7. lazyDog.get().wakeup();
  8. }
  9. }

When this class is wired, no provider for Dog needs to be available. Assist will internally create a handle to
get the Dog instance on the first call to get(). After the first call the same instance will be returned for each
subsequent call to get().

  1. Assist assist = new Assist();
  2. // we can inject this class safely
  3. LazilyInjected li = assist.instance(LazilyInjected.class);
  4. // now add the Dog instance
  5. assist.setSingelton(Dog.class, new IrishSetter());
  6. // and now the lazy dog can be woken up
  7. li.wakeUp();

This should be a rarity and over use may be indicative of underlying architectural problems.

@Scan

Simple class path scanning is supported via the @Scan.
It will only be evaluated on application configuration classes passed into one of the addConfig methods.

  1. @Scan("com.my.base.package")
  2. public class AppConfig {
  3. ...
  4. }

When this class is given to an addConfig method of Assist, the classpath (using whatever class loader the current
thread is using) is scanned recursively for all classes under ‘com.my.base.package’ that have the @Singleton
annotation. Providers are created for all found classes, and the get() method is called for each, forcing
instantiation. If you need to search for some other annotation type, set the target:

  1. @Scan(value = "com.my.base.package", target = Endpoint.class)

@Import

The @Import annotation can be used on a configuration class
to automatically include other configuration classes during processing:

First, define your persistence specific configuration class:

  1. public class PersistenceConfig {
  2. @Factory
  3. @Singleton
  4. public EntityManager jdbc(){
  5. EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("persistence");
  6. return entityManagerFactory.createEntityManager();
  7. }
  8. }

Then, define your main application config class, including an @Import for PersistenceConfig:

  1. @Import(PersistenceConfig.class)
  2. public class AppConfig {
  3. ...
  4. }

On startup, register AppConfig and PersistenceConfig will be included during processing:

  1. assist.addConfig(AppConfig.class); // your EntityManager will be available for injection

Note: Imported classes are processed before the class being registered.

@Aspects

Aspect Oriented Programming (AOP) is supported on @Factory methods with the use of the
@Aspects annotation.
You can, for example, add a Logging aspect to a provided instance:

First define your aspect:

  1. public class LoggingAspect implements BeforeMethod, AfterMethod {
  2. private Logger log;
  3. @Override
  4. public void init(Object instance) {
  5. this.log = LoggerFactory.getLogger(instance.getClass());
  6. }
  7. // before every method invocation, log a message
  8. @Override
  9. public void before(Invocation invocation) {
  10. log.info("entering {}", invocation);
  11. }
  12. // after every method invocation, log a message
  13. @Override
  14. public void after(Invocation invocation) {
  15. log.info("exiting {}", invocation);
  16. }
  17. }

Then apply the aspect to a factory method using @Aspects(LoggingAspect.class):

  1. @Factory
  2. @Named("aspect1")
  3. @Aspects(LoggingAspect.class)
  4. public CoffeeMaker aopFrenchPress1() {
  5. return new FrenchPress();
  6. }

Multiple aspects can be used on a single target:

  1. @Factory
  2. @Named("aspect2")
  3. @Singleton
  4. @Aspects({LoggingAspect.class, TimingAspect.class})
  5. public CoffeeMaker aopFrenchPress2() {
  6. return new FrenchPress();
  7. }

Keep in mind when using multiple aspects only one InvokeMethod
implementation can be defined in the list of aspects, if multiple are listed an error will be thrown during config
processing.

Before and after aspects will be called in the order they are defined.

Assist uses java.lang.reflect.Proxy to join the aspect classes with the provided types and as such the @Aspects
annotation is only usable on methods that return an interface type.

@Property

Assist has built-in property support using the @Property and
ConfigurationFacade classes.

To use the @Property annotation, an unqualified ConfigurationFacade must be made available for injection. For example,
via a @Factory method in an application configuration class:

  1. @Factory
  2. @Singleton
  3. public ConfigurationFacade configurationFacadeFactory() {
  4. // ordering of sources is relevant:
  5. // sources will be polled for properties in the order they were
  6. // added, and the first non-empty value from a source will be returned
  7. return ConfigurationFacade.build()
  8. .system() // get values from System.getProperty(...)
  9. .file("../conf/app-override.properties")
  10. .file("../conf/app.properties")
  11. .enableEnvironments()
  12. .enableCaching()
  13. .enableMacros()
  14. .finish();
  15. }

Using the @Property annotation without an available ConfigurationFacade will cause RuntimeExceptions during injection.

With the ConfigurationFacade created and available, fields and parameters can be injected from configuration sources.

  1. @Singleton
  2. public class SomeComponent {
  3. @Property("string")
  4. private String str;
  5. @Inject // the @Inject annotation is optional when @Property is present
  6. @Property("boolean")
  7. public Boolean bool; // any type with a static valueOf(String) method is supported
  8. @Property("integer")
  9. public int integer;
  10. @Property("numbers.list")
  11. public List<Double> numbers; // List, Sets, SortedSets
  12. @Inject
  13. private void setProps(@Property("enum") SomeEnum someEnum){
  14. // ...
  15. }
  16. // if you just want to use the ConfigurationFacade directly
  17. @Inject
  18. private void configureIt(ConfigurationFacade conf){
  19. // ... configure it ...
  20. }
  21. }

By default, properties are required, a RuntimeException will be thrown if a value can not be found
during injection. Individual properties can be made optional by setting required=false, e.g.:
@Property(value = "some.property", required = false)

@Scheduled

Methods in injected instances are schedulable using the @Scheduled annotation. Internally, a ScheduledExecutorService is
used to manage the scheduling and execution of the tasks. The executor must be made available to the Assist instance
performing the injection.

For example, we can configure the executor in a factory method:

  1. public class AppConfig {
  2. @Factory
  3. @Singleton // it is highly recommended that the executor be made a singleton
  4. public ScheduledExecutorService scheduledExecutorServiceFactory() {
  5. return Executors.newSingleThreadScheduledExecutor();
  6. }
  7. }

Using the @Scheduled annotation, set a method to be scheduled by Assist.

  1. public class ObjectWithATask {
  2. @Scheduled(name = "pointless-task", type = FIXED_RATE, period = 3, unit = TimeUnit.SECONDS, executions = 4)
  3. private void myScheduledTask(CoffeeMaker cm) {
  4. log.info("running task");
  5. }
  6. }

Now wire an instance and the task will be scheduled.

  1. Assist assist = new Assist();
  2. assist.addConfig(AppConfig.class);
  3. assist.instance(ObjectWithATask.class);

See the documentation in @Scheduled for more details.

Explicit Implementation Definition

It is possible to define the implementation of interfaces/abstract
classes directly, rather than using an application configuration class.

  1. Assist assist = new Assist();
  2. // this declares that for the CoffeeMaker interface, we want to use the PourOver class.
  3. assist.addImplementingClass(CoffeeMaker.class, PourOver.class);
  4. // we now have a provider for CoffeeMaker
  5. assert assist.hasProvider(CoffeeMaker.class);
  6. // and as expected when we get a CoffeeMaker instance, it's a PourOver
  7. assert assist.instance(CoffeeMaker.class).getClass() == PourOver.class;

Shutdown Container

All objects instantiated by Assist that are AutoCloseable will be tracked (using weak references) by
ShutdownContainer and when
assist.close() is called, they will be closed as appropriate.

Extensibility

ScopeFactory

Support for additional Provider scopes (beyond just Singleton) is handled with the
ScopeFactory interface.
See ThreadLocal and
ThreadLocalScopeFactory
for an example on how to create a ScopeFactory.

InstanceInterceptor

InstanceInterceptors are used (in general) to inject fields or methods of a class after an instance has been created.
For example, the InjectAnnotationInterceptor
injects the @Inject fields and methods of a class.

To add, for example, a log injector:

Define a @Log annotation:

  1. @Target(value = ElementType.FIELD)
  2. @Retention(value = RetentionPolicy.RUNTIME)
  3. @Documented
  4. public @interface Log {
  5. }

Define the interceptor:

  1. public class LogInjector implements InstanceInterceptor {
  2. @Override
  3. public void intercept(Object instance) {
  4. for (Field field : Reflector.of(instance).fields()) {
  5. if(field.isAnnotationPresent(Log.class)){
  6. field.set(instance, LoggerFactory.getLogger(instance.getClass()));
  7. }
  8. }
  9. }
  10. }

Register the instance interceptor:

  1. assist.register(new LogInjector());

Now, any time a Provider creates an object instance with fields annotated with @Log, those fields will be set to a Logger
created using the Slf4j LoggerFactory.

ValueLookup

Custom handling of @Inject fields and methods can be performed by adding additional
ValueLookup implementations to the
assist instance. A ValueLookup is tasked with finding the value that should be used to set an @Inject marked Field
or an @Inject marked method’s Parameter values. During injection processing Assist will iterate through all
registered ValueLookups (in prioritized order) until one of them returns a non-null value for the target.

A new ValueLookup can be registered with:

  1. assist.register(new CustomValueLookup());

ProviderWrapper

Perhaps the most powerful means of extensibility is to define and register new
ProviderWrappers. ProviderWrappers are used as their name implies:
to wrap a provider. This allows any arbitrary logic to be performed on either the provider or the instance that it provides.
Examples include the AspectWrapper and the
ScopeWrapper.

For example, creating a wrapper that adds caching to a provider (which would probably be better implemented as a scope,
but just for the sake of a simple example):

  1. public class CachingWrapper implements ProviderWrapper {
  2. @Override
  3. public <T> AssistProvider<T> wrap(AssistProvider<T> provider) {
  4. return new CachedProvider<>(provider);
  5. }
  6. @Override
  7. public int priority() {
  8. return 60000; // it is important to define a sane priority
  9. }
  10. public static class CachedProvider<T> extends AssistProviderWrapper<T> {
  11. private T cached;
  12. private long cachedTime = -1L;
  13. private long cacheExpiration = 15000;
  14. protected CachedProvider(AssistProvider<T> delegate) {
  15. super(delegate);
  16. }
  17. @Override
  18. public T get() {
  19. if (cached == null || (System.currentTimeMillis() > cachedTime + cacheExpiration)) {
  20. cached = super.get();
  21. }
  22. return cached;
  23. }
  24. }
  25. }

Prioritized

The configuration related interfaces in the assist module all implement the Prioritized interface. This provides a way to
control the order of execution for InstanceInterceptors and ValueLookups. The default priority
is 1000. There’s no simple rule for what priority should be set to for custom configurations, but you can use
assist.toString() to get a diagnostic printout of what Assist has registered and the order of execution.

Best Practices

Configure your Assist instance (just one) as early as possible in the main thread (ideally it’s the very first thing that happens).
It’s much better to have your app fail early and kill the JVM than at some random point down the road when it tries to
configure a provider in a request thread and it leaves everything in a walking wounded state.