avaje / avaje-inject Goto Github PK
View Code? Open in Web Editor NEWDependency injection via APT (source code generation) ala "Server-Side Dagger DI"
Home Page: https://avaje.io/inject
License: Apache License 2.0
Dependency injection via APT (source code generation) ala "Server-Side Dagger DI"
Home Page: https://avaje.io/inject
License: Apache License 2.0
Interesting looking project, I like the idea of a APT based IOC basically taking the best bits of Spring without all the cruft. Whats plans for the future?
This would enable things like filters to use a standard mechanism to be registered in order.
It would be awesome if dinject could swap objects for a test and reverert to the real object afterwards.
In my case Im trying to write Selenium usecase tests. The javalin server is allready started with all its controllers (dinject controlled singletons), but at some point I want that some controllers behave different.
@ExtendWith({SeleniumExtension.class})
public class InternalRestTest {
private final Long expectedChecksum = 101010101L;
@BeforeEach
public void beforeEach(SeleniumHelper sh) throws Exception {
sh.navigateTo("/");
}
@Test
public void test_getBundleChecksum(SeleniumHelper sh) {
InternalController internalRest = Mockito.spy(InternalController.class);
doReturn(expectedChecksum).when(internalRest).getBundleChecksum();
try (Context context = SystemContext.swapBean(internalRest)) { //<-- this would be nice
//Not working with context here but Im expecting a different behavior from allready
//started server
Long response = sh.await("internalRest.getBundleChecksum()", Long.class);
assertThat(response).isEqualTo(expectedChecksum);
}
}
}
This is the JUnit5 extension witch starts the server:
public class SeleniumExtension implements BeforeAllCallback, BeforeEachCallback, AfterEachCallback, ParameterResolver {
private WebDriver driver;
private final int port = 80;
@Override
public void beforeAll(ExtensionContext ec) throws Exception {
Application.main(null);
WebDriverManager.chromedriver().setup();
}
@Override
public void beforeEach(ExtensionContext ec) throws Exception {
System.setProperty("webdriver.chrome.silentOutput", "true");
ChromeOptions options = new ChromeOptions();
if (equalsIgnoreCase(System.getProperty("selenium.headless"), "true")) {
options.addArguments("headless");
}
driver = new ChromeDriver(options);
driver.manage().window().setSize(new Dimension(1_440, 1_050));
driver.manage().timeouts().implicitlyWait(10, SECONDS);
driver.manage().timeouts().setScriptTimeout(10, SECONDS);
}
@Override
public void afterEach(ExtensionContext ec) throws Exception {
driver.quit();
}
@Override
public boolean supportsParameter(ParameterContext pc, ExtensionContext ec) throws ParameterResolutionException {
return pc.getParameter().getType() == SeleniumHelper.class;
}
@Override
public Object resolveParameter(ParameterContext pc, ExtensionContext ec) throws ParameterResolutionException {
return new SeleniumHelper(driver, port);
}
}
And this is the main method called in the extension
public class Application {
public static void main(String[] args) {
InternalController internalController = getBean(InternalController .class);
Security securityController = getBean(Security.class);
Javalin javalin = Javalin.create()
.disableStartupBanner()
.enableCaseSensitiveUrls()
.enableStaticFiles("/static")
.accessManager(securityController)
.start(80);
javalin.post("/login", securityController::login, ANONYMOUS.asSet());
javalin.get("/logout", securityController::logout, AUTHENTICATED.asSet());
javalin.register(internalController);
}
}
Related to #28
This issue came up due to the use of lombok.Data
annotation. This change will ignore all lombok annotations.
These are used as tie breakers when multiple beans implement the same interface and a single bean is being injected.
@Primary
makes it the preferred bean to inject
@Secondary
makes it the least preferred bean to inject - will only be used if no other bean is available to inject. This makes it a kind of default
bean used for injection when no other bean is available to inject.
This is primarily added so that we can for example wire controllers for Javalin using BeanContext so that tests can wire test doubles / mocks for controllers and their dependencies during testing.
SystemContext.getBeanContext() provides the "normal" BeanContext when running the application but equally tests can provide an BeanContext with appropriate test doubles for the test.
In my controller, I am posting List of a Pojo. The route that is created is of ctx.bodyAsClass(List.class). Hence, it doesn't deserialize the Pojos as the values are in LinkedHashMap.
java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to <Pojo>
Currently I am accepting the data as Object and then converting it using Gson to the appropriate Collection of Pojos that the Api requires.
Refer to avaje/avaje-http#25
(Version 1.7)
Dependencies of spies are not injected:
BootContext bootContext = new BootContext();
bootContext.withSpy(AppleService.class);
BeanContext beanContext = bootContext.load();
AppleService appleServiceSpy = beanContext.getBean(AppleService.class);
doNothing()
.when(appleServiceSpy)
.foo(anyString(), anyString(), anyString());
System.out.println(appleServiceSpy);
System.out.println(appleServiceSpy.bananaService); //<- is null but should not
System.out.println(appleServiceSpy.peachService); //<- is null but should not
I've made a simple test project to reproduce:
https://github.com/yaskor/dinject-fruit
First of all, great library.
Only thing missing (for me) is @transactional.
One could define a DataSource and a TransactionManager and dinject would inject the transaction code into the annotated methods.
I have allready written something with an functional aproch like:
transactionManager.doInTransaction(()->{
foo1();
foo2();
})
But a annotation would be much nicer...
@Test
public void withMockitoSpy_whenPrimary_expect_spyUsed() {
try (BeanContext context = new BootContext()
.withSpy(PEmailer.class) // has a primary
.load()) {
UserOfPEmailer user = context.getBean(UserOfPEmailer.class);
PEmailer emailer = context.getBean(PEmailer.class);
user.email();
verify(emailer).email();
}
}
When using factory with beans implementing interfaces the generated class is missing imports for those interfaces.
In this example imports for Versioned and Serializable are missing.
I tried fixing it myself but didn't find a quick way how to debug apt processors. If you can provide me a hint about your development setup for dinject I would be happy fixing this myself.
package de.testung.example
import com.fasterxml.jackson.databind.ObjectMapper
import io.dinject.Bean
import io.dinject.Factory
@Factory
class ObjectMapperFactory {
@Bean
fun createObjectMapper(): ObjectMapper {
return ObjectMapper()
}
}
Generated class:
package de.testung.example;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.testung.example.ObjectMapperFactory;
import io.dinject.core.Builder;
import javax.annotation.Generated;
@Generated("io.dinject.generator")
public class ObjectMapperFactory$di {
public static void build(Builder builder) {
if (builder.isAddBeanFor(ObjectMapperFactory.class)) {
ObjectMapperFactory bean = new ObjectMapperFactory();
builder.register(bean, null);
}
}
public static void build_createObjectMapper(Builder builder) {
if (builder.isAddBeanFor(ObjectMapper.class)) {
ObjectMapperFactory factory = builder.get(ObjectMapperFactory.class);
ObjectMapper bean = factory.createObjectMapper();
builder.register(bean, null, Versioned.class, Serializable.class);
}
}
}
So with the bootContext.withBean(bean)
method we can supply beans that we want to use that override the otherwise normally injected bean.
For example, lets say we had a relatively expensive dependency (that talked to a database or something remote etc) and we want to build the context for testing purposes. We can provide a test double for that dependency and it would then be injected rather than the real thing.
@Test
public void withBean_expect_testDoublePumpUsed() {
TDPump testDoublePump = new TDPump();
try (BeanContext context = new BootContext()
// Use/inject this instance rather than the normal one AND the normally created bean is skipped
.withBean(testDoublePump)
.load()) {
String makeIt = context.getBean(CoffeeMaker.class).makeIt();
assertThat(makeIt).isEqualTo("done");
assertThat(testDoublePump.steam).isEqualTo(1);
assertThat(testDoublePump.water).isEqualTo(1);
}
}
/**
* Our test double that we want to wire.
*/
class TDPump implements Pump {
int water;
int steam;
@Override
public void pumpWater() {
water++;
}
@Override
public void pumpSteam() {
steam++;
}
}
``
@Factory
public class MyFactory {
@Bean
@Named("green")
Otherthing greenOther() {
return () -> "green";
}
@Bean
@Named("yellow")
Otherthing yellowOther() {
return () -> "yellow";
}
}
private final Otherthing green;
private final Otherthing yellow;
MultipleOtherThings(@Named("green") Otherthing green, @Named("yellow") Otherthing yellow) {
this.green = green;
this.yellow = yellow;
}
Hi @rbygrave,
when I execute this:
BootContext bootContext = new BootContext();
bootContext.withSpy(EmailService.class);
beanContext = bootContext.load();
EmailService emailService = beanContext.getBean(EmailService.class);
System.out.println(emailService.getClass());
// output--> class de.yaskor.autocontract.service.EmailService
doNothing().when(emailService).sendEmail(anyString(), anyString(), anyString());
following exception is thrown:
Argument passed to when() is not a mock!
Example of correct stubbing:
doThrow(new RuntimeException()).when(mock).someMethod();
this here works fine:
BootContext bootContext = new BootContext();
bootContext.withMock(EmailService.class);
beanContext = bootContext.load();
EmailService emailService = beanContext.getBean(EmailService.class);
System.out.println(emailService.getClass());
// output--> class de.yaskor.autocontract.service.EmailService$$EnhancerByMockitoWithCGLIB$$72a6db8a
doNothing().when(emailService).sendEmail(anyString(), anyString(), anyString());
Please have a look at the sout's. The "withSpy" method doesn't replace the bean with the a spy, the "withMock" method works fine.
@Singleton
public class Foo {
private final List<SomeInteface> somes;
@Inject
public Foo(List<SomeInteface> somes) {
this.somes = somes;
}
java.lang.IllegalStateException: No modules found suggests using Gradle and IDEA but with a setup issue? Review IntelliJ Settings / Build / Build tools / Gradle - 'Build and run using' value and set that to 'Gradle'. Refer to https://dinject.io/docs/gradle#idea
at io.dinject.BootContext.load(BootContext.java:360)
at io.dinject.SystemContext.init(SystemContext.java:48)
at io.dinject.SystemContext.<clinit>(SystemContext.java:45)
... 23 more
While there is some overlap, there also seems to be places where DI could make JPMS easier to live with. Is there a plan to add JPMS support? Here's an example of the Dagger Coffee App using modules. It works, but DI would definitely make it more elegant.
Dang, got regrets that I moved the annotations in order to make the build better but felt I did that for the wrong reasons. I could move the annotations back into dinject (which seems natural) as long as I move the integration tests which is what we have done for this 2.2 release.
@Factory
public class Configuration {
private final StartConfig startConfig;
@Inject // ... factory can have dependencies ...
public Configuration(StartConfig startConfig) {
this.startConfig = startConfig;
}
@Bean // a simple factory method
public AFact buildA() {
...
}
@Bean // a factory method with a dependency
public BFact buildB(Other other) {
...
}
ava.lang.NullPointerException
at io.kanuka.core.DBeanMap.getBean(DBeanMap.java:94)
at io.kanuka.core.DBuilder.getMaybe(DBuilder.java:126)
at io.kanuka.core.DBuilder.getMaybe(DBuilder.java:148)
at io.kanuka.core.DBuilder.get(DBuilder.java:197)
at io.kanuka.core.DBuilder.get(DBuilder.java:192)
at org.junk.Bar$di.build(Bar$di.java:10)
at org.junk._di$Factory.buildBar(_di$Factory.java:39)
at org.junk._di$Factory.createContext(_di$Factory.java:32)
at io.kanuka.BootContext.load(BootContext.java:80)
Hi @rbygrave,
It's not the first time I'm facing this issue. Even if @DependecyMeta
annotation contains the correct classes within the dependsOn
list, the createContext
method calls the builders in the wrong order causing:
Injecting null for com.example.MyClass when creating class com.example.MyClass2 - potential beans to inject: []
The generated createContext
methods is like:
@Override
public BeanContext createContext(Builder parent) {
builder.setParent(parent);
// other builders
build_MyClass2();
// other builders
build_MyClass();
return builder.build();
}
Any workarounds I can implement to force the generator to call the builders respecting the dependencies?
Thanks a lot ๐
So for the case where we have multi-module dependencies BUT ... on the modules there is only supplies
specified.
e.g. Have 2 modules where one is defined with
@ContextModule(name = "javalin-validator", provides = "validator")
... and the other module has NO @ContextModule
or nothing defined for provides
and dependsOn
.
In this scenario the current 1.9 behaviour will wire these modules in an undefined order (we don't define which is wired first).
With this change dinject with ALWAYS include the modules that have a provides
first and then follow that by modules that have nothing defined (and then followed by the normal ordering based on provides and dependsOn).
New artifacts are:
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-inject</artifactId>
</dependency>
<!-- java annotation processors -->
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-inject-generator</artifactId>
<scope>provided</scope>
</dependency>
And the package changes from io.dinject
-> io.avaje.inject
This move is part of extracting the annotations into a separate module - dinject-annotations
. With this changing the maven build of dinject-annotations, dinject-generator, dinject to use reactor build (modules).
Hi,
Love this library! But there are some dagger features that I miss such as multi-bindings etc.
Wouldn't it be an idea to leverage dagger for injection of dependencies and use dinject for code generation of routes? That way we get all the features of dagger2, your library becomes way less complex and could focus more on route/API generation than competing with dagger for DI?
Just a thought :)
Create a qualifier annotation. e.g.
@Qualifier
@Retention(RUNTIME)
public @interface Blue {
}
Specify the qualifier annotation rather than @Named
on beans and injection targets (constructor parameters, field injection).
@Blue
@Singleton
public class BlueStore implements SomeStore {
...
Constructor injection:
@Singleton
public class StoreManagerWithQualifier {
private final SomeStore store;
public StoreManagerWithQualifier(@Blue SomeStore store) {
this.store = store;
}
Field injection:
@Singleton
public class StoreManagerWithFieldQualifier {
@Inject
@Blue
SomeStore store;
For example:
public interface Repository<T,I> {
T findById(I id);
}
@Singleton
public class HazRepo implements Repository<Haz,Long> {
@Override
public Haz findById(Long id) {
...
}
}
@Singleton
public class HazManager {
private final Repository<Haz,Long> hazRepo;
@Inject
public HazManager(Repository<Haz, Long> hazRepo) {
this.hazRepo = hazRepo;
}
public Haz find(Long id) {
return hazRepo.findById(id);
}
}
@Factory
public class MyFactory {
@Bean
SomeBean buildSomeBean() {
return ...;
}
/**
* A factory method that has dependencies and returns void.
*/
@Bean
void useSomeBean(SomeBean someBean) {
}
@Bean
void another(SomeBean someBean) {
}
}
Im getting compile warnings when Im using follwing construct....
private final List<Class> withMocks;
private final List<Class> withSpies;
private ContextExtension(List<Class> withMocks, List<Class> withSpies) {
this.withMocks = withMocks;
this.withSpies = withSpies;
}
...
BootContext bootContext = new BootContext();
withMocks.forEach(mock -> bootContext.withMock(mock));
withSpies.forEach(spy -> bootContext.withSpy(spy));
beanContext = bootContext.load();
Changes detected - recompiling the module!
Compiling 26 source files to C:\Users\saka\Documents\NetBeansProjects\private\javalin-sample\target\test-classes
de/yaskor/autocontract/test/ContextExtension.java:[68,55] unchecked method invocation: method withMock in class io.dinject.BootContext is applied to given types
required: java.lang.Class<D>
found: java.lang.Class
de/yaskor/autocontract/test/ContextExtension.java:[68,56] unchecked conversion
required: java.lang.Class<D>
found: java.lang.Class
de/yaskor/autocontract/test/ContextExtension.java:[68,26] unchecked method invocation: method forEach in class java.util.ArrayList is applied to given types
required: java.util.function.Consumer<? super E>
found: java.util.function.Consumer<java.lang.Class>
de/yaskor/autocontract/test/ContextExtension.java:[69,53] unchecked method invocation: method withSpy in class io.dinject.BootContext is applied to given types
required: java.lang.Class<D>
found: java.lang.Class
de/yaskor/autocontract/test/ContextExtension.java:[69,54] unchecked conversion
required: java.lang.Class<D>
found: java.lang.Class
de/yaskor/autocontract/test/ContextExtension.java:[69,26] unchecked method invocation: method forEach in class java.util.ArrayList is applied to given types
required: java.util.function.Consumer<? super E>
found: java.util.function.Consumer<java.lang.Class>
How can I satisfy Class<D>
?
Hello Rob,
First of all, I really like this project and the simplicity of using a library vs an entire framework like quarkus.io.
But I also have a question/issue (not sure if this is the right place but could not find a better one :) ).
I have created a CoffeeApp like application with modules. Now I can run it inside IntelliJ and one module will load the other module with the ContextModule and resolve the dependencies.
But I also tried making a Jar file and then it failed, I get the following error:
Module [<applicationmodule>] has unsatisfied dependencies on modules: [<library module>]. Modules that were loaded ok are:[]. Consider using BootContext.withIgnoreMissingModuleDependencies() or BootContext.withSuppliedBeans(...)
After some searching I found that the issue is that this file is overwritten in the META-INF/services folder:
io.dinject.core.BeanContextFactory
When I add both _di$Factory lines manually to the 's version it will load.
So my question is: Is it possible to create a complete file with the modules in the correct order when building the project? Or am I creating the jar file incorrectly?
Best regards,
Tijs
"Request scoped injection" translates to generating BeanFactory/BeanFactory2 and utlimately using these as dependencies (in generated web routes).
@Controller
@Path("/foo")
public class AController {
@Inject
SomeService service; // a normal dependency
@Inject
Context context; // controller that injects the javalin context
@Get
public String get() {
return "hi " + context.toString() + service.hi();
}
Otherwise all modules in the classpath (via service loader) will be injected.
For component testing purposes we typically don't want to include other modules in DI.
It's almost a:
// exclude all other modules ... just wire this module (for testing purposes)
bootContext.withExludeModules();
import javax.inject.Provider;
import javax.inject.Singleton;
@Singleton
public class FooProvider implements Provider<Foo> {
private final SomeDependency bar;
FooProvider(SomeDependency bar) {
this.bar = bar;
}
@Override
public Foo get() {
...
return new Foo(...);
}
}
And then Foo is a bean that can be injected into other beans.
In Java 11 we can see the error:
Error:java: java.lang.NoClassDefFoundError: javax/annotation/PostConstruct
javax.annotation.PostConstruct
This is because the PostConstruct annotation is no longer part of the JDK and we need to instead include the dependency: javax.annotation:javax.annotation-api
Where Grinder
is an implementation class and not an interface
@Test
public void withMockitoSpy_postLoadSetup_expect_spyUsed() {
try (BeanContext context = new BootContext()
.withSpy(Pump.class)
.withSpy(Grinder.class) // HERE
.load()) {
...
CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
coffeeMaker.makeIt();
...
Grinder grinder = context.getBean(Grinder.class);
verify(grinder).grindBeans(); // throws IllegalStateException
}
}
java.lang.IllegalStateException: Injecting null for org.example.coffee.grind.Grinder when creating class org.example.coffee.CoffeeMaker - potential beans to inject: []
at io.dinject.core.DBuilder.get(DBuilder.java:223)
at io.dinject.core.DBuilder.get(DBuilder.java:207)
at org.example.coffee.CoffeeMaker$di.build(CoffeeMaker$di.java:14)
at org.example.coffee._di$Factory.build_CoffeeMaker(_di$Factory.java:228)
at org.example.coffee._di$Factory.createContext(_di$Factory.java:92)
at io.dinject.BootContext.load(BootContext.java:364)
at org.example.coffee.BootContext_mockitoSpyTest.withMockitoSpy_postLoadSetup_expect_spyUsed(BootContext_mockitoSpyTest.java:64)
...
This has the limitations that it does not support private fields
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.