Skip to content
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

Refactor scopes - Removes ApplicationScope & RequestScope in favour of "custom scopes" #134

Merged
merged 31 commits into from
Jul 21, 2021

Conversation

rbygrave
Copy link
Contributor

@rbygrave rbygrave commented Jul 19, 2021

  • Removes ApplicationScope - Migrate to BeanScope.newBuilder().build()
  • Removes RequestScope - Migrate to using custom scopes
  • Removes deprecated methods
  • Moves withSpy() withMock() methods of BeanScopeBuilder to only be available after beanScopeBuilder.forTesting()
  • Changes @InjectModule to use Class<?> for provides and requires (not strings)
  • Changes BeanScopeFactory to instead be Module and allow us to specify explicitly which modules to include in BeanScope
  • Adds support for "custom scopes", each scope generates it's own Module
  • Allows BeanScope to have a parent BeanScope

@mikehearn
Copy link

mikehearn commented Jul 19, 2021

Nice, from a quick scan through it feels like much more code was deleted than added, but without loss of expressive power.

Splits generation of old BeanScopeFactory into Module + a helper BeanFactory. This allows the use of the generated Module via BeanScopeBuilder.withModules().

This does not support:
- partial compile (need to reload meta data for custom scope modules on partial compile)
- external/requires dependencies provided by other scopes / typically the main scope
- parent/child relationship of BeanScopes (e.g. custom scope with parent of the global scope)
@rbygrave
Copy link
Contributor Author

rbygrave commented Jul 20, 2021

Yes, so far this is has the "good feeling" about it. I have just managed some initial "custom scope" support. I think I have about 3 more things to support for custom scopes.

I'm hoping that we can define a custom scope via:

import io.avaje.inject.InjectModule;
import jakarta.inject.Scope;

@Scope
@InjectModule(requires = ...)
public @interface MyCustomScope {
}

Where the @InjectModule is optional. We specify it when we have externally provided dependencies (that are not provided by another scope probably).

So at this point no extra annotation required for a custom scope. Each custom scope gets its own generated Module (and bean factory which got split out). We can then specify with BeanScopeBuilder which modules are used to create the BeanScope (looks like Guice in that regard).

The "global" modules generated from @Singleton/@Factory without any additional custom scope - are service loaded if no explicit BeanScopeBuilder.withModules(...) is used. I'm happy, this is all making good sense so far.

We should be able to support custom scope only cases. There would be no service loading used, all BeanScopes would be built with explicitly specified modules in that case.


Splits generation of old BeanScopeFactory into Module + a helper BeanFactory. This allows the use of the generated Module via BeanScopeBuilder.withModules().

This does not support:

  • partial compile (need to reload meta data for custom scope modules on partial compile)
  • external/requires dependencies provided by other scopes / typically the main scope
  • parent/child relationship of BeanScopes (e.g. custom scope with parent of the global scope)

rbygrave added 7 commits July 20, 2021 13:19
Effectively this means we read the module meta data back for custom scopes at the beginning (first compile round). For partial compile it gets mutated but we still have full meta data per module to write correct ordering etc on the last compile round.
This isn't as tight/ideal as we could hope for but ok for now.
@rbygrave
Copy link
Contributor Author

rbygrave commented Jul 20, 2021

Next steps:

  • parent/child relationship of BeanScopes

At this point we allow a custom scope to depend on anything provided by the "default" scope - effectively assuming that the custom scope will be created with the "default" scope as it's parent. At least, this is the initial plan as that allow us to get an initial release out for this. We ultimately could be tighter wrt externally provided dependencies of custom scopes (but this is probably ok).

Just need the parent / child support and then we can take stock and look / play with it.

rbygrave added 2 commits July 21, 2021 11:55
…te "BeanFactory")

Removes SimpleFactoryWriter, write the module with all the build methods together on the same class again. We can do this because we get the JavaFileObject early
 - This means our Module classes are now visible/usable in src/main
 - We write the content on last round as we only have the full ordering at that point
@rbygrave rbygrave changed the title Refactor scopes Refactor scopes - Removes ApplicationScope & RequestScope in favour of "custom scopes" Jul 21, 2021
@rbygrave
Copy link
Contributor Author

@mikehearn I have released this as version 6.5-RC0 to maven central. I don't have anything outstanding to do on it, it is ready to try out.

@rbygrave
Copy link
Contributor Author

rbygrave commented Jul 21, 2021

WRT the prior code example:

@CreateScope
annotation class MyGlobal

@CreateScope(external = [Foo::class, Bar::class], parent = MyGlobal::class)
annotation class MyRequest

@MyGlobal class A
@MyRequest class B(val a: A)

fun main() {
  val scope = MyGlobalScope.newBuilder().build()
  val requestScope = MyRequestScope.newFrom(scope).build()
}

Default scope

There is still this concept of the default scope (global scope). What this boils down to is that when we are not using custom scopes those beans/components goto a 'default' Module. When we build a BeanScope without specifying any explicit modules what happens is that is will use ServiceLoader to find and load all the 'default' Modules in the classpath/modulepath.

So we can use avaje-inject in many jars each with a 'default' Module, when we create BeanScope without specifying any explicit modules it loads all of these and wires the whole lot. So BeanScope.newBuilder().build() does this.

Now this is generally what we do when we build reusuable components or modular monoliths - we are wiring things from multiple jars into a single BeanScope.

Note that with 6.5.RC0 we don't need to use this 'default' scope. We can do everything as custom scopes.

Custom scope

Pretty similar to your original example

import jakarta.inject.Scope;

@Scope
public @interface MyGlobal {
}

Add with a dependency on a 'parent' scope and some other external dependencies - these all go into requires

package org.example;

import io.avaje.inject.InjectModule;
import jakarta.inject.Scope;

@Scope
@InjectModule(requires = {MyGlobal.class, Foo.class, Bar.class})
public @interface MyRequest {
}

So components with @MyRequest can have dependencies from anything that MyGlobal provides (plus Foo and Bar).

fun main() {
  val scope = MyGlobalScope.newBuilder().build()
  val requestScope = MyRequestScope.newFrom(scope).build()
}

Would be

BeanScope globalScope = BeanScope.newBuilder()
      .withModules(new MyGlobalModule())
      .build()

BeanScope requestScope = BeanScope.newBuilder()
      .withModules(new MyRequestModule())
      .withParent(globalScope)
      .withBean(Foo.class, foo)
      .withBean(Bar.class, bar)
      .build()

We alternatively could create it as a single scope by specifying both modules like below. avaje-inject will determine the order to run the modules based on the requires/provides (kind of Guice style):

BeanScope scope = BeanScope.newBuilder()
      .withModules(new MyGlobalModule(), new MyRequestModule())
      .withBean(Foo.class, foo)
      .withBean(Bar.class, bar)
      .build()

@rbygrave
Copy link
Contributor Author

Notes:

The generated 'default' module implements io.avaje.inject.spi.Module and has a META-INF/services/io.avaje.inject.spi.Module file to match (for service loading all the 'default' modules).

The generated 'custom' modules implement io.avaje.inject.spi.Module.Custom ... and have a META-INF/services/io.avaje.inject.spi.Module.Custom file to match but avaje-inject is not service loading the custom modules. In theory we don't need the services file for these custom scopes but they could be used if someone wanted to discover all the custom scopes in the classpath (I don't know why someone would want to do this yet, maybe it is something no one ever does).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants