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.
- 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; }
}
- 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.
- 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.
- 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:
Stringstarting withredirect:→ 302 redirectModelAndView→ rendered by FreeMarker- Everything else → serialized to JSON
- 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);
}
}
- 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.
- 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.