先简单聊聊SpringMVC
如果你们玩知乎,很可能会看到我的身影。我经常会去知乎水回答。在知乎有很多初学者都会问的一个问题:「我学习SpringMVC需要什么样的基础」
我一定会让他们先学Servlet,再学SpringMVC的。虽然说我们在现实开发中几乎不会写原生Servlet的代码了,但我始终认为学完Servlet再学SpringMVC,对理解SpringMVC是有好处的。
SpringMVC之前其实已经有另外一个web框架,那就是「大名鼎鼎」的Struts2。只要是Struts2有的功能,SpringMVC都会有。
初学Struts2的时候用的是XML配置的方式去开发的,再转到SpringMVC注解的时候,觉得SpringMVC真香。
Struts2在2020年已经不用学了,学SpringMVC的基础是Servlet,只要Servlet基础还行,上手SpringMVC应该不成问题。
从Servlet到SpringMVC,你会发现SpringMVC帮我们做了很多的东西,我们的代码肯定是没以前多了。
Servlet:
我们以前可能需要将传递进来的参数手动封装成一个Bean,然后继续往下传:
SpringMVC:
现在SpringMVC自动帮我们将参数封装成一个Bean
Servlet:
以前我们要导入其他的jar
包去手动处理文件上传的细节:
SpringMVC:
现在SpringMVC上传文件用一个MultipartFile对象都给我们封装好了
........
说白了,在Servlet时期我们这些活都能干,只不过SpringMVC把很多东西都给屏蔽了,于是我们用起来就更加舒心了。
在学习SpringMVC的时候实际上也是学习这些功能是怎么用的而已,并不会太难。这次整理的SpringMVC电子书其实也是在讲SpringMVC是如何使用的
比如说传递一个日期字符串来,SpringMVC默认是不能转成日期的,那我们可以怎么做来实现。
SpringMVC的文件上传是怎么使用的
SpringMVC的拦截器是怎么使用的
SpringMVC是怎么对参数绑定的
......
现在「电子书」已经放出来了,但是别急,重头戏在后面。显然,通过上面的电子书是可以知道SpringMVC是怎么用的。
但是这在面试的时候人家是不会问你SpringMVC的一些用法的,而SpringMVC面试问得最多的就是:SpringMVC请求处理的流程是怎么样的。
其实也很简单,流程就是下面这张图:
再简化一点,可以发现流程不复杂
在面试的时候甚至能一句话就讲完了,但这够吗,这是面试官想要的吗?那肯定不是。那SpringMVC是做了什么吗?
由于想要主流程更加清晰一点,我会在源码添加部分注释以及删减部分的代码
以@ResponseBody和@RequestBody的Controller代码讲解为主,这是线上环境用得最多的
DispatcherServlet源码
首先我们看看DispatcherServlet的类结构,可以清楚地发现实际DispatcherServlet就是Servlet接口的一个子类(这也就是为什么网上这么多人说DispatcherServlet的原理实际上就是Servlet)
我们在DispatcherServlet类上可以看到很多熟悉的成员变量(组件),所以看下来,我们要的东西,DispatcherServlet可全都有。
// 文件处理器
private MultipartResolver multipartResolver;
// 映射器
private List<HandlerMapping> handlerMappings;
// 适配器
private List<HandlerAdapter> handlerAdapters;
// 异常处理器
private List<HandlerExceptionResolver> handlerExceptionResolvers;
// 视图解析器
private List<ViewResolver> viewResolvers;
然后我们会发现它们在initStrategies()
上初始化:
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
请求进到DispatcherServlet,其实全部都会打到doService()
方法上。我们看看这个doService()
方法做了啥:
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 设置一些上下文...(省略一大部分)
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
try {
// 调用doDispatch
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
}
}
所以请求会走到doDispatch(request, response);
里边,我们再进去看看:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
// 检查是不是文件上传请求
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 找到HandlerExecutionChain
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
noHandlerFound(processedRequest, response);
return;
}
// 得到对应的hanlder适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 拦截前置处理
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 真实处理请求
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 视图解析器处理
applyDefaultViewName(processedRequest, mv);
// 拦截后置处理
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
}
}
这里的流程跟我们上面的图的流程几乎是一致的了。我们从源码可以知道的是,原来SpringMVC的拦截器是在MappingHandler的时候一齐返回的,返回的是一个HandlerExecutionChain
对象。这个对象也不难,我们看看:
public class HandlerExecutionChain {
private static final Log logger = LogFactory.getLog(HandlerExecutionChain.class);
// 真实的handler
private final Object handler;
// 拦截器List
private HandlerInterceptor[] interceptors;
private List<HandlerInterceptor> interceptorList;
private int interceptorIndex = -1;
}
OK,整体的流程我们是已经看完了,顺便要不我们去看看它是怎么找到handler的?点进去getHandler()
后,发现它就把默认实现的Handler遍历一遍,然后选出合适的:
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
// 遍历一遍默认的Handler实例,选出合适的就返回
for (HandlerMapping hm : this.handlerMappings) {
HandlerExecutionChain handler = hm.getHandler(request);
if (handler != null) {
return handler;
}
}
return null;
}
再进去getHandler
里边看看呗,里边又有几层,我们最后可以看到它根据路径去匹配,走到了lookupHandlerMethod
这么一个方法
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<Match>();
// 获取路径
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
// 对匹配的排序,找到最佳匹配的
if (!matches.isEmpty()) {
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
Collections.sort(matches, comparator);
if (logger.isTraceEnabled()) {
logger.trace("Found " + matches.size() + " matching mapping(s) for [" +
lookupPath + "] : " + matches);
}
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
if (CorsUtils.isPreFlightRequest(request)) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" +
request.getRequestURL() + "': {" + m1 + ", " + m2 + "}");
}
}
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
else {
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}
找拦截器大概也是上面的一个过程,于是我们就可以顺利拿到HandlerExecutionChain
了,找到HandlerExecutionChain
后,我们是先去拿对应的HandlerAdaptor
。我们也去看看里边做了什么:
// 遍历HandlerAdapter实例,找到个合适的返回
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
for (HandlerAdapter ha : this.handlerAdapters) {
if (ha.supports(handler)) {
return ha;
}
}
}
我们看一个常用HandlerAdapter实例RequestMappingHandlerAdapter
,会发现他会初始化很多的参数解析器,其实我们经常用的@ResponseBody
解析器就被内置在里边:
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();
resolvers.add(new MatrixVariableMethodArgumentResolver());
resolvers.add(new MatrixVariableMapMethodArgumentResolver());
resolvers.add(new ServletModelAttributeMethodProcessor(false));
// ResponseBody Requestbody解析器
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), t
// 等等
return resolvers;
}
得到HandlerAdaptor后,随之而行的就是拦截器的前置处理,然后就是真实的
mv = ha.handle(processedRequest, response, mappedHandler.getHandler())
。
这里边嵌套了好几层,我就不一一贴代码了,我们会进入ServletInvocableHandlerMethod#invokeAndHandle
方法,我们看一下这里边做了什么:
public void invokeAndHandle(ServletWebRequest webRequest,
ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
// 处理请求
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);
if (returnValue == null) {
if (isRequestNotModified(webRequest) || hasResponseStatus() || mavContainer.isRequestHandled()) {
mavContainer.setRequestHandled(true);
return;
}
}
//..
mavContainer.setRequestHandled(false);
try {
// 处理返回值
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
}
处理请求的方法我们进去看看invokeForRequest
public Object invokeForRequest(NativeWebRequest request, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 得到参数
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
// 调用方法
Object returnValue = doInvoke(args);
if (logger.isTraceEnabled()) {
logger.trace("Method [" + getMethod().getName() + "] returned [" + returnValue + "]");
}
return returnValue;
}
我们看看它是怎么处理参数的,getMethodArgumentValues
方法进去看看:
private Object[] getMethodArgumentValues(NativeWebRequest request, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 得到参数
MethodParameter[] parameters = getMethodParameters();
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
GenericTypeResolver.resolveParameterType(parameter, getBean().getClass());
args[i] = resolveProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
// 找到适配的参数解析器
if (this.argumentResolvers.supportsParameter(parameter)) {
try {
args[i] = this.argumentResolvers.resolveArgument(
parameter, mavContainer, request, this.dataBinderFactory);
continue;
}
//.....
}
return args;
}
这些参数解析器实际上在HandlerAdaptor内置的那些,这里不好放代码,所以我截个图吧:
针对于RequestResponseBodyMethodProcessor解析器我们看看里边做了什么:
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// 通过Converters对参数转换
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
// ...
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
return arg;
}
再进去readWithMessageConverters
里边看看:
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter param,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
// ...处理请求头
try {
inputMessage = new EmptyBodyCheckingHttpInputMessage(inputMessage);
// HttpMessageConverter实例去对参数转换
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
if (converter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter;
if (genericConverter.canRead(targetType, contextClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
}
if (inputMessage.getBody() != null) {
inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType);
body = genericConverter.read(targetType, contextClass, inputMessage);
body = getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType);
} else {
body = null;
body = getAdvice().handleEmptyBody(body, inputMessage, param, targetType, converterType);
}
break;
}
}
//...各种判断
return body;
}
看到这里,有没有看不懂,想要退出的感觉了??看看这份熟悉的配置:
<!-- 启动JSON返回格式 -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="messageConverters">
<list>
<ref bean="jacksonMessageConverter" />
</list>
</property>
</bean>
<bean id="jacksonMessageConverter"
class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/html;charset=UTF-8</value>
<value>application/json;charset=UTF-8</value>
<value>application/x-www-form-urlencoded;charset=UTF-8</value>
</list>
</property>
<property name="objectMapper" ref="jacksonObjectMapper" />
</bean>
<bean id="jacksonObjectMapper" class="com.fasterxml.jackson.databind.ObjectMapper" />
我们在SpringMVC想要使用@ResponseBody
返回JSON格式都会在配置文件上配置上面的配置,RequestMappingHandlerAdapter
这个适配器就是上面所说的那个,内置了RequestResponseBodyMethodProcessor
解析器,然后MappingJackson2HttpMessageConverter
实际上就是HttpMessageConverter
接口的实例
然后在返回的时候也经过HttpMessageConverter去将参数转换后,写给HTTP响应报文。转换的流程大致如图所示:
视图解析器后面就不贴了,大概的流程就如上面的源码,我再画个图来加深一下理解吧:
最后
SpringMVC我们使用的时候非常简便,在内部实际上帮我们做了很多(有各种的HandlerAdaptor),SpringMVC的请求流程面试的时候还是面得很多的,还是可以看看源码它帮我们做了什么,过一遍可能会发现自己能看懂以前的配置了。
现在已经工作有一段时间了,为什么还来写SpringMVC
呢,原因有以下几个:
我是一个对排版有追求的人,如果早期关注我的同学可能会发现,我的GitHub、文章导航的
read.me
会经常更换。现在的GitHub导航也不合我心意了(太长了),并且早期的文章,说实话排版也不太行,我决定重新搞一波。我的文章会分发好几个平台,但文章发完了可能就没人看了,并且图床很可能因为平台的防盗链就挂掉了。又因为有很多的读者问我:”你能不能把你的文章转成PDF啊?“
我写过很多系列级的文章,这些文章就几乎不会有太大的改动了,就非常适合把它们给”持久化“。
基于上面的原因,我决定把我的系列文章汇总成一个PDF/HTML/WORD/epub
文档。说实话,打造这么一个文档花了我不少的时间。为了防止白嫖,关注我的公众号回复「888」即可获取。
参考资料:
https://www.cnblogs.com/java-chen-hao/category/1503579.html
https://www.jianshu.com/p/1bff57c74037
https://stackoverflow.com/questions/18682486/why-does-spring-mvc-need-at-least-two-contexts
Servlet:我还活着呢!
我是Servlet, 由于很多框架把我深深地隐藏了起来,我变得似乎无关紧要了,很多人也选择性的把我给遗忘了。 其实,我还活得好好的呢, 只不过是从前台明星慢慢退居幕后而已。
好基友Servlet + JSP
想当年我刚刚诞生的时候,无数人对我趋之若鹜。
因为那个时候Web服务器只能处理静态的HTML页面,图片,JavaScript这样的东西, 比如Apache 这个著名的Web服务器。
人类想要看一点动态的内容,比如什么留言板,购物网站等,还得靠极为难用的CGI。
我一出生, 他们就欢呼着把CGI给抛弃,纷纷改用Java写Servlet程序, 再后来我的好兄弟JSP问世,我们简直形成了绝配。
我负责控制,JSP负责视图,再加上负责数据的Java Bean, MVC三驾马车正式形成,风靡一时,想当年,著名的开源论坛软件Jive就是我们的巅峰之作。
说起JSP,这小子有时候还不太服我,经常振振有词地说:“你Servlet没什么了不起的,我也可以当Controller!”
JSP确实可以当Controller, 早些年我还真的见过,一个长达6000多行的JSP,行使着Controller的职责,每当程序员要改这些代码就胆颤心惊,叫苦不迭。
其实JSP不知道,它本质上也就是Servlet ,JSP只不过穿了一件漂亮的外衣,给了程序员们一个轻松写动态页面的工具而已,实际运行的时候会被编译成Servlet类, 本质上我们是一样的。
我和JSP都生活在Servlet Container当中,Container这个词有点高大上,但是说白了,无非就是能执行Servlet和JSP的一个东西,比如说Tomcat, 比如说Jetty。
但是无论是我还是JSP, 我们能处理的只是HTTP请求,必须得有人把HTTP请求转发给我们才可以。
这件事情只有让Tomcat, Jetty他们来做了,他们自己可以接收HTTP请求,然后转发给我们;
他们也可以从别人,例如Apache那里接收HTTP请求,然后发给我们处理,处理完了再转发给Apache, Apache再发给人类的浏览器。
虽然有点麻烦, 但是这种方式确实非常灵活,各司其职,扩展性比较好,比如,一个Apache可以把请求分发给后台多个Tomcat中的一个。
Apache,Nginx 他们专心致志地去处理静态内容(HTML, JS, 图片) ,我们这里心无旁骛地执地执行“不讲逻辑的”业务逻辑,访问数据库,然后生成页面返回。
Application Server
日子过得波澜不惊,我一度认为,这个世界就将这么运行下去。
应用程序越开发越多,出现了一些通用的需求,比如安全,事务,分布式等等,这些需求应用程序不愿意处理,想丢给操作系统,操作系统也不愿意处理, 那怎么办?
不知道是谁提了一个叫做中间件的概念: 你们不愿意做的,我们中间件来做。
Java 世界也不敢怠慢,也搞出了一大堆的规范,像什么EJB,JMS,JTA等等,把我和JSP也合并到其中,形成一个大“杂烩”,叫做J2EE。
其中最春风得意的就是EJB这家伙,独自生活在EJB Container中(又是Container!),号称能支持真正地分布式计算:一个EJB可以有多个实例,分布到多个服务器中,应对用户的请求, 听起来很高深的技术。
他们把Servlet Container称为Web Container , 和EJB Container 一起,还有其他的一些东西,被合并到一个叫做Application Server当中去了。 最知名的几个Application Server 就是 Weblogic , WebSphere , JBoss。
国内的金蝶也实现过一个,叫做Apusic,虽然影响力不如前面那几位,但值得赞赏。
退居幕后
我和JSP都没有料到,EJB抢了我们的风头,成了系统的中心, 让我们极为不爽。
我和JSP岂能善罢甘休? 我们决定抓住EJB的弱点进行反击, 我们和人类一个叫做Rod Johnson的联合,让他出面,列举出EJB的36大罪状,昭告天下,这些罪状包括但不限于:笨重,性能低下,难于测试,昂贵....
EJB确实是个扶不起的阿斗, 很快就被人批得体无完肤,大家纷纷投入Rod Johnson 创建的Spring的怀抱。
我松了一口气, 可是很快就发现事情不对劲,大家纷纷用起了框架! 比如Struts, SpringMVC......
在这些框架中,我虽然处于一个非常重要的角色, 但是通常情况下只要配置一下web.xml,就可以把我扔到一边了。
Container 照例把HTTP请求传递给我,但是我却不能亲手处理,我需要传递给框架,框架分派给Controller,没我什么事了!
那些程序员们要做的事情就是写Controller, Service , DAO这些和我八班杆子打不着的东西。
我恨框架, 但是看到程序员们写代码写得那么高兴,又无话可说,毕竟框架极大地减少了他们的工作量:
之前对于每个HTTP请求,程序员得手工地去解析URL, 调用相关的Java Bean。
现在只需要用个配置文件或者注解就可以把URL给映射到一个Java 类。
之前对于HTTP请求中的参数, 程序员也得手工解析和验证。
现在也可以直接映射到Java 对象或者变量
......
用起来这么简单,他们不用才怪。
更让人生气的是,Rod他们后来倒腾出来一个叫做Spring Boot的东西,彻底地把我给隐藏起来了!
尤其是对于一个新手来说,甚至完全不知道我的存在。
Tomcat和Jetty这样的Servlet Container也很悲催,他们竟然被内嵌到了Spring Boot中! 程序员开发出的Web应用,就像一个普通的Java程序一样,从main函数开始运行。
我们彻底地退居幕后了!
不过我有义务提醒一下学习后端编程的同学,不要一上来就学习框架,不要被框架迷住你的双眼!
还是应该好好看看最基本的Java Web, 就是我和我的兄弟JSP。
威胁来临
虽然是退居幕后,但是我的核心地位依然稳固,是Java Web应用的中坚力量,我生活在Servlet Container中,专门处理HTTP请求,这么多年难逢敌手。
直到有一天,有个叫做Netty的家伙上门挑战。
这个Netty居然完全不用Servlet Container,或者换句话说,人家自己就是一个“Container” , 这对我来说绝对是釜底抽薪的攻击, 我引以为傲的Servlet 规范, Servlet API统统不管用了。
我只能处理HTTP, 可是这个Netty支持各种各样的协议:HTTP, FTP, UDP, 它还支持实现各种各样自定义的协议! 这就意味着程序员完全可以自定义一套自己应用的RPC协议,然后放到Netty上运行。
它的底层是Java NIO,又封装了Java NIO那些复杂的底层细节,可以轻松实现高性能、高可靠的网络服务器, 这实在是太可怕了。
我似乎看到了一个可怕的场景: 用Netty 开发的服务器端,运行着众多的Web 服务,他们之间使用私有的协议在互相调用,效率极高,性能极高, 根本没有Servlet, HTTP, Tomcat什么事。
让我稍感安慰的是,直接使用Netty的程序员们还不多,虽说有不少人在使用基于Netty的Dubbo, 但是Netty也被封装隐藏起来了。 我估计真正具有钻研精神的程序员才愿意去研究他吧。