平常我们开发时可能都是直接搭建springboot项目,然后写controller就可以直接调用了,如下:
@Controller // 或者restcontroller
public class UserController {
// 正常使用@Autowired注入:
@Autowired
UserService userService;
// 处理一个URL映射:
@GetMapping("/")
public ModelAndView index() {
...
}
...
}
这篇文章就是要探讨,其内部实现的原理,我们要探讨一下三个问题:
- Controller注解为什么可以映射实现如@WebServlet的功能
- Spring是如何工作的
- SpringBoot是如何将spring和tomcat集成的
先说springMVC
一般我们都知道,MVC分别指Model、View、Controller,这里主要讲Controller的过程,ModelAndView限于篇幅就不展开讲了。
Spring的做法其实很简单,就是通过一个能接受所有请求的Servlet,然后内部通过一些配置,来实现Mapping,所以我们一般都能看到,配置spring从如下代码开始(web.xml):
dispatcher
org.springframework.web.servlet.DispatcherServlet
dispatcher
/*
上下文加载
但仅仅配置这些,依然不行,读者可以尝试运行一下,不出意外会报错:
Could not open ServletContext resource [/WEB-INF/dispatcher-servlet.xml]
这是因为我们没有配置spring的上下文,学过springIOC都知道,spring核心就是容器装载bean然后依赖注入,但现在我们没有声明这个容器的位置,于是,spring会默认加载${servlet-name}-servlet.xml,这里我们是dispatcher-servlet.xml,但我们没有创建,所以会报错。解决方法就是去创建并声明一个上下文,配置方法有以下几种:
创建一个dispatcher-servlet.xml,并在这里注入bean
-
自己自定xml,也可以配置多个,如下:
contextConfigLocation classpath:applicationContext.xml,;classpath:bean.xml -
可以使用基于注解的java配置类,这个就像springIOC那样,有多种声明容器的方法,这里同样可以不使用默认的XmlWebApplicationContext,配置方法如下
contextClass org.springframework.web.context.support.AnnotationConfigWebApplicationContext contextConfigLocation com.cq.AppConfig 深入源码也可以看到DispatcherServlet继承自FrameworkServlet,而它有多个参数,我们上面定义的init-param元素,就是配置它的参数,而他包含一个contextClass属性:
public static final Class> DEFAULT_CONTEXT_CLASS = XmlWebApplicationContext.class; /** WebApplicationContext implementation class to create. */ private Class> contextClass = DEFAULT_CONTEXT_CLASS;
上面的配置我们使用注解配置上下文的方式取代了原先xml的配置。
ContextLoaderListener
除了DispatcherServlet的上下文,我们另外还可以在它的上一级配置上下文,也就是通常说的主上下文或者根上下文,说白了,DispatcherServlet里的上下文只有在对应的Servlet中有效,而ContextLoaderListener中配置的上下文对于所有的Servlet都有效,也可以不配置:
org.springframework.web.context.ContextLoaderListener
contextConfigLocation
/WEB-INF/spring/applicationContext.xml
DispatcherServlet处理请求
DispatcherServlet继承机构如下:
图上可以看到, DispatcherServlet继承了HttpServlet,HttpServlet的实现就是所有的入口都是service(request,response)方法,内部实现各种doGet、doPost方法。
因此DispatcherServlet处理请求的过程也是一样,只不过覆盖了HttpServlet的一些方法如下:
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {
HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
processRequest(request, response); // 如果是patch和null的请求则走这里
}
else {
super.service(request, response); // 其他请求依然走httpServlet父类的方法
}
}
httpServlet的service()方法这里就不介绍了,主要是一些校验,最终会调用各种doXXX()方法,而FrameworkServlet中doXXX系列实现中,都调用了上面提到的processRequest方法:
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
initContextHolders(request, localeContext, requestAttributes);
try {
doService(request, response);
}
catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}
finally {
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
logResult(request, response, failureCause, asyncManager);
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}
从代码中可以看到,这个函数的核心是调用doService方法,doService又去调用doDispatch方法,其之前和之后则是获取上下文参数以及解除上下文与线程的绑定,由于源码篇幅比较长,这里选取几行重要代码
mappedHandler = getHandler(processedRequest); // 获取url请求对应的执行器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());// 获取适配器
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
以上过程就是从HandlerMapping里取出对应的handler,而handlerMapping就是我们配置的几个Controler中获取对应的进行处理,具体流程比较复杂,这里不展开讲了。
不使用web.xml
既然Servelet3.0已经升级为无需配置web.xml,那我们使用springmvc的方式肯定也跟着进步,怎么配置呢,只需要实现WebApplicationInitializer即可,WebApplicationInitializer这个类有多个子类,也是抽象类,我们可以选择其中一个比如AbstractAnnotationConfigDispatcherServletInitializer,也就是注解配置类
public class ServletTestWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class>[] getRootConfigClasses() {
return new Class[0]; // 根上下文
}
@Override
protected Class>[] getServletConfigClasses() {
return new Class[]{AppConfig.class}; // servlet 上下文
}
@Override
protected String[] getServletMappings() {
return new String[]{"/*"}; // servlet上下文路径
}
}
WebApplicationInitializer接口只有一个方法onStartup(ServletContext servletContext),会在容器启动的时候执行,原因是SpringServletContainerInitializer类实现了ServletContainerInitializer,这个是javax.servlet包中的类,也就是容器启动的时候会调用,该方法有两个参数,一个是Set
ServletContainerInitializer类要求,继承这个接口并发挥作用必须在jar文件的 META-INF/services目录下声明一个文件,文件的名字是这个接口的完全限定类名称(这个类必须能被找到)。所以在META-INF可以找到这个声明