基于SpringBoot实现SSMP整合的案例
--SSMP整合案例--
源码下载:基于SpringBoot实现SSMP整合的案例源码 - 小小孙十八 - 博客园 (cnblogs.com)
最终效果
主页面
添加
删除
修改
分页
条件查询
整体案例中需要采用的技术如下:
- 实体类开发————使用Lombok快速制作实体类
- Dao开发————整合MyBatisPlus,制作数据层测试
- Service开发————基于MyBatisPlus进行增量开发,制作业务层测试类
- Controller开发————基于Restful开发,使用PostMan测试接口功能
- Controller开发————前后端开发协议制作
- 页面开发————基于VUE+ElementUI制作,前后端联调,页面数据处理,页面消息处理
- 列表
- 新增
- 修改
- 删除
- 分页
- 查询
- 项目异常处理
- 按条件查询————页面功能调整、Controller修正功能、Service修正功能
模块创建
对于这个案例如果按照企业开发的形式进行应该制作后台微服务,前后端分离的开发。
降低难度,稍作简化。后台做单体服务器,前端不使用前后端分离的制作了。
一个服务器即充当后台服务调用,又负责前端页面展示,降低学习的门槛。
下面我们创建一个新的模块,加载要使用的技术对应的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>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
</dependencies>
application.yml
server:
port: 80
实体类开发
本案例对应的模块表结构如下:
-- ----------------------------
-- 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;
}
对于实体类,可以使用工具: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等操作的快速添加。但是注意@Data注解并不会生成构造方法,有需求要手动添加。
import lombok.Data;
@Data
public class Book {
private Integer id;
private String type;
private String name;
private String description;
}
总结
- 实体类制作
- 使用lombok简化开发
- 导入lombok无需指定版本,由SpringBoot提供版本
- @Data注解
数据层开发-基础CRUD
本次的数据层开发使用MyBatisPlus技术,数据源使用Druid。
步骤①:导入MyBatisPlus与Druid对应的starter,当然mysql的驱动不能少
<dependencies>
<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>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</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
步骤③:使用MyBatisPlus的标准通用接口BaseMapper加速开发,别忘了@Mapper和泛型的指定
@Mapper
public interface BookDao extends BaseMapper<Book> {
}
步骤④:制作测试类测试结果,这个测试类制作是个好习惯,不过在企业开发中往往都为加速开发跳过此步,且行且珍惜吧
@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);
}
}
温馨提示
MyBatisPlus技术默认的主键生成策略为雪花算法,生成的主键ID长度较大,和目前的数据库设定规则不相符,需要配置一下使MyBatisPlus使用数据库的主键生成策略。在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生成策略为自增
查看MyBatisPlus运行日志
通过配置的形式就可以查阅执行期SQL语句,配置如下
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
再来看运行结果,此时就显示了运行期执行SQL的情况。
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4e3679] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@52c74d] 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: 51, 测试数据2, 测试数据name1, 测试数据description1
<== Total: 13
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4e3679]
此处设置的是日志的显示形式,当前配置的是控制台输出,当然还可以由更多的选择,根据需求切换即可
总结
-
手工导入starter坐标(2个),mysql驱动(1个)
-
配置数据源与MyBatisPlus对应的配置
-
开发Dao接口(继承BaseMapper)
-
制作测试类测试Dao功能是否有效
-
使用配置方式开启日志,设置日志输出方式为标准输出即可查阅SQL执行日志
数据层开发-分页功能制作
MyBatisPlus提供的分页操作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的形式创建这个对象,这个对象也是MyBatisPlus提供的,别选错包了。创建此对象时需要指定两个分页的基本数据
- 当前显示第几页
- 每页显示几条数据
可以通过创建Page对象时利用构造方法初始化这两个数据。
IPage page = new Page(2,5);
将该对象传入到查询方法selectPage后,可以得到查询结果,但当前操作查询结果返回值仍然是一个IPage对象。
IPage page = bookDao.selectPage(page, null);
因为这个对象中封装了若干个数据,而查询的结果作为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()); //详细数据
}
到这里就知道这些数据如何获取了,但是当去执行这个操作时,实际上这个分页功能当前是无效的。这个要源于MyBatisPlus的内部机制。
对于MySQL的分页操作使用limit关键字进行,而并不是所有的数据库都使用limit关键字实现的,这个时候MyBatisPlus为了制作的兼容性强,将分页操作设置为基础查询操作的升级版。
基础操作中有查询全部的功能,而在这个基础上只需要升级一下就可以得到分页操作。所以MyBatisPlus将分页操作做成了一个开关,你用分页功能就把开关开启,不用就不需要开启这个开关。
这个开关是通过MyBatisPlus的拦截器的形式存在的。具体设置方式如下:
定义MyBatisPlus拦截器并将其设置为Spring管控的bean
//在此使用Spring管理第三方bean的方式把bean初始化且加载给Spring的环境
@Configuration//配置类注解
public class MPConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
//创建拦截器框架
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//给框架添加分页拦截器
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
上述代码第一行是创建MyBatisPlus的拦截器栈,这个时候拦截器栈中没有具体的拦截器,第二行是初始化了分页拦截器,并添加到拦截器栈中。如果后期开发其他功能,需要添加全新的拦截器,按照第二行的格式继续add进去新的拦截器就可以了。
总结
- 使用IPage封装分页数据
- 分页操作依赖MyBatisPlus分页拦截器实现功能
- 借助MyBatisPlus日志查阅执行SQL语句
数据层开发-条件查询功能制作
执行一个模糊匹配对应的操作,由like条件书写变为了like方法的调用。
@Test
void testGetBy(){
//创建QueryWrapper的对象
QueryWrapper<Book> qw = new QueryWrapper<>();
//加条件
qw.like("name","Spring");
//select * from tbl_book where name like %spring%
bookDao.selectList(qw);
}
其中第一句QueryWrapper对象是一个用于封装查询条件的对象,该对象可以动态使用API调用的方法添加条件,最终转化成对应的SQL语句。 第二句就是一个条件了,需要什么条件,使用QueryWapper对象直接调用对应操作即可。比如做大于小于关系,就可以使用lt或gt方法,等于使用eq方法,等等。
这组API使用还是比较简单的,但是关于属性字段名的书写存在着安全隐患,比如查询字段name,当前是以字符串的形态书写的,万一写错,编译器还没有办法发现,只能将问题抛到运行器通过异常堆栈告诉开发者,不太友好。
MyBatisPlus针对字段检查进行了功能升级,全面支持Lambda表达式,就有了下面这组API。由QueryWrapper对象升级为LambdaQueryWrapper对象,这下就避免了上述问题的出现。
@Test
void testGetBy2(){
String name = "spring";
LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper<Book>();
lqw.like(Book::getName,name);
bookDao.selectList(lqw);
}
为了便于开发者动态拼写SQL,防止将null数据作为条件使用,MyBatisPlus还提供了动态拼装SQL的快捷书写方式。
@Test
void testGetBy2(){
String name = "spring";
LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper<Book>();
//if(name != null) lqw.like(Book::getName,name); //方式一:JAVA代码控制
lqw.like(name != null,Book::getName,name); //方式二:API接口提供控制开关
bookDao.selectList(lqw);
}
其实就是个格式,没有区别。
总结
-
使用QueryWrapper对象封装查询条件
-
推荐使用LambdaQueryWrapper对象
-
所有查询操作封装成方法调用
-
查询条件支持动态条件拼装
业务层开发
标准开发
标准业务层开发是组织业务逻辑功能,并根据业务需求,对数据持久层发起调用。
注意:
业务层的方法名定义一定要与业务有关,例如登录操作
login(String username,String password);
而数据层的方法名定义一定与业务无关,例如根据用户名密码查询
selectByUserNameAndPassword(String username,String password);
我们在开发的时候是可以根据完成的工作不同划分成不同职能的开发团队的。比如一员工制作数据层,他就可以不知道业务是什么样子,拿到的需求文档要求可能是这样的
接口:传入用户名与密码字段,查询出对应结果,结果是单条数据 接口:传入ID字段,查询出对应结果,结果是单条数据 接口:传入离职字段,查询出对应结果,结果是多条数据
但是进行业务功能开发的员工,拿到的需求文档要求差别就很大
接口:传入用户名与密码字段,对用户名字段做长度校验,4-15位,对密码字段做长度校验,8到24位,对密码字段做特殊字符校验,不允许存在空格,查询结果为对象。如果为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(2));
}
@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(14);
book.setType("测试数据a");
book.setName("测试数据123");
book.setDescription("测试数据123");
bookService.updateById(book);
}
@Test
void testDelete(){
bookService.removeById(14);
}
@Test
void testGetAll(){
bookService.getAll();
}
@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());
}
}
总结
- Service接口名称定义成业务名称,并与Dao接口名称进行区分
- 制作测试类测试Service功能是否有效
快速开发
MyBatisPlus技术不仅提供了数据层快速开发方案,业务层MyBatisPlus也给了一个通用接口,不推荐使用。其实就是一个封装+继承的思想,代码给出,实际开发慎用。
业务层接口快速开发
public interface IBookService extends IService<Book> {
//添加非通用操作API接口
}
业务层接口实现类快速开发,注意继承的类需要传入两个泛型,一个是数据层接口,另一个是实体类。
@Service
public class BookServiceImpl extends ServiceImpl<BookDao, Book> implements IBookService {
@Autowired
private BookDao bookDao;
//添加非通用操作API
}
若功能与需求不符,可在原始接口基础上定义新的API接口,但是不要和已有的API接口名冲突。
总结
- 使用通用接口(ISerivce)快速开发Service
- 使用通用实现类(ServiceImpl<M,T>)快速开发ServiceImpl
- 可以在通用接口基础上做功能重载或功能追加
- 注意重载时不要覆盖原始操作,避免原始提供的功能丢失
表现层开发
表现层的开发使用基于Restful的表现层接口开发,功能测试通过Postman工具进行。
表现层接口如下:
@RestController
@RequestMapping("/books")
public class BookController {
@Autowired
private BookService bookService;
//查询全部
@GetMapping
public List<Book> getAll(){
return bookService.getAll();
}
//添加
@PostMapping
//save操作用异步提交发送的话,其参数通过请求体传json数据过来
public Boolean save(@RequestBody Book book){
return bookService.save(book);
}
//修改
@PutMapping
//update操作用的也是请求体参数
public Boolean update(@RequestBody Book book){
return bookService.update(book);
}
//删除
@DeleteMapping("{id}")
//而删除和查询单个操作要使用路径变量来传参
//@PathVariable :从路径中获取变量
public Boolean delete(@PathVariable Integer id){
return bookService.delete(id);
}
//查询单条数据
@GetMapping("{id}")
public Book getById(@PathVariable Integer id){
return bookService.getById(id);
}
}
在使用Postman测试时关注提交类型,对应上即可,不然就会报405的错误码。
普通GET请求
POST、PUT请求传递json数据,后台使用@RequestBody接收数据
GET、DELETE请求传递路径变量,后台使用@PathVariable接收数据
总结
- 基于Restful制作表现层接口
- 新增:POST
- 删除:DELETE
- 修改:PUT
- 查询:GET
- 接收参数
- 实体数据:@RequestBody
- 路径变量:@PathVariable
表现层消息一致性处理
目前通过Postman测试后业务层接口功能是通的,但是这样的结果给到前端开发者会出现一个小问题。不同的操作结果所展示的数据格式差异化严重。
每种不同操作返回的数据格式都不一样,需要将所有操作的操作结果数据格式统一起来,需要设计表现层返回结果的模型类,用于后端与前端进行数据格式统一,也称为前后端数据协议
@Data
public class R {
private Boolean flag;
private Object data;
}
其中flag用于标识操作是否成功,data用于封装操作数据,现在的数据格式就变了
{
"flag": true,
"data":{
"id": 1,
"type": "计算机理论",
"name": "Spring实战 第5版",
"description": "Spring入门经典教程"
}
}
对R模型类添加构造方法:
@Data
public class R {
private Boolean flag;
private Object data;
public R(Boolean flag) {
this.flag = flag;
}
public R(Boolean flag,Object data){
this.flag=flag;
this.data=data;
}
public R() {}
}
表现层开发格式也需要转换一下
@RestController
@RequestMapping("/books")
public class BookController {
@Autowired
private BookService bookService;
//查询全部
@GetMapping
public R getAll(){
return new R(true,bookService.getAll());
}
//添加
@PostMapping
//save操作用异步提交发送的话,其参数通过请求体传json数据过来
public R save(@RequestBody Book book){
return new R(bookService.save(book));
}
//修改
@PutMapping
//update操作用的也是请求体参数
public R update(@RequestBody Book book){
return new R(bookService.update(book));
}
//删除
@DeleteMapping("{id}")
//而删除和查询单个操作要使用路径变量来传参
//@PathVariable :从路径中获取变量
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));
}
}
此时,全格式统一,现在后端发送给前端的数据格式就统一了,免去了不少前端解析数据的烦恼。
总结
-
设计统一的返回值结果类型便于前端开发读取数据
-
返回值结果类型可以根据需求自行设定,没有固定格式
-
返回值结果模型类用于后端与前端进行数据格式统一,也称为前后端数据协议
前后端联通性测试
后端的表现层接口开发完毕,就可以进行前端的开发了。
将前端页面保存到resources目录下的static目录中,建议执行maven的clean生命周期,避免缓存的问题出现。
在进行具体的功能开发之前,先做联通性的测试,通过页面发送异步提交(axios),这一步调试通过后再进行进一步的功能开发。
//钩子函数,VUE对象初始化完成后自动执行
created() {
//调用查询全部数据的操作
this.getAll();
},
methods: {
//列表
getAll() {
//发送异步请求
axios.get("/books").then((res)=>{
console.log(res.data);
});
}
}
只要后台代码能够正常工作,前端能够在日志中接收到数据,就证明前后端是通的,也就可以进行下一步的功能开发了。
总结
- 单体项目中页面放置在resources/static目录下
- created钩子函数用于初始化页面时发起调用
- 页面使用axios发送异步请求获取数据后确认前后端是否联通
页面基础功能开发
列表功能(非分页版)
列表功能主要操作就是加载完数据,将数据展示到页面上,此处要利用VUE的数据模型绑定,发送请求得到数据,然后页面上读取指定数据即可。
页面数据模型定义
由el-table标签的属性可知,页面显示的数据来源为dataList
<el-table size="small" current-row-key="id" :data="dataList" stripe highlight-current-row>
...
<el-table />
可以看到当前的dataList数据为空,需要给dataList添加数据
data:{
dataList: [], //当前页要展示的列表数据
...
},
异步请求获取数据
因为res.data数据中包含flag和data ,而此处要加载res.data数据中的data数据,故要写为res.data.data
。
//列表
getAll() {
axios.get("/books").then((res)=>{
this.dataList = res.data.data;
});
},
这样在页面加载时就可以获取到数据,并且由VUE将数据展示到页面上了。
添加功能
添加功能用于收集数据的表单是通过一个弹窗展示的,因此在添加操作前首先要进行弹窗的展示,添加后隐藏弹窗即可。因为这个弹窗一直存在,因此当页面加载时首先设置这个弹窗为不可显示状态,需要展示,切换状态即可。
由新建按钮的@click属性可知,当前按钮的点击事件是
handleCreate()
方法<el-button type="primary" class="butT" @click="handleCreate()">新建</el-button>
新增标签弹层的
visible.sync="dialogFormVisible"
默认状态
data:{
dialogFormVisible: false, //添加表单是否可见
...
},
切换为显示状态
//弹出添加窗口
handleCreate() {
this.dialogFormVisible = true;
},
由于每次添加数据都是使用同一个弹窗录入数据,所以每次操作的痕迹将在下一次操作时展示出来,需要在每次操作之前清理掉上次操作的痕迹。
定义清理数据操作
//重置表单
resetForm() {
this.formData = {};
},
切换弹窗状态时清理数据
//弹出添加窗口
handleCreate() {
this.dialogFormVisible = true;
this.resetForm();
},
至此准备工作完成,下面就要调用后台完成添加操作了。
添加操作
//添加
handleAdd () {
//发送异步请求
//在发请求的时候,把formData也发过去
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.dialogFormVisible4Edit = false;
this.$message.info("操作取消");
},
总结
- 请求方式使用POST调用后台对应操作
- 添加操作结束后动态刷新页面加载数据
- 根据操作结果不同,显示对应的提示信息
- 弹出添加Div时清除表单数据
删除功能
模仿添加操作制作删除功能,差别之处在于删除操作仅传递一个待删除的数据id到后台即可。
删除操作
由
<el-button type="danger" size="mini" @click="handleDelete(scope.row)">删除</el-button>
可知删除操作点击事件调用的是handleDelete
方法
// 删除
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) {
// 弹出提示框
this.$confirm("此操作永久删除当前数据,是否继续?","提示",{type:'info'}).then(()=>{
// 做删除业务
axios.delete("/books/"+row.id).then((res)=>{
if(res.data.flag){
this.$message.success("删除成功");
}else{
this.$message.error("数据同步失败,自动刷新");
}
}).finally(()=>{
this.getAll();
});
}).catch(()=>{
// 取消删除
this.$message.info("取消删除操作");
});
},
总结
- 请求方式使用Delete调用后台对应操作
- 删除操作需要传递当前行数据对应的id值到后台
- 删除操作结束后动态刷新页面加载数据
- 根据操作结果不同,显示对应的提示信息
- 删除操作前弹出提示框避免误操作
修改功能
修改功能可以说是列表功能、删除功能与添加功能的合体。几个相似点如下:
-
页面也需要有一个弹窗用来加载修改的数据,这一点与添加相同,都是要弹窗
-
弹出窗口中要加载待修改的数据,而数据需要通过查询得到,这一点与查询全部相同,都是要查数据
-
查询操作需要将要修改的数据id发送到后台,这一点与删除相同,都是传递id到后台
-
查询得到数据后需要展示到弹窗中,这一点与查询全部相同,都是要通过数据模型绑定展示数据
-
修改数据时需要将被修改的数据传递到后台,这一点与添加相同,都是要传递数据
所以整体上来看,修改功能就是前面几个功能的大合体
由
<el-button type="primary" size="mini" @click="handleUpdate(scope.row)">编辑</el-button>
可知删除操作点击事件调用的是handleUpdate
方法
查询并展示数据
//弹出编辑窗口
handleUpdate(row) {
axios.get("/books/"+row.id).then((res)=>{
//此处有可能初现flag==true,data==null的情况
if(res.data.flag && res.data.data != null ){
//展示弹层,加载数据。将data数据赋给formData
this.formData = res.data.data;
this.dialogFormVisible4Edit = true;
}else{
this.$message.error("数据同步失败,自动刷新");
}
}).finally(()=>{
// 重新加载数据
this.getAll();
});
},
修改操作
//修改
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出来,会发现程序马上崩溃掉。比如后台手工抛出一个异常,看看前端接收到的数据什么样子。
@PostMapping
public R save(@RequestBody Book book) throws IOException {
//手动制作一个BUG
if (true) throw new IOException();
return new R(bookService.save(book));
}
{
"timestamp": "2023-06-22T11:40:09.266+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; //用于封装消息
// 添加构造函数,处理当flag==false时的情况
public R(Boolean flag,String msg){
this.flag=flag;
this.msg=msg;
}
}
后台代码也要根据情况做处理,当前是模拟的错误。
@PostMapping
public R save(@RequestBody Book book) throws IOException {
//手动制作一个BUG
if (true) throw new IOException();
return new R(bookService.save(book));
}
然后在表现层做统一的异常处理,使用SpringMVC提供的异常处理器做统一的异常处理。
// 作为SpringMVC的异常处理器
// @ControllerAdvice
@RestControllerAdvice
public class ProjectExceptionAdvice {
//添加此注解后,方法就可以拦截所有的异常信息
// @ExceptionHandler
//也可以给注解添加参数,来标明处理具体的异常
@ExceptionHandler(Exception.class)
public R doException(Exception ex){
//记录日志
//发送消息给运维
//发送邮件给开发人员,ex对象发送给开发人员
ex.printStackTrace();
return new R(false,"服务器异常,请稍后再试!");
}
}
此时页面接收到的消息:
{
"flag": false,
"data": null,
"msg": "服务器异常,请稍后再试!"
}
页面上得到数据后,先判定是否有后台传递过来的消息,标志就是当前操作是否成功,如果返回操作结果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();
});
},
修改手动添加的BUG,不能定死。
@PostMapping
public R save(@RequestBody Book book) throws IOException {
//当book的Name属性为123时,抛出异常
if (book.getName().equals("123")) throw new IOException();
return new R(bookService.save(book));
}
问题:当前的提示信息,部分由前端管理也有部分由后端管理,耦合了!现在来分析如何将消息都归于后端来管理。
对页面的添加功能做修改:
//添加
handleAdd () {
axios.post("/books",this.formData).then((res)=>{
//判断当前操作是否成功
if(res.data.flag){
//1.关闭弹层
this.dialogFormVisible = false;
this.$message.success(res.data.msg);
}else{
this.$message.error(res.data.msg);
}
}).finally(()=>{
//2.重新加载数据
this.getAll();
});
},
在BookController.java中修改BUG区代码:
@PostMapping
public R save(@RequestBody Book book) throws IOException {
Boolean flag = bookService.save(book);
//手动制作一个BUG
if (book.getName().equals("123")) throw new IOException();
return new R(flag , flag ? "添加成功^_^" : "添加失败-_-!");
}
总结
- 使用注解@RestControllerAdvice定义SpringMVC异常处理器用来处理异常的
- 异常处理器必须被扫描加载,否则无法生效
- 表现层返回结果的模型类中添加消息属性用来传递消息到页面
页面功能开发
分页功能
分页功能的制作用于替换前面的查询全部,其中要使用到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> 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,最后一页无数据但是独立展示,对分页查询功能进行后台功能维护,如果当前页码值大于最大页码值,重新执行查询。 对于BUG,原则上来说需要基于业务需求来维护删除功能,而不是有统一方案来解决一切BUG。
@GetMapping("{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage,@PathVariable int pageSize){
IPage<Book> pageBook = bookService.getPage(currentPage, pageSize);
// 此处解决删完未退出当前页BUG
// 如果当前页码值大于总页码值,使用最大页码值作为当前页码值
if (currentPage > pageBook.getPages()){
pageBook = bookService.getPage((int) pageBook.getPages(),pageSize);
}
return new R(true,pageBook);
}
总结
- 使用el分页组件
- 定义分页组件绑定的数据模型
- 异步调用获取分页数据
- 分页数据页面回显
条件查询功能
最后一个功能来做条件查询,其实条件查询可以理解为分页查询的时候除了携带分页数据再多带几个数据的查询。这些多带的数据就是查询条件。比较一下不带条件的分页查询与带条件的分页查询差别之处,这个功能就好做了
-
页面封装的数据:带不带条件影响的仅仅是一次性传递到后台的数据总量,由传递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>
将查询条件组织成url参数,添加到请求url地址中,这里可以借助其他类库快速开发,当前使用手工形式拼接,降低学习要求
getAll() {
// 获取查询条件,拼接查询条件
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.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;
});
},
后台代码中定义实体类封查询条件,Controller接收参数
@GetMapping("{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage,@PathVariable int pageSize,Book book){
IPage<Book> pageBook = bookService.getPage(currentPage, pageSize, book);
if (currentPage > pageBook.getPages()){
pageBook = bookService.getPage((int) pageBook.getPages(),pageSize,book);
}
return new R(true,pageBook);
}
对应业务层接口与实现类进行修正
public interface BookService {
IPage<Book> getPage(int currentPage, int pageSize, Book book);
}
@Service
public class BookServiceImpl implements BookService {
@Override
public IPage<Book> getPage(int currentPage, int pageSize, Book book) {
IPage page = new Page(currentPage,pageSize);
LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper<>();
lqw.like(Strings.isNotEmpty(book.getType()),Book::getType,book.getType());
lqw.like(Strings.isNotEmpty(book.getName()),Book::getName,book.getName());
lqw.like(Strings.isNotEmpty(book.getDescription()),Book::getDescription,book.getDescription());
bookDao.selectPage(page,lqw);
return page;
}
}
总结
- 定义查询条件数据模型(当前封装到分页数据模型中)
- 异步调用分页功能并通过请求参数传递数据到后台
至此,基于SpringBoot实现SSMP整合的案例已经开发完毕。
转载自:https://juejin.cn/post/7248655627928567863