前言
标题是‘从零开始实现一个简易的Java MVC框架’,结果写了这么多才到实现MVC的时候…只能说前戏确实有点多了。不过这些前戏都是必须的,如果只是简简单单实现一个MVC的功能那就没有意思了,要有Bean容器、IOC、AOP和MVC才像是一个’框架’嘛。
实现准备
为了实现mvc的功能,先要为pom.xml添加一些依赖。
<properties>
...
<tomcat.version>8.5.31</tomcat.version>
<jstl.version>1.2</jstl.version>
<fastjson.version>1.2.47</fastjson.version>
</properties>
<dependencies>
...
<!-- tomcat embed -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>${tomcat.version}</version>
</dependency>
<!-- JSTL -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>${jstl.version}</version>
<scope>runtime</scope>
</dependency>
<!-- FastJson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
</dependencies>
-
tomcat-embed-jasper
这个依赖是引入了一个内置的tomcat,spring-boot默认就是引用这个嵌入式的tomcat包实现直接启动服务的。这个包除了加入了一个嵌入式的tomcat,还引入了java.servlet-api
和jsp-api
这两个包,如果不想用这种嵌入式的tomcat的话,可以去除tomcat-embed-jasper
然后引入这两个包。 jstl
用于解析jsp表达式的,比如在jsp页面编写下面这样c:forEach
语句就需要这个包。<c:forEach items="${list}" var="user"> <tr> <td>${user.id}</td> <td>${user.name}</td> </tr> </c:forEach>
-
fastjson
是阿里开发的一个json解析包,用于将实体类转换成json。类似的包还有Gson
和Jackson
等,这里就不具体比较了,可以挑选一个自己喜欢的。
实现MVC
MVC实现原理
首先我们要了解到MVC的实现原理,在使用spring-boot编写项目的时候,我们通常都是通过编写一系列的Controller来实现一个个链接,这是’现代’的写法。但是在以前springmvc甚至是struts2这类mvc框架都还没流行的时候,都是通过编写Servlet
来实现。
每一个请求都会对应一个Servlet
,然后还要在web.xml中配置这个Servlet
,然后对请求的接收和处理啥的都分布在一大堆的Servlet
中,代码十分混杂。
为了让人们编写的时候更专注于业务代码而减少对请求的处理,springmvc就通过一个中央的Servlet
,处理这些请求,然后再转发到对应的Controller中,这样就只有一个Servlet
统一处理请求了。下面的一段话来自spring的官方文档https://docs.spring.io/spring/docs/5.0.7.RELEASE/spring-framework-reference/web.html#mvc-servlet
Spring MVC, like many other web frameworks, is designed around the front controller pattern where a central
Servlet
, the
DispatcherServlet
, provides a shared algorithm for request processing while actual work is performed by configurable, delegate components. This model is flexible and supports diverse workflows.The
DispatcherServlet
, as anyServlet
, needs to be declared and mapped according to the Servlet specification using Java configuration or inweb.xml
. In turn theDispatcherServlet
uses Spring configuration to discover the delegate components it needs for request mapping, view resolution, exception handling, and more.
这段大致意思就是:springmvc通过中心Servlet(DispatcherServlet)来实现对控制controller的操作。这个Servlet
要通过java配置或者配置在web.xml中,它用于寻找请求的映射(即找到对应的controller),视图解析(即执行controller的结果),异常处理(即对执行过程的异常统一处理)等等
所以实现MVC的效果就是以下几点:
- 通过一个中央sevlet如
DispatcherServlet
来接收所有请求 - 根据请求找到对应的controller
- 执行controller获取结果
- 对controller的结果解析并转到对应视图
- 若有异常则统一处理异常
根据上面的步骤,我们先从步骤2、3、4、5开始,最后再实现1完成mvc。
创建注解
为了方便实现,先在com.zbw.mvc.annotation包下创建三个注解和一个枚举:RequestMapping
、RequestParam
、ResponseBody
、RequestMethod
。
package com.zbw.mvc.annotation;
import ...
/**
* http请求路径
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
/**
* 请求路径
*/
String value() default "";
/**
* 请求方法
*/
RequestMethod method() default RequestMethod.GET;
}
package com.zbw.mvc.annotation;
/**
* http请求类型
*/
public enum RequestMethod {
GET, POST
}
package com.zbw.mvc.annotation;
import ...
/**
* 请求的方法参数名
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestParam {
/**
* 方法参数别名
*/
String value() default "";
/**
* 是否必传
*/
boolean required() default true;
}
package com.zbw.mvc.annotation;
import ...
/**
* 用于标记返回json
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseBody {
}
这几个类的作用就不解释了,都是springmvc最常见的注解。
创建ModelAndView
为了能够方便的传递参数到前端,创建一个工具bean,相当于spring中简化版的ModelAndView
。这个类创建于com.zbw.mvc.bean包下
package com.zbw.mvc.bean;
import ...
/**
* ModelAndView
*/
public class ModelAndView {
/**
* 页面路径
*/
private String view;
/**
* 页面data数据
*/
private Map<String, Object> model = new HashMap<>();
public ModelAndView setView(String view) {
this.view = view;
return this;
}
public String getView() {
return view;
}
public ModelAndView addObject(String attributeName, Object attributeValue) {
model.put(attributeName, attributeValue);
return this;
}
public ModelAndView addAllObjects(Map<String, ?> modelMap) {
model.putAll(modelMap);
return this;
}
public Map<String, Object> getModel() {
return model;
}
}
实现Controller分发器
Controller分发器类似于Bean容器,只不过后者是存放Bean的而前者是存放Controller的,然后根据一些条件可以简单的获取对应的Controller。
先在com.zbw.mvc包下创建一个ControllerInfo
类,用于存放Controller的一些信息。
package com.zbw.mvc;
import ...
/**
* ControllerInfo 存储Controller相关信息
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ControllerInfo {
/**
* controller类
*/
private Class<?> controllerClass;
/**
* 执行的方法
*/
private Method invokeMethod;
/**
* 方法参数别名对应参数类型
*/
private Map<String, Class<?>> methodParameter;
}
然后再创建一个PathInfo
类,用于存放请求路径和请求方法类型
package com.zbw.mvc;
import ...
/**
* PathInfo 存储http相关信息
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PathInfo {
/**
* http请求方法
*/
private String httpMethod;
/**
* http请求路径
*/
private String httpPath;
}
接着创建Controller分发器类ControllerHandler
package com.zbw.mvc;
import ...
/**
* Controller 分发器
*/
@Slf4j
public class ControllerHandler {
private Map<PathInfo, ControllerInfo> pathControllerMap = new ConcurrentHashMap<>();
private BeanContainer beanContainer;
public ControllerHandler() {
beanContainer = BeanContainer.getInstance();
Set<Class<?>> classSet = beanContainer.getClassesByAnnotation(RequestMapping.class);
for (Class<?> clz : classSet) {
putPathController(clz);
}
}
/**
* 获取ControllerInfo
*/
public ControllerInfo getController(String requestMethod, String requestPath) {
PathInfo pathInfo = new PathInfo(requestMethod, requestPath);
return pathControllerMap.get(pathInfo);
}
/**
* 添加信息到requestControllerMap中
*/
private void putPathController(Class<?> clz) {
RequestMapping controllerRequest = clz.getAnnotation(RequestMapping.class);
String basePath = controllerRequest.value();
Method[] controllerMethods = clz.getDeclaredMethods();
// 1. 遍历Controller中的方法
for (Method method : controllerMethods) {
if (method.isAnnotationPresent(RequestMapping.class)) {
// 2. 获取这个方法的参数名字和参数类型
Map<String, Class<?>> params = new HashMap<>();
for (Parameter methodParam : method.getParameters()) {
RequestParam requestParam = methodParam.getAnnotation(RequestParam.class);
if (null == requestParam) {
throw new RuntimeException("必须有RequestParam指定的参数名");
}
params.put(requestParam.value(), methodParam.getType());
}
// 3. 获取这个方法上的RequestMapping注解
RequestMapping methodRequest = method.getAnnotation(RequestMapping.class);
String methodPath = methodRequest.value();
RequestMethod requestMethod = methodRequest.method();
PathInfo pathInfo = new PathInfo(requestMethod.toString(), basePath + methodPath);
if (pathControllerMap.containsKey(pathInfo)) {
log.error("url:{} 重复注册", pathInfo.getHttpPath());
throw new RuntimeException("url重复注册");
}
// 4. 生成ControllerInfo并存入Map中
ControllerInfo controllerInfo = new ControllerInfo(clz, method, params);
this.pathControllerMap.put(pathInfo, controllerInfo);
log.info("Add Controller RequestMethod:{}, RequestPath:{}, Controller:{}, Method:{}",
pathInfo.getHttpMethod(), pathInfo.getHttpPath(),
controllerInfo.getControllerClass().getName(), controllerInfo.getInvokeMethod().getName());
}
}
}
}
这个类最复杂的就是构造函数中调用的putPathController()
方法,这个方法也是这个类的核心方法,实现了controller类中的信息存放到pathControllerMap
变量中的功能。大概讲解一些这个类的功能流程:
- 在构造方法中获取Bean容器
BeanContainer
的单例实例 - 获取并遍历
BeanContainer
中存放的被RequestMapping
注解标记的类 - 遍历这个类中的方法,找出被
RequestMapping
注解标记的方法 - 获取这个方法的参数名字和参数类型,生成
ControllerInfo
- 根据
RequestMapping
里的value()
和method()
生成PathInfo
- 将生成的
PathInfo
和ControllerInfo
存到变量pathControllerMap
中 - 其他类通过调用
getController()
方法获取到对应的controller
以上就是这个类的流程,其中有个注意的点:
步骤4的时候,必须规定这个方法的所有参数名字都被RequestParam
注解标注,这是因为在java中,虽然我们编写代码的时候是有参数名的,比如String name
这样的形式,但是被编译成class文件后‘name’这个字段就会被擦除,所以必须要通过一个RequestParam
来保存名字。
但是大家在springmvc中并不用必须每个方法都用注解标记的,这是因为spring中借助了asm
,这种工具可以在编译之前拿到参数名然后保存起来。还有一种方法是在java8之后支持了保存参数名,但是必须修改编译器的参数来支持。这两种方法实现起来都比较复杂或者有限制条件,这里就不实现了,大家可以查找资料自己实现
实现结果执行器
接下来实现结果执行器,这个类中实现刚才mvc流程中的步骤3、4、5。
在com.zbw.mvc包下创建类ResultRender
package com.zbw.mvc;
import ...
/**
* 结果执行器
*/
@Slf4j
public class ResultRender {
private BeanContainer beanContainer;
public ResultRender() {
beanContainer = BeanContainer.getInstance();
}
/**
* 执行Controller的方法
*/
public void invokeController(HttpServletRequest req, HttpServletResponse resp, ControllerInfo controllerInfo) {
// 1. 获取HttpServletRequest所有参数
Map<String, String> requestParam = getRequestParams(req);
// 2. 实例化调用方法要传入的参数值
List<Object> methodParams = instantiateMethodArgs(controllerInfo.getMethodParameter(), requestParam);
Object controller = beanContainer.getBean(controllerInfo.getControllerClass());
Method invokeMethod = controllerInfo.getInvokeMethod();
invokeMethod.setAccessible(true);
Object result;
// 3. 通过反射调用方法
try {
if (methodParams.size() == 0) {
result = invokeMethod.invoke(controller);
} else {
result = invokeMethod.invoke(controller, methodParams.toArray());
}
} catch (Exception e) {
throw new RuntimeException(e);
}
// 4.解析方法的返回值,选择返回页面或者json
resultResolver(controllerInfo, result, req, resp);
}
/**
* 获取http中的参数
*/
private Map<String, String> getRequestParams(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
//GET和POST方法是这样获取请求参数的
request.getParameterMap().forEach((paramName, paramsValues) -> {
if (ValidateUtil.isNotEmpty(paramsValues)) {
paramMap.put(paramName, paramsValues[0]);
}
});
// TODO: Body、Path、Header等方式的请求参数获取
return paramMap;
}
/**
* 实例化方法参数
*/
private List<Object> instantiateMethodArgs(Map<String, Class<?>> methodParams, Map<String, String> requestParams) {
return methodParams.keySet().stream().map(paramName -> {
Class<?> type = methodParams.get(paramName);
String requestValue = requestParams.get(paramName);
Object value;
if (null == requestValue) {
value = CastUtil.primitiveNull(type);
} else {
value = CastUtil.convert(type, requestValue);
// TODO: 实现非原生类的参数实例化
}
return value;
}).collect(Collectors.toList());
}
/**
* Controller方法执行后返回值解析
*/
private void resultResolver(ControllerInfo controllerInfo, Object result, HttpServletRequest req, HttpServletResponse resp) {
if (null == result) {
return;
}
boolean isJson = controllerInfo.getInvokeMethod().isAnnotationPresent(ResponseBody.class);
if (isJson) {
// 设置响应头
resp.setContentType("application/json");
resp.setCharacterEncoding("UTF-8");
// 向响应中写入数据
try (PrintWriter writer = resp.getWriter()) {
writer.write(JSON.toJSONString(result));
writer.flush();
} catch (IOException e) {
log.error("转发请求失败", e);
// TODO: 异常统一处理,400等...
}
} else {
String path;
if (result instanceof ModelAndView) {
ModelAndView mv = (ModelAndView) result;
path = mv.getView();
Map<String, Object> model = mv.getModel();
if (ValidateUtil.isNotEmpty(model)) {
for (Map.Entry<String, Object> entry : model.entrySet()) {
req.setAttribute(entry.getKey(), entry.getValue());
}
}
} else if (result instanceof String) {
path = (String) result;
} else {
throw new RuntimeException("返回类型不合法");
}
try {
req.getRequestDispatcher("/templates/" + path).forward(req, resp);
} catch (Exception e) {
log.error("转发请求失败", e);
// TODO: 异常统一处理,400等...
}
}
}
}
通过调用类中的invokeController()
方法反射调用了Controller中的方法并根据结果解析对应的页面。主要流程为:
- 调用
getRequestParams()
获取HttpServletRequest中参数 - 调用
instantiateMethodArgs()
实例化调用方法要传入的参数值 - 通过反射调用目标controller的目标方法
- 调用
resultResolver()
解析方法的返回值,选择返回页面或者json
通过这几个步骤算是凝聚了MVC核心步骤了,不过由于篇幅问题,几乎每一步骤得功能都有所精简,如
- 步骤1获取HttpServletRequest中参数只获取get或者post传的参数,实际上还有 Body、Path、Header等方式的请求参数获取没有实现
- 步骤2实例化调用方法的值只实现了java的原生参数,自定义的类的实例化没有实现
- 步骤4异常统一处理也没具体实现
虽然有缺陷,但是一个MVC流程是完成了。接下来就要把这些功能组装一下了。
实现DispatcherServlet
终于到实现开头说的DispatcherServlet
了,这个类继承于HttpServlet
,所有请求都从这里经过。
在com.zbw.mvc下创建DispatcherServlet
package com.zbw.mvc;
import ...
/**
* DispatcherServlet 所有http请求都由此Servlet转发
*/
@Slf4j
public class DispatcherServlet extends HttpServlet {
private ControllerHandler controllerHandler = new ControllerHandler();
private ResultRender resultRender = new ResultRender();
/**
* 执行请求
*/
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 设置请求编码方式
req.setCharacterEncoding("UTF-8");
//获取请求方法和请求路径
String requestMethod = req.getMethod();
String requestPath = req.getPathInfo();
log.info("[DoodleConfig] {} {}", requestMethod, requestPath);
if (requestPath.endsWith("/")) {
requestPath = requestPath.substring(0, requestPath.length() - 1);
}
ControllerInfo controllerInfo = controllerHandler.getController(requestMethod, requestPath);
log.info("{}", controllerInfo);
if (null == controllerInfo) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
resultRender.invokeController(req, resp, controllerInfo);
}
}
在这个类里调用了ControllerHandler
和ResultRender
两个类,先根据请求的方法和路径获取对应的ControllerInfo
,然后再用ControllerInfo
解析出对应的视图,然后就能访问到对应的页面或者返回对应的json信息了。
然而一直在说的所有请求都从DispatcherServlet
经过好像没有体现啊,这是因为要配置web.xml才行,现在很多都在使用spring-boot的朋友可能不大清楚了,在以前使用springmvc+spring+mybatis时代的时候要写很多配置文件,其中一个就是web.xml,要在里面添加上。通过通配符*
让所有请求都走的是DispatcherServlet。
<servlet>
<servlet-name>springMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>springMVC</servlet-name>
<url-pattern>*</url-pattern>
</servlet-mapping>
不过我们无需这样做,为了致敬spring-boot,我们会在下一节实现内嵌Tomcat,并通过启动器启动。
缺陷
可能这一节的代码让大家看起来不是很舒服,这是因为目前这个代码虽然说功能已经是实现了,但是代码结构还需要优化。
首先DispatcherServlet
是一个请求分发器,这里面不应该有处理Http的逻辑代码的
其次我们把MVC步骤的3、4、5的时候都放在了一个类里,这样也不好,本来这里每一步骤的功能就很繁杂,还将这几步骤都放在一个类中,这样不利于后期更改对应步骤的功能。
还有目前也没实现异常的处理,不能返回异常页面给用户。
这些优化工作会在后期的章节完成的。
源码地址:doodle