SSM整合流程:
1.创建工程
2.SSM整合
- Spring
- SpringConfig
- Mybatis
- MybatisConfig
- JdbcConfig
- jdbc.properties
- SpringMVC
- ServletConfig
- SpringMVCConfig
3.功能模块
- 表与实体类
- dao(接口+自动代理)
- service(接口+实现类)
- 业务层接口测试(整合JUnit)
- controller
- 表现层接口测试(PostMan)
1 环境搭建
回顾:
在Spring中使用IOC容器来管理Druid连接池对象,具体操作流程如下:
1.使用第三方的技术,需要在pom.xml中添加依赖
2.在配置文件中将第三方的类制作成一个bean,让IOC容器进行管理
3.数据库连接需要基础的四要素驱动、连接、用户名和密码,注入到对应的bean中
4.从IOC容器中获取对应的bean对象
2 表现层数据封装模型:统一结果类封装
2.1 前端接收数据格式
- 增删改
在Controller层进行增删改操作给前端返回的是boolean类型数据
- 查单条
在Controller层查询单条数据返回给前端的不是直接数据是json数据,需要进行解析
- 查全部
在Controller层查询全部返回给前端的是集合,遍历之后才能进行解析得到数据
前端收到的数据个数太多,需要进行统一之后交给前端人员
解决方案:
前端接受数据格式-----创建结果模型类,封装数据到data属性中
前端后端数据传输协议
采用data封装返回的数据,code表示操作返回的状态(以0结尾表示失败,以1结尾表示成功),msg用来传错误信息
每种操作下数据格式发生变化:
- 增删改
- 查单条
- 查全部
注:上图中数据返回结果类中封装三个属性
- data:封装返回的数据
- code:封装返回状态码(操作结果),即何种操作及是否操作成功。这里的编码可以根据自己的需求进行定制,怎么写都是对的
- msg:封装特殊消息,如操作失败后为了封装返回的错误信息
设置统一数据返回结果类Result类如下:
Result类中的字段并不是固定的,可以根据需要自行增减
提供若干个构造方法,方便操作
2.2 统一数据返回结果类实现
①在controller包下创建Result类
构造方法可以提供多个以便于返回数据,具体写哪个构造方法写几个构造方法可以根据需求来
public class Result {
private Object data;
private Integer code;
private String msg;
public Object getData() {
return data;
}
//带消息
public Result(Integer code, Object data, String msg) {
this.data = data;
this.code = code;
this.msg = msg;
}
//不带消息
public Result(Integer code,Object data) {
this.data = data;
this.code = code;
}
public Result() {
}
public void setData(Object data) {
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
②定义返回状态码Code类
//Code类定义返回状态码,1结尾是成功,0结尾是失败
public class Code {
public static final Integer SAVE_OK = 20011;
public static final Integer DELETE_OK = 20021;
public static final Integer UPDATE_OK = 20031;
public static final Integer GET_OK = 20041;
public static final Integer SAVE_ERR = 20010;
public static final Integer DELETE_ERR = 20020;
public static final Integer UPDATE_ERR = 20030;
public static final Integer GET_ERR = 20040;
}
注:Code类的常量设计也不是固定的,可以根据需要自行增减,例如将查询再细分为GET_OK,GET_ALL_OK,GET_PAGE_OK
③修改BookController类的返回值
过去BookController中增删改查返回值:
@RestController
@RequestMapping("/books")
public class BookController {
@Autowired
private BookService bookService;
@PostMapping
public boolean save(@RequestBody Book book) {
return bookService.save(book);
}
@PutMapping
public boolean update(@RequestBody Book book) {
return bookService.update(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
public List<Book> getAll() {
return bookService.getAll();
}
}
现在我们已经对结果类进行了封装并定义了成功或失败的状态码,我们就需要对BookController中的增删改查方法的返回值进行修改,如下
//统一每一个控制器方法返回值
@RestController
@RequestMapping("/books")
public class BookController {
@Autowired
private BookService bookService;
@PostMapping
public Result save(@RequestBody Book book) {
boolean flag = bookService.save(book);
return new Result(flag ? Code.SAVE_OK:Code.SAVE_ERR,flag);
}
@PutMapping
public Result update(@RequestBody Book book) {
boolean flag = bookService.update(book);
return new Result(flag ? Code.UPDATE_OK:Code.UPDATE_ERR,flag);
}
@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id) {
boolean flag = bookService.delete(id);
return new Result(flag ? Code.DELETE_OK:Code.DELETE_ERR,flag);
}
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id) {
Book book = bookService.getById(id);
Integer code = book != null ? Code.GET_OK : Code.GET_ERR;
String msg = book != null ? "" : "数据查询失败,请重试!";
return new Result(code,book,msg);
}
@GetMapping
public Result getAll() {
List<Book> bookList = bookService.getAll();
Integer code = bookList != null ? Code.GET_OK : Code.GET_ERR;
String msg = bookList != null ? "" : "数据查询失败,请重试!";
return new Result(code,bookList,msg);
}
}
返回值类型要修改为Result,每个操作最终都通过new Result()封装为Result对象进行返回
3 异常处理
程序开发过程中不可避免遇到异常现象
出现异常现象的常见位置与常见诱因如下:
- 框架内部抛出的异常:因使用不合规导致
- 数据层抛出的异常:因外部服务器故障导致(例如:服务器访问超时)
- 业务层抛出的异常:因业务逻辑书写错误导致(例如:遍历业务书写操作,导致索引异常等)
- 表现层抛出的异常:因数据收集、校验等规则导致(例如:不匹配的数据类型间导致异常)
- 工具类抛出的异常:因工具类书写不严谨不够健壮导致(例如:必要释放的连接长期未释放等)
思考:
- 各个层级均出现异常,异常处理代码书写再哪一层?
所有异常都抛到表现层进行处理 - 表现层处理异常,每个方法中单独书写,代码书写量巨大且意义不大,如何解决?-----AOP思想
3.1 SpringMVC统一异常处理器
针对代码正常执行前端得到数据,而抛出异常时没有给前端反馈数据的情况,物品们使用异常处理器来统一地返回数据
异常处理器可以集中、统一地处理项目中的异常
具体格式如下:
3.1 异常处理器代码实现
流程如下:
①创建异常处理器类
这里要保证SpringMVC能够扫到异常处理类
//@RestControllerAdvice用于标识当前类为REST风格对应的异常处理器
@RestControllerAdvice
public class ProjectExceptionAdvice {
//@ExceptionHandler异常处理器
@ExceptionHandler(Exception.class)
public Result doException(Exception ex){
System.out.println("嘿嘿,异常你哪里跑!")
return new Result(666,null,"嘿嘿,异常你哪里跑!");
}
}
②添加int i = 1/0使程序抛异常
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id) {
int i = 1/0;
Book book = bookService.getById(id);
Integer code = book != null ? Code.GET_OK : Code.GET_ERR;
String msg = book != null ? "" : "数据查询失败,请重试!";
return new Result(code,book,msg);
}
③测试异常拦截是否成功
小结:
名称:@RestControllerAdvice
类型:类注解
位置:Rest风格开发的控制器增强类定义上方
作用:为Rest风格开发的控制器做增强
范例:
说明:此注解自带@ResponseBody注解与@Component注解,具备对应的功能
名称: @ExceptionHandler
类型:方法注解
位置:专用于异常处理的控制器方法上方
作用:设置指定异常的处理方案,功能等同于控制器方法,出现异常后终止原始控制器执行,并转入当前方法执行
范例:
说明:此类方可以根据处理的异常不同,制作多个方法分别处理对应的异常
4 项目处理异常方案
上面完成了异常处理器的创建流程,那么数据层、业务层的异常又是如何交到异常处理器里呢?
项目异常分类
- 业务异常
- 用户行为
注:第二、第三个请求分别由不规范的用户行为操作产生的异常和规范的用户行为产生的异常
- 系统异常
项目运行过程中可预计但无法避免的异常,比如数据库或服务器宕机
-
其它异常
编程人员未预期到的异常,如:用到的文件不存在
4.1 项目异常处理方案
- 业务异常(BusinessException)
- 发送对应消息传递给用户,提醒规范操作
- 举例:大家常见的就是提示用户名已存在或密码格式不正确等
- 发送对应消息传递给用户,提醒规范操作
- 系统异常(SystemException)
- 发送固定消息传递给用户,安抚用户
- 系统繁忙,请稍后再试
- 系统正在维护升级,请稍后再试
- 系统出问题,请联系系统管理员等
- 发送特定消息给运维人员,提醒维护
- 举例:可以发送短信、邮箱或者是公司内部通信软件
- 记录日志
- 发消息和记录日志对用户来说是不可见的,属于后台程序
- 其他异常(Exception)
- 发送固定消息传递给用户,安抚用户
- 发送特定消息给编程人员,提醒维护(纳入预期范围内)
- 一般是程序没有考虑全,比如未做非空校验等
- 记录日志
4.2 项目异常处理实现
步骤一:创建一个exception包,包下定义业务异常和系统异常的类,并定义code成员变量,生成getter和setter方法以及用到的构造方法。
①系统异常类
//自定义业务异常类,继承运行时异常RuntimeException,用于封装异常信息,对异常进行分类
public class SystemException extends RuntimeException{
private Integer code;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public SystemException(Integer code, String message) {
super(message);
this.code = code;
}
public SystemException(Integer code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
}
②业务异常类
//自定义业务异常类,继承运行时异常RuntimeException,用于封装异常信息,对异常进行分类
public class BusinessException extends RuntimeException{
private Integer code;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
public BusinessException(Integer code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
}
注:
- 让自定义异常类继承
RuntimeException
的好处是,后期在抛出这两个异常的时候,就不需要try...catch...或throws了- 自定义异常类中添加
code
属性的原因是为了更好的区分异常来自哪个业务
步骤二:增加异常编码定义
//状态码
public class Code {
public static final Integer SAVE_OK = 20011;
public static final Integer DELETE_OK = 20021;
public static final Integer UPDATE_OK = 20031;
public static final Integer GET_OK = 20041;
public static final Integer SAVE_ERR = 20010;
public static final Integer DELETE_ERR = 20020;
public static final Integer UPDATE_ERR = 20030;
public static final Integer GET_ERR = 20040;
public static final Integer SYSTEM_ERR = 50001;
public static final Integer SYSTEM_TIMEOUT_ERR = 50002;
public static final Integer SYSTEM_UNKNOWN_ERR = 59999;
public static final Integer BUSINESS_ERR = 60002;
}
注:在实际开发时,将系统异常通过AOP统一处理,对于业务异常需要具体处理
步骤三:模拟异常,触发自定义异常
public Book getById(Integer id) {
//模拟业务异常,包装成自定义异常
if(id == 1){
throw new BusinessException(Code.BUSINESS_ERR,"请不要使用你的技术挑战我的耐性!");
}
//模拟系统异常,将可能出现的异常进行包装,转换成自定义异常
try{
int i = 1/0;
}catch (Exception e){
throw new SystemException(Code.SYSTEM_TIMEOUT_ERR,"服务器访问超时,请重试!",e);
}
return bookDao.getById(id);
}
步骤四:处理器拦截异常并处理
//@RestControllerAdvice用于标识当前类为REST风格对应的异常处理器
@RestControllerAdvice
public class ProjectExceptionAdvice {
//@ExceptionHandler用于设置当前处理器类对应的异常类型
@ExceptionHandler(SystemException.class)
public Result doSystemException(SystemException ex){
//记录日志
//发送消息给运维
//发送邮件给开发人员,ex对象发送给开发人员
return new Result(ex.getCode(),null,ex.getMessage());
}
@ExceptionHandler(BusinessException.class)
public Result doBusinessException(BusinessException ex){
return new Result(ex.getCode(),null,ex.getMessage());
}
//除了自定义的异常处理器,保留对Exception类型的异常处理,用于处理非预期的异常
@ExceptionHandler(Exception.class)
public Result doOtherException(Exception ex){
//记录日志
//发送消息给运维
//发送邮件给开发人员,ex对象发送给开发人员
return new Result(Code.SYSTEM_UNKNOW_ERR,null,"系统繁忙,请稍后再试!");
}
}
小结:
这样一来,无论后台哪一层发生异常,都会以我们与前端约定好的方式进行返回,前端只需要把信息获取到,根据返回的正确与否来展示不同的内容即可。
每一层都会往上抛异常,到Controller层抛给异常处理类进行统一的异常处理,然后将结果按结果类封装发送给前端
5 前后台协议联调
5.1 环境准备
- 创建一个Web的Maven项目
- pom.xml添加SSM整合所需jar包
- 创建对应的配置类
- 编写Controller、Service接口、Service实现类、Dao接口和模型类
- resources下提供jdbc.properties配置文件
项目结构如下:
将资料\SSM功能页面
下面的静态资源拷贝到webapp下。
-
因为添加了静态资源,SpringMVC会拦截,所有需要在SpringConfig的配置类中将静态资源进行放行。
-
新建SpringMvcSupport,并将其设置为配置类即添加@Configuration注解
@Configuration public class SpringMvcSupport extends WebMvcConfigurationSupport { @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/pages/**").addResourceLocations("/pages/"); registry.addResourceHandler("/css/**").addResourceLocations("/css/"); registry.addResourceHandler("/js/**").addResourceLocations("/js/"); registry.addResourceHandler("/plugins/**").addResourceLocations("/plugins/"); } }
-
设置为配置类后,在SpringMvcConfig中扫描添加扫描SpringMvcSupport所在包
@Configuration @ComponentScan({"com.itheima.controller","com.itheima.config"}) @EnableWebMvc public class SpringMvcConfig { }
下面就是增删改查功能的逐一实现
5.2 列表功能
分析:我们需要在页面加载完后发送Ajax异步请求到后台获取列表数据并将数据展示在页面上。
1.找到页面的钩子函数,created()
2.created()方法中调用了this.getAll()方法
3.在getAll()方法中使用axios发送异步请求从后台获取数据
4.访问的路径为http://localhost/books
5.返回数据
实现代码:
getAll() {
//发送ajax请求
axios.get("/books").then((res)=>{
this.dataList = res.data.data;
});
}
前端页面展示:
5.3 添加功能
分析:我们需要完成图片的添加功能模块
1.找到页面上的新建按钮,按钮上绑定了@click="handleCreate()"方法
2.在method中找到handleCreate方法,方法中打开新增面板
3.新增面板中找到确定按钮,按钮上绑定了@click="handleAdd()"方法
4.在method中找到handleAdd方法
5.在方法中发送请求和数据,响应成功后将新增面板关闭并重新查询数据
代码实现:
handlecreate()用来打开添加面板
handleCreate() {
this.dialogFormVisible = true;
},
handleAdd()用来发送异步请求
handleAdd () {
//发送ajax请求
//this.formData是表单中的数据,最后是一个json数据
axios.post("/books",this.formData).then((res)=>{
this.dialogFormVisible = false;
this.getAll();
});
}
上述代码虽然完成了添加的基础功能,添加成功会关闭面板,重新查询数据,但仍然需要进行优化:
添加失败以后该如何处理?
1.在handlerAdd()中根据后台返回的数据对操作执行的不同情况进行不同的处理
2.如果后台返回的是成功,则提示成功信息,并关闭面板
3.如果后台返回的是失败,则提示错误信息
handleAdd () {
//发送ajax请求
axios.post("/books",this.formData).then((res)=>{
//如果操作成功,关闭弹层,显示数据
if(res.data.code == 20011){ //SAVE_OK
this.dialogFormVisible = false;
this.$message.success("添加成功");
}else if(res.data.code == 20010){ //SAVE_ERR
this.$message.error("添加失败");
}else{ //出现异常
this.$message.error(res.data.msg);
}
}).finally(()=>{
this.getAll();
});
}
①修改前端页面显示内容,使得在添加弹窗出现时刷新添加表单的内容,防止上次添加的内容在下次添加时存在
//弹出添加窗口
handleCreate() {
this.dialogFormVisible = true;
//重置表单
this.resetForm();
},
② 由于之前案例中Dao层返回值人为地设置为true,这样无法获取增删改操作的返回值,也就无法确定增删改操作的结果,从而难以根据操作结果像前端反馈
因此,我们需要将Dao层增删改操作的返回值由void改为int,这里的int即为数据库操作的影响行数
public interface BookDao {
// @Insert("insert into tbl_book values(null,#{type},#{name},#{description})")
@Insert("insert into tbl_book (type,name,description) values(#{type},#{name},#{description})")
public int save(Book book);
@Update("update tbl_book set type = #{type}, name = #{name}, description = #{description} where id = #{id}")
public int update(Book book);
@Delete("delete from tbl_book where id = #{id}")
public int delete(Integer id);
@Select("select * from tbl_book where id = #{id}")
public Book getById(Integer id);
@Select("select * from tbl_book")
public List<Book> getAll();
}
这样我们就可以拿到增删改操作的返回值,然后根据返回值来确定增删改操作成功或者失败,也就是判断影响的行数受否大于0
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
public boolean save(Book book) {
return bookDao.save(book) > 0;
}
public boolean update(Book book) {
return bookDao.update(book) > 0;
}
public boolean delete(Integer id) {
return bookDao.delete(id) > 0;
}
public Book getById(Integer id) {
if(id == 1){
throw new BusinessException(Code.BUSINESS_ERR,"请不要使用你的技术挑战我的耐性!");
}
// //将可能出现的异常进行包装,转换成自定义异常
// try{
// int i = 1/0;
// }catch (Exception e){
// throw new SystemException(Code.SYSTEM_TIMEOUT_ERR,"服务器访问超时,请重试!",e);
// }
return bookDao.getById(id);
}
public List<Book> getAll() {
return bookDao.getAll();
}
}
这里添加失败可以通过字符的长度来完成
5.4 修改功能
分析:
我们需要完成对单条图书信息进行修改的功能
1.找到页面中的编辑按钮,该按钮绑定了@click="handleUpdate(scope.row)"
2.在method的handleUpdate方法中发送异步请求根据ID查询图书信息
3.根据后台返回的结果,判断是否查询成功
如果查询成功打开修改面板回显数据,如果失败提示错误信息
4.修改完成后找到修改面板的确定按钮,该按钮绑定了@click="handleEdit()"
5.在method的handleEdit方法中发送异步请求提交修改数据
6.根据后台返回的结果,判断是否修改成功
如果成功提示错误信息,关闭修改面板,重新查询数据,如果失败提示错误信息
scope.row代表的是当前行的行数据,也就是说,scope.row就是选中行对应的json数据,如下:
{
"id": 1,
"type": "计算机理论",
"name": "Spring实战 第五版",
"description": "Spring入门经典教程,深入理解Spring原理技术内幕"
}
这里需要注意的是弹出窗口需要将根据id查询然后将需要修改的信息展示在弹窗中
//弹出编辑窗口
handleUpdate(row) {
// console.log(row); //row.id 查询条件
//查询数据,根据id查询
axios.get("/books/"+row.id).then((res)=>{
if(res.data.code == 20041){
//展示弹层,加载数据
this.formData = res.data.data;
this.dialogFormVisible4Edit = true;
}else{
this.$message.error(res.data.msg);
}
});
}
上面的handleUpdate()是弹出编辑窗口方法,我们还需要在handleEdit()中发送Ajax请求,使用put(),这里是dialogFormVisible4Edit定义显示弹窗,与添加功能区分开
handleEdit() {
//发送ajax请求
axios.put("/books",this.formData).then((res)=>{
//如果操作成功,关闭弹层,显示数据
if(res.data.code == 20031){
this.dialogFormVisible4Edit = false;
this.$message.success("修改成功");
}else if(res.data.code == 20030){
this.$message.error("修改失败");
}else{
this.$message.error(res.data.msg);
}
}).finally(()=>{
this.getAll();
});
}
5.6 删除功能
我们最后实现以下删除功能:
分析:
我们需要完成单条图书信息的删除功能。
1.找到页面的删除按钮,按钮上绑定了@click="handleDelete(scope.row)"
2.method的handleDelete方法弹出提示框
3.用户点击取消,提示操作已经被取消。
4.用户点击确定,发送异步请求并携带需要删除数据的主键ID
5.根据后台返回结果做不同的操作
如果返回成功,提示成功信息,并重新查询数据;如果返回失败,提示错误信息,并重新查询数据
首先,我们需要弹出提示框,防止用户误操作导致重要数据丢失,然后做删除业务,这里需要注意的是我们还需要完成取消删除操作,这里在取消删除操作弹出消息提示取消删除
handleDelete(row) {
//1.弹出提示框
this.$confirm("此操作永久删除当前数据,是否继续?","提示",{
type:'info'
}).then(()=>{
//2.做删除业务
axios.delete("/books/"+row.id).then((res)=>{
if(res.data.code == 20021){
this.$message.success("删除成功");
}else{
this.$message.error("删除失败");
}
}).finally(()=>{
this.getAll();
});
}).catch(()=>{
//3.取消删除
this.$message.info("取消删除操作");
});
}