DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Dependency Injection in Spring
  • Protect Your Invariants!
  • Smart Dependency Injection With Spring: Assignability (Part 2 of 3)
  • Spring Beans With Auto-Generated Implementations: How-To

Trending

  • What Is Plagiarism? How to Avoid It and Cite Sources
  • Secure by Design: Modernizing Authentication With Centralized Access and Adaptive Signals
  • Accelerating Debugging in Integration Testing: An Efficient Search-Based Workflow for Impact Localization
  • Grafana Loki Fundamentals and Architecture
  1. DZone
  2. Coding
  3. Frameworks
  4. Dependency Injection Implementation in Core Java

Dependency Injection Implementation in Core Java

Implement your own lightweight Dependency Injection in core Java without using any framework.

By 
Shital Devalkar user avatar
Shital Devalkar
·
Nov. 19, 20 · Tutorial
Likes (17)
Comment
Save
Tweet
Share
24.1K Views

Join the DZone community and get the full member experience.

Join For Free

Overview

This article will guide you to understand and build a lightweight Java application using your own Dependency Injection implementation.

Dependency Injection … DI… Inversion Of Control…IoC, I guess you might have heard these names so many times while your regular routine or specially interview preparation time that you wonder what exactly it is.

but if you really want to understand how internally it works then continue reading here.

So, What Is Dependency Injection?

Dependency injection is a design pattern used to implement IoC, in which instance variables (ie. dependencies) of an object got created and assigned by the framework. 

To use DI feature a class and it's instance variables just need to add annotations predefined by the framework.

The Dependency Injection pattern involves 3 types of classes.

  • Client Class: The client class (dependent class) depends on the service class.
  • Service Class: The service class (dependency class) that provides service to the client class. 
  • Injector Class: The injector class injects the service class object into the client class.

In this way, the DI pattern separates the responsibility of creating an object of the service class out of the client class. Below are a couple more terms used in DI. 

  • Interfaces that define how the client may use the services.
  • Injection refers to the passing of a dependency (a service) into the object (a client), this is also referred to as auto wire.

So, What Is Inversion of Control?

In short, "Don't call us, we'll call you."

  • Inversion of Control (IoC) is a design principle. It is used to invert different kinds of controls  (ie. object creation or dependent object creation and binding ) in object-oriented design to achieve loose coupling.
  • Dependency injection one of the approach to implement the IoC.  
  • IoC helps to decouple the execution of a task from implementation.
  • IoC helps it focus a module on the task it is designed for.
  • IoC prevents side effects when replacing a module.

  Class Diagram for DI Design Pattern

 

In the above class diagram, the Client class that requires UserService and AccountService objects does not instantiate the UserServiceImpl and AccountServiceImpl classes directly.

Instead, an Injector class creates the objects and injects them into the Client, which makes the Client independent of how the objects are created.

Types of Dependency Injection

  • Constructor Injection: The injector supplies the service (dependency) through the client class constructor. In this case, Autowired annotation added on the constructor.
  • Property Injection: The injector supplies the service (dependency) through a public property of the client class. In this case Autowired annotation added while member variable declaration.
  • Setter Method Injection: The client class implements an interface that declares the method(s) to supply the service (dependency) and the injector uses this interface to supply the dependency to the client class. 

In this case, Autowired annotation added while method declaration.

How It Works?

To understand Dependency Injection implementation, refer code snippets here, or download/clone the Tutorial shared here on GitHub.

Prerequisite

For a better understanding of this tutorial, it's good to have basic knowledge of Annotations and reflection in advance.

Required Java Library

Before begin with the coding steps, you can create new maven project in the eclipse and add reflection dependency in pom.xml

Java
 




x
16


 
1
    <properties>
2
        <maven.compiler.source>1.8</maven.compiler.source>
3
        <maven.compiler.target>1.8</maven.compiler.target>
4
    </properties>
5
 
          
6
    <dependencies>
7
        <dependency>
8
            <groupId>org.reflections</groupId>
9
            <artifactId>reflections</artifactId>
10
            <version>0.9.9-RC1</version>
11
            <scope>compile</scope>
12
        </dependency>
13
        
14
        <!--  other dependencies -->
15
    </dependencies>


Create User-Defined Annotations:

As described above DI implementation has to provide predefined annotations, which can be used while declaration of client class and service variables inside a client class.

Let's add basic annotations, which can be used by client and service classes:

CustomComponent.java

Java
 




xxxxxxxxxx
1


 
1
import java.lang.annotation.*;
2
 
          
3
/**
4
 * Client class should use this annotation
5
 */
6
@Retention(RetentionPolicy.RUNTIME)
7
@Target(ElementType.TYPE)
8
public @interface CustomComponent {
9
}


CustomAutowired.java  

Java
 




xxxxxxxxxx
1
12


 
1
import java.lang.annotation.*;
2
import static java.lang.annotation.ElementType.*;
3
import static java.lang.annotation.RetentionPolicy.RUNTIME;
4
 
          
5
/**
6
 * Service field variables should use this annotation
7
 */
8
@Target({ METHOD, CONSTRUCTOR, FIELD })
9
@Retention(RUNTIME)
10
@Documented
11
public @interface CustomAutowired {
12
}


CustomQualifier.java

Java
 




xxxxxxxxxx
1
13


 
1
import java.lang.annotation.*;
2
 
          
3
/**
4
 *  Service field variables should use this annotation
5
 *  This annotation Can be used to avoid conflict if there are multiple implementations of the same interface
6
 */
7
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
8
@Retention(RetentionPolicy.RUNTIME)
9
@Inherited
10
@Documented
11
public @interface CustomQualifier {
12
    String value() default "";
13
}


Service Interfaces

UserService.java

Java
 




xxxxxxxxxx
1


 
1
public interface UserService {
2
    String getUserName();
3
}


AccountService.java

Java
 




x


 
1
public interface AccountService {
2
    Long getAccountNumber(String userName);
3
}



Service Classes

These classes implement service interfaces and use DI annotations.

UserServiceImpl.java

Java
 




xxxxxxxxxx
1
11


 
1
import com.useraccount.di.framework.annotations.CustomComponent;
2
import com.useraccount.services.UserService;
3
 
          
4
@CustomComponent
5
public class UserServiceImpl implements UserService {
6
 
          
7
    @Override
8
    public String getUserName() {
9
        return "shital.devalkar";
10
    }
11
}


AccountServiceImpl.java

Java
 




xxxxxxxxxx
1
11


 
1
import com.useraccount.di.framework.annotations.CustomComponent;
2
import com.useraccount.services.AccountService;
3
 
          
4
@CustomComponent
5
public class AccountServiceImpl implements AccountService {
6
 
          
7
    @Override
8
    public Long getAccountNumber(String userName) {
9
        return 12345689L;
10
    }
11
}


Client Class

For using the DI features client class has to use predefined annotations provided by DI framework      for the client and service class.

UserAccountClientComponent.java

Java
 




xxxxxxxxxx
1
26


 
1
import com.useraccount.di.framework.annotations.*;
2
import com.useraccount.services.*;
3
 
          
4
/**
5
 * Client class, havin userService and accountService expected to initialized by
6
 * CustomInjector.java
7
 */
8
@CustomComponent
9
public class UserAccountClientComponent {
10
 
          
11
    @CustomAutowired
12
    private UserService userService;
13
 
          
14
    @CustomAutowired
15
    @CustomQualifier(value = "accountServiceImpl")
16
    private AccountService accountService;
17
 
          
18
    public void displayUserAccount() {
19
 
          
20
        String username = userService.getUserName();
21
 
          
22
        Long accountNumber = accountService.getAccountNumber(username);
23
 
          
24
        System.out.println("User Name: " + username + "Account Number: " + accountNumber);
25
    }
26
}


Injector Class

Injector class plays a major role in the DI framework. Because it is responsible to create instances of all clients and autowire instances for each service in client classes.

Steps:

  1. Scan all the clients under the root package and all sub packages
  2. Create an instance of client class.
  3. Scan all the services using in the client class (member variables, constructor parameters, method parameters)
  4. Scan for all services declared inside the service itself (nested dependencies), recursively
  5. Create instance for each service returned by step 3 and step 4
  6. Autowire: Inject (ie. initialize) each service with instance created at step 5
  7. Create Map all the client classes  Map<Class, Object>
  8. Expose API to get the getBean(Class classz)/getService(Class classz).
  9. Validate if there are multiple implementations of the interface or there is no implementation
  10. Handle Qualifier for services or autowire by type in case of multiple implementations.

CustomInjector.java  

This class heavily uses basic method provided by the java.lang.Class and org.reflections.Reflections.

Java
 




xxxxxxxxxx
1
135


 
1
import java.io.IOException;
2
import java.util.*;
3
import java.util.Map.Entry;
4
import java.util.stream.Collectors;
5
import javax.management.RuntimeErrorException;
6
import org.reflections.Reflections;
7
import com.useraccount.di.framework.annotations.CustomComponent;
8
import com.useraccount.di.framework.utils.*;
9
 
          
10
/**
11
 * Injector, to create objects for all @CustomService classes. auto-wire/inject
12
 * all dependencies
13
 */
14
public class CustomInjector {
15
    private Map<Class<?>, Class<?>> diMap;
16
    private Map<Class<?>, Object> applicationScope;
17
 
          
18
    private static CustomInjector injector;
19
 
          
20
    private CustomInjector() {
21
        super();
22
        diMap = new HashMap<>();
23
        applicationScope = new HashMap<>();
24
    }
25
 
          
26
    /**
27
     * Start application
28
     * 
29
     * @param mainClass
30
     */
31
    public static void startApplication(Class<?> mainClass) {
32
        try {
33
            synchronized (CustomInjector.class) {
34
                if (injector == null) {
35
                    injector = new CustomInjector();
36
                    injector.initFramework(mainClass);
37
                }
38
            }
39
        } catch (Exception ex) {
40
            ex.printStackTrace();
41
        }
42
    }
43
 
          
44
    public static <T> T getService(Class<T> classz) {
45
        try {
46
            return injector.getBeanInstance(classz);
47
        } catch (Exception e) {
48
            e.printStackTrace();
49
        }
50
        return null;
51
    }
52
 
          
53
    /**
54
     * initialize the injector framework
55
     */
56
    private void initFramework(Class<?> mainClass)
57
            throws InstantiationException, IllegalAccessException, ClassNotFoundException, IOException {
58
        Class<?>[] classes = ClassLoaderUtil.getClasses(mainClass.getPackage().getName());
59
        Reflections reflections = new Reflections(mainClass.getPackage().getName());
60
        Set<Class<?>> types = reflections.getTypesAnnotatedWith(CustomComponent.class);
61
        for (Class<?> implementationClass : types) {
62
            Class<?>[] interfaces = implementationClass.getInterfaces();
63
            if (interfaces.length == 0) {
64
                diMap.put(implementationClass, implementationClass);
65
            } else {
66
                for (Class<?> iface : interfaces) {
67
                    diMap.put(implementationClass, iface);
68
                }
69
            }
70
        }
71
 
          
72
        for (Class<?> classz : classes) {
73
            if (classz.isAnnotationPresent(CustomComponent.class)) {
74
                Object classInstance = classz.newInstance();
75
                applicationScope.put(classz, classInstance);
76
                InjectionUtil.autowire(this, classz, classInstance);
77
            }
78
        }
79
    }
80
 
          
81
    /**
82
     * Create and Get the Object instance of the implementation class for input
83
     * interface service
84
     */
85
    @SuppressWarnings("unchecked")
86
    private <T> T getBeanInstance(Class<T> interfaceClass) throws InstantiationException, IllegalAccessException {
87
        return (T) getBeanInstance(interfaceClass, null, null);
88
    }
89
 
          
90
    /**
91
     * Overload getBeanInstance to handle qualifier and autowire by type
92
     */
93
    public <T> Object getBeanInstance(Class<T> interfaceClass, String fieldName, String qualifier)
94
            throws InstantiationException, IllegalAccessException {
95
        Class<?> implementationClass = getImplimentationClass(interfaceClass, fieldName, qualifier);
96
 
          
97
        if (applicationScope.containsKey(implementationClass)) {
98
            return applicationScope.get(implementationClass);
99
        }
100
 
          
101
        synchronized (applicationScope) {
102
            Object service = implementationClass.newInstance();
103
            applicationScope.put(implementationClass, service);
104
            return service;
105
        }
106
    }
107
 
          
108
    /**
109
     * Get the name of the implimentation class for input interface service
110
     */
111
    private Class<?> getImplimentationClass(Class<?> interfaceClass, final String fieldName, final String qualifier) {
112
        Set<Entry<Class<?>, Class<?>>> implementationClasses = diMap.entrySet().stream()
113
                .filter(entry -> entry.getValue() == interfaceClass).collect(Collectors.toSet());
114
        String errorMessage = "";
115
        if (implementationClasses == null || implementationClasses.size() == 0) {
116
            errorMessage = "no implementation found for interface " + interfaceClass.getName();
117
        } else if (implementationClasses.size() == 1) {
118
            Optional<Entry<Class<?>, Class<?>>> optional = implementationClasses.stream().findFirst();
119
            if (optional.isPresent()) {
120
                return optional.get().getKey();
121
            }
122
        } else if (implementationClasses.size() > 1) {
123
            final String findBy = (qualifier == null || qualifier.trim().length() == 0) ? fieldName : qualifier;
124
            Optional<Entry<Class<?>, Class<?>>> optional = implementationClasses.stream()
125
                    .filter(entry -> entry.getKey().getSimpleName().equalsIgnoreCase(findBy)).findAny();
126
            if (optional.isPresent()) {
127
                return optional.get().getKey();
128
            } else {
129
                errorMessage = "There are " + implementationClasses.size() + " of interface " + interfaceClass.getName()
130
                        + " Expected single implementation or make use of @CustomQualifier to resolve conflict";
131
            }
132
        }
133
        throw new RuntimeErrorException(new Error(errorMessage));
134
    }
135
}


InjectionUtil.java  

This class heavily uses basic method provided by the java.lang.reflect.Field. 

The autowire() method in this class is recursive method because it takes care of injecting dependencies declared inside the service classes. (ie. nested dependencies)

Java
 




xxxxxxxxxx
1
46


 
1
import java.util.*;
2
import java.lang.reflect.Field;
3
 
          
4
import com.useraccount.di.framework.CustomInjector;
5
import com.useraccount.di.framework.annotations.*;
6
 
          
7
public class InjectionUtil {
8
 
          
9
    private InjectionUtil() {
10
        super();
11
    }
12
 
          
13
    /**
14
     * Perform injection recursively, for each service inside the Client class
15
     */
16
    public static void autowire(CustomInjector injector, Class<?> classz, Object classInstance)
17
            throws InstantiationException, IllegalAccessException {
18
        Set<Field> fields = findFields(classz);
19
        for (Field field : fields) {
20
            String qualifier = field.isAnnotationPresent(CustomQualifier.class)
21
                    ? field.getAnnotation(CustomQualifier.class).value()
22
                    : null;
23
            Object fieldInstance = injector.getBeanInstance(field.getType(), field.getName(), qualifier);
24
            field.set(classInstance, fieldInstance);
25
            autowire(injector, fieldInstance.getClass(), fieldInstance);
26
        }
27
    }
28
 
          
29
    /**
30
     * Get all the fields having CustomAutowired annotation used while declaration
31
     */
32
    private static Set<Field> findFields(Class<?> classz) {
33
        Set<Field> set = new HashSet<>();
34
        while (classz != null) {
35
            for (Field field : classz.getDeclaredFields()) {
36
                if (field.isAnnotationPresent(CustomAutowired.class)) {
37
                    field.setAccessible(true);
38
                    set.add(field);
39
                }
40
            }
41
            classz = classz.getSuperclass();
42
        }
43
        return set;
44
    }
45
}


ClassLoaderUtil.java

This class uses java.io.File to get the java files under root directory and sub directories for input package name and basic methods provided by java.lang.ClassLoader to get the list of all classes.

Java
 




xxxxxxxxxx
1
50


 
1
import java.io.File;
2
import java.io.IOException;
3
import java.net.URL;
4
import java.util.ArrayList;
5
import java.util.Enumeration;
6
import java.util.List;
7
 
          
8
public class ClassLoaderUtil {
9
 
          
10
    /**
11
     * Get all the classes for the input package
12
     */
13
    public static Class<?>[] getClasses(String packageName) throws ClassNotFoundException, IOException {
14
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
15
        assert classLoader != null;
16
        String path = packageName.replace('.', '/');
17
        Enumeration<URL> resources = classLoader.getResources(path);
18
        List<File> dirs = new ArrayList<>();
19
        while (resources.hasMoreElements()) {
20
            URL resource = resources.nextElement();
21
            dirs.add(new File(resource.getFile()));
22
        }
23
        List<Class<?>> classes = new ArrayList<>();
24
        for (File directory : dirs) {
25
            classes.addAll(findClasses(directory, packageName));
26
        }
27
        return classes.toArray(new Class[classes.size()]);
28
    }
29
 
          
30
    /**
31
     * Get all the classes for the input package, inside the input directory
32
     */
33
    public static List<Class<?>> findClasses(File directory, String packageName) throws ClassNotFoundException {
34
        List<Class<?>> classes = new ArrayList<>();
35
        if (!directory.exists()) {
36
            return classes;
37
        }
38
        File[] files = directory.listFiles();
39
        for (File file : files) {
40
            if (file.isDirectory()) {
41
                assert !file.getName().contains(".");
42
                classes.addAll(findClasses(file, packageName + "." + file.getName()));
43
            } else if (file.getName().endsWith(".class")) {
44
                String className = packageName + '.' + file.getName().substring(0, file.getName().length() - 6);
45
                classes.add(Class.forName(className));
46
            }
47
        }
48
        return classes;
49
    }
50
}


Application main class: 

UserAccountApplication.java

Java
 




xxxxxxxxxx
1
11


 
1
import com.useraccount.di.framework.CustomInjector;
2
 
          
3
public class UserAccountApplication {
4
 
          
5
    public static void main(String[] args) {
6
        CustomInjector.startApplication(UserAccountApplication.class);
7
 
          
8
        CustomInjector.getService(UserAccountClientComponent.class).displayUserAccount();
9
    }
10
}


Below is the comparison with dependencies added by spring.

1. Spring boot Dependencies:

2. Dependencies in this implementation:


Conclusion

This article should give a clear understanding of how DI or autowire dependencies work.

With the implementation of your own DI framework, you don't need heavy frameworks like Spring Boot. If you are really not using most of the by Spring Boot or any DI framework features in your application, like Bean Life cycle Management method executions and much more heavy stuff.

You can do a lot of things which are not mentioned here, by adding more user-defined annotations for various purpose eg. like bean scopes singleton, prototype, request, session, global-session, and many more features similar to Spring framework provides.

Thanks for taking the time to read this article, and I hope this gives a clear picture of how to use and internal working of Dependency Injection.

You can download/clone the tutorial shared here on GitHub.

Dependency injection Java (programming language) Implementation Spring Framework

Opinions expressed by DZone contributors are their own.

Related

  • Dependency Injection in Spring
  • Protect Your Invariants!
  • Smart Dependency Injection With Spring: Assignability (Part 2 of 3)
  • Spring Beans With Auto-Generated Implementations: How-To

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!