目录
准备工作
实体类的开发
数据层开发
测试数据层增删改查
分页查询:
条件查询:
业务层:
标准开发(基础CRUD):
快速开发(基于Mybatis-plus):
表现层
使用postman测试请求:
表现层数据一致性处理
前后端数据协议
前后端调用(axios)发送异步请求
显示全部数据
增添数据
删除数据
错误分析
修改数据
异常处理
springMVC异常处理器
分页查询
Bug
条件查询
结尾
为了强化对于springboot基础的学习,这里做一个图书管理系统,用springboot整合SSMP框架来实现这个系统,这篇文章会记录在这个小系统的开发中的思路、过程、以及一些知识点。
前端用到的知识:Vue axios Element-ui
后端用到的知识:Springboot SSMP(Spring SpringMVC Mybatis-plus)
首先,给数据库的表做一下初始化
在写pojo类时,我们通常的方法是定义一个类,类的成员变量跟表的字段相对应,但在这里我们提供一个更加简便的方法:Lombot
我们先在pom配置文件中添加一个Lombot的坐标
org.projectlombok
lombok
springboot中已经为我们收录了lombok的版本,因此这里不需要写版本号
在pojo类加一个注解@Data,就自动帮我们生成了get set toString hashCode equals等方法,因此我们不用再去手动创建这些方法
package com.yit.domain;
import lombok.Data;
@Data
public class Book {
private Integer id;
private String type;
private String name;
private String description;
}
数据层开发用到的技术为:Mybatis-plus,Druid
如果是从官网路径创建的项目,要先导入一下依赖坐标
com.baomidou
mybatis-plus-boot-starter
3.4.3
com.alibaba
druid-spring-boot-starter
1.2.6
再在配置文件中,连接上数据库
#把端口修改为80
server:
port: 80
#连接数据库
spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db1?serverTimezone=UTC
username: root
password: 123456
配置完成后,接下来就要做数据层接口,为数据层的接口BookDao添加一个@Mapper注解,用于识别。让这个类继承BaseMapper类,这样数据层基本就完成了,接下来就可以来测试数据层的各个方法。
@Mapper
public interface BookDao extends BaseMapper{
}
然后在text目录中新建一个dao包,写一个测试类,测试一下这个方法
@SpringBootTest
public class BookDaoTestCase {
@Autowired
BookDao bookDao;
@Test
void textSelectById(){
System.out.println(bookDao.selectById(1));
}
}
运行成功,说明前面的步骤没有问题。
当我们测试插入数据的方法时,会发现一个问题
原因是没有使用数据库中的id自增策略,不知道应该赋给id什么样的值,我们将配置文件改为以下内容,便可解决这个问题
#把端口修改为80
server:
port: 80
#连接数据库
spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db1?serverTimezone=UTC
username: root
password: 123456
mybatis-plus:
global-config:
db-config:
#使用数据库的id自增策略
id-type: auto
为了方便观察mp的运行情况,我们可以开启mp的调试日志
还是配置文件,加入下面的两行即可
mybatis-plus:
global-config:
db-config:
#使用数据库的id自增策略
id-type: auto
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
日志就已经在控制台上出现了,这个日志仅在开发时使用,在上线时,这个日志一定要关掉,不然服务器会崩溃的。
注:在测试查询所有的方法是,是要传入一个参数的,这里传null即可,这样就能查询出所有的数据
测试分页查询的方法,我们可以发现这里要传入两个参数
第一个是page,第二个是queryWrapper,我们可以给第二个参数传一个null,而第一个参数需要的就是一个page对象,因此,我们要在测试方法里new一个page对象出来
Page的构造方法中,current表示的是第几页的数据,size则是表示一页中有几条数据。但这时如果测试,会发现分页查询是没有生效的。在mp中如果要使用分页查询的功能,要添加拦截器,而添加拦截器就要在springboot项目中写一个配置类,通过配置类来完成拦截器的添加。我们在com.yit包下新建一个config包,在里面写一个MPConfig类代码如下
//定义mp拦截器
@Configuration//加载为配置类
public class MPConfig {
@Bean//将这个拦截器bean做出来
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//在拦截器中添加一个具体的拦截器,也就是用来做分页查询的拦截器,以后还会有其他的拦截器
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
这样才可以实现分页查询的功能,我们可以通过测试来观察一下结果。
分页查询成功实现,那我们如何拿到分页查询的这些数据呢?
可以通过page对象中的这五个方法,拿到总页数,当前页数,返回的数据,每页条数,数据总条数。
其实条件查询直接使用前面提到过的查询所有的方法即可,前面调用查询所有的时候,参数我们传入了一个null,其实这个参数就是条件。
我们将这个对象创建出来,后调用这个对象的like方法,其实对应的就是sql语句中的like关键字 模糊查询。将字段名和值传入like方法,再将这个对象作为条件传入selectList方法即可
@Test
void textSelextByCondition(){
QueryWrapper qw=new QueryWrapper<>();
qw.like("type","计算机基础");
bookDao.selectList(qw);
}
查询成功
为了方便书写并减少错误,mp还为我们提供了一个对象LambdaQuaryWrapper,效果是一样的,还不容易写错。
@Test
void textSelextByCondition2(){
LambdaQueryWrapper lqw=new LambdaQueryWrapper<>();
lqw.like(Book::getType,"计算机基础");
bookDao.selectList(lqw);
}
}
有时type的值可能为null,这样就没必要再连接这个条件,按照以前的思路,我们可以通过if条件进行判断,现在mp为我们提供了一种新的方式,可以在like的参数中传入一个布尔类型的值,如果为true就连接条件,如果是false就不连接条件。
观察一下运行结果,还是查询所有,并没有为我们连接条件
如果将type改为字符串,那么条件将会连接
写完数据层,接下来就要写业务层了
我们在com.yit包下新建一个service包,里面创建一个BookService接口,来定义service层中基本的方法,再创建一个impl包,在里面写这个接口的实现类,实现类的上面要加入@Service注解,将这个实现类定义为业务层的bean。注入BookDao接口并自动装配,然后在实现类中返回调用方法即可。条件查询先不着急写
public interface BookService {
Boolean add(Book book);
Boolean update(Book book);
Boolean delete(Integer id);
Book getById(Integer id);
List getAll();
IPage getPage(int currentPage,int pageSize);
}
@Service//将这个类定义为业务层的bean
public class BookServiceImpl implements BookService {
@Autowired
BookDao bookDao;
@Override
public Boolean add(Book book) {
//insert的返回值为影响行数,若影响行大于0,则操作成功
return bookDao.insert(book)>0;
}
@Override
public Boolean update(Book book) {
return bookDao.updateById(book)>0;
}
@Override
public Boolean delete(Integer id) {
return bookDao.deleteById(id)>0;
}
@Override
public Book getById(Integer id) {
return bookDao.selectById(id);
}
@Override
public List getAll() {
return bookDao.selectList(null);
}
@Override
public IPage getPage(int currentPage, int pageSize) {
IPage page=new Page(currentPage,pageSize);
bookDao.selectPage(page,null);
return page;
}
}
接下来就可以在text目录下com.yit包下新建一个service包来测试了
开发中测试时尽量还是要每个方法都测试一遍
因为业务层的代码逻辑大部分都相同,因此mp也为我们提供了快速开发的方式。因为mp中给出的getPage较为复杂,我们在这里重写一下
接口:
//跟数据层一样,里面已经为我们封装好了很多方法
public interface IBookService extends IService {
IPage getPage(int currentPage, int pageSize);
}
实现类:
@Service
public class IBookServiceImpl extends ServiceImpl implements IBookService {
@Autowired
BookDao bookDao;
public IPage getPage(int currentPage, int pageSize) {
IPage page=new Page(currentPage,pageSize);
bookDao.selectPage(page,null);
return page;
}
}
随后测试即可。如果想再加入业务层的其他功能,在接口和实现类中像标准开发一样写即可
数据层和业务层都完成了,那么接下来就要做表现层了。表现层中基于Restful进行表现层接口开发,然后使用postman来测试表现层接口
我们在yit包下新建一个controller包,里面新建一个BookController类,这里"/books"是命名规范,一般为模块名+s
@RestController
@RequestMapping("/books")
public class BookController {
//注入业务层
@Autowired
private IBookService iBookService;
@GetMapping//请求方式,get请求
public List getAll(){
return iBookService.list();
}
}
现在就可以运行这个springboot项目了,运行后可以发现
json格式的数据已经被发送到了页面上
现在我们用postman进行调试,postman是一款软件,可以到官网下载Download Postman | Get Started for Free
完成注册后输入http://localhost/books然后点击send就可以看到发送过来的数据了
接下来可以再来写一个post请求(注意看注释)
@PostMapping
public Boolean save(@RequestBody Book book){//请求体参数为book,接收json数据
return iBookService.save(book);
}
@PutMapping//这里是put方式提交 属于Restful开发方式
public Boolean update(@RequestBody Book book){
return iBookService.updateById(book);
}
//路径:http://localhost/books/1
@DeleteMapping("{id}")//这里是delete方式提交 属于Restful开发方式
public Boolean delete(@PathVariable Integer id){//从路径中获取id
return iBookService.removeById(id);
}
@GetMapping("{id}")
public Book getById(@PathVariable Integer id){
return iBookService.getById(id);
}
再来重启一下,再postman中测试
在测试post请求时选择Body-raw-JSON就可以在下面的文本框中来写JSON数据了
点击send后,可以看到响应中给出了true,说明请求成功
数据库中也成功添加了测试数据,是不是很好用呢
接下来就可以利用postman,对剩下的请求进行测试了,postman还是很容易上手的。
但是这个地方有一个弊端,就是有时候返回的时json数组,有时候返回一个json数据,有时候返回一个true,这不利于前端的开发,那我们怎么解决这个问题呢?
现在我们返回给前端的数据的格式是很乱的
我们可以将这些数据都放到统一的data里面,这时问题又来了,那如果我查到了一个null,那么此时到底是没有找到数据呢,还是过程中抛异常了呢,这个是没有办法确定的
我们在数据里面再增添一条flag就可以判断,到底是没查到还是过程中抛异常了
设计表现层的返回结果模型类,用于统一数据。在controller包下新建一个utils包,里面创建一个R类,类里面写两个属性flag和data,并加上@Data注解,就不用写get set等方法了,最后写一个flag的构造放法
@Data
public class R {
private Boolean flag;
public Object data;
public R(){
}
public R(boolean flag){
this.flag=flag;
}
public R(boolean flag,Object data){
this.flag=flag;
this.data=data;
}
}
接下来对BookController进行修改,把所有的返回值类型改为R
@RestController//加载为bean
@RequestMapping("/books")
public class BookController {
//注入业务层
@Autowired
private IBookService iBookService;
@GetMapping//请求方式,get请求
public R getAll(){
return new R(true,iBookService.list());
}
@PostMapping
public R save(@RequestBody Book book){//请求体参数为book,接收json数据
return new R(iBookService.save(book));
}
@PutMapping//这里是put方式提交 属于Restful开发方式
public R update(@RequestBody Book book){
return new R(iBookService.updateById(book));
}
//路径:http://localhost/books/1
@DeleteMapping("{id}")//这里是delete方式提交 属于Restful开发方式
public R delete(@PathVariable Integer id){//从路径中获取id
return new R(iBookService.removeById(id));
}
@GetMapping("{id}")
public R getById(@PathVariable Integer id){
return new R(true,iBookService.getById(id));
}
@GetMapping("{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage, @PathVariable int pageSize){
return new R(true,iBookService.getPage(currentPage,pageSize));
}
}
在springboot项目中,前端的相关文件是放在resources目录下的static目录中的,里面放了这些东西,前端页面的编写在这里就不过多赘述。
我们先在钩子函数中(Vue初始化完成后自动执行)来调用getAll()函数,拿到全部的数据
//钩子函数,VUE对象初始化完成后自动执行
created() {
//初始化完成后 自动加载所有数据
this.getAll();
},
methods: {
//列表
getAll() {
axios.get("/books").then(res=>{
console.log(res.data)
});
},
axios用来发送请求.get代表发送的是get请求,然后通过then(res=>{})拿到后端发送过来的数据,在花括号内就可以对这个拿到的数据进行操作了。这里在控制台输出res.data,就可以看到拿过来的数据显示在了浏览器的控制台上。在前端开发时,其实每次在拿到数据后最好在控制台上输出一下,这样就能看到这个数据的结构,然后通过 . 来获取不同层级的数据
可以发现,flag和data都被拿了过来,我们是不是通过赋值,让Vue data中的dataList=res.data.data就可以将数据填到表格上了呢,我们来试一下。
重新运行一下:
填入数据成功。
首先,找到新建的标签,发现按钮中绑定了一个单击事件,也就是说,当点击按钮时,系统就会去执行按钮绑定的事件,也就是函数通过单击新建按钮,要让新建的窗口弹出来
我们只需要将这个值改为true,窗口就显现出来了,观察窗口的标签结构,确定按钮绑定了handleAdd的单击事件,因此我们只需要在handleAdd的方法中,通过请求将模型数据发送到服务器,就可以完成对数据的新建.
post指定了发送的路径和要发送的数据,然后在回调函数中,先判断操作是否成功,若成功,将窗口关掉,并给出用户提示,添加成功。如果没有成功呢,提示用户添加失败,而且数据也是要重新加载一下的,因此这里我们可以写一个finally,重新加载数据 。
数据新建成功
这里还有一个小细节,每次打开窗口的时候,为了避免显示上一次操作留下的数据,在打开窗口的时候要做一下表单的重置。
再将取消的功能完善一下
这样我们的增添数据的功能就算是完成了,其他的功能也几乎是一个思路,后面只写每个思路不同的地方,其他相同的思路这里不再赘述。
首先,在表单中,将本行的数据封装成了一个scope,然后将scope.row作为参数传到函数中,scope.row其实就是获取到了本行的数据
为了防止用户误操作,在点击删除时先弹出一个对话框,点击确认后执行then,点击取消时执行catch。还有一点注意的就是,这里采用delete的方式进行提交,因为用到的是Restful进行表现层接口开发,我们就要用delete的方式进行提交。而+则是用来拼接字符串的,用row.id就可以拿到当前行的id值。
在我们的系统中,为什么会存在删除失败的情况呢?如果我们的页面是有多人在使用,另一个人在操作时,在其他页面已经将这个数据删除掉了,但我们这里并没有刷新,在我们点击到删除后,数据库中已经不存在这个数据了所以才会删除失败。在修改时也会遇到这个问题,因此,后面程序的编写中也要注意到这些问题。
修改数据的基本思路就是,点击按钮时先发送get请求,回显数据,供用户修改,然后再将修改后的数据提交给服务器,便可完成修改的操作。
回显数据
当后台存在异常时,前端就会返回下面格式的数据,那么我们应该怎么去处理呢
在utils包下新建一个类
//作为springmvc的异常处理器
//@ControllerAdvice//将其定义为异常处理器
@RestControllerAdvice//二者皆可
public class ProjectExceptionAdvice {
//拦截所有的异常信息
@ExceptionHandler
public R doException(Exception exception){
//记录日志
//通知运维
//通知开发
//在控制台上输出
exception.printStackTrace();
return new R("服务器故障,稍后再试");
}
}
当有异常出现后,就会被抛到这个类中,在这里,我们希望通过R类,提供出一句话来进行提示,那么我们就要在R类中进行修改
@Data
public class R {
private Boolean flag;
private Object data;
private String msg;
public R(){
}
public R(boolean flag){
this.flag=flag;
}
public R(boolean flag,Object data){
this.flag=flag;
this.data=data;
}
public R(String msg){
this.flag=false;
this.msg=msg;
}
}
新定义一个msg用来传递字符串,在构造方法中,写一句this.flag=false。因为既然是抛出了异常才会调用这个构造方法,那么flag一定为false
我们来人为的制造一个异常,然后在postman中进行测试
这次返回的数据就变了。
那么在我们的前端代码中,就也要进行修改
改成这样即可
我们直接将查询所有的函数,改造为分页查询
先将路径改好,先不做赋值,重启,我们来观察一下路径和数据在不在
接下来我们就根据返回的数据来对页面中的变量进行赋值,通过参照page对象中提供的方法,就可以得知 . 后应该写哪个变量名,我们将这些变量一一赋值给vue中的变量,就可以完成基本的分页查询了
但这时currentPage和pageSize是定死的,我们现在不能切换页面。找到handleCurrentChange的函数,这个函数的参数就是选中的页码值,只要让参数等于当前页码值,就可以完成换页的操作
这时的分页查询还是有一个bug的,就是如果你删除完一页数据时,这一页还是存在的,不会减少一页。即使这一页没有任何数据,那么这个问题应该怎么解决呢?
出现bug的原因是什么呢?
从响应的数据中可以看出,我们此时还是查询的第三页的数据,但是此时的第三页是没有数据的,我们只需要在表现层添加一个判断即可
@GetMapping("{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage, @PathVariable int pageSize){
IPage page=iBookService.getPage(currentPage,pageSize);
//如果当前页码值大于总页码值,那么重新执行查询操作,使用最大页码值作为当前页码值
if (currentPage>page.getPages()){
page=iBookService.getPage((int)page.getPages(),pageSize);
}
return new R(true,page);
}
这样这个小bug就解决了。
条件查询时,条件是跟着分页查询走的,因此我们直接在pagination中,添加条件的数据即可
然后将数据模型绑定到组件上
接下来在getAll的函数中来写条件查询
首先就是要组织参数,并且完成url地址的拼接,这里的url拼接的方式很巧妙,是利用+=来进行每个参数段的拼接
接下来在get请求的路径中拼接上param,就可以完成参数的携带
我们来做一下测试,在条件查询的输入框中输入几个值,然后点击查询,看一下发送的请求中是否携带了参数
观察路径,这里是没有问题的。接下来就要对BookController类中的分页查询方法进行修改
@GetMapping("{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage, @PathVariable int pageSize,Book book){
//这里传过来的参数会被自动装配到book中
System.out.println("参数===>"+book);
IPage page=iBookService.getPage(currentPage,pageSize);
//如果当前页码值大于总页码值,那么重新执行查询操作,使用最大页码值作为当前页码值
if (currentPage>page.getPages()){
page=iBookService.getPage((int)page.getPages(),pageSize);
}
return new R(true,page);
}
这里传过来的参数会被自动装配到参数中的book对象中,输出在控制台上看一下
参数确实被封装到了book对象中。
接下来就要把book对象作为参数传递到业务层来完成条件查询的操作。
@GetMapping("{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage, @PathVariable int pageSize,Book book){
//这里传过来的参数会被自动装配到book中
//System.out.println("参数===>"+book);
IPage page=iBookService.getPage(currentPage,pageSize,book);
//如果当前页码值大于总页码值,那么重新执行查询操作,使用最大页码值作为当前页码值
if (currentPage>page.getPages()){
page=iBookService.getPage((int)page.getPages(),pageSize,book);
}
return new R(true,page);
}
然后我们要回到IBookService接口中,写一个带book参数的方法重载
public interface IBookService extends IService {
IPage getPage(int currentPage, int pageSize);
IPage getPage(int currentPage, int pageSize,Book book);
}
再将实现类编写一下
@Override
public IPage getPage(int currentPage, int pageSize, Book book) {
LambdaQueryWrapper lambdaQueryWrapper=new LambdaQueryWrapper();
lambdaQueryWrapper.like(Strings.isNotEmpty(book.getType()),Book::getType,book.getType());
lambdaQueryWrapper.like(Strings.isNotEmpty(book.getName()),Book::getName,book.getName());
lambdaQueryWrapper.like(Strings.isNotEmpty(book.getDescription()),Book::getDescription,book.getDescription());
IPage page=new Page(currentPage,pageSize);
bookDao.selectPage(page,lambdaQueryWrapper);
return page;
}
这里是通过lambaQueryWrapper来写条件查询,第一个参数为布尔类型,为true是连接条件,为false时则不连接条件。这里是判断字符串是否为空,来决定是否连接条件,第二个参数是字段名,而第三个参数则是条件值。
运行成功!
这样一来项目的工作就基本完成了
这是目录中用到的文件。跟Javaweb相比,springboot为我们大大减轻了开发时的工作量,方便了我们的开发,非常非常感谢springboot的开发团队!