Bootstrap

Spring Web MVC(详解中)

Spring MVC(中)

RESTFul风格设计

RESTFul风格概述

在这里插入图片描述

RESTful(Representational State Transfer)是一种软件架构风格,用于设计网络应用程序和服务之间的通信。它是一种基于标准 HTTP 方法的简单和轻量级的通信协议,广泛应用于现代的Web服务开发。

通过遵循 RESTful 架构的设计原则,可以构建出易于理解、可扩展、松耦合和可重用的 Web 服务。RESTful API 的特点是简单、清晰,并且易于使用和理解,它们使用标准的 HTTP 方法和状态码进行通信,不需要额外的协议和中间件。

RESTful 架构通常用于构建 Web API,提供数据的传输和操作。它可以用于各种应用场景,包括客户端-服务器应用、单页应用(SPA)、移动应用程序和微服务架构等。

总而言之,RESTful 是一种基于 HTTP 和标准化的设计原则的软件架构风格,用于设计和实现可靠、可扩展和易于集成的 Web 服务和应用程序!
在这里插入图片描述
学习RESTful设计原则可以帮助我们更好去设计HTTP协议的API接口!!

RESTFul风格特点

  • 每一个URI代表1种资源(URI 是名词);
  • 客户端使用GET、POST、PUT、DELETE 4个表示操作方式的动词对服务端资源进行操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源;
  • 资源的表现形式是XML或者JSON
  • 客户端与服务端之间的交互在请求之间是无状态的,从客户端到服务端的每个请求都必须包含理解请求所必需的信息。

RESTFul风格设计规范

HTTP协议请求方式要求

REST 风格主张在项目设计、开发过程中,具体的操作符合HTTP协议定义的请求方式的语义

操作请求方式
查询操作GET
保存操作POST
删除操作DELETE
更新操作PUT

URL路径风格要求

REST风格下每个资源都应该有一个唯一的标识符,例如一个 URI(统一资源标识符)或者一个 URL(统一资源定位符)。资源的标识符应该能明确地说明该资源的信息,同时也应该是可被理解和解释的!

使用URL+请求方式确定具体的动作,他也是一种标准的HTTP协议请求!

操作传统风格REST 风格
保存/CRUD/saveEmpURL 地址:/CRUD/emp 请求方式:POST
删除/CRUD/removeEmp?empId=2URL 地址:/CRUD/emp/2 请求方式:DELETE
更新/CRUD/updateEmpURL 地址:/CRUD/emp 请求方式:PUT
查询/CRUD/editEmp?empId=2URL 地址:/CRUD/emp/2 请求方式:GET

总结

根据接口的具体动作,选择具体的HTTP协议请求方式

路径设计从原来携带动标识,改成名词,对应资源的唯一标识即可!

RESTFul风格好处

  1. 含蓄,安全

    使用问号键值对的方式给服务器传递数据太明显,容易被人利用来对系统进行破坏。使用 REST 风格携带数据不再需要明显的暴露数据的名称。

  2. 风格统一

    URL 地址整体格式统一,从前到后始终都使用斜杠划分各个单词,用简单一致的格式表达语义。

  3. 无状态

    在调用一个接口(访问、操作资源)的时候,可以不用考虑上下文,不用考虑当前状态,极大的降低了系统设计的复杂度。

  4. 严谨,规范

    严格按照 HTTP1.1 协议中定义的请求方式本身的语义进行操作。

  5. 简洁,优雅

    过去做增删改查操作需要设计4个不同的URL,现在一个就够了。

  6. 丰富的语义

    通过 URL 地址就可以知道资源之间的关系。它能够把一句话中的很多单词用斜杠连起来,反过来说就是可以在 URL 地址中用一句话来充分表达语义。

操作传统风格REST 风格
保存/CRUD/saveEmpURL 地址:/CRUD/emp 请求方式:POST
删除/CRUD/removeEmp?empId=2URL 地址:/CRUD/emp/2 请求方式:DELETE
更新/CRUD/updateEmpURL 地址:/CRUD/emp 请求方式:PUT
查询/CRUD/editEmp?empId=2URL 地址:/CRUD/emp/2 请求方式:GET

RESTFul风格实战

需求分析
  • 数据结构: User {id 唯一标识,name 用户名,age 用户年龄}
  • 功能分析
    • 用户数据分页展示功能(条件:page 页数 默认1,size 每页数量 默认 10)
    • 保存用户功能
    • 根据用户id查询用户详情功能
    • 根据用户id更新用户数据功能
    • 根据用户id删除用户数据功能
    • 多条件模糊查询用户功能(条件:keyword 模糊关键字,page 页数 默认1,size 每页数量 默认 10)
RESTFul风格接口设计

接口设计

功能接口和请求方式请求参数返回值
分页查询GET /userpage=1&size=10 param{ 响应数据 }
用户添加POST /user{ user 数据 }{响应数据}
用户详情GET /user/1路径参数{响应数据}
用户更新PUT /user{ user 更新数据}{响应数据}
用户删除DELETE /user/1路径参数{响应数据}
条件模糊GET /user/searchpage=1&size=10&keywork=关键字{响应数据}

问题讨论

为什么查询用户详情,就使用路径传递参数,多条件模糊查询,就使用请求参数传递?

误区:restful风格下,不是所有请求参数都是路径传递!可以使用其他方式传递!

在 RESTful API 的设计中,路径和请求参数和请求体都是用来向服务器传递信息的方式。

  • 对于查询用户详情,使用路径传递参数是因为这是一个单一资源的查询,即查询一条用户记录。使用路径参数可以明确指定所请求的资源,便于服务器定位并返回对应的资源,也符合 RESTful 风格的要求。
  • 而对于多条件模糊查询,使用请求参数传递参数是因为这是一个资源集合的查询,即查询多条用户记录。使用请求参数可以通过组合不同参数来限制查询结果,路径参数的组合和排列可能会很多,不如使用请求参数更加灵活和简洁。
    此外,还有一些通用的原则可以遵循:
  • 路径参数应该用于指定资源的唯一标识或者 ID,而请求参数应该用于指定查询条件或者操作参数。
  • 请求参数应该限制在 10 个以内,过多的请求参数可能导致接口难以维护和使用。
  • 对于敏感信息,最好使用 POST 和请求体来传递参数。
后台接口实现

准备用户实体类:

package com.gj.pojo;

/**
 * projectName: com.gj.pojo
 * 用户实体类
 */
public class User {

    private Integer id;
    private String name;

    private Integer age;

    public Integer getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

准备用户Controller:

/**
 * projectName: com.gj.controller
 *
 * description: 用户模块的控制器
 */
@RequestMapping("user")
@RestController
public class UserController {

    /**
     * 模拟分页查询业务接口
     */
    @GetMapping
    public Object queryPage(@RequestParam(name = "page",required = false,defaultValue = "1")int page,
                            @RequestParam(name = "size",required = false,defaultValue = "10")int size){
        System.out.println("page = " + page + ", size = " + size);
        System.out.println("分页查询业务!");
        return "{'status':'ok'}";
    }


    /**
     * 模拟用户保存业务接口
     */
    @PostMapping
    public Object saveUser(@RequestBody User user){
        System.out.println("user = " + user);
        System.out.println("用户保存业务!");
        return "{'status':'ok'}";
    }

    /**
     * 模拟用户详情业务接口
     */
    @PostMapping("/{id}")
    public Object detailUser(@PathVariable Integer id){
        System.out.println("id = " + id);
        System.out.println("用户详情业务!");
        return "{'status':'ok'}";
    }


    /**
     * 模拟用户更新业务接口
     */
    @PutMapping
    public Object updateUser(@RequestBody User user){
        System.out.println("user = " + user);
        System.out.println("用户更新业务!");
        return "{'status':'ok'}";
    }


    /**
     * 模拟条件分页查询业务接口
     */
    @GetMapping("search")
    public Object queryPage(@RequestParam(name = "page",required = false,defaultValue = "1")int page,
                            @RequestParam(name = "size",required = false,defaultValue = "10")int size,
                            @RequestParam(name = "keyword",required= false)String keyword){
        System.out.println("page = " + page + ", size = " + size + ", keyword = " + keyword);
        System.out.println("条件分页查询业务!");
        return "{'status':'ok'}";
    }
}

基于RESTFul风格练习(前后端分离模式)

案例功能和接口分析

功能预览

在这里插入图片描述

接口分析

学习计划查询

/* 
需求说明
    查询全部数据页数据
请求uri
    schedule
请求方式 
    get   
响应的json
    {
        "code":200,
        "flag":true,
        "data":[
            {id:1,title:'学习java',completed:true},
            {id:2,title:'学习html',completed:true},
            {id:3,title:'学习css',completed:true},
            {id:4,title:'学习js',completed:true},
            {id:5,title:'学习vue',completed:true}
        ]
    }
*/

学习计划删除

/* 
需求说明
    根据id删除日程
请求uri
    schedule/{id}
请求方式 
    delete
响应的json
    {
        "code":200,
        "flag":true,
        "data":null
    }
*/

学习计划保存

/* 
需求说明
    增加日程
请求uri
    schedule
请求方式 
    post
请求体中的JSON
    {
        title: '',
        completed: false
    }
响应的json
    {
        "code":200,
        "flag":true,
        "data":null
    }
*/

学习计划修改

/* 
需求说明
    根据id修改数据
请求uri
    schedule
请求方式 
    put
请求体中的JSON
    {
        id: 1,
        title: '',
        completed: false
    }
响应的json
    {
        "code":200,
        "flag":true,
        "data":null
    }
*/

工程项目准备

前端项目搭建

安装node和npm

  • node和npm简介

    Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,可以使 JavaScript 运行在服务器端。

    NPM全称Node Package Manager,是Node.js包管理工具,是全球最大的模块生态系统,里面所有的模块都是开源免费的;也是Node.js的包管理工具!

  • node和npm安装

    安装Nodejs,自动安装npm包管理工具!

    1. 打开官网https://nodejs.org/en下载对应操作系统的 LTS 版本。(资料中已有node安装包)

    2. 双击安装包进行安装,安装过程中遵循默认选项即可(或者参照https://www.runoob.com/nodejs/nodejs-install-setup.html )。安装完成后,可以在命令行终端输入 node -vnpm -v 查看 Node.js 和 npm 的版本号。
      在这里插入图片描述

  • npm镜像和版本升级

    • 配置阿里镜像
    npm config set registry https://registry.npmmirror.com
    
    • 查看镜像配置结果
    npm config get registry
    
    • 升级npm版本
    npm install -g [email protected]
    
  • 使用npm安装项目依赖

    使用cmd黑窗口,进入前端工程项目文件夹下(切记:进入到package.json同层文件夹下,运行一下命令)

    npm i
    
  • 启动前端程序

    npm run dev
    
后端项目搭建

数据库怎么办?使用HashMap模拟,所以不涉及和MyBatis、Spring的整合!

搭建后台项目

pom.xml

<properties>
    <spring.version>6.0.6</spring.version>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
    <!-- springioc相关依赖  -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>

    <!-- web相关依赖  -->
    <!-- 在 pom.xml 中引入 Jakarta EE Web API 的依赖 -->
    <!--
        在 Spring Web MVC 6 中,Servlet API 迁移到了 Jakarta EE API,因此在配置 DispatcherServlet 时需要使用
         Jakarta EE 提供的相应类库和命名空间。错误信息 “‘org.springframework.web.servlet.DispatcherServlet’
         is not assignable to ‘javax.servlet.Servlet,jakarta.servlet.Servlet’” 表明你使用了旧版本的
         Servlet API,没有更新到 Jakarta EE 规范。
    -->
    <dependency>
        <groupId>jakarta.platform</groupId>
        <artifactId>jakarta.jakartaee-web-api</artifactId>
        <version>9.1.0</version>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.15.0</version>
    </dependency>
    <!-- springwebmvc相关依赖  -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${spring.version}</version>
    </dependency>

</dependencies>

准备实体类

/**
 * projectName: com.gj.pojo
 *
 * description: 任务实体类
 */
public class Schedule {

    private Integer id;
    private String title;

    private Boolean completed;

    public Schedule() {
    }

    public Schedule(Integer id, String title, Boolean completed) {
        this.id = id;
        this.title = title;
        this.completed = completed;
    }

    public Integer getId() {
        return id;
    }

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

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Boolean getCompleted() {
        return completed;
    }

    public void setCompleted(Boolean completed) {
        this.completed = completed;
    }

    @Override
    public String toString() {
        return "Schedule{" +
                "id=" + id +
                ", title='" + title + '\'' +
                ", completed=" + completed +
                '}';
    }
}

准备R结果包装类

/**
 * projectName: com.gj.utils
 *
 * description: 返回结果类
 */
public class R {

    private int code = 200; //200成功状态码

    private boolean flag = true; //返回状态

    private Object data;  //返回具体数据
    
     public  static R ok(Object data){
        R r = new R();
        r.data = data;
        return r;
    }

    public static R  fail(Object data){
        R r = new R();
        r.code = 500; //错误码
        r.flag = false; //错误状态
        r.data = data;
        return r;
    }
     public int getCode() {
        return code;
    }

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

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

准备业务类

业务接口

/**
 * projectName: com.gj.service
 *
 * description: schedule业务接口
 */

public interface ScheduleService {

    /**
     * 返回全部学习计划
     * @return
     */
    List<Schedule> getAll();

    /**
     * 保存学习计划
     * @param schedule
     */
    void saveSchedule(Schedule schedule);

    /**
     * 更新学习计划
     * @param schedule
     */
    void updateSchedule(Schedule schedule);

    /**
     * 移除学习计划
     * @param
     */
    void removeById(Integer id);

}

业务实现

/**
 * projectName: com.gj.service.impl
 *
 * description:
 */
@Service
public class ScheduleServiceImpl  implements ScheduleService {
    //准备假数据
        private static Map<Integer,Schedule> scheduleMap;

        private static  int maxId = 5;

        static {

            scheduleMap = new HashMap<>();

            Schedule schedule = null;

            schedule = new Schedule(1, "学习Java", false);
            scheduleMap.put(1, schedule);

            schedule = new Schedule(2, "学习H5", true);
            scheduleMap.put(2, schedule);

            schedule = new Schedule(3, "学习Css", false);
            scheduleMap.put(3, schedule);
            schedule = new Schedule(4, "学习JavaScript", false);
            scheduleMap.put(4, schedule);

            schedule = new Schedule(5, "学习Spring", true);
            scheduleMap.put(5, schedule);

        }    
    /**
         * 返回全部学习计划
         *
         * @return
         */
        @Override
        public List<Schedule> getAll() {
            return new ArrayList<>(scheduleMap.values());
        }

        /**
         * 保存学习计划
         *
         * @param schedule
         */
        @Override
        public void saveSchedule(Schedule schedule) {
            maxId++;
            schedule.setId(maxId);
            scheduleMap.put(maxId,schedule);
        }

        /**
         * 更新学习计划
         *
         * @param schedule
         */
        @Override
        public void updateSchedule(Schedule schedule) {
            scheduleMap.put(schedule.getId(),schedule);
        }

        /**
         * 移除学习计划
         *
         * @param id
         */
        @Override
        public void removeById(Integer id) {
            scheduleMap.remove(id);
        }
    }

准备spring-mvc.配置文件

位置:resources/spring-mvc.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
     <!-- 扫描controller对应的包,将handler加入到ioc-->
    <context:component-scan base-package="com.gj.controller,com.gj.service" />

    <!--
       注意: 导入mvc命名空间!
       mvc:annotation-driven 是一个整合标签
                             他会导入handlerMapping和handlerAdapter
                             他会导入json数据格式转化器等等!
    -->
    <mvc:annotation-driven />
     <!-- viewResolver 不需要配置,因为我们不需要查找逻辑视图!!! -->

    <!-- 加入这个配置,SpringMVC 就会在遇到没有 @RequestMapping 的请求时放它过去 -->
    <!-- 所谓放它过去就是让这个请求去找它原本要访问的资源 -->
    <mvc:default-servlet-handler/>
</beans>

准备 web.xml配置文件

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
  version="4.0">
     <!-- 配置SpringMVC中负责处理请求的核心Servlet,也被称为SpringMVC的前端控制器 -->
  <servlet>
    <servlet-name>DispatcherServlet</servlet-name>
    <!-- DispatcherServlet的全类名 -->
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- 通过初始化参数指定SpringMVC配置文件位置 -->
    <init-param>
      <!-- 如果不记得contextConfigLocation配置项的名称,可以到DispatcherServlet的父类FrameworkServlet中查找 -->
      <param-name>contextConfigLocation</param-name>
      <!-- 使用classpath:说明这个路径从类路径的根目录开始才查找 -->
      <param-value>classpath:spring-mvc.xml</param-value>
    </init-param>
    <!-- 作为框架的核心组件,在启动过程中有大量的初始化操作要做,这些操作放在第一次请求时才执行非常不恰当 -->
    <!-- 我们应该将DispatcherServlet设置为随Web应用一起启动 -->
    <load-on-startup>1</load-on-startup>

  </servlet>

  <servlet-mapping>
    <servlet-name>DispatcherServlet</servlet-name>
    <!-- 对DispatcherServlet来说,url-pattern有两种方式配置 -->
    <!-- 方式一:配置“/”,表示匹配整个Web应用范围内所有请求。这里有一个硬性规定:不能写成“/*”。
      只有这一个地方有这个特殊要求,以后我们再配置Filter还是可以正常写“/*”。 -->
    <!-- 方式二:配置“*.扩展名”,表示匹配整个Web应用范围内部分请求 -->
    <url-pattern>/</url-pattern>
  </servlet-mapping>

</web-app>

后台增删改查实现

项目根路径设计

因为前端项目设置了后台访问的项目根路径为 /rest

我们后台项目也对应的设置:
在这里插入图片描述

SpringMVC解决跨域问题

假设我们有一个网站 http://example.com,现在需要跨域请求另外一个网站 http://api.example.com 中的数据。浏览器就会因为安全问题,拒绝客户端访问请求!

跨域问题是指在浏览器中发起跨域请求被浏览器拦截的问题。在同一个源域(同一协议、主机、端口),浏览器允许 JavaScript 发起跨域请求;在不同的源域下,浏览器对发起的异域请求会做出不同的限制。

常见的跨域问题的场景有:

  • 访问不同的子域名;
  • 访问不同的端口号;
  • 访问不同的协议(http、https);
  • 访问不同的域名;

基于CORS方式,解决跨域思路:

CORS(Cross-Origin Resource Sharing)是 W3C 制定的一种跨域解决方案,它给出了跨域请求和响应的标准。服务器端代码需要在响应头中设置 Access-Control-Allow-Origin,并指定访问来源域名名或 * 通配符,表示允许的跨域请求。浏览器可以根据响应头信息,判断是否允许该请求。

SpringMVC基于CORS思路解决跨域方案:

  • @CrossOrigin注解

    @CrossOrigin 注释在带注释的【控制器方法】 / 【控制器类】上启用跨源请求

    @RestController
    @RequestMapping("/account")
    public class AccountController {
    
      @CrossOrigin
      @GetMapping("/{id}")
      public Account retrieve(@PathVariable Long id) {
        // ...
      }
    
      @DeleteMapping("/{id}")
      public void remove(@PathVariable Long id) {
        // ...
      }
    }
    

    默认情况下, @CrossOrigin 允许:

    • All origins.
    • All headers.
    • All HTTP methods to which the controller method is mapped.
      注解核心设置属性讲解:
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface CrossOrigin {
    
      /**
       * 设置哪些客户端地址可以跨域访问! 格式为: 协议://主机地址:端口号
       * @return
       */
      @AliasFor("origins")
      String[] value() default {};
      @AliasFor("value")
      String[] origins() default {};
    
      /**
       * 设置哪些客户端的[自定义请求头]可以跨域访问!
       */
      String[] allowedHeaders() default {};
    
      /**
       * 设置哪些服务端的自定义响应头,可以被客户端读取!
       */
      String[] exposedHeaders() default {};
    
      /**
       *设置哪些请求方法,可以跨域方式! 
       */
      RequestMethod[] methods() default {};
    
      /**
       * 值为 true 或者 false
       * 客户端是否可以携带cookie!
       */
      String allowCredentials() default "";
    }
    
  • xml全局跨域配置

    <mvc:cors>
      <mvc:mapping path="/**"
        allowed-origins="*"
        allowed-methods="GET, PUT"
        allowed-headers="header1, header2, header3"
        exposed-headers="header1, header2" 
        allow-credentials="true"
        />
    
      <mvc:mapping path="/**"
        allowed-origins="https://domain1.com" />
    
    </mvc:cors>
    
业务实现

查询业务

/**
 * projectName: com.gj.controller
 *
 * description: 学习计划controller
 */
@CrossOrigin
/*
    @CrossOrigin 注释在带注释的控制器方法上启用跨源请求
    
    默认情况下, 
    @CrossOrigin 允许:
            All origins  任何请求主机地址
            All headers  任何请求头
            All HTTP methods to which the controller method is mapped.  任何请求方式!
   
    可以设置:
         @CrossOrigin(origins = "https://domain2.com") 指定允许跨域请求的主机地址
 */
@RequestMapping("schedule")
@RestController
public class ScheduleController
{

    @Autowired
    private ScheduleService scheduleService;

    @GetMapping
    public R showList(){
        List<Schedule> list = scheduleService.getAll();
        return  R.ok(list);
    }
}

修改业务

@PutMapping
public R changeSchedule(@RequestBody Schedule schedule){
    scheduleService.updateSchedule(schedule);
    return R.ok(null);
}

删除业务

@DeleteMapping("/{id}")
public R removeSchedule(@PathVariable Integer id){
    scheduleService.removeById(id);
    return R.ok(null);
}

保存业务

@PostMapping
public R saveSchedule(@RequestBody Schedule schedule){
    scheduleService.saveSchedule(schedule);
    return R.ok(null);
}
;