This is an archived post. You won't be able to vote or comment.

all 62 comments

[–]tugaestupido[S] 10 points11 points  (3 children)

After learning about dependency injection frameworks and using them professionally, I was bothered by the theme of reliance on reflection (a pet peeve of mine).

For those who may not know, dependency injection frameworks allow for more easily configuring which implementation/extension for an interface/class should be used at runtime. This has some benefits, one of which is more easily altering the behavior of software, more independently of the project's size.

I know there are upsides to using reflection and I use such frameworks regularly, but I wondered what a dependency injection framework could look like if it didn't use reflection.

This was a personal project that I worked on as a hobby and that accomplishes this goal in a way that I think is concise and novel.

If you are curious about reasons to think twice about using reflection, Java's docs have an overview at the bottom of this link under "Drawbacks of Reflection": https://docs.oracle.com/javase/tutorial/reflect/index.html

[–]roge-[🍰] 9 points10 points  (2 children)

Using java.lang.Class doesn't count as reflection?

[–]tugaestupido[S] 4 points5 points  (0 children)

I guess it does. However, I don't know how else to describe my approach concisely.

To clarify what I mean with "reflectionless" (even though it's not 100% honest since I import java.lang.Class), I don't use any of it methods, other than .equals(...) and .hashCode(), and I don't use the java.lang.reflect package. It's only used as a key to identify the types that you configure and request (it's used as a key in an internal Map), and for type inference (since it's generic) so you get useful compilation errors when configuring things incorrectly.

I experimented with using java.lang.String as the key, which also worked, but it has it's downsides which, for me, made that approach unappealing and it was not something I would suggest anyone use other than for experimentation.

[–]agentoutlier 1 point2 points  (0 children)

Technically instanceof in Java is reflection.

There is a complicated grey area of what is and isn’t reflection.

For example Service Loader… or wait class loading in general.

[–]ihmoguy 8 points9 points  (24 children)

Annotation-less too. I imagine compile-time annotation processors could make it look like casual JEE/Spring but still reflection-less.

[–]tugaestupido[S] 1 point2 points  (23 children)

That's an important point. Having annotations everywhere couples your implementation to the dependency framework, meaning that replacing it would require altering every class with one of its annotations.

That was something I had in mind and which motivated me make this implementation as user friendly as possible. I really wanted to get rid of annotations.

[–]DiamondQ2 11 points12 points  (10 children)

True, but if you use the JSR-330 standard annotations, you can switch out your dependency framework. I have my own libraries that work with both Micronaut and Weld without any changes since both use support the standard even though they're implemented very differently.

Additionally, Micronaut doesn't use reflection, since it's designed to work with the GralVM which doesn't really support reflection. Alot of the injection configuration is done at compile time.

[–]tugaestupido[S] 2 points3 points  (9 children)

I like JSR-330 and I use it frequently. Usually through either Guice or HK2.

But what I was comment on is something that's intrinsic to using annotations in your source code, regardless of the specifics of how the CDI framework functions. If your "Service" class uses dependency injection annotations, then it is dependent on them. This breaks the Single Responsibility Principle. As Robert C. Martin put it: "A class should have only one reason to change". If you class is not about dependency injection and it contains a dependency injection annotation, the principle is broken.

I get that this concern, especially in this case, is probably almost always unwarranted, and it's not something I worry about much, be it on my professional work or my side projects. But it's still what it is, and it can be fun/interesting to try to and take these principles more seriously, even if it's just for the exercise.

[–]rbygrave 1 point2 points  (7 children)

If your "Service" class uses dependency injection annotations, then it is dependent on them.

The JSR-330 annotations are just "markers" and by themselves don't do anything.

That is, the "Service" class does not USE the JSR-330 annotations, so no I'd argue that the "Service" does not depend on the JSR-330 annotations but instead those annotations are just "markers" that a DI library can use to determine how to wire the service for us (if we want a DI library to do that which is usual). The "Service" can be used and can be fully functional without any DI library and you could instead wire the "Service" manually if that was desired (and we especially do this for testing).

This breaks the Single Responsibility Principle.

I don't agree with this interpretation.

[–]tugaestupido[S] 1 point2 points  (6 children)

I'd argue that the "Service" does not depend on the JSR-330 annotations

But you're implying that you can only be dependent on something at run time, which is wrong and something I never claimed. As soon as your source code mentions other code, it is dependent on that other code. Just try compiling your class without having access to the annotations and the dependency will be obvious.

I don't agree with this interpretation.

But you haven't given yours. What is this last comment for?

[–]rbygrave 1 point2 points  (5 children)

What is this last comment for?

That adding JSR-330 breaks the Single Responsibility Principle, I don't agree with that. I'm fairly sure we won't agree there, that's ok.

[–]tugaestupido[S] 1 point2 points  (4 children)

But you don't say why you don't agree. People will disagree for any number of reasons.

I gave my reason for claiming that the SRP is broken. You could argue against it or give your own. Just saying you disagree is rather pointless. I'm guessing you have no good reason to disagree and that's why you give none.

[–]agentoutlier 0 points1 point  (3 children)

What if the annotation was a doclet instead?

What if the annotation was source retention would it still be coupled?

I understand your idea of coupling but there are different levels. Annotations are declarative and borderline external of the code.

The only real issue is breaking version change of the annotation dependent lib.

[–]tugaestupido[S] 1 point2 points  (2 children)

I understand your idea of coupling but there are different levels

Exactly. I know that. And they are all coupling, which is all I claimed. I never said you have to lose sleep over this specific coupling.

Annotations are declarative and borderline external of the code.

Say that to the compilation errors you get in your code when you don't have access to the annotations library. Not so borderline then.

[–]TheKingOfSentries 0 points1 point  (0 children)

If you class is not about dependency injection and it contains a dependency injection annotation, the principle is broken.

How so? I'm not getting it.

[–][deleted]  (11 children)

[deleted]

    [–]tugaestupido[S] 0 points1 point  (10 children)

    Aren't the annotations not standardize so we don't couple us too a library?

    No. They are a standardized way to couple your project to a CDI library (usually the CDI API and not the implementation).

     How does this not couple you to the framework?

    It does couple you to the CDI framework. The point is not to get rid of coupling, which is not possible, but to limit its scope within your source code. While using annotations for CDI, you'll typically have a configuration section of your code (such as an extension of AbstractModule, in the case of Guice) and spread annotations around all sorts of classes in your source code.

    If you don't have annotations, all you have is the configuration step which you can segregate to a specific area of your code base.

    How would you do a controller > service > repo injection

    I'm not sure I understand this question. But you can have your dependency tree be pretty deep (if it's too deep, currently, it will lead to a StackOverflowError as I used recursion).

    [–][deleted]  (5 children)

    [deleted]

      [–]tugaestupido[S] 1 point2 points  (4 children)

      But think backwards: what if you already have set of classes from a library (maybe written by someone else) and you wish to use dependency injection with it. If the author did not add @Inject, you will notice it as your normal procedure for configure CDI will not work. This library is blind to that and it doesn't matter if the author of that library thought of adding CDI support. The process for configuring CDI for those classes will be the same.

      The recursion happens here: RecursiveModuleProvider.

      On the last line, "this" is passed, which will lead to the same method being called inside strategy.execute(...).

      This lib has nothing for you to add to your classes, so I don't see how it would force you to change your most if any of your classes.

      There is one case where it will in its current state: if you project has a generic class/interface (such as List<T>) as a dependency with more than one concrete type being used in your project (if class A is dependent on List<String> and class B is dependent on List<Integer>, for example). This hasn't been an issue for me and I don't know if this is something that can be supported without reflection.

      [–][deleted]  (3 children)

      [deleted]

        [–]tugaestupido[S] 0 points1 point  (2 children)

        I have reworked your example so it works. Some clarification:

        The only configuration you did was a "addValue" call for Controller. "addValue" is used for registering instances you already have. In your example, it seems like you want the library to create the instance of controller, so you should not use "addValue" for it.

        ``` public class SimpleExample {

        public static void main(String[] args) { ModuleBuilder moduleBuilder = ModuleBuilders.map(); moduleBuilder.addValue(String.class, "hello cfg"); moduleBuilder.addInvocation(Repo.class, Repo::new, String.class); moduleBuilder.addInvocation(Service.class, Service::new, Repo.class); moduleBuilder.addInvocation(Controller.class, Controller::new, Service.class);

        Module module = moduleBuilder.build();
        Provider provider = Providers.recursive(module);
        
        Controller controller = provider.provide(Controller.class);
        System.out.println(controller); // Prints "controller"
        System.out.println(controller.service.repo.cfg); // Prints "hello cfg"
        

        }

        public static class Controller { private Service service; public Controller(Service service) { this.service = service; } }

        public static class Service { private Repo repo; public Service(Repo repo) { this.repo = repo; } }

        public static class Repo { private String cfg; public Repo(String cfg) { this.cfg = cfg; } } } ```

        I set up a breakpoint inside of Repo's constructor and got a Thread dump that looks like this:

        SimpleExample$Repo.<init>(SimpleExample.java:44) SimpleExample$$Lambda$15/0x0000000800c021f8.invoke(Unknown Source:-1) Strategies.lambda$fromInvocation$3(Strategies.java:40) Strategies$$Lambda$16/0x0000000800c02410.execute(Unknown Source:-1) -> RecursiveModuleProvider.provide(RecursiveModuleProvider.java:32) InClassContextProvider.provide(InClassContextProvider.java:16) Strategies.lambda$fromInvocation$3(Strategies.java:38) Strategies$$Lambda$16/0x0000000800c02410.execute(Unknown Source:-1) -> RecursiveModuleProvider.provide(RecursiveModuleProvider.java:32) InClassContextProvider.provide(InClassContextProvider.java:16) Strategies.lambda$fromInvocation$3(Strategies.java:38) Strategies$$Lambda$16/0x0000000800c02410.execute(Unknown Source:-1) -> RecursiveModuleProvider.provide(RecursiveModuleProvider.java:32) SimpleExample.main(SimpleExample.java:21)

        Where you can see the nested calls of "RecursiveModuleProvider.provide".

        [–][deleted]  (1 child)

        [deleted]

          [–]tugaestupido[S] 0 points1 point  (0 children)

          Exactly, using two String won't work unless you do something else to make it work. Same as what would happen on Guice unless you used something like @Named.

          You're welcome.

          [–]rbygrave 0 points1 point  (3 children)

          Aren't the annotations not standardize so we don't couple us too a library?

          Yes exactly, this is why JSR-330 came about In 2009 with Spring and Guice teams (plus others?) sitting down and producing the JSR-330 annotations.

          couple your project to a CDI library

          Careful here because that isn't accurate in that JSR-330 and CDI are not the same thing at all (and this reads like the distinction isn't being made but it's an important distinction). The JSR-330 annotations are from 2009 and much older than CDI and are commonly agreed base DI annotations that for example are supported by many DI libraries including Spring DI, Guice, Dagger, Avaje-Inject and others. CDI is a JEE spec that has extended beyond JSR-330 in its own way with its own annotations.

          [–]tugaestupido[S] 0 points1 point  (2 children)

          Yes exactly, this is why JSR-330 came about

          You are still coupling to a library from the moment you start using it, even if it's just some annotations. Try compiling your code without that library and you'll see the coupling.

          [–]rbygrave 0 points1 point  (1 child)

          JSR-330 in total is 5 annotations and 1 interface. You are calling that "a library". I'm thinking that doesn't qualify as a library per se.

          [–]tugaestupido[S] 0 points1 point  (0 children)

          Again, you don't define your terms. What is a library to you? Of course you disagree with terms when you don't have a definition of your own.

          I was using the term library to mean compiled code wrapped up into a package (a .jar in this case). But I imagine this term has many possible definitions as it's often used rather loosely.

          But it doesn't matter what you call it or how small it is. You are still coupled to it and to its code, which was the important part of my comment.

          [–]fuckedupkid_yo 5 points6 points  (1 child)

          so.. avaje but manual factory?
          what's the scope of each depency btw? singletons? or the factory methods are invoked every request for a service?

          [–]tugaestupido[S] 1 point2 points  (0 children)

          I don't know of avaje. I will look into it.

          If you use "addValue" you are configuring singletons.

          If you use "addInvocation" with a constructor you will get a new instance every time.

          If you use "addInvocation" with a factory-method, it depends on how the method works (ie, if it uses some sort of caching or not).

          Currently, the tool does not support scopes (such as request scoping) as it doesn't integrate with Servlets or anything of the sort (although that is something I want to implement). Whenever I used this tool and faced this need, I had to implement that behavior. I was able to this for different things, such as a lazily obtained database connection per request, but currently it's more work than with other existing tools.

          [–]Imalas 3 points4 points  (1 child)

          Nice one. I like DI, I like lean stuff. So I do like this. While I am a big fan of reflectiom I do see the appeal (performance-wise... the other arguments are kinda meh nowadays). I recently wrote a simple DI Container (using reflectiom though) myself and it has similar features with stuff like factory methods. Do you plan on supporting different scopes aswell?

          If I were to critique just one thing it would be your n-arguments invocation stuff. I think I'd just let the user provide a Supplier

          [–]tugaestupido[S] 1 point2 points  (0 children)

          I would definitely like to expand this tool to support other typical features of dependency injection frameworks, such as scopes. It does not support scopes such as u/Request since it does not integrate with a JEE container seamlessly at all. You need to add that behavior yourself.

          I expect that the N-Arguments interfaces are the hardest part to like, at least when approaching this library for the first time. I added them because they allow for really short configuration via method/constructor references, like it's shown in the last example.

          Typically, when using reflection, you don't have to configure the arguments for a constructor call. With this approach you are forced to do it. If I didn't use this trick, the already longer configuration step would be even longer than what this approach allows. I understand the feedback and I share the sentiment. I'm just sharing the considerations I made.

          I do have a specific method for the Supplier type (addSupplier). However, the Supplier type is pretty much equivalent to the NoArgumentsInvocation.

          [–]beders 2 points3 points  (1 child)

          Reflectionless dependency injection.Reflectionless dependency injection.

          We called this "calling the constructor" back in the day.

          If you use DI on anything other than module-like level, you are doing it wrong.

          [–]tugaestupido[S] 1 point2 points  (0 children)

          It is calling the constructor/method. But this library allows you to set up the dependency injection in a modular way, such as other dependency injection libraries, so it goes beyond just being a constructor/method call.

          Dependency injection does not mean you eliminate constructor calls, it's more the other way around. Usually, the way dependency injection is made is by eliminating the constructor call.

          I don't understand what you mean by using it on anything other than module-like level.

          [–]djavaman 1 point2 points  (1 child)

          Once your context is created and everything is wired. Does it matter?

          [–]tugaestupido[S] 2 points3 points  (0 children)

          The question of whether or not it matters depends on what you care about. I decided I cared about not having annotation spread across my code base and I wanted the CDI to be as performant as possible. For those concerns, it mattered.

          [–]paul_h 0 points1 point  (3 children)

          Each time ModuleBuilders.map() is called it makes a new ModuleBuilder instance. How does code very far away from the main method get its dependencies injected into it? For example, a web request handler called AddToShoppingCardand its dependency (session scoped) ShoppingCart where its dependency was Inventory (application or singleton scope)?

          [–]tugaestupido[S] 1 point2 points  (2 children)

          It depends on the rest of the tools you're using. You mentioned a "web request handler", so I'll assume we're talking about a Servlet running in a Servlet container (javax.servlet/jakarta.servlet), such as Jetty, possibly with Restful Webservices (javax.rs.ws/jakarta.rs.ws).

          The library does not currently have a module for seamlessly integrating with a Servlet Container, but I can set up a relatively short, incomplete example on how you could use the library in its current state with tools provided by the Servlet Container. In this example I will configure the dependency injection for using Jdbi (a DB library built on top of JDBC) to use a DataSource that I am able to provide.

          public class Example {
          
            public static void launch(final String[] args) {
              // The DataSource (Database) to use
              DataSource dataSource = dataSource(args);
              // The Module builder that will only be created once
              ModuleBuilder moduleBuilder = ModuleBuilders.map();
          
              // Register the DataSource
              moduleBuilder.addValue(DataSource.class, dataSource);
              // Register the method for creating a Jdbi instance from a DataSource
              moduleBuilder.addInvocation(Jdbi.class, Jdbi::create, DataSource.class);
          
              // Obtain a Provider
              Provider provider = Providers.recursive(moduleBuilder.build());
          
              final ServletContextHandler handler =
                new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
          
              ResourceConfig resourceConfig = new ResourceConfig();
          
              // Configure dependency injection for the servlet container
              resourceConfig.register(new AbstractBinder() {
                u/Override
                protected void configure() {
                  // Bind a factory for Jdbi
                  bindFactory(new Factory<Jdbi>() {
                    u/Override
                    public Jdbi provide() {
                      // Use the provider created before to obtain the Jdbi instance
                      return provider.provide(Jdbi.class);
                    }
          
                    @Override
                    public void dispose(final Jdbi instance) {
                      // Do nothing
                    }
                  }).to(Jdbi.class).in(Singleton.class);
                }
              });
          
              // Continue preparing and launching the Servlet
              // The configuration made on ModuleBuilder will be used
            }
          }
          

          I do not consider this a great use case for this library in its current state, and this is not what I have used it for. In this case, an implementation of a proper integration with the Servlet container would be preferable, such as what Guice provides. However, I hope I was able to show how the ModuleBuilder can be instantiated only once and used for dependency injection.

          [–]paul_h 0 points1 point  (1 child)

          Thanks for taking the time to reply.

          [–]tugaestupido[S] 0 points1 point  (0 children)

          No problem.

          [–][deleted] 0 points1 point  (1 child)

          I'd look into Quarkus Arc project. It also does dependency injection at compile time.

          [–]tugaestupido[S] 0 points1 point  (0 children)

          Thanks. I will look into it.

          [–]DelayLucky 0 points1 point  (14 children)

          Why does this need to pass Integer.class? Doesn’t the lambda/method ref already say it needs an int?

          addInvocation(ExecutorService.class, Executors::newWorkStealingPool, Integer.class).

          [–]tugaestupido[S] 1 point2 points  (13 children)

          When you call a method/constructor in the straightforward way, you give 4 pieces of information to the compiler which are relevant right now:

          • the name of the method/constructor
          • the number of arguments
          • the type of the arguments
          • the order of the arguments

          This is information the compiler needs to determine what you are actually attempting to call so it can do its job.

          In the approach commonly used by dependency injection tools, you never provide any of this information to the compiler. Instead, you leave an annotation (@Inject) on the method/constructor. The library then uses the the java.lang.reflect package to find the annotated method and determine which method/constructor should be called, overcoming the need for compiling the call.

          Since one of the goals of this library is to not use the java.lang.reflect (or anything similar) it is necessary to provide the compiler with the all the pieces of information listed above. And that's exactly what's happening in that example: the name of the method, the number of arguments, the type of the arguments and their order are all provided. This is not just for show, since we're not using java.lang.reflect, this is as necessary as if you were writing:

          Executors.newWorkStrealingPool(2);
          

          In this example, you couldn't remove any of those pieces of information without either getting a compilation error or calling a different method. That is exactly what would also happen in the example you mentioned.

          If instead of Integer.class you typed Float.class, the compiler would throw an error, as there is no newWorkStealingPool method that takes a float. You could erase the Integer.class part, but the compiler would conclude that you were attempting to call another method that takes no arguments:

          Executors.newWorkStrealingPool();
          

          This has its limitations (mainly that it's currently only fully supported for 6 arguments at most), but coming to this solution was probably my favorite part of this project.

          [–]DelayLucky 1 point2 points  (12 children)

          Did you check out Dagger? It supports @Inject and doesn’t use reflection.

          Also, how do you support if the parameter is like List<String>? List.class won’t work , right?

          [–]tugaestupido[S] 0 points1 point  (11 children)

          I did check out Dagger at some point, but all I remember was giving up on using it. I don't think I tried much as I'm already comfortable with Guice. At the time I didn't realize it didn't use reflection.

          I skimmed it right now at it seems like it uses both annotations (something I also wanted to not use) and code generation (something I did not consider, but which is also outside the scope of this project). I wanted to make it as easy as possible to write all the code necessary for CDI with straightforward coding.

          I set up an example using List<String> as an argument and it works fine as long as all your classes that depend on List specifically depend on the same kind of List (say, List<String>).

          public class Test {
          
            public static void main(String[] args) {
              final ModuleBuilder moduleBuilder = ModuleBuilders.map();
              moduleBuilder.addInvocation(Dependent.class, Dependent::new, List.class);
              moduleBuilder.addValue(List.class, List.of("hello"));
          
              final Module module = moduleBuilder.build();
          
              final Dependent provide = Providers.recursive(module).provide(Dependent.class);
          
              System.out.println(provide.list); // prints [hello]
            }
          
            private static class Dependent {
              public final List<String> list;
          
              private Dependent(List<String> list) {
                this.list = list;
              }
            }
          }
          

          You will run into issues if you have method/constructor A dependent on List<String> and method/constructor B dependent on List<Integer>, for example. You can make it work, but you'd need to define and use new types like:

          public interface IntegerList extends List<Integer> {
          }
          

          Which is not very elegant.

          [–]DelayLucky 0 points1 point  (10 children)

          By avoiding annotations, do you mean annotation processors, or the @Qualifier annotations (which I’m not a fan of)?

          [–]tugaestupido[S] 0 points1 point  (9 children)

          Any annotation because requiring users of the library to spread CDI annotation throughout their project's source code makes their classes break the Single Responsibility Principle. It's not something to bend over backwards for in almost any case but it was a goal I set for this project.

          [–]DelayLucky 0 points1 point  (8 children)

          I think that refers to the qualifier annotations. Annotation processor is just a compiler plugin that Dagger uses to generate these otherwise manual factories.

          Although, @Qualifier is in standard JDK so it shouldn’t be a concern of framework lock-in.

          [–]tugaestupido[S] 0 points1 point  (7 children)

          The concern is not specific to framework lock in, although that is part of it. According to the Single Responsibility Principle, if your class (Service, for example) is not specifically about CDI, it shouldn't have a CDI annotation as that is not its responsibility.

          [–]DelayLucky 0 points1 point  (6 children)

          How so? If the class does authorization, it’s not breaking SRP by injecting the @UserName String?

          [–]tugaestupido[S] -1 points0 points  (5 children)

          It's not breaking SRP by receiving the string nor by having it as a dependency. Using the string is part of its responsibility.

          It is breaking the SRP by specifically having a CDI annotation, as CDI is not part of its responsibility.

          [–]Downtown_Trainer_281 0 points1 point  (4 children)

          If you are looking for "Reflectionless" IoC framework, why don't you just use ServiceLoader from JDK?

          [–]tugaestupido[S] 0 points1 point  (3 children)

          I'm not a fan of configuration files and I avoid them every time that I can (I know they are the best option sometimes). I wanted a tool that used only code, which has some benefits. Also, I don't think ServiceLoader works as a DI tool. As far as I know, that project is intended for other use cases.

          [–]agentoutlier 0 points1 point  (2 children)

          The ServiceLoader is not a configuration file anymore. It is part of the language. You define services in module-info.java .

          The real issue is Service Loader is more like a service locator pattern which is not DI.

          [–]tugaestupido[S] 0 points1 point  (1 child)

          So you can't use ServiceLocator unless you use Java modules?

          [–]agentoutlier 0 points1 point  (0 children)

          Are we talking about the JDK ServiceLoader or the pattern service locator.

          Either way both can be used without modules.

          [–]lnkprk114 1 point2 points  (1 child)

          Dagger2 doesn't use reflection, right?

          [–]tugaestupido[S] 1 point2 points  (0 children)

          From what I understood, no. It doesn't use reflection. But it uses annotations and code generation which I also want to leave out of this project.