完成前边的内容之后,距离完整的spring框架就只剩下一个WebMVC了
对于spring的WebMvc来说,提供了以下的组件和API支持:

  1. 一个DispatcherServlet作为核心处理组件,接收所有URL请求,然后按MVC规则转发;
  2. 基于@Controller注解的URL控制器,由应用程序提供,Spring负责解析规则;
  3. 提供ViewResolver,将应用程序的Controller处理后的结果进行渲染,给浏览器返回页面;
  4. 基于@RestController注解的REST处理机制,由应用程序提供,Spring负责将输入输出变为JSON格式;
  5. 多种拦截器和异常处理器等。

但是对于我们的简单WebMvc来说,只需要支持核心内容就好

  1. DispatcherServlet
  2. @Controller注解
  3. @RestController注解
  4. ViewResolver

首先,Java Web应用一般遵循Servlet标准,这个标准定义了应用程序可以按接口编写哪些组件:Servlet、Filter和Listener,也规定了一个服务器(如Tomcat、Jetty、JBoss等)应该提供什么样的服务,按什么顺序加载应用程序的组件,最后才能跑起来处理来自用户的HTTP请求。

Servlet规范定义的组件有3类:

  1. Servlet:处理HTTP请求,然后输出响应;
  2. Filter:对HTTP请求进行过滤,可以有多个Filter形成过滤器链,实现权限检查、限流、缓存等逻辑;
  3. Listener:用来监听Web应用程序产生的事件,包括启动、停止、Session有修改等。

而服务器为一个应用程序提供一个“容器”,即Servlet Container,一个Server可以同时跑多个Container,不同的Container可以按URL、域名等区分,Container才是用来管理Servlet、Filter、Listener这些组件的:

那么问题是,组件由谁创建又是是恶销毁呢?毕竟除了Servelet容器外,还有IOC容器,IOC又有很多的bean。

因为我们没有机会改变Servlet规范,所以IOC也应该被Servlet容器管理

对于一个Web应用程序来说,启动时,应用程序本身只是一个war包,并没有main()方法,因此,启动时执行  的是Server的main()方法。以Tomcat服务器为例:

启动服务器,即执行Tomcat的main()方法;
Tomcat根据配置或自动检测到一个xyz.war包后,为这个xyz.war应用程序创建Servlet容器;
Tomcat继续查找xyz.war定义的Servlet、Filter和Listener组件,按顺序实例化每个组件(Listener最  先被实例化,然后是Filter,最后是Servlet);
用户发送HTTP请求,Tomcat收到请求后,转发给Servlet容器,容器根据应用程序定义的映射,把请求发送   个若干Filter和一个Servlet处理;
处理期间产生的事件则由Servlet容器自动调用Listener。
其中,第3步实例化又有很多方式:

通过在web.xml配置文件中定义,这也是早期Servlet规范唯一的配置方式;
通过注解@WebServlet、@WebFilter和@WebListener定义,由Servlet容器自动扫描所有class后创建组   件,这和我们用Annotation配置Bean,由IoC容器自动扫描创建Bean非常类似;
先配置一个Listener,由Servlet容器创建Listener,然后,Listener自己调用相关接口,手动创建 Servlet和Filter。
到底用哪种方式,取决于Web应用程序自己如何编写。对于使用Spring框架的Web应用程序来说,Servlet、   Filter和Listener数量少,而且是固定的,应用程序自身编写的Controller数量不定,但由IoC容器管  理,因此,采用方式3最合适。

具体来说,Tomcat启动一个基于Spring开发的Web应用程序时,按如下步骤初始化:

为Web应用程序准备Servlet容器;
根据配置实例化一个Spring提供的Listener;
Spring提供的Listener在初始化时启动IoC容器;
Spring提供的Listener在初始化时向Servlet容器注册Spring内置的一个DispatcherServlet。
当Tomcat把HTTP请求发送给Spring注册的Servlet后,因为它持有IoC容器的引用,就可以找到Controller    实例,因此,可以把请求继续转发给对应的Controller,这样就完成了HTTP请求的处理。

另外注意到Web应用程序除了提供Controller外,并不必须与Servlet API打交道,因为被Spring提供的  DispatcherServlet给隔离了。

所以,我们在开发Summer Framework的Web MVC模块时,应该以如下方式初始化:

应用程序必须配置一个Summer Framework提供的Listener;
Tomcat完成Servlet容器的创建后,立刻根据配置创建Listener;
Listener初始化时创建IoC容器;
Listener继续创建DispatcherServlet实例,并向Servlet容器注册;
DispatcherServlet初始化时获取到IoC容器中的Controller实例,因此可以根据URL调用不同Controller 实例的不同处理方法。
来源:廖雪峰官网https://liaoxuefeng.com/books/summerframework/web/start-ioc/index.html

启动流程总结

我们编写一个DispatcherServlet类

public class DispatcherServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
PrintWriter pw = resp.getWriter();
pw.write("<h1>Hello, world!</h1>");
pw.flush();
}
}

编写一个ContextLoaderListener,实现ServletContextListener,用于监听Servlet容器的启动和销毁

public class ContextLoaderListener implements ServletContextListener {
// Servlet容器启动时自动调用:
@Override
public void contextInitialized(ServletContextEvent sce) {
// 创建IoC容器:
var applicationContext = createApplicationContext(...);
// 实例化DispatcherServlet:
var dispatcherServlet = new DispatcherServlet();
// 注册DispatcherServlet:
var dispatcherReg = servletContext.addServlet("dispatcherServlet", dispatcherServlet);
dispatcherReg.addMapping("/");
dispatcherReg.setLoadOnStartup(0);
}
}

接下来我们要继续开发DispatcherServlet
首先我们先定义好要使用到的注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
String value() default "";
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface RestController {
String value() default "";
}

在DispatcherServlet初始化的时候,我们要找到所有标记了@Controller类注解的bean,扫描有@GetMapping以及@PostMapping,对于这些内容,我们像BeanDefinition一样,抽象一个类来处理特定的URL

class Dispatcher {
// 是否返回REST:
boolean isRest;
// 是否有@ResponseBody:
boolean isResponseBody;
// 是否返回void:
boolean isVoid;
// URL正则匹配:
Pattern urlPattern;
// Bean实例:
Object controller;
// 处理方法:
Method handlerMethod;
// 方法参数:
Param[] methodParameters;
}

因为参数还有@RequestParam等类型所以参数我们也要处理

class Param {
// 参数名称:
String name;
// 参数类型:
ParamType paramType;
// 参数Class类型:
Class<?> classType;
// 参数默认值
String defaultValue;
}

然后DispatcherServlet通过反射拿到一组Dispatcher,在doGet与doPost的方法中,一次匹配URL

因为我们要匹配/hello/{name}这种,所以我们只能使用正则表达式匹配

ModelAndView

为了处理ModelAndView,我们需要一个模板引擎,因此,抽象出ViewResolver接口

public interface ViewResolver {
// 初始化ViewResolver:
void init();

// 渲染:
void render(String viewName, Map<String, Object> model, HttpServletRequest req, HttpServletResponse resp);
}

WebMvcConfiguration配置

简化Web应用程序配置


@Configuration
public class WebMvcConfiguration {
private static ServletContext servletContext = null;
static void setServletContext(ServletContext ctx) {
servletContext = ctx;
}

@Bean(initMethod = "init")
ViewResolver viewResolver( //
@Autowired ServletContext servletContext, //
@Value("${summer.web.freemarker.template-path:/WEB-INF/templates}") String templatePath, //
@Value("${summer.web.freemarker.template-encoding:UTF-8}") String templateEncoding) {
return new FreeMarkerViewResolver(servletContext, templatePath, templateEncoding);
}

@Bean
ServletContext servletContext() {
return Objects.requireNonNull(servletContext, "ServletContext is not set.");
}
}

默认创建一个ViewResolver和ServletContext,注意ServletContext本身实际上是由Servlet容器提供的,但我们把它放入IoC容器,是因为许多涉及到Web的组件,如ViewResolver,需要注入ServletContext,才能从指定配置加载文件。

注意事项

在整个HTTP处理流程中,入口是DispatcherServlet的service()方法,整个流程如下:

Servlet容器调用DispatcherServlet的service()方法处理HTTP请求;
service()根据GET或POST调用doGet()或doPost()方法;
根据URL依次匹配Dispatcher,匹配后调用process()方法,获得返回值;
根据返回值写入响应:
void或null返回值无需写入响应;
String或byte[]返回值直接写入响应(或重定向);
REST类型写入JSON序列化结果;
ModelAndView类型调用ViewResolver写入渲染结果。
未匹配到判断是否静态资源:
符合静态目录(默认/static/)则读取文件,写入文件内容;
网站图标(默认/favicon.ico)则读取.ico文件,写入文件内容;
其他情况返回404。