Spring MVC
1 SpringMCV基本说明
- 简介:spring为展现层提供的基于MVC设计理念的优秀Web框架
- SpringMVC是一个容器,用来管理对象的,使用IOC核心技术。springmvc管理界面层中的控制器对象
- SpringMVC底层也是servlet,以servlet为核心,接收请求,处理请求,显示处理结果给用户
2 SpringMVC中的核心Servlet – DispatcherServlet
- DispatcherServlet是框架中的一个Servlet对象,负责接收请求,相应处理结果。它的父类是HttpServlet。
- DispatcherServlet也叫前端控制器(front controller)
- SpringMVC是管理控制器对象,原来没有SpringMVC之前使用Servlet作为控制器对象使用,现在通过SpringMVC容器创建一种叫做控制器的对象,代替Servlet行使控制器的角色,功能。
3 SpringMVC的使用方式
3.1 配置文件
web.xml:
<servlet>
<servlet-name>myweb</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>myweb</servlet-name>
<!--
url-pattern作用:把一些请求交给指定的servlet处理,这里定义的就是那些请求需要使用中央调度器来处理
1.使用拓展名的方式,格式*.xxx,xxx是自定义的拓展名,如*.do,*.action,*.mvc等等
2.使用斜杠"/"
"/":拦截除静态资源外的所有请求
"/*":拦截所有请求,包括静态资源(一般不这么写)
-->
<url-pattern>/</url-patter>
</servlet-mapping>
springmvc.xml:
<!--声明组件扫描器-->
<context:conponet-scan base-package="com.huhx.controller" />
3.2 创建后端控制器(Controller)
创建一个普通类,当做servelet使用,在类的上方加入@Controller注解
/**
* @Controller:创建控制器(处理器)对象
* 控制器:叫做后端控制器,自定义的类处理请求的。
* 位置:在类的上面,表示创建此类的对象,对象放在springmvc的容器中
*/
@Controller
public class Mycontroller {
//定义方法,处理请求,public void doGet(){}
/**
* springmvc框架,使用控制器类中的方法处理请求。
* 方法特点:
* 1.方法的形参,表示请求中的参数
* 2.方法的返回值,表示本次请求的处理请求
*/
/**
* @RequestMapping:请求映射
* 属性:value 请求中的uri地址,唯一值,以“/”开头
* 位置:1、在方法的上面(必须)2.在类定义的上面(可选)
* 作用把指定的请求,交给指定的方法来处理,等同于url-pattern
* 返回的ModelAndView:表示本次请求的处理结果(数据和视图)
* Model:表示数据
* View:表示视图
*/
@RequestMapping("/test")
public ModelAndView doSome(){
//使用这个方法处理请求,能处理请求的方法叫做控制器方法,可以有参数,也可以有返回值
ModelAndView mv = new ModelAndView();
mv.addObject("msg", "处理了请求");
mv.addObject("fun", "执行了方法");
//指定视图,setViewName("视图的完整路径")
mv.setViewName("/index.jsp");
return mv;
}
//上述方法可以直接简化成下面这种这种形式
@RequestMapping("/test")
public String doSome(){
return "/index.jsp";
}
/**
* 当框架调用完doSome()方法后,得到返回的ModelAndView。
* 框架会在后续的处理逻辑中,处理mv对象里面的数据和视图
* 对数据执行request.setAttribute("msg","处理了请求"),把数据放入了request的作用域中
* 对视图执行了forward转发操作,等同于request.getRequestDispatcher("index.jsp").forward(..)
*/
}
3.3 SpringMVC请求的处理方式
简单处理过程:
- 用户发起请求/xxx——>Tomcat接收请求---->将请求交给中央处理器DispaterServlet------->将请求交给后端控制器MyController(doSome()方法处理了请求)
3.4 使用视图解析器
当配置了视图解析器之后,我们可以直接将文件名作为视图名来使用,叫做视图的逻辑名称
使用方式:在springmvc.xml文件中配置视图解析器
<bean class="org.springframework.web.servlet.view.InternalResourceView">
<property name="prefix" value="/WEB-INF/view"/>
<property name="suffix" value=".jsp" />
</bean>
在doSome()方法中,可以在设置视图的时候直接使用如下代码:
mv.setViewName("index");
4 路径映射与请求参数携带
4.1 @RequestMapping
4.1.1 指定模块名称
- 在方法上标注:指定独有的访问路径
- 在类上标注:指定一类访问路径,类中所有方法的请求路径都是以该前缀开头的,不用在方法上重复声明
4.1.2 其他属性
-
method:限定请求方式
method = RequestMethod.POST; 表示只接收post请求,如果请求类型不对就会报错
-
params:限定请求参数,支持简单的表达式
params={“username”}:发起请求时必须带上一个username的参数,如果没带就会报错(404)
params={“!username”}:发起请求时必须不带上username的参数,如果携带就会报错(404)
params={“username=123”}:发起请求时username参数的值必须是123
params={“username=123”, “pwd”, “!age”}:发起请求时三个参数要求必须全部满足才能访问成功
-
headers:限定请求头,同样支持简单的表达式
-
consumes:只接收内容类型是哪种的请求,规定请求头中的Content-Type
-
produces:告诉浏览器返回的内容类型是什么,给响应头中加上Content-Type
4.1.3 RequestMapping的模糊匹配功能
URL地址可以写模糊的通配符(?、*、**)
?:能替代任意一个字符
*:能替代多个字符和一层路径
**:能替代多层路径
@RequestMapping("/test01"):只能处理/test01请求
@RequestMapping("/test0?"):能处理/test02、/test0a等等请求,存在多个匹配路径情况下,精确路径优先
@RequestMapping("/test0*"):能处理/test0、/test000等等请求
@RequestMapping("/**/test01"):能处理/a/test01、/a/abc/test01等等请求
4.2 @PathVariable
可以与@RequestMapping一同使用,@RequestMapping给定的路径上可以存在占位符,占位符的语法就是可以在任意路径的地方写一个{变量名}
@RequestMapping("/user/{id}")
public String hello(@PathVariable("id")String id) {
System.out.println(id);
return "success";
}
4.3 Restful风格URL
REST:即Representational State Transfer。(资源)表现层状态转化。是目前最流行的一种互联网软件架构。
资源:是网络上的一个实体,或者说是网络上的一个具体信息,可以是一段文本、一张图片、一种服务,总之是一个具体的存在,可以利用一个URI指向它,每种资源对应一个特定的URI,因此URI即为每一个资源的独一无二的识别符
表现层:把资源具体呈现出来的形式,叫做它的表现层,例如文本可以用txt格式表示,可以用JSON格式表示等
状态转化:每发送一个请求,就代表了客户端和服务器的一次交互过程,HTTP协议,是一种无状态协议,即所有的状态都保存在服务器端,因此如果客户端想要操作服务器,就必须通过某种手段,让服务器端发生“状态转化”。而这种转化是建立在表现层之上的。具体来说,就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源、PUT用来更新资源、DELETE用来删除资源
Restful风格的URL地址约束希望以非常简洁的URL地址来发送请求,怎样表示对一个资源的增删改查使用请求的方式来区分。
Rest风格的url地址的写法:/资源名/资源标识符
相同的url,发起不同的请求来表示对资源的不同操作:
/book/1 GET请求获取1号图书
/book/1 PUT请求更新1号图书
/book/1 DELETE请求删除1号图书
/book POST请求添加图书
问题1:如何从页面发起DELETE和PUT请求?
1、SpringMVC中有一个Filter,它可以把普通的请求转化为规定形式的请求,我们需要在web.xml中配置这个filter
<filter>
<filter-name>HiddenHttpMethodFilter</filter-name>
<fiter-class>org.springframework.web.filter.HiddenHttpMethodFilter</fiter-class>
</filter>
<filter-mapping>
<filter-name>HiddenHttpMethodFilter</filter-name>
<url-patter>/*</url-patter>
</filter-mapping>
2、在页面按照以下要求发起一个请求
- 创建一个post类型的表单
- 表单项中携带一个_method的参数,将其value设置为“put”或者“delete”
<form action="book/1" method="post">
<input name="_method" value="put" />
<input type="submit" value="更新一号图书" />
</form>
问题2:高版本的Tomcat不接收DELETE和PUT类型的请求
4.4 @RequesParam、@RequestHeader、@CookieValue(请求参数获取)
springmvc获取请求参数的默认方式:
直接给方法入参上写一个和请求参数名相同的变量。这个变量就来接收请求参数的值
@RequesParam:用于获取请求参数:
value:指定要获取的参数的key
required:这个参数是否是必须的,如果是true,没传入该参数时会报错,如果是false,则为默认值
defaultValue:默认值。没带默认值是null
@RequestMapping("/test")
public String hello(@RequesParam("user")String username) {
System.out.println(username);
return "success";
}
@RequestHeader:获取请求头中某个key的值
value:指定要获取的参数的key
required:这个参数是否是必须的,如果是true,没传入该参数时会报错,如果是false,则为默认值
defaultValue:默认值。没带默认值是null
@RequestMapping("/test")
public String hello(@RequesParam("user")String username, @RequestHeader("User-Agent")String userAgent) {
System.out.println(username);
System.out.println(userAgent);
return "success";
}
@CookieValue:获取某个cookie的值
value:指定要获取的参数的key
required:这个参数是否是必须的,如果是true,没传入该参数时会报错,如果是false,则为默认值
defaultValue:默认值。没带默认值是null
@RequestMapping("/test")
public String hello(@RequesParam("user")String username, @RequestHeader("User-Agent")String userAgent, @CookieValue("JSESSIONID")String jid) {
System.out.println(username);
System.out.println(userAgent);
System.out.println(jid);
return "success";
}
传入POJO类-springmvc可以自动封装:
- 将POJO中的每一个属性,从request参数中长时获取出来,并封装
- 还可以级联封装:对象中包含其他对象,包含的对象的属性也可以封装
- 请求参数的参数名要跟对象中的属性名称一一对应
public class Book {
private String name;
private String author;
private Double price;
}
@RequestMapping("/book")
public String hello(Book book) {
System.out.println(book);
return "success";
}
5 响应数据的返回
- Model:
- ModelAndView:
- Map
- 原生API,HttpResponse,HttpRequest,HttpSession
6 SpringMVC源码
6.1 DispatcherServlet结构
6.2 DispatcherServlet工作流程
-
所有请求过来DispatcherServlet收到请求
-
调用DispatcherServlet中的doDispatch()方法进行处理
- getHandler():根据当前请求地址找到能处理这个请求的目标处理器类(controller)
- getHandlerAdapter():根据当前处理器类获取到能执行这个处理器方法的适配器
- 使用刚才获取到的额适配器(AnnotationMethodHandlerAdapter)执行目标方法
- 目标方法执行后会返回一个ModelAndView对象
- 根据ModelAndView的信息转发到具体的页面,并可以在请求域中取出ModelAndView中的模型数据
-
getHandler()的细节:如何根据当前请求就能找到哪个controller能来处理
-
HandlerMapping:处理器的映射(运行时实际得到的是RequestMappingHandlerMapping),里边保存了每一个请求其对应的处理器,其中包含有mappingRegistry属性,里边保存有请求路径与Controller的映射关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7skkqaEK-1620823776393)(C:\Users\HHX\Desktop\1620733480625.png)]
-
在spring容器启动创建Controller对象的时候会扫描每个处理器上的@RequestMapping注解,并将其中携带的路径信息保存在handlerMap的属性中,就形成了Controller的映射
-
-
getHandlerAdapter()的细节:如何找到目标处理器类的适配器。要拿适配器去执行目标方法
- HandlerAdapter:处理器适配器,其具体的实现类如下图所示:
- HandlerAdapter:处理器适配器,其具体的实现类如下图所示:
-
探究HandlerMapping与HandlerAdapter的实现类是怎么拿到的:DispatcherServlet中的引用属(SpringMVC的九大组件)
@Nullable private MultipartResolver multipartResolver; //文件上传相关的解析器 /** LocaleResolver used by this servlet. */ @Nullable private LocaleResolver localeResolver; //区域信息解析器,国际化相关 /** ThemeResolver used by this servlet. */ @Nullable private ThemeResolver themeResolver; //主题效果更换 /** List of HandlerMappings used by this servlet. */ @Nullable private List<HandlerMapping> handlerMappings; //处理器映射器 /** List of HandlerAdapters used by this servlet. */ @Nullable private List<HandlerAdapter> handlerAdapters; //处理器适配器 /** List of HandlerExceptionResolvers used by this servlet. */ @Nullable private List<HandlerExceptionResolver> handlerExceptionResolvers; //异常处理解析器 /** RequestToViewNameTranslator used by this servlet. */ @Nullable private RequestToViewNameTranslator viewNameTranslator; /** FlashMapManager used by this servlet. */ @Nullable private FlashMapManager flashMapManager; /** List of ViewResolvers used by this servlet. */ @Nullable private List<ViewResolver> viewResolvers;
-
DispatcherServlet中九大组件初始化的地方:onRefresh()方法,该方法在spring容器初始化时是留给子类重写的,因此在容器启动时,该方法会执行,完成九大组件的初始化
protected void onRefresh(ApplicationContext context) { initStrategies(context); } protected void initStrategies(ApplicationContext context) { initMultipartResolver(context); initLocaleResolver(context); initThemeResolver(context); initHandlerMappings(context); initHandlerAdapters(context); initHandlerExceptionResolvers(context); initRequestToViewNameTranslator(context); initViewResolvers(context); initFlashMapManager(context); } private void initLocaleResolver(ApplicationContext context) { try { this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class); if (logger.isTraceEnabled()) { logger.trace("Detected " + this.localeResolver); } else if (logger.isDebugEnabled()) { logger.debug("Detected " + this.localeResolver.getClass().getSimpleName()); } } catch (NoSuchBeanDefinitionException ex) { // We need to use the default. this.localeResolver = getDefaultStrategy(context, LocaleResolver.class); if (logger.isTraceEnabled()) { logger.trace("No LocaleResolver '" + LOCALE_RESOLVER_BEAN_NAME + "': using default [" + this.localeResolver.getClass().getSimpleName() + "]"); } } }
组件的初始化,就是去容器中找这个组件,如果没有找到就用默认的配置。组件初始化完成之后,容器中就存在了相应组件的实现类了。
-
探究处理器的目标方法如何执行
执行handler中的方法是通过适配器来反射执行的,在doDispatch方法中的具体语句为:
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
其中ha就是上边我们获取到的HandlerMappingAdapter,进入handle方法,最终会来到
7 视图与视图解析器
7.1 forward前缀转发
@RequestMapping("/test")
public String handle() {
return "forward:/hello.jsp"; //一定要加“/”,不加是相对路径,容易出问题
}
7.2 redirect前缀指定重定向
@RequestMapping("/test")
public String handle() {
return "redirect:/hello.jsp";
}
【注】:带有前缀的返回值(forward操作与redirect操作)都不会触发视图解析器的拼串操作
7.3 视图解析的流程
- 任何方法的返回值都会被包装成ModelAndView对象
8 SpringMVC返回json数据
使用@ResponseBody注解
@RequestMapping("/test")
@ResponseBody
public String handle() {
return "success"; //一定要加“/”,不加是相对路径,容易出问题
}
【注】如果返回值为对象或者复杂对象,SpringMVC内部会通过Jackson包将复杂对象序列化为json格式的数据,然后返回。
9 SpringMVC拦截器
SrpingMVC提供了拦截器机制,允许运行目标方法之前进行一些拦截工作,或者目标方法运行之后进行一些其他处理,SpringMVC中的拦截器是一个接口:HandlerInterceptor
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3U4zchO0-1620823776400)(C:\Users\HHX\Desktop\1620787434154.png)]
- preHandle:在目标方法运行之前调用,返回boolean,返回为true时放行,返回为false时不放行
- postHandle:在目标方法运行之后调用
- afterCompletion:在请求整个完成之后,来到目标页面之后,放行或者拒绝请求
9.1 单拦截器运行流程
-
定义自己的拦截器类,实现HandlerInterceptor接口,并实现其中的方法
-
在Spring容器中注册自己的拦截器类
<mvc:interceptors> <!--配置一个拦截器,拦截所有请求--> <bean class="自己定义的拦截器类" /> <mvc:interceptor> <!--配置一个拦截器,只拦截/test请求--> <mvc:mapping path="/test"></mvc:mapping> <bean class="自己定义的拦截器类" /> </mvc:interceptor> </mvc:interceptors>
-
拦截器的正常运行流程:
拦截器的preHandle->目标方法->postHandle->页面->afterCompletion
只要preHandle不放行就没有以后的流程
只要放行了,afterCompletion就会执行
9.2 多拦截器运行流程
拦截器的preHandle是按照顺序执行的
拦截器的postHandle是按照逆序执行的
拦截器的afterCompletion是按照逆序执行的
已经放行的拦截器的afterCompletion总会执行
9.3 拦截器源码分析
10 SpringMVC异常处理
SpringMVC通过HandlerExceptionResolver来处理程序,在页面渲染之前(获取View对象之前),如果出现异常,SpringMVC会使用HandlerExceptionResolver对异常进行处理,如果所有的HandlerExceptionResolver都无法处理异常,则将异常抛给Tomcat
-
ExceptionHandlerExceptionResolver:与@ExceptionHandler注解联系使用
//告诉SpringMVC下面这个方法专门用来处理xxxException异常 //1.可以给方法上添加一个Exception参数,用来接收发生的异常 //2.直接返回ModelAndView也可以携带异常信息 @ExceptionHandler(value = {xxxException.class, xxException.class, ...}) public String handleException(Exception ex) { //视图解析器拼串,来到我们自己定义的错误页面 return "myerror"; } @ExceptionHandler(value = {xxxException.class, xxException.class, ...}) public String handleException(Exception ex) { //直接返回ModelAndView来携带异常信息 ModelAndView mv = new ModelAndView("myerror"); mv.addObject("ex", ex); return mv; }
@ExceptionHandler全局异常处理:@ControllerAdvice注解
- 自定义全局异常处理类,一般新建一个Exception包,在该包下新建类
- 在这个类上加上@ControllerAdvice注解
- 在该类中编写方法,方法上加注@ExceptionHandler注解
@ControllerAdvice public class MyExceptionHandler { @ExceptionHandler(value = {xxxException.class, xxException.class, ...}) public String handleException(Exception ex) { //视图解析器拼串,来到我们自己定义的错误页面 return "myerror"; } }
-
ResponseStatusExceptionResolver:与@ResponseStatus注解联系使用
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR,reason = "not an error , just info") public class MyException extends RuntimeException { public MyException() { } public MyException(String message) { super(message); } }
这样子,在SpringMvc中如果有某个@RequestMapping方法抛出该异常, 只要开启mvc:annotation-driven/,异常自动展示的界面都是如下的:
-
DefaultHandlerExceptionResolver:如果前两个异常处理解析器都没有工作的情况下,SpringMVC会默认使用该异常处理解析器来解析发生的异常,即跳转至Spring自己的错误页面
SpringMVC中自己定义的异常有以下几种,可以直接在源代码中找到
protected ModelAndView doResolveException( HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) { try { if (ex instanceof HttpRequestMethodNotSupportedException) { return handleHttpRequestMethodNotSupported( (HttpRequestMethodNotSupportedException) ex, request, response, handler); } else if (ex instanceof HttpMediaTypeNotSupportedException) { return handleHttpMediaTypeNotSupported( (HttpMediaTypeNotSupportedException) ex, request, response, handler); } else if (ex instanceof HttpMediaTypeNotAcceptableException) { return handleHttpMediaTypeNotAcceptable( (HttpMediaTypeNotAcceptableException) ex, request, response, handler); } else if (ex instanceof MissingPathVariableException) { return handleMissingPathVariable( (MissingPathVariableException) ex, request, response, handler); } else if (ex instanceof MissingServletRequestParameterException) { return handleMissingServletRequestParameter( (MissingServletRequestParameterException) ex, request, response, handler); } else if (ex instanceof ServletRequestBindingException) { return handleServletRequestBindingException( (ServletRequestBindingException) ex, request, response, handler); } else if (ex instanceof ConversionNotSupportedException) { return handleConversionNotSupported( (ConversionNotSupportedException) ex, request, response, handler); } else if (ex instanceof TypeMismatchException) { return handleTypeMismatch( (TypeMismatchException) ex, request, response, handler); } else if (ex instanceof HttpMessageNotReadableException) { return handleHttpMessageNotReadable( (HttpMessageNotReadableException) ex, request, response, handler); } else if (ex instanceof HttpMessageNotWritableException) { return handleHttpMessageNotWritable( (HttpMessageNotWritableException) ex, request, response, handler); } else if (ex instanceof MethodArgumentNotValidException) { return handleMethodArgumentNotValidException( (MethodArgumentNotValidException) ex, request, response, handler); } else if (ex instanceof MissingServletRequestPartException) { return handleMissingServletRequestPartException( (MissingServletRequestPartException) ex, request, response, handler); } else if (ex instanceof BindException) { return handleBindException((BindException) ex, request, response, handler); } else if (ex instanceof NoHandlerFoundException) { return handleNoHandlerFoundException( (NoHandlerFoundException) ex, request, response, handler); } else if (ex instanceof AsyncRequestTimeoutException) { return handleAsyncRequestTimeoutException( (AsyncRequestTimeoutException) ex, request, response, handler); } } catch (Exception handlerEx) { if (logger.isWarnEnabled()) { logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", handlerEx); } } return null; }
点开其中一个异常的处理函数,可以看到就是设置了一个响应头并重定向到一个错误页面,并给页面设置了一些信息。