|
| 1 | +--- |
| 2 | +slug: build-java-ioc-di-framework-from-scratch |
| 3 | +title: Build Java IoC/DI framework from scratch |
| 4 | +authors: [tu] |
| 5 | +tags: [java, ioc] |
| 6 | +--- |
| 7 | + |
| 8 | +When developing Keva project, I was struggled at finding a suitable IoC/DI framework: choose between [Spring](https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/beans.html), [Guice](https://github.com/google/guice), and others. |
| 9 | +While Spring is a popular choice, it is not a good choice for a project with a small number of components and need to start fast. |
| 10 | +On the other hand, Guice is also a popular choice, seems like it will start faster than Spring (because no need to scan class path for components), |
| 11 | +but I personally don't like its APIs with a lot of boilerplate (define explicit bindings, etc.). |
| 12 | + |
| 13 | +Finally, I've decided to build a Java IoC/DI framework from scratch, with Spring's IoC API and just contains the bare minimum logics of a DI framework. |
| 14 | +That means to remove almost the "magic" part of Spring IoC, and just focus on the core logics: create and manage beans, and inject dependencies. |
| 15 | + |
| 16 | +## Why need a DI/IoC? |
| 17 | + |
| 18 | +While some others can prefer writing code without DI/IoC: manually init instance/component and manually inject them, |
| 19 | +just like below: |
| 20 | + |
| 21 | +```java |
| 22 | +var svc = new ShippingService(new ProductLocator(), |
| 23 | + new PricingService(), new InventoryService(), |
| 24 | + new TrackingRepository(new ConfigProvider()), |
| 25 | + new Logger(new EmailLogger(new ConfigProvider()))); |
| 26 | +``` |
| 27 | + |
| 28 | +Many don't realize that their dependencies chain can become nested, and it quickly becomes unwieldy to wire them up manually. |
| 29 | +Even with factories (factory pattern), the duplication of your code is just not worth it. |
| 30 | + |
| 31 | +DI/IoC can help to init instance/component and inject them, and it's also automatically wire them up, so you don't have to write code manually. |
| 32 | +It also can be used to decouple the classes and improve testability, so we can get many of the benefits. |
| 33 | + |
| 34 | +But is that (IoC framework) creates magic? Yes, if you can trust the fact that this code does its job, |
| 35 | +then you can safely skip all of that property wrapping mumbo-jumbo. You've got other problems to solve. |
| 36 | + |
| 37 | +## How Keva IoC works |
| 38 | + |
| 39 | +Since Keva IoC is writing from scratch, I can control how magic the IoC framework will be, thus remove the unnecessary magic likes: bean lifecycle, property wrapping, etc. |
| 40 | + |
| 41 | +For just the bare minimal logics of a DI framework, it contains: |
| 42 | + |
| 43 | +- Scan beans (scan the `@Component` annotated classes) |
| 44 | +- Get the `beans` definitions, then create the `beans` |
| 45 | +- Store `beans` in a "bean container" |
| 46 | +- Scan the `@Autowire` annotations, then automatically inject dependencies |
| 47 | +## Implement Keva IoC |
| 48 | + |
| 49 | +Create annotation `@Component` first: |
| 50 | + |
| 51 | +```java |
| 52 | +@Retention(RetentionPolicy.RUNTIME) |
| 53 | +@Target(ElementType.TYPE) |
| 54 | +public @interface Component { |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +Create annotation `@Autowired`: |
| 59 | + |
| 60 | +```java |
| 61 | +@Target({ ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD }) |
| 62 | +@Retention(RetentionPolicy.RUNTIME) |
| 63 | +@Documented |
| 64 | +public @interface Autowired { |
| 65 | +} |
| 66 | +``` |
| 67 | + |
| 68 | +Since `@Autowired` is injected by type, but dependency injection may also be injected by name, the annotation `@Qualifier` is created: |
| 69 | + |
| 70 | +```java |
| 71 | +@Target({ ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE }) |
| 72 | +@Retention(RetentionPolicy.RUNTIME) |
| 73 | +@Inherited |
| 74 | +@Documented |
| 75 | +public @interface Qualifier { |
| 76 | + String value() default ""; |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +How to scan beans? We need a package helps to scan all the class in th `classpath`, [org.reflections](https://github.com/ronmamo/reflections) is a good choice. |
| 81 | + |
| 82 | +```java |
| 83 | +public static List<Class<?>> getClasses(String packageName) { |
| 84 | + List<Class<?>> classes=new ArrayList<>(); |
| 85 | + String path = packageName.replace('.','/'); |
| 86 | + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); |
| 87 | + URI pkg = Objects.requireNonNull(classLoader.getResource(path)).toURI(); |
| 88 | + Enumeration<URL> resources = classLoader.getResources(path); |
| 89 | + List<File> dirs = new ArrayList<>(); |
| 90 | + while (resources.hasMoreElements()) { |
| 91 | + URL resource = resources.nextElement(); |
| 92 | + dirs.add(new File(resource.getFile())); |
| 93 | + } |
| 94 | + for (File directory : dirs){ |
| 95 | + classes.addAll(findClasses(directory,packageName)); |
| 96 | + } |
| 97 | + return classes; |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +We have a `BeanContainer` class to store and manage all the beans: |
| 102 | + |
| 103 | +```java |
| 104 | +public class BeanContainer { |
| 105 | + public final Map<Class<?>, Map<String, Object>> beans = new HashMap<>(10); |
| 106 | + // ... |
| 107 | +``` |
| 108 | + |
| 109 | +After scanned and created all the `beans`, next we have to scan all the `@Autowire` annotations, and inject the dependencies: |
| 110 | + |
| 111 | +```java |
| 112 | +private void fieldInject(Class<?> clazz, Object classInstance) { |
| 113 | + Set<Field> fields = FinderUtil.findFields(clazz, Autowired.class); |
| 114 | + for (Field field : fields) { |
| 115 | + String qualifier = field.isAnnotationPresent(Qualifier.class) ? field.getAnnotation(Qualifier.class).value() : null; |
| 116 | + Object fieldInstance = _getBean(field.getType(), field.getName(), qualifier, true); |
| 117 | + field.set(classInstance, fieldInstance); |
| 118 | + } |
| 119 | +} |
| 120 | +``` |
| 121 | + |
| 122 | +That's basically the core logics of Keva IoC, for more details, please refer to [Keva IoC source code](https://github.com/keva-dev/keva-ioc/). |
| 123 | + |
| 124 | +## KevaIoC usage |
| 125 | + |
| 126 | +Let's say we have an interface `Engine.java`: |
| 127 | + |
| 128 | +```java |
| 129 | +public interface Engine { |
| 130 | + String getName(); |
| 131 | +} |
| 132 | +``` |
| 133 | + |
| 134 | +And we have a class `V8Engine.java` that implements `Engine`: |
| 135 | + |
| 136 | +```java |
| 137 | +@Component |
| 138 | +public class V8Engine implements Engine { |
| 139 | + public String getName() { |
| 140 | + return "V8"; |
| 141 | + } |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +And `SpiderMonkeyEngine.java` also implements `Engine`: |
| 146 | + |
| 147 | +```java |
| 148 | +@Component |
| 149 | +public class SpiderMonkeyEngine implements Engine { |
| 150 | + public String getName() { |
| 151 | + return "SpiderMonkey"; |
| 152 | + } |
| 153 | +} |
| 154 | +``` |
| 155 | + |
| 156 | +And a `Browser.java` class that need to inject an `Engine` implementation: |
| 157 | + |
| 158 | +```java |
| 159 | +@Component |
| 160 | +public class Browser { |
| 161 | + @Autowired |
| 162 | + String version; |
| 163 | + |
| 164 | + Engine engine; |
| 165 | + BrowserRenderer renderer; |
| 166 | + |
| 167 | + @Autowired |
| 168 | + public Browser(@Qualifier("v8Engine") Engine engine, BrowserRenderer renderer) { |
| 169 | + this.engine = engine; |
| 170 | + this.renderer = renderer; |
| 171 | + } |
| 172 | + |
| 173 | + public String run() { |
| 174 | + return renderer.render("This browser run on " + engine.getName()); |
| 175 | + } |
| 176 | + |
| 177 | + public String getVersion() { |
| 178 | + return renderer.render("Browser version: " + version); |
| 179 | + } |
| 180 | +} |
| 181 | +``` |
| 182 | + |
| 183 | +And the `Main.class` be like: |
| 184 | + |
| 185 | +```java |
| 186 | +public class Main { |
| 187 | + public static void main(String[] args) { |
| 188 | + KevaIoC context = KevaIoC.initBeans(Main.class); |
| 189 | + Browser browser = context.getBean(Browser.class); |
| 190 | + System.out.println(browser.run()); |
| 191 | + } |
| 192 | +} |
| 193 | +``` |
| 194 | + |
| 195 | +The APIs basically looks the same as Spring IoC, only the actual implementation is simpler and more concise, with less magic. |
| 196 | +Still the Keva codebase is clean and easy to understand based on elegant Spring IoC's API similar, and the startup time remains very fast due to its simplicity. |
| 197 | +
|
| 198 | +## Summary |
| 199 | +
|
| 200 | +Some of the Keva IoC's main features are: |
| 201 | + |
| 202 | +- Spring-like annotation-support, no XML |
| 203 | +- Fast startup time, small memory footprint (see performance section soon) |
| 204 | +- Pocket-sized, only basic features (no bean's lifecycle, no "Spring's magic") |
| 205 | +- Less opinionated, support mount existing beans (means can integrate well with other IoC/DI frameworks) |
| 206 | + |
| 207 | +Supported annotations: |
| 208 | + |
| 209 | +- `@ComponentScan` |
| 210 | +- `@Component` |
| 211 | +- `@Configuration` |
| 212 | +- `@Bean` |
| 213 | +- `@Autowired` (supports field injection, setter injection and constructor injection) |
| 214 | +- `@Qualifier` |
| 215 | +- Support mount existing beans via `.initBeans(Main.class, beanOne, beanTwo...)` static method |
| 216 | + |
| 217 | +KevaIoC is very fit for small applications, that has to have small memory footprint, small jar size and fast startup time, |
| 218 | +for example plugins, (embedded) standalone application, integration tests, jobs, Android applications, etc. |
| 219 | + |
| 220 | +Maybe in the future if more logic needed from Keva, I'll add more "magic" features, like bean's lifecycle, etc, but for now, it's enough. |
0 commit comments