Skip to content

Commit 0e807f5

Browse files
committed
add KevaIoc blog (#145)
1 parent dcff9ea commit 0e807f5

File tree

2 files changed

+226
-0
lines changed

2 files changed

+226
-0
lines changed
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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.

website/blog/authors.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
tu:
2+
name: Tu Huynh
3+
title: Keva maintainer
4+
url: https://tuhuynh.com/
5+
image_url: https://avatars.githubusercontent.com/u/13906546?v=4
6+
17
blu:
28
name: Viet Nguyen (Blu)
39
title: Keva @ Core/Replication

0 commit comments

Comments
 (0)