MVC设计模式
有过一定开发经验的人肯定都知道这个模式,先简单介绍下这种模式,然后再去讨论为啥要这么设计:
传统的web应用中应该主要包括这些组件,不同组件负责不同的模块。
- 数据实体:POJO
- 数据层:DAO
- 业务层:Service
- 控制层:Servlet
- 表示层(页面层):JSP页面或HTML页面
Model:模型对象拥有最多的处理任务,是应用程序的主体部分,它负责数据逻辑(业务逻辑)的处理和实现数据库的操作。在项目中除了控制层的控制器,几乎每一个Bean组件都属于模型,比如Service层、DAO层,以及POJO实体类等。
View:负责格式化数据并把它们呈现给用户,包括数据展示、用户交互、数据验证、页面设计等功能。说白了就是离用户最近的、展示给人们看的,比如HTML或者JSP页面。
Controller:负责接收并转发请求,对请求处理之后拿到响应结果,指派要使用的视图(类似于指定Servlet跳转到不同的页面进行展示),将响应结果返回给客户端。对应的组件一般是Servlet,很少用JSP页面直接处理其他页面过来的请求。
那么为啥要使用这种模式呢?在计算机领域有一句非常著名的话,没有什么事情是加一层不能解决的,如果有,那就两层。包括著名的计算机网络协议模型,也是分为很多层,应用层、传输层、网络层等等,这么设计的目的就是划分每一个模块的职能边界,
划清职责。可以想象下公司组织结构,一家互联网公司通常有多个部门,技术部、人事部、销售部、运营部等等,每个部门都有自己的职责,自己的管理方式,不同的绩效考核制度,当技术部的管理政策发生变动,对于其他任何部门来说不会有影响,反正部门
与部门之间的沟通跟以前保持一致(例如邮件),这样也就实现了松耦合。
spring mvc体系结构
根据图示简单的解释一波:
- 1、所有的http请求都交给dispatcherservlet,这是spring mvc的核心servlet,功能很简单,做分发;
- 2、dispatcherservlet拿到请求之后,根据handlermapping的配置找到处理请求的处理器handler(就是对应的servlet);
- 3、找到handler之后,通过handlerAdapter对handler进行封装,再以统一的适配器接口调用handler;
- 4、处理器处理完之后返回一个modelAndView(包含逻辑视图名)给dispatcherServlet;
- 5、由viewResolver完成逻辑视图名到真实视图对象的解析工作;
- 6、dispatcherServlet使用这个view对象对modelAndView中的模型数据进行视图渲染;
- 7、最终客户端得到的响应消息,可能是html页面或xml等等。
DispatcherServlet
1、配置
<servlet> <servlet-name>springMVC</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 获取springmvc的配置xml文件 --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath*:/springMVC.xml</param-value> </init-param> <!-- 表示容器启动时初始化该servlet --> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springMVC</servlet-name> <!-- 表明对什么样的url做拦截 --> <url-pattern>/</url-pattern> </servlet-mapping>
关于<url-pattern>/</url-pattern>有以下注意点:
a、*.xxx
比如配置的是*.do,那么将会拦截所有的.do请求。
b、/
覆盖容器的default servlet,凡是在web.xml文件中找不到匹配的URL,它们的访问请求都将交给该Servlet处理(静态资源也将会拦截). 所以web.xml没有配置其他特殊路径的servlet, 基本上所有的请求都交由DispatcherServlet处理。
c、/*
这种是错误配置,举例说明:
@Controller @RequestMapping("") public class Test { @RequestMapping("/test") public String test() { return "test"; } }
报错信息:
DispatcherServlet with name ‘taotao-sso’ processing GET request for [/test] RequestMappingHandlerMapping]-[DEBUG] Looking up handler method for path /test RequestMappingHandlerMapping]-[DEBUG] Returning handler method [public java.lang.String com.taotao.sso.controller.Test.test()] DispatcherServlet]-[DEBUG] Rendering view [org.springframework.web.servlet.view.InternalResourceView: name ‘test’; URL [/WEB-INF/jsp/test.jsp]] in DispatcherServlet with name ‘taotao-sso’ InternalResourceView]-[DEBUG] Forwarding to resource [/WEB-INF/jsp/test.jsp] in InternalResourceView ‘test’ DispatcherServlet]-[DEBUG] DispatcherServlet with name ‘taotao-sso’ processing GET request for [/WEB-INF/jsp/test.jsp] RequestMappingHandlerMapping]-[DEBUG] Looking up handler method for path /WEB-INF/jsp/test.jsp RequestMappingHandlerMapping]-[DEBUG] Did not find handler method for [/WEB-INF/jsp/test.jsp] PageNotFound]-[WARN] No mapping found for HTTP request with URI [/WEB-INF/jsp/test.jsp] in DispatcherServlet with name ‘taotao-sso’ DispatcherServlet]-[DEBUG] Successfully completed request
从日志可以清晰的看出, springmvc可以找到请求/test的handler, 之后springmvc转发请求’/WEB-INF/jsp/test.jsp’, 同样被DispatcherServlet处理, 之后就发出了/WEB-INF/jsp/test.jsp这样的转发请求, 自然会找不到handler, No mapping. 所以这样的请求会一直为404.
那么为什么配置成“/”就没有问题呢? -----以tomcat容器为例说明
“/”的意思是当前请求没有对应的servlet处理,就会交给此servlet处理,但是tomcat对于.jsp这种请求有做处理,首先找到你本地的tomcat配置文件,路径为:%TOMCAT_HOME%/conf/web.xml,内容如下:
<servlet> <servlet-name>jsp</servlet-name> <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class> <init-param> <param-name>fork</param-name> <param-value>false</param-value> </init-param> <init-param> <param-name>xpoweredBy</param-name> <param-value>false</param-value> </init-param> <load-on-startup>3</load-on-startup> </servlet> <servlet-mapping> <servlet-name>jsp</servlet-name> <url-pattern>*.jsp</url-pattern> <url-pattern>*.jspx</url-pattern> </servlet-mapping>
也就是说tomcat中定义了JspServlet,用于处理*.jsp、*.jspx结尾的url。所以<url-pattern>配置推荐*.xxx或/模式。
2、DispatcherServlet内部逻辑
在WebApplicationContext初始化之后,自动执行DispatcherServlet的initStrategies(),方法如下:
protected void initStrategies(ApplicationContext context) { initMultipartResolver(context);//文件上传解析,如果请求类型是multipart将通过MultipartResolver进行文件上传解析 initLocaleResolver(context);//本地化解析 initThemeResolver(context);//主题解析 initHandlerMappings(context);//通过HandlerMapping,将请求映射到处理器 initHandlerAdapters(context);//通过HandlerAdapter支持多种类型的处理器 initHandlerExceptionResolvers(context);//如果执行过程中遇到异常,将交给HandlerExceptionResolver来解析 initRequestToViewNameTranslator(context);//直接解析请求到视图名 initViewResolvers(context);//通过viewResolver解析逻辑视图到具体视图实现 initFlashMapManager(context);//flash映射管理器 }
初始化的这些组件主要包括:
相关注解
1、@RequestMapping
功能:将请求映射到控制器中的方法。
示例:
当然也可以在url中使用占位符的方式;
使用@PathVariable注解可以获得url中的userId。
来看下常见的参数传输方式:
2、@RequestParam
由于类反射对象默认不记录方法入参的名称,所以需要使用@RequestParam注解指定其对应的请求参数。
该注解包含3个参数:
required默认为true,如果该传参非必需,则可以加上required=false。
3、@CookieValue
将请求中的cookie的某个值绑定到处理方法中的某个入参上。
如果cookie中包含sessionId的值,那么直接从请求中获取这个值,并赋值到sessionId。并且由于required=false,因此请求中没有这个值,也不会报错。
4、@RequestHeader
将请求头中的某个属性值绑定到处理方法中的某个入参中。
5、pojo自动填充
6、使用servlet相关对象-----HttpServletRequest、HttpServletResponse
7、@RequestBody、@ResponseBody
相信很多开发者在平时开发接口(controller)的过程中,使用的比较多的就是map、json、或是dto等等,那么springmvc是如何处理这些格式呢?其实是用到了HttpMessageConverter。
先看一个示例,来了解这两个注解的使用方法:
实现原理:
在handle41方法中,springmvc根据requestbody的类型查找匹配到的HttpMessageConverter,例如此时是String的入参,因此找到的就是StringHttpMessageConverter,将请求信息转换成String,并绑定到@RequestBody入参上。
在handle42方法中,由于方法返回类型为byute[],由于此方法使用@ResponseBody注解,因此查找匹配到的ByteArrayHttpMessageConverter,将返回结果转换成byte[],并输出到客户端。
8、@RestController、@AsyncRestTemplate
a、@RestController
这是此注解的源码,这就很清晰了:@RestController = @Controller + @ResponseBody
b、@AsyncRestTemplate
根据此注解的名字很好理解,异步调用,示例如下:
此方法中开启了一个线程,也就是第一次调用的时候只会返回“=====hello”。
AsyncRestTemplate默认使用的是SimpleClientHttpRequestFactory进行http操作,更底层则是通过java.net.HttpURLConnection实现。
处理模型数据
对于spring mvc来说,返回的通常是M和V,M是数据,V则是展示数据的视图,主要涉及到的注解如下:
1、ModelAndView
该对象既包含model。也包含v,可以将模型数据看成是一个Map<String, Object>对象,主要方法如下:
2、@ModelAttribute
该注解的主要功能是将入参或出参中的对象添加到模型中,请看如下示例:
首先,spring mvc会将请求消息绑定到User对象中,然后以user为key,User为value,将这个对象存放到模型中。在准备对视图进行渲染之前,spring mvc会进一步的将模型中的数据转储到视图的上下文中并暴露给视图对象。
上面一种用法是将入参中的数据放到模型中,下面来看看将出参放到模型中:
处理方法的数据绑定
前面介绍了各种spring mvc将请求参数绑定到处理方法的入参上,那么它是怎么实现的呢?
1、数据绑定流程
- a、将ServletRequest对象以及处理方法的入参对象实例传递给DataBinder;
- b、DataBinder调用ConversionService转换组件进行数据类型转换、数据格式化;
- c、调用Validator组件对已经绑定了请求信息的数据的入参对象进行校验;
- d、最终生成BindResult对象,包含已完成数据绑定入参对象,还包含相应的校验错误对象。
视图解析
1、jsp和jstl
a、InternalResourceViewResolver
jsp在以前使用还是蛮多的,但是它也有它的劣势,因为jsp本质上是servlet,属于后端的东西,随着现在都流行前后分离,导致jsp没有那么受欢迎。视图解析器配置如下:
2、模板视图
FreeMarker和Velocity是使用非常多的页面模板技术。一般使用过程中,FreeMarker仅提供页面模板的功能,模型数据则是由java提供。
示例如下:
FreeMarkerConfigurer中定义了模板存放的路径、模板文件编码格式以及freeMarker的统一设置。classic_compatible为true是为了防止碰到null时,模板报错的问题。
FreeMarkerViewResolver中分别定义了视图解析器的优先级(优先于InternalResourceViewResolver)、视图后缀以及编码等等。
文件上传
1、配置MultipartResolver
注:必须先将Jakarta Commons FileUpload、Jakarta Commons io的jar包添加到类路径下。
页面:
controller:
MultipartFile主要方法:
注:负责上传文件的表单和一般表单有一些区别,表单的编码类型必须是multipart/form-data