Build Your Own Spring MVC Framework from Scratch: A Step‑by‑Step Guide
This tutorial walks Java backend developers through the complete process of understanding Spring MVC’s request flow, designing a custom MVC framework, and implementing core components such as DispatcherServlet, annotations, and handler mappings with full code examples and deployment instructions.
Spring is a staple for Java backend programmers, and beyond its reflection‑based features it contains many clever design patterns worth studying; reading its source code offers valuable lessons in coding standards and architecture.
1. Understand Spring MVC Runtime Flow and Its Nine Core Components
1) Spring MVC Execution Process
① User sends a request to the front‑controller DispatcherServlet.
② DispatcherServlet receives the request and invokes a HandlerMapping.
③ The handler mapping finds the appropriate handler (controller) and any interceptors, returning them to DispatcherServlet.
④ DispatcherServlet calls the handler via a HandlerAdapter.
⑤ The controller (backend controller) executes.
⑥ Controller returns a ModelAndView.
⑦ HandlerAdapter passes the ModelAndView back to DispatcherServlet.
⑧ DispatcherServlet forwards the ModelAndView to a ViewResolver.
⑨ ViewResolver resolves the view and renders it, filling the model data.
⑩ DispatcherServlet sends the rendered response to the client.
This flow shows how DispatcherServlet reduces coupling between components.
2) The Nine Core Components of Spring MVC
protected void initStrategies(ApplicationContext context) {<br/> // Multipart request handling<br/> initMultipartResolver(context);<br/> // Locale resolution for views and internationalization<br/> initLocaleResolver(context);<br/> // Theme resolution<br/> initThemeResolver(context);<br/> // Handler mapping discovery<br/> initHandlerMappings(context);<br/> // Adapter for invoking handlers<br/> initHandlerAdapters(context);<br/> // Exception handling<br/> initHandlerExceptionResolvers(context);<br/> // Translate request to view name<br/> initRequestToViewNameTranslator(context);<br/> // Resolve view names to actual View objects<br/> initViewResolvers(context);<br/> // Manage FlashMap for redirects<br/> initFlashMapManager(context);<br/>}2. Design Ideas for a Custom Spring MVC
This article implements only the essential @Controller, @RequestMapping, and @RequestParam annotations; readers can extend other features themselves.
1) Reading Configuration
Spring MVC is essentially a Servlet that extends HttpServlet. FrameworkServlet initializes the Spring MVC container and sets the Spring container as its parent. This tutorial focuses on the custom framework, so the Spring container itself is not detailed.
ServletConfig is used to read the web.xml configuration, loading the custom MyDispatcherServlet and its configuration file.
2) Initialization Phase
During initialization, the custom implementation will create a subset of the nine components, in order:
Load configuration files.
Scan all classes under the user‑specified package.
Instantiate scanned classes via reflection and store them in an IoC container (a Map of beanName‑bean, where beanName defaults to a lowercase first letter).
Initialize HandlerMapping by mapping URLs to methods in a Map for runtime lookup.
3) Runtime Phase
Each request is handled by doGet or doPost, which delegate to doDispatch. The dispatcher matches the request URL to a Method in the handler mapping, invokes the method via reflection, and returns the result. The runtime steps include:
Exception interception.
Parameter extraction and processing.
Reflective invocation of the matched controller method.
3. Implementing Your Own Spring MVC Framework
Project structure and Maven dependencies:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><br/> <modelVersion>4.0.0</modelVersion><br/> <groupId>com.liugh</groupId><br/> <artifactId>liughMVC</artifactId><br/> <version>0.0.1-SNAPSHOT</version><br/> <packaging>war</packaging><br/> <properties><br/> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><br/> <maven.compiler.source>1.8</maven.compiler.source><br/> <maven.compiler.target>1.8</maven.compiler.target><br/> <java.version>1.8</java.version><br/> </properties><br/> <dependencies><br/> <dependency><br/> <groupId>javax.servlet</groupId><br/> <artifactId>javax.servlet-api</artifactId><br/> <version>3.0.1</version><br/> <scope>provided</scope><br/> </dependency><br/> </dependencies><br/></project>web.xml configuration:
<?xml version="1.0" encoding="UTF-8"?><br/><web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"><br/> <servlet><br/> <servlet-name>MySpringMVC</servlet-name><br/> <servlet-class>com.liugh.servlet.MyDispatcherServlet</servlet-class><br/> <init-param><br/> <param-name>contextConfigLocation</param-name><br/> <param-value>application.properties</param-value><br/> </init-param><br/> <load-on-startup>1</load-on-startup><br/> </servlet><br/> <servlet-mapping><br/> <servlet-name>MySpringMVC</servlet-name><br/> <url-pattern>/*</url-pattern><br/> </servlet-mapping><br/></web-app>application.properties (only the package to scan): scanPackage=com.liugh.core Custom annotations:
package com.liugh.annotation;<br/><br/>import java.lang.annotation.Documented;<br/>import java.lang.annotation.ElementType;<br/>import java.lang.annotation.Retention;<br/>import java.lang.annotation.RetentionPolicy;<br/>import java.lang.annotation.Target;<br/><br/>@Target(ElementType.TYPE)<br/>@Retention(RetentionPolicy.RUNTIME)<br/>@Documented<br/>public @interface MyController {<br/> String value() default "";<br/>} package com.liugh.annotation;<br/><br/>import java.lang.annotation.Documented;<br/>import java.lang.annotation.ElementType;<br/>import java.lang.annotation.Retention;<br/>import java.lang.annotation.RetentionPolicy;<br/>import java.lang.annotation.Target;<br/><br/>@Target({ElementType.TYPE, ElementType.METHOD})<br/>@Retention(RetentionPolicy.RUNTIME)<br/>@Documented<br/>public @interface MyRequestMapping {<br/> String value() default "";<br/>} package com.liugh.annotation;<br/><br/>import java.lang.annotation.Documented;<br/>import java.lang.annotation.ElementType;<br/>import java.lang.annotation.Retention;<br/>import java.lang.annotation.RetentionPolicy;<br/>import java.lang.annotation.Target;<br/><br/>@Target(ElementType.PARAMETER)<br/>@Retention(RetentionPolicy.RUNTIME)<br/>@Documented<br/>public @interface MyRequestParam {<br/> String value();<br/>}Main dispatcher servlet (simplified):
package com.liugh.servlet;<br/><br/>import java.io.File;<br/>import java.io.IOException;<br/>import java.io.InputStream;<br/>import java.lang.reflect.Method;<br/>import java.net.URL;<br/>import java.util.ArrayList;<br/>import java.util.HashMap;<br/>import java.util.List;<br/>import java.util.Map;<br/>import java.util.Map.Entry;<br/>import java.util.Properties;<br/><br/>import javax.servlet.ServletConfig;<br/>import javax.servlet.ServletException;<br/>import javax.servlet.http.HttpServlet;<br/>import javax.servlet.http.HttpServletRequest;<br/>import javax.servlet.http.HttpServletResponse;<br/><br/>import com.liugh.annotation.MyController;<br/>import com.liugh.annotation.MyRequestMapping;<br/><br/>public class MyDispatcherServlet extends HttpServlet {<br/> private Properties properties = new Properties();<br/> private List<String> classNames = new ArrayList<>();<br/> private Map<String, Object> ioc = new HashMap<>();<br/> private Map<String, Method> handlerMapping = new HashMap<>();<br/> private Map<String, Object> controllerMap = new HashMap<>();<br/><br/> @Override<br/> public void init(ServletConfig config) throws ServletException {<br/> doLoadConfig(config.getInitParameter("contextConfigLocation"));<br/> doScanner(properties.getProperty("scanPackage"));<br/> doInstance();<br/> initHandlerMapping();<br/> }<br/><br/> @Override<br/> protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {<br/> this.doPost(req, resp);<br/> }<br/><br/> @Override<br/> protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {<br/> try {<br/> doDispatch(req, resp);<br/> } catch (Exception e) {<br/> resp.getWriter().write("500!! Server Exception");<br/> }<br/> }<br/><br/> private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {<br/> if (handlerMapping.isEmpty()) return;<br/> String url = req.getRequestURI().replace(req.getContextPath(), "").replaceAll("/+", "/");<br/> if (!handlerMapping.containsKey(url)) {<br/> resp.getWriter().write("404 NOT FOUND!");<br/> return;<br/> }<br/> Method method = handlerMapping.get(url);<br/> Class<?>[] paramTypes = method.getParameterTypes();<br/> Map<String, String[]> paramMap = req.getParameterMap();<br/> Object[] paramValues = new Object[paramTypes.length];<br/> for (int i = 0; i < paramTypes.length; i++) {<br/> String typeName = paramTypes[i].getSimpleName();<br/> if (typeName.equals("HttpServletRequest")) {<br/> paramValues[i] = req;<br/> continue;<br/> }<br/> if (typeName.equals("HttpServletResponse")) {<br/> paramValues[i] = resp;<br/> continue;<br/> }<br/> if (typeName.equals("String")) {<br/> for (Entry<String, String[]> entry : paramMap.entrySet()) {<br/> String value = java.util.Arrays.toString(entry.getValue()).replaceAll("\\[|\\]", "").replaceAll(", ", ",");<br/> paramValues[i] = value;<br/> }<br/> }<br/> }<br/> method.invoke(controllerMap.get(url), paramValues);<br/> }<br/><br/> private void doLoadConfig(String location) {<br/> InputStream is = this.getClass().getClassLoader().getResourceAsStream(location);<br/> try {<br/> properties.load(is);<br/> } catch (IOException e) { e.printStackTrace(); } finally {<br/> if (is != null) try { is.close(); } catch (IOException e) { e.printStackTrace(); }<br/> }<br/> }<br/><br/> private void doScanner(String packageName) {<br/> URL url = this.getClass().getClassLoader().getResource("/" + packageName.replaceAll("\\.", "/"));<br/> File dir = new File(url.getFile());<br/> for (File file : dir.listFiles()) {<br/> if (file.isDirectory()) {<br/> doScanner(packageName + "." + file.getName());<br/> } else {<br/> classNames.add(packageName + "." + file.getName().replace(".class", ""));<br/> }<br/> }<br/> }<br/><br/> private void doInstance() {<br/> for (String className : classNames) {<br/> try {<br/> Class<?> clazz = Class.forName(className);<br/> if (clazz.isAnnotationPresent(MyController.class)) {<br/> ioc.put(toLowerFirstWord(clazz.getSimpleName()), clazz.newInstance());<br/> }<br/> } catch (Exception e) { e.printStackTrace(); }<br/> }<br/> }<br/><br/> private void initHandlerMapping() {<br/> for (Map.Entry<String, Object> entry : ioc.entrySet()) {<br/> Class<?> clazz = entry.getValue().getClass();<br/> if (!clazz.isAnnotationPresent(MyController.class)) continue;<br/> String baseUrl = "";<br/> if (clazz.isAnnotationPresent(MyRequestMapping.class)) {<br/> baseUrl = clazz.getAnnotation(MyRequestMapping.class).value();<br/> }<br/> for (Method method : clazz.getMethods()) {<br/> if (!method.isAnnotationPresent(MyRequestMapping.class)) continue;<br/> String url = method.getAnnotation(MyRequestMapping.class).value();<br/> url = (baseUrl + "/" + url).replaceAll("/+/", "/");<br/> handlerMapping.put(url, method);<br/> controllerMap.put(url, clazz.newInstance());<br/> System.out.println(url + "," + method);r/> }<br/> }<br/> }<br/><br/> private String toLowerFirstWord(String name) {<br/> char[] chars = name.toCharArray();<br/> chars[0] += 32;<br/> return String.valueOf(chars);<br/> }<br/>}Test controller demonstrating two endpoints:
package com.liugh.core.controller;<br/><br/>import java.io.IOException;<br/>import javax.servlet.http.HttpServletRequest;<br/>import javax.servlet.http.HttpServletResponse;<br/>import com.liugh.annotation.MyController;<br/>import com.liugh.annotation.MyRequestMapping;<br/>import com.liugh.annotation.MyRequestParam;<br/><br/>@MyController<br/>@MyRequestMapping("/test")<br/>public class TestController {<br/> @MyRequestMapping("/doTest")<br/> public void test1(HttpServletRequest request, HttpServletResponse response, @MyRequestParam("param") String param) {<br/> try { response.getWriter().write("doTest method success! param:" + param); } catch (IOException e) { e.printStackTrace(); }<br/> }<br/><br/> @MyRequestMapping("/doTest2")<br/> public void test2(HttpServletRequest request, HttpServletResponse response) {<br/> try { response.getWriter().println("doTest2 method success!"); } catch (IOException e) { e.printStackTrace(); }<br/> }<br/>}Running the application and accessing http://localhost:8080/liughMVC/test/doTest?param=liugh returns a successful response, while requesting a non‑existent URL yields a 404 page, as shown in the screenshots below.
The custom Spring MVC framework is now complete. Source code is available at https://github.com/qq53182347/liughMVC .
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
ITFLY8 Architecture Home
ITFLY8 Architecture Home - focused on architecture knowledge sharing and exchange, covering project management and product design. Includes large-scale distributed website architecture (high performance, high availability, caching, message queues...), design patterns, architecture patterns, big data, project management (SCRUM, PMP, Prince2), product design, and more.
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.
