【JavaEE】(13) Spring Web MVC 入门

一、什么是 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 对象存储的键值对信息)。

(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、工作中项目开发的流程

  1. 开需求说明会。(产品经理设计好需求文档后召开,所有人坐一起看需求合不合理)
  2. 分工
  • 方案设计(概要设计,详细设计(功能分工),数据库设计(一般详细设计中就涉及了),接口设计(写好了就要给调用方,如前端)(没有标准格式,模仿前辈的格式写))
  • 方案 review (看看接口啥的合不合理)
  • 开发
  • 自测(postman)
  1. 提测(测试人员的工作)

  2. 联调(前后端坐一起测试)

  3. 上线

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();
    }
}

目录: