SpringBoot基础篇(三)

三、基于 SpringBoot 实现 SSMP 整合

5、SSMP 整合综合案例

SpringBoot 能够整合的技术太多太多了,对于初学者来说慢慢来,一点点掌握。前面咱们做了 4 个整合了,下面就通过一个稍微综合一点的案例,将所有知识贯穿起来,同时做一个小功能,体会一下。不过有言在先,这个案例制作的时候,你可能会有这种感觉,说好的 SpringBoot 整合其他技术的案例,为什么感觉 SpringBoot 整合其他技术的身影不多呢?因为这东西书写太简单了,简单到瞬间写完,大量的时间做的不是这些整合工作。

先看一下这个案例的最终效果

主页面

image38

添加

image39

删除

image40

修改

image41

分页

image42

条件查询

image43

整体案例中需要采用的技术如下,先了解一下,做到哪一个说哪一个

  1. 实体类开发——使用 Lombok 快速制作实体类
  2. Dao 开发——整合 MyBatis-Plus,制作数据层测试
  3. Service 开发——基于 MyBatis-Plus 进行增量开发,制作业务层测试类
  4. Controller 开发——基于 Restful 开发,使用 PostMan 测试接口功能
  5. Controller 开发——前后端开发协议制作
  6. 页面开发——基于 Vue + ElementUI 制作,前后端联调,页面数据处理,页面消息处理
    • 列表
    • 新增
    • 修改
    • 删除
    • 分页
    • 查询
  7. 项目异常处理
  8. 按条件查询——页面功能调整、Controller 修正功能、Service 修正功能

可以看的出来,东西还是很多的,希望通过这个案例,各位小伙伴能够完成基础开发的技能训练。整体开发过程采用做一层测一层的形式进行,过程完整,战线较长,希望各位能跟紧进度,完成这个小案例的制作。

5.0、模块创建

对于这个案例如果按照企业开发的形式进行应该制作后台微服务,前后端分离的开发。

image44

我知道这个对初学的小伙伴要求太高了,咱们简化一下。后台做单体服务器,前端不使用前后端分离的制作了。

image45

一个服务器既充当后台服务调用,又负责前端页面展示,降低学习的门槛。

下面我们就可以创建一个新的模块,加载要使用的技术对应的 starter,修改配置文件格式为 yml 格式,并把 Web 访问端口先设置成 80。

pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

application.yml

server:
  port: 80

5.1、实体类开发

本案例对应的模块表结构如下:

-- ----------------------------
-- Table structure for tbl_book
-- ----------------------------
DROP TABLE IF EXISTS `tbl_book`;
CREATE TABLE `tbl_book` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `type` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 51 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of tbl_book
-- ----------------------------
INSERT INTO `tbl_book` VALUES(1, '计算机理论', 'Spring实战 第5版', 'Spring入门经典教程,深入理解Spring原理技术内幕');
INSERT INTO `tbl_book` VALUES(2, '计算机理论', 'Spring 5核心原理与30个类手写实战', '十年沉淀之作,手写Spring精华思想');
INSERT INTO `tbl_book` VALUES(3, '计算机理论', 'Spring 5 设计模式', '深入Spring源码剖析Spring源码中蕴含的10大设计模式');
INSERT INTO `tbl_book` VALUES(4, '计算机理论', 'Spring MVC+MyBatis开发从入门到项目实战', '全方位解析面向Web应用的轻量级框架,带你成为Spring MVC开发高手');
INSERT INTO `tbl_book` VALUES(5, '计算机理论', '轻量级Java Web企业应用实战', '源码级剖析Spring框架,适合已掌握Java基础的读者');
INSERT INTO `tbl_book` VALUES(6, '计算机理论', 'Java核心技术 卷I 基础知识(原书第11版)', 'Core Java 第11版,Jolt大奖获奖作品,针对Java SE9、10、11全面更新');
INSERT INTO `tbl_book` VALUES(7, '计算机理论', '深入理解Java虚拟机', '5个维度全面剖析JVM,大厂面试知识点全覆盖');
INSERT INTO `tbl_book` VALUES(8, '计算机理论', 'Java编程思想(第4版)', 'Java学习必读经典,殿堂级著作!赢得了全球程序员的广泛赞誉');
INSERT INTO `tbl_book` VALUES(9, '计算机理论', '零基础学Java(全彩版)', '零基础自学编程的入门图书,由浅入深,详解Java语言的编程思想和核心技术');
INSERT INTO `tbl_book` VALUES(10, '市场营销', '直播就该这么做:主播高效沟通实战指南', '李子柒、李佳琦、薇娅成长为网红的秘密都在书中');
INSERT INTO `tbl_book` VALUES(11, '市场营销', '直播销讲实战一本通', '和秋叶一起学系列网络营销书籍');
INSERT INTO `tbl_book` VALUES(12, '市场营销', '直播带货:淘宝、天猫直播从新手到高手', '一本教你如何玩转直播的书,10堂课轻松实现带货月入3W+');

根据上述表结构,制作对应的实体类

实体类

public class Book {
    private Integer id;
    private String type;
    private String name;
    private String description;
}

实体类的开发可以自动通过工具手工生成 getter / setter 方法,然后覆盖 toString() 方法,方便调试,等等。不过这一套操作书写很繁琐,有对应的工具可以帮助我们简化开发,介绍一个小工具 Lombok。

Lombok,一个 Java 类库,提供了一组注解,简化 POJO 实体类开发,SpringBoot 目前默认集成了 Lombok 技术,并提供了对应的版本控制,所以只需要提供对应的坐标即可,在 pom.xml 中添加 Lombok 的坐标。

<dependencies>
    <!--lombok-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

使用 Lombok 可以通过一个注解 @Data 完成一个实体类对应的 getter、setter、toString、equals、hashCode 等操作的快速添加,是在编译期设置这些方法

import lombok.Data;

// @Getter
// @Setter
// @NoArgsConstructor
// @AllArgsConstructor
@Data
public class Book {
    private Integer id;
    private String type;
    private String name;
    private String description;
}

到这里实体类就做好了,是不是使用 Lombok 简化了好多,这种工具在 Java 开发中还有很多,后面课程中遇到了能用的东西时,在不增加各位小伙伴大量的学习时间的情况下,尽量多给大家介绍一些

总结

  1. 实体类制作
  2. 使用 Lombok 简化开发
    • 导入 Lombok 无需指定版本,由 SpringBoot 提供版本
    • @Data 注解

5.2、数据层开发——基础 CRUD

MP 快速开发

数据层开发本次使用 MyBatis-Plus 技术,数据源使用前面学习的 Druid,学都学了都用上

步骤 ①:导入 MyBatis-Plus 与 Druid 对应的 starter,当然 MySQL 的驱动不能少

<dependencies>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.2</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.2.8</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

步骤 ②:配置数据库连接相关的数据源配置

server:
  port: 80

spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
      username: root
      password: root

步骤 ③:使用 MP 的标准通用接口 BaseMapper 加速开发,继承 BaseMapper 即可,别忘了 @Mapper 和泛型的指定

@Mapper
public interface BookDao extends BaseMapper<Book> {
}

步骤 ④:制作测试类测试结果,这个测试类制作是个好习惯,不过在企业开发中往往都为加速开发跳过此步,且行且珍惜吧

package com.itheima.dao;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.itheima.domain.Book;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class BookDaoTestCase {
    @Autowired
    private BookDao bookDao;

    @Test
    void testGetById() {
        System.out.println(bookDao.selectById(1));
    }

    @Test
    void testSave() {
        Book book = new Book();
        book.setType("测试数据123");
        book.setName("测试数据123");
        book.setDescription("测试数据123");
        bookDao.insert(book);
    }

    @Test
    void testUpdate() {
        Book book = new Book();
        book.setId(17);
        book.setType("测试数据abcdefg");
        book.setName("测试数据123");
        book.setDescription("测试数据123");
        bookDao.updateById(book);
    }

    @Test
    void testDelete() {
        bookDao.deleteById(16);
    }

    @Test
    void testGetAll() {
        bookDao.selectList(null);
    }
}

温馨提示

MP 技术默认的主键生成策略为雪花算法,生成的主键 ID 长度较大,和目前的数据库设定规则不相符,需要配置一下使 MP 使用数据库的主键生成策略,方式嘛还是老一套,做配置。在 application.yml 中添加对应配置即可,具体如下:

server:
  port: 80

spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
      username: root
      password: root

mybatis-plus:
  global-config:
    db-config:
      table-prefix: tbl_ # 设置表名通用前缀
      id-type: auto      # 设置主键id字段的生成策略为参照数据库设定的策略,当前数据库设置id生成策略为自增

查看 MP 运行日志

在进行数据层测试的时候,因为基础的 CRUD 操作均由 MP 给我们提供了,所以就出现了一个局面,开发者不需要书写 SQL 语句了,这样程序运行的时候总有一种感觉,一切的一切都是黑盒的,作为开发者我们啥也不知道就完了。如果程序正常运行还好,如果报错了,这个时候就很崩溃,你甚至都不知道从何下手,因为传递参数、封装 SQL 语句这些操作完全不是你干预开发出来的,所以查看执行期运行的 SQL 语句就成为当务之急。

SpringBoot 整合 MP 的时候充分考虑到了这点,通过配置的形式就可以查阅执行期 SQL 语句,配置如下:

mybatis-plus:
  global-config:
    db-config:
      table-prefix: tbl_
      id-type: auto
  configuration:
    # 为方便调试开启MyBatis-Plus的日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

再来看运行结果,此时就显示了运行期执行 SQL 的情况。

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c9a6717] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6ca30b8a] will not be managed by Spring
==>  Preparing: SELECT id,type,name,description FROM tbl_book
==> Parameters: 
<==    Columns: id, type, name, description
<==        Row: 1, 计算机理论, Spring实战 第5版, Spring入门经典教程,深入理解Spring原理技术内幕
<==        Row: 2, 计算机理论, Spring 5核心原理与30个类手写实战, 十年沉淀之作,手写Spring精华思想
<==        Row: 3, 计算机理论, Spring 5 设计模式, 深入Spring源码剖析Spring源码中蕴含的10大设计模式
<==        Row: 4, 计算机理论, Spring MVC+MyBatis开发从入门到项目实战, 全方位解析面向Web应用的轻量级框架,带你成为Spring MVC开发高手
<==        Row: 5, 计算机理论, 轻量级Java Web企业应用实战, 源码级剖析Spring框架,适合已掌握Java基础的读者
<==        Row: 6, 计算机理论, Java核心技术 卷I 基础知识(原书第11版), Core Java 第11版,Jolt大奖获奖作品,针对Java SE9、10、11全面更新
<==        Row: 7, 计算机理论, 深入理解Java虚拟机, 5个维度全面剖析JVM,大厂面试知识点全覆盖
<==        Row: 8, 计算机理论, Java编程思想(第4版), Java学习必读经典,殿堂级著作!赢得了全球程序员的广泛赞誉
<==        Row: 9, 计算机理论, 零基础学Java(全彩版), 零基础自学编程的入门图书,由浅入深,详解Java语言的编程思想和核心技术
<==        Row: 10, 市场营销, 直播就该这么做:主播高效沟通实战指南, 李子柒、李佳琦、薇娅成长为网红的秘密都在书中
<==        Row: 11, 市场营销, 直播销讲实战一本通, 和秋叶一起学系列网络营销书籍
<==        Row: 12, 市场营销, 直播带货:淘宝、天猫直播从新手到高手, 一本教你如何玩转直播的书,10堂课轻松实现带货月入3W+
<==        Row: 13, 测试类型, 测试数据, 测试描述数据
<==        Row: 14, 测试数据update, 测试数据update, 测试数据update
<==        Row: 15, 测试数据123, 测试数据123, 测试数据123
<==      Total: 15

其中清晰的标注了当前执行的 SQL 语句是什么,携带了什么参数,对应的执行结果是什么,所有信息应有尽有。

此处设置的是日志的显示形式,当前配置的是控制台输出,当然还可以由更多的选择,根据需求切换即可

image46

总结

  1. 手工导入 starter 坐标(2 个),MySQL 驱动(1 个)

  2. 配置数据源与 MyBatis-Plus 对应的配置

  3. 开发 Dao 接口(继承 BaseMapper)

  4. 制作测试类测试 Dao 功能是否有效

  5. 使用配置方式开启日志,设置日志输出方式为标准输出即可查阅 SQL 执行日志

5.3、数据层开发——分页功能制作

前面仅仅是使用了 MP 提供的基础 CRUD 功能,实际上 MP 给我们提供了几乎所有的基础操作,这一节说一下如何实现数据库端的分页操作

分页操作需要设定分页对象 IPage,MP 提供的分页操作 API 如下:

@Test
void testGetPage() {
    IPage page = new Page(2, 5);
    bookDao.selectPage(page, null);
    System.out.println(page.getCurrent());
    System.out.println(page.getSize());
    System.out.println(page.getTotal());
    System.out.println(page.getPages());
    System.out.println(page.getRecords());
}

其中 selectPage 方法需要传入一个封装分页数据的对象,可以通过 new 的形式创建这个对象,当然这个对象也是 MP 提供的,别选错包了。创建此对象时就需要指定分页的两个基本数据

  • 当前显示第几页
  • 每页显示几条数据

可以通过创建 Page 对象时利用构造方法初始化这两个数据

IPage page = new Page(2, 5);

将该对象传入到查询方法 selectPage 后,可以得到查询结果,但是我们会发现当前操作查询结果返回值仍然是一个 IPage 对象,这又是怎么回事?

IPage page = bookDao.selectPage(page, null);

原来这个 IPage 对象中封装了若干个数据,而查询的结果作为 IPage 对象封装的一个数据存在的,可以理解为查询结果得到后,又塞到了这个 IPage 对象中,其实还是为了高度的封装,一个 IPage 描述了分页所有的信息。下面 5 个操作就是 IPage 对象中封装的所有信息了

@Test
void testGetPage() {
    IPage page = new Page(2, 5);
    bookDao.selectPage(page, null);
    System.out.println(page.getCurrent());      // 当前页码值
    System.out.println(page.getSize());         // 每页显示数
    System.out.println(page.getTotal());        // 数据总量
    System.out.println(page.getPages());        // 总页数
    System.out.println(page.getRecords());      // 详细数据
}

到这里就知道这些数据如何获取了,但是当你去执行这个操作时,你会发现并不像我们分析的这样,实际上这个分页当前是无效的。为什么这样呢?这个要源于 MP 的内部机制。

对于 MySQL 的分页操作使用 limit 关键字进行,而并不是所有的数据库都使用 limit 关键字实现的,这个时候 MP 为了制作的兼容性强,将分页操作设置为基础查询操作的升级版,你可以理解为 IPhone6 与 IPhone6S-Plus 的关系。

分页操作是在 MyBatis-Plus 的常规操作基础上增强得到,内部是动态的拼写 SQL 语句,因此需要增强对应的功能,使用 MyBatis-Plus 拦截器实现。

基础操作中有查询全部的功能,而在这个基础上只需要升级一下(PLUS)就可以得到分页操作。所以 MP 将分页操作做成了一个开关,你用分页功能就把开关开启,不用就不需要开启这个开关。而我们现在没有开启这个开关,所以分页操作是没有的。这个开关是通过 MP 的拦截器的形式存在的,其中的原理这里不分析了,有兴趣的小伙伴可以学习 MyBatis-Plus 这门课程进行详细解读。具体设置方式如下:

定义 MP 拦截器并将其设置为 Spring 管控的 Bean

@Configuration
public class MPConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        // 1.定义MP拦截器
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 2.添加具体的拦截器
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }
}

上述代码第一行是创建 MP 的拦截器栈,这个时候拦截器栈中没有具体的拦截器,第二行是初始化了分页拦截器,并添加到拦截器栈中。如果后期开发其他功能,需要添加全新的拦截器,按照第二行的格式继续 add 进去新的拦截器就可以了。

总结

  1. 使用 IPage 封装分页数据
  2. 分页操作依赖 MyBatis-Plus 分页拦截器实现功能
  3. 借助 MyBatis-Plus 日志查阅执行 SQL 语句

5.4、数据层开发——条件查询功能制作

除了分页功能,MP 还提供有强大的条件查询功能。以往我们写条件查询要自己动态拼写复杂的 SQL 语句,现在简单了,MP 将这些操作都制作成 API 接口,调用一个又一个的方法就可以实现各种套件的拼装。这里给大家普及一下基本格式,详细的操作还是到 MP 的课程中查阅吧

下面的操作就是执行一个模糊匹配对应的操作,由 like 条件书写变为了 like 方法的调用

@Test
void testGetByWrapper() {
    QueryWrapper<Book> qw = new QueryWrapper<>();
    qw.like("name", "Spring");
    bookDao.selectList(qw);
}

其中第一句 QueryWrapper 对象是一个用于封装查询条件的对象,该对象可以动态使用 API 调用的方法添加条件,最终转化成对应的 SQL 语句。第二句就是一个条件了,需要什么条件,使用 QueryWapper 对象直接调用对应操作即可。比如做大于小于关系,就可以使用 ltgt 方法,等于使用 eq 方法,等等,此处不做更多的解释了。

这组 API 使用还是比较简单的,但是关于属性字段名的书写存在着安全隐患,比如查询字段 name,当前是以字符串的形态书写的,万一写错,编译器还没有办法发现,只能将问题抛到运行器通过异常堆栈告诉开发者,不太友好。

MP 针对字段检查进行了功能升级,全面支持 Lambda 表达式,就有了下面这组 API。由 QueryWrapper 对象升级为 LambdaQueryWrapper 对象,这下就避免了上述问题的出现

@Test
void testGetByWrapper2() {
    String name = "Spring";
    LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper<>();
    lqw.like(Book::getName, name);
    bookDao.selectList(lqw);
}

为了便于开发者动态拼写 SQL,防止将 null 数据作为条件使用,MP 还提供了动态拼装 SQL 的快捷书写方式

@Test
void testGetByWrapper2() {
    String name = "Spring";

    LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper<Book>();
    // 方式一:Java代码控制
    // if (name != null) lqw.like(Book::getName,name);
    // 方式二:API接口提供控制开关
    lqw.like(name != null, Book::getName, name);
    // lqw.like(Strings.isNotEmpty(name), Book::getName, "Spring");
    bookDao.selectList(lqw);
}

其实就是个格式,没有区别。关于 MP 的基础操作就说到这里吧,如果这一块知识不太熟悉的小伙伴还是去完整的学习一下 MP 的知识吧,这里只是蜻蜓点水的用了几个操作而已。

总结

  1. 使用 QueryWrapper 对象封装查询条件

  2. 推荐使用 LambdaQueryWrapper 对象

  3. 所有查询操作封装成方法调用

  4. 查询条件支持动态条件拼装

5.5、业务层开发

基础开发

数据层开发告一段落,下面进行业务层开发,其实标准业务层开发很多初学者认为就是调用数据层,怎么说呢?这个理解是没有大问题的,更精准的说法应该是组织业务逻辑功能,并根据业务需求,对数据持久层发起调用。有什么差别呢?目标是为了组织出符合需求的业务逻辑功能,至于调不调用数据层还真不好说,有需求就调用,没有需求就不调用。

Service 层接口定义与数据层接口定义具有较大区别,不要混用。一个常识性的知识普及一下,业务层的方法名定义一定要与业务有关,例如登录操作

login(String username, String password);

而数据层的方法名定义一定与业务无关,是一定,不是可能,也不是有可能,例如根据用户名密码查询

selectByUserNameAndPassword(String username, String password);

我们在开发的时候是可以根据完成的工作不同划分成不同职能的开发团队的。比如一个哥们制作数据层,他就可以不知道业务是什么样子,拿到的需求文档要求可能是这样的

接口:传入用户名与密码字段,查询出对应结果,结果是单条数据
接口:传入ID字段,查询出对应结果,结果是单条数据
接口:传入离职字段,查询出对应结果,结果是多条数据

但是进行业务功能开发的哥们,拿到的需求文档要求差别就很大

接口:传入用户名与密码字段,对用户名字段做长度校验,4-15位,对密码字段做长度校验,8到24位,
对xxx字段做特殊字符校验,不允许存在空格,查询结果为对象。
如果为null,返回BusinessException,封装消息码INFO_LOGON_USERNAME_PASSWORD_ERROR

你比较一下,能是一回事吗?差别太大了,所以说业务层方法定义与数据层方法定义差异化很大,只不过有些入门级的开发者手懒或者没有使用过公司相关的 ISO 标准化文档而已。

多余的话不说了,咱们做案例就简单制作了,业务层接口定义如下:

public interface BookService {
    Boolean save(Book book);
    Boolean update(Book book);
    Boolean delete(Integer id);
    Book getById(Integer id);
    List<Book> getAll();
    IPage<Book> getPage(int currentPage, int pageSize);
}

业务层实现类如下,转调数据层即可

@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;

    @Override
    public Boolean save(Book book) {
        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<Book> getAll() {
        return bookDao.selectList(null);
    }

    @Override
    public IPage<Book> getPage(int currentPage, int pageSize) {
        IPage page = new Page(currentPage, pageSize);
        bookDao.selectPage(page, null);
        return page;
    }
}

别忘了对业务层接口进行测试,测试类如下

@SpringBootTest
public class BookServiceTest {
    @Autowired
    private BookService bookService;

    @Test
    void testGetById() {
        System.out.println(bookService.getById(4));
    }

    @Test
    void testSave() {
        Book book = new Book();
        book.setType("测试数据123");
        book.setName("测试数据123");
        book.setDescription("测试数据123");
        bookService.save(book);
    }

    @Test
    void testUpdate() {
        Book book = new Book();
        book.setId(17);
        book.setType("测试数据abcd");
        book.setName("测试数据123");
        book.setDescription("测试数据123");
        bookService.updateById(book);
    }

    @Test
    void testDelete() {
        bookService.delete(52);
    }

    @Test
    void testGetAll() {
        List<Book> list = bookService.getAll();
        for (Book book : list) {
            System.out.println(book);
        }
    }

    @Test
    void testGetPage() {
        IPage<Book> page = bookService.getPage(2, 5);
        System.out.println(page.getCurrent());
        System.out.println(page.getSize());
        System.out.println(page.getTotal());
        System.out.println(page.getPages());
        System.out.println(page.getRecords());
    }
}

总结

  1. Service 接口名称定义成业务名称,并与 Dao 接口名称进行区分
  2. 制作测试类测试 Service 功能是否有效

业务层快速开发

其实 MP 技术不仅提供了数据层快速开发方案,业务层 MP 也给了一个通用接口 ISerivce<T> 和 通用实现类 ServiceImpl<M, T>,个人观点不推荐使用,凑合能用吧,其实就是一个封装 + 继承的思想,代码给出,实际开发慎用

image47

业务层接口快速开发

public interface IBookService extends IService<Book> {
    // 添加非通用操作API接口
    // 追加的操作与原始操作通过名称区分,功能类似
    /*Boolean delete(Integer id); // 原操作removeById
    Boolean insert(Book book); // 原操作save
    Boolean modify(Book book); // 原操作updateById
    Book get(Integer id);*/
}

业务层接口实现类快速开发,关注继承的类需要传入两个泛型,一个是数据层接口,另一个是实体类

@Service
public class IBookServiceImpl extends ServiceImpl<BookDao, Book> implements IBookService {
}

如果感觉 MP 提供的功能不足以支撑你的使用需要(其实是一定不能支撑的,因为需求不可能是通用的),在原始接口基础上接着定义新的 API 接口、在通用类基础上做功能重载或功能追加就行了,此处不再说太多了,就是自定义自己的操作了。注意重载时不要覆盖原始操作,避免原始提供的功能丢失,即不要和已有的 API 接口名冲突即可。

@Service
public class IBookServiceImpl extends ServiceImpl<BookDao, Book> implements IBookService {
    @Autowired
    private BookDao bookDao;

    // 添加非通用操作API
    // 实现类追加功能
    public Boolean insert(Book book) {
        return bookDao.insert(book) > 0;
    }

    public Boolean modify(Book book) {
        return bookDao.updateById(book) > 0;
    }

    public Boolean delete(Integer id) {
        return bookDao.deleteById(id) > 0;
    }

    public Book get(Integer id) {
        return bookDao.selectById(id);
    }
}

测试类定义:

@SpringBootTest
public class IBookServiceTestCase {
    @Autowired
    private IBookService bookService;

    @Test
    void testGetById() {
        System.out.println(bookService.getById(4));
    }

    @Test
    void testSave() {
        Book book = new Book();
        book.setType("测试数据123");
        book.setName("测试数据123");
        book.setDescription("测试数据123");
        bookService.save(book);
    }

    @Test
    void testUpdate() {
        Book book = new Book();
        book.setId(53);
        book.setType("测试数据abcd");
        book.setName("测试数据123");
        book.setDescription("测试数据123");
        bookService.updateById(book);
    }

    @Test
    void testDelete() {
        bookService.removeById(53);
    }

    @Test
    void testGetAll() {
        List<Book> list = bookService.list();
        list.forEach(System.out::println);
    }

    @Test
    void testGetPage() {
        IPage<Book> page = new Page<>(2, 5);
        bookService.page(page);
        System.out.println(page.getCurrent());
        System.out.println(page.getSize());
        System.out.println(page.getTotal());
        System.out.println(page.getPages());
        System.out.println(page.getRecords());
    }
}

总结

  1. 使用通用接口(ISerivce<T>)快速开发 Service
  2. 使用通用实现类(ServiceImpl<M, T>)快速开发 ServiceImpl
  3. 可以在通用接口基础上做功能重载或功能追加
  4. 注意重载时不要覆盖原始操作,避免原始提供的功能丢失

5.6、表现层开发

终于做到表现层了,做了这么多都是基础工作。其实你现在回头看看,哪里还有什么 SpringBoot 的影子?前面 1、2 步就搞完了。继续完成表现层制作吧,咱们表现层的开发使用基于 Restful 的表现层接口开发,功能测试通过 Postman 工具进行

表现层接口如下:

@RestController
@RequestMapping("/books")
public class BookController {
    @Autowired
    private IBookService bookService;

    @GetMapping
    public List<Book> getAll() {
        return bookService.list();
    }

    @PostMapping
    public Boolean save(@RequestBody Book book) {
        return bookService.save(book);
    }

    @PutMapping
    public Boolean update(@RequestBody Book book) {
        return bookService.updateById(book);
    }

    @DeleteMapping("{id}")
    public Boolean delete(@PathVariable Integer id) {
        return bookService.removeById(id);
    }

    @GetMapping("{id}")
    public Book getById(@PathVariable Integer id) {
        return bookService.getById(id);
    }

    @GetMapping("{currentPage}/{pageSize}")
    public IPage<Book> getPage(@PathVariable int currentPage, @PathVariable int pageSize) {
        IPage<Book> page = new Page<>(currentPage, pageSize);
        bookService.page(page);
        return page;
    }
}

在使用 Postman 测试时关注提交类型,对应上即可,不然就会报 405 的错误码了

1、普通 GET 请求,查询所有数据

image48

2、POST 请求传递 JSON 数据,后台使用 @RequestBody 接收数据,增加数据

image49

3、PUT 请求传递 JSON 数据,后台使用 @RequestBody 接收数据,修改数据

image50

4、DELETE 请求传递路径变量,后台使用 @PathVariable 接收数据,删除数据

image51

5、GET 请求传递路径变量,后台使用 @PathVariable 接收数据,查询单条数据

image52

6、GET 请求传递路径变量,后台使用 @PathVariable 接收数据,分页查询

image53


image54

总结

  1. 基于 Restful 制作表现层接口
    • 新增:POST
    • 删除:DELETE
    • 修改:PUT
    • 查询:GET
  2. 接收参数
    • 实体数据:@RequestBody
    • 路径变量:@PathVariable

5.7、表现层消息一致性处理

目前我们通过 Postman 测试后业务层接口功能是通的,但是这样的结果给到前端开发者会出现一个小问题。不同的操作结果所展示的数据格式差异化严重

增删改操作结果

true

查询单个数据操作结果

{
    "id": 1,
    "type": "计算机理论",
    "name": "Spring实战 第5版",
    "description": "Spring入门经典教程"
}

查询全部数据操作结果

[
    {
        "id": 1,
        "type": "计算机理论",
        "name": "Spring实战 第5版",
        "description": "Spring入门经典教程"
    },
    {
        "id": 2,
        "type": "计算机理论",
        "name": "Spring 5核心原理与30个类手写实战",
        "description": "十年沉淀之作"
    }
]

每种不同操作返回的数据格式都不一样,而且还不知道以后还会有什么格式,这样的结果让前端人员看了是很容易让人崩溃的,必须将所有操作的操作结果数据格式统一起来,需要设计表现层返回结果的模型类,用于后端与前端进行数据格式统一,也称为前后端数据协议

@Data
public class R {
    private Boolean flag;
    private Object data;

    public R() {
    }

    public R(Boolean flag) {
        this.flag = flag;
    }

    public R(Boolean flag, Object data) {
        this.flag = flag;
        this.data = data;
    }
}

其中 flag 用于标识操作是否成功,data 用于封装操作数据,现在的数据格式就变了

{
    "flag": true,
    "data": {
        "id": 1,
        "type": "计算机理论",
        "name": "Spring实战 第5版",
        "description": "Spring入门经典教程"
    }
}

表现层开发格式也需要转换一下,表现层接口统一返回值类型结果

@RestController
@RequestMapping("/books")
public class BookController {
    @Autowired
    private IBookService bookService;

    @GetMapping
    public R getAll() {
        return new R(true, bookService.list());
    }

    @PostMapping
    public R save(@RequestBody Book book) {
        /*R r = new R();
        boolean flag = bookService.save(book);
        r.setFlag(flag);*/
        return new R(bookService.save(book));
    }

    @PutMapping
    public R update(@RequestBody Book book) {
        return new R(bookService.updateById(book));
    }

    @DeleteMapping("{id}")
    public R delete(@PathVariable Integer id) {
        return new R(bookService.removeById(id));
    }

    @GetMapping("{id}")
    public R getById(@PathVariable Integer id) {
        return new R(true, bookService.getById(id));
    }

    @GetMapping("{currentPage}/{pageSize}")
    public R getPage(@PathVariable int currentPage, @PathVariable int pageSize) {
        IPage<Book> page = new Page<>(currentPage, pageSize);
        return new R(true, bookService.page(page));
    }
}

经过这么一折腾,全格式统一,现在后端发送给前端的数据格式就统一了,免去了不少前端解析数据的麻烦。

总结

  1. 设计统一的返回值结果类型便于前端开发读取数据

  2. 返回值结果类型可以根据需求自行设定,没有固定格式

  3. 返回值结果模型类用于后端与前端进行数据格式统一,也称为前后端数据协议

5.8、前后端联通性测试

后端的表现层接口开发完毕,就可以进行前端的开发了。

前后端分离结构设计中页面归属前端服务器

单体工程中将前端人员开发的页面保存到 resources 目录下的 static 目录中,建议执行 Maven 的 clean 生命周期,避免缓存的问题出现。

image55

在进行具体的功能开发之前,先做联通性的测试,通过前端页面发送异步提交(axios),调用后端接口,这一步调试通过后再进行进一步的功能开发

<!DOCTYPE html>
<html>
<head>
    <!-- 页面meta -->
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>基于SpringBoot整合SSM案例</title>
    <meta content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" name="viewport">

    <!-- 引入样式 -->
    <link rel="stylesheet" href="../plugins/elementui/index.css">
    <link rel="stylesheet" href="../plugins/font-awesome/css/font-awesome.min.css">
    <link rel="stylesheet" href="../css/style.css">
</head>

<body class="hold-transition">
<div id="app">
    <div class="content-header">
        <h1>图书管理</h1>
    </div>

    <div class="app-container">
        <div class="box">
            <div class="filter-container">
                <el-input placeholder="图书类别" style="width: 200px;" class="filter-item"></el-input>
                <el-input placeholder="图书名称" style="width: 200px;" class="filter-item"></el-input>
                <el-input placeholder="图书描述" style="width: 200px;" class="filter-item"></el-input>
                <el-button @click="getAll()" class="dalfBut">查询</el-button>
                <el-button type="primary" class="butT" @click="handleCreate()">新建</el-button>
            </div>

            <el-table size="small" current-row-key="id" :data="dataList" stripe highlight-current-row>
                <el-table-column type="index" align="center" label="序号"></el-table-column>
                <el-table-column prop="type" label="图书类别" align="center"></el-table-column>
                <el-table-column prop="name" label="图书名称" align="center"></el-table-column>
                <el-table-column prop="description" label="描述" align="center"></el-table-column>
                <el-table-column label="操作" align="center">
                    <template slot-scope="scope">
                        <el-button type="primary" size="mini" @click="handleUpdate(scope.row)">编辑</el-button>
                        <el-button type="danger" size="mini" @click="handleDelete(scope.row)">删除</el-button>
                    </template>
                </el-table-column>
            </el-table>

            <!--分页组件-->
            <div class="pagination-container">
                <el-pagination
                        class="pagiantion"
                        @current-change="handleCurrentChange"
                        :current-page="pagination.currentPage"
                        :page-size="pagination.pageSize"
                        layout="total, prev, pager, next, jumper"
                        :total="pagination.total">
                </el-pagination>
            </div>

            <!-- 新增标签弹层 -->
            <div class="add-form">
                <el-dialog title="新增图书" :visible.sync="dialogFormVisible">
                    <el-form ref="dataAddForm" :model="formData" :rules="rules" label-position="right" label-width="100px">
                        <el-row>
                            <el-col :span="12">
                                <el-form-item label="图书类别" prop="type">
                                    <el-input v-model="formData.type"/>
                                </el-form-item>
                            </el-col>

                            <el-col :span="12">
                                <el-form-item label="图书名称" prop="name">
                                    <el-input v-model="formData.name"/>
                                </el-form-item>
                            </el-col>
                        </el-row>

                        <el-row>
                            <el-col :span="24">
                                <el-form-item label="描述">
                                    <el-input v-model="formData.description" type="textarea"></el-input>
                                </el-form-item>
                            </el-col>
                        </el-row>
                    </el-form>

                    <div slot="footer" class="dialog-footer">
                        <el-button @click="cancel()">取消</el-button>
                        <el-button type="primary" @click="handleAdd()">确定</el-button>
                    </div>
                </el-dialog>
            </div>

            <!-- 编辑标签弹层 -->
            <div class="add-form">
                <el-dialog title="编辑检查项" :visible.sync="dialogFormVisible4Edit">
                    <el-form ref="dataEditForm" :model="formData" :rules="rules" label-position="right" label-width="100px">
                        <el-row>
                            <el-col :span="12">
                                <el-form-item label="图书类别" prop="type">
                                    <el-input v-model="formData.type"/>
                                </el-form-item>
                            </el-col>
                            <el-col :span="12">
                                <el-form-item label="图书名称" prop="name">
                                    <el-input v-model="formData.name"/>
                                </el-form-item>
                            </el-col>
                        </el-row>
                        <el-row>
                            <el-col :span="24">
                                <el-form-item label="描述">
                                    <el-input v-model="formData.description" type="textarea"></el-input>
                                </el-form-item>
                            </el-col>
                        </el-row>
                    </el-form>
                    <div slot="footer" class="dialog-footer">
                        <el-button @click="cancel()">取消</el-button>
                        <el-button type="primary" @click="handleEdit()">确定</el-button>
                    </div>
                </el-dialog>
            </div>
        </div>
    </div>
</div>
</body>

<!-- 引入组件库 -->
<script src="../js/vue.js"></script>
<script src="../plugins/elementui/index.js"></script>
<script type="text/javascript" src="../js/jquery.min.js"></script>
<script src="../js/axios-0.18.0.js"></script>

<script>
    var vue = new Vue({
        el: '#app',
        data: {
            dataList: [], // 当前页要展示的列表数据
            dialogFormVisible: false, // 添加表单是否可见
            dialogFormVisible4Edit: false, // 编辑表单是否可见
            formData: {}, // 表单数据
            rules: { // 校验规则
                type: [{required: true, message: '图书类别为必填项', trigger: 'blur' }],
                name: [{required: true, message: '图书名称为必填项', trigger: 'blur' }]
            },
            pagination: { // 分页相关模型数据
                currentPage: 1, // 当前页码
                pageSize: 10, // 每页显示的记录数
                total: 0 // 总记录数
            }
        },

        // 钩子函数,Vue对象初始化完成后自动执行
        created() {
            // 调用查询全部数据的操作
            this.getAll();
        },

        methods: {
            // 列表
            getAll() {
                // 发送异步请求
                axios.get("/books").then((res) => {
                    console.log(res.data);
                });
            } 
        }
    })
</script>
</html>

getAll() 方法中发送异步提交:

// 列表
getAll() {
    axios.get("/books").then((res) => {
        console.log(res.data);
    });
},

只要后台代码能够正常工作,前端能够在日志中接收到数据,就证明前后端是通的,也就可以进行下一步的功能开发了

总结

  1. 单体项目中页面放置在 resources/static 目录下
  2. created 钩子函数用于初始化页面时发起调用
  3. 页面使用 axios 发送异步请求获取数据后确认前后端是否联通

5.9、页面基础功能开发

① 列表功能(非分页版)

列表功能主要操作就是加载完数据,将数据展示到页面上,此处要利用 Vue 的数据模型绑定,发送请求得到数据,然后页面上读取指定数据即可

页面数据模型定义

data: {
    dataList: [], // 当前页要展示的列表数据
    ...
},

异步请求获取数据

// 列表
getAll() {
    // 发送异步请求
    axios.get("/books").then((res) => {
        this.dataList = res.data.data;
    });
},

这样在页面加载时就可以获取到数据,并且由 Vue 将数据展示到页面上了

总结:

  1. 将查询数据返回到页面,利用前端数据绑定进行数据展示

② 添加功能

添加功能用于收集数据的表单是通过一个弹窗展示的,因此在添加操作前首先要进行弹窗的展示,添加后隐藏弹窗即可。因为这个弹窗一直存在,因此当页面加载时首先设置这个弹窗为不可显示状态,需要展示,切换状态即可

默认状态

data: {
    dialogFormVisible: false, // 添加表单是否可见
    ...
},

切换为显示状态

// 弹出添加窗口
handleCreate() {
    this.dialogFormVisible = true;
},

由于每次添加数据都是使用同一个弹窗录入数据,所以每次操作的痕迹将在下一次操作时展示出来,需要在每次操作之前清理掉上次操作的痕迹

定义清理数据操作

// 重置表单
resetForm() {
    this.formData = {};
},

切换弹窗状态时清理数据

// 弹出添加窗口
handleCreate() {
    this.dialogFormVisible = true;
    this.resetForm();
},

至此准备工作完成,下面就要调用后台完成添加操作了

添加操作

  1. 将要保存的数据传递到后台,通过 post 请求的第二个参数传递 JSON 数据到后台
  2. 根据返回的操作结果决定下一步操作
    • 如何是 true 就关闭添加窗口,显示添加成功的消息
    • 如果是 false 保留添加窗口,显示添加失败的消息
  3. 无论添加是否成功,页面均进行刷新,动态加载数据(对 getAll 操作发起调用)
// 添加
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();
    });
},

取消添加操作

// 取消
cancel() {
    this.dialogFormVisible = false;
    this.$message.info("操作取消");
},

总结

  1. 请求方式使用 POST 调用后台对应操作
  2. 添加操作结束后动态刷新页面加载数据
  3. 根据操作结果不同,显示对应的提示信息
  4. 弹出添加 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("取消删除操作");
    });
},

总结

  1. 请求方式使用 Delete 调用后台对应操作
  2. 删除操作需要传递当前行数据对应的 id 值到后台
  3. 删除操作结束后动态刷新页面加载数据
  4. 根据操作结果不同,显示对应的提示信息
  5. 删除操作前弹出提示框避免误操作

④ 修改功能

修改功能可以说是列表功能、删除功能与添加功能的合体。几个相似点如下:

  1. 页面也需要有一个弹窗用来加载修改的数据,这一点与添加相同,都是要弹窗

  2. 弹出窗口中要加载待修改的数据,而数据需要通过查询得到,这一点与查询全部相同,都是要查数据

  3. 查询操作需要将要修改的数据 id 发送到后台,这一点与删除相同,都是传递 id 到后台

  4. 查询得到数据后需要展示到弹窗中,这一点与查询全部相同,都是要通过数据模型绑定展示数据

  5. 修改数据时需要将被修改的数据传递到后台,这一点与添加相同,都是要传递数据

所以整体上来看,修改功能就是前面几个功能的大合体

查询并展示数据

// 弹出编辑窗口
handleUpdate(row) {
    axios.get("/books/" + row.id).then((res) => {
        if (res.data.flag && res.data.data != null) {
            // 展示弹层,加载数据
            this.dialogFormVisible4Edit = true;
            this.formData = res.data.data;
        } else {
            this.$message.error("数据同步失败,自动刷新");
        }
    }).finally(() => {
        // 重新加载数据
        this.getAll();
    });
},

删除消息维护

// 删除
handleDelete(row) {
    // console.log(row);
    this.confirm("此操作将永久删除当前信息,是否继续?", "提示", {type: "info"}).then(() => {
        // console.log("success");
        axios.delete("/books/" + row.id).then((res) => {
            if (res.data.flag) {
                this.message.success("删除成功");
            } else {
                // this.message.error("删除失败");
                this.message.error("数据同步失败,自动刷新");
            }
        }).finally(() => {
            // 重新加载数据
            this.getAll();
        });
    }).catch(() => {
        // console.log("fail");
        this.$message.info("取消操作");
    });
},

修改操作

// 修改
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();
    });
},

取消添加和修改

// 取消
cancel() {
    this.dialogFormVisible = false;
    this.dialogFormVisible4Edit = false;
    this.$message.info("当前操作取消");
},

总结

  1. 加载要修改数据通过传递当前行数据对应的 id 值到后台查询数据(同删除与查询全部)
  2. 利用前端双向数据绑定将查询到的数据进行回显(同查询全部)
  3. 请求方式使用 PUT 调用后台对应操作(同新增传递数据)
  4. 修改操作结束后动态刷新页面加载数据(同新增)
  5. 根据操作结果不同,显示对应的提示信息(同新增)

5.10、业务消息一致性处理

目前的功能制作基本上达成了正常使用的情况,什么叫正常使用呢?也就是这个程序不出 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; // 用于封装消息

    // 构造方法根据实际需要编写
    public R(Boolean flag, Object data, String msg) {
        this.flag = flag;
        this.data = data;
        this.msg = msg;
    }

    public R(Boolean flag, String msg) {
        this.flag = flag;
        this.msg = msg;
    }

    public R(String msg) {
        this.flag = false;
        this.msg = msg;
    }
}

后台代码也要根据情况做处理,可以在表现层 Controller 中进行消息统一处理,当前是模拟的错误

目的:国际化

@PostMapping
public R save(@RequestBody Book book) throws IOException {
    // return new R(bookService.save(book));
    Boolean flag = bookService.save(book);
    return new R(flag, flag ? "添加成功^_^" : "添加失败-_-!");
}

然后在表现层做统一的异常处理,出现异常后返回指定信息。使用 SpringMVC 提供的异常处理器做统一的异常处理,新建一个类作为异常处理器

// @ControllerAdvice
@RestControllerAdvice
public class ProjectExceptionAdvice {
    // 拦截所有异常信息
    // @ExceptionHandler
    // 拦截某个具体异常
    @ExceptionHandler(Exception.class)
    public R doOtherException(Exception ex) {
        // 记录日志
        // 发送消息给运维
        // 发送邮件给开发人员,ex对象发送给开发人员
        ex.printStackTrace();
        // return new R(false, null, "系统错误,请稍后再试!");
        // return new R(false, "服务器故障,请稍后再试!");
        return new R("服务器故障,请稍后再试!");
    }
}

页面上得到数据后,先判定是否有后台传递过来的消息,标志就是当前操作是否成功,如果返回操作结果 false,就读取后台传递的消息

// 添加
handleAdd() {
    // 发送ajax请求
    axios.post("/books", this.formData).then((res) => {
        // 如果操作成功,关闭弹层,显示数据
        if (res.data.flag) {
            this.dialogFormVisible = false;
            this.message.success("添加成功");
        } else {
            // this.message.error("添加失败");
            this.$message.error(res.data.msg); // 消息来自于后台传递过来,而非固定内容
        }
    }).finally(() => {
        this.getAll();
    });
},

页面消息处理

// 添加
handleAdd() {
    // 发送ajax请求
    axios.post("/books", this.formData).then((res) => {
        // 如果操作成功,关闭弹层,显示数据
        if (res.data.flag) {
            this.dialogFormVisible = false;
            // this.message.success("添加成功");
            this.message.success(res.data.msg);
        } else {
            this.$message.error(res.data.msg); // 消息来自于后台传递过来,而非固定内容
        }
    }).finally(() => {
        this.getAll();
    });
},

总结

  1. 使用注解 @RestControllerAdvice 定义 SpringMVC 异常处理器用来处理异常
  2. 异常处理器必须被扫描加载,否则无法生效
  3. 表现层返回结果的模型类中添加消息属性用来传递消息到页面

5.11、页面功能开发

⑤ 分页功能

分页功能的制作用于替换前面的查询全部,其中要使用到 elementUI 提供的分页组件

<!--分页组件-->
<div class="pagination-container">
    <el-pagination
        class="pagiantion"
        @current-change="handleCurrentChange"
        :current-page="pagination.currentPage"
        :page-size="pagination.pageSize"
        layout="total, prev, pager, next, jumper"
        :total="pagination.total">
    </el-pagination>
</div>

为了配合分页组件,封装分页对应的数据模型。定义分页组件需要使用的数据并将数据绑定到分页组件:

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<Book> page = new Page<>(currentPage, pageSize);
    return new R(true, bookService.page(page));
}

页面根据分页操作结果读取对应数据,并进行数据模型绑定

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

总结

  1. 使用 el 分页组件
  2. 定义分页组件绑定的数据模型
  3. 异步调用获取分页数据
  4. 分页数据页面回显

⑥ 删除功能维护

由于使用了分页功能,当最后一页只有一条数据时,删除操作就会出现 BUG,最后一页无数据但是独立展示。其实这个问题解决方案很多,这里给出比较简单的一种处理方案:对分页查询功能进行后台功能维护,对查询结果进行校验,如果当前页码值大于最大页码值,使用最大页码值作为当前页码值重新执行查询。

@GetMapping("{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage, @PathVariable int pageSize) {
    IPage<Book> page = new Page<>(currentPage, pageSize);
    bookService.page(page);

    // 如果当前页码值大于总页码值,则重新执行查询操作,使用最大页码值作为当前页码值
    if (currentPage > page.getPages()) {
        page = new Page<>(page.getPages(), pageSize);
        bookService.page(page);
    }

    // return new R(true, bookService.page(page));
    return new R(true, page);
}

⑦ 条件查询功能

最后一个功能来做条件查询,其实条件查询可以理解为分页查询的时候除了携带分页数据再多带几个数据的查询。这些多带的数据就是查询条件。比较一下不带条件的分页查询与带条件的分页查询差别之处,这个功能就好做了

  • 页面封装的数据:带不带条件影响的仅仅是一次性传递到后台的数据总量,由传递 2 个分页相关的数据转换成 2 个分页数据加若干个条件

  • 后台查询功能:查询时由不带条件,转换成带条件,反正不带条件的时候查询条件对象使用的是 null,现在换成具体条件,差别不大

  • 查询结果:不管带不带条件,出来的数据只是有数量上的差别,其他都差别,这个可以忽略

经过上述分析,看来需要在页面发送请求的格式方面做一定的修改,后台的调用数据层操作时发送修改,其他没有区别

页面发送请求时,两个分页数据仍然使用路径变量,其他条件采用动态拼装 url 参数的形式传递

页面封装查询条件字段

查询条件数据封装可采用单独封装,也可与分页操作混合封装,此处选择后者

pagination: { // 分页相关模型数据
    currentPage: 1, // 当前页码
    pageSize: 10,   // 每页显示的记录数
    total: 0,       // 总记录数
    name: "",
    type: "",
    description: ""
},

页面添加查询条件字段对应的数据模型绑定名称

<div class="filter-container">
    <el-input placeholder="图书类别" v-model="pagination.type" class="filter-item"/>
    <el-input placeholder="图书名称" v-model="pagination.name" class="filter-item"/>
    <el-input placeholder="图书描述" v-model="pagination.description" class="filter-item"/>
    <el-button @click="getAll()" class="dalfBut">查询</el-button>
    <el-button type="primary" class="butT" @click="handleCreate()">新建</el-button>
</div>

组织数据成为 get 请求发送的数据

将查询条件组织成 url 参数,添加到请求 url 地址中,这里可以借助其他类库快速开发,当前使用手工形式拼接,降低学习要求。条件参数组织可以通过条件判定书写的更简洁

// 分页查询
// 条件查询
getAll() {
    // 获取查询条件,组织参数,拼接url地址
    // console.log(this.pagination.type);
    param = "?type=" + this.pagination.type;
    param += "&name=" + this.pagination.name;
    param += "&description=" + this.pagination.description;
    console.log("-----------------" + param);

    // 发送异步请求
    axios.get("/books/" + this.pagination.currentPage + "/" + this.pagination.pageSize+param).then((res) => {
        // 页面回显数据
        this.pagination.pageSize = res.data.data.size;
        this.pagination.currentPage = res.data.data.current;
        this.pagination.total = res.data.data.total;

        this.dataList = res.data.data.records;
    });
},

后台代码中定义实体类封装查询条件

@GetMapping("{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage, @PathVariable int pageSize, Book book) {
    // System.out.println("参数 ==>" + book);

    IPage<Book> page = bookService.getPage(currentPage, pageSize, book);
    // 如果当前页码值大于总页码值,则重新执行查询操作,使用最大页码值作为当前页码值
    if (currentPage > page.getPages()) {
        page = bookService.getPage((int) page.getPages(), pageSize, book);
    }
    return new R(true, page);
}

业务层接口功能开发:

public interface IBookService extends IService<Book> {
    IPage<Book> getPage(Integer currentPage, Integer pageSize, Book book);
}

@Service
public class BookServiceImpl2 extends ServiceImpl<BookDao, Book> implements IBookService {
    public IPage<Book> getPage(Integer currentPage, Integer pageSize, Book book) {
        LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper<>();
        lqw.like(Strings.isNotEmpty(queryBook.getName()), Book::getName, book.getName());
        lqw.like(Strings.isNotEmpty(queryBook.getType()), Book::getType, book.getType());
        lqw.like(Strings.isNotEmpty(queryBook.getDescription()), Book::getDescription, book.getDescription());

        IPage page = new Page(currentPage, pageSize);
        return bookDao.selectPage(page, lqw);
    }
}

总结

  1. 定义查询条件数据模型(当前封装到分页数据模型中)
  2. 异步调用分页功能并通过请求参数传递数据到后台

5.12、案例总结

  1. pom.xml:配置起步依赖
  2. application.yml:设置数据源、端口、框架技术相关配置等
  3. dao:继承 BaseMapper、设置 @Mapper
  4. dao 测试类
  5. service:调用数据层接口或 MyBatis-Plus 提供的接口快速开发
  6. service 测试类
  7. controller:基于 Restful 开发,使用 Postman 测试跑通功能
  8. 页面:放置在 resources 目录下的 static 目录中

四、基础篇完结

基础篇到这里就全部结束了,在基础篇中带着大家学习了如何创建一个 SpringBoot 工程,然后学习了 SpringBoot 的基础配置语法格式,接下来对常见的市面上的实用技术做了整合,最后通过一个小的案例对前面学习的内容做了一个综合应用。整体来说就是一个最基本的入门,关于 SpringBoot 的实际开发其实接触的还是很少的,我们到实用篇和原理篇中继续吧,各位小伙伴,加油学习,再见。

  • 基础篇
    • 能够创建 SpringBoot 工程
    • 基于 SpringBoot 实现 SSM / SSMP 整合
  • 实用篇
    • 运维实用篇
    • 能够掌握 SpringBoot 程序多环境开发
    • 能够基于 Linux 系统发布 SpringBoot 工程
    • 能够解决线上灵活配置 SpringBoot 工程的需求
    • 开发实用篇
    • 能够基于 SpringBoot 整合任意第三方技术
  • 原理篇
    • 掌握 SpringBoot 内部工作流程
    • 理解 SpringBoot 整合第三方技术的原理
    • 实现自定义开发整合第三方技术的组件
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇