系列文章:
在前面两篇,我们已经成功实现了 Spring 最核心的功能 IOC和DI,从这篇开始,我们来看看如何实现 MVC:

MVC 的核心就是那九大组件 【Spring】MVC:九大核心组件分析,而其中最重要的三个:HandlerMappings,HandlerAdapters,ViewResolvers。
1.MYHandlerMapping
封装 [Controller对象,处理请求的方法,可以处理的请求路径] 的一对一关系。实质上是把把 IOC 容器管理的Bean实例进行了封装(包括代理对象的替换),并建立了映射关系。
public class MYHandlerMapping {
// 处理请求的具体Controller对象
private Object Controller;
// 处理请求的具体方法
private Method method;
// 可以处理的请求路径
private Pattern pattern;
// 构造函数
public MYHandlerMapping(Object controller, Method method, Pattern pattern) {
Controller = controller;
this.method = method;
this.pattern = pattern;
}
// getter、setter...
public Object getController() {
return Controller;
}
public void setController(Object controller) {
Controller = controller;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public Pattern getPattern() {
return pattern;
}
public void setPattern(Pattern pattern) {
this.pattern = pattern;
}
}
2.MYHandlerAdpter
将Request变成Handler可以处理的参数,并与其形参匹配后执行。
每个 HandlerMapping 都对应一个 HandlerAdpter。因为要拿到HandlerMapping才能干活,所以有几个 HandlerMapping 就有几个 HandlerAdapter。
public class MYHandlerAdpter {
//......
}
supports()
判断当前handler能否被当前adpter进行适配,即将传入参数转换成该handler的参数并处理。因为可能要被适配handler还有文件上传等,所以这个判断还是有必要的
public boolean supports(Object handler) {
return (handler instanceof MYHandlerMapping);
}
handle()
处理请求,核心是将 request 的请求参数与处理方法的入参一一对应,然后通过 IOC 容器中的bean实例去执行方法,最后将执行结果返回
public MYModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception{
MYHandlerMapping handlerMapping = (MYHandlerMapping) handler;
// 处理请求的方法的(参数名,参数位置)
Map<String, Integer> paramIdxMapping = new HashMap<String, Integer>();
// 拿到有注解的参数,获取其参数名和参数位置
// 注:二维数组:[i][j];i-在参数中第几个位置,j-第几个注解(因为一个参数可能有多个注解
Annotation[][] pa = handlerMapping.getMethod().getParameterAnnotations();
for (int i = 0; i < pa.length; i++) {
// 第 i 个位置参数的注解们
for (Annotation a : pa[i]) {
if (a instanceof MYRequestParam) {
String paramName = ((MYRequestParam) a).value();
if (!"".equals(paramName.trim())) {
// @RequestParam(value = paramName)
paramIdxMapping.put(paramName, i);
}
}
}
}
// 获取方法中request,response参数位置
Class<?>[] parameterTypes = handlerMapping.getMethod().getParameterTypes();
// 从第一个参数开始遍历
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> type = parameterTypes[i];
if (type == HttpServletRequest.class || type == HttpServletResponse.class) {
// 注:这里放入的是类型,因为HTTPServletRequest与response唯一
paramIdxMapping.put(type.getName(), i);
}
}
// 最后传给method的参数列表,其中参数必须是与方法形参列表对应
Object[] paramValues = new Object[parameterTypes.length];
// 获取request的参数列表
Map<String, String[]> params = req.getParameterMap();
// 遍历Request的参数列表,填充方法的参数 paramValues
for (Map.Entry<String, String[]> parm : params.entrySet()) {
// 根据参数名,判断当前参数是否是method所需
if (!paramIdxMapping.containsKey(parm.getKey())) {
continue;
}
// 拿到当前参数value
// 通过正则将 []删除,空白字符换成 ,
String value = Arrays.toString(parm.getValue()).replaceAll("\\[|\\]","").replaceAll("\\s",",");
// 拿到当前参数在方法形参中的位置
Integer idx = paramIdxMapping.get(parm.getKey());
// 放入实参数组
// 注:Request中带的参数都是String类型,这里需要将它们转为method需要的正确类型
// paramTypes[idx] = idx位置的类型
paramValues[idx] = caseStringValue(value,parameterTypes[idx]);
}
// 判断当前方法是否需要Request,Response作为参数
if (paramIdxMapping.containsKey(HttpServletRequest.class.getName())) {
Integer reqIdx = paramIdxMapping.get(HttpServletRequest.class.getName());
// 拿到Request位置
paramValues[reqIdx] = req;
}
if (paramIdxMapping.containsKey(HttpServletResponse.class.getName())) {
Integer respIdx = paramIdxMapping.get(HttpServletResponse.class.getName());
paramValues[respIdx] = resp;
}
// 执行方法,获取返回结果
Object result = handlerMapping.getMethod().invoke(handlerMapping.getController(), paramValues);
// 如果该方法返回null(出错。。),或没有返回值(增加,删除。。。)。
// 那么,执行完该方法就完事了,没有后续步骤了
if (result == null || result instanceof Void) {
return null;
}
// 如果该方法返回ModelAndView
// 那么,还需要再多走一步,即对 ModelAndView进行解析
boolean isModelAndView = handlerMapping.getMethod().getReturnType() == MYModelAndView.class;
if (isModelAndView) {
// 注意,这里要将返回值强转为ModelAndView
return (MYModelAndView) result;
}
return null;
}
caseStringValue()
将String类型的value,转换为指定类型
private Object caseStringValue(String value, Class<?> parameterType) {
if (String.class == parameterType) {
return value;
}
if (Integer.class == parameterType) {
return Integer.valueOf(value);
} else if (Double.class == parameterType) {
return Double.valueOf(value);
} else {
if (value != null) {
return value;
}
return null;
}
//...还有Long等
// 可以考虑策略模式
}
3.MYModelAndView
Controller 层返回的对象,里面包含了要返回的页面,以及页面里所需要的参数。需要Resolver(模板引擎)去解析成View,View再通过模板引擎即系model,然后返回页面。
ModelAndView —(ViewResolver) —> View — (模板引擎解析model)—> HTML
public class MYModelAndView {
private String ViewName;
private Map<String, ?> model;
public MYModelAndView(String viewName, Map<String, ?> model) {
ViewName = viewName;
this.model = model;
}
public MYModelAndView(String viewName) {
ViewName = viewName;
}
public String getViewName() {
return ViewName;
}
public Map<String, ?> getModel() {
return model;
}
}
4.MYViewResolver
解析ModelAndeView的View路径,返回视图对象View。
注意:ViewResolver有多种,可将ModelAndView解析成多种View(如html,json,outputStream等)。
public class MYViewResolver {
private final String DEFALUT_TEMPALTE_SUFIX = ".html";
// 视图目录
private File templateRootDir;
public MYViewResolver(String templateRoot) {
String templateRootPath = this.getClass().getClassLoader().getResource(templateRoot).getFile();
this.templateRootDir = new File(templateRootPath);
}
// 通过页面Name,返回相应View视图
public MYView resolveViewName(String viewName, Locale locale) throws Exception {
if (null == viewName || "".equals(viewName.trim())) {
return null;
}
// 给没有 .html的加上后缀(我们可以在ModelAndView中写500.html,也可以直接写 500)
viewName = viewName.endsWith(DEFALUT_TEMPALTE_SUFIX) ? viewName : (viewName + DEFALUT_TEMPALTE_SUFIX);
// 返回相应视图
File templateFile = new File((templateRootDir.getPath() + "/" + viewName).replaceAll("/+", "/"));
return new MYView(templateFile);
}
}
5.MYView
视图对象,负责解析model,并返回页面。
注意:可以有多种返回结果(如 html,json),但这里就定义了一种基本的html返回
public class MYView {
public final String DEFULAT_CONTENT_TYPE = "text/html;charset=utf-8";
// 视图代表的页面(文件)
private File viewFile;
public MYView(File viewFile) {
this.viewFile = viewFile;
}
//......
}
render()
解析model中数据(相当于自定义模板引擎) ,最后将解析的结果通过response写出
模板引擎是解析html/jsp等页面中的 {{}} 等数据标签的工具
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception{
StringBuilder sb = new StringBuilder();
// 通过 RandomAccessFile 将要返回的页面读进内存
// 注:RandomAccessFile(提供文件读写功能) > FileInputStream + FileOutputStream。所以要指定 r或w 模式,另外 RandomAccessFile 还提供随机读写(seek等)。
RandomAccessFile ra = new RandomAccessFile(this.viewFile, "r");
// 逐行读取html文件,并进行数据解析
String line = null;
while(null != (line = ra.readLine())) {
// 为了字符集匹配,这里通过字节读取line,然后再new String
line = new String(line.getBytes("ISO-8859-1"), "utf-8");
// 通过正则表达式判断有 ¥{ } 的位置,即需要放入数据的位置
Pattern pattern = Pattern.compile("¥\\{[^\\}]+\\}",Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(line);
// 不断寻找有 ¥{ } 的位置
while (matcher.find()){
String paramName = matcher.group();
// 获取模板中的参数名
paramName = paramName.replaceAll("¥\\{|\\}","");
// 在model中通过参数名获取相应参数
Object paramValue = model.get(paramName);
// 为 null 的话,就不管,最后输出的结果还是 ¥{ }
if(null == paramValue){ continue;}
// 不为null,就将 ¥{ } 替换为相应参数
// 注意:这里对特殊字符要处理,比如异常
line = matcher.replaceFirst(makeStringForRegExp(paramValue.toString()));
// 更新 matcher,开始下一轮寻找
matcher = pattern.matcher(line);
}
// 将当前line添加到要输出的html中
sb.append(line);
}
// 将页面返回
response.setCharacterEncoding("utf-8");
response.getWriter().write(sb + "");
}
makeStringForRegExp()
处理特殊字符
public static String makeStringForRegExp(String str) {
return str.replace("\\", "\\\\").replace("*", "\\*")
.replace("+", "\\+").replace("|", "\\|")
.replace("{", "\\{").replace("}", "\\}")
.replace("(", "\\(").replace(")", "\\)")
.replace("^", "\\^").replace("$", "\\$")
.replace("[", "\\[").replace("]", "\\]")
.replace("?", "\\?").replace(",", "\\,")
.replace(".", "\\.").replace("&", "\\&");
}
6.MYDispatchServlet
DispatchServlet 入口类,负责初始化九大组件,然后分发请求
@Slf4j
public class MYDispatchServlet extends HttpServlet {
private final String CONTEXT_CONFIG_LOCATION = "contextConfigLocation";
// IOC 容器
private MYApplicationContext context;
// 保存 HandlerMapping 的容器(用于判断能否处理外部请求)
private List<MYHandlerMapping> handlerMappings = new ArrayList<MYHandlerMapping>();
// 保存 <HandlerMapping,HandlerAdpter> 映射关系的容器(用于获取执行方法的请求适配器)
private Map<MYHandlerMapping, MYHandlerAdpter> handereAdpters = new HashMap<MYHandlerMapping, MYHandlerAdpter>();
// 保存视图解析器的容器
private List<MYViewResolver> viewResolvers = new ArrayList<MYViewResolver>();
@Override
public void init(ServletConfig config) throws ServletException {
// 1.初始化ApplicationContext !!!
// tomcat会加载web.xml并创建其中配置的servlet(即DispatchServlet),同时会执行init方法
// 这里的config即web.xml配置信息,其中 contextConfigLocation 参数配置的是 application.properties 路径
context = new MYApplicationContext(config.getInitParameter(CONTEXT_CONFIG_LOCATION));
// 2.初始化SpringMVC九大组件
initStrategies(context);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
// 分发请求并处理
doDispatch(req, resp);
} catch (Exception e) {
// 顶层异常处理,如果处理请求的方法抛出异常,在这里捕获后返回提前写好的500页面
resp.getWriter().println("500 Exception,Details:\r\n" + Arrays.toString(e.getStackTrace()).replaceAll("\\[|\\]", "").replaceAll(",\\s", "\r\n"));
e.printStackTrace();
}
}
//......
}
web.xml 相关配置如下
initStrategies()
protected void initStrategies(MYApplicationContext context) {
// 多文件上传的组件
initMultipartResolver(context);
// 初始化本地语言环境
initLocaleResolver(context);
// 初始化模板处理器
initThemeResolver(context);
// handlerMapping,必须实现
initHandlerMappings(context);
// 初始化参数适配器,必须实现
initHandlerAdapters(context);
// 初始化异常拦截器
initHandlerExceptionResolvers(context);
// 初始化视图预处理器
initRequestToViewNameTranslator(context);
// 初始化视图转换器,必须实现
initViewResolvers(context);
// 参数缓存器
initFlashMapManager(context);
}
这些非必须实现的组件,我们将初始化方法直接空实现就好
private void initMultipartResolver(MYApplicationContext context) {
}
private void initLocaleResolver(MYApplicationContext context) {
}
private void initThemeResolver(MYApplicationContext context) {
}
private void initHandlerExceptionResolvers(MYApplicationContext context) {
}
private void initRequestToViewNameTranslator(MYApplicationContext context) {
}
private void initFlashMapManager(MYApplicationContext context) {
}
initHandlerMappings()
初始化 HandlerMapping
private void initHandlerMappings(MYApplicationContext context) {
// 通过 ApplicationContext#getBeanDefinitionNames 拿到所有的 beanName
String[] beanNames = context.getBeanDefinitionNames();
try {
// 根据 beanName 遍历所有 bean,去寻找所有 Controller 对象
for (String beanName : beanNames) {
// 获取到具体的bean(由于是单例的,在factoryBeanObjectCache容器中就能获取到)
Object controller = context.getBean(beanName);
// 获取到bean的Class,然后判断是否有 @MYController 注解
Class<?> clazz = controller.getClass();
// 如果不是 Controller 就返回进行下一轮循环
if (!clazz.isAnnotationPresent(MYController.class)) {
continue;
}
// 获取当前 Controller 的公有url,即类上 @RequestMapping 的路径
String baseUrl = "";
if (clazz.isAnnotationPresent(MYRequestMapping.class)) {
MYRequestMapping annotation = clazz.getAnnotation(MYRequestMapping.class);
baseUrl = annotation.value();
}
// 获取所有方法的处理路径
Method[] methods = clazz.getMethods();
for (Method method : methods) {
if (!method.isAnnotationPresent(MYRequestMapping.class)) {
continue;
}
MYRequestMapping annotation = method.getAnnotation(MYRequestMapping.class);
String regex = ("/" + baseUrl + "/" + annotation.value().replaceAll("\\*", ".*")).replaceAll("/+", "/");
Pattern pattern = Pattern.compile(regex);
// 构建处理器,并加入handlerMapping
this.handlerMappings.add(new MYHandlerMapping(controller, method, pattern));
log.info("Mapped " + regex + "," + method);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
initHandlerAdapters()
初始化参数适配器
private void initHandlerAdapters(MYApplicationContext context) {
// 为每一个 HandlerMapping 都创建一个 HandlerAdpter
for (MYHandlerMapping handlerMapping : this.handlerMappings) {
this.handereAdpters.put(handlerMapping, new MYHandlerAdpter());
}
}
initViewResolvers()
初始化视图转换器
private void initViewResolvers(MYApplicationContext context) {
// 拿到在配置文件中配置的模板存放路径(layouts)
String templateRoot = context.getConfig().getProperty("templateRoot");
// 通过相对路径找到目标后,获取到绝对路径
// 注:getResourse返回的是URL对象,getFile返回文件的绝对路径
String templateRootPath = this.getClass().getClassLoader().getResource(templateRoot).getFile();
// 拿到模板目录下的所有文件名(这里是所有html名)
File templateRootDir = new File(templateRootPath);
String[] templates = templateRootDir.list();
// 视图解析器可以有多种,且不同的模板需要不同的Resolver去解析成不同的View(jsp,html,json。。)
// 但这里其实就只有一种(解析成html)
// 为了仿真才写了这个循环,其实只循环一次
for (int i = 0; i < templates.length; i ++) {
this.viewResolvers.add(new MYViewResolver(templateRoot));
}
}
doDispatch()
请求分发 --> 通过反射执行相应方法 --> 对结果进行解析与渲染。 到这里init方法已经执行过,即已经创建好了,所以这里处理请求其实只需要拿出相应组件即可。
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception{
// 1.通过从Request中拿到URL,去匹配一个HandlerMapping
MYHandlerMapping handler = getHandler(req);
// 如果当前没有处理当前请求的方法,返回404页面
if (handler == null) {
processDispatchResult(req, resp, new MYModelAndView("404"));
return;
}
// 2.获取当前handler对应的处理参数的Adpter
MYHandlerAdpter handlerAdpter = getHandlerAdptor(handler);
// 3.Adpter负责处理 request 中携带的参数然后执行处理请求的方法
// 执行的结果可能是null(增加、删除、异常...)也可能是ModelAndView(查询...)
// Adpter真正调用处理请求的方法,返回ModelAndView(存储了页面上值,和页面模板的名称)
MYModelAndView mv = handlerAdpter.handle(req, resp, handler);
// 4.真正输出,将方法执行进行处理然后返回
// 如果上面返回的是 ModelAndView ,那么还要通过视图解析器和模板引擎进行解析
processDispatchResult(req, resp, mv);
}
getHandler()
通过Request获取相应handler
private MYHandlerMapping getHandler(HttpServletRequest req) {
if (this.handlerMappings.isEmpty()) return null;
String url = req.getRequestURI();
String contextPath = req.getContextPath();
url = url.replace(contextPath, "").replaceAll("/+", "/");
for (MYHandlerMapping handler : this.handlerMappings) {
Matcher matcher = handler.getPattern().matcher(url);
// 如果没有匹配上就继续遍历handler
if (!matcher.matches()) {
continue;
}
return handler;
}
return null;
}
processDispatchResult()
将ModelAndView解析成 HTML、json、outputStream、freemark等 --> 然后解析数据 --> 最后输出给前端
private void processDispatchResult(HttpServletRequest req, HttpServletResponse resp, MYModelAndView mv) throws Exception {
// null 表示方法返回类型是void,或返回值是null。不做额外处理
if(mv == null) {
return;
}
// 如果没有视图解析器就返回,因为无法处理ModelAndView
if (this.viewResolvers.isEmpty()) {
return;
}
// 遍历视图解析器
for (MYViewResolver viewResolver : this.viewResolvers) {
// 通过相应解析器,返回相应页面 View
MYView view = viewResolver.resolveViewName(mv.getViewName(), null);
// View通过模板引擎(自定义的)解析后输出
view.render(mv.getModel(), req, resp);
return;
}
}
getHandlerAdptor()
获取 HandlerAdptor,然后就可执行处理请求的方法了
private MYHandlerAdpter getHandlerAdptor(MYHandlerMapping handler) {
if (this.handereAdpters.isEmpty()) {
return null;
}
MYHandlerAdpter handlerAdpter = this.handereAdpters.get(handler);
// 判断当前handler能否被当前adptor进行适配
if (handlerAdpter.supports(handler)) {
return handlerAdpter;
}
return null;
}
到此 MVC 部分就写完了,我们测试一下
这里web容器我没有采用tomcat,而是用的jetty(已经提前配置好了,pom文件由于篇幅限制就不展示了,后面需要的同学可以在我的GitHub拉取源码)
首先,我们放问一个没有的页面
果然,跟 MYDIspatchServlet#doDispatch 中的分发逻辑一样,如果没有匹配到 handler 就返回 404 页面。下面我们再来访问一下
结果如下:
那如果有异常会是什么什么样子呢?
完整代码我放到 GitHub 上了,可以点击这里跳转…







还没有评论,来说两句吧...