标题是‘从零开始实现一个简易的Java MVC框架’,结果写了这么多才到实现MVC的时候...只能说前戏确实有点多了。不过这些前戏都是必须的,若是只是简简单单实现一个MVC的功能那就没有意思了,要有Bean容器、IOC、AOP和MVC才像是一个'框架'嘛。html
为了实现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
而后引入这两个包。java
jstl
用于解析jsp表达式的,好比在jsp页面编写下面这样c:forEach
语句就须要这个包。git
<c:forEach items="${list}" var="user">
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
</tr>
</c:forEach>
复制代码
fastjson
是阿里开发的一个json解析包,用于将实体类转换成json。相似的包还有Gson
和Jackson
等,这里就不具体比较了,能够挑选一个本身喜欢的。github
首先咱们要了解到MVC的实现原理,在使用spring-boot编写项目的时候,咱们一般都是经过编写一系列的Controller来实现一个个连接,这是'现代'的写法。可是在之前springmvc甚至是struts2这类mvc框架都还没流行的时候,都是经过编写Servlet
来实现。web
每个请求都会对应一个Servlet
,而后还要在web.xml中配置这个Servlet
,而后对请求的接收和处理啥的都分布在一大堆的Servlet
中,代码十分混杂。spring
为了让人们编写的时候更专一于业务代码而减小对请求的处理,springmvc就经过一个中央的Servlet
,处理这些请求,而后再转发到对应的Controller中,这样就只有一个Servlet
统一处理请求了。下面的一段话来自spring的官方文档docs.spring.io/spring/docs…apache
Spring MVC, like many other web frameworks, is designed around the front controller pattern where a central
Servlet
, theDispatcherServlet
, provides a shared algorithm for request processing while actual work is performed by configurable, delegate components. This model is flexible and supports diverse workflows.jsonThe
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.api
这段大体意思就是:springmvc经过中心Servlet(DispatcherServlet)来实现对控制controller的操做。这个Servlet
要经过java配置或者配置在web.xml中,它用于寻找请求的映射(即找到对应的controller),视图解析(即执行controller的结果),异常处理(即对执行过程的异常统一处理)等等
因此实现MVC的效果就是如下几点:
DispatcherServlet
来接收全部请求根据上面的步骤,咱们先从步骤二、三、四、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最多见的注解。
为了可以方便的传递参数到前端,建立一个工具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分发器相似于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
变量中的功能。大概讲解一些这个类的功能流程:
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流程中的步骤三、四、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()
实例化调用方法要传入的参数值resultResolver()
解析方法的返回值,选择返回页面或者json经过这几个步骤算是凝聚了MVC核心步骤了,不过因为篇幅问题,几乎每一步骤得功能都有所精简,如
虽然有缺陷,可是一个MVC流程是完成了。接下来就要把这些功能组装一下了。
终于到实现开头说的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步骤的三、四、5的时候都放在了一个类里,这样也很差,原本这里每一步骤的功能就很繁杂,还将这几步骤都放在一个类中,这样不利于后期更改对应步骤的功能。
还有目前也没实现异常的处理,不能返回异常页面给用户。
这些优化工做会在后期的章节完成的。
- 从零开始实现一个简易的Java MVC框架(一)--前言
- 从零开始实现一个简易的Java MVC框架(二)--实现Bean容器
- 从零开始实现一个简易的Java MVC框架(三)--实现IOC
- 从零开始实现一个简易的Java MVC框架(四)--实现AOP
- 从零开始实现一个简易的Java MVC框架(五)--引入aspectj实现AOP切点
- 从零开始实现一个简易的Java MVC框架(六)--增强AOP功能
- 从零开始实现一个简易的Java MVC框架(七)--实现MVC
- 从零开始实现一个简易的Java MVC框架(八)--制做Starter
- 从零开始实现一个简易的Java MVC框架(九)--优化MVC代码
源码地址:doodle