【SpringMVC从入门到精通】06、RESTful

1,518 阅读9分钟

06、RESTful

1、RESTful 简介

RESTRepresentational State Transfer,表现层资源状态转移

  • 表现层/表示层:前端的视图页面到后端的控制层即为“表现层”
  • 资源:Web 工程部署到服务器上后,当前 Web 工程中的内容在服务器上都叫“资源”(万物皆资源)
  • 状态:资源的表现形式,例如,HTML/JSP 页面、CSS/JS 文件、图片/音频/视频等皆为资源的“状态”
  • 转移:浏览器发送请求到服务器,服务端就将请求的资源“转移”到客户端

RESTful:基于REST构建的 API 就是RESTful风格

1.1、资源

  • 资源是一种看待服务器的方式,即将服务器看作是由很多离散的资源组成。每个资源是服务器上一个可命名的抽象概念
  • 因为资源是一个抽象的概念,所以它不仅仅能代表服务器文件系统中的一个文件、数据库中的一张表等等具体的东西,也可以将资源设计的要多抽象有多抽象,只要想象力允许而且客户端应用开发者能够理解
  • 与面向对象设计类似,资源是以名词为核心来组织的,首先关注的是“名词”。一个资源可以由一个或多个URI来标识。URI既是资源的名称,也是资源在 Web 上的地址。对某个资源感兴趣的客户端应用,可以通过资源的URI与其进行交互

URIUniform Resource Identifier):统一资源标志符

URLUniform Resource Locator):统一资源定位符

URI是一个抽象的、高层次的概念,而URL是具体的方式。简单来说,URL是一种URI

1.2、资源的表述

  • 资源的表述是一段对于资源在某个特定时刻的状态的描述。可以在客户端-服务器端之间转移(交换)
  • 资源的表述可以有多种格式,例如HTML/XML/JSON/纯文本/图片/视频/音频等等
  • 资源的表述格式可以通过协商机制来确定。请求-响应方向的表述通常使用不同的格式

1.3、状态转移

  • 状态转移说的是:在客户端和服务器端之间转移(transfer)代表资源状态的表述
  • 通过转移和操作资源的表述,来间接实现操作资源的目的

2、RESTful 实现

RESTful的实现,具体说就是:HTTP 协议里面,四个表示操作方式的动词GETPOSTPUTDELETE

它们分别对应四种基本操作:

  • GET用来获取资源
  • POST用来新建资源
  • PUT用来更新资源
  • DELETE用来删除资源

REST风格URL地址不使用问号键值对方式携带请求参数,而是:

提倡使用统一的风格设计,从前到后各个单词使用斜杠分开,将要发送给服务器的数据作为地址的一部分,以保证整体风格的一致性URL

以往,我们访问资源的方式五花八问。例如,

  • 获取用户信息通过getUserById/selectUserById/findUserById/等
  • 删除用户信息通过deleteUserById/removeUserById
  • 更新用户信息通过updateUser/modifyUser/saveUser
  • 新增用户信息通过addUser/createUser/insertUser

上述操作的资源都是用户信息。按照RESTful思想,既然操作的资源一样,那么请求路径就应该一样

用一张表格来对比传统方式和REST风格对资源操作的区别

操作传统方式REST 风格
查询getUserById?id=1user/1-->get请求
保存saveUseruser-->post请求
删除deleteUserById?id=1user/1-->delete请求
更新updateUseruser-->put请求

3、使用 RESTful 模拟操作用户资源

需求分析:使用 RESTful 模拟用户资源的增删改查,通过路径中的占位符传递请求参数,通过不同的请求方式对应资源的不同操作

RESTful 路径请求方式操作
/userGET查询所有用户信息
/user/1GET根据用户ID查询用户信息
/userPOST添加用户信息
/user/1DELETE根据用户ID删除用户信息
/userPUT修改用户信息

3.1、用户的查询、添加

后台代码实现

RESTfulController.java

@Controller
@RequestMapping("restfulcontroller")
public class RESTfulController {
    @RequestMapping(value = "/user", method = RequestMethod.GET)
    public String getAllUser() {
        System.out.println("查询所有用户信息");
        return "success";
    }

    @RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
    public String getUserById(@PathVariable("id") String id) {
        System.out.println("根据用户ID查询用户信息:" + id);
        return "success";
    }

    @RequestMapping(value = "/user", method = RequestMethod.POST)
    public String insertUser(User user) { // User 沿用之前的对象
        System.out.println("添加用户信息:" + user);
        return "success";
    }

    /** 注:由于 PUT、DELETE 请求比较特殊,后面再做补充 */
}

前台测试代码

restful.html

<a th:href="@{/restfulcontroller/user}">查询所有用户信息</a><br/>
<a th:href="@{/restfulcontroller/user/1}">根据用户ID查询用户信息</a><br/>
<form th:action="@{/restfulcontroller/user}" method="post">
    用户名:<input type="text" name="username"><br/>
    密码:<input type="password" name="password"><br/>
    性别:<input type="radio" name="gender" value="male"><input type="radio" name="gender" value="female"><br/>
    年龄:<input type="number" name="age"><br/>
    邮箱:<input type="text" name="email"><br/>
    <input type="submit" value="添加用户">
</form>

为了能够访问到restful.html前台页面资源,可以通过在控制器中定义一个控制器方法来返回其视图,也可以通过在04-SpringMVC 视图中提到的view-controller视图控制器代替。因为这里只是为了实现页面跳转,没有其他请求过程的处理,所以可以通过在 SpringMVC 配置文件中使用<view-controller>标签进行设置

<mvc:view-controller path="/restful" view-name="restful"></mvc:view-controller>

测试结果

后台日志信息

查询所有用户信息
根据用户ID查询用户信息:1
添加用户信息:User{username='admin', password='11111', gender='male', age='18', email='123@qq.com'}

3.2、用户的修改、删除

01- @RequestMapping 注解 中,我们提到过form表单默认只支持GETPOST请求,如果直接通过method属性指定为PUTDELETE,会默认以GET请求方式处理

而想要实现PUTDELETE请求,需要在web.xml中配置 SpringMVC 提供的HiddenHttpMethodFilter过滤器,并在前台页面使用隐藏域来设置PUTDELETE类型的请求方式

当然,也可以使用AJAX发送PUTDELETE请求,但是需要注意PUTDELETE仅部分浏览器支持

为了更清楚地了解HiddenHttpMethodFilter过滤器到底干了什么,这里对其源码进行剖析

public class HiddenHttpMethodFilter extends OncePerRequestFilter {
    private static final List<String> ALLOWED_METHODS = Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),HttpMethod.DELETE.name(),HttpMethod.PATCH.name()));
    public static final String DEFAULT_METHOD_PARAM = "_method";
    private String methodParam = DEFAULT_METHOD_PARAM;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
        HttpServletRequest requestToUse = request;
        // 要求原请求方式为 POST 请求
        if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
            // methodParam值:_method
            // paramValue值:_method属性值
            String paramValue = request.getParameter(this.methodParam);
            if (StringUtils.hasLength(paramValue)) {
                // _method属性值转为全大写形式,put==>PUT,delete==>DELETE
                String method = paramValue.toUpperCase(Locale.ENGLISH);
                // 判断_method属性值是否为{"PUT","DELETE","PATCH"}中的一个
                if (ALLOWED_METHODS.contains(method)) {
                    // “偷梁换柱”,包装为一个新的请求对象
                    requestToUse = new HttpMethodRequestWrapper(request, method);
                }
            }
        }
        filterChain.doFilter(requestToUse, 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;
        }
    }
}

“源码在手,天下我有”。接下来,将理论付诸实践

配置文件

web.xml配置HiddenHttpMethodFilter过滤器

<filter>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

后台代码实现

@RequestMapping(value = "/user", method = RequestMethod.PUT)
public String updateUser(User user) {
    System.out.println("修改用户信息:" + user);
    return "success";
}
// 这里的写法格式没有问题,但业务逻辑其实大有问题,下面再详细说
@RequestMapping(value = "/user", method = RequestMethod.DELETE)
public String deleteUser(User user) {
    System.out.println("删除用户信息:" + user);
    return "success";
}

前台测试代码

在添加用户的表单基础上,添加隐藏域<input type="hidden" name="_method" value="put">

<form th:action="@{/restfulcontroller/user}" method="post">
    <input type="hidden" name="_method" value="put">
    用户名:<input type="text" name="username"><br/>
    密码:<input type="password" name="password"><br/>
    性别:<input type="radio" name="gender" value="male"><input type="radio" name="gender" value="female"><br/>
    年龄:<input type="number" name="age"><br/>
    邮箱:<input type="text" name="email"><br/>
    <input type="submit" value="修改用户">
</form>
<hr/>
<!-- 这里的写法格式没有问题,但业务逻辑其实大有问题,下面再详细说 -->
<form th:action="@{/restfulcontroller/user}" method="post">
    <input type="hidden" name="_method" value="delete">
    用户名:<input type="text" name="username"><br/>
    密码:<input type="password" name="password"><br/>
    性别:<input type="radio" name="gender" value="male"><input type="radio" name="gender" value="female"><br/>
    年龄:<input type="number" name="age"><br/>
    邮箱:<input type="text" name="email"><br/>
    <input type="submit" value="删除用户">
</form>

测试结果

修改用户

删除用户

后台日志信息

修改用户信息:User{username='user', password='11111', gender='female', age='1', email='111@qq.com'}
删除用户信息:User{username='user', password='11111', gender='female', age='1', email='111@qq.com'}

到这里,应该需要指明的是我们在设计RESTful实现时,对删除用户信息的要求下面这样的

RESTful 路径请求方式操作
/user/1DELETE根据用户ID删除用户信息

换句话说,这里应该通过超链接而非表单形式,即通过用户ID来对用户信息进行删除操作。而一般情况下,我们是通过行编辑删除某一行数据,或是通过选中表单的数据来进行批量删除,这些功能在详细案例时会详细介绍

4、CharacterEncodingFilter 和 HiddenHttpMethodFilter 的配置顺序

目前,我们在web.xml配置文件中对CharacterEncodingFilterHiddenHttpMethodFilter的配置顺序如下

<!--处理编码-->
<filter>
    <filter-name>characterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
        <param-name>forceResponseEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>characterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 略 -->
<!--配置 org.springframework.web.filter.HiddenHttpMethodFilter: 可以把 POST 请求转为 DELETE 或 POST 请求 -->
<filter>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

如果,将两者顺序颠倒互换,即

<!--配置 org.springframework.web.filter.HiddenHttpMethodFilter: 可以把 POST 请求转为 DELETE 或 POST 请求 -->
<filter>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 略 -->
<!--处理编码-->
<filter>
    <filter-name>characterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
        <param-name>forceResponseEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>characterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

看看顺序改变之后,对请求的编码会有什么影响

后台日志信息

修改用户信息:User{username='张三', password='123456', gender='male', age='18', email='123@qq.com'}

可以发现,中文的“张三”乱码了,这是为什么呢?

02-SpringMVC 获取请求参数一节中的7、处理乱码问题中,我们尝试在获取请求参数之前,通过request.setCharacterEncoding("UTF-8");来设置请求编码格式,但是没有生效。分析的原因是“请求参数获取在前,设置编码格式在后”导致的。

我们也提出了 2 种解决方案:

  • 1、获取请求参数之后,手动解码编码。但是这种显然不合理,所以直接 pass
  • 2、获取请求参数之前“做手脚”。这种方式就是 SpringMVC 中提供的CharacterEncodingFilter过滤器,来对请求编码做统一处理

现在的问题就是:在CharacterEncodingFilter之前配置了HiddenHttpMethodFilter导致了失效

所以我们需要搞清楚,为什么CharacterEncodingFilter的配置顺序会影响到编码的效果?或者说为什么HiddenHttpMethodFilter会使之失效?

通过上面对HiddenHttpMethodFilter源码的剖析,它会获得_method这个请求参数,这就导致执行到CharacterEncodingFilter过滤器时,已经是获取请求参数之后了,所以会导致上述中文乱码问题

因此,我们必须要将CharacterEncodingFilter过滤器尽量配置在其他过滤器之前。这样就能保证在任何过滤器获取请求之前,获得的失已经处理过编码格式的请求参数了

我们再将CharacterEncodingFilterHiddenHttpMethodFilter的配置顺序还原至之前的状态,即CharacterEncodingFilter在前而HiddenHttpMethodFilter在后的情况,进行测试再查看后台日志信息

修改用户信息:User{username='张三', password='', gender='male', age='18', email='123@qq.com'}

这时,当请求参数中包含中文时,就不会出现乱码的情况了

总结

本节重点掌握内容

  • 明确RESTRESTful的关系,明确表现层、资源、状态、转移这几个概念的含义
  • REST,表现层资源状态转移
  • RESTful,基于REST构建的 API 就是RESTful风格
  • 明确RESTful的实现,是通过不同的请求方式来对应资源的不同操作,通过路径中的占位符传递请求参数
  • 熟练掌握如何通过RESTful进行资源的增删改查操作,以及如何处理PUTDELETE这两种特殊的请求方式
  • 明确CharacterEncodingFilterHiddenHttpMethodFilter的配置顺序,明白两个过滤器的源码处理逻辑

附上导图,仅供参考

20.png