当前位置: 首页 > news >正文

Spring MVC:综合练习 - 深刻理解前后端交互过程

目录

1. Lombok

1.1 引入 lombok 依赖

1.1.1 通过 Maven 仓库引 lombok 依赖

 1.1.2 通过插件引入 lombok 依赖

1.2 @Data

1.3 其他注解

2. 接口文档

2.1 接口(api)

2.2 接口文档

3. 综合练习 - 加法计算器

3.1 定义接口文档

3.2 准备工作 - 前端代码

3.3 后端代码

3.4 运行测试

4. 综合练习 - 用户登录

4.1 定义接口文档

​编辑

4.2 准备工作 - 使用 Ajax 编写前端代码

4.3 后端代码

4.3.1 校验接口

4.3.2 返回用户信息接口

4.4 运行测试

 5. 综合练习 - 留言板

5.1 接口文档

5.2 后端代码

5.2.1 接口一: 用户发表留言

5.2.2 接口二: 获取留言信息

5.3 前端代码

5.4 运行测试

6. 综合练习 - 图书管理系统

6.1 接口文档

6.2 后端代码

6.2.1 接口一: 登录验证

6.2.2 接口二: 提供图书列表

6.3 前端代码

6.3.1 登录验证页面

6.3.2 图书列表展示页面

6.4 运行测试

7. 应用分层

7.1 为什么要应用分层

7.2 如何分层

7.2.1 对图书系统的代码进行分层


1. Lombok

Lombok 是⼀个 Java 工具具库, 通过添加注解的方式, 简化 Java 的开发.

第一步, 将 lombok 的依赖导入 pom 文件中.

导入 lombok 的依赖有两种方式:

  1. 在 Maven 仓库中找 lombok 依赖(通用)
  2. 通过插件引 lombok 依赖(非通用)

1.1 引入 lombok 依赖

1.1.1 通过 Maven 仓库引 lombok 依赖

Maven 仓库: https://mvnrepository.com/

<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30</version><scope>provided</scope>
</dependency>

 1.1.2 通过插件引入 lombok 依赖

1.2 @Data

导入 lombok 依赖后, 我们就可以对类使用 @Data 注解, 这样就不用对类中的私用属性定义 setter 和 getter 方法, 也不用重写 toString, hashCode, equals 等方法, @Data 注解会帮我们自动生成.

因此, @Data 可以显著减少代码量, 使代码更加简洁易读.

我们可以通过 idea 反编译的文件来观察:

 并且, 在类外也可以成功使用 @Data 生成的这些方法:

1.3 其他注解

@Data 虽然很方便, 但是在某些特定的场景下, 我们并不希望生成所有的方法, 比如: 我们不想重写 toString(), 就是像打印出内存. 那么, 此时 @Data 显然就不适用了.

其实, @Data = @Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor  + @NoArgsConstructor

因此, 当我们想生成某个特定的方法时, 就可以使用某个特定的注解.

例如, 只对类中的属性生成 get 和 set 方法:

当使用这些注解作用于某个属性上时, 那就只对这个属性生成 get 和 set 方法:

2. 接口文档

2.1 接口(api)

接口, 又称为 api, 这里的接口并非 Java 中的 interface.

  • 通俗来说,api 就是别人写的一些函数/类,你直接拿过来就能用: api是一个广义的概念,操作系统会提供api、标准库会提供api、第三方库会提供api、其他各种开源项目会提供api、甚至工作中项目组给你的代码中也会提供api。
  • api也可以理解为,别人给你提供的库/程序,你都能用来干啥: 举个例子:对于同班同学,你可以给他在微信上发消息、可以问他题、可以和他聊天、....,这是你同学向你提供的api;你谈了个对象,你可以和你的对象亲亲抱抱举高高....,这是你对象向你提供的api。
  • 而基于api,你可以用来编程(api的目的就是用于编程): 比如接上例,基于你同学或者对象向你提供的api,你可以做出规划(编程):周末约同学打球;周末约对象看电影......

而对于Java程序猿的我们,我们可以使用标准库向我们提供的api去编程,比如ArrayList、StringBuffer、.......

2.2 接口文档

一个项目的开发, 大概总体分为两个大步骤:

步骤1: 需求阶段

  1. 用户提出需求
  2. 产品经理编写需求文档(需求文档中包含了相关功能)
  3. 评审需求文档

步骤2: 开发阶段

  1. 提出开发方案并进行评审
  2. 排期(接口文档什么时候提供, 什么时候开发完成, 什么时候自测完成, 时候时候联调, 什么时候上线)
  3. 提供接口文档以及相应依赖
  4. 前后端各自进行开发

综上, 需求开发前, 需要先定义接口文档.

写接口时, 前后端需要严格遵守接口文档中的要求去实现, 并且后续开发时, 前后端也会严格按照文档去调用相关 api.

接口文档, 通俗来说就是一个说明书, 它规定了 "前后端交互的接口" 该如何去实现. 比如: 接口文档中会说明某个方法的方法功能, 方法名称, 参数名称, 参数类型, 返回值类型....... 

下图就是 Java 中 String 的接口文档, 我们调用相关 api 时, 要严格遵守文档中的要求去调用:

因此, 接口文档不能随意修改, 如果对接口文档进行了修改, 那么一定要告知使用方.(接口文档由服务提供方去写的, 修改时也要让提供方去修改, 使用方不要去修改)


3. 综合练习 - 加法计算器

结合之前对 Spring 和前端的学习, 这里进行一下小练习.

第一个练习, 实现一个加法计算器:       

3.1 定义接口文档

开发计算器先, 需要约定好前后端交互接口的格式, 这里简单制定的接口文档如下:

3.2 准备工作 - 前端代码

我们根据需求文档, 编写出前端代码. 

这里对前端代码进行一下简单解释:

其中, action 的值, 就是前端所要调用的后端方法的路径.

<!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="get"><h1>计算器</h1>数字1:<input name="num1" type="text"><br>数字2:<input name="num2" type="text"><br><input type="submit" value=" 点击相加 "></form>
</body></html>

3.3 后端代码

我们后端也是一样, 需要根据接口文档的要求, 写出实现该计算功能的方法.

当前端调用我们的方法时, 我们要返回正确的结果.(前端调用后端方法, 后端服务器返回响应) 

实现加法功能的代码比较简单, 这里就不赘述了, 本例主要是想告诉大家前后端交互的过程.

@RestController
@RequestMapping("/calc")
public class CalcController {// 实现加法@RequestMapping("/sum")public String sum(Integer num1, Integer num2) {if(num1 == null || num2 == null) {return "输入有误, 请重新输入!!";}int ret = num1 + num2;return "<h1>计算结果为: " + ret + "</h1>";}
}

3.4 运行测试


4. 综合练习 - 用户登录

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

  1. 如果正确, 则跳转到首页. 首页显示当前登录用户.
  2. 如果不正确, 前端告知用户密码错误.
  3. 后续再访问首页, 可以获取到用户登录信息.

4.1 定义接口文档

接口文档中的请求方式 GET 和 POST 都可以, 只是语义上的区别.

4.2 准备工作 - 使用 Ajax 编写前端代码

在上个练习中, 我们使用的是 form 标签, 使用 action 属性指定数据提交的 URL(后端接口地址), 但这样会导致页面跳转.

但是, 如果使用 Ajax 提交表单数据, 就不会导致页面跳转. 这是因为 AJAX 的核心目的是在不重新加载整个页面的情况下, 与服务器进行异步通信并以 "增量更新" 的形式更新页面的局部内容.

借助 JQuery 实现 Ajax.

由于我们专攻于后端, 这里仅对前端代码进行简单介绍:

登录界面:

<!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>// 点击登录按钮, 调用 login 方法// 进而调用 Ajaxfunction login() {// 发起 Ajax 的语法: $.ajax(参数) 参数是一个对象: { 对象的内容 }$.ajax({url: "/user/login",type: "post",data: {userName: $("#userName").val(),password: $("#password").val()},success: function (result) {if(result) {// 密码正确location.href = "index.html";// location.assign("index.html");}else {alert("密码错误!!");}}});}</script>
</body></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$.ajax({url: "/user/getLoginUser",type: "get",success: function (userName) {// 选中 span 标签, 对 span 进行赋值// span 标签使用 text 赋值$("#loginUser").text(userName);}});</script>
</body></html>

4.3 后端代码

4.3.1 校验接口

进行账号密码的校验很简单, 比较两个字符串是否相等即可.

需要注意的是, 当校验成功后, 我们还需要将用户的用户名信息存到 Session 中, 以便后续根据 sessionid 查找 session 获取并返回用户信息.

比较账号密码时, 我们可以使用 Spring 第三方库提供的接口 StringUtils.hasLength() (字符串是否有长度)来判断用户输入的账号密码是否合法.

4.3.2 返回用户信息接口

由于我们已经将用户信息存到 session 中, 因此只需要根据 Sessionid 获取 Session, 再调用getAttribute 方法来获取用户信息即可.

@RestController
@RequestMapping("/user")
public class UserController {// 校验接口@RequestMapping("/login")public boolean login(String userName, String password, HttpSession session) {// 处理非法输入
//        if(userName == "" || userName == null || password == "" || password == null) {
//            return false;
//        }// String 提供的 api => 检验字符串是否有长度if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {return false;}// 目前还未学习数据库相关操作// 这里先假设 userName = password = 1111// 使用字符串常量调用 equals 方法, 避免空指针异常if("1111".equals(userName) && "1111".equals(password)) {// 若登录成功, 则将用户信息存储在 Session 中session.setAttribute("userName", userName);return true;}return false;}// 查询用户登录接口@RequestMapping("/getLoginUser")public String getLoginUser(HttpSession session) {return (String)session.getAttribute("userName");}}

4.4 运行测试

上文说到, 返回用户信息, 本质上是通过 Sessionid 获取 session, 从而获取用户名的. 

因此, 即使我们登陆成功, 但如果修改 sessionid 的值, 那么也是不能查找到用户信息的, 也就是在第二个页面中不会返回用户名:


 5. 综合练习 - 留言板

需求: 

  1. 提交留言: 用户输入留言信息之后, 后端需要把留言信息保存起来.
  2. 展示留言: 页面展示时, 需要从后端获取到所有的留言信息.

5.1 接口文档

发表留言接口:

获取留言接口:

5.2 后端代码

5.2.1 接口一: 用户发表留言

由于接口文档要求请求 Body 和响应 Body 均为 JSON 格式, 因此我们需要保证以下两点:

  1. 确保前端发送来的是 JSON 数据, 我们后端用的也是 JSON 来接收: 接口接收数据时, 使用 @RequestBody 将接口参数和请求中的 JSON 绑定起来. 
  2. 确保我们后端返回的是 JSON 数据, 并且响应的 Content-Type 为 application/json: 接口返回数据时, 需要保证返回的是 JSON 数据.

以上两点是必须要保证的, 否则结果将不符合预期. 

使得接口返回的是一个 JSON 数据, 有以下两种方式:

  1. 返回的是一个对象(Spring 会自动序列化为 JSON 格式并将 Content-Type 转换为 application-json)
  2. 返回字符串, 但是需要手动将 Content-Type 设置为 application-json(通过 @RequestMapping 的 produces 参数来设置)

这里采用第二种方式.

注意, 返回的数据一定要是 JSON 的格式, 浏览器收到后才能进行正确的解析:

JSON 数据格式:
{"key": "value"
}

由于我们后端还需要返回留言信息, 因此, 当后端接口接收到留言时, 要对留言进行保存.

可以使用 List 将收到的所有的留言信息保存在后端服务器上.

并且, 可以将留言封装为一个类 MessageInfo, 每一条留言就是一个对象. 这样, 就可以将留言信息存到 List 中了(List<MessageInfo>).

注意: 当后端返回的数据的数据格式为 JSON 时, 那么前端收到后, 就可以直接把这个数据当做一个对象使用, 不用再进行转换.

因此, 一定场景需求下, 后端返回 JSON 格式的数据, 是对前端有帮助的.

5.2.2 接口二: 获取留言信息

直接返回 List 即可, 返回的 List 也是一个对象, Spring 会自动帮我们转换为 JSON 格式.

// 留言
@Data
public class MessageInfo {private String from;private String to;private String message;
}
@RestController
@RequestMapping("/message")
public class MessageController {// 保存留言板信息List<MessageInfo> list = new ArrayList<>();// 接口一: 用户发表留言@RequestMapping(value = "/publish", produces = "application/json")public String publish(@RequestBody MessageInfo messageInfo) {if(!StringUtils.hasLength(messageInfo.getFrom())|| !StringUtils.hasLength(messageInfo.getTo())|| !StringUtils.hasLength(messageInfo.getMessage())) {return "{\"ok\": 0}";}list.add(messageInfo);return "\"ok\": " + 1;}// 接口二: 获取留言信息@RequestMapping("/getList")public List<MessageInfo> getList() {return list;}

5.3 前端代码

在前端中, 我们也要确保以下两个要点:

  1. 确保请求中的 Body 为 JSON 格式的数据
  2. 确保请求的 Content-Type 为 application/json

也就是说, Body 中内容的格式要和 Content-Type 的格式对应上. 比如: Body 是 JSON, 但 Content-Type 不是 JSON, 那就会发生错误. 

Ajax 默认传的是一个对象(非 JSON), 若要要传 JSON, 则需要调用 JSON.stringify 方法进行转换:

当前端收到用户的留言后, 要将留言展示在界面上, 并且调用后端接口, 让后端存储起来.

这里仍然使用 Ajax 进行数据传输.

前后端参数的对应关系如下:

每次刷新页面时, 浏览器都会重新加载前端代码, 包括 HTML, CSS 和 JavaScript. 这个过程会导致页面的内容被重置.

在之前的练习中, 我们都未使用后端存储用户的输入信息, 因此当用户刷新浏览器页面后, 原来的界面就会被重置.

但是, 在本例中, 我们已经将留言存储在了后端服务器上, 因此, 我们可以通过前端代码调用后端接口, 通过后端获取列表中的留言信息, 将留言展示到界面上.

因此, 就算用户刷新页面, 但由于后端服务器没有重新启动, 所以存储在后端的数据也不会丢失. 

故, 前段代码中应两次调用后端接口:

  1. 用户留言后, 前端将留言信息发送给后端.
  2. 用户刷新页面时, 前端应调用后端接口获取存储的留言, 并展示到界面上.

前端获取到列表中的留言信息后, 循环添加并展示到页面上.

<!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>load();// 用户刷新时, 就会调用这个方法// 前端就会从后端获取留言列表, 展示在页面上function load() {$.ajax({url: "/message/getList",type: "post",success: function(messageList) {if(messageList != null && messageList.length > 0) {for(var m of messageList) {var divE = "<div>"+ m.from +"对" + m.to + "说:" + m.message+"</div>";//3. 把节点添加到页面上    $(".container").append(divE); }}}});}function submit(){//1. 获取留言的内容var from = $('#from').val();var to = $('#to').val();var say = $('#say').val();// 判断非法输入if (from== '' || to == '' || say == '') {return;}var data = {from: from,to: to,message: say};$.ajax({url: "/message/publish",type: "post",contentType: "application/json",// 对象转 JSONdata: JSON.stringify(data),success: function(result) {                    if(result.ok == 1) {//2. 构造节点var divE = "<div>"+from +"对" + to + "说:" + say+"</div>";//3. 把节点添加到页面上    $(".container").append(divE);//4. 清空输入框的值$('#from').val("");$('#to').val("");$('#say').val("");}else {alert("输入非法数据!!");}} });}</script>
</body></html>

5.4 运行测试

之所以刷新页面后, 数据没有丢失, 是因为页面刷新时, 前端调用了后端接口, 获取了存储的留言信息, 并重新进行了展示, 因此用户刷新后也能看到之前的留言.

但是, 一旦后端服务器重启了, 那么意味着存储在后端 List 对象中的留言信息也会被销毁, 刷新后就看不到了.


6. 综合练习 - 图书管理系统

我们先实现以下两个简单页面: 

6.1 接口文档

6.2 后端代码

根据界面图可知, 后端要实现以下两个接口:

  1. 接口一: 登录验证
  2. 接口二: 提供图书列表

并且, 在实现这两个接口前, 需要先创建一个图书类.

通过观察页面内容, 我们可以得知类中一些的字段, 如:

id, 图书名, 作者, 库存数量, 价格, 出版社, 状态.

@Data
public class BookInfo {private long bookId;private String bookName;private String author;private long num;private BigDecimal price;private String publish;// 状态信息, 习惯上使用数字private int status; // 1 -> 可借阅, 2 -> 不可借阅// 图书状态的中文藐视// 开发中, 一般交给前端处理. 由于学习, 在后端这里直接就处理了private String statusCN;
}

6.2.1 接口一: 登录验证

这一接口和之前的 "用户登录" 接口是一致的:

并且由于还未学习数据库相关操作, 这里暂且把账号密码写死, 都为: "1111"

@RequestMapping("/user")
@RestController
public class UserController {// 登录验证接口@RequestMapping("/login")public boolean login(String name, String password, HttpSession session) {if(!StringUtils.hasLength(name) || !StringUtils.hasLength(password)) {return false;}// 由于未学习数据库, 假设: name = password = "1111"if("1111".equals(name) && "1111".equals(password)) {session.setAttribute("userName", name);session.setAttribute("password", password);return true;}return false;}
}

6.2.2 接口二: 提供图书列表

登录成功后, 将进入图书列表页面:

此时, 我们后端需要从数据库中查询图书信息, 将查询到的图书, 存储到 List 中, 并返回给前端.

由于, 我们还未学习数据库相关操作, 这里也暂且 mock 数据.

@RestController
@RequestMapping("/book")
// 获取图书列表
public class BookController {@RequestMapping("/getList")public List<BookInfo> getList(){// 还未学习数据库操作, 这里暂且 mock 数据List<BookInfo> bookList = mockData();// 对结果进行二次处理for(BookInfo bookInfo : bookList) {if(bookInfo.getStatus() == 1) {bookInfo.setStatusCN("可借阅");}else {bookInfo.setStatusCN("不可借阅");}}return bookList;}// mock 图书数据private List<BookInfo> mockData() {List<BookInfo> bookList = new ArrayList<>();for (int i = 1; i <= 5; i++) {BookInfo bookInfo = new BookInfo();bookInfo.setBookId(i);bookInfo.setAuthor("作者" + i);bookInfo.setBookName("图书" + i);bookInfo.setPublish("出版社" + i);bookInfo.setNum(new Random().nextInt(100));bookInfo.setPrice(BigDecimal.valueOf(new Random().nextInt(100)));bookInfo.setStatus(i % 5 == 0 ? 2 : 1);bookList.add(bookInfo);}return bookList;}
}

6.3 前端代码

很明显, 前端需要设计两个页面, 并且需要调用两次不同的后端接口:

  1. 登录验证页面 => 调用后端的登录验证接口
  2. 图书展示页面(登录成功后, 跳转到该页面) => 调用后端的获取图书列表接口

6.3.1 登录验证页面

该页面中, 用户输入账号密码, 并点击提交按钮后, 前端应调用后端登录验证接口, 且传递的数据格式为普通对象.

后端会返回 true(验证成功) 或者 false(验证失败), 前端应根据返回接口做出进一步处理:

  1. 若验证成功 => 跳转图书展示页面
  2.  若验证失败 => 对用户进行相关提示(这里使用 alert 弹框提示)
<!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() {$.ajax({url: "/user/login",type: "post",data: {name: $("#userName").val(),password: $("#password").val()},success: function(result) {if(result) {location.href = "book_list.html";   }else {alert("账号密码错误!!");}}});}</script>
</body></html>

6.3.2 图书列表展示页面

用户登录成功后, 跳转到该页面.

本页面的前端代码, 需要调用后端接口, 获取数据库中的图书信息, 并且后端返回的 List 中的图书信息, 拼接到一起, 展示到页面上:

这里给大家介绍一个 JQuery 的方法:

需要注意的是, html() 方法的功能是替换数据, 并非追加数据. 

<!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><tr><td><input type="checkbox" name="selectBook" value="1" id="selectBook" class="book-select"></td><td>1</td><td>大秦帝国第一册</td><td>我是作者</td><td>23</td><td>33.00</td><td>北京出版社</td><td>可借阅</td><td><div class="op"><a href="book_update.html?bookId=1">修改</a><a href="javascript:void(0)" onclick="deleteBook(1)">删除</a></div></td></tr><tr><td><input type="checkbox" name="selectBook" value="1" id="selectBook" class="book-select"></td><td>2</td><td>大秦帝国第二册</td><td>我是作者</td><td>23</td><td>33.00</td><td>北京出版社</td><td>可借阅</td><td><div class="op"><a href="book_update.html?bookId=2">修改</a><a href="javascript:void(0)" onclick="deleteBook(2)">删除</a></div></td></tr><tr><td><input type="checkbox" name="selectBook" value="1" id="selectBook" class="book-select"></td><td>3</td><td>大秦帝国第三册</td><td>我是作者</td><td>23</td><td>33.00</td><td>北京出版社</td><td>可借阅</td><td><div class="op"><a href="book_update.html?bookId=3">修改</a><a href="javascript:void(0)" onclick="deleteBook(3)">删除</a></div></td></tr><tr><td><input type="checkbox" name="selectBook" value="1" id="selectBook" class="book-select"></td><td>4</td><td>大秦帝国第四册</td><td>我是作者</td><td>23</td><td>33.00</td><td>北京出版社</td><td>可借阅</td><td><div class="op"><a href="book_update.html?bookId=4">修改</a><a href="javascript:void(0)" onclick="deleteBook(4)">删除</a></div></td></tr></tbody></table><div class="demo"><ul id="pageContainer" class="pagination justify-content-center"></ul></div><script>getBookList();function getBookList() {$.ajax({url: "/book/getList",type: "get",success: function(bookList) {var newHtml = '';for(var book of bookList) {newHtml += '<tr>';newHtml += '<td><input type="checkbox"name="selectBook" value="' + book.bookId + '" id="selectBook" class="book-select"></td>';newHtml += '<td>' + book.bookId + '</td>';newHtml += '<td>' + book.bookName + '</td>';newHtml += '<td>' + book.author + '</td>';newHtml += '<td>' + book.num + '</td>';newHtml += '<td>' + book.price + '</td>';newHtml += '<td>' + book.publish + '</td>';newHtml += '<td>' + book.statusCN + '</td>';newHtml += '<td><div class="op">';newHtml += '<a href="book_update.html?bookId=' + book.bookId + '">修改</a>';newHtml += '<a href="javascript:void(0)" onclick="deleteBook(' + book.bookId + ')">删除</a>';newHtml += '</div></td></tr>';}// .html => 置换 tbody 标签里面的内容$("tbody").html(newHtml);}});}//翻页信息$("#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) {//获取复选框的idvar ids = [];$("input:checkbox[name='selectBook']:checked").each(function () {ids.push($(this).val());});console.log(ids);alert("批量删除成功");}}</script></div>
</body></html>

6.4 运行测试


7. 应用分层

7.1 为什么要应用分层

实际开发中, 一个项目的代码量是非常大的, 大量逻辑的实现就会使得代码 "杂乱无章".

而应用分层就是将不同类型的代码组织起来, 让他们看起来没有那么杂乱~ 就像一个公司一样, 有后端团队, 前端团队, 人力团队, 财务团队, ....

并且, 对代码进行分类, 有利于看代码, 查找代码, 回忆代码功能. 

这样说的应用分层, 对我们 java 后端开发者来分层的.

7.2 如何分层

主要分为以下三层:

  1. 表现层, 使用 Controller 表示 => 和客户端进行交互. 接收参数, 和返回数据.(最靠近用户)
  2. 业务逻辑层 , 使用 Service 表示 => 对从数据库拿到的数据进行二次处理(业务方面).
  3. 数据层, 使用 Dao 表示 => 数据的管理(存储, 查询)

此外, 我们后端代码中, 还存在很多类, 我们称之为实体类, 使用 model, pojo, entity 来表示.

以上三层是逐级调用的关系: 客户端将请求传给表现层, 表现层接收参数并调用业务逻辑层, 业务逻辑层调用数据层, 数据层去数据库查数据, 查到后将数据返回给业务逻辑层, 业务逻辑层处理数据后返回给表现层, 表现层返回给客户端.

7.2.1 对图书系统的代码进行分层

以三层架构的思想, 对上面的图书系统进行分层:

实体类(用到的类都放到这个包中):

表现层:

业务逻辑层: 

数据层:


END

相关文章:

Spring MVC:综合练习 - 深刻理解前后端交互过程

目录 1. Lombok 1.1 引入 lombok 依赖 1.1.1 通过 Maven 仓库引 lombok 依赖 1.1.2 通过插件引入 lombok 依赖 1.2 Data 1.3 其他注解 2. 接口文档 2.1 接口(api) 2.2 接口文档 3. 综合练习 - 加法计算器 3.1 定义接口文档 3.2 准备工作 - 前端代码 3.3 后端代码 …...

Debian常用命令

以下是完整的 Linux 命令大全&#xff0c;适用于 Debian、Ubuntu 及其衍生系统&#xff0c;涵盖系统管理、文件操作、磁盘管理、用户管理、网络调试、安全、进程管理等多个方面。 目录 基本命令关机与重启文件和目录管理文件搜索挂载文件系统磁盘空间管理用户和群组管理文件和…...

【FFmpeg】FLV 格式分析 ③ ( Tag Body 数据块体结构 - Vedio Data 视频数据 )

文章目录 一、Tag Body 数据块体结构 - Video Data 视频数据1、Vedio Data 视频数据 类型标识2、Vedio Data 视频数据 结构分析3、Composition Time Offset 字段涉及的时间计算4、AVC Packet Type 字段说明① AVC Sequence Header 类型② AVC NALU 类型③ AVC End of Sequence …...

开源鸿蒙开发者社区记录

lava鸿蒙社区可提问 Laval社区 开源鸿蒙项目 OpenHarmony 开源鸿蒙开发者论坛 OpenHarmony 开源鸿蒙开发者论坛...

MinIO的安装与使用

目录 1、安装MinIO 1.1 下载 MinIO 可执行文件 1.2 检查 MinIO 是否安装成功 1.3 设置数据存储目录 1.4 配置环境变量&#xff08;可选&#xff09; 1.5 编写启动的脚本 1.6 开放端口 1.7 访问 2、项目实战 2.1 引入依赖 2.2 配置yml文件 2.3 编写Minio配置类 2.4…...

【分布式日志篇】从工具选型到实战部署:全面解析日志采集与管理路径

网罗开发 &#xff08;小红书、快手、视频号同名&#xff09; 大家好&#xff0c;我是 展菲&#xff0c;目前在上市企业从事人工智能项目研发管理工作&#xff0c;平时热衷于分享各种编程领域的软硬技能知识以及前沿技术&#xff0c;包括iOS、前端、Harmony OS、Java、Python等…...

Spring Boot/MVC

一、Spring Boot的创建 1.Spring Boot简化Spring程序的开发,使用注解和配置的方式开发 springboot内置了tomact服务器 tomact:web服务器,默认端口号8080,所以访问程序使用8080 src/main/java:Java源代码 src/main/resource:静态资源或配置文件,存放前端代码(js,css,html) s…...

uni-app连接EventSource

前言 uniapp默认是不支持event-source&#xff0c;这里是借助renderjs进行SSE连接 正文 引入event-source-polyfill 这里演示的是直接将代码下载到本地进行引入 下载地址 把里面的eventsource.min.js文件放到项目中的static文件夹 项目封装event-source.vue组件 <templ…...

[SCTF2019]babyre

[SCTF2019]babyre 一、查壳 无壳&#xff0c;64位 二、IDA分析 1.没有main&#xff0c;那就shifef12 点击&#xff1a; 再进&#xff1a; 都是花指令&#xff0c;所以要先解决花指令 三、解决花指令&#xff0c;得到完整的 main 往上面翻&#xff0c;注意看爆红的&#x…...

简洁实用的wordpress外贸模板

简洁、实用、大气的wordpress外贸模板&#xff0c;适合跨境电商搭建外贸B2B产品展示型网站。 简洁实用的wordpress外贸模板 - 简站WordPress主题简洁、实用、大气的wordpress外贸模板&#xff0c;适合跨境电商搭建外贸B2B产品展示型网站。https://www.jianzhanpress.com/?p828…...

每日一题 414. 第三大的数

414. 第三大的数 简单 class Solution { public:int thirdMax(vector<int>& nums) {int n nums.size();long first , second,third;first second third LONG_MIN ;bool find false;for(auto num : nums){if(num > first){ third second;second first…...

TVM框架学习笔记

TVM是陈天齐等人一个开源的深度学习编译器栈,用于优化和部署机器学习模型到各种硬件后端。它支持多种前端框架,如TensorFlow、PyTorch、ONNX等,并且可以在不同的硬件平台上运行,包括CPU、GPU和专用加速器。官方文档: Apache TVM Documentation — tvm 0.20.dev0 documenta…...

Codeforces Round 998 (Div. 3)

文章目录 EF E 原题链接 思路&#xff1a; 题目要求对于 G 中存在路径的两个点&#xff0c;在 F 中也必须存在路径&#xff0c;不是两个点存在直连的边。 两个点存在路径&#xff0c;说明俩个点在同一个连通块。我们用并查集来维护图的连通块。 最终的要求就是把 F 的并查集通…...

Vue.js 渐进式增强:如何逐步为传统项目注入活力

Vue.js 是一个渐进式框架&#xff0c;这意味着你可以将它逐步引入到现有项目中&#xff0c;而无需彻底重构。渐进式增强特别适合那些已经在使用传统服务器渲染框架&#xff08;如 PHP、Django、Laravel&#xff09;的项目&#xff0c;为它们增加动态交互功能。本篇教程将介绍如…...

【算法】经典博弈论问题——巴什博弈 python

目录 前言巴什博弈(Bash Game)小试牛刀PN分析实战检验总结 前言 博弈类问题大致分为&#xff1a; 公平组合游戏、非公平组合游戏&#xff08;绝大多数的棋类游戏&#xff09;和 反常游戏 巴什博弈(Bash Game) 一共有n颗石子&#xff0c;两个人轮流拿&#xff0c;每次可以拿1~m颗…...

【技术洞察】2024科技绘卷:浪潮、突破、未来

涌动与突破 2024年&#xff0c;科技的浪潮汹涌澎湃&#xff0c;人工智能、量子计算、脑机接口等前沿技术如同璀璨星辰&#xff0c;方便了大家的日常生活&#xff0c;也照亮了人类未来的道路。这一年&#xff0c;科技的突破与创新不断刷新着人们对未来的想象。那么回顾2024年的科…...

【0x06】HCI_Authentication_Complete事件详解

目录 一、事件概述 二、事件格式及参数 2.1. HCI_Authentication_Complete事件格式 2.2. Status 2.3. Connection_Handle 三、事件的生成于处理 3.1. 事件生成 3.2. 认证流程 3.2.1. 发送认证请求 3.2.2. 处理流程 3.2.3. 示例代码 四、应用场景 4.1. 设备配对与连…...

JS-Web API -day03

一、事件流 1.1 事件流与两个阶段说明 事件流 指的是事件完整执行过程中的流动路径 假设页面有个div标签&#xff0c;当触发事件时&#xff0c;会经历两个阶段&#xff0c;分别是捕获阶段、冒泡阶段 捕获阶段&#xff1a;Document - Element html - Elementbody - Element div…...

电子应用设计方案103:智能家庭AI浴缸系统设计

智能家庭 AI 浴缸系统设计 一、引言 智能家庭 AI 浴缸系统旨在为用户提供更加舒适、便捷和个性化的沐浴体验&#xff0c;融合了人工智能技术和先进的水疗功能。 二、系统概述 1. 系统目标 - 实现水温、水位和水流的精确控制。 - 提供多种按摩模式和水疗功能。 - 具备智能清洁…...

linux静态库+嵌套makefile

linux静态库嵌套makefile 文章目录 linux静态库嵌套makefile1、概述2、代码结构3、代码1&#xff09;main.c2&#xff09;主makefile3&#xff09;fun.c4&#xff09;func.h5&#xff09;静态库makefile 4、运行效果1&#xff09;在main.c目录下执行make2&#xff09;到output目…...

AIP-127 HTTP和gRPC转码

编号127原文链接AIP-127: HTTP and gRPC Transcoding状态批准创建日期2019-08-22更新日期2019-08-22 遵守面向资源设计的API使用RPC进行定义&#xff0c;但面向资源设计框架允许这些API表现为整体上符合REST/JSON约定的接口。这一点很重要&#xff0c;可以帮助开发者利用现有知…...

代码随想录算法训练营day32

代码随想录算法训练营 —day32 文章目录 代码随想录算法训练营前言一、动态规划理论基础二、509. 斐波那契数动态规划动态规划优化空间版递归法 三、70. 爬楼梯动态规划动态规划空间优化 746. 使用最小花费爬楼梯动态规划空间优化 总结 前言 今天是算法营的第32天&#xff0c…...

设计模式的艺术-开闭原则

原则使用频率图&#xff08;仅供参考&#xff09; 1.如何理解开闭原则 简单来说&#xff0c;开闭原则指的是 “对扩展开放&#xff0c;对修改关闭”。 当软件系统需要增加新的功能时&#xff0c;应该通过扩展现有代码的方式来实现&#xff0c;而不是去修改已有的代码。 例如我…...

【易康eCognition实验教程】003:点云数据加载浏览与操作详解

文章目录 一、加载并创建点云数据二、三维浏览1. 点云模式2. 点云视图设置 三、使用点云 一、加载并创建点云数据 本实验点云数据位于专栏配套实验数据包中的data003.rar中的terrian.las&#xff0c;解压后进行以下实验操作。 打开ecognition软件&#xff0c;点击【File】→【…...

海外雅思备考经验

1.18号斯图雅思考试 第一次考雅思&#xff0c;第一次在国外考雅思&#xff01; 最近在德国斯图加特联培&#xff0c;报考了1月18号的雅思机考&#xff0c;下面分享一些考试经验. ✌️考试地点 EZ Plus WEST Hasenbergstr. 31/1,, in the backyard of Hasenbergstrae 31, Stuttg…...

Oracle之Merge into函数使用

Merge into函数为Oracle 9i添加的语法&#xff0c;用来合并update和insert语句。所以也经常用于update语句的查询优化&#xff1a; 一、语法格式&#xff1a; merge into A using B on (A.a B.a) --注意on后面带括号&#xff0c;且不能更新join的字段 when matched then upd…...

Spring Boot 自定义属性

Spring Boot 自定义属性 在 Spring Boot 应用程序中&#xff0c;application.yml 是一个常用的配置文件格式。它允许我们以层次化的方式组织配置信息&#xff0c;并且比传统的 .properties 文件更加直观。 本文将介绍如何在 Spring Boot 中读取和使用 application.yml 中的配…...

前端面试题-问答篇-5万字!

1. 请描述CSS中的层叠&#xff08;Cascade&#xff09;和继承&#xff08;Inheritance&#xff09;规则&#xff0c;以及它们在实际开发中的应用。 在CSS中&#xff0c;层叠&#xff08;Cascade&#xff09;和继承&#xff08;Inheritance&#xff09;是两个关键的规则&#x…...

2025年1月21日(树莓派点亮呼吸灯第一次修改)

系统信息&#xff1a; Raspberry Pi Zero 2W 系统版本&#xff1a; 2024-10-22-raspios-bullseye-armhf Python 版本&#xff1a;Python 3.9.2 已安装 pip3 支持拍摄 1080p 30 (1092*1080), 720p 60 (1280*720), 60/90 (640*480) 已安装 vim 已安装 git 学习目标&#xff1a;…...

【Linux网络编程】传输层协议

目录 一&#xff0c;传输层的介绍 二&#xff0c;UDP协议 2-1&#xff0c;UDP的特点 2-2&#xff0c;UDP协议端格式 三&#xff0c;TCP协议 3-1&#xff0c;TCP报文格式 3-2&#xff0c;TCP三次握手 3-3&#xff0c;TCP四次挥手 3-4&#xff0c;滑动窗口 3-5&#xf…...

JavaScript系列(41)--状态管理实现详解

JavaScript状态管理实现详解 &#x1f504; 今天&#xff0c;让我们深入探讨JavaScript的状态管理实现。状态管理是现代前端应用中的核心概念&#xff0c;它帮助我们有效地管理和同步应用数据。 状态管理基础概念 &#x1f31f; &#x1f4a1; 小知识&#xff1a;状态管理是一…...

flume和kafka整合 flume和kafka为什么一起用?

‌Flume和Kafka一起使用的主要原因是为了实现高效、可靠的数据采集和实时处理。‌‌12 实时流式日志处理的需求 Flume和Kafka结合使用的主要目的是为了完成实时流式的日志处理。Flume负责数据的采集和传输,而Kafka则作为消息缓存队列,能够有效地缓冲数据,防止数据堆积或丢…...

【深度学习】 自动微分

自动微分 正如上节所说&#xff0c;求导是几乎所有深度学习优化算法的关键步骤。 虽然求导的计算很简单&#xff0c;只需要一些基本的微积分。 但对于复杂的模型&#xff0c;手工进行更新是一件很痛苦的事情&#xff08;而且经常容易出错&#xff09;。 深度学习框架通过自动…...

什么是三高架构?

大家好&#xff0c;我是锋哥。今天分享关于【什么是三高架构?】面试题。希望对大家有帮助&#xff1b; 什么是三高架构? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 “三高架构”通常是指高可用性&#xff08;High Availability&#xff09;、高性能&#xff…...

IOS 自定义代理协议Delegate

QuestionViewCell.h文件代码&#xff0c;定义代理协议 protocol QuestionViewCellDelegate <NSObject>- (void)cellIsOpenDidChangeAtIndexPath:(NSIndexPath *)indexPath;endinterface QuestionViewCell : UITableViewCellproperty (nonatomic, weak) id<QuestionVi…...

【flutter版本升级】【Nativeshell适配】nativeshell需要做哪些更改

flutter 从3.13.9 升级&#xff1a;3.27.2 nativeshell组合库中的 1、nativeshell_build库替换为github上的最新代码 可以解决两个问题&#xff1a; 一个是arg("--ExtraFrontEndOptions--no-sound-null-safety") 在新版flutter中这个构建参数不支持了导致的build错误…...

C#编程:List.ForEach与foreach循环的深度对比

在C#中&#xff0c;List<T>.ForEach 方法和传统的 foreach 循环都用于遍历列表中的元素并对每个元素执行操作&#xff0c;但它们之间有一些关键的区别。 List<T>.ForEach 方法 方法签名&#xff1a;public void ForEach(Action<T> action)类型&#xff1a;…...

leetcode_2762. 不间断子数组

2762. 不间断子数组 - 力扣&#xff08;LeetCode&#xff09; 运用滑动窗口和multise(平衡二叉树实现) 符合条件 右窗口向右扩展 不符合条件 左窗口向左扩展 class Solution { public:long long continuousSubarrays(vector<int>& nums) {int max, min; //表示窗…...

Java学习教程,从入门到精通,JDBC创建数据库语法知识点及案例代码(99)

JDBC创建数据库语法知识点及案例代码 一、JDBC创建数据库语法 在JDBC中&#xff0c;创建数据库主要通过执行SQL语句来实现。其基本语法如下&#xff1a; CREATE DATABASE database_name;CREATE DATABASE 是固定的SQL语句关键字&#xff0c;用于指定创建数据库的操作。databa…...

进阶——第十六届蓝桥杯(sscanf的运用)

声明变量 char tx_buf[30];char rx_buf[30];char car_type[5];char car_num[5];char car_time[15]; int sscanf(const char *str, const char *format,...); sscanf函数功能 sscanf 函数从字符串 str 中读取数据&#xff0c;根据 format 所指定的格式将数据存储到后续的变量中…...

嵌入式硬件篇---ADC模拟-数字转换

文章目录 前言第一部分&#xff1a;STM32 ADC的主要特点1.分辨率2.多通道3.转换模式4.转换速度5.触发源6.数据对齐7.温度传感器和Vrefint通道 第二部分&#xff1a;STM32 ADC的工作流程&#xff1a;1.配置ADC2.启动ADC转换 第三部分&#xff1a;ADC转化1.抽样2.量化3.编码 第四…...

Spark Streaming编程基础

文章目录 1. 流式词频统计1.1 Spark Streaming编程步骤1.2 流式词频统计项目1.2.1 创建项目1.2.2 添加项目依赖1.2.3 修改源目录1.2.4 添加scala-sdk库1.2.5 创建日志属性文件 1.3 创建词频统计对象1.4 利用nc发送数据1.5 启动应用&#xff0c;查看结果 2. 编程模型的基本概念3…...

android wifi AsyncChannel(WifiManager和WifiP2pManager)

AynscChannel的讲解 [Android]AsyncChannel介绍-CSDN博客 WifiP2pManager里的channel的使用理解 WifiP2pManager.java public void createGroup(Channel c, ActionListener listener) {checkChannel(c);c.mAsyncChannel.sendMessage(CREATE_GROUP, WifiP2pGroup.NETWORK_ID_PE…...

生存网络与mlr3proba

在R语言中,mlr3包是一个用于机器学习的强大工具包。它提供了一种简单且灵活的方式来执行超参数调整。 生存网络是一种用于生存分析的模型,常用在医学和生物学领域。生存分析是一种统计方法,用于研究事件发生的时间和相关因素对事件发生的影响。生存网络可以用来预测个体在给…...

C#Object类型的索引,序列化和反序列化

前言 最近在编写一篇关于标准Mes接口框架的文章。其中有一个非常需要考究的内容时如果实现数据灵活和可使用性强。因为考虑数据灵活性&#xff0c;所以我一开始选取了Object类型作为数据类型&#xff0c;Object作为数据Value字段&#xff0c;String作为数据Key字段&#xff0c…...

【动态规划】记忆化搜索

Ban or Problem - A - Codeforces 【CCPC】2022年绵阳站部分题解&#xff08;ACGM&#xff09;_ban or pick, whats the trick-CSDN博客 #include<iostream> using namespace std; #include<cstring> #include<algorithm> #define inf -0x3f3f3f3f #defi…...

深度学习 DAY1:RNN 神经网络及其变体网络(LSTM、GRU)

实验介绍 RNN 网络是一种基础的多层反馈神经网络&#xff0c;该神经网络的节点定向连接成环&#xff0c;其内部状态可以展示动态时序行为。相比于前馈神经网络&#xff0c;该网络内部具有很强的记忆性&#xff0c;它可以利用它内部的记忆来处理任意时序的输入序列&#xff0c;…...

BW复制ERP数据源跑程序激活后才可见

场景&#xff1a; BW提取ERP数据走ODP通道之后&#xff0c;数据源需要用下列程序激活加入白名单后才能被BW系统访问到&#xff1b; 检查&#xff1a; 1、RSA6检查数据源是否可正常使用&#xff0c;若为绿√表示可正常访问&#xff0c;反之&#xff0c;则不行。 2、白名单表ROO…...

MarsCode青训营打卡Day10(2025年1月23日)|稀土掘金-147.寻找独一无二的糖葫芦串、119.游戏队友搜索

资源引用&#xff1a; 147.寻找独一无二的糖葫芦串 119.游戏队友搜索 今日小记&#xff1a; 回乡聚会陪家人&#xff0c;休息一天~ 稀土掘金-147.寻找独一无二的糖葫芦串&#xff08;147.寻找独一无二的糖葫芦串&#xff09; 题目分析&#xff1a; 给定n个长度为m的字符串表…...

无人机 PX4 飞控 | PX4源码添加自定义参数方法并用QGC显示与调整

无人机 PX4 飞控 | PX4源码添加自定义参数方法并用QGC显示与调整 0 前言 之前文章添加了一个自定义的模块&#xff0c;本篇文章在之前的自定义模块中&#xff0c;添加两个自定义参数 使用QGC显示出来&#xff0c;并通过QGC调整参数值&#xff0c;代码实现参数更新 新增的参…...