你的位置:首页 > 信息动态 > 新闻中心
信息动态
联系我们

11-SSM_SpringMVC(下)

2021/12/31 11:38:29

目录

一,页面导航的方式

1,转发到一个jsp页面

1.1 使用字符串转发

1.2 使用ModelAndView转发

2,重定向到一个jsp页面

2.1 使用字符串重定向

2.2 使用ModelAndView重定向

3,重定向或者转发到控制器

二,异常处理

1,@ExceptionHandler 注解

2,实现步骤

2.1 自定义异常类

2.2 编写控制器

2.3 编写error.jsp、idError.jsp、nameError.jsp页面

2.4 测试

3,优化

三,拦截器

1,介绍

2,自定义拦截器

3,配置拦截器

四,文件上传与下载

1,文件上传

1.1 上传过程

1.2 限制文件大小和类型

2,文件下载

五,RESTful介绍

1,REST概念

2,RESTful概念

六,RESTful风格的API设计原则

1,API设计/URL设计

1.1 动词加宾语

1.2 宾语必须是名词

1.3 避免多级URL

七,HTTP状态码

1,状态码2xx

2,状态码3xx

3,状态码4xx

4,状态码5xx

八,服务器响应

九,RESTful案例

1,查询

2,添加

3,更新

4,删除

5,RESTful风格的更新和删除遇到的问题

5.1 产生的原因

5.2 解决方案

十,自己封装响应结果


一,页面导航的方式

页面导航分为两种:1、转发 2、重定向

springMVC有以下两种方式实现页面的转发或重定向:

  • 1、返回字符串
  • 2、使用ModelAndView

在SpringMVC中两种导航进行页面导航的时候使用不同的前缀指定转发还是重定向

  • 转发: forward:url 默认
  • 重定向: redirect:url

准备工作:创建一个新的控制器NavigationController.java:

package com.kkb.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;

@Controller
@RequestMapping("navigation")
public class NavigationController {

}

1,转发到一个jsp页面

1.1 使用字符串转发

    //1、转发到一个JSP页面
    //使用字符串转发
    @RequestMapping("test01-1")
    public String test011(HttpServletRequest request){
        System.out.println("test01-1-------------");
        request.setAttribute("teamName","lacker");
        //return "ok";//默认方式:由视图解析器处理之后将逻辑视图转为物理资源路径
        return "forward:/jsp/ok.jsp";//当添加了forward前缀之后,视图解析器中的前后缀就失效了,必须自己编写绝对路径
    }

1.2 使用ModelAndView转发

    //使用ModelAndView转发
    @RequestMapping("test01-2")
    public ModelAndView test012(){
        System.out.println("test01-2-------------");
        ModelAndView mv=new ModelAndView();
        mv.addObject("teamName","热火");
        //mv.setViewName("ok");//默认方式:由视图解析器处理之后将逻辑视图转为物理资源路径
        mv.setViewName( "forward:/jsp/ok.jsp");//当添加了forward前缀之后,视图解析器中的前后缀就失效了,必须自己编写绝对路径
        return mv;
    }

2,重定向到一个jsp页面

2.1 使用字符串重定向

页面上无法获取到存储在request作用域中的值,请求中断了

    //2、重定向到一个JSP页面
    //使用字符串重定向
    @RequestMapping("test02-1")
    public String test021(HttpServletRequest request){
        request.setAttribute("teamName","勇士");//页面上无法获取到存储在request作用域中的值,请求中断了
        return "redirect:/jsp/ok.jsp";//当添加了redirect前缀之后,视图解析器中的前后缀就失效了,必须自己编写绝对路径
    }

2.2 使用ModelAndView重定向

存储在request作用域中的值以参数的形式追加在URL后面

http://localhost:8080/jsp/ok.jsp?teamName=huangfeng&teamId=1002

    //使用ModelAndView重定向
    @RequestMapping("test02-2")
    public ModelAndView test022(){
        ModelAndView mv=new ModelAndView();
        mv.addObject("teamName","huangfeng");
        mv.addObject("teamId","1002");//存储在request作用域中的值以参数的形式追加在URL后面 http://localhost:8080/jsp/ok.jsp?teamName=huangfeng&teamId=1002
        mv.setViewName( "redirect:/jsp/ok.jsp");//当添加了redirect前缀之后,视图解析器中的前后缀就失效了,必须自己编写绝对路径
        return mv;
    }

地址栏中的值可以在页面中获取到。修改ok.jsp

<h1>ok-----------${requestScope.teamName}</h1>
<h2>从地址栏中获取的参数值:teamName=${param.teamName},teamId=${param.teamId}</h2>

3,重定向或者转发到控制器

    //3、转发或者重定向到控制器
    @RequestMapping("test03-1")
        public ModelAndView test031(HttpServletRequest request){
        System.out.println("test03-1---转发到控制器");
        ModelAndView mv=new ModelAndView();
        mv.addObject("teamName","达拉斯小牛");
        mv.setViewName("forward:/navigation/test01-1");
        return mv;
    }
    @RequestMapping("test03-2")
    public ModelAndView test032(HttpServletRequest request){
        System.out.println("test03-1---重定向到控制器");
        ModelAndView mv=new ModelAndView();
        mv.addObject("teamName","kaierteren");
        mv.addObject("teamId","1003");
        mv.setViewName("redirect:/navigation/test01-1");//参数值直接追加到URL后面
        return mv;
    }

二,异常处理

SpringMVC框架常用@ExceptionHandler 注解处理异常。

1,@ExceptionHandler 注解

@ExceptionHandler 可以将一个方法指定为异常处理方法。

被注解的方法,其返回值可以是 ModelAndView、String,或 void,方法名随意,方法参数可以是Exception 及其子类对象、HttpServletRequest、HttpServletResponse 等。系统会自动为这些方法参数赋值。

对于异常处理注解的用法,也可以直接将异常处理方法注解于 Controller 之中.

2,实现步骤

2.1 自定义异常类

TeamException.java

public class TeamException extends  Exception{
    public TeamException() {
    }

    public TeamException(String message) {
        super(message);
    }
}

TeamIdException.java

public class TeamIdException extends  TeamException{

    public TeamIdException() {
    }

    public TeamIdException(String message) {
        super(message);
    }
}

TeamNameException.java

public class TeamNameException extends  TeamException{
    public TeamNameException() {
    }

    public TeamNameException(String message) {
        super(message);
    }
}

2.2 编写控制器

编写异常控制器ExController

@Controller
@RequestMapping("ex")
public class ExController {

    @RequestMapping("test01/{id}/{name}/{loc}")
    public ModelAndView test01(@PathVariable("id") Integer teamId,@PathVariable("name") String teamName,@PathVariable("loc") String loc) throws TeamException {
        ModelAndView mv=new ModelAndView();
        if(teamId<=1000){
            // 抛出自定义的异常
            throw new TeamIdException("teamId不合法!必须在1000之上!");
        }
        if("test".equals(teamName)){
            throw  new TeamNameException("teamName不合法!不能使用test!");
        }
        if("test".equals(loc)){
            throw  new TeamException("team属性错误!请仔细检查参数!");
        }
        System.out.println(10/0);// 其他异常
        mv.setViewName("ok");// 没有异常则跳转到ok页面
        return mv;
    }

    // 包含所有能处理的异常类型
    @ExceptionHandler(value = {TeamIdException.class,TeamNameException.class,Exception.class})
    public ModelAndView exHandler(Exception ex){
        ModelAndView mv=new ModelAndView();
        mv.addObject("msg",ex.getMessage());
        if(ex instanceof TeamIdException)
             mv.setViewName("idError");
        else if(ex instanceof TeamNameException)
            mv.setViewName("nameError");
        else
            mv.setViewName("error");
        return mv;
    }
}

2.3 编写error.jsp、idError.jsp、nameError.jsp页面

error.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>error</title>
</head>
<body>
    <h1>默认的错误页面--${msg}</h1>
</body>
</html>

idError.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>id error</title>
</head>
<body>
    <h1>teamId Error----${msg}</h1>
</body>
</html>

nameError.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>name error</title>
</head>
<body>
    <h1>teamName error---${msg}</h1>
</body>
</html>

2.4 测试

http://localhost:8080/ex/test01/100/test1
http://localhost:8080/ex/test01/1004/test
http://localhost:8080/ex/test01/1001/test1

3,优化

一般将异常处理方法专门定义在一个类中,作为全局的异常处理类。而不是将异常处理方法整合到controller里(如上)。

使用注解@ControllerAdvice,就是“控制器增强”,是给控制器对象增强功能的。使用
@ControllerAdvice 修饰的类中可以使用@ExceptionHandler。

当使用@RequestMapping 注解修饰的方法抛出异常时,会执行@ControllerAdvice 修饰的类中的异常处理方法。

@ControllerAdvice 注解所在的类需要进行包扫描,否则无法创建对象。

1,springmvc配置文件中添加对类的扫描

<context:component-scan base-package="com.kkb.exceptions"/>

2,定义全局异常处理类

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = TeamIdException.class)
    public ModelAndView exHandler1(Exception ex){
        ModelAndView mv=new ModelAndView();
        mv.addObject("msg",ex.getMessage());
        mv.setViewName("idError");
        return mv;
    }

    @ExceptionHandler(value = TeamNameException.class)
    public ModelAndView exHandler2(Exception ex){
        ModelAndView mv=new ModelAndView();
        mv.addObject("msg",ex.getMessage());
        mv.setViewName("nameError");
        return mv;
    }

    @ExceptionHandler(value = TeamException.class)
    public ModelAndView exHandler4(Exception ex){
        ModelAndView mv=new ModelAndView();
        mv.addObject("msg",ex.getMessage());
        mv.setViewName("nameError");
        return mv;
    }

    @ExceptionHandler(value = Exception.class)
    public ModelAndView exHandler3(Exception ex){
        ModelAndView mv=new ModelAndView();
        mv.addObject("msg",ex.getMessage());
        mv.setViewName("error");
        return mv;
    }
}

三,拦截器

1,介绍

SpringMVC 中的 拦截器( Interceptor)是非常重要的,它的主要作用是拦截指定的用户请求,并进行相应的预处理与后处理。

拦截的时间点在“处理器映射器HandlerMapping根据用户提交的请求映射出了所要执行的处理器类,并且也找到了要执行该处理器类的处理器适配器,在处理器适配器HandlerAdaptor执行处理器之前”。在处理器映射器映射出所要执行的处理器类时,已经将拦截器与处理器组合为了一个处理器执行链HandlerExecutionChain,并返回给了前端控制器。

自定义拦截器,需要实现 HandlerInterceptor 接口。而该接口中含有三个方法:

preHandle(request,response, Object handler): 

  • 该方法在处理器方法执行之前执行。其返回值为boolean,若为true,则紧接着会执行处理器方法,且 会将afterCompletion()方法放入到一个专门的方法栈中等待执行。 

postHandle(request,response, Object handler,modelAndView): 

  • 该方法在处理器方法执行之后执行。处理器方法若最终未被执行,则该方法不会执行。由于该方法是在处 理器方法执行完后执行,且该方法参数中包含 ModelAndView,所以该方法可以修改处理器方法的处理结果 数据,且可以修改跳转方向。 

afterCompletion(request,response, Object handler, Exception ex): 

  • 当 preHandle()方法返回true时,会将该方法放到专门的方法栈中,等到对请求进行响应的所工作完 成之后才执行该方法。即该方法是在前端控制器渲染(数据填充)了响应页面之后执行的,此时对 ModelAndView再操作也对响应无济于事。 
  • afterCompletion最后执行的方法,清除资源,例如在Controller方法中加入数据

2,自定义拦截器

MyInterceptor.java

package com.kkb.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyInterceptor implements HandlerInterceptor {

    // 执行时间: 控制器方法执行之前,在ModelAndView返回之前
    // 使用场景: 登录验证
    // 返回值 true : 继续执行控制器方法 表示放行    false: 不会继续执行控制器方法,表示拦截
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle-------------------");
        return true;
    }

    // 执行时间: 控制器方法执行之hou后,在ModelAndView返回之前,有机会修改返回值
    // 使用场景: 日记记录,记录登录的ip,时间
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle-------------------");
    }

    // 执行时间: 控制器方法执行之后,在ModelAndView返回之后,没有机会修改返回值
    // 使用场景: 全局资源的一些操作
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion-------------------");
    }
}

MyInterceptor2.java

package com.kkb.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyInterceptor2 implements HandlerInterceptor {

    // 执行时间: 控制器方法执行之前,在ModelAndView返回之前
    // 使用场景: 登录验证
    // 返回值 true : 继续执行控制器方法 表示放行    false: 不会继续执行控制器方法,表示拦截
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle2-------------------");
        return true;
    }

    // 执行时间: 控制器方法执行之hou后,在ModelAndView返回之前,有机会修改返回值
    // 使用场景: 日记记录,记录登录的ip,时间
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle-2------------------");
    }

    // 执行时间: 控制器方法执行之后,在ModelAndView返回之后,没有机会修改返回值
    // 使用场景: 全局资源的一些操作
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion2-------------------");
    }
}

3,配置拦截器

如果有多个拦截器的时候: (类似于嵌套的方式)

  • preHandle:按照配置前后顺序执行 
  • postHandle:按照配置前后逆序执行 
  • afterCompletion:按照配置前后逆序执行
    <!--配置拦截器-->
    <mvc:interceptors>
        <!--按顺序配置多个拦截器-->
        <mvc:interceptor>
            <mvc:mapping path="/**"/>
            <bean class="com.kkb.interceptor.MyInterceptor" id="myInterceptor"></bean>
        </mvc:interceptor>
        <mvc:interceptor>
            <mvc:mapping path="/**"/>
            <bean class="com.kkb.interceptor.MyInterceptor2" id="myInterceptor2"></bean>
        </mvc:interceptor>
        <mvc:interceptor>
            <mvc:mapping path="/**"/>
            <bean class="com.kkb.interceptor.FileInterceptor" id="fileInterceptor"></bean>
        </mvc:interceptor>
    </mvc:interceptors>

四,文件上传与下载

Spring MVC为文件上传提供了直接支持,这种支持是通过即插即用的MultipartResolver实现.

Spring中有一个MultipartResolver的实现类:CommonsMultipartResolver。 

在SpringMVC上下文中默认没有装配MultipartResolver,因此默认情况下不能处理文件上传工作。

如果想使用Spring的文件上传功能,则需要先在上下文中配置MultipartResolver。

1,文件上传

1.1 上传过程

1,在pom.xml中添加依赖

        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.3.1</version>
        </dependency>

2,springmvc.xml文件中配置MultipartResolver

    <!--文件上传-->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"></bean>

3,fileHandle.jsp页面表单

注意method="post" enctype="multipart/form-data"

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>文件操作</title>
</head>
<body>
    <form action="/file/upload" method="post" enctype="multipart/form-data">
        请选择文件:<input type="file" name="myFile" /><br/>
        <button type="submit">上传文件</button>
    </form>

</body>
</html>

4,配置java代码(注意要创建文件夹保存上传之后的文件)

package com.kkb.controller;

import org.apache.commons.io.FileUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.UUID;

@Controller
@RequestMapping("file")
public class FileController {

    /**
     * 文件上传
     * @param myFile
     * @param request
     * @return
     */
    @RequestMapping("upload")
    public String upload(@RequestParam("myFile") MultipartFile myFile, HttpServletRequest request){ 
        // 获取文件的原始名称(用于获得后缀名) d:\te.aa\txcat.jpg
        String originalFilename = myFile.getOriginalFilename();
        // 实际开发中,一般都要将文件重新名称进行存储
        // 存储到服务器的文件名称=随机的字符串+根据实际名称获取到源文件的后缀
        String fileName= UUID.randomUUID().toString().replace("-","") +originalFilename.substring(originalFilename.lastIndexOf("."));
        System.out.println(fileName);
        // 文件存储路径
        String realPath = request.getServletContext().getRealPath("/uploadFile")+"/";
        try {
            myFile.transferTo(new File(realPath+fileName));//真正的文件上传到服务器指定的位置
            System.out.println("上传成功!"+realPath+fileName);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "ok";
    }

    // springmvc中不推荐直接访问jsp页面,所以这里进行转发(默认方式)
    @RequestMapping("hello")
    public String hello(){
        return "fileHandle";
    }
}

1.2 限制文件大小和类型

1, springmvc.xml文件中配置CommonsMultipartResolver的属性以及文件类型拦截器

tomcat中对文件的可上传大小做了限制

    <!--文件上传-->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <!--文件上传的大小限制 以字节B为单位  5m :1024*1024*5  1M=1024KB 1KB=1024B-->
        <property name="maxUploadSize" value="5242880"/>
        <property name="defaultEncoding" value="utf-8"/>
    </bean>
    <!--配置拦截器-->
    <mvc:interceptors>
        <!--按顺序配置多个拦截器-->
        <mvc:interceptor>
            <mvc:mapping path="/**"/>
            <bean class="com.kkb.interceptor.FileInterceptor" id="fileInterceptor"></bean>
        </mvc:interceptor>
    </mvc:interceptors>

2,编写FileInterceptor

package com.kkb.interceptor;

import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Iterator;
import java.util.Map;

public class FileInterceptor implements HandlerInterceptor {
    /**
     * 在文件上传之前判断文件后缀是否合法
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 判断是否是文件上传的请求
        boolean flag=true;
        if(request instanceof MultipartHttpServletRequest){
            MultipartHttpServletRequest multipartRequest= (MultipartHttpServletRequest) request;
            Map<String, MultipartFile> fileMap = multipartRequest.getFileMap();
            // 遍历文件
            Iterator<String> iterator = fileMap.keySet().iterator();
            while(iterator.hasNext()){
                String key = iterator.next();
                MultipartFile file = multipartRequest.getFile(key);
                String originalFilename = file.getOriginalFilename();
                String hz = originalFilename.substring(originalFilename.lastIndexOf("."));
                // 判断后缀是否合法
                if(!hz.toLowerCase().equals(".png") && !hz.toLowerCase().equals(".jpg")){
                    request.getRequestDispatcher("/jsp/fileTypError.jsp").forward(request,response);
                    flag=false;
                }
            }
        }
        return flag;
    }
}

3,编写fileTypeError.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>fileError</title>
</head>
<body>
    <h1>文件上传的类型有误!后缀必须是.png或者是.jpg</h1>
</body>
</html>

2,文件下载

1,前端页面

    <form action="/file/download" method="post" enctype="multipart/form-data">
        <button type="submit">下载图片--4e27abf2c3724985a0877599773143c6.jpg</button>
    </form>

2,在FileController中添加方法

    @RequestMapping("download")
    public ResponseEntity<byte[]> download(HttpServletRequest request) throws IOException {
        // 指定文件的路径
        String path=request.getServletContext().getRealPath("/uploadFile")+"/4e27abf2c3724985a0877599773143c6.jpg";
        // 创建响应 的头信息的对象
        HttpHeaders headers=new HttpHeaders();
        // 标记以流的方式作出响应
        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        // 以附件的形式响应给用户
        headers.setContentDispositionFormData("attachment",URLEncoder.encode("4e27abf2c3724985a0877599773143c6.jpg","utf-8"));
        File file=new File(path);
        ResponseEntity<byte[]> resp=new ResponseEntity<>(FileUtils.readFileToByteArray(file),headers,HttpStatus.CREATED);
        return resp;
    }

五,RESTful介绍

1,REST概念

REST(英文:Representational State Transfer,简称REST,意思:表述性状态转换,描述了一个架构样式的网络系统,比如web应用)。

它是一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件,它主要用于客户端和服务端交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。

它本身并没有什么使用性,其核心价值在于如何设计出符合REST风格的网络接口。

2,RESTful概念

REST指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是RESTful。

RESTful的特性:

1,资源(Resources):

  • 互联网所有的事物都可以被抽象为资源 。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的存在。
  • 可以用一个URI(统一资源定位符)指向它,每种资源对应一个特性的URI。
  • 要获取这个资源,访问它的URI就可以,因此URI即为每一个资源的独一无二的识别符。

2,表现层(Representation):

  • 把资源具体呈现出来的形式,叫做它的表现层(Representation)。比如,文本可以用txt格式表现,也可以用HTML格式、XML格式、JSON格式表现,甚至可以采用二进制格式。

3,状态转换(State Transfer):

每发出一个请求,就代表了客户端和服务器的一次交互过程。

HTTP协议,是一个无状态协议,即所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生“状态转换”(State Transfer)。而这种转换是建立在表现层之上的,所以就是“表现层状态转换”。

具体来说就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。他们分别对应四种基本操作:GET用来获取资源,POST用来新建资源,PUT用来更新资源,DELETE用来删除资源。

原来的操作资源的方式:

  • http://localhost:8080/getExpress.do?id=1 
  • http://localhost:8080/saveExpress.do 
  • http://localhost:8080/updateExpress.do 
  • http://localhost:8080/deleteExpress.do?id=1

原来用的方式当然没有问题,但是如果有更简洁的方式就更好了,此时就是RESTful风格。

使用RESTful操作资源:

  • GET /expresses #查询所有的快递信息列表
  • GET /express/1006 #查询一个快递信息
  • POST /express #新建一个快递信息
  • PUT /express/1006 #更新一个快递信息(全部更新)
  • PATCH /express/1006 #更新一个快递信息(部分更新)
  • DELETE /express/1006 #删除一个快递信息

六,RESTful风格的API设计原则

1,API设计/URL设计

1.1 动词加宾语

RESTful 的核心思想就是客户端的用户发出的数据操作指令都是"动词 + 宾语"的结构。例如GET/expresses 这个命令,GET是动词,/expresses 是宾语。

动词通常就是五种 HTTP 方法,对应 CRUD 操作。 

  • GET:读取(Read) 
  • POST:新建(Create) 
  • PUT:更新(Update) 
  • PATCH:更新(Update),通常是部分更新 
  • DELETE:删除(Delete) 

PS: 

  • 1、根据 HTTP 规范,动词一律大写。 
  • 2、一些代理只支持POST和GET方法, 为了使用这些有限方法支持RESTful API,需要一种办法覆盖 http原来的方法。使用订制的HTTP头 X-HTTP-Method-Override 来覆盖POST 方法.

1.2 宾语必须是名词

宾语就是 API 的 URL,是 HTTP 动词作用的对象。它应该是名词,不能是动词。比如,/expresses 这个 URL 就是正确的。

以下这些URL都是不推荐的,因为带上了动词,不是推荐写法。

  • /getAllExpresses
  • /getExpress
  • /createExpress
  • /deleteAllExpress

不要混淆名词单数和复数,为了保持简单,只对所有资源使用复数(查询所有)。

1.3 避免多级URL

如果资源中有多级分类,也不建议写出多级的URL。例如要获取球队中某个队员,有人可能这么写:

GET /team/1001/player/1005

这种写法的语义不够明确,所以推荐使用查询字符串做后缀,改写为

GET /team/1001?player=1005.

七,HTTP状态码

客户端的用户发起的每一次请求,服务器都必须给出响应。响应包括 HTTP 状态码和数据两部分。

HTTP 状态码就是一个三位数,分成五个类别。这五大类总包含了100多种状态码。覆盖了绝大部分可能遇到的情况。每一种状态码都有标准的(或者约定的)解释,客户端只需查看状态码,就可以判断出发生了什么情况,所以服务器应该返回尽可能精确的状态码。

五类状态码分别如下: 

  • 1xx:相关信息 
  • 2xx:操作成功 
  • 3xx:重定向 
  • 4xx:客户端错误 
  • 5xx:服务器错误 

PS:API 不需要1xx状态码,所以这个类别直接忽略。

1,状态码2xx

200 状态码表示操作成功,但是不同的方法可以返回更精确的状态码。

  • GET: 200 OK 表示一切正常
  • POST: 201 Created 表示新的资源已经成功创建
  • PUT: 200 OK
  • PATCH: 200 OK
  • DELETE: 204 No Content 表示资源已经成功删除

2,状态码3xx

API 用不到 301 状态码(永久重定向)和 302 状态码(暂时重定向, 307 也是这个含义),因为它们可以由应用级别返回,浏览器会直接跳转,API 级别可以不考虑这两种情况。

API 用到的 3xx 状态码,主要是 303 See Other ,表示参考另一个 URL。它与 302 和 307 的含义一样,也是"暂时重定向",区别在于 302 和 307 用于 GET 请求,而 303 用于 POST 、 PUT 和 DELETE 请求。

收到 303 以后,浏览器不会自动跳转,而会让用户自己决定下一步怎么办。

我们只需要关注一下304状态码就可以了 

  • 304 : Not Modified 客户端使用缓存数据

3,状态码4xx

4xx 状态码表示客户端错误。

  • 400 Bad Request:服务器不理解客户端的请求,未做任何处理。
  • 401 Unauthorized:用户未提供身份验证凭据,或者没有通过身份验证。
  • 403 Forbidden:用户通过了身份验证,但是不具有访问资源所需的权限。
  • 404 Not Found:所请求的资源不存在,或不可用。
  • 405 Method Not Allowed:用户已经通过身份验证,但是所用的 HTTP 方法不在他的权限之内。
  • 410 Gone:所请求的资源已从这个地址转移,不再可用。
  • 415 Unsupported Media Type:客户端要求的返回格式不支持。比如,API 只能返回 JSON 格式,但是 客户端要求返回 XML 格式。
  • 422 Unprocessable Entity :客户端上传的附件无法处理,导致请求失败。
  • 429 Too Many Requests:客户端的请求次数超过限额。

4,状态码5xx

5xx 状态码表示服务端错误。一般来说,API 不会向用户透露服务器的详细信息,所以只要两个状态码就够了。

  • 500 Internal Server Error:客户端请求有效,服务器处理时发生了意外。 
  • 503 Service Unavailable:服务器无法处理请求,一般用于网站维护状态。

八,服务器响应

服务器返回的信息一般不推荐纯文本,而是建议大家选择JSON 对象,因为这样才能返回标准的结构化数据。

所以,服务器回应的 HTTP 头的 Content-Type 属性要设为 application/json 。客户端请求时,也要明确告诉服务器,可以接受 JSON 格式,即请求的 HTTP 头的 ACCEPT 属性也要设成application/json 。

当发生错误的时候,除了返回状态码之外,也要返回错误信息。所以我们可以自己封装要返回的信息。

九,RESTful案例

1,查询

1,前端页面restful.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>restful</title>
    <script src="/js/jquery-1.11.1.js"></script>
</head>
<body>
    <form id="myForm" action="" method="post">
        球队ID:<input type="text" name="teamId" id="teamId" /><br/>
        球队名称:<input type="text" name="teamName" /><br/>
        球队位置:<input type="text" name="location" /><br/>
        <button type="button" id="btnGetAll">查询所有GET</button>
        <button type="button" id="btnGetOne">查询单个GET</button>
        <button type="button" id="btnPost">添加POST</button>
        <button type="button" id="btnPut">更新PUT</button>
        <button type="button" id="btnDel">删除DELETE</button>
    </form>
    <p id="showResult"></p>
</body>
</html>
<script>
    //页面加载完毕之后给按钮绑定事件
    $(function () {

        //给 查询所有GET 按钮绑定单击事件
        $("#btnGetAll").click(function () {
            //发起异步请求
            $.ajax({
                type: "GET",
                url: "/teams", //RESTful风格的API定义
                data: "",
                dataType:"json",
                success: function(list) {
                    var str = "";
                    for (var i = 0; i < list.length; i++) {
                        var obj = list[i];
                        str += obj.teamId + "----" + obj.teamName + "----" + obj.location + "<br/>";
                    }
                    $("#showResult").html(str);
                }
            });
        });

        //给 查询单个GET 按钮绑定单击事件
        $("#btnGetOne").click(function () {
            //发起异步请求
            $.ajax({
                type: "GET",
                url: "/team/"+$("#teamId").val(), //RESTful风格的API定义
                data: "",
                dataType:"json",
                success: function(obj){
                    if(obj==""){
                        $("#showResult").html("没有复合条件的数据!");
                    }else {
                        $("#showResult").html(obj.teamId + "----" + obj.teamName + "----" + obj.location + "<br/>");
                    }
                }
            });
        });
    });
</script>

2,编写控制器RestfulController.java

package com.kkb.controller;

import com.kkb.pojo.Team;
import com.kkb.vo.AjaxResultVO;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.ArrayList;
import java.util.List;

/**
 * ClassName: RestfulController
 * restful风格的控制器
 * @author wanglina
 * @version 1.0
 */
@Controller
public class RestfulController {

    private static List<Team> teamList;
    static {
        teamList=new ArrayList<>(3);
        for(int i=1;i<=3;i++){
            Team team=new Team();
            team.setTeamId(1000+i);
            team.setTeamName("湖人"+i);
            team.setLocation("洛杉矶"+i);
            teamList.add(team);
        }
    }

    /**
     * 查询所有的球队
     * @return
     */
    @RequestMapping(value = "teams",method = RequestMethod.GET)
    @ResponseBody
    public List<Team> getAll(){
        System.out.println("查询所有GET--发起的请求---------");
        return teamList;
    }

    /**
     * 根据id 查询单个的球队
     * @return
     */
    @RequestMapping(value = "team/{id}",method = RequestMethod.GET)
    @ResponseBody
    public Team getOne(@PathVariable("id")int id){
        System.out.println("查询单个GET--发起的请求---------");
        for (Team team : teamList) {
            if(team.getTeamId()==id){
                return team;
            }
        }
        return null;
    }

    // 发送hello请求时可以跳转到restful.jsp页面
    @RequestMapping("hello")
    public String hello(){
        return "restful";
    }
}

2,添加

1,在前端页面中给btnPost 按钮绑定单击事件

        //给 添加POST 按钮绑定单击事件
        $("#btnPost").click(function () {
            alert($("#myForm").serialize());
            //发起异步请求
            $.ajax({
                type: "POST",
                url: "/team", //RESTful风格的API定义
                data: $("#myForm").serialize(),//表单的所有数据以?&形式追加在URL后面 /team?teamId=1006&teamName=kuaichuan&location=las
                dataType:"json",
                success: function(msg){
                    //alert( "Data Saved: " + msg ); 
                    $("#showResult").html(msg);
                }
            });
        });

2,控制器中添加方法

/**
 * 添加一个球队 
 */ 

@RequestMapping(value = "team",method = RequestMethod.POST) 
@ResponseBody 
public String add(Team team){ 
    System.out.println("添加POST--发起的请求---------"); 
    teamList.add(team); 
    return "201"; 
} 

3,更新

1,前端页面上脚本中添加如下内容:

type仍使用POST是因为tomcat只对GET和POST的data数据进行封装,其他方法不会处理。

可以在data字段中添加_method=PUT并在web.xml中配置过滤器

如果只是在url中传递参数,则不需要考虑上述问题

$("#btnPut").click(function(){ 
    alert($("#myForm").serialize()); 
    $.ajax({ 
        type: "POST", 
        url: "/team/"+$("#teamId").val(), 
        data: $("#myForm").serialize()+"&_method=PUT", 
        dataType:"json", 
        //headers:{"X-HTTP-Method-Override":"GET"}, 
        success: function(msg){ 
            $("#showResult").html(msg); 
        } 
    }); 
});

2,控制器中添加方法

这里的method仍使用PUT方法

@RequestMapping(value = "/team/{id}",method = RequestMethod.PUT) 
@ResponseBody 
public String update(@PathVariable("id") int id,Team newTeam){ 
    System.out.println(id); 
    for (Team team : teamList) { 
        if(team.getTeamId()==id){ 
            team.setTeamName(newTeam.getTeamName()); 
            team.setLocation(newTeam.getLocation()); 
            return "204"; 
        } 
    }
    return "404"; 
} 

4,删除

1,前端页面上脚本中添加如下内容:

        //给 删除DELETE 按钮绑定单击事件
        $("#btnDel").click(function () {
            alert($("#myForm").serialize());
            //发起异步请求
            $.ajax({
                type: "POST",
                url: "/team/"+$("#teamId").val(), //RESTful风格的API定义
                data: "_method=DELETE",
                success: function(msg){
                    $("#showResult").html(msg);
                }
            });
        });

2,控制器中添加方法

/** 
 * 根据ID删除一个球队 
 */ 
@RequestMapping(value = "team/{id}",method = RequestMethod.DELETE) 
@ResponseBody 
public String del(@PathVariable("id")int id){ 
    System.out.println("删除DELETE--发起的请求---------"); 
    for (Team team1 : teamList) { 
        if(team1.getTeamId()==id){ 
            teamList.remove(team1); 
        return "204"; 
        } 
    }
    return "500"; 
}

5,RESTful风格的更新和删除遇到的问题

在Ajax中,采用Restful风格PUT和DELETE请求传递参数无效,传递到后台的参数值为null

5.1 产生的原因

Tomcat封装请求参数的过程: 

  • 1.将请求体中的数据,封装成一个map 
  • 2.request.getParameter(key)会从这个map中取值 
  • 3.SpringMvc封装POJO对象的时候,会把POJO中每个属性的值进行request.getParamter(); AJAX发送PU或者DELETE请求时,请求体中的数据通过request.getParamter()拿不到。 Tomcat一检测到是PUT或者DELETE就不会封装请求体中的数据为map,只有POST形式的请求才封装请求为 map。

5.2 解决方案

1,前端页面中的ajax发送请求的时候在url中加 &_method=”PUT” 或者 &_method=”DELETE” 即可

2,web.xml中配置

配置的时候多个过滤器需要注意顺序

    <!-- 使用Rest风格的URI 将页面普通的post请求转为指定的delete或者put请求
原理:在Aajx中发送post请求后,带_method参数,将其修改为PUT,或者DELETE请求-->
    <filter>
        <filter-name>httpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>httpMethodFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

 

十,自己封装响应结果

一般情况下,将和数据库表相关的实体类存放在pojo目录下,和业务相关的实体类放在vo目录下(value object)

1,自己封装返回的实体类

package com.kkb.vo;

import java.util.List;

/**
 * ClassName: AjaxResultVO
 * 自己封装的返回的结果类
 * @version 1.0
 */
public class AjaxResultVO<T> {

    private Integer code;//返回的状态码
    private String msg;//返回的信息(一般错误的信息或者异常的信息)
    private List<T> list; //返回的数据有可能是集合
    private T obj;//返回的数据有可能是对象

    public AjaxResultVO() {
        code = 200;
        msg = "OK";
        list = null;
        obj = null;

    }

    public AjaxResultVO(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public AjaxResultVO(Integer code, String msg, List<T> list) {
        this.code = code;
        this.msg = msg;
        this.list = list;
    }

    public AjaxResultVO(Integer code, String msg, T obj) {
        this.code = code;
        this.msg = msg;
        this.obj = obj;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public List<T> getList() {
        return list;
    }

    public void setList(List<T> list) {
        this.list = list;
    }

    public T getObj() {
        return obj;
    }

    public void setObj(T obj) {
        this.obj = obj;
    }
}

2,修改控制器的返回值

package com.kkb.controller;

import com.kkb.pojo.Team;
import com.kkb.vo.AjaxResultVO;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.ArrayList;
import java.util.List;

/**
 * ClassName: RestfulController
 * restful风格的控制器
 * @version 1.0
 */
@Controller
public class RestfulController {

    private static List<Team> teamList;
    static {
        teamList=new ArrayList<>(3);
        for(int i=1;i<=3;i++){
            Team team=new Team();
            team.setTeamId(1000+i);
            team.setTeamName("湖人"+i);
            team.setLocation("洛杉矶"+i);
            teamList.add(team);
        }
    }

    /**
     * 根据ID删除一个球队
     */
    @RequestMapping(value = "team/{id}",method = RequestMethod.DELETE)
    @ResponseBody
    public AjaxResultVO<Team> del(@PathVariable("id")int id){
        System.out.println("删除DELETE--发起的请求---------");
        for (Team team1 : teamList) {
            if(team1.getTeamId()==id){
                teamList.remove(team1);
                return new AjaxResultVO<Team>();
            }
        }
        return new AjaxResultVO<Team>(500,"要删除的ID不存在~~~");
    }

    /**
     * 根据ID更新一个球队
     */
    @RequestMapping(value = "team/{id}",method = RequestMethod.PUT)
    @ResponseBody
    public AjaxResultVO<Team> add(@PathVariable("id")int id, Team team){
        System.out.println("更新PUT--发起的请求---------");
        for (Team team1 : teamList) {
            if(team1.getTeamId()==id){
                team1.setLocation(team.getLocation());
                team1.setTeamName(team.getTeamName());
                return new AjaxResultVO<Team>();
            }
        }
        return new AjaxResultVO<Team>(500,"要更新的ID不存在~~~");
    }
    /**
     * 添加一个球队
     */
    @RequestMapping(value = "team",method = RequestMethod.POST)
    @ResponseBody
    public AjaxResultVO<Team> add(Team team){
        System.out.println("添加POST--发起的请求---------");
        teamList.add(team);
        return new AjaxResultVO<>(200,"OK");
    }

    /**
     * 查询所有的球队
     * @return
     */
    @RequestMapping(value = "teams",method = RequestMethod.GET)
    @ResponseBody
    public AjaxResultVO<Team> getAll(){
        System.out.println("查询所有GET--发起的请求---------");
        return new AjaxResultVO<>(200,"OK",teamList);
    }

    /**
     * 根据id 查询单个的球队
     * @return
     */
    @RequestMapping(value = "team/{id}",method = RequestMethod.GET)
    @ResponseBody
    public AjaxResultVO<Team> getOne(@PathVariable("id")int id){
        System.out.println("查询单个GET--发起的请求---------");
        for (Team team : teamList) {
            if(team.getTeamId()==id){
                return new AjaxResultVO<>(200,"OK",team);
            }
        }
        return null;
    }

    @RequestMapping("hello")
    public String hello(){
        return "restful";
    }
}

3,修改前端页面

success方法中统一用vo接收参数

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>restful</title>
    <script src="/js/jquery-1.11.1.js"></script>
</head>
<body>
    <form id="myForm" action="" method="post">
        球队ID:<input type="text" name="teamId" id="teamId" /><br/>
        球队名称:<input type="text" name="teamName" /><br/>
        球队位置:<input type="text" name="location" /><br/>
        <button type="button" id="btnGetAll">查询所有GET</button>
        <button type="button" id="btnGetOne">查询单个GET</button>
        <button type="button" id="btnPost">添加POST</button>
        <button type="button" id="btnPut">更新PUT</button>
        <button type="button" id="btnDel">删除DELETE</button>
    </form>
    <p id="showResult"></p>
</body>
</html>
<script>
    //页面加载完毕之后给按钮绑定事件
    $(function () {

        //给 删除DELETE 按钮绑定单击事件
        $("#btnDel").click(function () {
            alert($("#myForm").serialize());
            //发起异步请求
            $.ajax({
                type: "POST",
                url: "/team/"+$("#teamId").val(), //RESTful风格的API定义
                data: "_method=DELETE",
                success: function(vo){
                   if(vo.code==200){
                       $("#showResult").html("删除成功!");
                   }else {
                       $("#showResult").html(vo.msg);
                   }
                }
            });
        });

        //给 更新PUT 按钮绑定单击事件
        $("#btnPut").click(function () {
            alert($("#myForm").serialize());
            //发起异步请求
            $.ajax({
                type: "POST",
                url: "/team/"+$("#teamId").val(), //RESTful风格的API定义
                data: $("#myForm").serialize()+"&_method=PUT",//表单的所有数据以?&形式追加在URL后面 /team?teamId=1006&teamName=kuaichuan&location=las
                dataType:"json",
                success: function(vo){
                    if(vo.code==200){
                        $("#showResult").html("更新成功!");
                    }else {
                        $("#showResult").html(vo.msg);
                    }
                }
            });
        });

        //给 添加POST 按钮绑定单击事件
        $("#btnPost").click(function () {
            alert($("#myForm").serialize());
            //发起异步请求
            $.ajax({
                type: "POST",
                url: "/team", //RESTful风格的API定义
                data: $("#myForm").serialize(),//表单的所有数据以?&形式追加在URL后面 /team?teamId=1006&teamName=kuaichuan&location=las
                dataType:"json",
                success: function(vo){
                    if(vo.code==200){
                        $("#showResult").html("添加成功!");
                    }else {
                        $("#showResult").html(vo.msg);
                    }
                }
            });
        });

        //给 查询所有GET 按钮绑定单击事件
        $("#btnGetAll").click(function () {
            //发起异步请求
            $.ajax({
                type: "GET",
                url: "/teams", //RESTful风格的API定义
                data: "",
                dataType:"json",
                success: function(vo) {
                    if (vo.code == 200) {
                        var list=vo.list;
                        var str = "";
                        for (var i = 0; i < list.length; i++) {
                            var obj = list[i];
                            str += obj.teamId + "----" + obj.teamName + "----" + obj.location + "<br/>";
                        }
                        $("#showResult").html(str);
                    }else{
                        $("#showResult").html(vo.msg);
                    }
                }
            });
        });

        //给 查询单个GET 按钮绑定单击事件
        $("#btnGetOne").click(function () {
            //发起异步请求
            $.ajax({
                type: "GET",
                url: "/team/"+$("#teamId").val(), //RESTful风格的API定义
                data: "",
                dataType:"json",
                success: function(vo){
                    if(vo.code==200){
                        var obj=vo.obj;
                        if(obj==""){
                            $("#showResult").html("没有复合条件的数据!");
                        }else {
                            $("#showResult").html(obj.teamId + "----" + obj.teamName + "----" + obj.location + "<br/>");
                        }
                    }else{
                        $("#showResult").html(vo.msg);
                    }
                }
            });
        });
    });
</script>