希望通过阅读源码来熟悉spring框架。
spring版本为4.1.0
WEB.XML:
<web-app>
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
contextConfigLocation属性:
当然还有一点就是contextConfigLocation的初始化参数的问题,如果你不加这个参数,那么默认的配置文件名为/WEB-INF/[servlet-name]-servlet.xml。
在org.springframework.web.servlet.FrameworkServlet这个类中有写关于contextConfigLocation的注释。如下:
<p>The default namespace is"'servlet-name'-servlet", e.g. "test-servlet" for a
*servlet-name "test" (leading to a"/WEB-INF/test-servlet.xml" default location
*with XmlWebApplicationContext). The namespace can also be set explicitly via
*the "namespace" servlet init-param.
源码:
org.springframework.context.support.AbstractRefreshableConfigApplicationContext(line:98):
org.springframework.web.context.support.XmlWebApplicationContext(line:136):
org.springframework.web.context.support.XmlWebApplicationContext(line:68):
org.springframework.web.servlet.FrameworkServlet(line:339):
org.springframework.web.servlet.FrameworkServlet(line:643):
解析:加载时调用getConfigLocation方法获取配置文件的地址,但是由于我们未配置,所以this.configLocations==null,所以调用getDefaultConfigLocation()。XmlWebApplicationContext是
AbstractRefreshableConfigApplicationContext的子类,覆盖了getDefaultConfigLocation()方法,所以实际调用的是上图的getDefaultConfigLocation方法。getDefaultConfigLocation里面getNamespace()并加上前(/WEB-INF/)后(.xml)缀返回作为默认配置文件地址。
而namespace熟悉,在开始configureAndRefreshWebApplicationContext方法中已经设置为,servletName+”-servlet”。
所以默认的地址为/WEB-INF/[servletName]-servlet.xml。
关于url-pattern的配置:
/*就是截取所有的请求。所以在springmvc里会有一个问题,他会把你的.jsp请求也拦截,还有就是静态文件也拦截,这就尴尬了~
这里讲几种配置方式:
1):加后缀,比如所有的.do结尾的请求全被拦截,这样子.jsp和其他静态资源文件就不会被拦截。
3)/模式,上网查怎么让springmvc获取静态资源的时候,很多地方是让你在web.xml里面url-patter里面配置为/,其实个人觉得这个和第一种方式是差不多。
2)/*模式,这种模式下所有请求被拦截。那么也就是当你访问.jsp的时候也会被拦截。
这时就要考虑怎么让jsp访问到,并能够解析~
下面就围绕这3种方式去解析,并会附上相关源码,请各位看官不要捉急。
1:后缀型(*.do)
这种方式是最简单的,你要访问springmvc里面的controller,在配置里面路径全部配置为已.do结尾的路径,然后在web.xml里面的url-pattern值设为*.do。这样就只会拦截以.do结尾的路径。而对于静态文件的地址就不会拦截。这种方式就不加源码说明了,太简单了。
2:/模式:
在前面其实是已经说了,这种模式其实是和后缀型差不多的,只不过他的后缀是/。所以只会拦截以/结尾的路径罢了。其实也是很简单的。这里需要指出的是,你的配置文件配置路径一点要注意是以/结尾,不然的话会请求不到。这里以SimpleUrlHandlerMapping为例,
配置文件:
<beanid="myHandlerMapping"class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<propertyname="urlMap">
<map>
<entrykey="/user/" value-ref="userController"></entry>
</map>
</property>
</bean>
3:/*模式:
这种模式就相对比较麻烦了,因为它会拦截所有的请求。这时就会出现一个问题。比如我们请求/user/,匹配到userController这个handler,然后调用其中的handlerAdapter方法,得到对应的以.jsp为后缀的视图,然后转发,这个请求再次被DispatcherServlet截取到,再次匹配适合的handler结果没匹配到,然后就在发送个404错误给页面。
上面都是文字,看起来肯定不爽,下面配上相应的源码,很开心吧~~:
源码:
请求后匹配handler:
org.springframework.web.servlet.DispatcherServlet:(line:916)
遍历所有的handlerMapping,看哪个与路径匹配。就返回,没有则返回null。
org.springframework.web.servlet.DispatcherServlet:(line:1098)
//怎么获取handler就到这里,这里就不深入了,里面无非就是获取你的请求路径,和配置文件里面配置的handler匹配罢了。
接下来是匹配handlerAdapter
org.springframework.web.servlet.DispatcherServlet:(line:922)
与前面的方法差不多,先遍历所有的handlerAdapter,然后看他支不支持这个handler
org.springframework.web.servlet.DispatcherServlet:(line:1138)
所谓支不支持,其实就是调用supports方法,在上面源代码可以看到。
以SimpleControllerHandlerAdapter为例
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter(line:42):
在支持方法里面其实就是看你是不是实现了某个接口,因为在后面,他要将你强制转为这个接口对象,然后调用方法。如下
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter(line:47):
如果所有的handlerAdapter都不匹配,那么就会抛出错误,请从下往上看第三张图。
假设都已经匹配完。
那么就会调用其相关接口实现的方法,以SimpleConrollrHandlerAdapter为例,调用handleRequest方法,返回ModelAndView对象。
接下来就是视图解析器的工作了。在配置文件中配置,这里以
为例。
遍历所有的viewResolvers来解析。
org.springframework.web.servlet.DispatcherServlet:(line:1263)
Handler里面实现的adapter方法返回可以这样写,newModalAndView(“login”),
以我前面的配置为例,则是转发至/user/login.jsp。
如果是,new ModalAndView(“redirect:login”)
则是重定向,如果redirect改为forward则还是转发。
org.springframework.web.servlet.view.UrlBasedViewResolver(line:428):
下面代码是对上面的说法的证明。
如果viewName是以redirect:开头的,那么则创建RedirectView返回。
如果是以forward:开头,那么则创建InternalResourceView对象返回。
如果什么都不带,则调用父类的createView方法。最终还是创建InteralResourceView对象。
(上面的内容是以前面的配置文件里面将viewResolver配置为InteralResourceView 为基础的,不同情况不同分析,各位不要搞混了~)
以forward为例:
则在viewName加上前后缀之后转发。
转发之后又要判断是否被拦截,又重复前面的工作。
(好,大体上把springmvc的流程讲了一遍,由于我这里没有配置到interceptor所以也没讲到哪里调用。这里先略过,如果大家觉得需要,可以在以后的文章中提到。)
下面是第二轮请求:
转发的路径为/user/login.jsp,当前模式是/*,所有的路径都拦截,当然也包括这个。所以再一次进入DispatcherServlet的doDispatch方法中。
但是很遗憾没找到.
org.springframework.web.servlet.DispatcherServlet:(line:915)
MappedHandler==null,所以调用noHandlerFound方法,然后就直接返回了,页面报404错误。
怎么才能访问到jsp呢?
上网查下,怎么样在springmvc中调用静态文件。
(当然jsp不属于静态资源文件,需要通过相关的类将它转化为servlet,这里提到了静态文件,是因为可以从怎么解决访问静态文件的方法中获取怎么去解决访问jsp的问题)
这里讲一个最简单的方式,引入mvc命名空间。
并在配置文件中加入这么一句话 <mvc:default-servlet-handler/>。
上网查一下肯定也有很多人写了关于<mvc:default-servlet-handler/>具体作用的技术贴。这里我为大家详细讲解下<mvc:default-servlet-handler/>的作用。
深入底层,那么肯定要从spring怎么去解析xml配置开始咯,我截取些比较关键的代码为大家说明。
org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader(line:178)
这段代码已经开始在解析xml文件了,首先看你的标签是不是默认命名空间的默认的则调用parseDefaultElement解析,不是默认的则调用parseCustomElement解析。<mvc:default-servlet-handler/>是mvc命名空间下的标签,所以是调用parseCustomElement解析。然后获取对应的命名空间的handler进行解析。
org.springframework.web.servlet.config.MvcNamespaceHandler
以default-servlet-handler为例:
org.springframework.web.servlet.config.DefaultServletHandlerBeanDefinitionParser
这段解析default-servlet-handle标签的作用是:
创建一个DefaultServletHttpRequestHandler,用来匹配/**也就是所有的路径。
然后再创建一个SimpleUrlHandlerMapping,并将匹配的urlMap加入。
看似只有这样,其实最后一句话又注册了3个类。
org.springframework.web.servlet.config.MvcNamespaceUtils(line:54)
BeanNameUrlHandlerMapping:用来匹配id或者name或者alias以/为开头的bean。
org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping
HttpRequestHandlerAdapter和SimpleControllerHandlerAdapter分别支持实现HttpRequestHandler和Controller的类。
由上面的解析,可以了解到,其实default-servlet-handle不过是帮我们注册几个类罢了。
这时需要注意一个问题,就是:
像我这样配置是不合适的,因为default-servlet-handler标签会先被解析,那么比如我访问的是/user/,其实我是想访问userController,但是由于default-servlet-handler帮我们注册的DefaultServletHttpRequestHandler是匹配/**,所以也能匹配到。
在前面贴出来过的代码中我们可以看到,一旦匹配到合适的handler,就会立即返回,而不会做下一次匹配。
所以被返回的是DefaultServletHttpRequestHandler,这就跟我们的意图不符了,所以在使用default-servlet-handler时,最好是把default-servlet-handler标签的位置放到所有的handlerMapping的最后。
这个时候我们不经要发问,DefaultServletHttpRequestHandler起到了什么作用呢?
其实DefaultServletHttpRequestHandler的工作很简单,只是做一个转发工作而已。
org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler(line:114)
转发给谁呢,转发给tomcat提供的名字叫default的一个默认的servlet。
这里要说明的是不同服务器叫的名字不一样
代码中也有体现。
org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler(line:54)
Default会直接读取你的文件在页面显示。
那么我这里也配置下,然后访问jsp,看看结果。
我的配置就是/*,然后加上default-servlet-handle标签,这里就不贴出了。
能访问到对于的jsp,当然这肯定不是我们所希望的,因为我们希望的是解析后的jsp。
其实这也很简单,为什么呢?我们可以试想下为什么tomcat可以解析jsp呢?是不是因为它把*.jsp的请求截取到了,然后转发到一个servlet里面然后再生产相应的servlet呢?
那么肯定也有一个跟default这个servlet一样的默认去解析jsp的一个servlet。
果然,在tomcat的web.xml里面找到
我们再查看jsp对应的servlet为
JspServlet这个类。其实我们并不需要知道对应的是哪个类,只要知道servletname为jsp,再联系下前面的DefaultServletHttpRequestHandler这个类,我们就可以找到怎么去解析jsp的方法了。只要在写一个Handler将请求*.jsp的请求转发给servletname为jsp的servlet就可以了。
定义一个JspRequestHandler:
public class JspRequestHandler implementsHttpRequestHandler,ServletContextAware{
privateServletContext servletContext=null;
publicServletContext getServletContext() {
returnservletContext;
}
publicvoid setServletContext(ServletContext servletContext) {
this.servletContext= servletContext;
}
publicvoid handleRequest(HttpServletRequest request, HttpServletResponse response)
throwsServletException, IOException {
RequestDispatcherrd = this.servletContext.getNamedDispatcher("jsp");
if(rd == null) {
thrownew IllegalStateException("A RequestDispatcher could not be located forthe default servlet jsp");
}
rd.forward(request,response);
}
}
代码和DefaultServletHttpRequestHandler,只要将default改为jsp就好了。
在配置文件里注册下。
还需要在handlerMapping里面配置下,如
这样就会把请求都拦截,然后调用我的jspHandler,然后转发给jsp。
访问下jsp页面看看效果:
果然可以。
问题又来了,你拦截了所有的,那么静态文件肯定也被你拦截了,这肯定不行,那么其实我们可以再jspHandler里面判断下,如果是jsp结尾的就转发给jsp,其他的就转发给default。这也是一个解决方法。
那么如果这样解决的话,那么mvc:default-servlet-handler这个标签确实是没有什么用了,可以去掉。
当然这样写可能更能满足你的要求,就是只拦截.jsp结尾的请求
刚开始的时候本来有个想法,是这样的,就是注册个interceptor,如果是.jsp结尾的就直接拦截住,不再转发了,先不管它不转发后会干嘛,先看这个思想可不可行呢?
肯定是不可行的。因为先匹配handler在调用interceptor之前,如果没有匹配到handler就直接调用noHandlerFound方法返回了。
org.springframework.web.servlet.DispatcherServlet(line:922)
ApplyPreHandler方法会遍历拦截器,依次调用,但是在这之前,没有获取到合适的handler,那么就已经return了。
上面就是大致的springmvc的一些最基本的配置,以及为什么这么配置,还讲了3中urlpattern。
其实我感觉最主要的是配合了源码做的一些解析,希望大家能从中获益,也不枉我写了N个小时的帖子(加上自己的一些测试,确实是N个小时啊)~~