This is the third part of my "DI From Scratch" series. In the previous article, we built a basic DI container. Now, we want to take it yet another step further and automatically discover the available service classes.
DI Stage 7: Auto-detecting Services
Find the source code of this section on github
Our current state represents (a strongly simplified, yet functional) version of libraries such as Google Guice. However, if you are familiar with Spring Boot, it goes one step further. Do you think that it's annoying that we have to specify the service classes explicitly in a set? Wouldn't it be nice if there was a way to auto-detect service classes? Let's find them!
public class ClassPathScanner {
// this code is very much simplified; it works, but do not use it in production!
public static Set<Class<?>> getAllClassesInPackage(String packageName) throws Exception {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
String path = packageName.replace('.', '/');
Enumeration<URL> resources = classLoader.getResources(path);
List<File> dirs = new ArrayList<>();
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
dirs.add(new File(resource.getFile()));
}
Set<Class<?>> classes = new HashSet<>();
for (File directory : dirs) {
classes.addAll(findClasses(directory, packageName));
}
return classes;
}
private static List<Class<?>> findClasses(File directory, String packageName) throws Exception {
List<Class<?>> classes = new ArrayList<>();
if (!directory.exists()) {
return classes;
}
File[] files = directory.listFiles();
for (File file : files) {
if (file.isDirectory()) {
classes.addAll(findClasses(file, packageName + "." + file.getName()));
} else if (file.getName().endsWith(".class")) {
classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)));
}
}
return classes;
}
}
Here we have to deal with the ClassLoader
API. This particular API is quite old and dates back to the earliest days of Java, but it still works. We start with a packageName
to scan for. Each Thread
in the JVM has a contextClassLoader
assigned, from which the Class
objects are being loaded. Since the classloader operates on files, we need to convert the package name into a file path (replacing '.'
by '/'
). Then, we ask the classloader for all resources in this path, and convert them into File
s one by one. In practice, there will be only one resource here: our package, represented as a directory.
From there, we recursively iterate over the file tree of our directory package, loking for files ending in .class
. We convert every class file we encounter into a class name (cutting off the trailing .class
) to end up with our class name. Then, we finally call Class.forName(...)
on it to retrieve the class.
So we have a way to retrieve all classes in our base package. How do we use it? Let's add a static factory method to our DIContext
class that produces a DIContext
for a given base package:
public static DIContext createContextForPackage(String rootPackageName) throws Exception {
Set<Class<?>> allClassesInPackage = ClassPathScanner.getAllClassesInPackage(rootPackageName);
Set<Class<?>> serviceClasses = new HashSet<>();
for(Class<?> aClass : allClassesInPackage){
serviceClasses.add(aClass);
}
return new DIContext(serviceClasses);
}
Finally, we need to make use of this new factory method in our createContext()
method:
private static DIContext createContext() throws Exception {
String rootPackageName = Main.class.getPackage().getName();
return DIContext.createContextForPackage(rootPackageName);
}
We retrieve the base package name from the Main
class (the class I've used to contain my main()
method).
But wait! We have a problem. Our classpath scanner will detect all classes, whether they are services or not. We need to tell the algorithm which ones we want with - you guessed it - an annotation:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
}
Let's annotate our services with it:
@Service
public class ServiceAImpl implements ServiceA { ... }
@Service
public class ServiceBImpl implements ServiceB { ... }
... and filter our classes accordingly:
public static DIContext createContextForPackage(String rootPackageName) throws Exception {
Set<Class<?>> allClassesInPackage = ClassPathScanner.getAllClassesInPackage(rootPackageName);
Set<Class<?>> serviceClasses = new HashSet<>();
for(Class<?> aClass : allClassesInPackage){
if(aClass.isAnnotationPresent(Service.class)){
serviceClasses.add(aClass);
}
}
return new DIContext(serviceClasses);
}
We are done... are we?
And there you have it - a minimalistic, poorly optimized, yet fully functional DI container. But hold on, why do you need several megabytes worth of library code if the core is so simple? Well...
Our classpath scanner is very wonky. It certainly doesn't cover all cases, nesting depths, inner classes etc. Libraries like Guava's
ClassPath
do a much better job at this.A big advantage of DI is that we can hook into the lifecycle of a service. For example, we might want to do something once the service has been created (
@PostConstruct
). We might want to inject dependencies via setters, not fields. We might want to use constructor injection, as we did in the beginning. We might want to wrap our services in proxies to have code executed before and after each method (e.g.@Transactional
). All of those "bells and whistles" are provided e.g. by Spring.Our wiring algorithm doesn't respect base classes (and their fields) at all.
Our
getServiceInstance(...)
method is very poorly optimized, as it linearly scans for the matching instance every time.You will certainly want to have different contexts for testing and production. If you are interested in that, have a look at Spring Profiles.
We only have one way of defining services; some might require additional configuration. See Springs
@Configuration
and@Bean
annotations for details on that.Many other small bits and pieces.
Summary
We have created a very simple DI container which:
- encapsulates the creation of a service network
- creates the services and wires them together
- is capable of scanning the classpath for service classes
- showcases the use of reflection and annotations
We also discussed the reasoning for our choices:
- First, we replaced static references by objects and constructors.
- Then, we introduced interfaces to further decouple the objects.
- We discovered that cyclic dependencies are a problem, so we introduced setters.
- We observed that calling all setters for building the service network manually is error-prone. We resorted to reflection to automate this process.
- Finally, we added classpath scanning to auto-detect service classes.
If you came this far, thanks for reading along! I hope you enjoyed the read.
Top comments (8)
Thanks for this very nice article on DI. It was great working stepwise through the refinement procedure which helped make the subject really come to life.
FYI, I translated the exercise code (as parallel as possible) into C# in .net core and was amazed at how, once again, your stepwise approach made it so much easier to reason out. The code is on github.com/ChrisLaforet/Understand... if you are interested. Thanks again :)
@martin Hausler, thanks for this post. Please how can I make this program accept configuration files (XML or text files)? Thanks for your time.
Hi! Sorry for the late response, somehow your "@" mention didn't work and I didn't get notified about your answer.
The XML/Text files you refer to would replace the classpath scan. Instead of scanning all classes, you'll read a predefined list of service class names from a file, typically XML. Let's simplify it a bit and use plain text, for example:
(left is interface, right is implementation)
This would tell you that the interface "MyServiceA" is implemented by a class "MyServiceAImpl". To find the classes, you'd simply have to use
Class.forName(className)
. Use those interface/class pairs instead of the classpath scan.Thanks a lot, I'm very grateful.
Very informative and flawlessly written!
Thanks, glad you enjoyed it!
Finally I can grasp the picture. This is the best explanation I've ever seen. Thank you so much :)