# 手写MVC
作者:Ethan.Yang
博客:https://blog.ethanyang.cn (opens new window)
相关源码参考: 手写Spring代码仓库 (opens new window)
# 一、组件与流程
SpringMVC 的本质是一个 请求分发与处理框架,核心就是 DispatcherServlet 调度中心。理解它的 九大核心组件 和 请求调用链,就能掌握整个执行流程。
# 九大核心组件
- DispatcherServlet(前端控制器)
- 核心角色,所有请求统一入口。
- 负责接收请求、分发任务、返回结果,本身不做业务逻辑。
- HandlerMapping(处理器映射器)
- 根据请求的 URL / 注解 / 配置 找到对应的处理器(
Controller)。 - 相当于“路由表”。
- 根据请求的 URL / 注解 / 配置 找到对应的处理器(
- HandlerAdapter(处理器适配器)
- 负责执行找到的处理器。
- 不同类型的处理器(注解式、接口式)需要不同适配器。
- Controller(处理器)
- 真正的业务处理类。
- 常见形式:
@Controller、@RestController。
- ModelAndView(模型与视图)
- 封装结果:
- 模型数据(Model:要返回给前端的数据)。
- 逻辑视图名(ViewName:页面模板标识)。
- 封装结果:
- ViewResolver(视图解析器)
- 将逻辑视图名解析为实际视图对象(如
userList → userList.jsp)。
- 将逻辑视图名解析为实际视图对象(如
- View(视图)
- 负责渲染,生成最终 HTML 页面或 JSON 响应。
- HandlerExceptionResolver(异常处理器)
- 统一捕获并处理 Controller 执行时的异常。
- 返回错误页面或 JSON 响应。
- LocaleResolver(本地化解析器)
- 解析用户的地区/语言,用于国际化支持。
# 请求调用链

# 学习重点
- DispatcherServlet 是整个流程的“大脑”,串联所有组件。
- HandlerMapping + HandlerAdapter 是核心:
- 前者负责“找到处理器”。
- 后者负责“执行处理器”。
- 最后通过 ViewResolver → View 才能得到最终可见结果。
# 二、代码实现
# YymHandlerMapping
存储 URL 和 对应的Controller 以及Controller 中具体的method
/**
* 保存URL 和 method的映射
*/
public class YymHandlerMapping {
// URL 的正则匹配
private Pattern pattern;
// 保存映射的方法
private Method method;
// Method对应的实例对象
private Object controller;
public YymHandlerMapping(Pattern pattern, Object controller, Method method) {
this.pattern = pattern;
this.method = method;
this.controller = controller;
}
public Pattern getPattern() {
return pattern;
}
public void setPattern(Pattern pattern) {
this.pattern = pattern;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public Object getController() {
return controller;
}
public void setController(Object controller) {
this.controller = controller;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# YymModelAndView
处理器适配器需要返回该组件给前端, 前端根据该组件渲染页面, 放在前面实现
/**
* 处理器适配器需要返回该组件给前端, 前端根据该组件渲染页面, 放在前面实现
*/
public class YymModelAndView {
private String viewName;
private Map<String, ?> model;
public YymModelAndView(String viewName, Map<String, ?> model) {
this.viewName = viewName;
this.model = model;
}
public YymModelAndView(String viewName) {
this.viewName = viewName;
}
public String getViewName() {
return viewName;
}
public Map<String, ?> getModel() {
return model;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# YymHandlerAdapter
前端控制器 本身不会调用Controller, 是通过url找到对应的映射器, 再通过映射器找到对应的适配器, 通过构造函数将映射器传进适配器, 再适配器中进行参数组装, 调用具体的web方法
/**
* 前端控制器 本身不会调用Controller, 会通过匹配对应的HandlerAdapter来执行对应的Controller
*/
public class YymHandlerAdapter {
/**
* 执行 Handler 对应的方法,并封装成 YymModelAndView 结果返回
*
* @param req HTTP 请求对象
* @param resp HTTP 响应对象
* @param handler 封装了 Controller 类和目标方法的映射信息
* @return YymModelAndView 结果视图对象
*/
public YymModelAndView handler(HttpServletRequest req, HttpServletResponse resp, YymHandlerMapping handler) throws Exception {
// public String hello(@YymRequestParam("name") String name, HttpServletRequest req, HttpServletResponse resp)
// 以这个方法为例, 需要构建业务参数 以及 req resp参数, 才能正确调用controller.hello()方法
// 1. 构建参数名与参数位置的映射,例如:name -> 0
Map<String, Integer> paramIndexMapping = new HashMap<>();
// 2. 解析方法参数上的 @YymRequestParam 注解,填充 paramIndexMapping
Annotation[][] pa = handler.getMethod().getParameterAnnotations();
for (int i = 0; i < pa.length; i++) {
for (Annotation a : pa[i]) {
if (a instanceof YymRequestParam) {
String paramName = ((YymRequestParam) a).value();
if (!paramName.trim().isEmpty()) {
paramIndexMapping.put(paramName, i);
}
}
}
}
// 3. 记录 HttpServletRequest 和 HttpServletResponse 参数的位置
Class<?>[] paramTypes = handler.getMethod().getParameterTypes();
for (int i = 0; i < paramTypes.length; i++) {
Class<?> type = paramTypes[i];
if (type == HttpServletRequest.class || type == HttpServletResponse.class) {
paramIndexMapping.put(type.getName(), i);
}
}
// 4. 构造方法调用时的参数值数组 paramValues
Map<String, String[]> requestParams = req.getParameterMap();
Object[] paramValues = new Object[paramTypes.length];
for (Map.Entry<String, String[]> entry : requestParams.entrySet()) {
String paramName = entry.getKey();
String value = Arrays.toString(entry.getValue())
.replaceAll("\\[|\\]", "") // 去除中括号
.replaceAll("\\s+", ","); // 空格换成逗号,简化处理
if (!paramIndexMapping.containsKey(paramName)) continue;
int index = paramIndexMapping.get(paramName);
paramValues[index] = castStringValue(value, paramTypes[index]);
}
// 5. 设置 HttpServletRequest 和 HttpServletResponse 参数
if (paramIndexMapping.containsKey(HttpServletRequest.class.getName())) {
int index = paramIndexMapping.get(HttpServletRequest.class.getName());
paramValues[index] = req;
}
if (paramIndexMapping.containsKey(HttpServletResponse.class.getName())) {
int index = paramIndexMapping.get(HttpServletResponse.class.getName());
paramValues[index] = resp;
}
// 6. 通过反射调用 Controller 方法
Object result = handler.getMethod().invoke(handler.getController(), paramValues);
// 7. 处理方法返回值
if (result == null || result instanceof Void) return null;
// 返回值为 YymModelAndView 时直接返回
if (handler.getMethod().getReturnType() == YymModelAndView.class) {
return (YymModelAndView) result;
}
// 其他返回类型暂不支持,返回 null
return null;
}
/**
* 简单的参数类型转换器:String -> Integer/Double/String
*/
private Object castStringValue(String value, Class<?> paramType) {
if (String.class == paramType) {
return value;
} else if (Integer.class == paramType || int.class == paramType) {
return Integer.valueOf(value);
} else if (Double.class == paramType || double.class == paramType) {
return Double.valueOf(value);
} else {
// 其他类型暂不处理,直接返回字符串或 null
return value != null ? value : null;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# YymViewResolver
/**
* 视图解析器:用于根据视图名(如 "hello")找到对应的 HTML 模板文件
*/
public class YymViewResolver {
private final String DEFAULT_TEMPLATE_SUFFIX = ".html"; // 默认模板文件后缀
private File tempateRootDir; // 模板根目录
/**
* 构造方法,传入模板根目录(如:templates)
*
* @param templateRoot 类路径下模板根目录
*/
public YymViewResolver(String templateRoot) {
// 通过类加载器获取模板文件目录路径
String templateRootPath = this.getClass().getClassLoader().getResource(templateRoot).getFile();
this.tempateRootDir = new File(templateRootPath);
}
/**
* 根据视图名解析出具体的 YymView 对象
*
* @param viewName 控制器返回的逻辑视图名
* @return YymView 视图对象
*/
public YymView resolveViewName(String viewName) {
if (viewName == null || "".equals(viewName.trim())) {
return null;
}
// 补全文件后缀(如:index → index.html)
viewName = viewName.endsWith(DEFAULT_TEMPLATE_SUFFIX)
? viewName
: (viewName + DEFAULT_TEMPLATE_SUFFIX);
// 拼接文件路径并构造模板文件
File templateFile = new File((tempateRootDir.getPath() + "/" + viewName).replaceAll("/+", "/"));
return new YymView(templateFile);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# YymView
/**
* 视图对象:用于渲染页面模板内容,替换变量,输出到前端
*/
public class YymView {
private File viewFile; // HTML模板文件
public YymView(File templateFile) {
this.viewFile = templateFile;
}
/**
* 渲染页面,将模板中的占位符替换为 model 中的实际数据
*
* @param model 传入的模型数据
* @param req 请求对象
* @param resp 响应对象
* @throws Exception 异常处理
*/
public void render(Map<String, ?> model, HttpServletRequest req, HttpServletResponse resp) throws Exception {
StringBuffer sb = new StringBuffer();
// 用于逐行读取模板内容
RandomAccessFile ra = new RandomAccessFile(this.viewFile, "r");
String line;
while ((line = ra.readLine()) != null) {
// 解决中文乱码问题,将文件内容从 ISO-8859-1 转成 UTF-8
line = new String(line.getBytes("ISO-8859-1"), "utf-8");
// 正则匹配形如 ¥{xxx} 的变量占位符(注意这里使用了 ¥ 而不是 $)
Pattern pattern = Pattern.compile("¥\\{[^\\}]+\\}", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(line);
// 逐个查找并替换变量
while (matcher.find()) {
String paramName = matcher.group(); // 如 ¥{name}
paramName = paramName.replaceAll("¥\\{|\\}", ""); // 去掉¥{ 和 }
// 从模型中获取变量值
Object paramValue = model.get(paramName);
// 将变量值替换占位符,注意需要对特殊字符做转义
line = matcher.replaceFirst(makeStringForRegExp(paramValue.toString()));
matcher = pattern.matcher(line); // 重建 matcher,防止替换错位
}
sb.append(line);
}
// 写回前端响应
resp.setCharacterEncoding("utf-8");
resp.getWriter().write(sb.toString());
}
/**
* 处理正则表达式中的特殊字符,防止替换错误
*
* @param str 原始字符串
* @return 转义后的字符串
*/
public static String makeStringForRegExp(String str) {
return str.replace("\\", "\\\\").replace("*", "\\*")
.replace("+", "\\+").replace("|", "\\|")
.replace("{", "\\{").replace("}", "\\}")
.replace("(", "\\(").replace(")", "\\)")
.replace("^", "\\^").replace("$", "\\$")
.replace("[", "\\[").replace("]", "\\]")
.replace("?", "\\?").replace(",", "\\,")
.replace(".", "\\.").replace("&", "\\&");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# YymDispatcherServlet
重点关注, 该组件是其它组件调用以及初始化的核心
web.xml配置了
当有请求进入时, 经过Filter -> Interceptor(MVC controller临界点) -> 就会走DispatcherServlet的doPost/Get方法, 进行mvc
/**
* 前端控制器
* 委派模式
* 职责:负责任务调度,请求分发
* Servlet spring入口
*/
public class YymDispatcherServlet extends HttpServlet {
private YymApplicationContext applicationContext;
private List<YymHandlerMapping> handlerMappings = new ArrayList<YymHandlerMapping>();
private Map<YymHandlerMapping, YymHandlerAdapter> handlerAdapters = new HashMap<YymHandlerMapping, YymHandlerAdapter>();
private List<YymViewResolver> viewResolvers = new ArrayList<YymViewResolver>();
@Override
public void init(ServletConfig config) throws ServletException {
// 初始化spring IOC 核心容器
applicationContext = new YymApplicationContext();
//初始化九大组件
initStrategies(applicationContext);
System.out.println("Yym Spring framework is init.");
}
private void initStrategies(YymApplicationContext context) {
// 多文件上传的组件
// initMultipartResolver(context);
// 初始化本地语言环境
// initLocaleResolver(context);
// 初始化模板处理器
// initThemeResolver(context);
// 初始化处理器映射器
initHandlerMappings(context);
// 初始化参数适配器
initHandlerAdapters(context);
// 初始化异常拦截器
// initHandlerExceptionResolvers(context);
//初始化视图预处理器
// initRequestToViewNameTranslator(context);
// 初始化视图转换器
initViewResolvers(context);
// FlashMap管理器
// initFlashMapManager(context);
}
private void initHandlerMappings(YymApplicationContext context) {
if (this.applicationContext.getBeanDefinitionCount() == 0) {
return;
}
for (String beanName : this.applicationContext.getBeanDefinitionNames()) {
Object instance = applicationContext.getBean(beanName);
Class<?> clazz = instance.getClass();
if (!clazz.isAnnotationPresent(YymController.class)) {
continue;
}
//相当于提取 class 上配置的url
String baseUrl = "";
if (clazz.isAnnotationPresent(YymRequestMapping.class)) {
YymRequestMapping requestMapping = clazz.getAnnotation(YymRequestMapping.class);
baseUrl = requestMapping.value();
}
//只获取public的方法
for (Method method : clazz.getMethods()) {
if (!method.isAnnotationPresent(YymRequestMapping.class)) {
continue;
}
// 提取每个方法上面配置的url 相当于@PostMapping 标记的url
YymRequestMapping requestMapping = method.getAnnotation(YymRequestMapping.class);
// 将具体方法的url通过正则和对应的方法 对应的web类进行映射
String regex = ("/" + baseUrl + "/" + requestMapping.value().replaceAll("\\*", ".*")).replaceAll("/+", "/");
Pattern pattern = Pattern.compile(regex);
handlerMappings.add(new YymHandlerMapping(pattern, instance, method));
System.out.println("Mapped : " + regex + "," + method);
}
}
}
private void initHandlerAdapters(YymApplicationContext context) {
// 通过简单的方式Map, 将映射器和适配器做映射, 这样servlet不会直接调用映射器, 而是通过适配器进行调用
for (YymHandlerMapping handlerMapping : handlerMappings) {
this.handlerAdapters.put(handlerMapping, new YymHandlerAdapter());
}
}
private void initViewResolvers(YymApplicationContext context) {
String templateRoot = context.getConfig().getProperty("templateRoot");
String templateRootPath = this.getClass().getClassLoader().getResource(templateRoot).getFile();
File templateRootDir = new File(templateRootPath);
for (File file : templateRootDir.listFiles()) {
this.viewResolvers.add(new YymViewResolver(templateRoot));
}
}
@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 {
// 委派,根据URL去找到一个对应的Method并通过response返回
try {
doDispatch(req, resp);
} catch (Exception e) {
try {
processDispatchResult(req, resp, new YymModelAndView("500"));
} catch (Exception e1) {
e1.printStackTrace();
resp.getWriter().write("500 Exception,Detail : " + Arrays.toString(e.getStackTrace()));
}
}
}
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
//1、通过URL获得一个HandlerMapping
YymHandlerMapping handler = getHandler(req);
if (handler == null) {
processDispatchResult(req, resp, new YymModelAndView("404"));
return;
}
//2、根据一个HandlerMaping获得一个HandlerAdapter
YymHandlerAdapter adapter = getHandlerAdapter(handler);
//3、解析某一个方法的形参和返回值之后,统一封装为ModelAndView对象
// 将该HandlerMaping 作为构造参数传入适配器, 在适配器中调用具体的Controller方法
YymModelAndView mv = adapter.handler(req, resp, handler);
// 就把ModelAndView变成一个ViewResolver
processDispatchResult(req, resp, mv);
}
/**
* 找到对应的 Handler(Controller + Method)
* 此处只是找对应的Handler
* 在调用Handler的方法时, 才需要适配器
*/
private YymHandlerMapping getHandler(HttpServletRequest req) {
if (this.handlerMappings.isEmpty()) {
return null;
}
// contextPath 是项目访问的根路径 http://localhost:8080/myapp/user/query
// /myapp/user/query
String url = req.getRequestURI();
// /myapp
String contextPath = req.getContextPath();
// /user/query , 因为handlerMapping 中存的是相对路径
url = url.replaceAll(contextPath, "").replaceAll("/+", "/");
// 获取对应的handlerMapping, 通过url的正则映射
for (YymHandlerMapping mapping : handlerMappings) {
Matcher matcher = mapping.getPattern().matcher(url);
if (!matcher.matches()) {
continue;
}
return mapping;
}
return null;
}
private void processDispatchResult(HttpServletRequest req, HttpServletResponse resp, YymModelAndView mv) throws Exception {
if (null == mv) {
return;
}
if (this.viewResolvers.isEmpty()) {
return;
}
for (YymViewResolver viewResolver : this.viewResolvers) {
YymView view = viewResolver.resolveViewName(mv.getViewName());
//直接往浏览器输出
view.render(mv.getModel(), req, resp);
return;
}
}
private YymHandlerAdapter getHandlerAdapter(YymHandlerMapping handler) {
if (this.handlerAdapters.isEmpty()) {
return null;
}
return this.handlerAdapters.get(handler);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208