1. 整合 Junit
代码语言:javascript复制Spring 整合 JUnit 的制作方式
//加载spring整合junit专用的类运行器
@RunWith(SpringJUnit4ClassRunner.class)
//指定对应的配置信息
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTestCase {
//注入你要测试的对象
@Autowired
private AccountService accountService;
@Test
public void testGetById(){
//执行要测试的对象对应的方法
System.out.println(accountService.findById(2));
}
}
其中核心代码是前两个注解,第一个注解@RunWith
是设置 Spring 专用于测试的类运行器,简单说就是 Spring 程序执行程序有自己的一套独立的运行程序的方式,不能使用 JUnit 提供的类运行方式了,必须指定一下,但是格式是固定的,琢磨一下,每次都指定一样的东西,这个东西写起来没有技术含量啊,第二个注解@ContextConfiguration
是用来设置 Spring 核心配置文件或配置类的,简单说就是加载 Spring 的环境你要告诉 Spring 具体的环境配置是在哪里写的,虽然每次加载的文件都有可能不同,但是仔细想想,如果文件名是固定的,这个貌似也是一个固定格式。似然有可能是固定格式,那就有可能每次都写一样的东西,也是一个没有技术含量的内容书写
代码语言:javascript复制SpringBoot 就抓住上述两条没有技术含量的内容书写进行开发简化,能走默认值的走默认值,能不写的就不写,具体格式如下
@SpringBootTest
class Springboot02ConfigurationApplicationTests {
@Autowired
private BookDao bookDao;
@Test
void contextLoads() {
bookDao.save();
}
}
你加载的配置类或者配置文件是哪一个?就是我们前面启动程序使用的引导类。如果想手工指定引导类有两种方式 。
第一种方式使用属性的形式进行,在注解@SpringBootTest 中添加 classes 属性指定配置类
第二种方式回归原始配置方式,仍然使用@ContextConfiguration 注解进行,效果是一样的
温馨提示 :
- 使用 SpringBoot 整合 JUnit 需要保障导入 test 对应的 starter,由于初始化项目时此项是默认导入的
总结:
- 导入测试对应的 starter
- 测试类使用@SpringBootTest 修饰
- 使用自动装配的形式添加要测试的对象
- 测试类如果存在于引导类所在包或子包中无需指定引导类
- 测试类如果不存在于引导类所在的包或子包中需要通过 classes 属性指定引导类
2. 整合 MyBatis
SpringBoot 整合 MyBatis 的步骤
- 创建模块时勾选要使用的技术,MyBatis,由于要操作数据库,还要勾选对应数据库
- 配置数据源相关信息,没有这个信息你连接哪个数据库都不知道
- 创建实体类
- 创建映射接口
- 编写测试类进行测试
其中手工导入对应技术的 starter,和对应数据库的坐标为
代码语言:javascript复制<dependencies>
<!--1.导入对应的starter-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
数据源配置信息为:
代码语言:javascript复制#2.配置相关信息
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
username: root
password: root
总结
- 整合操作需要勾选 MyBatis 技术,也就是导入 MyBatis 对应的 starter
- 数据库连接相关信息转换成配置
- 数据库 SQL 映射需要添加@Mapper 被容器识别到
- MySQL 8.X 驱动强制要求设置时区
- 修改 url,添加 serverTimezone 设定
- 修改 MySQL 数据库配置
- 驱动类过时,提醒更换为 com.mysql.cj.jdbc.Driver
3. 整合 MyBatis-Plus
SpringBoot 整合 MyBatis-Plus 的步骤为
- 手工添加 MyBatis-Plus 对应的 starter
- 数据层接口使用 BaseMapper 简化开发
- 需要使用的第三方技术无法通过勾选确定时,需要手工添加坐标
其中导入对应的 starter
代码语言:javascript复制<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
关于这个坐标,此处要说明一点,之前我们看的 starter 都是 spring-boot-starter-???,也就是说都是下面的格式
代码语言:javascript复制Spring-boot-start-***
而这个坐标的名字书写比较特殊,是第三方技术名称在前,boot 和 starter 在后。此处简单提一下命名规范,后期原理篇会再详细讲解
温馨提示: 目前数据库的表名定义规则是 tbl_模块名称,为了能和实体类相对应,需要做一个配置,相关知识各位小伙伴可以到 MyBatisPlus 课程中去学习,此处仅给出解决方案。配置 application.yml 文件,添加如下配置即可,设置所有表名的通用前缀名
代码语言:javascript复制mybatis-plus:
global-config:
db-config:
table-prefix: tbl_ #设置所有表的通用前缀名称为tbl_
4. 整合 Druid
SpringBoot 整合 Druid 的步骤为:
- 整合 Druid 需要导入 Druid 对应的 starter
- 根据 Druid 提供的配置方式进行配置
- 整合第三方技术通用方式
- 导入对应的 starter
- 根据提供的配置格式,配置非默认值对应的配置项
导入对应的坐标 :
代码语言:javascript复制<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
修改配置,在数据源配置中有一个 type 属性,专用于指定数据源类型
代码语言:javascript复制spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
这里其实要提出一个问题的,目前的数据源配置格式是一个通用格式,不管你换什么数据源都可以用这种形式进行配置。但是新的问题又来了,如果对数据源进行个性化的配置
,例如配置数据源对应的连接数量,这个时候就有新的问题了。每个数据源技术对应的配置名称都一样吗?肯定不是啊,各个厂商不可能提前商量好都写一样的名字啊,怎么办?就要使用专用的配置格式了。这个时候上面这种通用格式就不能使用了,怎么办?还能怎么办?按照 SpringBoot 整合其他技术的通用规则来套啊,导入对应的 starter,进行相应的配置即可。
步骤 ①:导入对应的 starter
代码语言:javascript复制<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
步骤 ②:修改配置
代码语言:javascript复制spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
username: root
password: root
关于整合第三方技术就两个步骤 : 导入对应 starter,使用对应配置
5. SSMP 整合综合案例
SpringBoot 能够整合的技术太多太多了,对于初学者来说慢慢来,一点点掌握。前面咱们做了 4 个整合了,下面就通过一个稍微综合一点的案例,将所有知识贯穿起来,同时做一个小功能,体会一下。不过有言在先,这个案例制作的时候,你可能会有这种感觉,说好的 SpringBoot 整合其他技术的案例,为什么感觉 SpringBoot 整合其他技术的身影不多呢?因为这东西书写太简单了,简单到瞬间写完,大量的时间做的不是这些整合工作。
整体案例中需要采用的技术如下,先了解一下,做到哪一个说哪一个
- 实体类开发————使用 Lombok 快速制作实体类
- Dao 开发————整合 MyBatisPlus,制作数据层测试
- Service 开发————基于 MyBatisPlus 进行增量开发,制作业务层测试类
- Controller 开发————基于 Restful 开发,使用 PostMan 测试接口功能
- Controller 开发————前后端开发协议制作
- 页面开发————基于 VUE ElementUI 制作,前后端联调,页面数据处理,页面消息处理
- 列表
- 新增
- 修改
- 删除
- 分页
- 查询
- 项目异常处理
- 按条件查询————页面功能调整、Controller 修正功能、Service 修正功能
5.1 创建模块
对于这个案例如果按照企业开发的形式进行应该制作后台微服务,前后端分离的开发。
我知道这个对初学的小伙伴要求太高了,咱们简化一下。后台做单体服务器,前端不使用前后端分离的制作了。
一个服务器即充当后台服务调用,又负责前端页面展示,降低学习的门槛。 添加依赖 :
代码语言:javascript复制 <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>
5.2 实体类开发
- 实体类制作
- 使用 lombok 简化开发
- 导入 lombok 无需指定版本,由 SpringBoot 提供版本
- @Data 注解
添加 lombok 依赖
代码语言:javascript复制 <!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
使用 lombok 可以通过一个注解@Data 完成一个实体类对应的 getter,setter,toString,equals,hashCode 等操作的快速添加
代码语言:javascript复制import lombok.Data;
@Data
public class Book {
private Integer id;
private String type;
private String name;
private String description;
}
5.3 数据层开发——基础 CRUD
步骤 ①:导入 MyBatisPlus 与 Druid 对应的 starter,当然 mysql 的驱动不能少
代码语言:javascript复制<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
步骤 ②:配置数据库连接相关的数据源配置
代码语言:javascript复制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 加速开发,别忘了@Mapper 和泛型的指定
代码语言:javascript复制@Mapper
public interface BookDao extends BaseMapper<Book> {
}
步骤 ④:制作测试类测试结果,这个测试类制作是个好习惯,不过在企业开发中往往都为加速开发跳过此步,且行且珍惜吧
温馨提示 MP 技术默认的主键生成策略为雪花算法,生成的主键 ID 长度较大,和目前的数据库设定规则不相符,需要配置一下使 MP 使用数据库的主键生成策略,方式嘛还是老一套,做配置。在 application.yml 中添加对应配置即可,具体如下
代码语言:javascript复制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 语句,配置如下
再来看运行结果,此时就显示了运行期执行 SQL 的情况。
总结:
- 手工导入 starter 坐标(2 个),mysql 驱动(1 个)
- 配置数据源与 MyBatisPlus 对应的配置
- 开发 Dao 接口(继承 BaseMapper)
- 制作测试类测试 Dao 功能是否有效
- 使用配置方式开启日志,设置日志输出方式为标准输出即可查阅 SQL 执行日志
5.4 数据层开发——分页功能制作
分页功能实现步骤:
- 使用 IPage 封装分页数据
- 分页操作依赖 MyBatisPlus 分页拦截器实现功能
- 借助 MyBatisPlus 日志查阅执行 SQL 语句
MP 提供的分页操作 API 如下
代码语言:javascript复制@Test@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 提供的,别选错包了。创建此对象时就需要指定分页的两个基本数据
- 当前显示第几页
- 每页显示几条数据
代码语言:javascript复制可以通过创建 Page 对象时利用构造方法初始化这两个数据
IPage page = new Page(2,5);
原来这个 IPage 对象中封装了若干个数据,而查询的结果作为 IPage 对象封装的一个数据存在的,可以理解为查询结果得到后,又塞到了这个 IPage 对象中,其实还是为了高度的封装,一个 IPage 描述了分页所有的信息。下面 5 个操作就是 IPage 对象中封装的所有信息了
定义 MP 拦截器并将其设置为 Spring 管控的 bean
这个只是一个 MP 拦截器的架子
拦截器的具体内容为 :
上述代码第一行是创建 MP 的拦截器栈,这个时候拦截器栈中没有具体的拦截器,第二行是初始化了分页拦截器,并添加到拦截器栈中。如果后期开发其他功能,需要添加全新的拦截器,按照第二行的格式继续 add 进去新的拦截器就可以了。
5.5 数据层开发——条件查询功能制作
- 使用 QueryWrapper 对象封装查询条件
- 推荐使用 LambdaQueryWrapper 对象
- 所有查询操作封装成方法调用
- 查询条件支持动态条件拼装
执行一个模糊匹配对应的操作,由 like 条件书写变为了 like 方法的调用
代码语言:javascript复制@Test
public void testGetById() {
QueryWrapper<Book> queryWrapper = new QueryWrapper<>();
queryWrapper.like("name" , "Spring");
bookDao.selectList(queryWrapper);
}
其中第一句 QueryWrapper 对象是一个用于封装查询条件的对象,该对象可以动态使用 API 调用的方法添加条件,最终转化成对应的 SQL 语句。第二句就是一个条件了,需要什么条件,使用 QueryWapper 对象直接调用对应操作即可。比如做大于小于关系,就可以使用 lt 或 gt 方法,等于使用 eq 方法,等等,此处不做更多的解释了。
MP 针对字段检查进行了功能升级,全面支持 Lambda 表达式,就有了下面这组 API。由 QueryWrapper 对象升级为 LambdaQueryWrapper 对象,这下就变了上述问题的出现
为了便于开发者动态拼写 SQL,防止将 null 数据作为条件使用,MP 还提供了动态拼装 SQL 的快捷书写方式
5.6 业务层开发
其实标准业务层开发很多初学者认为就是调用数据层,怎么说呢?这个理解是没有大问题的,更精准的说法应该是组织业务逻辑功能,并根据业务需求,对数据持久层发起调用。有什么差别呢?目标是为了组织出符合需求的业务逻辑功能,至于调不调用数据层还真不好说,有需求就调用,没有需求就不调用。
一个常识性的知识普及一下,业务层的方法名定义一定要与业务有关,例如登录操作
代码语言:javascript复制login(String username,String password);
而数据层的方法名定义一定与业务无关,是一定,不是可能,也不是有可能,例如根据用户名密码查询
代码语言:javascript复制selectByUserNameAndPassword(String username,String password);
我们在开发的时候是可以根据完成的工作不同划分成不同职能的开发团队的。比如一个哥们制作数据层,他就可以不知道业务是什么样子,拿到的需求文档要求可能是这样的
代码语言:javascript复制接口:传入用户名与密码字段,查询出对应结果,结果是单条数据
接口:传入ID字段,查询出对应结果,结果是单条数据
接口:传入离职字段,查询出对应结果,结果是多条数据
但是进行业务功能开发的哥们,拿到的需求文档要求差别就很大
代码语言:javascript复制接口:传入用户名与密码字段,对用户名字段做长度校验,4-15位,对密码字段做长度校验,8到24位,对喵喵喵字段做特殊字符校验,不允许存在空格,查询结果为对象。如果为null,返回BusinessException,封装消息码INFO_LOGON_USERNAME_PASSWORD_ERROR
你比较一下,能是一回事吗?差别太大了,所以说业务层方法定义与数据层方法定义差异化很大,只不过有些入门级的开发者手懒或者没有使用过公司相关的 ISO 标准化文档而已。
业务层接口定义如下:
代码语言:javascript复制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);
}
业务层实现类如下,转调数据层即可
代码语言:javascript复制@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;
}
}
别忘了对业务层接口进行测试,测试类如下
代码语言:javascript复制@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("-----------------");
book.setName("测试数据123");
book.setDescription("测试数据123");
bookService.updateById(book);
}
@Test
void testDelete(){
bookService.removeById(18);
}
@Test
void testGetAll(){
bookService.list();
}
@Test
void testGetPage(){
IPage<Book> page = new Page<Book>(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());
}
}
总结
- Service 接口名称定义成业务名称,并与 Dao 接口名称进行区
- 制作测试类测试 Service 功能是否有效
5.7 业务层快速开发
其实 MP 技术不仅提供了数据层快速开发方案,业务层 MP 也给了一个通用接口,个人观点不推荐使用,凑合能用吧,其实就是一个封装 继承的思想,代码给出,实际开发慎用
业务层接口快速开发
代码语言:javascript复制public interface IBookService extends IService<Book> {
//添加非通用操作API接口
}
业务层接口实现类快速开发,关注继承的类需要传入两个泛型,一个是数据层接口,另一个是实体类
代码语言:javascript复制@Service
public class BookServiceImpl extends ServiceImpl<BookDao, Book> implements IBookService {
@Autowired
private BookDao bookDao;
//添加非通用操作API
}
如果感觉 MP 提供的功能不足以支撑你的使用需要,其实是一定不能支撑的,因为需求不可能是通用的,在原始接口基础上接着定义新的 API 接口就行了,此处不再说太多了,就是自定义自己的操作了,但是不要和已有的 API 接口名冲突即可。
总结
使用通用接口(ISerivce<T>)快速开发Service
- 使用通用实现类(ServiceImpl<M,T>)快速开发 ServiceImpl
- 可以在通用接口基础上做功能重载或功能追加
- 注意重载时不要覆盖原始操作,避免原始提供的功能丢失
5.8 表现层开发
表现层接口如下:
代码语言:javascript复制@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.modify(book);
}
@DeleteMapping("{id}")
public Boolean delete(@PathVariable Integer id){
return bookService.delete(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){
return bookService.getPage(currentPage,pageSize);
}
}
总结
- 基于 Restful 制作表现层接口
- 新增:POST
- 删除:DELETE
- 修改:PUT
- 查询:GET
- 接收参数
- 实体数据:@RequestBody
- 路径变量:@PathVariable
5.9 表现层消息一致性处理
- 设计统一的返回值结果类型便于前端开发读取数据
- 返回值结果类型可以根据需求自行设定,没有固定格式
- 返回值结果模型类用于后端与前端进行数据格式统一,也称为前后端数据协议
设计模板 :
进行一致性处理
代码语言:javascript复制@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){
return new R(bookService.save(book));
}
@PutMapping
public R update(@RequestBody Book book){
return new R(bookService.modify(book));
}
@DeleteMapping("{id}")
public R delete(@PathVariable Integer id){
return new R(bookService.delete(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){
return new R(true , bookService.getPage(currentPage,pageSize));
}
}
5.10 前后端联通性测试
在进行具体的功能开发之前,先做联通性的测试,通过页面发送异步提交(axios),这一步调试通过后再进行进一步的功能开发
代码语言:javascript复制//列表
getAll() {
axios.get("/books").then((res)=>{
console.log(res.data);
});
},
只要后台代码能够正常工作,前端能够在日志中接收到数据,就证明前后端是通的,也就可以进行下一步的功能开发了
总结
- 单体项目中页面放置在 resources/static 目录下
- created 钩子函数用于初始化页面时发起调用
- 页面使用 axios 发送异步请求获取数据后确认前后端是否联通
5.11 页面基础功能开发
F-1.列表功能(非分页版)
列表功能主要操作就是加载完数据,将数据展示到页面上,此处要利用 VUE 的数据模型绑定,发送请求得到数据,然后页面上读取指定数据即可
页面数据模型定义
代码语言:javascript复制data:{
dataList: [],//当前页要展示的列表数据
...
},
异步请求获取数据
代码语言:javascript复制//列表
getAll() {
axios.get("/books").then((res)=>{
this.dataList = res.data.data;
});
},
这样在页面加载时就可以获取到数据,并且由 VUE 将数据展示到页面上了
总结:
- 将查询数据返回到页面,利用前端数据绑定进行数据展示
F-2.添加功能
添加功能用于收集数据的表单是通过一个弹窗展示的,因此在添加操作前首先要进行弹窗的展示,添加后隐藏弹窗即可。因为这个弹窗一直存在,因此当页面加载时首先设置这个弹窗为不可显示状态,需要展示,切换状态即可
默认状态
代码语言:javascript复制data:{
dialogFormVisible: false,//添加表单是否可见
...
},
切换为显示状态
代码语言:javascript复制//弹出添加窗口
handleCreate() {
this.dialogFormVisible = true;
},
由于每次添加数据都是使用同一个弹窗录入数据,所以每次操作的痕迹将在下一次操作时展示出来,需要在每次操作之前清理掉上次操作的痕迹 定义清理数据操作
代码语言:javascript复制//重置表单
resetForm() {
this.formData = {};
},
切换弹窗状态时清理数据
代码语言:javascript复制//弹出添加窗口
handleCreate() {
this.dialogFormVisible = true;
this.resetForm();
},
至此准备工作完成,下面就要调用后台完成添加操作了
添加操作
代码语言:javascript复制//添加
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 操作发起调用)
F-3.删除功能
模仿添加操作制作删除功能,差别之处在于删除操作仅传递一个待删除的数据 id 到后台即可
删除操作
代码语言:javascript复制// 删除
handleDelete(row) {
axios.delete("/books/" row.id).then((res)=>{
if(res.data.flag){
this.$message.success("删除成功");
}else{
this.$message.error("删除失败");
}
}).finally(()=>{
this.getAll();
});
},
删除操作提示信息
代码语言:javascript复制// 删除
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 值到后台
- 删除操作结束后动态刷新页面加载数据
- 根据操作结果不同,显示对应的提示信息
- 删除操作前弹出提示框避免误操作
F-4.修改功能
修改功能可以说是列表功能、删除功能与添加功能的合体。几个相似点如下:
- 页面也需要有一个弹窗用来加载修改的数据,这一点与添加相同,都是要弹窗
- 弹出窗口中要加载待修改的数据,而数据需要通过查询得到,这一点与查询全部相同,都是要查数据
- 查询操作需要将要修改的数据 id 发送到后台,这一点与删除相同,都是传递 id 到后台
- 查询得到数据后需要展示到弹窗中,这一点与查询全部相同,都是要通过数据模型绑定展示数据
- 修改数据时需要将被修改的数据传递到后台,这一点与添加相同,都是要传递数据
所以整体上来看,修改功能就是前面几个功能的大合体 查询并展示数据
代码语言:javascript复制//弹出编辑窗口
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("数据同步失败,自动刷新");
}
});
},
修改操作
代码语言:javascript复制//修改
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 调用后台对应操作(同新增传递数据)
- 修改操作结束后动态刷新页面加载数据(同新增)
- 根据操作结果不同,显示对应的提示信息(同新增)
5.12 业务消息一致性处理
目前的功能制作基本上达成了正常使用的情况,什么叫正常使用呢?也就是这个程序不出 BUG,如果我们搞一个 BUG 出来,你会发现程序马上崩溃掉。比如后台手工抛出一个异常,看看前端接收到的数据什么样子
代码语言:javascript复制{
"timestamp": "2021-09-15T03:27:31.038 00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/books"
}
面对这种情况,前端的同学又不会了,这又是什么格式?怎么和之前的格式不一样?
代码语言:javascript复制{
"flag": true,
"data": {
"id": 1,
"type": "计算机理论",
"name": "Spring实战 第5版",
"description": "Spring入门经典教程"
}
}
看来不仅要对正确的操作数据格式做处理,还要对错误的操作数据格式做同样的格式处理
首先在当前的数据结果中添加消息字段,用来兼容后台出现的操作消息
代码语言:javascript复制@Data
public class R{
private Boolean flag;
private Object data;
private String msg; //用于封装消息
}
后台代码也要根据情况做处理,当前是模拟的错误
代码语言:javascript复制@PostMapping
public R save(@RequestBody Book book) throws IOException {
Boolean flag = bookService.insert(book);
return new R(flag , flag ? "添加成功^_^" : "添加失败-_-!");
}
然后在表现层做统一的异常处理,使用 SpringMVC 提供的异常处理器做统一的异常处理
代码语言:javascript复制@RestControllerAdvice
public class ProjectExceptionAdvice {
@ExceptionHandler(Exception.class)
public R doOtherException(Exception ex){
//记录日志
//发送消息给运维
//发送邮件给开发人员,ex对象发送给开发人员
ex.printStackTrace();
return new R(false,null,"系统错误,请稍后再试!");
}
}
页面上得到数据后,先判定是否有后台传递过来的消息,标志就是当前操作是否成功,如果返回操作结果 false,就读取后台传递的消息
代码语言:javascript复制//添加
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();
});
},
总结
- 使用注解@RestControllerAdvice 定义 SpringMVC 异常处理器用来处理异常的
- 异常处理器必须被扫描加载,否则无法生效
- 表现层返回结果的模型类中添加消息属性用来传递消息到页面
5.13 页面功能开发
F-5.分页功能
分页功能的制作用于替换前面的查询全部,其中要使用到 elementUI 提供的分页组件
代码语言:javascript复制<!--分页组件-->
<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>
为了配合分页组件,封装分页对应的数据模型
代码语言:javascript复制data:{ pagination: { //分页相关模型数据 currentPage: 1,
//当前页码 pageSize:10, //每页显示的记录数 total:0,
//总记录数 } },
修改查询全部功能为分页查询,通过路径变量传递页码信息参数
代码语言:javascript复制getAll() {
axios.get("/books/" this.pagination.currentPage "/" this.pagination.pageSize).then((res) => {
});
},
后台提供对应的分页功能
代码语言:javascript复制@GetMapping("/{currentPage}/{pageSize}")
public R getAll(@PathVariable Integer currentPage,@PathVariable Integer pageSize){
IPage<Book> pageBook = bookService.getPage(currentPage, pageSize);
return new R(null != pageBook ,pageBook);
}
页面根据分页操作结果读取对应数据,并进行数据模型绑定
代码语言:javascript复制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;
});
},
对切换页码操作设置调用当前分页操作
代码语言:javascript复制//切换页码
handleCurrentChange(currentPage) {
this.pagination.currentPage = currentPage;
this.getAll();
},
总结
- 使用 el 分页组件
- 定义分页组件绑定的数据模型
- 异步调用获取分页数据
- 分页数据页面回显
F-6.删除功能维护
由于使用了分页功能,当最后一页只有一条数据时,删除操作就会出现 BUG,最后一页无数据但是独立展示,对分页查询功能进行后台功能维护,如果当前页码值大于最大页码值,重新执行查询。其实这个问题解决方案很多,这里给出比较简单的一种处理方案
代码语言:javascript复制@GetMapping("{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage,@PathVariable int pageSize){
IPage<Book> page = bookService.getPage(currentPage, pageSize);
//如果当前页码值大于了总页码值,那么重新执行查询操作,使用最大页码值作为当前页码值
if( currentPage > page.getPages()){
page = bookService.getPage((int)page.getPages(), pageSize);
}
return new R(true, page);
}
F-7.条件查询功能
最后一个功能来做条件查询,其实条件查询可以理解为分页查询的时候除了携带分页数据再多带几个数据的查询。这些多带的数据就是查询条件。比较一下不带条件的分页查询与带条件的分页查询差别之处,这个功能就好做了
- 页面封装的数据:带不带条件影响的仅仅是一次性传递到后台的数据总量,由传递 2 个分页相关的数据转换成 2 个分页数据加若干个条件
- 后台查询功能:查询时由不带条件,转换成带条件,反正不带条件的时候查询条件对象使用的是 null,现在换成具体条件,差别不大
- 查询结果:不管带不带条件,出来的数据只是有数量上的差别,其他都差别,这个可以忽略
经过上述分析,看来需要在页面发送请求的格式方面做一定的修改,后台的调用数据层操作时发送修改,其他没有区别
页面发送请求时,两个分页数据仍然使用路径变量,其他条件采用动态拼装 url 参数的形式传递
页面封装查询条件字段
代码语言:javascript复制pagination: {
//分页相关模型数据
currentPage: 1, //当前页码
pageSize:10, //每页显示的记录数
total:0, //总记录数
name: "",
type: "",
description: ""
},
页面添加查询条件字段对应的数据模型绑定名称
代码语言:javascript复制<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>
将查询条件组织成 url 参数,添加到请求 url 地址中,这里可以借助其他类库快速开发,当前使用手工形式拼接,降低学习要求
代码语言:javascript复制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;
});
},
后台代码中定义实体类封查询条件
代码语言:javascript复制@GetMapping("{currentPage}/{pageSize}")
public R getAll(@PathVariable int currentPage,@PathVariable int pageSize,Book book) {
System.out.println("参数=====>" book);
IPage<Book> pageBook = bookService.getPage(currentPage,pageSize);
return new R(null != pageBook ,pageBook);
}
对应业务层接口与实现类进行修正
代码语言:javascript复制public interface IBookService extends IService<Book> {
IPage<Book> getPage(Integer currentPage,Integer pageSize,Book queryBook);
}
代码语言:javascript复制@Service
public class BookServiceImpl2 extends ServiceImpl<BookDao,Book> implements IBookService {
public IPage<Book> getPage(Integer currentPage,Integer pageSize,Book queryBook){
IPage page = new Page(currentPage,pageSize);
LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper<Book>();
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);
}
}
页面回显数据
代码语言:javascript复制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;
});
},
总结
- 定义查询条件数据模型(当前封装到分页数据模型中)
- 异步调用分页功能并通过请求参数传递数据到后台