Building a Miniature Spring Framework from Scratch

This walkthrough demonstrates how to recreate the essential parts of the Spring ecosystem in a compact, learning-oriented codebase named Summer Framework. The goal is not feature parity but a clear, step-by-step construction of an IoC container, AOP engine, JDBC helper, declarative transactions, Web MVC, and a Spring-Boot-style launcher—each trimmed to its core concepts.

  1. IoC Container

The heart of any Spring-like system is the ApplicationContext. Summer keeps only AnnotationConfigApplicationContext, drops XML, and supports singleton beans discovered via @ComponentScan.

public class AnnotationConfigApplicationContext implements ConfigurableApplicationContext {
    private final Map<String, BeanDefinition> registry = new ConcurrentHashMap<>();
    private final List<BeanPostProcessor> postProcessors = new ArrayList<>();
    private final PropertyResolver propertyResolver;

    public AnnotationConfigApplicationContext(Class<?> config, PropertyResolver pr) {
        this.propertyResolver = pr;
        scan(config);
        instantiateSingletons();
        injectDependencies();
        initializeBeans();
    }
}

BeanDefinition stores only what is strictly needed: bean name, type, factory details, init/destroy hooks, and a single instance slot.

1.1 Classpath Scanning

ResourceResolver walks JAR files and directories, turning com/example/Foo.class into the fully-qualified name com.example.Foo.

public class ResourceResolver {
    public List<String> scan(String basePackage) {
        String path = basePackage.replace('.', '/');
        Enumeration<URL> roots = Thread.currentThread()
                                         .getContextClassLoader()
                                         .getResources(path);
        ...
    }
}

1.2 Property Injection

PropertyResolver merges YAML, properties files, environment variables, and supports ${key:default} placeholders with pluggable type converters.

public class PropertyResolver {
    private final Map<String, String> props = new HashMap<>();
    private final Map<Class<?>, Function<String, Object>> converters = new HashMap<>();

    public <T> T getProperty(String key, Class<T> target) {
        String value = resolve(key);
        return (T) converters.get(target).apply(value);
    }
}

1.3 Dependency Injection & Lifecycle

Constructor injection is handled first (strong dependencies); field/setter injection follows (weak dependencies). A Set<String> guards against constructor-level circular references. @PostConstruct and @PreDestroy are invoked via reflection.

1.4 BeanPostProcessor & Proxying

Post-processors can replace the bean instance—typical for proxies. To keep injection correct, the original object is remembered and exposed via postProcessOnSetProperty.

public interface BeanPostProcessor {
    Object postProcessBeforeInitialization(Object bean, String name);
    default Object postProcessOnSetProperty(Object bean, String name) { return bean; }
}
  1. AOP with ByteBuddy

Instead of CGLIB, Summer uses ByteBuddy to generate runtime subclasses. The ProxyResolver is ~50 lines:

public class ProxyResolver {
    private final ByteBuddy byteBuddy = new ByteBuddy();

    public <T> T createProxy(T target, InvocationHandler handler) {
        Class<?> proxy = byteBuddy.subclass(target.getClass())
                                    .method(ElementMatchers.any())
                                    .intercept(InvocationHandlerAdapter.of(handler))
                                    .make()
                                    .load(target.getClass().getClassLoader())
                                    .getLoaded();
        return (T) proxy.getDeclaredConstructor().newInstance();
    }
}

@Around("handlerBeanName") on a class triggers AroundProxyBeanPostProcessor, which wraps the bean with the named InvocationHandler. @Transactional is just another annotation-driven post-processor that wraps methods in connection-handling proxies.

  1. JDBC & Declarative Transactions

JdbcTemplate offers query, update, and execute callbacks. It joins any ongoing transaction stored in a ThreadLocal<Connection> managed by DataSourceTransactionManager.

@Transactional
public class UserService {
    @Autowired JdbcTemplate jdbc;

    public void register(String email, String pwd) {
        jdbc.update("INSERT INTO users(email, pwd) VALUES (?,?)", email, pwd);
    }
}

Only REQUIRED propagation is supported; isolation levels and save-points are intentionally omitted for brevity.

  1. Web MVC

A single DispatcherServlet (registered automatical via ContextLoaderListener) inspects every @Controller/@RestController at startup, building a list of Dispatcher objects that hold compiled regex patterns for paths and parameter metadata.

@RestController
public class HelloApi {
    @GetMapping("/hello/{name}")
    public Map<String, String> greet(@PathVariable String name) {
        return Map.of("msg", "Hello, " + name);
    }
}

Return values are routed as follows:

  • String starting with redirect: → 302 redirect
  • ModelAndView → rendered by FreeMarker
  • Everything else → serialized to JSON
  1. Boot Module

SummerApplication.run(...) starts an embedded Tomcat, unpacks the WAR to a temp folder if needed, and wires the classpath so java -jar app.war just works. The launcher is under 200 lines and deliberate avoids the complexity of Spring Boot’s custom class-loader.

public class Main {
    public static void main(String[] args) {
        SummerApplication.run("src/main/webapp", "target/classes", AppConfig.class, args);
    }
}
  1. Packaging & Runtime

The Maven build duplicates compiled classes into the WAR root, adds Main-Class and Class-Path entries to META-INF/MANIFEST.MF, and keeps WEB-INF/lib for Tomcat’s internal loader. The resulting artifact is both an executable fat-jar and a deployable WAR.

  1. Final Thoughts

In roughly 1 000 lines of non-test code, Summer Framework demonstrates:

  • Classpath scanning & annotation-driven configuration
  • Constructor/field/setter injection with circular-dependency detection
  • Pluggable post-processors for proxying and cross-cutting concerns
  • Template-based JDBC and minimal transaction management
  • Annotation-based MVC with path variables, JSON, and view resolution
  • Self-contained bootable web application

While far from Spring’s industrial breadth, the exercise crystallizes the foundational patterns that make modern Java frameworks tick.

Tags: ioc-container ByteBuddy aop JDBC-template declarative-transactions

Posted on Sat, 23 May 2026 20:05:42 +0000 by Zup