本专栏将从基础开始,循序渐进,以实战为线索,逐步深入SpringBoot相关知识相关知识,打造完整的云原生学习步骤,提升工程化编码能力和思维能力,写出高质量代码。希望大家都能够从中有所收获,也请大家多多支持。
专栏地址:SpringBoot专栏
本文涉及的代码都已放在gitee上:gitee地址
如果文章知识点有错误的地方,请指正!大家一起学习,一起进步。
本文所涉及的代码已经编译打包到我的云服务器中:http://47.106.176.37:8080/pages/books.html 欢迎大家测试访问。
先看一下这个案例的最终效果
主页面
添加
删除
修改
分页
条件查询
整体案例中需要采用的技术如下
- 实体类开发————使用Lombok快速制作实体类
- Dao开发————整合MyBatisPlus,制作数据层测试
- Service开发————基于MyBatisPlus进行增量开发,制作业务层测试类
- Controller开发————基于Restful开发,使用PostMan测试接口功能
- Controller开发————前后端开发协议制作
- 页面开发————基于VUE+ElementUI制作,前后端联调,页面数据处理,页面消息处理
- 列表
- 新增
- 修改
- 删除
- 分页
- 查询
- 项目异常处理
- 按条件查询————页面功能调整、Controller修正功能、Service修正功能
希望通过这个案例,各位小伙伴能够完成基础开发的技能训练。整体开发过程采用做一层测一层的形式进行,过程完整。
列表功能主要操作就是加载完数据,将数据展示到页面上,此处要利用VUE的数据模型绑定,发送请求得到数据,然后页面上读取指定数据即可。
页面数据模型定义
data:{
dataList: [], //当前页要展示的列表数据
...
},
异步请求获取数据
//列表
getAll() {
axios.get("/books").then((res)=>{
this.dataList = res.data.data;
});
},
这样在页面加载时就可以获取到数据,并且由VUE将数据展示到页面上了。
添加功能用于收集数据的表单是通过一个弹窗展示的,因此在添加操作前首先要进行弹窗的展示,添加后隐藏弹窗即可。因为这个弹窗一直存在,因此当页面加载时首先设置这个弹窗为不可显示状态,需要展示,切换状态即可。
默认状态
data:{
dialogFormVisible: false, //添加表单是否可见
...
},
切换为显示状态
//弹出添加窗口
handleCreate() {
this.dialogFormVisible = true;
},
由于每次添加数据都是使用同一个弹窗录入数据,所以每次操作的痕迹将在下一次操作时展示出来,需要在每次操作之前清理掉上次操作的痕迹。
定义清理数据操作
//重置表单
resetForm() {
this.formData = {};
},
切换弹窗状态时清理数据
//弹出添加窗口
handleCreate() {
this.dialogFormVisible = true;
this.resetForm();
},
至此准备工作完成,下面就要调用后台完成添加操作了。
添加操作
//添加
handleAdd () {
//发送异步请求
axios.post("/books",this.formData).then((res)=>{
//如果操作成功,关闭弹层,显示数据
if(res.data.flag){
this.dialogFormVisible = false;
this.$message.success("添加成功");
}else {
this.$message.error("添加失败");
}
}).finally(()=>{
this.getAll();
});
},
- 将要保存的数据传递到后台,通过post请求的第二个参数传递json数据到后台
- 根据返回的操作结果决定下一步操作
- 如何是true就关闭添加窗口,显示添加成功的消息
- 如果是false保留添加窗口,显示添加失败的消息
- 无论添加是否成功,页面均进行刷新,动态加载数据(对getAll操作发起调用)
取消添加操作
//取消
cancel(){
this.dialogFormVisible = false;
this.$message.info("操作取消");
},
总结
- 请求方式使用POST调用后台对应操作
- 添加操作结束后动态刷新页面加载数据
- 根据操作结果不同,显示对应的提示信息
- 弹出添加Div时清除表单数据
模仿添加操作制作删除功能,差别之处在于删除操作仅传递一个待删除的数据id到后台即可。
删除操作
// 删除
handleDelete(row) {
axios.delete("/books/"+row.id).then((res)=>{
if(res.data.flag){
this.$message.success("删除成功");
}else{
this.$message.error("删除失败");
}
}).finally(()=>{
this.getAll();
});
},
删除操作提示信息
// 删除
handleDelete(row) {
//1.弹出提示框
this.$confirm("此操作永久删除当前数据,是否继续?","提示",{
type:'info'
}).then(()=>{
//2.做删除业务
axios.delete("/books/"+row.id).then((res)=>{
if(res.data.flag){
this.$message.success("删除成功");
}else{
this.$message.error("删除失败");
}
}).finally(()=>{
this.getAll();
});
}).catch(()=>{
//3.取消删除
this.$message.info("取消删除操作");
});
},
总结
- 请求方式使用Delete调用后台对应操作
- 删除操作需要传递当前行数据对应的id值到后台
- 删除操作结束后动态刷新页面加载数据
- 根据操作结果不同,显示对应的提示信息
- 删除操作前弹出提示框避免误操作
修改功能可以说是列表功能、删除功能与添加功能的合体。几个相似点如下:
页面也需要有一个弹窗用来加载修改的数据,这一点与添加相同,都是要弹窗
弹出窗口中要加载待修改的数据,而数据需要通过查询得到,这一点与查询全部相同,都是要查数据
查询操作需要将要修改的数据id发送到后台,这一点与删除相同,都是传递id到后台
查询得到数据后需要展示到弹窗中,这一点与查询全部相同,都是要通过数据模型绑定展示数据
修改数据时需要将被修改的数据传递到后台,这一点与添加相同,都是要传递数据
所以整体上来看,修改功能就是前面几个功能的大合体
查询并展示数据
//弹出编辑窗口
handleUpdate(row) {
axios.get("/books/"+row.id).then((res)=>{
if(res.data.flag){
//展示弹层,加载数据
this.formData = res.data.data;
this.dialogFormVisible4Edit = true;
}else{
this.$message.error("数据同步失败,自动刷新");
}
});
},
修改操作
//修改
handleEdit() {
axios.put("/books",this.formData).then((res)=>{
//如果操作成功,关闭弹层并刷新页面
if(res.data.flag){
this.dialogFormVisible4Edit = false;
this.$message.success("修改成功");
}else {
this.$message.error("修改失败,请重试");
}
}).finally(()=>{
this.getAll();
});
},
总结
- 加载要修改数据通过传递当前行数据对应的id值到后台查询数据(同删除与查询全部)
- 利用前端双向数据绑定将查询到的数据进行回显(同查询全部)
- 请求方式使用PUT调用后台对应操作(同新增传递数据)
- 修改操作结束后动态刷新页面加载数据(同新增)
- 根据操作结果不同,显示对应的提示信息(同新增)
目前的功能制作基本上达成了正常使用的情况,什么叫正常使用呢?也就是这个程序不出BUG,如果我们制造一个BUG出来,你会发现程序马上崩溃掉。比如后台手工抛出一个异常,看看前端接收到的数据什么样子。
{
"timestamp": "2021-09-15T03:27:31.038+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/books"
}
面对这种情况,前端的同学又不会了,这又是什么格式?怎么和之前的格式不一样?
{
"flag": true,
"data":{
"id": 1,
"type": "计算机理论",
"name": "Spring实战 第5版",
"description": "Spring入门经典教程"
}
}
看来不仅要对正确的操作数据格式做处理,还要对错误的操作数据格式做同样的格式处理。
首先在当前的数据结果中添加消息字段,用来兼容后台出现的操作消息。
@Data
public class R{
private Boolean flag;
private Object data;
private String msg; //用于封装消息
}
后台代码也要根据情况做处理,当前是模拟的错误。
@PostMapping
public R save(@RequestBody Book book) throws IOException {
Boolean flag = bookService.insert(book);
return new R(flag , flag ? "添加成功^_^" : "添加失败-_-!");
}
然后在表现层做统一的异常处理,使用SpringMVC提供的异常处理器做统一的异常处理。
@RestControllerAdvice
public class ProjectExceptionAdvice {
@ExceptionHandler(Exception.class)
public R doOtherException(Exception ex){
//记录日志
//发送消息给运维
//发送邮件给开发人员,ex对象发送给开发人员
ex.printStackTrace();
return new R(false,null,"系统错误,请稍后再试!");
}
}
页面上得到数据后,先判定是否有后台传递过来的消息,标志就是当前操作是否成功,如果返回操作结果false,就读取后台传递的消息。
//添加
handleAdd () {
//发送ajax请求
axios.post("/books",this.formData).then((res)=>{
//如果操作成功,关闭弹层,显示数据
if(res.data.flag){
this.dialogFormVisible = false;
this.$message.success("添加成功");
}else {
this.$message.error(res.data.msg); //消息来自于后台传递过来,而非固定内容
}
}).finally(()=>{
this.getAll();
});
},
分页功能的制作用于替换前面的查询全部,其中要使用到elementUI提供的分页组件。
为了配合分页组件,封装分页对应的数据模型。
data:{
pagination: {
//分页相关模型数据
currentPage: 1, //当前页码
pageSize:10, //每页显示的记录数
total:0, //总记录数
}
},
修改查询全部功能为分页查询,通过路径变量传递页码信息参数。
getAll() {
axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize).then((res) => {
});
},
后台提供对应的分页功能。
@GetMapping("/{currentPage}/{pageSize}")
public R getAll(@PathVariable Integer currentPage,@PathVariable Integer pageSize){
IPage pageBook = bookService.getPage(currentPage, pageSize);
return new R(null != pageBook ,pageBook);
}
页面根据分页操作结果读取对应数据,并进行数据模型绑定。
getAll() {
axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize).then((res) => {
this.pagination.total = res.data.data.total;
this.pagination.currentPage = res.data.data.current;
this.pagination.pagesize = res.data.data.size;
this.dataList = res.data.data.records;
});
},
对切换页码操作设置调用当前分页操作。
//切换页码
handleCurrentChange(currentPage) {
this.pagination.currentPage = currentPage;
this.getAll();
},
由于使用了分页功能,当最后一页只有一条数据时,删除操作就会出现BUG,最后一页无数据但是独立展示,对分页查询功能进行后台功能维护,如果当前页码值大于最大页码值,重新执行查询。其实这个问题解决方案很多,这里给出比较简单的一种处理方案。
@GetMapping("{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage,@PathVariable int pageSize){
IPage page = bookService.getPage(currentPage, pageSize);
//如果当前页码值大于了总页码值,那么重新执行查询操作,使用最大页码值作为当前页码值
if( currentPage > page.getPages()){
page = bookService.getPage((int)page.getPages(), pageSize);
}
return new R(true, page);
}
最后一个功能来做条件查询,其实条件查询可以理解为分页查询的时候除了携带分页数据再多带几个数据的查询。这些多带的数据就是查询条件。比较一下不带条件的分页查询与带条件的分页查询差别之处,这个功能就好做了
页面封装的数据:带不带条件影响的仅仅是一次性传递到后台的数据总量,由传递2个分页相关数据转换成2个分页数据加若干个条件
后台查询功能:查询时由不带条件,转换成带条件,反正不带条件的时候查询条件对象使用的是null,现在换成具体条件,差别不大
查询结果:不管带不带条件,出来的数据只是有数量上的差别,其他都差别,这个可以忽略
经过上述分析,看来需要在页面发送请求的格式方面做一定的修改,后台的调用数据层操作时发送修改,其他没有区别。
页面发送请求时,两个分页数据仍然使用路径变量,其他条件采用动态拼装url参数的形式传递。
页面封装查询条件字段
pagination: {
//分页相关模型数据
currentPage: 1, //当前页码
pageSize:10, //每页显示的记录数
total:0, //总记录数
name: "",
type: "",
description: ""
},
页面添加查询条件字段对应的数据模型绑定名称
查询
新建
将查询条件组织成url参数,添加到请求url地址中,这里可以借助其他类库快速开发,当前使用手工形式拼接,降低学习要求
getAll() {
//1.获取查询条件,拼接查询条件
param = "?name="+this.pagination.name;
param += "&type="+this.pagination.type;
param += "&description="+this.pagination.description;
console.log("-----------------"+ param);
axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize+param).then((res) => {
this.dataList = res.data.data.records;
});
},
后台代码中定义实体类封查询条件
@GetMapping("{currentPage}/{pageSize}")
public R getAll(@PathVariable int currentPage,@PathVariable int pageSize,Book book) {
System.out.println("参数=====>"+book);
IPage pageBook = bookService.getPage(currentPage,pageSize);
return new R(null != pageBook ,pageBook);
}
对应业务层接口与实现类进行修正
public interface IBookService extends IService {
IPage getPage(Integer currentPage,Integer pageSize,Book queryBook);
}
@Service
public class BookServiceImpl2 extends ServiceImpl implements IBookService {
public IPage getPage(Integer currentPage,Integer pageSize,Book queryBook){
IPage page = new Page(currentPage,pageSize);
LambdaQueryWrapper lqw = new LambdaQueryWrapper();
lqw.like(Strings.isNotEmpty(queryBook.getName()),Book::getName,queryBook.getName());
lqw.like(Strings.isNotEmpty(queryBook.getType()),Book::getType,queryBook.getType());
lqw.like(Strings.isNotEmpty(queryBook.getDescription()),Book::getDescription,queryBook.getDescription());
return bookDao.selectPage(page,lqw);
}
}
页面回显数据
getAll() {
//1.获取查询条件,拼接查询条件
param = "?name="+this.pagination.name;
param += "&type="+this.pagination.type;
param += "&description="+this.pagination.description;
console.log("-----------------"+ param);
axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize+param).then((res) => {
this.pagination.total = res.data.data.total;
this.pagination.currentPage = res.data.data.current;
this.pagination.pagesize = res.data.data.size;
this.dataList = res.data.data.records;
});
},
先在maven中打包项目:
接下来需要在项目目录新建Dockerfile,并配置Dockerfile的内容
具体内容如下:
# 导入jdk8镜像
FROM openjdk:8-jdk-slim
#设置作者
LABEL maintainer=hashnode
#将target目录中的*.jar 放在更目录下,新的jar包为app.jar,由于只有一个jar,所以这样这样操作
COPY target/*.jar /app.jar
#docker启动时的命令,相当于 java -jar /app.jar,注意:这里为app.jar,因为在上一步制作镜像的时候已经把jar更名为app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
jdk镜像可以在如下地址进行寻找:
由于windows本地没有docker环境,不能制作docker镜像,所以我们复制target目录和Dockerfile到服务器中,为了和配置文件一致,这里新建一个upload文件夹,并把jar包需要放在target目录下,如下图所示
然后使用electerm将该目录上传到服务器:
然后进入/root/upload目录制作镜像:
接下来运行镜像即可:
先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦