5 minutes
Part 1 - Building a Mini Spring Framework (v0.1)-Dependency Injection Basics
The problem Spring solves
easy right ? let’s see :)
imagine you are building an e-commerce backend, and you have OrderService that needs to process payments
the naive approach is: inside the OrderService you write for example
PaymentProcess process = new PaypalService();
if your boss comes and says “we’re switching the contract from PayPal to Stripe” what do you have to do to your code?
The answer is use an interface
public interface PaymentProcess {
void pay(double amount);
}
and the OrderService looks like
public class OrderService {
private final PaymentProcess paymentProcess;
public OrderService(PaymentProcess payment) {
this.paymentProcess = payment;
}
public void checkout() {
paymentProcess.pay(100.00);
}
}
and PayPal or Stripe services look like
public class PaypalService implements PaymentService {
@Override
public void pay(double amount){
System.out.println("Paying " + amount + " € using PayPal");
}
}
the same for Stripe service
so now in the main class we create a new orderservice
public class Main {
public static void main(String[] args) {
PaymentProcess paymentProcess = new PaypalService();
OrderService orderService = new OrderService(paymentProcess);
orderService.checkout();
}
}
so spring container’s job is to eliminate that line of code
PaymentProcess paymentProcess = new PaypalService();
==> so we want the Container to call that constructor for us automatically
The Missing Piece: Configuration
so the problem here is like we indicated before if your company doesn’t support PayPal for any reason like boycott or any other reason you should delete it from the main and from the OrderService
so the idea is creating a class let’s call it BeanDefinition it will act like a configuration file “yes i know we should use xml file or annotation” but for mini-spring v01 we’ll use a class that contains a hashMap; key,value so the constructor of this class BeanDefinition will put as a key the PaymentProcess and as a value PaypalService
and now it’s easy if we want to change the PaypalService to StripeService, we’ll change it once => in this configuration file ok!
so the config class looks like
public class BeanDefinition {
Map<Class<?>, Class<?>> beanMapping = new HashMap<>();
public BeanDefinition(){
beanMapping.put(PaymentProcess.class, PaypalService.class);
}
}
good now we did the config file but we’re not done yet we should have the bridge between the config file (BeanDefinition) and the main method so we call it container: yep it’s like the spring container not really but you get the idea ;)
so the main method asks the container “I need a PaymentProcess” the container takes the request to the BeanDefinition map it looks up the key PaymentProcess and finds the value PaypalService
So the container creates the PaypalService magically! yep it’s the Reflection concept
OK! but what is java reflection
ok so this blog is not to deep dive into java reflection but think about it like the ability to manipulate java classes at runtime so when you do
Class<?> c = myClass.class;
// so c it's not a copy of myClass or something else
// it's a reflection of it
// like a mirror
// it's not like
myClass mc = new myClass();
// but c == mc is true!
read more about java reflection here
Nice now when you understand the reflection let’s move on to the master piece of our program which is the container or the IOC container like spring ;)
so the role of the container is the creation of the object in this case we want the paypalService created inside the container not inside the main remember ;)
before we write code the method that will create the container should:
- get an interfaceClass input of type Class < ? > in our case it’s PaymentProcess (why interfaceClass? cuz in the future we’ll have not just PaypalService)
- and then we go to our map and get the value and store it in an object class too
- then using reflection like you learned to create an instance from it
public class MiniContainer {
BeanDefinition config = new BeanDefinition();
public Object getBean(Class<?> interfaceClass) {
Class<?> myClass = config.beanMapping.get(interfaceClass); // so when we do map.get(key) we get the value: in this case it's paypal
if (myClass == null) {
throw new RuntimeException("Bean not found!");
}
Object object;
try {
Constructor<?> constructor = myClass.getDeclaredConstructor();
object = constructor.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
return object;
}
}
and boom you now created your own spring v01
small test :) ‘malk mkhlo3 HHH’
the result is true or false? and why
public class Main {
public static void main(String[] args) {
MiniContainer miniContainer = new MiniContainer();
PaymentProcess p1 = (PaymentProcess) miniContainer.getBean(PaymentProcess.class);
PaymentProcess p2 = (PaymentProcess) miniContainer.getBean(PaymentProcess.class);
System.out.println(p1 == p2);
}
}
send me a message if you did it :) message
and of course check the github-repo
flowchart TD
%% ===== BEFORE =====
subgraph BEFORE["Before: Hard-coded dependency"]
OS["OrderService"]
PS["PaypalService"]
OS -->|"new PaypalService()"| PS
end
%% ===== AFTER =====
subgraph AFTER["After: BeanDefinition + Container"]
A["App starts"]
BDNEW["Create BeanDefinition"]
A --> BDNEW
BDCONST["BeanDefinition constructor"]
BDNEW --> BDCONST
MAP["Map: PaymentProcess → PaypalService"]
BDCONST --> MAP
CONT["Container"]
MAIN["Main asks for PaymentProcess"]
MAIN --> CONT
CONT -->|"lookup PaymentProcess"| MAP
MAP -->|"returns PaypalService"| CONT
REFLECT["Reflection creates object"]
CONT --> REFLECT
PAYOBJ["PaypalService instance\n(as PaymentProcess)"]
REFLECT --> PAYOBJ
OS2["OrderService"]
PAYOBJ --> OS2
end