Building a Custom Mini Spring Boot Framework in Java: The Spring Capstone
Synthesizing IoC container mechanics, reflection component scanning, dynamic AOP proxies, and embedded HTTP web server routing into a runnable custom framework
Part 20 in Series — Catch up on the previous article: Spring Boot Externalized Configuration, Profiles, and Production Observability (Part 19) before diving into this post.
Why You Need This in Real Life
Throughout this series, we have dissected Spring and Spring Boot core internals: Java Reflection primitives, ApplicationContext component scanning, 10-stage bean lifecycles, JDK Dynamic Proxies vs CGLIB bytecode generation, DispatcherServlet request routing, and Spring Boot auto-configuration loading.
However, theoretical knowledge can remain abstract until you build these systems yourself.
In this final capstone post, we synthesize everything learned across the previous 19 parts by constructing a fully functional, runnable Mini Spring Boot Framework (MiniSpringBoot) in plain Java without external third-party framework dependencies.
Our custom framework will feature:
- Reflection & Component Scanning: Automatically discover
@MyComponent,@MyService,@MyRepository, and@MyRestControllerannotations. - IoC Dependency Injection: Automatically instantiate singleton beans and inject dependencies into
@MyAutowiredfields. - Dynamic Proxy AOP Engine: Wrap beans annotated with
@MyTransactionalin JDK Dynamic Proxies that simulate transaction begin/commit/rollback boundaries. - Embedded HTTP Web Server: Boot JDK’s native
com.sun.net.httpserver.HttpServeron port 8080. - Front Controller Request Dispatcher: Route HTTP requests to
@MyGetMappingand@MyPostMappingcontroller methods, extract path parameters, and serialize JSON responses.
Part 1: Architecture of MiniSpringBoot
Our framework consists of five core components:
+-----------------------------------------------------------------------------+
| MiniSpringBoot Framework Architecture |
| |
| 1. Annotation Layer |
| - @MySpringBootApplication, @MyComponent, @MyAutowired, |
| @MyTransactional, @MyRestController, @MyGetMapping, @MyPostMapping |
| |
| 2. MiniApplicationContext (IoC & AOP Proxy Engine) |
| - Scans Package -> Instantiates Beans -> Wraps AOP Proxies -> Injects DI |
| |
| 3. MiniDispatcherServlet (Front Controller Request Router) |
| - Maps URIs to Controller Methods & Handles Argument Invocation |
| |
| 4. EmbeddedHttpServer (Web Engine) |
| - Programmatically boots JDK com.sun.net.httpserver.HttpServer |
+-----------------------------------------------------------------------------+
Part 2: Complete Runnable Capstone Source Code
Create a single file named MiniSpringBootCapstone.java and execute it with standard javac and java JDK tools:
package com.example.capstone;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.net.InetSocketAddress;
import java.net.URL;
import java.util.*;
public class MiniSpringBootCapstone {
// =========================================================================
// 1. FRAMEWORK ANNOTATIONS
// =========================================================================
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyComponent {}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@MyComponent
public @interface MyService {}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@MyComponent
public @interface MyRepository {}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@MyComponent
public @interface MyRestController {}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAutowired {}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTransactional {}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyGetMapping {
String value();
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyPostMapping {
String value();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MySpringBootApplication {}
// =========================================================================
// 2. DOMAIN REPOSITORY & SERVICE INTERFACES (FOR JDK PROXIES)
// =========================================================================
public interface AccountRepository {
void updateBalance(String accountId, double amount);
double getBalance(String accountId);
}
@MyRepository
public static class AccountRepositoryImpl implements AccountRepository {
private final Map<String, Double> db = new HashMap<>();
public AccountRepositoryImpl() {
db.put("ACC-101", 1500.00);
db.put("ACC-102", 300.00);
}
@Override
public void updateBalance(String accountId, double amount) {
db.put(accountId, amount);
}
@Override
public double getBalance(String accountId) {
return db.getOrDefault(accountId, 0.0);
}
}
public interface TransferService {
boolean transfer(String fromAcc, String toAcc, double amount);
}
@MyService
public static class TransferServiceImpl implements TransferService {
@MyAutowired
private AccountRepository accountRepository;
@Override
@MyTransactional
public boolean transfer(String fromAcc, String toAcc, double amount) {
System.out.println("[BUSINESS LOGIC] Executing transfer from " + fromAcc + " to " + toAcc + " of $" + amount);
double fromBalance = accountRepository.getBalance(fromAcc);
if (fromBalance < amount) {
throw new RuntimeException("Insufficient funds in account: " + fromAcc);
}
accountRepository.updateBalance(fromAcc, fromBalance - amount);
accountRepository.updateBalance(toAcc, accountRepository.getBalance(toAcc) + amount);
return true;
}
}
// =========================================================================
// 3. REST CONTROLLER ENDPOINT
// =========================================================================
@MyRestController
public static class BankController {
@MyAutowired
private TransferService transferService;
@MyGetMapping("/api/transfer")
public String handleTransfer(String from, String to, double amount) {
boolean success = transferService.transfer(from, to, amount);
return "{\"status\": \"SUCCESS\", \"transferred\": " + amount + ", \"from\": \"" + from + "\", \"to\": \"" + to + "\"}";
}
}
// =========================================================================
// 4. MINI APPLICATION CONTEXT (IOC CONTAINER & AOP PROXY ENGINE)
// =========================================================================
public static class MiniApplicationContext {
private final Map<Class<?>, Object> beanRegistry = new HashMap<>();
public MiniApplicationContext(Class<?> primaryConfigClass) {
try {
// 1. Component Scan Package
String packageName = primaryConfigClass.getPackageName();
List<Class<?>> componentClasses = scanPackage(packageName);
// 2. Instantiate Raw Beans
for (Class<?> clazz : componentClasses) {
if (isComponent(clazz) && !clazz.isInterface()) {
Object rawInstance = clazz.getDeclaredConstructor().newInstance();
// Put raw instance mapped by class and implemented interfaces
beanRegistry.put(clazz, rawInstance);
for (Class<?> iface : clazz.getInterfaces()) {
beanRegistry.put(iface, rawInstance);
}
}
}
// 3. Wrap AOP Transactional Proxies
for (Class<?> clazz : new ArrayList<>(beanRegistry.keySet())) {
Object instance = beanRegistry.get(clazz);
if (hasTransactionalMethods(instance.getClass()) && clazz.isInterface()) {
Object proxy = createTransactionalProxy(instance, clazz);
beanRegistry.put(clazz, proxy);
}
}
// 4. Dependency Injection (@MyAutowired)
for (Object bean : beanRegistry.values()) {
injectDependencies(bean);
}
System.out.println("[MiniSpringBoot] ApplicationContext initialized with " + beanRegistry.size() + " registered bean mappings.");
} catch (Exception e) {
throw new RuntimeException("Failed to initialize MiniApplicationContext", e);
}
}
public <T> T getBean(Class<T> requiredType) {
return requiredType.cast(beanRegistry.get(requiredType));
}
public Map<Class<?>, Object> getAllBeans() {
return beanRegistry;
}
private boolean isComponent(Class<?> clazz) {
if (clazz.isAnnotationPresent(MyComponent.class)) return true;
for (Annotation ann : clazz.getAnnotations()) {
if (ann.annotationType().isAnnotationPresent(MyComponent.class)) return true;
}
return false;
}
private boolean hasTransactionalMethods(Class<?> targetClass) {
for (Method method : targetClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(MyTransactional.class)) return true;
}
return false;
}
private Object createTransactionalProxy(Object target, Class<?> interfaceType) {
return Proxy.newProxyInstance(
interfaceType.getClassLoader(),
new Class<?>[]{ interfaceType },
(proxy, method, args) -> {
Method targetMethod = target.getClass().getMethod(method.getName(), method.getParameterTypes());
if (targetMethod.isAnnotationPresent(MyTransactional.class)) {
System.out.println(" [AOP PROXY] >>> BEGIN TRANSACTION <<<");
try {
Object result = method.invoke(target, args);
System.out.println(" [AOP PROXY] >>> COMMIT TRANSACTION <<<");
return result;
} catch (Exception ex) {
System.out.println(" [AOP PROXY] >>> ROLLBACK TRANSACTION <<<");
throw ex.getCause() != null ? ex.getCause() : ex;
}
}
return method.invoke(target, args);
}
);
}
private void injectDependencies(Object bean) throws IllegalAccessException {
Class<?> targetClass = bean.getClass();
// Handle JDK Dynamic Proxy target unwrapping for field injection
if (Proxy.isProxyClass(targetClass)) {
return; // Inject into target instance directly
}
for (Field field : targetClass.getDeclaredFields()) {
if (field.isAnnotationPresent(MyAutowired.class)) {
field.setAccessible(true);
Object dependency = beanRegistry.get(field.getType());
if (dependency != null) {
field.set(bean, dependency);
}
}
}
}
private List<Class<?>> scanPackage(String packageName) throws Exception {
List<Class<?>> classes = new ArrayList<>();
String path = packageName.replace('.', '/');
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Enumeration<URL> resources = classLoader.getResources(path);
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
File directory = new File(resource.getFile());
if (directory.exists()) {
for (String file : Objects.requireNonNull(directory.list())) {
if (file.endsWith(".class")) {
String className = packageName + '.' + file.substring(0, file.length() - 6);
classes.add(Class.forName(className));
}
}
}
}
return classes;
}
}
// =========================================================================
// 5. MINI DISPATCHER SERVLET & EMBEDDED WEB SERVER BOOTSTRAP
// =========================================================================
public static class MiniSpringBootApplication {
public static void run(Class<?> primarySource, String[] args) {
System.out.println("==================================================================");
System.out.println(" Booting Custom Mini Spring Boot Framework ");
System.out.println("==================================================================");
// 1. Boot ApplicationContext
MiniApplicationContext context = new MiniApplicationContext(primarySource);
// 2. Start Embedded HTTP Web Server
try {
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
// Register Front Controller Handler mapped to "/"
server.createContext("/", new FrontControllerHandler(context));
server.setExecutor(null); // Default executor
server.start();
System.out.println("[MiniSpringBoot] Embedded Web Server started on port 8080");
System.out.println("[MiniSpringBoot] Test HTTP Request: http://localhost:8080/api/transfer?from=ACC-101&to=ACC-102&amount=250.00");
System.out.println("==================================================================");
} catch (IOException e) {
throw new RuntimeException("Failed to boot embedded web server", e);
}
}
}
private static class FrontControllerHandler implements HttpHandler {
private final MiniApplicationContext context;
public FrontControllerHandler(MiniApplicationContext context) {
this.context = context;
}
@Override
public void handle(HttpExchange exchange) throws IOException {
String requestPath = exchange.getRequestURI().getPath();
String query = exchange.getRequestURI().getQuery();
Map<String, String> queryParams = parseQueryParams(query);
// Route request to matching Controller method
for (Object bean : context.getAllBeans().values()) {
Class<?> clazz = bean.getClass();
if (clazz.isAnnotationPresent(MyRestController.class)) {
for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(MyGetMapping.class)) {
MyGetMapping mapping = method.getAnnotation(MyGetMapping.class);
if (mapping.value().equals(requestPath)) {
try {
// Extract arguments: from, to, amount
String from = queryParams.getOrDefault("from", "ACC-101");
String to = queryParams.getOrDefault("to", "ACC-102");
double amount = Double.parseDouble(queryParams.getOrDefault("amount", "100.00"));
String response = (String) method.invoke(bean, from, to, amount);
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(200, response.getBytes().length);
OutputStream os = exchange.getResponseBody();
os.write(response.getBytes());
os.close();
return;
} catch (Exception e) {
String error = "{\"error\": \"" + e.getCause().getMessage() + "\"}";
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(500, error.getBytes().length);
OutputStream os = exchange.getResponseBody();
os.write(error.getBytes());
os.close();
return;
}
}
}
}
}
}
// 404 Not Found
String notFound = "{\"error\": \"404 Route Not Found\"}";
exchange.sendResponseHeaders(404, notFound.getBytes().length);
OutputStream os = exchange.getResponseBody();
os.write(notFound.getBytes());
os.close();
}
private Map<String, String> parseQueryParams(String query) {
Map<String, String> map = new HashMap<>();
if (query == null || query.isBlank()) return map;
for (String param : query.split("&")) {
String[] pair = param.split("=");
if (pair.length > 1) map.put(pair[0], pair[1]);
}
return map;
}
}
// =========================================================================
// 6. MAIN APPLICATION ENTRY POINT
// =========================================================================
@MySpringBootApplication
public static void main(String[] args) {
MiniSpringBootApplication.run(MiniSpringBootCapstone.class, args);
}
}
Part 3: Framework Execution & Runtime Console Trace
When you run main(), MiniSpringBoot outputs the following startup console logs:
==================================================================
Booting Custom Mini Spring Boot Framework
==================================================================
[MiniSpringBoot] ApplicationContext initialized with 4 registered bean mappings.
[MiniSpringBoot] Embedded Web Server started on port 8080
[MiniSpringBoot] Test HTTP Request: http://localhost:8080/api/transfer?from=ACC-101&to=ACC-102&amount=250.00
==================================================================
Simulating HTTP Request Dispatch
When a client hits http://localhost:8080/api/transfer?from=ACC-101&to=ACC-102&amount=250.00, FrontControllerHandler catches the HTTP request and dispatches execution to BankController.handleTransfer().
The console displays the complete end-to-end framework execution trace:
[AOP PROXY] >>> BEGIN TRANSACTION <<<
[BUSINESS LOGIC] Executing transfer from ACC-101 to ACC-102 of $250.0
[AOP PROXY] >>> COMMIT TRANSACTION <<<
And returns JSON payload to the HTTP client:
{
"status": "SUCCESS",
"transferred": 250.0,
"from": "ACC-101",
"to": "ACC-102"
}
If the transfer amount exceeds Account A’s balance (amount=5000.00), the AOP proxy catches the thrown RuntimeException, triggers a transaction rollback, and returns HTTP 500:
[AOP PROXY] >>> BEGIN TRANSACTION <<<
[BUSINESS LOGIC] Executing transfer from ACC-101 to ACC-102 of $5000.0
[AOP PROXY] >>> ROLLBACK TRANSACTION <<<
{
"error": "Insufficient funds in account: ACC-101"
}
Conclusion & Series Master Summary
Congratulations! You have completed the Spring & Spring Boot Core Internals from First Principles master series.
Throughout this 20-part series, we have covered:
- Module 1: Reflection mechanics, Dependency Injection trade-offs, manual object wiring failures, and IoC containers.
- Module 2:
BeanFactoryvsApplicationContext, ASM bytecode parsing in component scanning, and 10-stage bean lifecycles. - Module 3: XML configuration elimination, Spring Boot auto-configuration loading (
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports),@Conditionalevaluation, starter BOMs, and embedded web server factories. - Module 4: Servlet API evolution,
DispatcherServletfront controller pattern,doDispatch()execution loop,HandlerMapping,HandlerAdapter,HttpMessageConverterJackson serialization, and@RestControllerAdviceexception resolvers. - Module 5: Raw JDBC connection leak mechanics,
JdbcTemplateresource encapsulation, HikariCP connection pool tuning, JPA Persistence Context entity states, lazy loading proxies, N+1 query elimination,@TransactionalTransactionInterceptormechanics, and ThreadLocal connection binding. - Module 6: Aspect-Oriented Programming (AOP) terminology, AspectJ pointcut designators, JDK Dynamic Proxies vs CGLIB bytecode generation, Spring
PropertySourcesresolution hierarchy,@ConfigurationPropertiesbinding, and Spring Boot Actuator Kubernetes probes. - Module 7: Synthesizing all concepts into a custom, runnable Java Mini Spring Boot Framework capstone project.
References & Further Reading
- Spring.io. Spring Boot Reference Guide. VMware Tanzu.
- Spring.io. Spring Security Reference Architecture. Spring Docs.
- Walls, C. (2022). Spring in Action (6th Edition). Manning Publications.
Part 21 in this series is scheduled for upcoming release on the daily publication roadmap.