文章目录
- 1. 引言
- 2. 创建工程
- 3. SSM 整合
- 2. 统一结果封装
- 3. 统一异常处理
- 3.1 异常处理器
- 3.2 项目异常处理方案
- 3.2.1 异常分类
- 3.2.2 异常解决方案
- 3.2.3 异常解决方案的具体实现
- 4. 前后台协议联调
- 4.1 列表功能
- 4.2 添加功能
- 4.3 修改功能
- 4.4 删除功能
- 5. 拦截器
- 5.1 拦截器概念
1. 引言
前面已经学习了 Mybatis、Spring 和 SpringMVC 三个框架,现在要把这三个框架整合在一起,完成业务功能开发,整合流程如下:
2. 创建工程
(1) 新建 module
(2) 选择模板
(3) module 名称和路径
(4) 补充项目结构
(5) 新建必要的包
3. SSM 整合
Spring 配置类
@Configuration
@ComponentScan("com.itheima.service")
@PropertySource("jdbc.properties")
@Import({JdbcConfig.class, MybatisConfig.class})
public class SpringConfig {
}
不检查 bean 自动装配的语法:
2. 统一结果封装
如果后端返回的数据类型很杂乱,前端解析数据会很麻烦。所以后端要能够返回统一的数据类型。
所以就想能不能将返回结果的数据进行统一,思路为:
创建结果模型类,将返回的结果数据封装到 data 属性中;
将返回的数据是何种操作及是否操作成功封装到 code 属性中;
将操作失败后返回的错误信息封装到 msg 属性中
具体步骤如下:
(1) 设置统一数据返回结果类
public class Result {
//返回的数据是何种操作及是否操作成功
private Integer code;
//返回的结果数据
private Object data;
//操作失败后返回的错误信息,可选属性
private String msg;
//添加构造方法,方便设置属性,set太麻烦
public Result() {
}
//不带消息的构造方法
public Result(Integer code, Object data) {
this.code = code;
this.data = data;
}
//带消息的构造方法
public Result(Integer code, Object data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
}
//setter...getter...省略
}
注意:Result 类名及类中的字段并不是固定的,可以根据需要自行增减提供若干个构造方法,方便操作。
(2) 定义返回码 Code 类
public class Code {
//增删改查分别为2001、2002、2003、2004
//成功为1,失败为2
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 等。
(3) 修改 Controller 类的返回值
@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);
int code = (book!=null) ? Code.GET_OK : Code.GET_ERR;
String msc = (book!=null) ? "" : "查询失败,请重试";
return new Result(code,book,msc);
}
@GetMapping
public Result getAll() {
List<Book> bookList = bookService.getAll();
// bookList里面没有东西不是查询失败,是真的没有
// bookList为空才是查询失败
int code = (bookList!=null) ? Code.GET_OK : Code.GET_ERR;
String msc = (bookList!=null) ? "" : "查询失败,请重试";
return new Result(code,bookList,msc);
}
}
3. 统一异常处理
3.1 异常处理器
异常的种类及出现异常的原因:
- 框架内部抛出的异常:因使用不合规导致
- 数据层抛出的异常:因外部服务器故障导致(例如:服务器访问超时)
- 业务层抛出的异常:因业务逻辑书写错误导致(例如:遍历业务书写操作,导致索引异常等)
- 表现层抛出的异常:因数据收集、校验等规则导致(例如:不匹配的数据类型间导致异常)
- 工具类抛出的异常:因工具类书写不严谨不够健壮导致(例如:必要释放的连接长期未释放等)
看完上面这些出现异常的位置,会发现,在任何一个位置都有可能出现异常,而且这些异常是不能避免的。所以就得处理异常。
思考:
- 各个层级均可能出现异常,异常处理代码书写在哪一层?
所有的异常均抛出到表现层进行处理(数据层抛到业务层,业务层抛到表现层) - 异常的种类很多,表现层如何将所有的异常都处理到呢?
异常分类 - 表现层处理异常,如果用 try-catch 写,代码量巨大且意义不强,如何解决?
AOP
对于上面这些问题及解决方案,SpringMVC 提供了一套解决方案:
异常处理器:集中统一地处理项目中出现的异常。
(1) 创建异常处理器类
//当前类为REST风格对应的异常处理器
//被SpringMvcConfig中的@ComponentScan("com.itheima.controller")扫描到
@RestControllerAdvice
public class ProjectExceptionAdvice {
//拦截所有类型的异常
@ExceptionHandler(Exception.class)
public Result doException(Exception ex){
return new Result(666, null);
}
}
一定要确保 SpringMvcConfig 能够扫描到异常处理器类。
(2) 让程序抛出异常
修改 BookController 的 getById 方法,添加 int i = 1/0。
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id) {
int i = 1/0;
Book book = bookService.getById(id);
int code = (book!=null) ? Code.GET_OK : Code.GET_ERR;
String msc = (book!=null) ? "" : "查询失败,请重试";
return new Result(code,book,msc);
}
启动运行程序,测试:
3.2 项目异常处理方案
3.2.1 异常分类
异常处理器已经能够使用了,那么在项目中该如何来处理异常呢?
因为异常的种类有很多,如果每个异常都对应一个@ExceptionHandler,那就要写很多个方法来处理各自的异常,所以在处理异常之前,需要对异常进行一个分类:
(1) 业务异常(BusinessException)
- 规范的用户行为产生的异常:用户在页面输入内容的时候未按照指定格式进行数据填写,如在年龄框输入的是字符串。
- 不规范的用户行为操作产生的异常:如用户故意传递错误数据
(2) 系统异常(SystemException)
- 项目运行过程中可预计但无法避免的异常:如数据库或服务器宕机
(3) 其他异常(Exception)
- 编程人员未预期到的异常,如:用到的文件不存在
将异常分类以后,针对不同类型的异常,要提供具体的解决方案。
3.2.2 异常解决方案
(1) 业务异常(BusinessException)
- 发送对应消息传递给用户,提醒规范操作
常见的就是提示用户名已存在、密码格式不正确等
(2) 系统异常(SystemException)
- 发送固定消息传递给用户,安抚用户
- 系统繁忙,请稍后再试
- 系统正在维护升级,请稍后再试
- 系统出问题,请联系系统管理员等
- 发送特定消息给运维人员,提醒维护
- 可以发送短信、邮箱或者是公司内部通信软件
- 记录日志
发消息和记录日志对用户来说是不可见的,属于后台程序。
(3) 其他异常(Exception)
- 发送固定消息传递给用户,安抚用户
- 发送特定消息给编程人员,提醒维护(纳入预期范围内)
- 一般是程序没有考虑全,比如未做非空校验等
- 记录日志
3.2.3 异常解决方案的具体实现
- 先通过自定义异常,完成 BusinessException 和SystemException 的定义
- 将其他异常包装成自定义异常类型
- 在异常处理器类中对不同的异常进行处理
(1) 自定义异常类
//运行时异常可以不处理,自动向上抛。
//如果该类不继承RuntimeException,该类下的每个方法都要throws SystemException
public class SystemException extends RuntimeException{
//出异常之后,加个编号,来帮助识别是哪一种异常
private Integer code;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
//加上需要的构造方法
//alt+insert会出来很多构造方法,选择需要的即可,也可以都加
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;
}
}
BusinessException.java 由 SystemException.java 复制粘贴就行,会自动更改构造方法名。
//运行时异常可以不处理,自动向上抛。
//如果该类不继承RuntimeException,该类下的每个方法都要throws SystemException
public class BusinessException extends RuntimeException{
//出异常之后,加个编号,来帮助识别是哪一种异常
private Integer code;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
//加上需要的构造方法
//alt+insert会出来很多构造方法,选择需要的即可,也可以都加
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;
}
}
(2) 将其他异常包成自定义异常
具体的包装方式有:
- 方式 1:直接 throw 自定义异常。
- 方式 2:try-catch,在 catch 中重新 throw 自定义的异常。
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id) {
//模拟业务异常,使用方式 1
if (id == 1){
throw new BusinessException(Code.BUSINESS_ERR, "请不要使用你的技术挑战我的耐心!");
}
//模拟系统异常,使用方式 2
try {
int i = 1/0;
}catch (Exception e){
throw new SystemException(Code.SYSTEM_TIMEOUT_ERR, "服务器访问超时,请重试!", e);
}
Book book = bookService.getById(id);
int code = (book!=null) ? Code.GET_OK : Code.GET_ERR;
String msc = (book!=null) ? "" : "查询失败,请重试";
return new Result(code,book,msc);
}
上面为了使 code 看着更专业些,在 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_UNKNOW_ERR = 59999;
public static final Integer BUSINESS_ERR = 60002;
}
(3) 拦截并处理自定义异常
//当前类为REST风格对应的异常处理器
//被SpringMvcConfig中的@ComponentScan("com.itheima.controller")扫描到
@RestControllerAdvice
public class ProjectExceptionAdvice {
//拦截系统异常
@ExceptionHandler(SystemException.class)
public Result doException(SystemException ex){
//记录日志
//发送消息给运维
//发送邮件给开发人员,ex对象发送给开发人员
return new Result(ex.getCode(), null, ex.getMessage());
//getMessage是继承得来的
}
//拦截业务异常
@ExceptionHandler(BusinessException.class)
public Result doException(BusinessException ex){
return new Result(ex.getCode(), null, ex.getMessage());
//getMessage是继承得来的
}
//拦截其他未知的异常
@ExceptionHandler(Exception.class)
public Result doException(Exception ex){
//记录日志
//发送消息给运维
//发送邮件给开发人员,ex对象发送给开发人员
return new Result(Code.SYSTEM_UNKNOW_ERR, null, "系统繁忙,请稍后再试");
}
}
运行测试:
(1) 测试业务异常
(2) 测试系统异常
4. 前后台协议联调
4.1 列表功能
4.2 添加功能
新增时首先显示窗口,清理表单数据,然后添加表单中填写的数据。
//弹出添加窗口
handleCreate() {
this.dialogFormVisible = true;
this.resetForm();
},
//重置表单
resetForm() {
this.formData = {};
},
//添加
handleAdd () {
//发送ajax请求
axios.post("/books",this.formData).then((res)=>{
//如果操作成功,关闭弹层,显示数据
if(res.data.code == 20011){
this.dialogFormVisible = false;
this.$message.success("添加成功");
}else if(res.data.code == 20010){
this.$message.error("添加失败");
}else{//其他:如服务器问题
this.$message.error(res.data.msg);
}
}).finally(()=>{
this.getAll();
});
},
4.3 修改功能
//弹出编辑窗口
handleUpdate(row) {
//查询数据,根据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);
}
});
},
//编辑
handleEdit() {
//发送ajax请求
axios.put("/books",this.formData).then((res)=>{
console.log(res.data);
//如果操作成功,关闭弹层,显示数据
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();
});
},
4.4 删除功能
// 删除
handleDelete(row) {
//弹出提示框
this.$confirm("此操作永久删除当前数据,是否继续?", "提示", {
type: 'info'
}).then(()=>{//确定时
//做删除业务
axios.delete("/books/"+row.id).then((res)=>{
if(res.data.code == 20021){
this.getAll();
this.$message.success("删除成功");
}else{
this.$message.error("删除失败");
}
});
}).catch(()=>{//取消时
//取消删除
this.$message.info("取消删除操作");
});
}
5. 拦截器
5.1 拦截器概念
讲解拦截器的概念之前,先看一张图:
(1) 浏览器发送一个请求会先到 Tomcat 的 web 服务器
(2) Tomcat 服务器接收到请求以后,会去判断请求的是静态资源还是动态资源
(3) 如果是静态资源,会直接到 Tomcat 的项目部署目录下去直接访问
(4) 如果是动态资源,就需要交给项目的后台代码进行处理
(5) 在找到具体的方法之前,可以去配置过滤器(可以配置多个),按照顺序进行执行
(6) 然后进入到到中央处理器(SpringMVC 中的内容),SpringMVC 会根据配置的规则进行拦截
(7) 如果满足规则,则进行处理,找到其对应的 controller 类中的方法进行执行,完成后返回结果
(8) 如果不满足规则,则不进行处理
(9) 这个时候,如果需要在每个 Controller 方法执行的前后添加业务,具体该如何来实现?这个就是拦截器要做的事
拦截器(Interceptor)是一种动态拦截方法调用的机制,在 SpringMVC 中动态拦截控制器方法的执行作用:在指定的方法调用前后执行预先设定的代码阻止原始方法的执行总结:拦截器就是用来做增强看完以后,大家会发现