Spring Web MVC 入门

1. 什么是 Spring Web MVC

Spring Web MVC 是基于 Servlet API 构建的原始 Web 框架,从从⼀开始就包含在Spring框架中。它的 正式名称"SpringWebMVC"来⾃其源模块的名称(Spring-webmvc),但它通常被称为"Spring MVC".

什么是Servlet呢?

Servlet 是⼀种实现动态⻚⾯的技术.准确来讲Servlet是⼀套JavaWeb开发的规范,或者说是⼀套 Java Web开发的技术标准.只有规范并不能做任何事情,必须要有⼈去实现它.所谓实现Servlet规 范,就是真正编写代码去实现Servlet规范提到的各种功能,包括类、⽅法、属性等. Servlet 规范是开放的,除了Sun公司,其它公司也可以实现Servlet规范,⽬前常⻅的实现了 Servlet 规范的产品包括Tomcat、Weblogic、Jetty、Jboss、WebSphere等,它们都被称 为"Servlet 容器".Servlet 容器⽤来管理程序员编写的Servlet类

从上述定义我们可以得出一个信息:Spring Web MVC 是一个 Web 框架

以下简称为 Spring MVC

2. MVC 定义

MVC是ModelViewController的缩写,它是软件⼯程中的⼀种软件架构设计模式,它把软件系统分 为模型、视图和控制器三个基本部分

View(视图):指在应用程序中专门用来与浏览器进行交互,展示数据的资源

Model(模型):是应用程序的主体部分,用来处理程序中数据逻辑的部分

Controller(控制器):可以理解为一个分发器,用来决定对于视图发来的请求,需要用哪一个模型来处理,以及处理完后需要跳回到哪一个视图,即用来连接视图和模型

⽐如去饭店吃饭

客⼾进店之后,服务员来接待客⼾点餐,客⼾点完餐之后,把客⼾菜单交给前厅,前厅根据客⼾菜单 给后厨下达命令.后厨负责做饭,做完之后,再根据菜单告诉服务员,这是X号餐桌客⼈的饭. 在这个过程中

服务员就是View(视图),负责接待客⼾,帮助客⼾点餐,以及给顾客端饭

前厅就是Controller(控制器),根据⽤⼾的点餐情况,来选择给哪个后厨下达命令

后厨就是Model(模型),根据前厅的要求来完成客⼾的⽤餐需求

3. Spring MVC

MVC是⼀种架构设计模式,也是⼀种思想, ⽽SpringMVC是对MVC思想的具体实现.除此之外, Spring MVC还是⼀个Web框架.

总结来说,SpringMVC是⼀个实现了MVC模式的Web框架.

所以,SpringMVC主要关注有两个点:

  1. MVC

  2. Web 框架

前面创建 Spring Boot 项目时,勾选的 Spring Web 框架就是 Spring MVC 框架

Spring Boot 是实现 Spring MVC 的一种方式,Spring Boot 可以添加很多依赖,借助这些依赖实现不同功能,其通过添加 Spring Web MVC 框架来实现 Web 功能

而 Spring 在实现 MVC 时,也结合自身项目特点,做了一些改变,相对而言,用下面这个图来描述 Soring MVC 更加合适一些:

4. 使用 Spring MVC

使用 Spring MVC 就是通过浏览器和用户程序进行交互

主要分为以下三个方面:

  1. 建立连接:将用户(浏览器)和 Java 程序连接起来,也就是访问一个地址能够调用到我们的 Spring 程序

  2. 请求:用户请求的时候会带一些参数,在程序中要想办法获取参数,所以请求主要是获取参数的功能

  3. 响应:执行了业务逻辑之后,要把程序执行的结果返回给用户,也就是响应

4.1 项目准备

Spring MVC 项目创建和 Spring Boot 创建项目相同,在创建的时候选择 Spring Web 就相当于创建了 Spring MVC 的项目

4.2 建立连接

在 Spring MVC 中使用 @RequestMapping 来实现 URL 路由映射,也就是浏览器连接程序的作用

创建一个 UserController 类,实现用户通过浏览器和程序的交互,代码如下:

java 复制代码
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    @RequestMapping("/sayHi")
    public String sayHi() {
        return "Hello Spring MVC";
    }
}

tip:方法名和路径名可以不同

接下来访问:http://127.0.0.1:8080/sayHi 就可以看到程序返回的数据了

4.2.1 @RequestMapping 注解介绍

@RequestMapping 是 Spring Web MVC 应用程序中最常被用到的注解之一,它是用来注册接口的路由映射的

表示服务收到请求时,路径为 /sayHi 的请求就会调用 sayHi 这个方法的代码

路由映射:当用户访问一个 URL 时,将用户的请求对应到程序中某个类的某个方法的过程叫做路由映射

其与 @RequestController 需要同时存在,若注释掉 @RequestController

一个项目中会有很多类,每个类中又有很多方法,当我们要访问 /sayHi 时,Spring 会对所有类进行扫描,只有类加了注解 @RequestController,Spring 才会去看这个类里面有没有 @RequestMapping("/sayHi"),如果有,就执行该方法;若是类没有加注解 @RequestController,Spring就不会进入该类,即使该类中有 @RequestMapping("/sayHi")

4.2.2 @RequestMapping 使用

@RequestMapping 既可以修饰类,也可以修饰方法,当修饰类和方法时,访问的地址是类路径 + 方法路径

@RequestMapping 标识一个类:设置映射请求的请求路径的初始信息

@RequestMapping 标识一个方法:设置映射请求路径的具体信息

java 复制代码
@RequestMapping("/user")
@RestController
public class UserController {
    @RequestMapping("/sayHi")
    public String sayHi() {
        return "Hello Spring MVC";
    }
}

访问地址:http://127.0.0.1:8080/user/sayHi

tip:

  1. @RequestMapping 的 URL 路径最前面加不加**/**都可以,Spring 程序启动时会进行判断,如果前面没有加 /,Spring 会拼接上一个 /,通常情况下,建议加上 /

  2. 注解没有先后顺序之分

4.2.3 @RequestMapping 是 GET 还是 POST 请求

GET 请求:

浏览器发送的请求类型都是 get,通过上面案例可知 @RequestMapping 支持 get 请求

通过 Fiddler 也可以观察到:

POST 请求:

通过 form 表单来构造请求:创建 test.html:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="/user/sayHi" method="post">
        <input type="submit" value="提交">
    </form>
</body>
</html>

从运行结果可以看出:@RequestMapping 既支持 get 请求,又支持 post 请求,同理,也支持其他的请求方式

4.2.4 指定 GET/POST 方法类型

方法一:我们可以显式的指定 @RequestMapping 来接收 POST 的情况,如下:

方法二:使用 @GetMapping 或 @PostMapping 注解来设置

@PostMapping 使用方法同理


5. 使用 Postman 创建请求

界面介绍

6. 请求

6.1 传递单个参数

接收单个参数,在 Spring MVC 中直接用方法中的参数就可以,如下代码:

java 复制代码
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/param")
@RestController
public class ParamController {
    @RequestMapping("/p1")
    public String p1(String name) {
        return "接收到参数,name:" + name;
    }
}

使用 Postman 构建发送请求:

Spring MVC 根据方法的参数名,找到对应的参数,赋值给方法

如果参数不一致,是获取不到参数的,如下:

tip:

使用基本类型来接收参数时,参数必须传(除非是 boolean 类型),否则会报 500 错误

类型不匹配时,会报 400 错误

使用包装类型,如果不传参数,Spring 接收到的数据则为 null,因此在企业开发中,对于参数可能为空的数据,建议使用包装类型

6.2 传递多个参数

和接收单个参数一样,直接使用方法的参数接收即可,如下:

tip:当有多个参数时,前后端进行参数匹配时,是以参数的名称进行匹配的,因此参数的位置是不影响后端获取参数的结果

6.3 传递对象

当参数比较多时,方法声明就需要有很多形参,并且后续每新增一个参数,也需要修改方法声明

因此我们不妨将这些参数封装成一个对象

Spring MVC 也可以自动实现对象参数的赋值,如下:

创建一个 User 对象:

java 复制代码
package com.example.sprignmvc_demo_20241021;

public class User {
    private String name;
    private int age;
    private Integer gender;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    public Integer getGender() {
        return gender;
    }

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

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

传递对象代码:

Spring 会根据参数名称自动绑定到对象的各个属性上,如果某个属性未传递,则赋值为 null(基本类型则赋值为默认初始值,如 int 被赋值为 0)

6.4 后端参数重命名(后端参数映射)

某些特殊情况下,前端传递的参数 key 和我们后端接收的 key 可以不一致,如:前端传递了一个 userName,而后端是使用 name 字段来接收的,这样就会出现参数接收不到的情况,如果出现这种情况,我们就可以使用 @RequestParam 重命名前后端的参数值,如下:

若此时前端使用 name 来传参:

查看 @RequestParam 的源码:

可以看到,required 的默认值为 true,表示的含义是:该注解修饰的参数默认为必传参数

既然有默认,就可以更改:

再次运行:

不报错了,但是接收不到前端传的 name 参数,这是因为使用 @RequestParam 进行参数重命名时,请求参数只能和 @RequestParam 声明的名称一致,才能进行参数绑定和赋值

6.5 传递数组

Spring MVC 可以自动绑定数组参数的赋值

像第二种方式,会自动进行分割,如下:

6.6 传递集合

这种传参方式和传数组是一样的,HTTP 默认将其封装成了一个数组,相当于是传递了一个数组,而 List 无法用来接收该数组

此时就需要使用 @RequestParam 来进行参数绑定,将数绑定成 List

6.7 传递 JSON 数据

6.7.1 JSON 概念

JSON:JavaScriptObjectNotation 【JavaScript 对象表⽰法】

JSON是⼀种轻量级的数据交互格式.它基于ECMAScript(欧洲计算机协会制定的js规范)的⼀个⼦集, 采⽤完全独⽴于编程语⾔的⽂本格式来存储和表⽰数据。--百度百科

简单来说:JSON就是⼀种数据格式,有⾃⼰的格式和语法,使⽤⽂本表⽰⼀个对象或数组的信息,因此 JSON本质是字符串. 主要负责在不同的语⾔中数据传递和交换.

6.7.2 JSON 和 JavaScript 的关系

没有关系,只是语法相似

6.7.3 JSON 语法

JSON 是一个字符串,其格式非常类似于 JavaScript 对象字面量的格式

看一段 JSON 数据:

javascript 复制代码
{
	"squadName": "Super hero squad",
	"homeTown": "Metro City",
	"formed": 2016,
	"secretBase": "Super tower",
	"active": true,
	"members": [{
		"name": "Molecule Man",
		"age": 29,
		"secretIdentity": "Dan Jukes",
		"powers": ["Radiation resistance", "Turning tiny", "Radiation blast"]
	},
	{
		"name": "Madame Uppercut",
		"age": 39,
		"secretIdentity": "Jane Wilson",
		"powers": ["Million tonne punch", "Damage resistance", "Superhuman reflexes"]
	},
	{
		"name": "Eternal Flame",
		"age": 1000000,
		"secretIdentity": "Unknown",
		"powers": ["Immortality", "Heat Immunity", "Inferno", "Teleportation", "Interdimensional travel"]
	}]
}

也可以压缩表示:(和上面数据一样,只不过上面数据进行了格式化,更易读)

javascript 复制代码
{"squadName":"Super hero squad","homeTown":"Metro City","formed":2016,"secretBase":"Super tower","active":true,"members":[{"name":"Molecule Man","age":29,"secretIdentity":"Dan Jukes","powers":["Radiation resistance","Turning tiny","Radiation blast"]},{"name":"Madame Uppercut","age":39,"secretIdentity":"Jane Wilson","powers":["Million tonne punch","Damage resistance","Superhuman reflexes"]},{"name":"Eternal Flame","age":1000000,"secretIdentity":"Unknown","powers":["Immortality","Heat Immunity","Inferno","Teleportation","Interdimensional travel"]}]}

JSON 的语法:

数据在 键值对(Key / Value) 中

数据由 逗号 分隔

对象用 { } 表示

数组用 [ ] 表示

值可以为对象,也可以为数组,数组中可以包含多个对象

JSON 的两种结构:

对象:大括号 { } 保存的对象是一个无序的 键值对 集合,一个对象以 左括号 { 开始,右括号 } 结束,每个 键 后跟一个冒号 : ,键值对使用 逗号 分隔

数组:中括号 [ ] 保存的数组是 值(Value) 的有序集合,一个数组以 左中括号 [ 开始,右中括号 ] 结束,值之间使用 逗号 分隔

6.7.4 JSON 字符串和 Java 对象互转

JSON 本质上是一个字符串,通过文本来存储和描述数据

Spring MVC 框架也集成了 JSON 的转换工具,我们可以直接使用,来完成 JSON 字符串和 Java 对象的互转

本质上是 jackson-databind 提供的转换功能,Spring MVC 框架中已经把该工具包引入了进来,我们可以直接使用,若要脱离 Spring MVC 使用,需要引入相关依赖,如下:

javascript 复制代码
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.13.5</version>
</dependency>

JSON 的转换工具包有很多,jackson-databind 只是其中的一种

后端实现:

java 复制代码
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonTest {
    public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper(); // 该类来自于 jackson-databind,用于处理 JSON

        User user = new User();
        user.setName("lisi");
        user.setAge(18);
        user.setGender(1); // 表示 男

        // 对象转 JSON
        String s = objectMapper.writeValueAsString(user);
        System.out.println(s);

        // JSON 转对象
        User user1 = objectMapper.readValue(s, User.class);
        System.out.println(user1);
    }
}

使用 ObjectMapper 对象提供的两个方法,可以完成对象和 JSON 字符串的互转

writeValueAsString:把对象转为 JSON 字符串

readValue:把字符串转为对象

6.7.5 JSON 优点

简单易⽤:语法简单,易于理解和编写,可以快速地进⾏数据交换

跨平台⽀持: JSON可以被多种编程语⾔解析和⽣成,可以在不同的平台和语⾔之间进⾏数据交换和 传输

轻量级:相较于XML格式,JSON数据格式更加轻量级,传输数据时占⽤带宽较⼩,可以提⾼数据传输 速度

易于扩展: JSON的数据结构灵活,⽀持嵌套对象和数组等复杂的数据结构,便于扩展和使⽤

安全性:JSON数据格式是⼀种纯⽂本格式,不包含可执⾏代码,不会执⾏恶意代码,因此具有较⾼ 的安全性

6.8 传递 JSON 对象

接收 JSON 对象,需要使用 @RequestBody 注解

RequestBody:请求正文,这个注解作用在请求正文的数据绑定,请求参数必须写在请求正文中

后端实现:

java 复制代码
    @RequestMapping("/p8")
    public String p8(@RequestBody User user) {
        return "user:" + user;
    }

tip:区分传递对象时使用 get 和 post 方式

区分传递对象与传递 JSON 对象

6.9 获取 URL 中参数(@PathVariable)

path variable:路径变量

这个注解主要作用在请求 URL 路径上的数据绑定

默认传递餐宿写在 URL 上,Spring MVC 就可以获取到

多个参数的情况:

tip:如果方法参数名称和需要绑定的 URL 中的变量名称一致时,可以简写,不用给 @PathVariable 的属性赋值,如上述例子中的 name 变量;反之则需要

6.10 传递文件(@RequestPart)

重命名:

6.11 获取 Cookie/Session

HTTP 协议自身是属于 "无状态" 协议

无状态是指:默认情况下 HTTP 协议的客户端和服务器之间的这次通信和下次通信之间没有直接的联系

但是实际开发中,我们很多时候是需要知道请求之间的关联关系的

例如:登录网站成功后,第二次访问的时候,服务器就能知道该请求是否已经登录过了

上述途中的 "令牌" 通常就存储在 Cookie 字段中

⽐如去医院挂号

  1. 看病之前先挂号.挂号时候需要提供⾝份证号,同时得到了⼀张"就诊卡",这个就诊卡就相当于患 者的"令牌".
  2. 后续去各个科室进⾏检查,诊断,开药等操作,都不必再出⽰⾝份证了,只要凭就诊卡即可识别出当 前患者的⾝份.
  3. 看完病了之后,不想要就诊卡了,就可以注销这个卡.此时患者的⾝份和就诊卡的关联就销毁了.(类 似于⽹站的注销操作)
  4. ⼜来看病,可以办⼀张新的就诊卡,此时就得到了⼀个新的"令牌"

此时在服务器这边就需要记录 "令牌" 信息,以及令牌对应的用户信息,这个就是 Cookie机制所作的工作

6.11.2 Session

会话:就是对话的意思

在计算机领域,会话是一个客户与服务器之间的不中断的请求响应,对客户的每个请求,服务器能识别出请求来自于同一个客户

当一个位置的客户像 Web 应用程序发送第一个请求时,就开始了一个会话

当客户明确结束会话或服务器在一个时限内没有接收到客户的任何请求时,会话就结束了

⽐如我们打客服电话

每次打客服电话,是⼀个会话.挂断电话,会话就结束了

下次再打客服电话,⼜是⼀个新的会话.

如果我们⻓时间不说话,没有新的请求,会话也会结束.

服务器同一时刻收到的请求是很多的,服务器需要清楚的区分每个请求是属于哪个用户,也就是属于哪个会话,就需要在服务器这边记录每个会话以及与用户的信息的对应关系

Session 是服务器为了保存用户信息而创建的一个特殊的对象

Session 的本质就是一个 "哈希表",存储了一些键值对结构,Key 就是 SessionID,Value 就是用户信息(用户信息可以根据需求灵活设计)

SessionID 是由服务器生成的一个 "唯一性字符串",从 Session 机制的角度来看,这个唯一性字符串称为 "SessionID",但是站在整个登录流程中看待,也可以把这个唯一性字符串称为 "token"

上述例子中的 令牌ID 就可以看作是 SessionID,只不过令牌除了 ID 之外,还会带有一些其他信息,比如时间、签名等

  1. 当用户登录的时候,服务器在 Session 中新增一个新记录,并把 SessionID 返回给客户端(通过 HTTP 响应中的 Set-Cookie 字段返回)
  2. 客户端后续再给服务器发送请求的时候,需要在请求中带上 SessionID(通过 HTTP 请求中的 Cookie 字段带上)
  3. 服务器收到请求之后,根据请求中的 SessionID 在 Session 信息中获取到对应的用户信息,再进行后续操作,找不到则重新创建 Session,并把 SessionID 返回

tip:Session 默认是保存在内存中的,如果重启服务器则 Session 数据就会丢失

  • Cookie 是客户端保存用户信息的一种机制;Session 是服务器端保存用户信息的一种机制
  • Cookie 和 Session 之间主要是通过 SessionID 关联起来的,SessionID 是 Cookie 和 Session 之间的桥梁
  • Cookie 和 Session 经常会在一起配合使用,但不是必须配合

完全可以用 Cookie 来保存一些数据在客户端,这些数据不一定是用户身份信息,也不一定是 SessionID

Session 中的 SessionID 也不是非得通过 Cookie / Set-Ckkoie 传递,比如通过 URL 传递

  1. 传统方法获取:
java 复制代码
    // 获取 Cookie
    @RequestMapping("/getCookie")
    public String getCookie(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        // cookie 为空判断
        // if (cookies == null) return "Cookie 为 null";
        for (Cookie cookie : cookies) {
            System.out.println(cookie.getName() + ":" + cookie.getValue());
        }
        return "Cookie 获取成功";
    }
  • Spring MVC是基于ServletAPI构建的原始Web框架,也是在Servlet的基础上实现的
  • HttpServletRequest , HttpServletResponse 是Servlet提供的两个类,是Spring MVC⽅法的内置对象.需要时直接在⽅法中添加声明即可.
  • HttpServletRequest 对象代表客⼾端的请求,当客⼾端通过HTTP协议访问服务器时,HTTP请 求头中的所有信息都封装在这个对象中,通过这个对象提供的⽅法,可以获得客⼾端请求的所有信 息.
  • HttpServletResponse 对象代表服务器的响应.HTTP响应的信息都在这个对象中,⽐如向客⼾ 端发送的数据,响应头,状态码等.通过这个对象提供的⽅法,可以获得服务器响应的所有内容
  • Spring MVC在这两个对象的基础上进⾏了封装,给我们提供更加简单的使⽤⽅法.

此时没有设置 Cookie,通过浏览器访问 http://127.0.0.1:8080/header/getCookie,得到 Cookie 为 null

在浏览其中设置 Cookie 的值(手动添加 Cookie)

再次访问:

  1. 简洁方法获取:
java 复制代码
    @RequestMapping("/getCookie2")
    public String getCookie2(@CookieValue("zhangsan") String value) {
        return "从 Cookie 中获取信息:" + value;
    }

运行结果:

两种方式对比:

第一种方法可以获取多次参数

第二种方法每获取一个参数都需要加一个注解


tip:Postman 设置 Cookie


6.11.5 获取 Session

  1. Session 存储和获取

Session 是服务器端的机制,我们需要先存储,才能获取

Session 也是基于 HttpServletRequest 来存储和获取的

  1. Session 存储
java 复制代码
    @RequestMapping("/setSession")
    public String setSession(HttpServletRequest request) {
        // 先获取到 session 对象,若没有,则创建一个空 session
        HttpSession session = request.getSession();
        session.setAttribute("userName", "zhangsan");
        session.setAttribute("age", 18);
        return "设置 session 成功";
    }

第一次运行上述程序,是没有 session 对象的,可以在 Fiddler 中观察到 Set-Cookie 生成 SessionID:

  1. Session 读取(使用 HttpServletRequest)
java 复制代码
    @RequestMapping("/getSession")
    public String getSession(HttpServletRequest request) {
        HttpSession session = request.getSession();
        // session 是类似 map 的结构
        // 判空
        if (session.getAttribute("userName") == null) return "session 为 null";

        String userName = (String)session.getAttribute("userName");
        System.out.println(session.getAttribute("age"));
        return "从 session 中获取信息,userName:" + userName;
    }

request.getSession() 共做了两件事:先从 Cookie 中拿到 SessionID,然后从 SessionID 中拿到 Session

设置完 session 后,再获取 session:

可以看到,Http 请求时,将 SessionID 通过 Cookie 传递到了服务器

  1. 简洁获取①
java 复制代码
    @RequestMapping("/getSession2")
    public String getSession2(HttpSession session) {
        String userName = (String)session.getAttribute("userName");
        System.out.println(session.getAttribute("age"));
        return "从 session 中获取信息,userName:" + userName;
    }

tip:Session 存储在服务器的内村上,服务重启时,Session 会丢失

先设置 session,再获取

  1. 简洁获取②
java 复制代码
    @RequestMapping("/getSession3")
    public String getSession3(@SessionAttribute String userName) {
        return "从 session 中获取信息,userName:" + userName;
    }

tip:由于 Session 是服务器端的,只能由代码去构建,不能用 Postman 构建


6.11.6 获取 Header

  1. 传统方式:

获取 Header 也是从 HttpServletRequest 中获取

java 复制代码
    @RequestMapping("/getHeader")
    public String getHeader(HttpServletRequest request) {
        String userAgent = request.getHeader("User-Agent");
        return "User-Agent:" + userAgent;
    }
  1. 简洁方式:
java 复制代码
    @RequestMapping("/getHeader2")
    public String getHeader2(@RequestHeader("User-Agent") String userAgent) {
        return "User-Agent:" + userAgent;
    }

post 方式:在 Headers 选项中根据需求添加即可

7. 响应

7.1 返回静态页面

创建前端页面 hello.html

html 代码:

后端代码:

java 复制代码
@RequestMapping("/response")
@RestController
public class ResponseController {
    @RequestMapping("/returnHtmlPage")
    public String returnHtmlPage() {
        return "/hello.html";
    }
}

我们期待返回如下界面:

结果却是:

这时需要将 @RestController 改为 @Controller:

7.1.1 @RestController 和 @Controller

二者的差异就在 @RequestBody 上,@ResponseBody表示返回数据

早期前后端未分离时,后端需要返回视图,就用到 @Controller

现在前后端分离,后端仅需给前端返回数据,具体展示哪个页面给用户由前端决定,因此用到 @ResponseBody,再加上 @Controller 中其他的作用,就成了现在的 @RequestController

tip:

@Controller:定义一个控制器,Spring 框架启动时加载,把这个对象交给 Spring 管理

@ResponseBody:定义返回的数据格式为非视图,返回一个 text/html 信息

7.2 返回数据 @ResponseBody

@ResponseBody 既是类注解,又是方法注解

如果作用在类上,表示该类的所有方法返回的都是数据

如果作用在方法上,表示该方法返回的是数据,其他方法不受影响

tip:将 returnData 方法上的 @ResponseBody 去掉,程序会报 404 错误

程序会认为需要返回的是视图,根据内容 "hahahahahahaha" 去查找文件,但是查找不到,路径不存在,报 404 异常

7.3 返回 HTML 代码片段

直接返回即可,当后端返回数据,若数据中有 HTML 代码,会被浏览器直接解析

通过 Fiddler 观察响应结果 Content-Type: text/html

响应中的 Content-Type 常见取值有以下几种:

  • text / html:body 数据格式是 HTML
  • text / css:body 数据格式是 CSS
  • application / javascript:body 数据格式是 JavaScript
  • application / json:body 数据格式是 JSON

如果请求的是 js 文件,Spring MVC 会自动设置 Content-Type 为 application / javascript

如果请求的是 css 文件,Spring MVC 会自动设置 Content-Type 为 text / css

7.4 返回 JSON

7.5 设置状态码

Spring MVC 会根据我们方法的返回结果自动设置响应状态码,也可以手动指定状态码

通过 Spring MVC 的内置对象 HttpServletResponse 提供的方法来进行设置

java 复制代码
    @ResponseBody
    @RequestMapping("/setStatus")
    public User setStatus(HttpServletResponse response) {
        User user = new User();
        user.setName("zhangsan");
        user.setAge(18);
        response.setStatus(500);
        return user;
    }

7.6 设置 Header

http 响应报头也会向客户端传递一些附加信息,比如服务程序的名称;请求的资源已移动到新地址等,比如:Content-Type,Local 等

这些信息通过 @RequestMapping 注解的属性来实现,其源码如下:

7.6.1 通过设置 produces 属性的值,设置响应的报头 Content-Type

7.6.2 通过 HttpServletResponse 内置方法设置 Header

8. 案例

8.1 加法计算器

需求:输入两个整数,点击 "点击相加" 按钮,显示计算结果

1. 约定前后端交互接口

概念:

约定"前后端交互接⼝"是进⾏Web开发中的关键环节.

接⼝⼜叫API(ApplicationProgrammingInterface),我们⼀般讲到接⼝或者API,指的都是同⼀个东 西.

是指应⽤程序对外提供的服务的描述,⽤于交换信息和执⾏任务

简单来说,就是允许客⼾端给服务器发送哪些HTTP请求,并且每种请求预期获取什么样的HTTP响应.

现在"前后端分离"模式开发,前端和后端代码通常由不同的团队负责开发.双⽅团队在开发之前,会提前 约定好交互的⽅式.客⼾端发起请求,服务器提供对应的服务.服务器提供的服务种类有很多,客⼾端按 照双⽅约定指定选择哪⼀个服务.

接⼝,其实也就是我们前⾯⽹络模块讲的的"应⽤层协议".把约定的内容写在⽂档上,就是"接⼝⽂档",接 ⼝⽂档也可以理解为是应⽤程序的"操作说明书".

在项⽬开发前,根据需求先约定好前后端交互接⼝,双⽅按照接⼝⽂档进⾏开发.

接⼝⽂档通常由服务提供⽅来写,交由服务使⽤⽅确认,也就是客⼾端.

接⼝⽂档⼀旦写好,尽量不要轻易改变.

如若需要改变,必须要通知另⼀⽅知晓.


需求分析

加法计算器功能,对两个整数进行相加,需要客户端提供参与计算的两个数,服务端返回这两个整数的计算结果

基于以上分析,定义接口


接口定义

  • 请求路径:calc / sum
  • 请求方式:GET / POST
  • 接口描述:计算两个整数相加

请求参数:

|------|---------|------|-----------|
| 参数名 | 类型 | 是否必须 | 备注 |
| num1 | Integer | 是 | 参与计算的第一个数 |
| num2 | Integer | 是 | 参与计算的第二个数 |


响应数据

  • Content-Type:text / html
  • 响应内容:计算机计算结果:8

2. 服务器代码

java 复制代码
@RequestMapping("/calc")
@RestController
public class CalcController {
    @RequestMapping("/sum")
    public String sum(@RequestParam("num1") Integer num1,
                      @RequestParam("num2") Integer num2) {
        Integer sum = num1 + num2;
        return "计算机计算结果:" + sum;
    }
}

tip:使用 @RequestParam 限定参数为必传(方法一,其他两种在下面示例)

使用 Postman 测试:

3. 前端代码:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
     <form action="calc/sum" method="post">
        <h1>计算器</h1>
        数字1:<input name="num1" type="text"><br>
        数字2:<input name="num2" type="text"><br>
        <input type="submit" value=" 点击相加 ">
    </form>
</body>

</html>

运行:

8.2 用户登录

需求:用户输入账号和密码,后端进行校验密码是否正确

  • 如果不正确,前端进行用户告知
  • 如果正确,跳转到首页,首先显示当前登录用户
  • 后续再访问首页,可以获取到登录用户信息

1. 约定前后端交互接口

对于后端而言,不涉及前端页面的展示,只需提供两个功能

  1. 登录页面:通过账号和密码,校验输入的账号密码是否正确,并告知前端
  2. 首页:告知前端当前登录用户,如果当前已有用户登录,返回登录的账号;如果没有,返回空

接口定义

(1) 校验接口

请求路径:/user /login

请求方式:POST

接口描述:校验账号密码是否正确


请求参数

|----------|--------|------|-------|
| 参数名 | 类型 | 是否必传 | 备注 |
| userName | String | 是 | 校验的账号 |
| password | String | 是 | 校验的密码 |


响应数据

Content-Type:test 、 html

响应内容:true // 账号密码验证成功;false // 账号密码验证失败


(2) 查询登录用户接口

请求路径:/user /getLoginUser

请求方式:GET

接口描述:查询当前登录的用户

请求参数:无

响应数据

Content-Type:text / html

响应内容:zhangsan(返回当前登录的用户名)

2. 服务端代码

java 复制代码
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/user")
@RestController
public class UserController {
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public Boolean login(String userName, String password, HttpServletRequest request) {
        // 方式二:
//        if (userName == null || "".equals(userName)) return false; // 使用常量.equals,避免空指针异常
        // 方式三:Spring 提供检测字符串是否有长度
        if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) return false;

        // 不为空,校验账号和密码是否正确
        if ("admin".equals(userName) && "admin".equals(password)) {
            // 设置 Session,为了下面获取用户名
            HttpSession session = request.getSession(true); // 不填也行,默认为 true
            session.setAttribute("userName", userName);

            return true;
        }

        return false;
    }

    @RequestMapping (value = "/getLoginUser", method = RequestMethod.GET)
    public String getLoginUser(HttpSession session) {
        if (session.getAttribute("userName") != null) {
            return (String)session.getAttribute("userName");
        }
        return "";
    }
}

tip:限定必传参数的另外两种方式,上述代码中展现

Postman 测试:

3. 前端代码

ajax 是异步的,form 表单是同步的

当用户无输入时,form 表单会一直等待,输入后会跳转新页面

而 ajax 可以不跳转新页面,如下例:

当我们鼠标从账号框转移到密码框,并点击后

它会进行重复检测,这个过程肯定是在后端进行的,浏览器不能存储很大量的信息,这个检测过程并不会跳转新页面,ajax 可以实现

(1) 登录页面 login.html

对于前端而言,当点击登录按钮时,需要把用户输入的信息传递到后端进行校验,后端校验成功,则跳转到首页:index.html;后端校验失败,则直接弹窗报错

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>登录页面</title>
</head>

<body>
  <h1>用户登录</h1>
  用户名:<input name="userName" type="text" id="userName"><br>
  密码:<input name="password" type="password" id="password"><br>
  <input type="button" value="登录" onclick="login()">
  
  <script src="js/jquery-3.7.1.min.js"></script>
  <script>
    function login() {
        // 这里使用的是 jquery 封装过的 ajax(更简洁),不是原生的
        // 语法:$.ajax({}); ajax 的参数是一个对象,写在 {} 里,主要内容就是完成接口文档信息
        $.ajax({
          type: "post", // 请求类型
          url: "/user/login", // 访问路径
          // 传参 key: value
          data: {
            "userName": $("#userName").val(), // 用 # 获取 id
            "password": $("#password").val()
          },
          // 返回结果
          success: function(body) { // 此处的 body 变量就是后端返回的 true 或 false
              if (body==true) {
                // 跳转到 index 页面
                location.href = "index.html";
              } else {
                // 当前页面
                alert("密码错误");
              }
          }
        });
    }

  </script>
</body>

</html>

tip:页面跳转的三种方式:

  • window.location.href = "book_list.html";
  • window.location.assign("book_list.html");
  • window.location.replace("book_list.html");

以上写法,通常把 "window" 省略,比如:window.location.href = "book_list.html"; 写成 location.href = "book_list.html";

三者区别参考:location.assign()、location.href、location.replace(url)的不同

(2) 首页代码 index.html

首页代码比较简单,只显示当前登录用户即可

当前登录用户需要从后端获取,并显示到前端

html 复制代码
<!doctype html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>用户登录首页</title>
</head>

<body>
    登录人: <span id="loginUser"></span>

    <script src="js/jquery-3.7.1.min.js"></script>
    <script>
        $.ajax({
            type: "get",
            url: "/user/getLoginUser",
            success: function(userName) {
                $("#loginUser").text(userName);
            }
        });
    </script>
</body>

</html>

tip:上述两个前端代码中的 jquery 存于本地

4. 运行测试

多次刷新 http://127.0.0.1:8080/index.html 发现依然可以获取到登录用户

如果重启服务器,则登录人显示为空,这是因为 Session 存在内存中,如果不做任何处理,默认服务器重启,Session 数据就丢失了

8.3 留言板

需求:

输入留言信息,点击提交,后端把数据存储起来

页面展示输入的表白墙的信息

1. 约定前后端交互接口

需求分析

后端需要提供两个服务

提交留言:用户输入留言信息后,后端需要把留言信息保存起来

展示留言:页面展示时,需要从后端后取到所有的留言信息

接口定义

(1) 获取全部留言

全部留言信息,这里用 List 来表示,可以用 JSON 来描述这个 List 数据

请求:

GET /message/getList

响应:JSON 格式

javascript 复制代码
[
    {
        "from": "黑猫",
        "to": "白猫",
        "message": "喵"
    }, {
        "from": "黑狗",
        "to": "白狗",
        "message": "汪"
    },
    // ...
]

浏览器给服务器发送一个 GET /message/getList 这样的请求,就能返回当前一共有哪些留言记录,结果以 JSON 的格式返回回来

(2) 发表新留言

请求:body 也为 JSON 格式

javascript 复制代码
POST /message/publish

{
    "from": "黑猫",
    "to": "白猫",
    "message": "喵"
}

响应:JSON 格式

javascript 复制代码
{
    ok: 1
}

浏览器给服务器发送一个 POST /message/publish 这样的请求,就能把当前的留言提交给服务器

2. 实现服务器端代码

定义 MessageInfo 对象
java 复制代码
public class MessageInfo {
    private String from;
    private String to;
    private String message;

    public String getFrom() {
        return from;
    }

    public void setFrom(String from) {
        this.from = from;
    }

    public String getTo() {
        return to;
    }

    public void setTo(String to) {
        this.to = to;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}
MessageController
java 复制代码
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

@RequestMapping("/message")
@RestController
public class MessageController {
    //存储留言信息
    List<MessageInfo> messageInfos = new ArrayList<>();

    // 获取接口
    @RequestMapping("/getList")
    public List<MessageInfo> getList() {
        return messageInfos;
    }

    // 发布接口
    @RequestMapping("/publish")
    public String publish(@RequestBody MessageInfo messageInfo) { // 请求是 JSON 格式,参数需要加上 @RequestBody
        // 参数都不为空,将对象添加到 list 中
        if (StringUtils.hasLength(messageInfo.getFrom())
            && StringUtils.hasLength(messageInfo.getTo())
            && StringUtils.hasLength(messageInfo.getMessage())) {
                messageInfos.add(messageInfo);
                return "{\"ok\": 1}";
        }
        return "{\"ok\": 0}";
    }
}
后端测试:

成功了,但是此时的响应类型是 text 格式

需要将其转为 JSON 格式,如下:

3. lombok

3.1 介绍

lombok 是一个 Java goon工具库,通过添加注解的方式,简化 Java 的开发

引入依赖:

在 pom.xml 文件中的 <dependencies> </dependencies> 中添加以下语句

java 复制代码
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
3.2 使用:

前面定义 MessageInfo 时,需要写每个参数的 get set 方法,很麻烦

lombok 可以通过注解的方式,帮我们消除一些冗长代码,使代码看起来简洁一点,如下:

java 复制代码
import lombok.Data;

@Data
public class MessageInfo {
    private String from;
    private String to;
    private String message;
}

@Data 注解会帮助我们自动生成一些方法

3.3 原理解释

可以观察加了 @Data 注解之后,Idea 反编译的 class 文件

这不是真正的字节码文件,而是 Idea 根据字节码进行反编译后的文件

反编译是将可执行的程序代码转换为某种形式的高级编程语言,使其具有更易读的格式

反编译是一种逆向工程,它的作用和编译器的作用相反

可以看出,lombok 是一款在编译时期生成代码的工具包

Java 程序的运行原理:

lombok 的作用如下图:

3.4 lombok 包含的方法

|--------------------------|---------------------------------------|
| 注解 | 作用 |
| @Getter | 自动添加 gerrer 方法 |
| @Setter | 自动添加 setter 方法 |
| @ToString | 自动添加 toString方法 |
| @EqualsAndHashCode | 自动添加 equals 方法和 hashCode 方法 |
| @NoArgsConstructor | 自动添加无参构造方法 |
| @AllArgsConstructor | 自动添加全属性构造方法,顺序按照属性的定义顺序 |
| @NonNull | 属性不能为 null |
| @RequiredArgsConstructor | 自动添加必须属性的构造方法,final + @NonNull 的属性为必须 |

@Data=@Getter+@Setter+@ToString+@EqualsAndHashCode+@RequiredArgsConstructor +@NoArgsConstructor

当我们只需要其中某种方法时,可以单独注解,如下:

4. 前端代码

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>留言板</title>
    <style>
        .container {
            width: 350px;
            height: 300px;
            margin: 0 auto;
            /*水平方向居中*/
            /* border: 1px black solid; */
            text-align: center;
        }

        .grey {
            color: grey;
        }

        .container .row {
            width: 350px;
            height: 40px;

            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .container .row input {
            width: 260px;
            height: 30px;
        }

        #submit {
            width: 350px;
            height: 40px;
            background-color: orange;
            color: white;
            border: none;
            margin: 10px;
            border-radius: 5px;
            font-size: 20px;
        }
    </style>
</head>

<body>
    <div class="container">
        <h1>留言板</h1>
        <p class="grey">输入后点击提交, 会将信息显示下方空白处</p>
        <div class="row">
            <span>谁:</span> <input type="text" name="" id="from">
        </div>
        <div class="row">
            <span>对谁:</span> <input type="text" name="" id="to">
        </div>
        <div class="row">
            <span>说什么:</span> <input type="text" name="" id="say">
        </div>
        <input type="button" value="提交" id="submit" onclick="submit()">
        <!-- <div>A 对 B 说: hello</div> -->
    </div>

    <script src="js/jquery-3.7.1.min.js"></script>
    <script>

        // 给点击按钮注册点击事件
        function submit() {
            // 1. 获取到编辑框内容
            let from = $("#from").val();
            let to = $("#to").val();
            let say = $("#say").val();

            if (from == "" || to == "" || say == "") {
                alert("请检查输入内容");
                return;
            }
            // 检验完成,发起请求
            $.ajax({
                url: "/message/publish",
                type: "post",
                contentType: "application/json",
                data: {
                    from: from,
                    to: to,
                    message: say
                },
                success: function (result) {
                    if (result.ok == 1) {
                        // 成功
                        alert("添加成功");
                    } else {
                        // 失败
                        alert("添加失败");
                    }
                }
            });
        }

    </script>
</body>

</html>

测试:期望弹出一个失败或者成功的弹窗

结果并没有,而是报了两个错误

错误一:

第一个错误是因为图标问题,暂不考虑

看第二个错误,发现其状态码为 400

再看控制台给的提示:

JSON解析错误:无法识别的标记"from":应为(JSON字符串、数字、数组、对象或标记"null"、"true"或"false")]

看这个描述可能是 JSON 格式的问题,通过 Fiddler 抓包查看:

可以看出,正常情况下,请求应该是一个 JSON 字段,结果在浏览器请求却出现了字符串,说明问题出现在前端代码中

将 data 转为 JSON 格式

修改后重启服务:

可以添加成功,但是并没有按照我们的要求:页面展示输入的表白墙的信息

错误二:

此时,数据确实添加到后端了,但是前端并没有向后端要这些数据,页面中没有任何展示

我们希望页面在加载时就显示这些数据,因此将向后端请求数据的 ajax 写到 <script> 的最开始,如下:

重启服务,使用 Postman 构造两条数据,然后浏览器刷新显示:

出现了 undefined,肯定是刚才写的代码有问题,观察后端定义的 MessageInfo 发现:

第三个参数有问题,将其修改为 msg.message,重新启动服务:

成功了,当我们在浏览器中添加数据时:

错误三:

只显示弹窗,并没有添加数据,这是因为添加完之后并没有请求 List,没有重新加载页面,因此成功后也应该请求 List:

重启服务:

错误四:

重启服务:

添加失败了,并且浏览器的开发者工具没有错误信息,后端控制台也没有错误日志

这时我们就需要自己打印错误日志

既然页面可以弹出 添加错误 的弹窗,说明程序一定可以走到这里

通过这些日志,可以知道,result.ok 并没有获取到这个对象的属性,后端传过来的不是一个对象,而是一个字符串

这种情况下,前端也可以主动将该字符串转为 JSON 对象,如下:

重启服务,成功。

tip:两种改为 JSON 的方法(两者不能同时进行转换)

前端方法如上图

后端方法:

前端完整代码:
html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>留言板</title>
    <style>
        .container {
            width: 350px;
            height: 300px;
            margin: 0 auto;
            /*水平方向居中*/
            /* border: 1px black solid; */
            text-align: center;
        }

        .grey {
            color: grey;
        }

        .container .row {
            width: 350px;
            height: 40px;

            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .container .row input {
            width: 260px;
            height: 30px;
        }

        #submit {
            width: 350px;
            height: 40px;
            background-color: orange;
            color: white;
            border: none;
            margin: 10px;
            border-radius: 5px;
            font-size: 20px;
        }
    </style>
</head>

<body>
    <div class="container">
        <h1>留言板</h1>
        <p class="grey">输入后点击提交, 会将信息显示下方空白处</p>
        <div class="row">
            <span>谁:</span> <input type="text" name="" id="from">
        </div>
        <div class="row">
            <span>对谁:</span> <input type="text" name="" id="to">
        </div>
        <div class="row">
            <span>说什么:</span> <input type="text" name="" id="say">
        </div>
        <input type="button" value="提交" id="submit" onclick="submit()">
        <!-- <div>A 对 B 说: hello</div> -->
    </div>

    <script src="js/jquery-3.7.1.min.js"></script>
    <script>
        getList();
        // 将 ajax 封装到一个方法中,提高可读性
        function getList() {
            $.ajax({
                type: "get",
                url: "/message/getList",
                success: function (message) { // 由于后端返回的是一个 List,因此使用 for 循环打印到页面上
                    for (let msg of message) {
                        // 构造 html 元素
                        let html = "<div>" + msg.from + " 对 " + msg.to + " 说: " + msg.message + "</div>";

                        // 把构造好的元素添加进去
                        $(".container").append(html);
                    }
                }
            });
        }

        // 给点击按钮注册点击事件
        function submit() {
            // 1. 获取到编辑框内容
            let from = $("#from").val();
            let to = $("#to").val();
            let say = $("#say").val();

            if (from == "" || to == "" || say == "") {
                alert("请检查输入内容");
                return;
            }
            // 检验完成,发起请求
            $.ajax({
                url: "/message/publish",
                type: "post",
                contentType: "application/json",
                data: JSON.stringify({
                    from: from,
                    to: to,
                    message: say
                }),
                success: function (result) {
                    // 打印日志
                    console.log(result);
                    console.log(result.ok);

                    // let o = JSON.parse(result);
                    
                    if (result.ok == 1) {
                        // 成功
                        alert("添加成功");
                        // 构造 html 元素
                        let html = "<div>" + from + " 对 " + to + " 说: " + say + "</div>";

                        // 把构造好的元素添加进去
                        $(".container").append(html);
                        // 同时清理之前输入框的内容
                        $(":text").val("");

                    } else {
                        // 失败
                        alert("添加失败");
                    }
                }
            });
        }

    </script>
</body>

</html>
总结:

5. 插件 EditStarter

前面插入 lombok 是通过一段 <dependency> 来插入的,以下有两种方式可以快速添加

方式一:去 maven 中央仓库查

复制上述 <dependency> 到 pom.xml 中即可

粘贴过来的代码段会有版本信息,而上面写的没有版本信息,但那时按住 Ctrl 将鼠标移到 lombok 上,显示出的版本信息也是 1.18.34

这是因为 Spring Boot 帮我们进行了版本管理

Spring Boot 不仅帮我们进行了版本管理,还帮我们处理了这些 jar 包之间的冲突

若我们有喜好的版本,也可以通过 <version> 来自己调整

方法二:EditStartes

在 pom.xml 文件中,单击右键,选择 Generate,如下图:

进入 EditStarters 的编辑界面,添加对应的依赖即可

tip:不是所有的依赖都可以在这里添加,这个界面和 Spring Boot 创建项目界面是一样的

在这里找不到的依赖,还是需要去 Maven 仓库查找坐标来添加

8.4 图书管理系统

需求:

登录:用户输入账号,密码完成登录

列表展示:展示图书

8.4.1 Bootstrap

这里使用的是版本 4

官网介绍:Bootstrap 是全球最受欢迎的前端框架,用于构建响应式、移动设备优先的网站。利用 jsDelivr 和我们提供的入门模板帮助你快速掌握 Bootstrap。

具体不展开

8.4.2 约定前后端交互接口

需求分析

图书管理系统是一个相对较大的案例,这里先实现其中一部分功能

根据需求可知,后端需要提供两个接口

账号密码校验接口:根据输入用户名和密码校验登录是否通过

图书列表:提供图书列表信息

接口定义

  1. 登录接口

URL:POST /user/longin

请求参数:name=admin&password=admin

响应:true // 账号密码验证成功;false // 账号密码验证失败

  1. 图书列表展示

URL:POST /book/getList

请求参数:无

响应:返回图书列表,如下:

javascript 复制代码
[
    {
        "id": 1,
        "bookName": "活着",
        "author": "余华",
        "num": 270,
        "price": 20,
        "publishName": "北京文艺出版社",
        "status": 1,
        "statusCN": "可借阅"
    },
    ...
]

字段说明

|-------------|--------------------|
| id | 图书 ID |
| bookName | 图书名称 |
| author | 作者 |
| num | 数量 |
| price | 定价 |
| publishName | 图书出版社 |
| status | 图书状态 1-可借阅;其他-不可借阅 |
| statusCN | 图书状态中文含义 |

8.4.3 服务器代码

创建图书类 BookInfo

java 复制代码
import lombok.Data;

import java.math.BigDecimal;

@Data
public class BookInfo {
    private Integer id; // 图书ID
    private String bookName; // 图书名称
    private String author; // 作者
    private Integer num; // 数量
    private BigDecimal price; // (定价)不要用 float 和 double 类型,可以用 long 或 BigDecimal 类型(精度问题)
    private String publishName; // 图书出版社
    private Integer status; // (图书状态)不建议用 Boolean 类型,若后续要增加状态很麻烦
    private String statusCN;
}

创建 UserController,实现登录验证接口

java 复制代码
import jakarta.servlet.http.HttpSession;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/user")
@RestController
public class UserController {
    @RequestMapping("/login")
    public Boolean login(String name, String password, HttpSession session) {
        // 1. 校验参数格式
        if (!StringUtils.hasLength(name) || !StringUtils.hasLength(password)) {
            return false;
        }
        // 2. 从数据库中校验账号和密码是否正确,但是当前未涉及到数据库,所以暂且写死
        if ("admin".equals(name) && "admin".equals(password)) {
            // 3. 如果正确,存储 Session,返回 true
            // session 的内容,取决于后面需要从 session 中获取什么
            session.setAttribute("uesrName", name);
            return true;
        }
        return false;
    }
}

创建 BookController,获取图书列表

java 复制代码
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

@RequestMapping("book")
@RestController
public class BookController {
    @RequestMapping("/getList")
    public List<BookInfo> getList() {
        // 从数据库中查询
        List<BookInfo> bookInfos = mockData();
        // 在企业开发中,此处不该返回字符串,就应该返回状态码,但是为了方便后续展开,返回字符串
        for (BookInfo bookInfo : bookInfos) {
            if (bookInfo.getStatus() == 1) {
                bookInfo.setStatusCN("可借阅");
            } else {
                bookInfo.setStatusCN("不可借阅");
            }
        }
        return bookInfos;
    }

    private List<BookInfo> mockData() {
        List<BookInfo> bookInfos = new ArrayList<>();
        for (int i = 1; i <= 15; i++) {
            BookInfo bookInfo = new BookInfo();
            bookInfo.setId(i);
            bookInfo.setBookName("图书" + i);
            bookInfo.setAuthor("作者" + i);
            bookInfo.setPublishName("出版社" + i);
            bookInfo.setNum(new Random().nextInt(100));
            bookInfo.setPrice(new BigDecimal(new Random().nextInt(70) + 10)); // 随机数 10-80
            bookInfo.setStatus(i % 5 == 0 ? 0 : 1); // 逢 5 的倍数设为 (没什么特殊含义)
            bookInfos.add(bookInfo);
        }
        return bookInfos;
    }
}

数据采用 mock 的方式

假数据,开发过程中前后端并行开发,假如前端先完成了工作,后端还没完成,前端不能干等着,而是按照接口定义造一些假数据来模拟后端传来的数据,根据 mock 的数据来进行测试

使用 Postman 测试后端代码

8.4.4 前端代码

登录页面:

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="css/bootstrap.min.css">
    <link rel="stylesheet" href="css/login.css">
    <script type="text/javascript" src="js/jquery.min.js"></script>
</head>

<body>
    <div class="container-login">
        <div class="container-pic">
            <img src="pic/computer.png" width="350px">
        </div>
        <div class="login-dialog">
            <h3>登陆</h3>
            <div class="row">
                <span>用户名</span>
                <input type="text" name="userName" id="userName" class="form-control">
            </div>
            <div class="row">
                <span>密码</span>
                <input type="password" name="password" id="password" class="form-control">
            </div>
            <div class="row">
                <button type="button" class="btn btn-info btn-lg" onclick="login()">登录</button>
            </div>
        </div>
    </div>
    <script src="js/jquery.min.js"></script>
    <script>
        function login() {
            // location.href = "book_list.html";
            // 先校验 userName 和 password 是否为空
            $.ajax({
                type: "post",
                url: "/user/login",
                data: {
                    name: $("#userName").val(),
                    password: $("#password").val()
                },
                success: function(result) {
                    if (result) {
                        // 登录成功
                        location.href = "book_list.html";
                    } else {
                        alert("账号或密码错误");
                    }
                }
            });
        }
    </script>
</body>

</html>

图书列表展示:

java 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图书列表展示</title>
    <link rel="stylesheet" href="css/bootstrap.min.css">

    <link rel="stylesheet" href="css/list.css">
    <script type="text/javascript" src="js/jquery.min.js"></script>
    <script type="text/javascript" src="js/bootstrap.min.js"></script>
    <script src="js/jq-paginator.js"></script>

</head>

<body>
    <div class="bookContainer">
        <h2>图书列表展示</h2>
        <div class="navbar-justify-between">
            <div>
                <button class="btn btn-outline-info" type="button" onclick="location.href='book_add.html'">添加图书</button>
                <button class="btn btn-outline-info" type="button" onclick="batchDelete()">批量删除</button>
            </div>
        </div>

        <table>
            <thead>
                <tr>
                    <td>选择</td>
                    <td class="width100">图书ID</td>
                    <td>书名</td>
                    <td>作者</td>
                    <td>数量</td>
                    <td>定价</td>
                    <td>出版社</td>
                    <td>状态</td>
                    <td class="width200">操作</td>
                </tr>
            </thead>
            <tbody id="bookList">

            </tbody>
        </table>

        <div class="demo">
            <ul id="pageContainer" class="pagination justify-content-center"></ul>
        </div>
        <script>

            getBookList();
            function getBookList() {
                $.ajax({
                    type: "get",
                    url: "/book/getList",
                    success: function(books) {
                        var finalHtml = "";
                        for (var book of books) {
                            finalHtml += '<tr>';
                            finalHtml += '<td><input type="checkbox" name="selectBook" value="'+book.id+'" id="selectBook" class="book-select"></td>';
                            finalHtml += '<td>'+book.id+'</td>';
                            finalHtml += '<td>'+book.bookName+'</td>';
                            finalHtml += '<td>'+book.author+'</td>';
                            finalHtml += '<td>'+book.num+'</td>';
                            finalHtml += '<td>'+book.price+'</td>';
                            finalHtml += '<td>'+book.publishName+'</td>';
                            finalHtml += '<td>'+book.statusCN+'</td>';
                            finalHtml += '<td>';
                            finalHtml += '<div class="op">';
                            finalHtml += '<a href="book_update.html?bookId='+book.id+'">修改</a>';
                            finalHtml += '<a href="javascript:void(0)" onclick="deleteBook('+book.id+')">删除</a>';
                            finalHtml += '</div>';
                            finalHtml += '</td>';
                            finalHtml += '</tr>';
                        }
                        $("#bookList").html(finalHtml);
                    }
                });
            }
    
            //翻页信息
            $("#pageContainer").jqPaginator({
                totalCounts: 100, //总记录数
                pageSize: 10,    //每页的个数
                visiblePages: 5, //可视页数
                currentPage: 1,  //当前页码
                first: '<li class="page-item"><a class="page-link">首页</a></li>',
                prev: '<li class="page-item"><a class="page-link" href="javascript:void(0);">上一页<\/a><\/li>',
                next: '<li class="page-item"><a class="page-link" href="javascript:void(0);">下一页<\/a><\/li>',
                last: '<li class="page-item"><a class="page-link" href="javascript:void(0);">最后一页<\/a><\/li>',
                page: '<li class="page-item"><a class="page-link" href="javascript:void(0);">{{page}}<\/a><\/li>',
                //页面初始化和页码点击时都会执行
                onPageChange: function (page, type) {
                    console.log("第"+page+"页, 类型:"+type);
                }
            });
            function deleteBook(id) {
                var isDelete = confirm("确认删除?");
                if (isDelete) {
                    //删除图书
                    alert("删除成功");
                }
            }
            function batchDelete() {
                var isDelete = confirm("确认批量删除?");
                if (isDelete) {
                    //获取复选框的id
                    var ids = [];
                    $("input:checkbox[name='selectBook']:checked").each(function () {
                        ids.push($(this).val());
                    });
                    console.log(ids);
                    alert("批量删除成功");
                }
            }

        </script>
    </div>
</body>

</html>

8.4.5 运行测试

9. 应用分层

目前我们程序的代码有点"杂乱",然而当前只是"一点点功能"的开发,如果将整个项目功能完成,代码会更加的"杂乱无章"(文件乱,代码内容乱),因此可以引入应用分层来解决该问题

9.1 介绍

阿里开发手册中,关于工程结构部分,定义了常见工程的应用分层结构:

什么是应用分层

应⽤分层是⼀种软件开发设计思想,它将应⽤程序分成N个层次,这N个层次分别负责各⾃的职责,多个 层次之间协同提供完整的功能.根据项⽬的复杂度,把项⽬分成三层,四层或者更多层.

常⻅的MVC设计模式,就是应⽤分层的⼀种具体体现.

为什么要应用分层

在最开始的时候,为了让项⽬快速上线,我们通常是不考虑分层的.但是随着业务越来越复杂,⼤量的 代码混在⼀起,会出现逻辑不清晰、各模块相互依赖、代码扩展性差、改动⼀处就牵⼀发⽽动全⾝等 问题.所以学习对项⽬进⾏分层就是我们程序员的必修课了.

如何分层(三层架构)

上⼀节中学习的"MVC", 就是把整体的系统分成了Model(模型),View(视图)和Controller (控制器)三个层次,也就是将⽤⼾视图和业务处理隔离开,并且通过控制器连接起来,很好地实现 了表现和逻辑的解耦,是⼀种标准的软件分层架构

⽬前现在更主流的开发⽅式是"前后端分离"的⽅式,后端开发⼯程师不再需要关注前端的实现, 所以对 于Java后端开发者,⼜有了⼀种新的分层架构:把整体架构分为表现层、业务逻辑层和数据层.这种分层 ⽅式也称之为"三层架构".

  • 表现层:就是展示数据结果和接受用户指令的,最靠近用户的一层
  • 业务逻辑层:负责处理业务逻辑,里面有复杂业务的具体实现
  • 数据层:负责存储和管理与应用程序相关的数据

前面的代码并不符合这种设计思想,将所有的代码都堆砌在一起了

按照上面的层次划分,Spring MVC 站在后端开发人员的角度上,也进行了支持,把上面的代码划分为三个部分:

  • 请求处理、响应数据:负责接收页面的请求,给页面响应数据
  • 逻辑处理:负责业务逻辑处理的代码
  • 数据访问:负责业务数据的维护操作,包括增、删、改、查等操作

这三个部分,在 Spring 的实现中,均有体现:

  • Controller:控制层,接收前端发送的请求,对请求进行处理,并响应数据
  • Service:业务逻辑层,处理具体的业务逻辑
  • Dao:数据访问层,也称为持久层,负责数据访问操作,包括数据的增、删、改、查

9.2 代码重构

使用上面的分层思想,对代码进行改造

9.2.1 先创建对应的包路径,并把代码转移到对应的目录

9.2.2 代码拆分

控制层

java 复制代码
import com.example.springbook.model.BookInfo;
import com.example.springbook.service.BookService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RequestMapping("book")
@RestController
public class BookController {
    @RequestMapping("/getList")
    public List<BookInfo> getList() {
        BookService bookService = new BookService();
        List<BookInfo> bookInfos = bookService.getList();
        return bookInfos;
    }
}

业务逻辑层

java 复制代码
import com.example.springbook.dao.BookDao;
import com.example.springbook.model.BookInfo;

import java.util.List;

public class BookService {
    public List<BookInfo> getList() {
        BookDao bookDao = new BookDao();
        // 从数据库中查询
        List<BookInfo> bookInfos = bookDao.mockData();
        // 在企业开发中,此处不该返回字符串,就应该返回状态码,但是为了方便后续展开,返回字符串
        for (BookInfo bookInfo : bookInfos) {
            if (bookInfo.getStatus() == 1) {
                bookInfo.setStatusCN("可借阅");
            } else {
                bookInfo.setStatusCN("不可借阅");
            }
        }
        return bookInfos;
    }
}

数据访问层

java 复制代码
import com.example.springbook.model.BookInfo;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class BookDao {
    public List<BookInfo> mockData() {
        List<BookInfo> bookInfos = new ArrayList<>();
        for (int i = 1; i <= 15; i++) {
            BookInfo bookInfo = new BookInfo();
            bookInfo.setId(i);
            bookInfo.setBookName("图书" + i);
            bookInfo.setAuthor("作者" + i);
            bookInfo.setPublishName("出版社" + i);
            bookInfo.setNum(new Random().nextInt(100));
            bookInfo.setPrice(new BigDecimal(new Random().nextInt(70) + 10)); // 随机数 10-80
            bookInfo.setStatus(i % 5 == 0 ? 0 : 1); // 逢 5 的倍数设为 (没什么特殊含义)
            bookInfos.add(bookInfo);
        }
        return bookInfos;
    }
}

9.3 应用分层的好处

降低层与层之间的依赖,结构更加明确,利于各层逻辑的复用

开发人员可以只关注整个结构中的某一层,极大的降低了维护成本和维护时间

可以很容易的用新的实现来替换原有层次的实现

有利于标准化

9.4 MVC 和三层架构的区别和联系

从概念上来讲,⼆者都是软件⼯程领域中的架构模式.

MVC架构模式由三部分组成,分别是:模型(Model),视图(View)和控制器(Controller).

三层架构将业务应⽤划分为:表现层,业务逻辑层,数据访问层.

MVC中,视图和控制器合起来对应三层架构中的表现层.模型对应三层架构中的业务逻辑层,数据层, 以及实体类

⼆者其实是从不同⻆度对软件⼯程进⾏了抽象.

MVC模式强调数据和视图分离,将数据展⽰和数据处理分开,通过控制器对两者进⾏组合.

三层架构强调不同维度数据处理的⾼内聚和低耦合,将交互界⾯,业务处理和数据库操作的逻辑分开.

⻆度不同也就谈不上互相替代了,在⽇常的开发中可以经常看到两种共存的情况,⽐如我们设计模型层的时候往往也会拆分出业务逻辑层(Service层)和数据访问层(Dao层)

但是⼆者的⽬的是相同的,都是"解耦,分层,代码复⽤"

9.5 软件设计原则:高内聚低耦合

⾼内聚指的是:⼀个模块中各个元素之间的联系的紧密程度,如果各个元素(语句、程序段)之间的联 系程度越⾼,则内聚性越⾼,即"⾼内聚"。

低耦合指的是:软件中各个层、模块之间的依赖关联程序越低越好。修改⼀处代码,其他模块的代码 改动越少越好.

⾼内聚低耦合⽭盾吗?

不⽭盾,⾼内聚指的是⼀个模块中各个元素之间的联系的紧密程度,低耦合指的是各个模块之间的紧 密程度

这就好⽐⼀个企业,包含很多部⻔,各个部⻔之间的关联关系要尽可能的⼩,⼀个部⻔发⽣问题,要尽 可能对降低对其他部⻔的影响,就是耦合. 但是部⻔内部员⼯关系要尽量紧密,遇到问题⼀起解决,克服.这叫做内聚.

⽐如邻⾥邻居,楼上漏⽔,楼下遭殃,就是耦合. 家庭⼀个成员⽣病,其他成员帮忙照顾,就叫内聚. ⼀个家庭内部的关系越紧密越好, ⼀个家庭尽可能少的影响另⼀个家庭,就是低耦合.

10. 总结

1. 注解总结

@RequestMapping:路由映射

@RequestParam:后端参数重命名

@RequestBody:接收 JSON 类型的参数

@PathVariable:接收路径参数

@RequestPart:上传文件

@ResponseBody:返回数据

@CookieValue:从 Cookie 中获取值

@SessionAttribute:从 Session 中获取值

@RequestHeader:从 Header 中获取值

@Controller:定义一个控制器,Spring 框架启动时加载,把这个对象交给 Spring 管理,默认返回视图

@RestController:@ResponseBody + @Controller 返回数据

Cookie和Session都是会话机制,Cookie是客⼾端机制,Session是服务端机制.⼆者通过SessionId 来关联.SpringMVC内置HttpServletRequest,HttpServletResponse两个对象.需要使⽤时,直接在 ⽅法中添加对应参数即可,Cookie和Session可以从HttpServletRequest中来获取,也可以直接使⽤ HttpServletResponse设置Http响应状态码.

相关推荐
码蜂窝编程官方1 分钟前
【含开题报告+文档+PPT+源码】基于SpringBoot+Vue的虎鲸旅游攻略网的设计与实现
java·vue.js·spring boot·后端·spring·旅游
gqkmiss1 分钟前
Chrome 浏览器 131 版本开发者工具(DevTools)更新内容
前端·chrome·浏览器·chrome devtools
Summer不秃7 分钟前
Flutter之使用mqtt进行连接和信息传输的使用案例
前端·flutter
旭日猎鹰11 分钟前
Flutter踩坑记录(二)-- GestureDetector+Expanded点击无效果
前端·javascript·flutter
Viktor_Ye17 分钟前
高效集成易快报与金蝶应付单的方案
java·前端·数据库
hummhumm19 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
AuroraI'ncoding36 分钟前
时间请求参数、响应
java·后端·spring
乐闻x1 小时前
Vue.js 性能优化指南:掌握 keep-alive 的使用技巧
前端·vue.js·性能优化
一条晒干的咸魚1 小时前
【Web前端】创建我的第一个 Web 表单
服务器·前端·javascript·json·对象·表单
Amd7941 小时前
Nuxt.js 应用中的 webpack:compiled 事件钩子
前端·webpack·开发·编译·nuxt.js·事件·钩子