Implementing a Custom Spring MVC Framework in Java
This tutorial walks through building a lightweight Spring MVC clone in Java, covering the MVC workflow, nine core components, project setup, custom annotations, a DispatcherServlet implementation, and a test controller, with full source code and deployment instructions.
Spring framework is essential for Java backend developers, and studying its source code and design patterns provides valuable learning opportunities.
Understanding Spring MVC Flow and Components
Spring MVC follows the MVC pattern, with DispatcherServlet as the front controller that coordinates HandlerMapping , HandlerAdapter , ViewResolver , and other components to process requests and render responses.
The article enumerates the nine core components of Spring MVC and explains their responsibilities, such as multipart handling, locale resolution, theme resolution, handler mapping, adapter invocation, exception resolution, view resolution, and flash map management.
Designing a Custom Spring MVC
The tutorial outlines three steps: (1) understand the runtime process and nine components, (2) design the framework’s architecture, and (3) implement the framework based on the design.
Project Setup
A Maven project is created with the necessary dependencies (e.g., javax.servlet-api ) and a web.xml that registers MyDispatcherServlet . The application.properties file specifies the package to scan (e.g., scanPackage=com.liugh.core ).
Custom Annotations
Three annotations are defined to mimic Spring’s MVC annotations:
@MyController – marks a class as a controller and optionally provides a bean name.
@MyRequestMapping – can be placed on classes and methods to map URLs.
@MyRequestParam – placed on method parameters to bind request parameters.
package com.liugh.annotation;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyController {
String value() default "";
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyRequestMapping {
String value() default "";
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyRequestParam {
String value();
}DispatcherServlet Implementation
The core class MyDispatcherServlet extends HttpServlet and overrides init , doGet , and doPost . Its responsibilities include:
Loading configuration from application.properties .
Scanning the specified package for classes.
Instantiating beans annotated with @MyController and storing them in an IoC container.
Building a handler mapping that links URLs to controller methods.
Dispatching incoming requests: parsing the URL, locating the handler method, preparing method arguments (including HttpServletRequest , HttpServletResponse , and parameters annotated with @MyRequestParam ), and invoking the method via reflection.
package com.liugh.servlet;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import com.liugh.annotation.*;
public class MyDispatcherServlet extends HttpServlet {
private Properties properties = new Properties();
private List
classNames = new ArrayList<>();
private Map
ioc = new HashMap<>();
private Map
handlerMapping = new HashMap<>();
private Map
controllerMap = new HashMap<>();
@Override
public void init(ServletConfig config) throws ServletException {
doLoadConfig(config.getInitParameter("contextConfigLocation"));
doScanner(properties.getProperty("scanPackage"));
doInstance();
initHandlerMapping();
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
doDispatch(req, resp);
} catch (Exception e) {
resp.getWriter().write("500!! Server Exception");
}
}
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
if (handlerMapping.isEmpty()) return;
String url = req.getRequestURI().replace(req.getContextPath(), "").replaceAll("/+", "/");
if (!handlerMapping.containsKey(url)) {
resp.getWriter().write("404 NOT FOUND!");
return;
}
Method method = handlerMapping.get(url);
Class
[] paramTypes = method.getParameterTypes();
Map
paramMap = req.getParameterMap();
Object[] paramValues = new Object[paramTypes.length];
for (int i = 0; i < paramTypes.length; i++) {
String typeName = paramTypes[i].getSimpleName();
if (typeName.equals("HttpServletRequest")) {
paramValues[i] = req;
continue;
}
if (typeName.equals("HttpServletResponse")) {
paramValues[i] = resp;
continue;
}
if (typeName.equals("String")) {
for (Map.Entry
entry : paramMap.entrySet()) {
String value = Arrays.toString(entry.getValue())
.replaceAll("\\[|\\]", "")
.replaceAll(",\\s", ",");
paramValues[i] = value;
}
}
}
method.invoke(controllerMap.get(url), paramValues);
}
private void doLoadConfig(String location) {
InputStream is = this.getClass().getClassLoader().getResourceAsStream(location);
try {
properties.load(is);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is != null) {
try { is.close(); } catch (IOException e) { e.printStackTrace(); }
}
}
}
private void doScanner(String packageName) {
URL url = this.getClass().getClassLoader().getResource("/" + packageName.replaceAll("\\.", "/"));
File dir = new File(url.getFile());
for (File file : dir.listFiles()) {
if (file.isDirectory()) {
doScanner(packageName + "." + file.getName());
} else {
String className = packageName + "." + file.getName().replace(".class", "");
classNames.add(className);
}
}
}
private void doInstance() {
if (classNames.isEmpty()) return;
for (String className : classNames) {
try {
Class
clazz = Class.forName(className);
if (clazz.isAnnotationPresent(MyController.class)) {
ioc.put(toLowerFirstWord(clazz.getSimpleName()), clazz.newInstance());
}
} catch (Exception e) { e.printStackTrace(); }
}
}
private void initHandlerMapping() {
if (ioc.isEmpty()) return;
for (Map.Entry
entry : ioc.entrySet()) {
Class
clazz = entry.getValue().getClass();
if (!clazz.isAnnotationPresent(MyController.class)) continue;
String baseUrl = "";
if (clazz.isAnnotationPresent(MyRequestMapping.class)) {
baseUrl = clazz.getAnnotation(MyRequestMapping.class).value();
}
for (Method method : clazz.getMethods()) {
if (!method.isAnnotationPresent(MyRequestMapping.class)) continue;
String methodUrl = method.getAnnotation(MyRequestMapping.class).value();
String url = (baseUrl + "/" + methodUrl).replaceAll("/+/", "/");
handlerMapping.put(url, method);
controllerMap.put(url, clazz.newInstance());
System.out.println(url + "," + method);
}
}
}
private String toLowerFirstWord(String name) {
char[] chars = name.toCharArray();
chars[0] += 32;
return String.valueOf(chars);
}
}Testing the Framework
A sample controller demonstrates handling requests with parameters and returning plain‑text responses.
package com.liugh.core.controller;
import javax.servlet.http.*;
import com.liugh.annotation.*;
@MyController
@MyRequestMapping("/test")
public class TestController {
@MyRequestMapping("/doTest")
public void test1(HttpServletRequest request, HttpServletResponse response,
@MyRequestParam("param") String param) {
try {
response.getWriter().write("doTest method success! param:" + param);
} catch (IOException e) { e.printStackTrace(); }
}
@MyRequestMapping("/doTest2")
public void test2(HttpServletRequest request, HttpServletResponse response) {
try {
response.getWriter().println("doTest2 method success!");
} catch (IOException e) { e.printStackTrace(); }
}
}Running the application and accessing http://localhost:8080/liughMVC/test/doTest?param=liugh returns the expected success message, while a non‑existent URL yields a 404 response.
The complete source code is hosted on GitHub: https://github.com/qq53182347/liughMVC .
Java Captain
Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.