接上次博客:JavaEE进阶(3)Spring Web MVC入门(MVC 定义、什么是Spring MVC?学习Spring MVC:项目准备、建立连接【注解介绍、指定请求、请求、响应】)-CSDN博客
目录
计算器实现
成果图大概长这样:
比较简陋,但是是对我们之前学习的一个回顾复习和应用。
我把前端代码提供给大家,大家只需要直接把这个.html复制粘贴进到resources里面的static目录下即可:
<!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>
接下来我们重启一下服务器,在网页端访问URL,确保前端代码正常。
现在前端已经没有问题,我们就来思考后端怎么写:
一般情况下,后端接到需求会做什么?或者说,一个标准的软件开发生命周期,通常包括哪些主要阶段?
-
需求评审:
- 描述: 在这个阶段,团队对需求进行评审,确保它们是合理的、可行的,并且满足项目的目标。
- 关注点: 需求的清晰性、完整性、一致性和可验证性。
-
开发:
- 描述: 在这个阶段,开发团队根据需求进行开发工作,包括接口定义、具体编码和开发人员的自测。
- 关注点: 代码的质量、可维护性、性能、安全性等。
-
联调:
- 描述: 后端和前端进行联合测试,确保两者之间的协作正常,接口通信无误。
- 关注点: 接口的一致性、数据的正确传递、系统整体的协同工作。
-
体测阶段(测试人员的工作):
- 描述: 测试人员进行全面的测试,包括功能测试、性能测试、安全性测试等,以保证软件的质量。
- 关注点: 发现并修复潜在的问题、确保系统的稳定性和可靠性。
-
上线:
- 描述: 将经过测试和验收的软件版本部署到生产环境,使其对用户可用。
- 关注点: 部署的平稳、数据的一致性、系统的可用性。
-
维护:
- 描述: 在软件上线后,进行系统监控、bug修复、性能优化等工作,确保系统稳定运行。
- 关注点: 实时监控、快速响应问题、定期维护和升级。
-
下线:
- 描述: 在软件生命周期的某个阶段,决定停止对某个版本或模块的支持和维护。
- 关注点: 优化资源分配、减少维护成本、保障系统的可持续性。
以上每个阶段都有其独特的目标和关注点,而整个流程则确保了软件的高质量、可维护性和可用性。
我们目前只会涉及前三个部分:
需求评审
这是一个简单的加法计算器接口,用户可以通过发送包含num1和num2参数的请求,然后服务器将返回这两个数字的和。
开发阶段
接口定义: 两个原则
- 看我需要什么(请求参数)
- 看对方需要什么(响应结果)
- 我需要: 参与计算的两个数字
- 对方需要什么: 需要计算结果
在现代 Web 开发中,"前后端交互接口",通常被称为 API(Application Programming Interface),是整个开发过程中的关键环节。API是应用程序对外提供服务的描述,用于定义应用程序之间如何交换信息和执行任务。与JavaSE阶段学习中的[类和接口]中的接口不同,这里的接口更类似于应用程序提供给其他程序或系统使用的一种契约。
具体来说,API定义了允许客户端向服务器发送哪些 HTTP 请求,以及每种请求预期获取什么样的 HTTP 响应。在前后端分离的开发模式中,前端和后端通常由不同的团队负责开发。在实际开发中,双方会在开发之前进行沟通,提前约定好交互的方式,明确定义了前后端之间的接口规范。
这种规范被记录在"接口文档"中,也可以被理解为应用程序的"操作说明书"。接口文档包含了客户端可以访问的所有接口、每个接口的请求参数、响应格式、可能的错误码等信息。通过遵循这份文档,前端开发人员可以清晰地知道如何与后端进行交互,而后端开发人员也能够按照文档的规范提供相应的服务。
总的来说,API在现代 Web 开发中是极为重要的,它为前后端开发团队提供了一个明确的协作标准,促使双方更高效地进行开发工作。
JavaEE里面的的接口和JavaSE 阶段的接口概念不同,此处的接口你可以理解为表示的是API,也就是客户端和服务端的约定。
-
JavaSE阶段的接口(Interface in JavaSE):
- 在JavaSE中,接口是一种抽象数据类型,它定义了一组方法的签名,但没有提供方法的具体实现。类实现接口时需要提供方法的具体实现。
- 接口在JavaSE中用于实现多继承,因为一个类可以实现多个接口,但只能继承一个类。
- JavaSE中的接口主要用于定义类的协议,以确保类提供了一组特定的方法。
-
JavaEE阶段的接口(Interface in JavaEE):
- 在JavaEE(Java Platform, Enterprise Edition)中,接口通常指的是应用程序接口(API)。
- JavaEE接口可能包括一组类和方法,用于开发企业级应用程序,例如Servlet API、JDBC API、JMS API等。
- 这些接口定义了开发者需要遵循的规范,以便他们的代码能够与JavaEE平台协同工作。
总的来说,在JavaSE中,接口更侧重于面向对象的编程概念,而在JavaEE中,接口更侧重于定义用于开发企业级应用程序的规范和约定。
这里我们的计算器大概的接口定义如下(现实生活中没那么草率,是一个单独的文档):
- 接口路径: /calc/sum
- 请求参数:
- num1: 第一个数字
- num2: 第二个数字
- 返回结果:包含两个数字相加的结果
具体编码
package com.example.UserControl;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/calc")
@RestController
public class CalcController {
@RequestMapping("/sum")
public String sum(Integer num1, Integer num2){
Integer sum = num1+num2;
return "计算机计算结果: "+sum;
}
}
自测
我们写完之后先别急着继续,先去测试一下后端代码,小步慢跑:
由此可知,后端代码没有问题,我们前面也已经测试过前端代码没有问题,那么如果以后运行的时候再出错,我们就可以排除前端代码和后端代码出错这两种情况了,而专注于前后端交互的问题。
我们到实际网页试试看:
呀!报了一个405!
看来前后端交互出问题了,我们需要去判断:
1、请求有没有发出去?通过抓包判断。
2、 后端有没有接收到请求?通过打印日志:
我们多加一行,用来判断请求是否到达后端。如果到了,那么第一行会先执行打印,反之,即可判断出问题所在——前端代码出问题了:
package com.example.UserControl;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/calc")
@RestController
public class CalcController {
@RequestMapping("/sum")
public String sum(Integer num1, Integer num2){
System.out.println("========================");
Integer sum = num1+num2;
return "计算机计算结果: "+sum;
}
}
如果后端有问题:Debug或者把参数打印出来。
Debug:
最后,前后端交互出问题,请求没有发出去。
在我的HTML表单中,将操作属性设置为"calc/sum",但在CalcController类中,我在类级别使用@RequestMapping注解指定了"/calc"。因此,表单操作的正确路径应包含类级别和方法级别的映射。
尝试将表单操作更新如下:
<form action="/calc/sum" method="post">
<!-- 表单内容 -->
</form>
确保在操作属性中包含前导 "/",以确保路径相对于应用程序的根上下文。
前端可以单击右键查看页面源代码,判断是否因为缓存的缘故还没有更新代码:
可以强制刷新,也可以Ctrl+L+DEL删除缓存。
用户登录
有两个接口:
需求评审
1、验证用户——用户名和密码:在实际实现中,服务器端应该对接收到的用户名和密码进行验证,如果验证通过,则返回 true,否则返回 false。在响应中,可以添加其他信息,如用户信息、token等,以满足具体业务需求。此处我们仅包含基本的验证信息。
2、获取登录的用户:在实际实现中,服务器端应该验证用户的登录状态,如果用户已经登录,则返回相应的用户信息。如果用户未登录,可以根据具体业务需求返回相应的未登录状态或重定向到登录页面。
开发阶段
接口定义如下:
接口路径: /login/check
请求方法: POST
请求参数:
- userName(字符串):用户账户名
- password(字符串):用户密码
响应:
- 成功时,返回 true 表示用户和密码正确。
- 失败时,返回 false 表示密码错误或其他验证失败。
示例:
// 请求示例
POST /login/check
{
"userName": "example_user",
"password": "example_password"
}
// 响应示例
{
"authenticated": true
}
接口路径: /login/index
请求方法: GET
请求参数: 无
响应:
- 成功时,返回登录的用户信息。
- 失败时,可以返回相应的错误信息或状态码。
示例:
// 请求示例
GET /login/index
// 响应示例
{
"userId": 123,
"userName": "example_user",
"email": "user@example.com"
}
具体编码
正常情况下我们应该是连接数据库进行校验,但是JDBC的做法较复杂在,我们现在还没有学习到新的框架,所以这里就先简单的模拟即可:
package com.example.UserControl;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
@RequestMapping("/login")
@RestController
public class LoginController {
@RequestMapping("/check")
public boolean check(String userName, String password, HttpSession session){
//校验账号和密码是否为空
// if (userName==null || "".equals(userName) || password==null || "".equals(password)){
// return false;
// }
if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
return false;
}
//校验账号和密码是否正确
//数据模拟
if ("sangsang".equals(userName) && "123456".equals(password)){
session.setAttribute("userName",userName);
return true;
}
return false;
}
@RequestMapping("/index")
public String index(HttpSession session){
String userName = (String)session.getAttribute("userName");
return userName;
}
}
这里index方法里面的参数 session = requestgetSession(); 默认 = requestgetSession(true);
所以如果没有session,会自动创建一个session。
我们通过将常量值写在变量值后面,能够避免空指针异常,因为在这种写法中,即使 userName 是 null,也不会触发空指针异常。这是因为常量调用方法不会出现空指针异常,而将常量写在前面容易导致空指针异常。
自测
后端代码:
先来看看传递为空是否会报错:
返回200OK:
这也侧面反映出,在Spring中,如果在Controller方法中使用HttpSession参数,并尝试获取某个属性时,如果该属性不存在,Spring会自动创建一个新的HttpSession对象,并返回一个新的SessionID。
在我们的代码中,如果userName属性在session中不存在,session.getAttribute("userName")将返回null。然后,return userName; 语句将返回一个null字符串。在HTTP响应中,返回200 OK是符合预期的,因为请求成功完成,即使返回的字符串是null。
接下来输入预先设定好的值看看:
看来后端代码没有问题。
那么接下来就开始布置我们的前端代码:
确保前端代码放在了当前项目的目录下:
静态的前端代码没有问题。
现在我们需要编写一下login.html里的函数:
注意,这里和我们以前使用Servlet实现前后端交互的时候不一样,我们现在通过引入jQuery库,使用Ajax :
回顾:
Ajax(Asynchronous JavaScript and XML)是一种用于在浏览器和服务器之间进行异步数据交换的技术。它允许在不重新加载整个页面的情况下,通过在后台与服务器进行小规模的数据交换,更新页面的部分内容。
主要特点包括:
-
异步性(Asynchronous): Ajax 可以在不阻塞用户界面的情况下执行,允许页面其他部分继续运行而不受到影响。这通过使用异步的 JavaScript 和 XMLHttpRequest 对象来实现。
-
数据交换: Ajax 可以与服务器进行数据交换,通常以 JSON 或 XML 格式传递数据。这使得可以实时地从服务器获取数据并在页面上更新,而无需整个页面的刷新。
-
动态更新页面: Ajax 可以在不刷新整个页面的情况下更新页面的部分内容。这可以提高用户体验,减少对服务器的请求次数,以及减小页面加载时间。
-
交互性: Ajax 可以用于与用户的交互,例如在用户填写表单时,实时验证数据而不需要整个页面的刷新。
虽然名字中包含 "XML",但实际上,Ajax 可以用于与服务器交换任何格式的数据,不仅限于 XML。在现代的 Web 开发中,JSON 成为更常用的数据交换格式,因为它更轻量且易于处理。
常见的 Ajax 实现是通过 JavaScript 中的 XMLHttpRequest 对象或使用更现代的 Fetch API。此外,许多现代 JavaScript 框架和库(如 jQuery、Axios、Fetch API)都提供了更简便的方式来执行 Ajax 请求。
jQuery是一个快速、小巧且功能丰富的JavaScript库。它是由John Resig在2006年创建的,旨在简化JavaScript在各种浏览器中的DOM操作和事件处理。jQuery的目标是使Web开发更加简单、便捷,克服了不同浏览器之间的兼容性问题,提供了一致的API,帮助开发者更容易实现复杂的交互效果和动态页面操作。
jQuery主要特点包括:
-
跨浏览器兼容性: jQuery通过提供统一的API,解决了不同浏览器之间的兼容性问题,使开发者更轻松地编写跨浏览器兼容的代码。
-
DOM操作: jQuery简化了DOM(文档对象模型)的操作,提供了简单而强大的选择器,使开发者能够方便地选择和操作HTML元素。
-
事件处理: jQuery简化了事件处理,通过使用易于理解和使用的方法,开发者可以轻松地附加和处理事件。
-
动画效果: jQuery提供了丰富的动画效果和过渡效果,可以轻松地创建各种动态页面效果。
-
AJAX支持: jQuery简化了使用AJAX进行异步通信的过程,通过简单的API,可以方便地进行数据加载和提交。
-
插件架构: jQuery支持插件,使开发者能够使用现有的插件或编写自己的插件来扩展库的功能。
由于其强大而简洁的功能,jQuery在过去十多年中一直是最流行的JavaScript库之一。然而,随着现代浏览器和原生JavaScript API的改进,以及更先进的前端框架的崛起,一些项目可能更倾向于使用原生JavaScript或其他库和框架。
在 JavaScript 中进行页面重定向的操作,有三种方式可以实现:
- location.href = "index.html";: 这是最常见和常用的方式,它将浏览器的地址改变为指定的 URL,类似于用户手动输入地址并按下回车键。
- location.assign("index.html");: 这也是进行页面重定向的一种方式,效果和location.href基本相同,将浏览器地址改变为指定的 URL。
- location.replace("index.html");: 这种方式也是进行页面重定向的一种方式,但与前两者不同的是,它不会在浏览器的历史记录中留下记录。当使用 replace 方法时,新页面会替代浏览器历史记录中的当前页面,而用户点击浏览器的后退按钮将直接跳过新页面。
在 jQuery 中,val() 方法用于获取或设置表单元素的值,这包括输入框、文本域、以及一些其他表单元素。当使用 val() 方法时,不带参数表示获取元素的当前值,而带参数(val(" "))则表示设置元素的值(赋值)。
在我们上面的代码中:
- $("#userName").val() 用于获取 ID 为 "userName" 的输入框的当前值。
- $("#password").val() 用于获取 ID 为 "password" 的输入框的当前值。
如果要设置输入框的值,可以将参数传递给 val() 方法,例如:
- $("#userName").val("新的值");
- $("#password").val("新的密码");
这样会将相应的输入框的值设置为指定的新值。
<!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="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script>
function login() {
$.ajax({
type: "post",
url: "/login/check",
data:{
userName: $("#userName").val(),
password: $("#password").val()
},
success: function(result){
if(result==true){
//用户名和密码正确
location.href = "index.html";
// location.assign("index.html");
// location.replace("index.html")
}else{
alert("用户名或密码错误!");
}
}
});
}
</script>
</body>
</html>
再来编写index.html:
在 jQuery 中,选择器用于定位和操作 HTML 元素。选择器可以通过不同的方式指定元素,而 # 符号用于选中具有特定 ID 的元素。
例如,$("#loginUser") 就是一个选择器,它会选择具有 ID 为 "loginUser" 的元素。这个选择器告诉 jQuery 选择页面中具有指定 ID 的元素,并对其进行操作。
在我们的代码中,$("#loginUser").text(result); 的意思是将 result 的值设置为 ID 为 "loginUser" 的元素的文本内容。
如果要选择其他类型的元素,例如使用类名或标签名,可以使用不同的选择器:
- 通过类名选择:$(".className"),其中 "className" 是我们要选择的类名。
- 通过标签名选择:$("tagName"),其中 "tagName" 是我们要选择的标签名。
jQuery 的选择器非常灵活,我们可以根据不同的需求选择页面上的不同元素。
<!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({
url: "/login/index",
type: "get",
success:function(result){
$("#loginUser").text(result);
}
});
</script>
</body>
</html>
接下来我们可以去网页端自测:
右键点击“查看页面源代码”,强制刷新(Ctrl + F5)或清空缓存(Ctrl+shift+delete)之后可以看到我们的前端代码已经更新:
此时我们什么都不输入,直接点击“登录”按钮:
用户名和密码为空,也会自动创建session:
输入错误的密码,同样弹出提示框:
输入正确密码后:
这里关于清缓存,插一句题外话——“后端的缓存怎么清?”
在前后端交互中,确保数据的一致性和正确性是至关重要的。对于缓存的清理,我们主要提到的有几种情况:
-
后端缓存清理:
- 接口数据缓存: 如果后端接口返回的数据不正确,首先要确保后端代码逻辑正确。如果代码没有问题,可以考虑清除后端缓存,以确保下次请求能够获取最新的数据。这可以通过后端缓存系统提供的清理机制或者手动清理缓存来实现。
- 数据库查询缓存: 如果系统使用数据库查询缓存,也需要考虑清理数据库缓存。这通常可以通过执行相应的数据库操作来实现。
-
前端缓存清理:
- 浏览器缓存: 如果页面显示不正确,可能是因为浏览器缓存了旧的资源文件。在这种情况下,可以尝试清除浏览器缓存,或者使用版本控制的方式确保前端资源文件的更新能够被正确加载。
- 前端应用状态: 前端应用可能会维护一些状态或者缓存一些数据,确保在需要的时候清理这些缓存,以便获取最新的数据。
-
CDN 缓存:
- 如果系统使用了 CDN(Content Delivery Network),可能还需要考虑 CDN 缓存的清理。CDN 可能会缓存前端资源文件或者一些静态内容,确保在更新后清理相应的 CDN 缓存。
-
代理服务器缓存:
- 如果系统中使用了代理服务器,代理服务器也可能对请求进行缓存。在某些情况下,可能需要清理代理服务器的缓存,以确保后端数据的一致性。
在实际操作中,清理缓存是一个敏感的操作,我们需要谨慎处理。最好在非生产环境进行测试,并确保清理缓存的操作不会对系统造成不必要的影响。另外,建议在系统中使用合适的缓存控制策略,以减少出现这类问题的可能性。
留言板
需求评审
1、提交留言:这个接口的作用是将用户提交的留言信息保存到系统中,通过指定的发表人、接收人和留言内容。成功时返回 true,失败时返回 false。
2、获取留言:这个接口的作用是获取系统中所有的留言信息,返回一个包含留言信息的列表。每个留言信息可能包括发表人、接收人、留言内容等信息,具体信息结构由 MessageInfo 类型定义。
开发阶段
接口定义如下
1. 提交留言接口
URL: /message/publish
参数:
- from:发表人
- to:接收人
- message:信息内容
返回:
- true:提交成功
- false:提交失败
2. 获取留言接口
URL: /message/getList
参数: 无
返回:
List<MessageInfo>:包含全部留言信息的列表
具体编码
前端代码布置:
我们先来看看静态的前端网页:
前端代码没有问题。但是存在一个问题,我们刷新页面,数据就会消失。
所以我们需要把它存起来,这就需要借助后端来实现。
所以我们开始写后端代码:
package com.example.UserControl;
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;
}
public MessageInfo(String from, String to, String message) {
this.from = from;
this.to = to;
this.message = message;
}
}
但是这样写又麻烦又乱:想要新增每次都要重新写上get和set,会很麻烦。
此时能不能有另一种方法,可以允许我们不用写那么多的get、set方法?
我们可以借助一个工具包——Lombok:
Lombok(来自"lots of boilerplate code"的缩写)是一款Java的注解处理工具,旨在通过使用注解简化Java代码中的样板代码(boilerplate code),提高代码的简洁性和可读性。Lombok通过在编译时自动生成Java代码,减少了开发者需要手动编写的getter、setter、toString等常见方法,从而减少了冗长的代码。
Lombok主要功能:
-
简化样板代码: Lombok旨在减少Java代码中的样板代码,即那些重复、机械性的代码,如getter、setter等。
-
提高简洁性和可读性: 通过使用 Lombok,开发者可以专注于业务逻辑而不是冗长的代码结构,从而提高代码的简洁性和可读性。
-
注解处理工具: Lombok通过注解处理工具的方式工作,这意味着它在编译时插入生成的代码,而不会在运行时引入任何额外的开销。
-
自动生成代码: Lombok可以自动生成常见的方法,如getter、setter、toString,以及其他一些有用的方法,如hashCode、equals等。
总之,使用Lombok可以使Java代码更加精简,减少了模板性代码的编写,同时保持代码的可读性和维护性。这使得我们能够更专注于实现业务逻辑,而不必担心大量的样板代码。
我们可以来看看具体的Lombok提供的一些常见注解及其功能:
注解 | 作用 |
---|---|
@Getter | 自动生成属性的 getter 方法。 |
@Setter | 自动生成属性的 setter 方法。 |
@ToString | 自动生成 toString 方法,方便打印对象的字符串表示。 |
@EqualsAndHashCode | 自动生成 equals 和 hashCode 方法,用于对象比较和散列。 |
@NoArgsConstructor | 自动添加无参构造方法,用于创建对象实例。 |
@AllArgsConstructor | 自动添加全属性构造方法,顺序按照属性的定义顺序 |
@NonNull | 用于标记属性,表示属性不能为 null。 |
@RequiredArgsConstructor | 自动生成必需属性的构造方法,final + @NonNull 的属性为必需。 |
-
@Getter / @Setter:
- 用于自动生成类的getter和setter方法。
- 可以用在类级别或字段级别。
@Getter @Setter public class MyClass { private String name; private int age; }
-
@NoArgsConstructor / @AllArgsConstructor:
- 自动生成无参构造方法和全参构造方法。
- 可以用在类级别。
@NoArgsConstructor @AllArgsConstructor public class MyClass { private String name; private int age; }
-
@ToString:
- 自动生成toString方法。
- 可以用在类级别。
@ToString public class MyClass { private String name; private int age; }
-
@Data:
- 包含了@Getter、@Setter、@ToString、@EqualsAndHashCode等方法的组合注解。
- 可以用在类级别。
@Data public class MyClass { private String name; private int age; }
-
@Builder:
- 自动生成builder模式的构造器。
- 可以用在类级别。
@Builder public class MyClass { private String name; private int age; }
-
@Slf4j:
- 自动生成Slf4j日志实例,可以直接使用日志方法。
- 可以用在类级别。
@Slf4j public class MyClass { public void myMethod() { log.debug("Debug message"); } }
使用Lombok可以大幅度减少冗长的代码,提高开发效率,并且由于生成的代码在编译时自动生成,不会增加运行时的开销。需要注意的是,在使用Lombok的项目中,IDE需要安装相应的Lombok插件,以便在开发过程中正确显示生成的代码。
引入依赖:
1、新项目:创建项目时直接加入依赖:
2、老项目:
别使用太新的版本,也别使用太旧的,可以随大流,选择下载人数多的:
更快的引入依赖的方式:
删除刚刚引入的lombok,然后安装插件:
重启IDEA之后:
主要是GitHub是国外的网站,太慢了。
如果你以前下载过,现在找不到Gitee的,可以尝试更新一下自己的插件,有可能过时了。
也可以直接把URL改成阿里云的:
总之,跳到这个页面:
注意:不是所有依赖都可以在这里添加的, 这个界面和SpringBoot创建项目界面⼀样。
依赖不在这⾥的, 还需要去Maven仓库查找坐标,添加依赖 。
为了探寻Lombok的原理,我们可以开始运行项目,然后点开target:
-
编译: 当编写Java代码时,我们使用的是高级语言,通常是类似于人类语言的代码。这些代码保存在以 .java 为扩展名的源代码文件中。当我们使用Java编译器(例如Javac)编译这些源代码时,它将被转换为字节码文件(.class 文件),这是一种与平台无关的中间表示形式。
-
反编译: 字节码文件中包含了Java虚拟机(JVM)可以理解和执行的指令。如果想查看或理解这些字节码文件,可以使用反编译器将其转换回高级语言。这是一种将字节码翻译回类似于Java源代码的过程。反编译后的代码通常并非与原始源代码完全一致,但它足够接近,以便我们可以理解。
-
Lombok的作用: Lombok是一个Java库,旨在通过使用注解来减少Java代码中的样板代码。Lombok的注解在源代码中添加了一些标记,然后在编译过程中,Lombok通过自定义注解处理器(annotation processor)来修改或生成额外的Java代码。这些生成的代码通常包含了我们通常情况下需要手动编写的重复代码,如getter和setter方法。
-
IDEA反编译显示的内容: 当我们在IDEA中查看target目录下的类文件时,我们实际上是查看的是IDEA自动生成的反编译后的高级语言表示形式。这对于我们理解代码、调试和学习某些库的工作原理非常有用。
继续编写后端代码:
package com.example.UserControl;
import org.springframework.util.StringUtils;
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 {
private List<MessageInfo> messageInfos = new ArrayList<>();
@RequestMapping("/publish")
public boolean publishMessage(MessageInfo messageInfo){
if(!StringUtils.hasLength(messageInfo.getFrom())
|| !StringUtils.hasLength(messageInfo.getTo())
|| !StringUtils.hasLength(messageInfo.getMessage())){
return false;
}
//暂时存放在内存中
messageInfos.add(messageInfo);
return true;
}
@RequestMapping("/getList")
public List<MessageInfo> getList(){
return messageInfos;
}
}
自测
我们先来自测一下 后端代码,确保它没有问题:
我们再添加一条:
好的,后端没有问题。
现在我们开始修正前端代码:
我们把函数补充完整:
function submit() {
//1. 获取留言的内容
var from = $('#from').val();
var to = $('#to').val();
var say = $('#say').val();
if (from == '' || to == '' || say == '') {
return;
}
$.ajax({
type: "post",
url: "/message/publish",
data: {
from: from,
to: to,
message: say
},
success: function (result) {
if (result == true) {
//添加成功
//2. 构造节点
var divE = "<div>" + from + "对" + to + "说:" + say + "</div>";
//3. 把节点添加到页面上
$(".container").append(divE);
//4. 清空输入框的值
$('#from').val("");
$('#to').val("");
$('#say').val("");
}else{
alert("发表失败");
}
}
});
}
现在去测试一下:
使用Postman测试一下是否拿到值了:
但是刷新过后:
因为如果我们没有进行任何调用(点击“提交”),只是单纯的刷新页面,它不会去执行函数。
我们现在希望的是它进行加载的时候直接从后端拿到我们的数据,并显示在页面上。
所以messagewall.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="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script>
//页面加载时, 显示留言信息
//从后端获取到留言信息, 并显示在页面上
$.ajax({
type: "get",
url: "/message/getList",
success: function(messages){
for(var message of messages){
var html = "<div>"+message.from+" 对 "+message.to+" 说: "+message.message+"</div>";
$(".container").append(html);
}
}
});
function submit() {
//1. 获取留言的内容
var from = $('#from').val();
var to = $('#to').val();
var say = $('#say').val();
if (from == '' || to == '' || say == '') {
return;
}
$.ajax({
type: "post",
url: "/message/publish",
data: {
from: from,
to: to,
message: say
},
success: function (result) {
if (result == true) {
//添加成功
//2. 构造节点
var divE = "<div>" + from + "对" + to + "说:" + say + "</div>";
//3. 把节点添加到页面上
$(".container").append(divE);
//4. 清空输入框的值
$('#from').val("");
$('#to').val("");
$('#say').val("");
}else{
alert("发表失败");
}
}
});
}
</script>
</body>
</html>
现在我们再次测试:
刷新过后仍然存在:
我们使用Postman 添加:
刷新网页:
大功告成!
图书管理系统
我们目前先完成图书管理系统的两个功能,随着后面的学习,我们会逐渐把后面的功能补全。
需求评审
1、用户登录验证: 服务器接收来自前端的用户名和密码,通过 /user/login 接口进行身份验证。如果验证成功,返回 true 表示用户名和密码正确;如果验证失败,返回 false 表示用户名或密码错误。响应中可以携带其他信息,如用户信息或 token。
2、获取图书列表: 前端通过调用 /book/getList 接口请求图书列表,后端从数据库或其他数据源获取数据。服务器返回一个包含图书信息的 JSON 数据列表,其中每个图书由 BookInfo 类型定义。前端使用 JavaScript 渲染页面,展示图书信息。
开发阶段
接口定义如下
1. 登录接口
URL: /user/login
请求方法: POST
参数:
- userName (String): 用户名
- passWord (String): 密码
返回:
- true:用户名和密码正确
- false:用户名或密码错误
示例:
Request:
{
"userName": "exampleUser",
"passWord": "examplePassword"
}
Response (成功时):
{
"status": true,
"message": "Login successful"
}
Response (失败时):
{
"status": false,
"message": "Invalid username or password"
}
2. 图书列表接口
URL: /book/getList
请求方法: GET
参数: 无
返回:
List<BookInfo>: 包含图书信息的列表,其中 BookInfo 为图书信息的数据结构。
BookInfo 数据结构:
{
"bookId": 1,
"title": "Example Book",
"author": "John Doe",
// 其他图书信息字段...
}
示例:
Response:
{
"books": [
{
"bookId": 1,
"title": "The Great Gatsby",
"author": "F. Scott Fitzgerald"
// 其他图书信息字段...
},
{
"bookId": 2,
"title": "To Kill a Mockingbird",
"author": "Harper Lee"
// 其他图书信息字段...
},
// 更多图书...
]
}
具体编码:
首先我们先创建一个新的项目:
添加前端代码:
添加完前端代码之后照例运行IDEA,启动项目,去网页看看前端有没有问题:
前端代码没啥问题。
接下来就是编写后端代码:
我们可以先来看看我们最后希望实现的页面,来确定大致的代码设计方案:
BookInfo:
package com.example.librarysystem;
import java.math.BigDecimal;
import lombok.Data;
@Data
public class BookInfo {
private Integer ID;
private String bookName;
private String author;
private Integer count;
private BigDecimal price;
private String publish;
//尽可能避免存储字符串:
private Integer state;//1-可借阅 2-不可借阅
}
userController:
package com.example.librarysystem;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/login")
public boolean login(String userName, String password, HttpSession session){
//校验参数
if(!StringUtils.hasLength(userName) || (!StringUtils.hasLength(password))){
return false;
}
//校验密码是否正确
if("admin".equals(userName) && "admin".equals(password)){
//存Session
session.setAttribute("userName",userName);
return true;
}
return false;
}
}
BookController:
我们这里的数据采用mock的方式。
我们简单了解一下什么是mock?
Mock:虚拟的,假的。
当我们在开发软件时,有时候我们要测试一个部分,但其他部分可能还没有完成。为了不让未完成的部分影响我们的测试,我们使用了一种叫做"Mock"(模拟)的技术。
工作中也是,开发时通常几个团队并行开发。开发后需要进行测试(自测),如果测试的时候,依赖方还没有开发完成,调用方就采用Mock的方式先进行自己这边的测试。
在软件开发中,Mock就像是临时的替身,帮助我们测试软件的一部分,而不受其他部分的影响。这样我们可以更早地发现问题并更方便地进行测试。
如果你想看看更详尽的介绍:
"Mock"(模拟)是软件开发中一种常见的测试技术,用于模拟或替代系统中的组件、服务或对象,以便进行独立的单元测试或集成测试。Mock对象用于模拟实际组件的行为,使得测试可以在一个相对孤立的环境中进行,而不依赖于整个系统的状态或其他组件的可用性。
在软件开发中,有两个主要的概念与Mock相关:
单元测试: 在单元测试中,开发者通常希望测试单个组件(如一个函数、一个类的方法)的行为,而不涉及其他组件。如果被测试的组件依赖于其他组件,而这些组件尚未实现,开发者可能会使用Mock对象代替这些未实现的组件,以确保测试的独立性。
集成测试: 在系统的不同部分集成到一起时,开发者希望确保这些部分能够正确协同工作。如果某个组件的开发尚未完成,但其他组件已经准备好,开发者可能会使用Mock对象代替未完成的组件,以测试系统的其他部分。
使用Mock的主要目的是隔离被测试的组件,以便更容易定位和修复问题,并使测试更加可靠和可重复。Mock对象通常会模拟被替代组件的行为,使得测试过程更加灵活,而不受外部依赖的影响。
为了设置好状态,我们添加一个属性:
这个数据不存在数据库中。
BookInfo更新为:
package com.example.librarysystem;
import java.math.BigDecimal;
import lombok.Data;
@Data
public class BookInfo {
private Integer ID;
private String bookName;
private String author;
private Integer count;
private BigDecimal price;
private String publish;
//尽可能避免存储字符串:
private Integer state;//1-可借阅 2-不可借阅
private String stateCN;
}
package com.example.librarysystem;
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(){
//1.从数据库中获取数据
//数据采用Mock的方式
List<BookInfo> bookInfos = mockBookData();
//2.对数据进行处理——对状态进行处理
for(BookInfo bookInfo : bookInfos){
if(bookInfo.getState()==1){
bookInfo.setStateCN("可借阅");
}else if(bookInfo.getState()==2){
bookInfo.setStateCN("不可借阅");
}
}
//3.返回数据
return bookInfos;
}
public List<BookInfo> mockBookData(){
List<BookInfo> bookInfos = new ArrayList<>();
for(int i =0;i<15;i++){
BookInfo bookInfo = new BookInfo();
bookInfo.setID(i);
bookInfo.setBookName("图书"+i);
bookInfo.setAuthor("作者"+i);
bookInfo.setCount(i*16+4);
bookInfo.setPrice(new BigDecimal(new Random().nextInt(102)));
bookInfo.setPublish("出版社"+i);
bookInfo.setState(i%5==0?2:1);
bookInfos.add(bookInfo);
}
return bookInfos;
}
}
自测
好的,后端代码也同样没有问题。
接下来就是我们前后端交互代码:
先来编写login.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() {
$.ajax({
type:"post",
url:"/user/login",
data:{
userName:$("#userName").val(),
password:$("#password").val()
} ,
success:function(result){
if(result==true){
//验证成功
location.href = "book_list.html";
}else{
alert("用户名或密码错误!");
}
}
});
}
</script>
</body>
</html>
我们现在可以稍微测试一下:
发现没有正确输入也会跳转到列表页,于是打开页面源代码:
清一下缓存:
此时就成功了:
当输入正确时:
正确响应。
这里的代码逻辑比较复杂了,我们可以看看,如果代码不知道哪里出问题了,怎么通过DeBug的方式来查出问题所在:
比如我们用户名和密码都输入正确,但是:
打个断点:
重新发送请求,它会一直运转,此时不用管它,回到我们的后端代码:
下一步:
返回false???
这里是校验参数不为null的,但是明明此时都不为空:
很长的关系可以一个一个点开运行看:
嗯,此时前半句是对的,我们就是得到false才对,否则就进该逻辑了:
再看后半句:
竟然返回true?因为是“或”的关系,所以就进去了……
仔细一看,原来是少写了“ ! ”,我们加上就没问题了:
好,回到正题,我们接下来继续编写前端代码book_list.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>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({
type: "get",
url: "/book/getList",
success: function (books) {
console.log(books);
var finalHtml = "";
for(var book of books){
//拼接html
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.count+'</td>';
finalHtml +='<td>'+book.price+'</td>';
finalHtml +='<td>'+book.publish+'</td>';
finalHtml +='<td>'+book.stateCN+'</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>';
}
$("tbody").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>
分页操作啥的先不急,我们学完数据库会再补充完善的。
应用分层
通过上述练习,我们学习了Spring MVC的简单功能开发,但是我们也同时发现了一些问题。
目前,我们的程序代码显得有些杂乱,尤其在我们只完成了一小部分功能的情况下。
想象一下,如果我们要完成整个项目的功能,代码可能会变得更加混乱无章,文件结构和代码内容都可能变得难以管理。
为了解决这个问题,我们需要考虑一些软件工程的最佳实践,以确保我们的代码具有良好的组织结构、可维护性和可扩展性。
举例来说,在公司初创阶段,由于资源有限,通常会有个人兼职多个职责,例如财务、人事和行政。这种兼职模式在初始阶段可以满足基本需求,但随着公司的逐渐壮大,需要更专业和高效的管理体系。
但是随着公司规模的增大,组织结构会逐渐演变为更为细分的形式。最初的财务、人事和行政部门可能会进一步划分为更专业化的财务部、人事部、行政部等。每个部门内部也会根据需要进行更详细的职能划分,以确保各项工作能够有序进行。
类似地,在项目开发方面,初始阶段可能会采用前后端合并的方式进行开发,尤其是当项目功能相对简单时。比如我们刚完成那些项目……
但随着项目的发展,为了提高开发效率和质量,会将前端和后端逐渐分开,形成不同的团队。我们甚至可以根据项目的复杂性进一步划分更为细粒度的团队,例如专注于特定功能模块或业务领域的团队。
对于后端开发而言,随着后端人员不再涉及前端,可能会引入新的分层方式。常见的分层方式包括:
业务逻辑层(Service层): 负责处理业务逻辑和业务规则,保持与具体数据存储和前端展示的独立性。
数据访问层(DAO层): 负责与数据库或其他数据存储进行交互,执行数据的读取和写入操作。
控制层(Controller层): 接收前端请求,调用业务逻辑层的服务,并返回处理结果给前端。
这种分层方式有助于提高代码的可维护性、可扩展性和复用性。同时,不同的团队或开发者可以专注于各自负责的层级,提高整体开发效率。随着项目的不断演进,可能还会引入其他层级或模块,以适应不断变化的需求。
基于此,我们接下来学习应用分层。
阿里开发手册
阿里开发手册是由阿里巴巴集团内部制定的一套开发规范和最佳实践的文档,该手册旨在规范阿里巴巴公司内部的软件开发流程,提高代码质量、可维护性和团队协作效率。
这一套开发手册不仅包括了代码编写规范,还包括了系统设计、测试、部署等方面的规范建议。
阿里开发手册侧重于规范和最佳实践,是一份用于内部开发团队的指导文档。由于阿里巴巴的技术实践在业界具有较高的影响力,因此阿里开发手册也被广泛关注和应用,成为一份具有指导意义的开发规范。
你可以直接在网上搜索并下载阿里开发手册,我们接下来就基于阿里开发手册来详细探讨各种企业代码规范。
阿里开发手册中, 关于工程结构部分,定义了常见工程的应用分层结构:
企业应用的典型分层结构(简单了解即可):
-
开放接口层:
该层负责将服务接口暴露给外部系统。可以通过RPC(Remote Procedure Call)将Service接口封装成对外提供的服务,也可以通过Web将其封装成HTTP接口。此外,网关控制层也属于这一层,用于控制访问、权限验证等。 -
终端显示层:
终端显示层负责不同端的模板渲染和执行显示操作。这包括各种渲染方式,如velocity渲染、JS渲染、JSP渲染以及移动端展示层等。 -
Web层:
Web层主要处理访问控制、基本参数校验和一些不需要复用的业务简单处理。它负责将请求转发到Service层,并执行一些预处理工作。 -
Service层:
Service层是相对具体的业务逻辑服务层。它包含业务逻辑的具体实现,处理具体的业务需求。在这一层,通常会执行各种业务规则、算法等。 -
Manager层:
Manager层是通用业务处理层,具有以下特征:- 与DAO层进行交互,封装对DAO层业务通用能力的处理。
- 将Service层的通用能力下沉,处理缓存方案、中间件通用处理等。
- 封装对第三方平台的操作,预处理返回结果并转化异常信息。
-
DAO层:
数据访问层,负责与底层数据库(如MySQL、Oracle、Hbase等)进行数据交互。在这一层,数据的读取、写入、更新等操作被具体实现。 -
外部接口或第三方平台:
包括其他部门的RPC开放接口、基础平台提供的接口,以及其他公司的HTTP接口。这一层涵盖了系统与外部系统进行数据交互的接口。
通过这样的分层结构,企业应用实现了不同层次的职责划分,使得代码更具有可维护性和可扩展性。不同层次的代码逻辑清晰,降低了耦合性,提高了团队的协作效率。
什么是应用分层?
应用分层是一种软件开发设计思想,其核心概念是将应用程序划分为多个层次,每个层次负责特定的职责,通过协同工作来提供完整的功能。这种分层的设计有助于提高代码的可维护性、可扩展性和可重用性。根据项目的复杂性,我们可以将应用程序分为三层、四层或更多层。常见的MVC(Model-View-Controller)设计模式就是应用分层思想的一种具体实现。
为什么需要应用分层?
在项目初期,为了迅速上线,我们通常可能会忽略分层的设计。然而,随着业务的不断发展和项目的逐渐复杂化,如果大量代码混在一起,就会出现逻辑不清晰、各模块相互依赖、代码扩展性差、修改一个地方影响全局等问题。因此,学习如何对项目进行分层设计成为程序员的必修课,以应对日益庞大和复杂的软件开发需求。
如何分层(三层架构)?
在三层架构中,我们可以借鉴MVC设计模式的思想,将整个系统分为三个主要层次:
-
模型层(Model): 负责处理应用程序的数据逻辑,包括数据的读取、存储和处理。这一层通常与数据库交互,确保数据的一致性和完整性。
-
视图层(View): 负责用户界面的展示和用户交互。这一层关注于呈现数据,并将用户的操作传递给控制层进行处理。
-
控制层(Controller): 负责处理用户输入、业务逻辑和调度应用程序的流程。控制层接收用户的请求,处理相应的业务逻辑,并更新模型和视图,是任务量最大的一个层次。
这种分层架构使得系统的不同部分之间保持独立性,实现了解耦,提高了代码的可维护性和可测试性。同时,它也为团队协作提供了清晰的界面,使开发人员能够更容易地协同工作,专注于各自领域的开发。
目前现在更主流的开发方式是 "前后端分离" 的方式,它将前端和后端的开发分离开来,使得后端开发者不再需要关注前端的具体实现细节。这种开发方式也促使了新的分层架构的应用,其中最常见的是"三层架构",将整体架构分为表现层、业务逻辑层和数据层。
-
表现层(View、Controller):
表现层是最靠近用户的一层,负责展示数据结果和接受用户指令。在Web应用中,通常由前端负责实现,后端提供API接口供前端调用。表现层的主要任务是将用户的请求转发给业务逻辑层,并将业务逻辑层返回的数据呈现给用户。 -
业务逻辑层:
业务逻辑层负责处理具体的业务逻辑,包括业务规则的制定和实现。这一层通常包含了应用程序的核心功能,是整个系统的"大脑"。后端开发者在这一层实现了复杂业务的具体逻辑,保持了与表现层的独立性。 -
数据层:
数据层负责存储和管理与应用程序相关的数据。这包括数据库的设计、数据的存储和检索等。后端开发者通过数据层与数据库进行交互,确保数据的持久性和一致性。
在这种分层架构中,每一层都有特定的职责,实现了代码的模块化和解耦。前后端分离使得前端和后端可以独立开发、测试和部署,提高了团队的协作效率。此外,这种"三层架构"也更易于维护和扩展,使得系统更具可伸缩性和可维护性。
相比之下,之前的代码可能是基于传统的开发方式,将所有的代码堆砌在一起,难以维护和扩展。"前后端分离"及"三层架构"为现代应用开发带来了更好的架构设计和开发实践。
我们可以基于“三层架构”来看看我们的图书管理系统代码:
按照上面的层次划分,Spring MVC站在后端开发人员的角度上,也进行了支持,将上面的代码划分为三个部分:
-
请求处理、响应数据:
负责接收页面的请求,给页面响应数据。 -
逻辑处理:
负责业务逻辑处理的代码。 -
数据访问:
负责业务数据的维护操作,包括增、删、改、查等操作。
在Spring的实现中,这三个部分均有体现:
-
Controller(控制层):
接收前端发送的请求,对请求进行处理,并响应数据。 -
Service(业务逻辑层):
处理具体的业务逻辑。 -
Dao(数据访问层,也称为持久层):
负责数据访问操作,包括数据的增、删、改、查。
这种划分使得代码更加模块化,每个部分都有明确的职责,有助于提高代码的可维护性和可扩展性。开发者可以专注于各自负责的领域,降低了不同部分之间的耦合度,同时也符合了前文提到的"三层架构"的设计思想。
我们接下来就对我们的代码进行相应的应用分层:
补充完整:BookServices
package com.example.librarysystem.book.service;
import com.example.librarysystem.book.dao.BookDao;
import com.example.librarysystem.book.model.BookInfo;
import com.sun.org.apache.bcel.internal.generic.NEW;
import java.util.List;
public class BookService {
/*
* 根据数据层返回的结果, 对数据进行处理
* */
public List<BookInfo> bookInfoList(){
BookDao bookDao= new BookDao();
List<BookInfo> bookInfos= bookDao.mockBookData();
//2.对数据进行处理——对状态进行处理
for(BookInfo bookInfo : bookInfos){
if(bookInfo.getState()==1){
bookInfo.setStateCN("可借阅");
}else if(bookInfo.getState()==2){
bookInfo.setStateCN("不可借阅");
}
}
return bookInfos;
}
}
BookDao和BookServices 也就写好了:
接下来是BookController的补充:
package com.example.librarysystem.book.controller;
import com.example.librarysystem.book.model.BookInfo;
import com.example.librarysystem.book.service.BookService;
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(){
//1.从数据库中获取数据
//数据采用Mock的方式
BookService bookService=new BookService();
List<BookInfo> bookInfos = bookService.bookInfoList();
//3.返回数据
return bookInfos;
}
}
程序运行没有问题:
MVC 和三层架构的区别和联系
思考题,没有标准答案~~~
关于二者的关系, 一直存在不同的观点。有人认为三层架构是MVC模式的一种实现,也有人认为MVC是三层架构的替代方案, 等等各种说法都有。
根本原因是大家站在不同的角度来看待这个问题的。
JavaEE 部分的学习重在"实践", 大家需要根据自己的理解, 能够自圆其说, 说出自己的观点即可,也不建议大家去背书。
我们这里只给出二者的介绍,你可以自行理解:
从概念上来讲,二者都是软件工程领域中的架构模式。
MVC(Model-View-Controller)和三层架构都是软件工程领域中常见的架构模式,它们在抽象和解耦方面有着不同的关注点,但又有一些联系和相似之处。
MVC 模式:
-
组成部分:
- 模型(Model):负责应用程序的数据和业务逻辑。
- 视图(View):负责展示数据给用户,与用户交互。
- 控制器(Controller):负责处理用户的输入、调度模型和视图之间的交互。
-
关注点:
- 数据和视图分离,通过控制器协调数据和视图的交互。
- 通过控制器对模型和视图进行组合,实现用户界面的响应和数据的处理。
MVC中,视图和控制器合起来对应三层架构中的表现层。模型对应三层架构中的业务逻辑层、数据层以及实体类。
三层架构:
-
组成层次:
- 表现层:负责展现数据和接收用户输入,相当于MVC中的视图和控制器。
- 业务逻辑层:负责处理具体的业务逻辑,相当于MVC中的模型和部分控制器。
- 数据访问层:负责与数据库进行交互,进行数据的增、删、改、查操作。
-
关注点:
- 强调不同维度数据处理的高内聚和低耦合。
- 将交互界面、业务处理和数据库操作的逻辑分开,实现模块化和可维护性。
二者其实是从不同角度对软件工程进行了抽象。
角度不同也就谈不上互相替代了,在日常的开发中我们可以经常看到两种共存的情况,比如我们设计模型层的时候往往也会拆分出业务逻辑层(Service层)和数据访问层(Dao层)。
但是二者的目的是相同的,都是“解耦,分层,代码复用”。
区别和联系:
-
关注层次不同:
- MVC更注重在应用内部的交互,强调模型、视图和控制器的协同工作。
- 三层架构更注重在整个应用中的分层,强调表现层、业务逻辑层和数据访问层的划分。
-
抽象和解耦:
- MVC强调数据和视图的分离,通过控制器解耦。
- 三层架构强调在不同层次上的数据处理高内聚和低耦合。
-
共同目标:
两者的共同目标是"解耦,分层,代码复用",都追求更好的可维护性、可扩展性和代码复用性。
在实际开发中,这两种架构模式并不是互斥的,经常可以看到它们的共存。在设计模型层时,也会拆分出业务逻辑层和数据访问层。综合使用MVC和三层架构的思想有助于更好地组织和管理复杂的软件系统。
既然说两者的共同目标有“解耦”,那我们现在就来详细了解一下软件设计原则:高内聚低耦合。
高内聚和低耦合是软件设计中非常重要的原则,它们有助于构建可维护、可扩展、可重用的软件系统。
-
高内聚(High Cohesion): 意味着一个模块内的各个元素(如类、方法等)彼此关联紧密,共同完成某一特定的任务或目标。高内聚的模块更容易理解、测试和维护,因为它们功能相关,修改一个地方不容易对其他地方产生影响。内聚度越高,模块的独立性越好。
-
低耦合(Low Coupling): 表示模块之间的依赖关系尽量保持松散,一个模块的变化不会引起其他模块的大量变化。低耦合有助于系统的灵活性和可维护性,因为各个模块相对独立,可以更容易替换、修改或重用。降低耦合度可以通过接口定义清晰、依赖注入、使用设计模式等方式来实现。
我们可以举几个例子来解释这两个概念:
-
企业内聚: 企业的各个部门内部员工关系要尽量紧密,形成高内聚,以便在解决问题时能够协同合作。但是,各个部门之间的关系要尽可能小,以降低耦合度,减少一个部门的问题对其他部门的影响。
-
家庭内聚: 家庭成员之间的关系要尽量紧密,形成高内聚,以便在解决问题时能够共同协作。但是,家庭与邻里之间的关系要尽可能小,以降低耦合度,减少家庭问题对邻里的影响。
综合来看,高内聚和低耦合是相辅相成的原则,共同有助于构建更加健壮和可维护的软件系统。
企业命名规范
这里的企业规范适用于多数企业, 但是均不做强制要求. 具体还是以你所在企业为准。
命名规约是在编写Java代码时应当遵循的一系列规范和最佳实践。
以下是我对阿里巴巴Java开发手册中的命名规约的摘要和总结:
-
命名规范:
- 代码中的命名不得以下划线或美元符号开始或结束。
- 严禁使用拼音与英文混合的方式,不允许直接使用中文。
- 类名使用UpperCamelCase风格,方法名、参数名、成员变量、局部变量使用lowerCamelCase风格。
- 常量命名全部大写,单词间用下划线隔开。
- 抽象类以Abstract或Base开头,异常类以Exception结尾,测试类以Test结尾。
- 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。
-
POJO 类中的布尔类型变量: 不要加is,避免序列化错误。
-
设计模式体现在类名中: 如果使用到设计模式,建议在类名中体现出具体模式,有利于阅读者理解架构设计思想。
-
接口类的方法和属性: 方法和属性不加任何修饰符号(public也不加),并加上有效的Javadoc注释。
-
Service和DAO类: Service和DAO类的实现类用Impl的后缀与接口区别。
-
枚举类: 类名带上Enum后缀,成员名称全大写,单词间用下划线隔开。
-
各层命名规约:
- Service/DAO层方法命名规约:
- 获取单个对象的方法用get前缀。
- 获取多个对象的方法用list前缀。
- 获取统计值的方法用count前缀。
- 插入的方法用save(推荐)或insert前缀。
- 删除的方法用remove(推荐)或delete前缀。
- 修改的方法用update前缀。
- 领域模型命名规约:
- 数据对象:xxxDO,xxx为数据表名。
- 数据传输对象:xxxDTO,xxx为业务领域相关的名称。
- 展示对象:xxxVO,xxx一般为网页名称。
- POJO是DO/DTO/BO/VO的统称,禁止命名成xxxPOJO。
- Service/DAO层方法命名规约:
这些规约有助于我们实现代码的一致性、可读性和可维护性,是在Java开发中良好的编码风格。
关于企业命名,还有一些需要格外注意的地方:
-
类名的大驼峰风格:
类名采用大驼峰命名法,即所有单词首字母都需要大写。但有一些特殊情形除外,如DO(Data Object)、BO(Business Object)、DTO(Data Transfer Object)、VO(View Object)、AO(Application Object)等。 -
方法名、参数名、成员变量、局部变量的小驼峰风格:
方法名、参数名、成员变量、局部变量统一使用小驼峰命名法,即除了第一个单词外,其他单词的首字母大写。 -
包名的小写风格:
包名统一使用小写字母,且点分隔符之间有且仅有一个自然语义的英语单词。这有助于提高包的可读性和识别性。
常见命名风格介绍:
-
大驼峰(帕斯卡命名法):
- 所有单词的首字母都需要大写,例如:UserController。
-
小驼峰:
- 除了第一个单词,其他单词的首字母大写,例如:userController。
-
蛇形(下划线命名法):
- 用下划线(_)作为单词间的分隔符,一般使用小写字母,例如:user_controller。
-
串形(脊柱命名法):
- 用短横线(-)作为单词间的分隔符,也叫脊柱命名法,例如:user-controller。
这些命名风格的选择主要还是取决于团队的约定和项目的实际需求。而且在采用特定的命名风格时,保持一致性是至关重要的,这样可以提高我们代码的可维护性和可读性。通过这种规范,团队也能够更容易地理解和协作,从而提高代码质量和开发效率。