Loading

25-SpringMVC-1(请求相关)

1. 概述

  • SpringMVC 是 Spring 的 Web 模块。
  • Spring 为展现层提供的基于 MVC 设计理念的优秀的 Web 框架,是目前最主流的 MVC 框架之一。
  • Spring3.0 后全面超越 Struts2,成为最优秀的 MVC 框架。
  • Spring MVC 通过一套 MVC 注解,让 POJO(Plain Old Java Object) 成为处理请求的控制器,而无须实现任何接口。
  • 支持 REST 风格的 URL 请求。
  • 采用了松散耦合可插拔组件结构,比其他 MVC 框架更具扩展性和灵活性。

2. HelloWorld

2.1 步骤

  • 创建 Web 工程
  • 导包
    • SpringMVC 是 Spring 的 Web 模块;所有模块的运行都是依赖核心模块[IOC 模块]
      commons-logging-1.1.3.jar // 日志
      spring-aop-4.0.0.RELEASE.jar // 支持注解
      // 核心 ↓
      spring-beans-4.0.0.RELEASE.jar
      spring-context-4.0.0.RELEASE.jar
      spring-core-4.0.0.RELEASE.jar
      spring-expression-4.0.0.RELEASE.jar
      
    • Web 模块
      spring-web-4.0.0.RELEASE.jar
      spring-webmvc-4.0.0.RELEASE.jar
      
  • 写配置文件
    • web.xml
      <!--
          SpringMVC 思想:有一个前端控制器能拦截所有请求,并智能派发
          这个前端控制器是一个 Servlet,应该在 web.xml 中配置这个 Servlet 来拦截所有请求
       -->
      
      <!-- The front controller of this Spring Web application,
              responsible for handling all application requests -->
      <servlet>
          <servlet-name>springDispatcherServlet</servlet-name>
          <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
          <init-param>
              <!-- contextConfigLocation 指定 SpringMVC 配置文件的位置 -->
              <param-name>contextConfigLocation</param-name>
              <param-value>classpath:springMVC.xml</param-value>
          </init-param>
          <!-- 
              Servlet 启动加载,原本是在第 1 次访问创建对象。设置该子标签后,
              Servlet 会在 Server 启动时创建对象。值越小,优先级越高,越先创建对象
           -->
          <load-on-startup>1</load-on-startup>
      </servlet>
      
      <!-- Map all requests to the DispatcherServlet for handling -->
      <servlet-mapping>
          <servlet-name>springDispatcherServlet</servlet-name>
          <!-- 
              '/*' 和 '/' 都是拦截所有请求。
              但是 '/*' 范围更广,会拦截到 *.jsp 页面,一旦拦截,jsp 页面就无法显示。
              '/' 也会拦截所有请求,但不会拦截 jsp。
           -->
          <url-pattern>/</url-pattern>
      </servlet-mapping>
      
    • springMVC.xml (创建 Spring Bean Configuration File,即 Web 层的 Spring 容器)
      <!-- 扫描组件 -->
      <context:component-scan base-package="cn.edu.nuist"></context:component-scan>
      
      <!-- 配置一个视图解析器,能帮我们拼接页面地址 -->
      <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
          <property name="prefix" value="/WEB-INF/pages/"></property>
          <property name="suffix" value=".jsp"></property>
      </bean>
      
  • 编写测试代码
    • index.jsp
      <a href="hello">HelloWorld!</a>
      
    • success.jsp
      <body>SUCCESS!</body>
      
    • MyFirstController
      @Controller // 告诉 SpringMVC 这是一个处理器,可以处理请求
      public class MyFirstController {
          /*
           * '/' 代表从当前项目下开始,处理当前项目下的 hello 请求
           */
          @RequestMapping("/hello")
          public String myFirstRequest() {
              System.out.println("请求收到了,正在处理中...");
              /*
               * [视图解析器] 会自动拼串:
               * 	<property name="prefix" value="/WEB-INF/pages/">
               * 	<property name="suffix" value=".jsp">
               * → /WEB-INF/pages/[方法返回值].jsp
               */
              // return "/WEB-INF/pages/success.jsp";
              return "success";
          }
      }
      

2.2 细节

2.2.1 运行流程

  1. 客户端点击超链接会发送 http://localhost:8080/21_SpringMVC/hello 请求
  2. 来到 tomcat 服务器,SpringMVC 的「前端控制器 DispatcherServlet」收到请求
  3. 查看请求地址和 @RequestMapping 标注的哪个匹配,来找使用哪个类的哪个方法来处理请求
  4. 前端控制器找到了 {目标处理器类&目标处理方法},直接利用反射执行目标方法
  5. 方法执行完成以后会有一个返回值,SpringMVC 认为这个返回值就是要去的页面地址
  6. 拿到方法返回值后,用「视图解析器」拼串得到完整的页面地址
  7. 拿到页面地址后,前端控制器帮我们转发到指定页面

2.2.2 @RequestMapping 简述

告诉 SpringMVC 这个方法用来处理什么请求。
@RequestMapping("/hello") 这个 '/' 可以省略,即使省略,也是默认从当前项目下开始。不过习惯加上。

2.2.3 配置文件的默认位置

可以看出,如果不指定配置文件位置,SpringMVC 默认会去找如下文件:

所以,若不想指定配置文件位置,就在 web 应用的 WEB-INF 目录下,指定配置文件名为 <servlet-name>-servlet.xml 即可。

2.2.4 核心控制器的 url-pattern

Tomcat Server 的 web.xml 中有一个 DefaultServlet,其 url-pattern 的配置是 /,The default servlet for all web applications, that serves static resources. → DefaultServlet 就负责找到并返回静态资源。

「前端控制器」的 url-pattern 也配置为了 /,这就相当于把 DefaultServlet 禁用了。此时,若再去访问一个静态资源(除去 Jsp 和 Servlet 外,其余都是静态资源)时,就会让「前端控制器」来处理该请求,而「前端控制器」就会去比较哪个方法的 @RequestMapping 与请求对应,肯定找不到,所以最终返回 404。

为什么 Jsp 能访问?

  • 服务器的 web.xml 中还有一个 JspServlet,其 url-pattern 配置为 *.jsp
    The JSP page compiler and execution servlet, which is the mechanism
    used by Tomcat to support JSP pages. Traditionally, this servlet is
    mapped to the URL pattern "*.jsp".
    
  • 因为我们项目自身 web.xml 中并没有覆盖服务器的 JspServlet 的配置;所以,JspServlet 能正常处理 Jsp 请求。
  • 若把「前端控制器」的 url-pattern 配置为了 /*,这时,连 Jsp 也访问不到了。
  • 我们把项目的 url-pattern 配置为 / 也是为了迎合后来的 REST 风格的 URL 地址。

@RequestMapping

Spring MVC 使用 @RequestMapping 注解为控制器指定可以处理哪些 URL 请求。

我们把项目的 url-pattern 配置为 / 也是为了迎合后来的 REST 风格的 URL 地址。

3. @RequestMapping

Spring MVC 使用 @RequestMapping 注解为控制器指定可以处理哪些 URL 请求。

3.1 注解标注在类上

在控制器的类定义及方法定义处都可标注 @RequestMapping。

  • 类定义处:提供初步的请求映射信息。相对于 WEB 应用的根目录
  • 方法处:提供进一步的细分映射信息。相对于类定义处的 URL。若类定义处未标注 @RequestMapping,则方法处标记的 URL 相对于 WEB 应用的根目录

3.2 注解中的属性

标准的 HTTP 请求报头:

属性之间是的关系,联合使用多个条件可让请求映射更加精确化。

3.2.1 映射请求路径

DispatcherServlet 截获请求后,就通过将请求 URL 与控制器上 @RequestMapping 注解的 value 属性值进行比对以此确定请求所对应的处理方法。

3.2.2 映射请求方法

public enum RequestMethod {
    GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
}

若不是按规定的方式请求:405 - Request method 'GET' not supported

3.2.3 映射请求参数

params 规定请求携带的请求参数,支持简单的表达式 (不按要求的直接 404),参数规则间用 , 隔开。

params={"username"}        请求必须携带名为 username 的参数
params={"!username"}       请求不能携带名为 username 的参数
params={"username=admin"}  请求必须携带名为 username 且值为 admin 的参数
params={"username!=admin"} 请求必须携带名为 username 但值不能是 admin 的参数 // 不带该参数也OK

举例:{"param1=value1", "param2"} 请求必须包含名为 param1 和 param2 的两个请求参数,且 param1 参数的值必须为 value1。

3.2.4 映射请求头

headers 规定请求头,使用方式和 params 类似 (不按要求的也是直接 404)。

3.3 Ant 风格的 URL

Ant 风格资源地址支持 3 种匹配符:

 ?     匹配文件名中的一个字符(0个/多个都不行)
 *     匹配文件名中的任意字符或者一层路径(0层/多层都不行)
**     匹配多层路径

@RequestMapping 还支持 Ant 风格的 URL

/user/*/createUser     匹配 /user/aaa/createUser、/user/bbb/createUser 等 URL
/user/**/createUser    匹配 /user/createUser、/user/aaa/bbb/createUser 等 URL
/user/createUser??     匹配 /user/createUseraa、/user/createUserbb 等 URL

3.4 @PathVariable

带占位符的 URL 是 Spring3.0 新增的功能,该功能在 SpringMVC 向 REST 目标挺进发展过程中具有里程碑的意义。

通过 @PathVariable 可以将 URL 中占位符参数绑定到控制器处理方法的形参中:URL 中的 {xxx} 占位符可以通过 @PathVariable("xxx") 绑定到操作方法的形参中。

4. REST

4.1 REST 风格

  • 简述
  • 示例(URL 起名方式:/资源名/资源标识符
  • 【总结】URL 定位资源,用 HTTP 动词(GET,POST,DELETE,PUT)描述操作。
    • 看 URL 就知道要什么
    • 看 HTTP Method 就知道干什么
    • 看 HTTP StatusCode 就知道结果如何

4.2 HiddenHttpMethodFilter

浏览器 form 表单只支持 GET 与 POST 请求,而 DELETE、PUT 等 method 并不支持,Spring3.0 添加了这个过滤器,可以将这些请求转换为标准的 HTTP 方法,使得支持 GET、POST、PUT 与 DELETE 请求。

public class HiddenHttpMethodFilter extends OncePerRequestFilter {

    public static final String DEFAULT_METHOD_PARAM = "_method";

    private String methodParam = DEFAULT_METHOD_PARAM;

    public void setMethodParam(String methodParam) {
        Assert.hasText(methodParam, "'methodParam' must not be empty");
        this.methodParam = methodParam;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response
            , FilterChain filterChain) throws ServletException, IOException {
        String paramValue = request.getParameter(this.methodParam);
        if ("POST".equals(request.getMethod()) && StringUtils.hasLength(paramValue)) {
            String method = paramValue.toUpperCase(Locale.ENGLISH);
            HttpServletRequest wrapper = new HttpMethodRequestWrapper(request, method);
            filterChain.doFilter(wrapper, response);
        } else {
            filterChain.doFilter(request, response);
        }
    }

    private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {

        private final String method;

        public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
            super(request);
            this.method = method;
        }

        @Override
        public String getMethod() {
            return this.method;
        }
    }
}

5. 请求处理

  • Spring MVC 通过分析处理方法的签名,HTTP 请求信息绑定到处理方法的相应形参中。
  • Spring MVC 对控制器处理方法签名的限制是很宽松的,几乎可以按喜欢的任何方式对方法进行签名。
  • 必要时可以对方法及方法形参标注相应的注解(@PathVariable 、@RequestParam、@RequestHeader 等)
  • Spring MVC 框架会将 HTTP 请求的信息绑定到相应的方法形参中,并根据方法的返回值类型做出相应的后续处理

5.1 @RequestParam

  • 在处理方法形参处使用 @RequestParam 可以把请求参数传递给请求方法
  • 注解属性
    • value 参数名
    • required 是否必须。默认为 true,表示请求参数中必须包含对应的参数;若不存在,将抛出异常
    • defaultValue 默认值,当没有传递参数时使用该值

5.2 @RequestHeader

  • 使用 @RequestHeader 绑定请求报头的属性值
  • 请求头包含了若干个属性,服务器可据此获知客户端的信息,通过 @RequestHeader 即可将请求头中的属性值绑定到处理方法的形参中
  • 注解属性同 @RequestParam

5.3 @CookieValue

使用 @CookieValue 绑定请求中的 Cookie 值到处理方法的某个形参。

5.4 POJO 作为参数

使用 POJO 对象绑定请求参数值时,Spring MVC 会按请求参数名和 POJO 属性名进行自动匹配,自动为该对象填充属性值。支持级联属性。如:dept.deptId、dept.address.tel 等。

5.5 ServletAPI 作为参数

MVC 的 Handler 方法可以接受如下 ServletAPI 类型的参数(源码参考:AnnotationMethodHandlerAdapter)

  1. HttpServletRequest
  2. HttpServletResponse
  3. HttpSession
  4. java.security.Principal
  5. Locale
  6. InputStream
  7. OutputStream
  8. Reader
  9. Writer

5.6 乱码问题

5.6.1 GET 乱码解决

5.6.2 POST 乱码解决

配置字符编码过滤器。但要注意:当有多个 Filter 时,一般都会将字符编码 Filter 置首!因为 CharacterEncodingFilter 必须要在第一次获取请求参数之前设置才有意义。

假设现在配了俩过滤器:HiddenHttpMethodFilter 和 CharacterEncodingFilter。如果先配置了 HiddenHttpMethodFilter,而其 doFilter() 中调用的 request.getParameter(this.methodParam) 会使得字符编码过滤器失效。所以,一般在 web.xml 中都会先配置字符编码过滤器。

public class CharacterEncodingFilter extends OncePerRequestFilter {

    private String encoding;

    // 是否强制 ServletResponse 的编码格式和 ServletRequest 的编码格式一样
    private boolean forceEncoding = false; // false 就只是设置了 request 的编码格式

    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }

    public void setForceEncoding(boolean forceEncoding) {
        this.forceEncoding = forceEncoding;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
            response, FilterChain filterChain) throws ServletException, IOException {
        if (this.encoding != null && (this.forceEncoding
                || request.getCharacterEncoding() == null)) {
            // 设置请求编码
            request.setCharacterEncoding(this.encoding);
            if (this.forceEncoding) {
                // 设置响应编码
                response.setCharacterEncoding(this.encoding);
            }
        }
        filterChain.doFilter(request, response);
    }
}
posted @ 2020-09-15 12:14  tree6x7  阅读(112)  评论(0编辑  收藏  举报