一、什么是 Spring Web MVC
1、理清关系
Spring Web(关于 Web 开发的框架)从一开始就包含在 Spring 框架中,而 Spring Web 中包含一个模块叫 Spring-webmvc,因此 Spring Web 又称为 Spring Web MVC = Spring MVC。而 Spring Boot 是对 Spring 的进一步包装。简而言之,Spring 框架包含 Spring Web 模块,Spring Web = Spring MVC = Spring Web MVC,它实现了 MVC 这种设计模式,而 Spring Boot 用更高级的方式实现了 MVC 设计模式,所以 Spring Boot 项目中可以进行 Spring Web 开发。
2、什么是 MVC 设计模式

但是现在为了提高开发效率,Spring Web 中直接没有 View 这一部分了,View 是前端的任务,我们负责的只是 Controller 和 Model:

学习 Sring MVC,就是学习:
- **建立连接:**浏览器访问一个地址,能够调用我们的 Sring 程序。
- **请求:**获取到浏览器传入的参数。
- **响应:**执行业务逻辑后,把结果返回给浏览器。
二、建立连接(@RequestMapping)
作用:实现 URL 路由映射。就是浏览器访问一个 URL,能将请求对应到项目中的某个类的某个方法。
作用对象:类、方法 。URL 的路径通常由类路径+方法路 径构成,所以类、方法都要加这个注解。我们要在类上加注解,原因有两个:(1)增强可读性,在熟悉项目的过程中,快速找到程序的入口。(2)避免冲突,保证不同类中的同名方法的路径不冲突。
注意事项:
1、路径不能重复

报错:

准确讲是路径+请求方式唯一:



2、路径通过 @RestController 搜索
因为项目中会导入很多第三方包,所以类和方法就很多,无法知道类中的哪些方法是要执行的。因此规定,加了 @RestController 的类才会被继续搜索,加了 @RequestMapping 的方法才会被执行。
因此,没有写对注解的类,不会被映射到,那么也就没有该资源路径,跟 url 写错时浏览器的报错一样。
3、URL 路径中第一个 / 可以不加
但是加上最好。


4、支持所有方法的请求
浏览器访问 URL,默认发送的是GET请求,显然支持 GET。想测试其它请求,可以用 form 表单构造:



支持 POST 请求:

但实际上后端是不写前端代码的,这种方式测试效率太低了,我们用 postman:


对 postman 的body的一些解释:

5、可以指定支持的方法(@GetMapping)
浏览器能够成功收到响应,肯定是后端能够支持请求的方法。通过 @RequestMapping 指定:method 属性是数组,可以设置支持多个方法。


注意:当只有 value 属性时,可以省略属性。其他情况不能省略属性。
还有其它注解,每种方法都是类似的:

三、请求
1、用方法的参数获取请求的参数


2、参数类型不匹配,报错 400
我后端要的 int,你传的是 String,请求错误,报错 400(Bad Request):



3、前端多了参数
没有任何问题:

4、前端少了参数
如果不是基础类型 ,默认为null ;如果是基础类型,boolean 默认为 false ,其它报错 500(程序异常):





因此,工作中一般使用包装类。基础类型只有在明确前端一定会传递参数,或者会设置默认值的情况下使用。
5、多个参数传递
前端传参的顺序可以打乱:

参数太多时,URL 暴露太多参数且不太美观,使用 POST,最好在 body 放参数:



注意,在postman 中,使用 post 方法无法通过 body 传递数据(这是 postman 根据 get 和 post 语义限制的,不是 http 协议的限制):

接口 (就是 URL 路由映射执行的方法)的声明不能随意改动 ,后端改了参数,前端也要跟着改,这是欠揍的行为。并且参数太多了也不好看,所以要包装成对象。
6、对象传递
java
package com.edu.springbootdemo.entity;
public class Person {
private String name;
private int age;
private String 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 String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
'}';
}
}


好处1 :对于对象 ,如果基础类型属性 前端没有传参 ,会设置对应的默认值:

好处2 :不用改动接口。如果想加参数,直接在类里加。如果加的非必要参数,对前端没有任何影响;如果加的必要参数,对前端有影响。
7、后端参数重命名(@RequestParam)
后端 ,我们希望代码可读性好 ,那么参数的命名最好有意义。而前端 ,更希望别人看不懂传递的参数,所以不要求可读性。这时,可以在后端重命名,将可读性差的前端参数,改为可读性好的参数名:


但是 @RequestParam默认重命名参数是必需参数,不传会报错误:

日志警告:

设置属性,改为非必须:


8、数组传递

前端两种方式:


9、集合传递(@RequestParam)
List 集合类传递跟数组一样,不过要用 @RequestParam 参数绑定(List 是接口也行):





10、JSON 传递数据
当传的数据格式很复杂时,比如又有对象,又有集合类,前端该怎么传数据?


这时就需要用到 JSON 数据格式。实际工作中,通常只有一个参数 时会用方法参数传 ,其余 的都会用 body 里的 JSON 传。
10.1、语法
- 分隔键值对:冒号。
- 分隔数据:逗号。
- 对象用:{}。
- 数组用:[]。
这种是没压缩的 JSON,可读性强:

这种是压缩的 JSON,没有空格,可读性差,但占带宽少:

可以用 JSON 格式化工具校验和排版(随便搜一个):
在线JSON校验格式化工具(Be JSON)https://www.bejson.com/
10.2、JSON 和 Java 对象的转换
常用工具:jackson(String MVC 中集成的,我们用这个),如果不在 Spring MVC 项目中用,要加依赖:
XML
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.5</version>
</dependency>
Sring Boot 中有:

fastjson/fastjson2 :阿里巴巴的,用 fastjson2,fastjson漏洞太多。gson,谷歌的。
接下来进行转换,在测试代码中写。测试代码的类名一般与源代码中的类名对应,只不过名字中多了一个 Test 部分。


如果是使用 fastjson2 或者 gson 也是一样的,会提供接口调用,只不过需要配置依赖。
注意 :对象中必须要有无参构造方法 (如果没有自定义构造方法,会有一个默认的无参构造方法;如果自定义了,无参构造方法就没了。),因为 json 转 person 对象,会先创建一个对象,这个对象是通过无参构造方法创建的。我们要养成习惯:给对象加无参构造方法。
10.3、传递 JSON 数据(@RequestBody)



10.4、JSON 的优势
- 跨平台:在多种语言上兼容,比如 Python、C++ 等。
- 简单、可读性强。
- 轻量:相对于 XML。
- 易于扩展:可以嵌套很多对象、数组、集合类对象。
- 安全性强:纯文本,不含可执行代码。
11、接收 URL 路径中的参数(@PathVariable)
一般每个文章、视频都有唯一的资源路径,有时我们想访问某个资源路径,就需要获取 URL 路径中的参数:


接收到的 {articleId} 绑定到 @PathVariable 后跟着的 articleId,当然也可以重命名为 id。


不能少参数,因为没有这个资源路径,报错 404:

也不能交换顺序,虽然路径能对应上,但是类型不匹配,报错 400 请求错误:


12、接收文件(@RequestPart)




其中的transferTo 方法表示上传文件,其实就是对文件 I/O 操作代码的包装:





如果需要重命名 ,用 @RequestPart:


13、获取 Cookie/Session
13.1、什么是 Cookie/Session
HTTP 协议 是"无状态"的,即没有记忆 ,客户端和服务器的通信数据,在下一次是记不住的。我们就可以用 Cookei/Session 搭配 ,让 HTTP 协议能够将多次的通信联系起来。
比如在登录场景中,通过这种方式,让第一次登录跟后续的访问联系起来:

Session 是一个类哈希结构:

Cookie 就像是客户端持有的令牌(比如泡汤泉时,可以通过令牌反复入场、出场);Session 就像服务器持有的档案(记录该令牌的所有信息)。Cookie 可以伪造(再买一个一摸一样的令牌产品),但是 Session 不能伪造(存在别人系统里的,管理员才能访问)。因此,Cookie 是不安全 (在浏览器客户端中可以设置),Session 是安全的(由操作系统生成,存在服务器内存中的,所以不能改 SessionId 的值,但是后端代码可以设置其对应的 Session 对象存储的键值对信息)。
13.2、获取 Cookie
(1)传统方法
Spring 是基于 Servlet 框架实现的,我们可以使用 原始的 Servlet 提供的接口 获取 HTTP 请求和响应 ,这种方法的一个优势 :可以获取请求/响应中的所有信息(不只是 Cookie)。
比如,获取请求中的请求字符的值:


所以我们可以用 HttpServletRequest 获取 cookie:

当没有设置 Cookie 时,HttpServletRequest 返回的 Cookie 为空:

在postman 伪造 cookie:


在浏览器伪造 cookie:会话级别cookie,浏览器关闭才会删除。跟服务器没关系。


(2)注解方法(@CookieValue)
相当于把上面的 Servlet 方法包装了一下:

13.3、存储 Session
通过 HTTP 请求来获取 session 对象。如果请求中的 cookie 没有 sessionId,则默认创建新的 session 对象;如果 cookie 中有 sessionId,则获取对应的 session 对象。
getSession:

然后在 session 对象中设置键值对:




13.4、获取 Session(@SessionAttribute)

因为服务器重启,存储在内存中的 session 对象会删除,重启后:

设置 session 再访问:




以上是 Edge 浏览器的会议对象,接下来换成 Chrome 浏览器:

再将 Edge 浏览器的 id 拿到 Chrome 浏览器访问,内容变成了 woman:
可以得出结论:不同客户端会话,通过 cookie 中保存的 session 来区分。
14、获取 Header(@RequestHeader)

四、响应
1、返回静态页面(@Controller)
@RequestController = @Controller + @ResponseBody
- @Controller: 用于标识某个对象是控制器 ,返回视图(在以前)。
- @ResponseBody: 用于返回数据(而现在,项目常是前后端完全分离,因此不再返回视图,只返回数据)。
- @RequestController: 两者的结合,既用于标识某个类是控制器 ,又用于标识返回的是数据。


返回视图 ,不加 / ,表示相对于 static 下的 /return/test.html:


返回视图 ,加 / ,表示相对于 static 下的 /test.html:


如果 URL 中没有路径 ,则默认访问 index.html、index.htm、index.jsp(tomcat 的设定):

如果刷新了没变,有三种方法(这是前端经常出现的问题,有前端缓存):
- crl + F5:强制刷新。
- crl + shift + delete:清除缓存。cookie 一般不删,里面有一些登录信息,删了还要重新输。

- 确认后端代码:映射路径是否正确。清除后端缓存(maven 命令 clean,如果 target 文件没删掉,手动删)。
2、返回数据(@RequestBody)
只有 @Controller ,如果返回 的不是视图,而是数据 ,则会报错 404(没有这个视图文件):


@Controller 加上 @ResponseBody ,返回数据:


或者直接使用 @RequestController
3、返回 HTML 代码片段
当返回的数据是 HTML 代码片段时,浏览器 会自动解析为 HTML:


原因是响应默认字符串 为text/html 类型:

4、返回 JSON 数据
响应中,默认对象 是application/json类型:



5、设置状态码
状态码可以设置,并且返回的页面内容(可以自由排版)也跟状态码无关。
我的页面:



浏览器设计的 404 页面:

b 站设计的 404 页面:

6、设置 Header
6.1、设置 Content-Type
(1)使用 Servlet 接口
html 如果不想被解析,可以设置为text/plain(纯文本)类型:


(2)使用注解(@RequestMapping)

也可以指定返回的字符串 是 json 类型:



6.2、设置其它 Header


五、综合练习
1、工作中项目开发的流程
- 开需求说明会。(产品经理设计好需求文档后召开,所有人坐一起看需求合不合理)
- 分工
- 方案设计(概要设计,详细设计(功能分工),数据库设计(一般详细设计中就涉及了),接口设计(写好了就要给调用方,如前端)(没有标准格式,模仿前辈的格式写))
- 方案 review (看看接口啥的合不合理)
- 开发
- 自测(postman)
-
提测(测试人员的工作)
-
联调(前后端坐一起测试)
-
上线
2、计算器
需求:加法计算器功能,对两个整数进⾏相加,需要客⼾端提供参与计算的两个数,服务端返回这两个整数计算的结果。
(1)接口设计
接口定义:(为什么用 POST:我想把请求参数放到正文,但是后端代码绑定不到 postman 发起的 GET 请求中放正文的参数)
|------|----------|
| 请求路径 | calc/sum |
| 请求方式 | POST |
| 接口描述 | 计算两个整数的和 |
请求参数:
|------|---------|------|-------|
| 参数名 | 类型 | 是否必须 | 备注 |
| num1 | Integer | 是 | 第一个整数 |
| num2 | Integer | 是 | 第二个整数 |
响应数据:
|--------------|-----------|
| Content-Type | text/html |
| 正文(输入不合法) | 输入不合法 |
| 正文(输入合法) | 计算结果:8 |
(2)后端代码
java
package com.edu.springbootdemo.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/calc")
public class CalcController {
@PostMapping("/sum")
public String sum(Integer num1, Integer num2) {
if(num1 == null || num2 == null) {
return "输入不合法";
}
Integer sum = num1 + num2;
return "计算结果:" + sum;
}
}
(3)前端代码
AI 生成、copy 模板等:
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 标签内容中,接收到的带 name 的标签的数据,提交到 calc/sum 路径 -->
<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>
(4)接口测试
- 后端接口测试:


- 前端接口测试:




3、用户登录
需求:⽤⼾输⼊账号和密码,后端进⾏校验密码是否正确。
- 如果不正确,前端进⾏⽤⼾告知
- 如果正确,跳转到⾸⻚.⾸⻚显⽰当前登录⽤⼾
- 后续再访问⾸⻚,可以获取到登录⽤⼾信息
(1)接口设计
- 校验接口
接口定义:
|------|------------|
| 请求路径 | user/login |
| 请求方式 | POST |
| 接口描述 | 校验账号密码是否正确 |
请求参数:
|----------|--------|------|--------|
| 参数名 | 类型 | 是否必须 | 备注 |
| username | String | 是 | 校验的用户名 |
| password | String | 是 | 校验的密码 |
响应数据:
|----|----------------------|
| 正文 | 校验成功:true 校验失败:false |
- 查询接口
接口定义:
|------|-------------------|
| 请求路径 | user/getLoginUser |
| 请求方式 | GET |
| 接口描述 | 查询登录用户的信息 |
请求参数:无
响应数据:
|--------------|-----------|
| Content-Type | text/html |
| 正文 | zhangsan |
(2)后端代码
java
package com.edu.springbootdemo.Controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class userController {
@PostMapping("/login")
public Boolean login(String username, String password, HttpSession session) {
/**
* 1、参数校验(是否为空、参数范围)
* 2、判断密码是否匹配
*/
// if(username == null || password == null || username.length() == 0 || password.length() == 0) {
// return false;
// }
// if(StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
// if("admin".equals(username) && "admin".equals(password)) {
// return true;
// }
// }
if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
return false;
}
// TODO: 可改成数据库操作,暂且硬编码
if("admin".equals(username) && "admin".equals(password)) {
// 建立会话,保存用户名
session.setAttribute("username", username);
return true;
}
return false;
}
@GetMapping("getLoginUser")
public String getLoginUser(HttpServletRequest request) {
// 会话不存在,返回 null
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
// 从会话中获取用户名,并返回
return (String) session.getAttribute("username");
}
}

(3)前端代码
使用 ajax 技术:发起请求,等待后端处理,显示结果。
login.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="jquery-3.7.1.min.js"></script>
<script>
function login() {
$.ajax(
{
url: "/user/login",
method: "post",
data: {
username: $("#userName").val(),
password: $("#password").val()
},
success: function(result) {
if(result) {
location.href = "index.html";
} else {
alert("用户名或密码错误");
}
}
}
);
}
</script>
</body>
</html>
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="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script>
$.ajax({
type: "get",
url: "/user/getLoginUser",
success:function(userName){
$("#loginUser").text(userName);
}
});
</script>
</body>
</html>
(4)接口测试
- 后端接口测试:
校验接口:


查询接口:

- 前端接口测试:
未登录状态:


登陆状态:


4、留言板
需求:
- 输⼊留⾔信息,点击提交.后端把数据存储起来.
- ⻚⾯展⽰输⼊的表⽩墙的信息
(1)接口设计
- 获取全部留言
接口定义:
|------|-----------------|
| 请求路径 | message/getList |
| 请求方式 | GET |
| 接口描述 | 获取所有留言 |
请求参数:无
响应数据:
|--------------|------------------------------------------------------------------------------------------------|
| Content-Type | application/json |
| 正文 | [ { "from": xxx, "to": xxx, "say": xxx }, { "from": xxx, "to": xxx, "say": xxx }, //...... ] |
- 发表新留言
接口定义:
|------|--------------------|
| 请求路径 | message/addMessage |
| 请求方式 | POST |
| 接口描述 | 发表新留言 |
请求参数:
|-------------|-------------|------|-------------------------------------------------|
| 参数名 | 类型 | 是否必须 | 备注 |
| messageInfo | MessageInfo | 是 | 留言信息 格式: { "from": xxx, "to": xxx, "say": xxx } |
响应数据:
|----|----------------------|
| 正文 | 添加成功:true 添加失败:false |
(2)lombok 工具
作用:添加注解,减少冗余代码,如类中的 get、set、toString 等方法。

下载方式:
- 安装 EditStarters 插件:
pom 文件里,右键 Generate


如果出现超时问题,换成阿里云镜像:https://start.aliyun.com/

- 上面这个方法找不到的,就从仓库中查询。
- lombok 不建议在新建项目的时候加入依赖,会出现 bug。
(3)后端代码
java
package com.edu.springbootdemo.Controller;
import com.edu.springbootdemo.entity.MessageInfo;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/message")
public class MessageController {
private List<MessageInfo> messageList = new ArrayList<>();
@GetMapping("/getList")
public List<MessageInfo> getMessages() {
return messageList;
}
@PostMapping("/addMessage")
public Boolean addMessage(@RequestBody MessageInfo messageInfo) {
System.out.println("接收到参数:" + messageInfo);
if(!StringUtils.hasLength(messageInfo.getFrom()) || !StringUtils.hasLength(messageInfo.getTo()) || !StringUtils.hasLength(messageInfo.getSay())) {
return false;
}
messageList.add(messageInfo);
return true;
}
}
(4)前端代码
(5)接口测试
- 后端接口测试:
添加留言接口:


获取所有留言接口:

- 前端接口测试:

前端提交后没有反应(碰到这种情况,第一反应清缓存,或者 F12 看日志):

后端日志也出现警告:

分析哪里出错:前端代码是从之前的代码复制过来的,肯定没问题。后端代码也测试了没问题。剩下的肯定是前后端交互的问题。

添加正文类型:JSON

留言:

关掉窗口重新进入:

5、图书管理系统
需求:
- 登录:⽤⼾输⼊账号,密码完成登录功能
- 列表展⽰:展⽰图书
准备:新建一个项目,加入 lombok 依赖(从这里加入会有 bug,后面说怎么解决)。

如果新建项目出现标红,清缓存:

如果发现 lombok 不起作用,把 POM 文件多余的插件删了:

(1)接口设计
- 登录接口:
接口定义:
|------|--------------|
| 请求路径 | user/login |
| 请求方式 | POST |
| 接口描述 | 校验用户名、密码是否正确 |
请求参数:
|----------|--------|------|-----|
| 参数名 | 类型 | 是否必须 | 备注 |
| username | String | 是 | 用户名 |
| password | String | 是 | 密码 |
响应数据:
|----|----------------------|
| 正文 | 校验正确:true 校验错误:false |
- 展示接口:
接口定义:
|------|--------------|
| 请求路径 | book/getList |
| 请求方式 | GET |
| 接口描述 | 获取所有图书信息的列表 |
请求参数:无
响应数据:
|----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 正文 | JSON 格式 [{ id: 图书ID, bookName: 图书名, author: 作者, count: 数量, price: 定价, publish: 图书出版社, status: 图书状态 1-可借阅, 其它-不可借阅, statusCN: 图书状态中文含义(一般是前端处理,后端只需要返回状态码。这个练习比较反常,后端处理了) }, ......] |
(2)后端代码
BookInfo:
java
package com.edu.springbookdemo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class BookInfo {
private Integer id;
private String bookName;
private String author;
private Integer count;
// 不要用 float、double 类型,因为精度问题
// 推荐使用 BigDecimal 类型;或者 long 类型,将单位换算成分,例如 1.1 元 = 110 分
private BigDecimal price;
private String publish;
private Integer status; // 0-不可借阅,1-可借阅
private String statusCN;
}
UserController:
java
package com.edu.springbookdemo;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public Boolean login(String username, String password) {
if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
return false;
}
// TODO: 目前未学数据库操作,暂且写死
if ("admin".equals(username) && "admin".equals(password)) {
return true;
}
return false;
}
}
BookController:
java
package com.edu.springbookdemo;
import org.springframework.web.bind.annotation.GetMapping;
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;
@RestController
@RequestMapping("/book")
public class BookController {
@GetMapping("/getList")
public List<BookInfo> getBookList() {
List<BookInfo> bookList = mockBookList();
for (BookInfo book : bookList) {
if (book.getStatus() == 1) {
book.setStatusCN("可借阅");
} else {
book.setStatusCN("不可借阅");
}
}
return bookList;
}
/**
* 模拟数据库返回的数据
* @return
*/
private List<BookInfo> mockBookList() {
List<BookInfo> bookList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
BookInfo book = new BookInfo();
book.setId(i);
book.setBookName("图书" + i);
book.setAuthor("作者" + i);
book.setCount(i*5+1);
book.setPrice(new BigDecimal(new Random().nextInt(100)));
book.setPublish("出版社" + i);
book.setStatus(i%5);
// 状态中文名不会存储在数据库中,而是通过状态值转换得到
bookList.add(book);
}
return bookList;
}
}
(3)前端代码
导入模板,实现 js 部分:
login.html:
html
<script>
function login() {
$.ajax({
url: "/user/login",
type: "POST",
data: {
username: $("#userName").val(),
password: $("#password").val()
},
success: function(result) {
if (result) {
location.href = "book_list.html";
} else {
alert("用户名或密码错误!");
}
}
});
// location.href = "book_list.html";
}
</script>
book_list.html:
html
<script>
getBookList();
function getBookList() {
$.ajax({
url: "/book/getList",
type: "get",
success: function (bookList) {
let html = "";
for (let book of bookList) {
html += "<tr><td><input type=\"checkbox\" name=\"selectBook\" value=" + book.id + " id=\"selectBook\" class=\"book-select\"></td>";
html += "<td>" + book.id + "</td>";
html += "<td>" + book.bookName + "</td>";
html += "<td>" + book.author + "</td>";
html += "<td>" + book.count + "</td>";
html += "<td>" + book.price + "</td>";
html += "<td>" + book.publish + "</td>";
html += "<td>" + book.statusCN + "</td>";
html += "<td><div class=\"op\"><a href=\"book_update.html?bookId=1\">修改</a><a href=\"javascript:void(0)\" onclick=\"deleteBook(1)\">删除</a>";
html += "</div></td></tr>";
}
$("tbody").append(html);
}
});
}
<script>
(4)接口测试
- 登录接口测试:
后端接口测试:



前端接口测试:

- 展示接口测试:
后端接口测试:

前端接口测试:

六、应用分层
1、三层架构
MVC 将视图展示和逻辑处理分隔,再通过控制器连接,实现了解耦。但目前主流 "前后端分离",MVC 不再适用。现在更青睐于三层架构:
- 表现层 Controller:接收请求参数、返回展示的数据。
- 业务逻辑层 Service:对数据进行加工。
- 数据访问层 Dao:访问数据并进行整合。

实体类就如 BookInfo。还有细分,如 model、entity、pojo 等,后续数据库部分学习。
分层的目的 :实现高内聚,低耦合。模块内部关系紧密,模块间互相影响程度尽量小。
优点:易于各层逻辑复用(想用某层直接调用,不用管其他层)、易于替换(直接替换某个层,不会影响其他层)、可读性增强(根据分层名字快速分清每个模块的大致功能)。
2、企业规范
可参考 《阿里巴巴 Java 开发手册》,具体根据企业要求。
常用命名规程:
- 类名:大驼峰。首字母大写,UpperCamelCase。
- 方法名、参数名、变量名:小驼峰。首字母小写,localValue。
- 常量:全部大写。MAX_STOCK_COUNT。
- 抽象类:Abstract 或 Base开头。
- 异常类:Exception 结尾。
- 测试类:Test 结尾。
- 包名:全小写。
3、将图书管理系统中的代码分层
因为数据库操作还没学,暂时不分登录接口。下面分层展示接口:
BookDao:负责访问、整合数据。
java
package com.edu.springbookdemo.dao;
import com.edu.springbookdemo.entity.BookInfo;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class BookDao {
/**
* 模拟数据库返回的数据
* @return
*/
public List<BookInfo> mockBookList() {
List<BookInfo> bookList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
BookInfo book = new BookInfo();
book.setId(i);
book.setBookName("图书" + i);
book.setAuthor("作者" + i);
book.setCount(i*5+1);
book.setPrice(new BigDecimal(new Random().nextInt(100)));
book.setPublish("出版社" + i);
book.setStatus(i%5);
// 状态中文名不会存储在数据库中,而是通过状态值转换得到
bookList.add(book);
}
return bookList;
}
}
BookService:负责处理数据
java
package com.edu.springbookdemo.service;
import com.edu.springbookdemo.entity.BookInfo;
import com.edu.springbookdemo.dao.BookDao;
import java.util.List;
public class BookService {
public List<BookInfo> getBookList() {
BookDao bookDao = new BookDao();
List<BookInfo> bookList = bookDao.mockBookList();
for (BookInfo book : bookList) {
if (book.getStatus() == 1) {
book.setStatusCN("可借阅");
} else {
book.setStatusCN("不可借阅");
}
}
return bookList;
}
}
BookController:负责接收参数,返回展示数据
java
package com.edu.springbookdemo.controller;
import com.edu.springbookdemo.entity.BookInfo;
import com.edu.springbookdemo.service.BookService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/book")
public class BookController {
@GetMapping("/getList")
public List<BookInfo> getBookList() {
BookService bookService = new BookService();
return bookService.getBookList();
}
}
目录:
