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


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>


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM