【SpringMVC】RESTFul簡介以及案例實現

gonghr發表於2021-09-02

RESTful

概念

REST:Representational State Transfer,表現層資源狀態轉移。

  • 資源
    資源是一種看待伺服器的方式,即,將伺服器看作是由很多離散的資源組成。每個資源是伺服器上一個可命名的抽象概念。因為資源是一個抽象的概念,所以它不僅僅能代表伺服器檔案系統中的一個檔案、資料庫中的一張表等等具體的東西,可以將資源設計的要多抽象有多抽象,只要想象力允許而且客戶端應用開發者能夠理解。與物件導向設計類似,資源是以名詞為核心來組織的,首先關注的是名詞。一個資源可以由一個或多個URI來標識。URI既是資源的名稱,也是資源在Web上的地址。對某個資源感興趣的客戶端應用,可以通過資源的URI與其進行互動。

  • 資源的表述
    資源的表述是一段對於資源在某個特定時刻的狀態的描述。可以在客戶端-伺服器端之間轉移(交換)。資源的表述可以有多種格式,例如HTML/XML/JSON/純文字/圖片/視訊/音訊等等。資源的表述格式可以通過協商機制來確定。請求-響應方向的表述通常使用不同的格式。

  • 狀態轉移
    狀態轉移說的是:在客戶端和伺服器端之間轉移(transfer)代表資源狀態的表述。通過轉移和操作資源的表述,來間接實現操作資源的目的。

RESTFul的實現

具體說,就是 HTTP 協議裡面,四個表示操作方式的動詞:GETPOSTPUTDELETE

它們分別對應四種基本操作:GET 用來獲取資源,POST 用來新建資源,PUT 用來更新資源,DELETE 用來刪除資源。

REST 風格提倡 URL 地址使用統一的風格設計,從前到後各個單詞使用斜槓分開,不使用問號鍵值對方式攜帶請求引數,而是將要傳送給伺服器的資料作為 URL 地址的一部分,以保證整體風格的一致性。

操作 傳統方式 REST風格
查詢操作 getUserById?id=1 user/1-->get請求方式
儲存操作 saveUser user-->post請求方式
刪除操作 deleteUser?id=1 user/1-->delete請求方式
更新操作 updateUser user-->put請求方式

如何處理PUT和DELETE請求

由於瀏覽器只支援傳送get和post方式的請求,那麼該如何傳送put和delete請求呢?

SpringMVC 提供了 HiddenHttpMethodFilter 幫助我們將 POST 請求轉換為 DELETE 或 PUT 請求

HiddenHttpMethodFilter原始碼


public class HiddenHttpMethodFilter extends OncePerRequestFilter {
    private static final List<String> ALLOWED_METHODS;
    public static final String DEFAULT_METHOD_PARAM = "_method";
    private String methodParam = "_method";

    public HiddenHttpMethodFilter() {
    }

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

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        HttpServletRequest requestToUse = request;
        if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
            String paramValue = request.getParameter(this.methodParam);
            if (StringUtils.hasLength(paramValue)) {
                String method = paramValue.toUpperCase(Locale.ENGLISH);
                if (ALLOWED_METHODS.contains(method)) {
                    requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
                }
            }
        }

        filterChain.doFilter((ServletRequest)requestToUse, response);
    }

    static {
        ALLOWED_METHODS = Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
    }

    private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
        private final String method;

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

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

原始碼解析


private static final List<String> ALLOWED_METHODS;

static {
        ALLOWED_METHODS = Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
    }

ALLOWED_METHODS是一個靜態常量型別的字串List,使用靜態程式碼塊在類載入時對其進行初始化,儲存的內容為[PUT,DELETE,PATCH]HttpMethod是一個列舉型別。

public static final String DEFAULT_METHOD_PARAM = "_method";
private String methodParam = "_method";

HiddenHttpMethodFilter類的靜態常量“預設方法引數”是"_method",給定私有方法引數也是"_method"。

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

提供了一個方法用於修改類內預設的方法引數並且斷言形參不能為空。

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        HttpServletRequest requestToUse = request;
        if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
            String paramValue = request.getParameter(this.methodParam);
            if (StringUtils.hasLength(paramValue)) {
                String method = paramValue.toUpperCase(Locale.ENGLISH);
                if (ALLOWED_METHODS.contains(method)) {
                    requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
                }
            }
        }

        filterChain.doFilter((ServletRequest)requestToUse, response);
    }

這個方法是該類中實現過濾器功能的主要方法,是重點。第一個if判斷要求請求的方式必須是POST否則方法不會執行。paramValue獲取請求引數this.methodParam的值,這裡需要注意當前請求必須傳輸請求引數_method。將paramValue統一為大寫字母之後,判斷如果ALLOWED_METHODS包含paramValue的請求方式,就把該請求方式和request作為引數建立HttpMethodRequestWrapper靜態內部類,並把返回值賦給requestToUse,過濾器放行的時候原本的request已經被替換成了requestToUse這一新的,以paramValue為請求方式的請求。

下面看一下HttpMethodRequestWrapper靜態內部類

private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
        private final String method;

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

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

HttpMethodRequestWrapper類的繼承關係如下:

image

HttpMethodRequestWrapper類重寫了getMethod()方法,偷樑換柱,實現了對請求方式的修改,可以使getMethod()方法返回PUTDELETE

由於HttpMethodRequestWrapper實現了HttpServletRequest介面,所以利用多型,可以在上面講的doFilterInternal方法中,使得父類引用HttpServletRequest指向該類,在放行時把原來的request替換,也就實現了,伺服器傳送PUTDELETE的需求。

使用方法

 <form action="/rest/12" method="post">
    <input type="hidden" name="_method" value="DELETE">
    <input type="submit" value="delete">
  </form>
  <form action="/rest/12" method="post">
    <input type="hidden" name="_method" value="PUT">
    <input type="submit" value="put">
  </form>
  <form action="/rest/12" method="post">
    <input type="submit" value="post">
  </form>
  <form action="/rest/12" method="get">
    <input type="submit" value="get">
  </form>

控制器

    @RequestMapping(value = "/rest/{id}",method = RequestMethod.DELETE)
    public String testrestDELETE(@PathVariable int id, Model model){
        model.addAttribute("msg","delete請求"+id);
        return "SUCCESS";
    }
    @RequestMapping(value = "/rest/{id}",method = RequestMethod.PUT)
    public String testrestPUT(@PathVariable int id,Model model){
        model.addAttribute("msg","put請求"+id);
        return "SUCCESS";
    }
    @RequestMapping(value = "/rest/{id}",method = RequestMethod.POST)
    public String testrestPOST(@PathVariable int id,Model model){
        model.addAttribute("msg","post請求"+id);
        return "SUCCESS";

    }
    @RequestMapping(value = "/rest/{id}",method = RequestMethod.GET)
    public String testrestDELETE(@PathVariable int id, ModelMap modelMap){
        modelMap.addAttribute("msg","get請求"+id);
        return "SUCCESS";
    }

RESTFul案例

案例需求

傳統 CRUD 小專案,實現對員工資訊的增刪改查。
image

add:新增員工
delete:刪除員工
update:更新員工

專案結構

├─springMVC-demo04
│  ├─src
│  │  ├─main
│  │  │  ├─java
│  │  │  │  └─com
│  │  │  │      └─springmvc
│  │  │  │          └─gonghr
│  │  │  │              ├─bean
│  │  │  │              ├─controller
│  │  │  │              └─dao
│  │  │  ├─resources
│  │  │  └─webapp
│  │  │      ├─static
│  │  │      │  └─js
│  │  │      └─WEB-INF
│  │  │          └─templates
│  │  └─test
│  │      └─java
│  └─target
│      ├─classes
│      ├─generated-sources
│      ├─maven-archiver
│      ├─maven-status
│      └─springMVC-demo04-1.0-SNAPSHOT
└─src

SpringMVC 框架搭建

【SpringMVC】SpringMVC搭建框架

準備工作

準備實體類

/bean/Employee.java


public class Employee {

    private Integer id;
    private String lastName;

    private String email;
    //1 male, 0 female
    private Integer gender;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Integer getGender() {
        return gender;
    }

    public void setGender(Integer gender) {
        this.gender = gender;
    }

    public Employee(Integer id, String lastName, String email, Integer gender) {
        super();
        this.id = id;
        this.lastName = lastName;
        this.email = email;
        this.gender = gender;
    }

    public Employee() {
    }
}

準備dao模擬資料

/dao/EmployeeDao

@Repository
public class EmployeeDao {

   private static Map<Integer, Employee> employees = null;
   
   static{
      employees = new HashMap<Integer, Employee>();

      employees.put(1001, new Employee(1001, "E-AA", "aa@163.com", 1));
      employees.put(1002, new Employee(1002, "E-BB", "bb@163.com", 1));
      employees.put(1003, new Employee(1003, "E-CC", "cc@163.com", 0));
      employees.put(1004, new Employee(1004, "E-DD", "dd@163.com", 0));
      employees.put(1005, new Employee(1005, "E-EE", "ee@163.com", 1));
   }
   
   private static Integer initId = 1006;
   
   public void save(Employee employee){
      if(employee.getId() == null){
         employee.setId(initId++);
      }
      employees.put(employee.getId(), employee);
   }
   
   public Collection<Employee> getAll(){
      return employees.values();
   }
   
   public Employee get(Integer id){
      return employees.get(id);
   }
   
   public void delete(Integer id){
      employees.remove(id);
   }
}

功能清單

功能 URL 地址 請求方式
訪問首頁√ / GET
查詢全部資料√ /employee GET
刪除√ /employee/2 DELETE
跳轉到新增資料頁面√ /toAdd GET
執行儲存√ /employee POST
跳轉到更新資料頁面√ /employee/2 GET
執行更新√ /employee PUT

具體功能

訪問首頁

/templates/index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" >
    <title>Title</title>
</head>
<body>
<h1>首頁</h1>
<a th:href="@{/employee}">訪問員工資訊</a>
</body>
</html>

xml配置首頁的前端控制器
springMVC.xml

<!--開啟mvc註解驅動-->
    <mvc:annotation-driven></mvc:annotation-driven>
    <mvc:view-controller path="/" view-name="index"></mvc:view-controller>

查詢所有員工資料

控制器

@RequestMapping(value = "/employee", method = RequestMethod.GET)
public String getEmployeeList(Model model){
    Collection<Employee> employeeList = employeeDao.getAll();
    model.addAttribute("employeeList", employeeList);
    return "employee_list";
}

employee_list.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Employee Info</title>
    <script type="text/javascript" th:src="@{/static/js/vue.js}"></script>
</head>
<body>

    <table border="1" cellpadding="0" cellspacing="0" style="text-align: center;" id="dataTable">
        <tr>
            <th colspan="5">Employee Info</th>
        </tr>
        <tr>
            <th>id</th>
            <th>lastName</th>
            <th>email</th>
            <th>gender</th>
            <th>options(<a th:href="@{/toAdd}">add</a>)</th>
        </tr>
        <tr th:each="employee : ${employeeList}">
            <td th:text="${employee.id}"></td>
            <td th:text="${employee.lastName}"></td>
            <td th:text="${employee.email}"></td>
            <td th:text="${employee.gender}"></td>
            <td>
                <a class="deleteA" @click="deleteEmployee" th:href="@{'/employee/'+${employee.id}}">delete</a>
                <a th:href="@{'/employee/'+${employee.id}}">update</a>
            </td>
        </tr>
    </table>
</body>
</html>

注意利用Thymeleaf檢視編寫超連結時如果要在超連結中拼接request域中的資料,要在@{}內部利用+${}進行拼接,否則大括號和字串都會被解析。

正確寫法:

<a th:href="@{'/employee/'+${employee.id}}">update</a>

瀏覽器解析結果:
http://localhost:8080/springMVC_demo04/employee/1001

如果寫成:

<a th:href="@{/employee/${employee.id}}">update</a>

那麼瀏覽器位址列會顯示解析結果:http://localhost:8080/springMVC_demo04/employee/$%7Bemployee.id%7D

刪除功能

  • 建立處理delete請求方式的表單
<!-- 作用:通過超連結控制表單的提交,將post請求轉換為delete請求 -->
<form id="delete_form" method="post">
    <!-- HiddenHttpMethodFilter要求:必須傳輸_method請求引數,並且值為最終的請求方式 -->
    <input type="hidden" name="_method" value="delete"/>
</form>
  • 刪除超連結繫結點選事件

引入vue.js

<script type="text/javascript" th:src="@{/static/js/vue.js}"></script>

注意:這裡引入vue.js後瀏覽器是無法檢索到的,因為該檔案是後期加入的一個靜態檔案,而專案的war包早就打好,沒有加入vue.js,所以需要重新打包專案。maven-->LifeCycle-->package

image

配置預設servlet處理器

springMVC.xml

<!--    開放對靜態資源的訪問-->
<!--    靜態資源先被springMVC前端控制器處理,如果找不到相應的請求對映,就交給預設的servlet處理-->
    <mvc:default-servlet-handler></mvc:default-servlet-handler>

刪除超連結

<a class="deleteA" @click="deleteEmployee" th:href="@{'/employee/'+${employee.id}}">delete</a>

通過vue處理點選事件

<script type="text/javascript">
    var vue = new Vue({
        el:"#dataTable",
        methods:{
            //event表示當前事件
            deleteEmployee:function (event) {
                //通過id獲取表單標籤
                var delete_form = document.getElementById("delete_form");
                //將觸發事件的超連結的href屬性為表單的action屬性賦值
                delete_form.action = event.target.href;
                //提交表單
                delete_form.submit();
                //阻止超連結的預設跳轉行為
                event.preventDefault();
            }
        }
    });
</script>
  • 控制器方法
@RequestMapping(value = "/employee/{id}", method = RequestMethod.DELETE)
public String deleteEmployee(@PathVariable("id") Integer id){
    employeeDao.delete(id);
    return "redirect:/employee";  //請求重定向
}

跳轉到新增資料頁面

  • 配置view-controller
<mvc:view-controller path="/toAdd" view-name="employee_add"></mvc:view-controller>
  • 建立employee_add.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Add Employee</title>
</head>
<body>

<form th:action="@{/employee}" method="post">
    lastName:<input type="text" name="lastName"><br>
    email:<input type="text" name="email"><br>
    gender:<input type="radio" name="gender" value="1">male
    <input type="radio" name="gender" value="0">female<br>
    <input type="submit" value="add"><br>
</form>

</body>
</html>

執行儲存

  • 控制器方法
@RequestMapping(value = "/employee", method = RequestMethod.POST)
public String addEmployee(Employee employee){
    employeeDao.save(employee);
    return "redirect:/employee";
}

跳轉到更新資料頁面

  • 修改超連結
<a th:href="@{'/employee/'+${employee.id}}">update</a>
  • 控制器方法
@RequestMapping(value = "/employee/{id}", method = RequestMethod.GET)
public String getEmployeeById(@PathVariable("id") Integer id, Model model){
    Employee employee = employeeDao.get(id);
    model.addAttribute("employee", employee);
    return "employee_update";
}
  • 建立employee_update.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Update Employee</title>
</head>
<body>

<form th:action="@{/employee}" method="post">
    <input type="hidden" name="_method" value="put">
    <input type="hidden" name="id" th:value="${employee.id}">  <!-- 注意隱藏資料id -- >
    lastName:<input type="text" name="lastName" th:value="${employee.lastName}"><br>
    email:<input type="text" name="email" th:value="${employee.email}"><br>
    <!--
        th:field="${employee.gender}"可用於單選框或核取方塊的回顯
        若單選框的value和employee.gender的值一致,則新增checked="checked"屬性
    -->
    gender:<input type="radio" name="gender" value="1" th:field="${employee.gender}">male
    <input type="radio" name="gender" value="0" th:field="${employee.gender}">female<br>
    <input type="submit" value="update"><br>
</form>

</body>
</html>

執行更新

  • 控制器方法
@RequestMapping(value = "/employee", method = RequestMethod.PUT)
public String updateEmployee(Employee employee){
    employeeDao.save(employee);
    return "redirect:/employee";
}

處理靜態資源的過程

在上面案例中的引入vue.js靜態檔案過程中有幾個零碎的步驟,下面講解一下原因。

在javaweb專案中,靜態資源是被預設servlet處理的。

進入到Tomcat的配置檔案目錄中E:\DevTols\apache-tomcat-9.0.30\conf,開啟web.xml

強調一點:Tomcat中的web.xml作用於部署到Tomcat的所有工程,而工程中的web.xml只針對於當前工程,如果當前工程中的web.xml與Tomcat中的web.xml發生衝突,則以當前工程中的web.xml為準。

    <servlet>
        <servlet-name>default</servlet-name>
        <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
        <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>listings</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

這是Tomcat中預設的servlet,觀察其請求路徑<url-pattern>/</url-pattern>,與工程中的DispatcharServler的請求路徑重衝突。

    <servlet>
        <servlet-name>springMVC</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>springMVC</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

則以當前專案中的servlet為準,所有請求都用DispatcharServler處理,而DispatcharServler的處理方式是在控制器中尋找相應的請求對映,而控制器中沒有訪問靜態資源的請求對映,所以無法找到靜態資源,報404錯誤。

解決方案就是在springMVC.xml中開放對靜態資源的訪問

<!--    開放對靜態資源的訪問-->
<!--    靜態資源先被springMVC前端控制器處理,如果找不到相應的請求對映,就交給預設的servlet處理-->
    <mvc:default-servlet-handler></mvc:default-servlet-handler>

注意:開放對靜態資源的訪問和開放mvc註解驅動必須同時進行。如果只開放對靜態資源的訪問,則所有請求都由預設servlet處理;如果只開放mvc註解驅動,則所有請求都由DispatcharServlet處理。
如果都開放,則先用DispatcharServlet處理,如果沒有找到請求對映,則由預設servlet處理。

<!--開啟mvc註解驅動-->
    <mvc:annotation-driven></mvc:annotation-driven>
<!--    開放對靜態資源的訪問-->
<!--    靜態資源先被springMVC前端控制器處理,如果找不到相應的請求對映,就交給預設的servlet處理-->
    <mvc:default-servlet-handler></mvc:default-servlet-handler>

相關文章