SpringMVC实现原理与SpringBootMVC实现原理
一、MVC
MVC是模型(Model)、视图(View)、控制器(Controller)的简写,是一种软件设计规范。是将业务逻辑、数据、显示分离的方法来组织代码。
MVC主要作用是降低了视图与业务逻辑间的双向耦合。
MVC不是一种设计模式,MVC是一种架构模式。当然不同的MVC存在差异。
Model(模型):数据模型,提供要展示的数据,因此包含数据和行为,可以认为是领域模型或JavaBean组件(包含数据和行为),不过现在一般都分离开来:Value Object(数据Dao) 和 服务层(行为Service)。也就是模型提供了模型数据查询和模型数据的状态更新等功能,包括数据和业务。
View(视图):负责进行模型的展示,一般就是我们见到的用户界面,客户想看到的东西。
Controller(控制器):接收用户请求,委托给模型进行处理(状态改变),处理完毕后把返回的模型数据返回给视图,由视图负责展示。也就是说控制器做了个调度员的工作。
二、SpringMVC实现原理
官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc
Spring MVC是Spring Framework的一部分,是基于Java实现MVC的轻量级Web框架。Spring的web框架围绕DispatcherServlet设计。DispatcherServlet的作用是将请求分发到不同的处理器。从Spring 2.5开始,使用Java 5或者以上版本的用户可以采用基于注解的controller声明方式。
Spring MVC框架像许多其他MVC框架一样, 以请求为驱动 , 围绕一个中心Servlet分派请求及提供其他功能,DispatcherServlet是一个实际的Servlet (它继承自HttpServlet 基类)。
SpringMVC底层实现流程
1.DispatcherServlet表示前置控制器,是整个SpringMVC的控制中心。用户发出请求,DispatcherServlet接收请求并拦截请求。
我们假设请求的url为 : http://localhost:8080/SpringMVC/hello
如上url拆分成三部分:
- http://localhost:8080 服务器域名
- SpringMVC 部署在服务器上的web站点
- hello 表示控制器
通过分析,如上url表示为:请求位于服务器localhost:8080上的SpringMVC站点的hello控制器。
2.HandlerMapping为处理器映射。DispatcherServlet调用HandlerMapping,HandlerMapping根据请求url查找Handler。
3.HandlerExecution表示具体的Handler,其主要作用是根据url查找控制器,如上url被查找控制器为:hello。
4.HandlerExecution将解析后的信息传递给DispatcherServlet,如解析控制器映射等。
5.HandlerAdapter表示处理器适配器,其按照特定的规则去执行Handler。
6.Handler让具体的Controller执行。
7.Controller将具体的执行信息返回给HandlerAdapter,如ModelAndView。
8.HandlerAdapter将视图逻辑名或模型传递给DispatcherServlet。
9.DispatcherServlet调用视图解析器(ViewResolver)来解析HandlerAdapter传递的逻辑视图名。
10视图解析器将解析的逻辑视图名传给DispatcherServlet。
11.DispatcherServlet根据视图解析器解析的视图结果,调用具体的视图。
源码简单解析MVC
在spring-webmvc的jar包下找到DispatcherServlet类,这是SpringMVC整个Web框架的核心和控制中心;
1.DispatcherServlet生成对象时,先对属性进行初始化的设置
public static final String WEB_APPLICATION_CONTEXT_ATTRIBUTE = DispatcherServlet.class.getName() + ".CONTEXT";
public static final String LOCALE_RESOLVER_ATTRIBUTE = DispatcherServlet.class.getName() + ".LOCALE_RESOLVER";
public static final String THEME_RESOLVER_ATTRIBUTE = DispatcherServlet.class.getName() + ".THEME_RESOLVER";
public static final String THEME_SOURCE_ATTRIBUTE = DispatcherServlet.class.getName() + ".THEME_SOURCE";
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, this.getThemeSource());
2.DispatcherServlet接收到用户发来的请求,根据请求的URL调用处理器映射器来得到对应的处理器
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
Iterator var2 = this.handlerMappings.iterator();
while(var2.hasNext()) {
HandlerMapping mapping = (HandlerMapping)var2.next();
HandlerExecutionChain handler = mapping.getHandler(request);//请求对应的处理器
if (handler != null) {
return handler;
}
}
}
return null;
}
3.处理器解析请求以后,DispatcherServlet根据解析到的信息调用处理器适配器来得到控制器
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
Iterator var2 = this.handlerAdapters.iterator();
while(var2.hasNext()) {
HandlerAdapter adapter = (HandlerAdapter)var2.next();
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
4.找到控制器后,由控制器Controller执行真正的处理请求的行为(根据请求调用service层对象访问数据库、得到数据、实现需求等)
并具体的执行信息(ModelAndView)返回给HandlerAdapter,HandlerAdapter将视图逻辑名或模型传递给DispatcherServlet。
5.DispatcherServlet调用视图解析器(ViewResolver)来解析HandlerAdapter传递的逻辑视图名。
@Nullable
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception {
if (this.viewResolvers != null) {
Iterator var5 = this.viewResolvers.iterator();
while(var5.hasNext()) {
ViewResolver viewResolver = (ViewResolver)var5.next();
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
}
return null;
}
6.DispatcherServlet根据视图解析器解析的视图结果,调用具体的视图,返回到用户。
view.render(mv.getModelInternal(), request, response);
三、SpringBootMVC实现原理
与其他Spring开发功能相似,SpringBoot为MVC的框架也实现了自动默认配置,这些自动配置由WebMvcAutoConfiguration类来实现。
在这里主要对SpringBootMVC实现视图解析器、静态资源处理、格式转换器自动配置原理进行简单记录。
视图解析器
SpringBoot中的视图解析功能由ContentNegotiatingViewResolver类来实现,由该类的实例对象来根据方法的返回值取得视图对象,再进行渲染。WebMvcAutoConfiguration类中有得到视图解析器viewResolver对象的方法
@Bean
@ConditionalOnBean({ViewResolver.class})
@ConditionalOnMissingBean(name = {"viewResolver"},value = {ContentNegotiatingViewResolver.class})
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
resolver.setContentNegotiationManager((ContentNegotiationManager)beanFactory.getBean(ContentNegotiationManager.class));
// ContentNegotiatingViewResolver使用所有其他视图解析器来定位视图,具有较高的优先级
resolver.setOrder(-2147483648);
return resolver;
}
接下来可以探究ContentNegotiatingViewResolver类中解析视图(获取视图)操作的源码,类中定义了一个方法如下
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
List<MediaType> requestedMediaTypes = this.getMediaTypes(((ServletRequestAttributes)attrs).getRequest());
if (requestedMediaTypes != null) {//获取候选的视图对象
List<View> candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes);
View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs);//选择一个最合适的试图对象bestView然后返回这个对象
if (bestView != null) {
return bestView;
}
}
如何获取候选的对象,点进getCandidateViews的源码
private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes) throws Exception {
List<View> candidateViews = new ArrayList();
if (this.viewResolvers != null) {
Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
Iterator var5 = this.viewResolvers.iterator();
while(var5.hasNext()) {
ViewResolver viewResolver = (ViewResolver)var5.next();
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
candidateViews.add(view);
}
Iterator var8 = requestedMediaTypes.iterator();
while(var8.hasNext()) {
MediaType requestedMediaType = (MediaType)var8.next();
List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
Iterator var11 = extensions.iterator();
while(var11.hasNext()) {
String extension = (String)var11.next();
String viewNameWithExtension = viewName + '.' + extension;
view = viewResolver.resolveViewName(viewNameWithExtension, locale);
if (view != null) {
candidateViews.add(view);
}
}
}
}
}
可得,该方法getCandidateViews是将所有的视图都放到List中,然后把所有的视图解析器拿来进行while循环逐个解析
如何获得最合适的视图,点进getBestView方法的源码
private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {
Iterator var4 = candidateViews.iterator();
while(var4.hasNext()) {
View candidateView = (View)var4.next();
if (candidateView instanceof SmartView) {
SmartView smartView = (SmartView)candidateView;
if (smartView.isRedirectView()) {
return candidateView;
}
}
}
var4 = requestedMediaTypes.iterator();
while(var4.hasNext()) {
MediaType mediaType = (MediaType)var4.next();
Iterator var10 = candidateViews.iterator();
while(var10.hasNext()) {
View candidateView = (View)var10.next();
if (StringUtils.hasText(candidateView.getContentType())) {
MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType());
if (mediaType.isCompatibleWith(candidateContentType)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Selected '" + mediaType + "' given " + requestedMediaTypes);
}
attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, 0);
return candidateView;
}
}
}
}
return null;
}
}
可得,也就是在候选的view中根据需求条件得到一个最合适的视图返回;
结论:ContentNegotiatingViewResolver这个视图解析器是组合了所有的视图解析器对象来实现视图解析功能;视图解析器的组合逻辑在ContentNegotiatingViewResolver类中的initServletContext方法中定义;
protected void initServletContext(ServletContext servletContext) {
Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.obtainApplicationContext(), ViewResolver.class).values();
ViewResolver viewResolver;
if (this.viewResolvers == null) {
this.viewResolvers = new ArrayList(matchingBeans.size());
Iterator var3 = matchingBeans.iterator();
while(var3.hasNext()) {
viewResolver = (ViewResolver)var3.next();
if (this != viewResolver) {
this.viewResolvers.add(viewResolver);
}
}
} else {
for(int i = 0; i < this.viewResolvers.size(); ++i) {
viewResolver = (ViewResolver)this.viewResolvers.get(i);
if (!matchingBeans.contains(viewResolver)) {
String name = viewResolver.getClass().getName() + i;
this.obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(viewResolver, name);
}
}
}
AnnotationAwareOrderComparator.sort(this.viewResolvers);
this.cnmFactoryBean.setServletContext(servletContext);
}
静态资源处理
静态资源:前端写好的固定页面
访问webjars中的静态资源
使用SpringBoot导入静态资源,需要使用webjars导入;当我们想要访问webjars中的静态资源时,WebMvcAutoConfigurationAdapter中的方法addResourceHandlers规定了访问该静态资源的规则:
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
if (!registry.hasMappingForPattern("/webjars/**")) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{"/webjars/**"}).addResourceLocations(new String[]{"classpath:/META-INF/resources/webjars/"}).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl).setUseLastModified(this.resourceProperties.getCache().isUseLastModified()));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl).setUseLastModified(this.resourceProperties.getCache().isUseLastModified()));
}
}
}
源码中指出:当我们访问所有路径为/webjars/**的静态资源时,都需要去classpath:/META-INF/resources/webjars/里找到对应的资源
访问自己编写的静态资源
当我们需要访问自己编写的静态资源时,访问的路径是什么?再看到上面addResourceHandlers方法中有一行代码:
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
这里通过getStaticPathPattern方法设置了静态资源的路径;查看WebMvcProperties类中这个方法的源码;
public String getStaticPathPattern() {
return this.staticPathPattern;
}
查看staticPathPattern这个属性在构造器中的初始化:
this.staticPathPattern = "/**";
可得,该类中构造器对staticPathPattern(静态资源路径)的初始化为"/**",访问目录下的任何资源;
而这个属性的定义会找WebProperties这个类中内部类Resources的getStaticLocations()方法;
public String[] getStaticLocations() {
return this.staticLocations;
}
Resources是WebProperties类中的一个静态内部类,其构造器对staticLocations的初始化值为一个常量;
public Resources() {
this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
该常量属性在Resources类中的定义为
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
至此,我们得知了静态资源自动配置的默认访问路径;可知:这里声明了访问静态资源时,查找静态资源的路径目录,即上面定义的静态String[]数组里的内容
"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"
因此,在这四个目录下存放的静态资源可以被识别和访问,我们可以在根目录resources下创建相应的文件夹来存放静态资源;
eg:访问http://localhost:8080/hello.js,就会在以上路径目录中查找相对应的文件
格式转换器
在WebMvcAutoConfiguration类中定义了格式转换的方法
@Bean
public FormattingConversionService mvcConversionService() {
Format format = this.mvcProperties.getFormat();
WebConversionService conversionService = new WebConversionService((new DateTimeFormatters()).dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime()));
this.addFormatters(conversionService);
return conversionService;
}
getXxx()方法就规定了相应的格式;
以上;
需要注意的是,spring中导入的web-mvc的jar包不同版本源码可能会有所不同。SpringBoot版本不同,mvc配置类中的源码也会有所差别。