1.通用异常处理
默认我们需要service层的异常需要抛出,而servcie层被Spring管理,默认Spring的事务回滚只对RunTimeException起作用,所以我们要把service层的编译器异常转换为运行时异常抛出。
只有这种异常不需要影响业务,或者通过这种异常可以执行其他业务,我们才需要try
1.1.场景预设
1.1.1.场景
我们预设这样一个场景,假如我们做新增商品,需要接收下面的参数:
price:价格
name:名称
然后对数据做简单校验:
- 价格不能为空
新增时,自动形成ID,然后随商品对象一起返回
1.1.2.代码
在ly-item-service中编写实体类:
@Data
public class Item {
private Integer id;
private String name;
private Long price;
}
在ly-item-service中编写业务:
service:
package com.leyou.item.service;
import com.leyou.item.pojo.Item;
import org.springframework.stereotype.Service;
@Service
public class ItemService {
public Item saveItem(Item item){
if(item.getPrice()==null){
throw new RuntimeException("价格不能为空");
}
item.setId(100);
return item;
}
}
- 这里先随表指定一个id,然后直接返回,没有做数据库操作
controller:
package com.leyou.item.controller;
import com.leyou.item.pojo.Item;
import com.leyou.item.service.ItemService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/item")
public class ItemController {
@Autowired
private ItemService itemService;
@PostMapping("/save")
public ResponseEntity<Item> save(Item item){
Item item1 = itemService.saveItem(item);
return ResponseEntity.status(200).body(item1);
}
}
1.2.1初步测试
现在我们启动项目,做下测试:
通过IDEA的HttpClient访问工具去访问:
发现参数不正确时,返回了500。看起来没问题
1.2.2.自定义异常
虽然上面返回没什么问题,但是500这个错误太过于暴露(服务器内部错误),我们想在出异常时自定义一个状态码,而这个状态码只需要前后端工作人员知道什么意思即可,不需要让外部人员知道。比如:
501 价格不能为空
400 无效的请求参数
404 XX不存在
401 登录失效或未登录
如果我们在抛异常时直接加状态码,会出现新的问题:
RuntimeException不支持这种写法,那我们就创建一个类继承RuntimeException,多添加一个参数
这个自定义的异常类会出现在多个微服务中,所以,定义在ly-common通用模块中
package com.leyou.common.exception;
import lombok.Getter;
@Getter
public class LyException extends RuntimeException {
private int status; //状态码
public LyException( int status,String message) {
super(message);
this.status = status;
}
}
修改ItemService中的方法
)]
重启测试发现,无用!状态码还是500,不是自己写的501
)]
这是因为,异常是抛了,但是没有没有捕获,controller层直接把异常抛到浏览器上去了,所以还需要自己统一捕获异常
1.2.3.统一捕获异常
Spring 在3.2版本后面增加了一个ControllerAdvice注解。
ControllerAdvice拆分开来就是Controller Advice,关于Advice,在Spring Aop中,其是用于封装一个切面所有属性的,包括切入点和需要织入的切面逻辑。
这里ContrllerAdvice也可以这么理解,其抽象级别应该是用于对Controller进行“切面”环绕的,而具体的业务织入方式则是通过结合其他的注解来实现的。
@ControllerAdvice是在类上声明的注解,其用法主要有三点:
-
结合方法型注解@ExceptionHandler,用于捕获Controller中抛出的指定类型的异常,从而达到不同类型的异常区别处理的目的;
-
结合方法型注解@InitBinder,用于request中自定义参数解析方式进行注册,从而达到自定义指定格式参数的目的;
-
结合方法型注解@ModelAttribute,表示其标注的方法将会在目标Controller方法执行之前执行。
@ControllerAdvice的用法基本是将其声明在某个bean上,然后在该bean的方法上使用其他的注解来指定不同的织入逻辑。不过这里@ControllerAdvice并不是使用AOP的方式来织入业务逻辑的,而是Spring内置对其各个逻辑的织入方式进行了内置支持。
接下来,我们使用SpringMVC提供的统一异常拦截器,因为是统一处理,我们放到ly-common
项目中:
新建一个类,名为:BasicExceptionAdvice
然后代码如下:
package com.leyou.common.advice;
import com.leyou.common.exception.LyException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@Slf4j
@ControllerAdvice
public class BasicExceptionAdvice {
@ExceptionHandler(LyException.class)
public ResponseEntity<String> handleException(LyException e){
e.printStackTrace(); //TODO 在开发阶段为了能更好的解决bug所以现在把bug的信息打印出来
//从LyException中获取信息
return ResponseEntity.status(e.getStatus()).body(e.getMessage());
}
}
解读:
@ControllerAdvice
:默认情况下,会拦截所有加了@Controller
的类
-
@ExceptionHandler(RuntimeException.class)
:作用在方法上,声明要处理的异常类型,可以有多个,这里指定的是RuntimeException
。被声明的方法可以看做是一个SpringMVC的Handler
:- 参数是要处理的异常,类型必须要匹配
- 返回结果可以是
ModelAndView
、ResponseEntity
等,基本与handler
类似
-
这里等于从新定义了返回结果,我们可以随意指定想要的返回类型。此处使用了String
此处使用了spring的注解,因此需要在ly-common中引入web依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
重启项目测试:
成功返回了错误信息!
很明显,成功地返回了一句话(响应体body中存放的内容),在响应的头信息中有我们想要的状态码,但是这两个信息不在一起,不方便前端人员解析,所以我们作为后台开发人员需要把状态码和错误信息描述存放到一起,顺便我们在加一个时间字段。我们可以定义一个对象,对象中有:状态码、错误信息描述、时间。这就叫做自定义异常结果。
1.2.4 自定义异常结果
在刚刚编写的BasicExceptionAdvice中,我们返回的结果是String,为了让异常结果更友好,我们不在返回在ly-common中定义异常结果对象:
package com.leyou.common.exceptions;
import lombok.Getter;
import org.joda.time.DateTime;
@Getter
public class ExceptionResult {
private int status;
private String message;
private String timestamp;
public ExceptionResult(LyException e) {
this.status = e.getStatus();
this.message = e.getMessage();
this.timestamp = DateTime.now().toString("yyyy-MM-dd HH:mm:ss");
}
}
这里使用了日期工具类:JodaTime,我们要引入依赖在ly-common中:
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
修改异常处理逻辑:
再次测试:完美了!
1.2.5.自定义异常枚举
刚才的处理看似完美,但是在我们会因为各种各样的情况抛异常,比如请求参数不正确时、用户名和密码错误时、要查询的内容不存在时、上传内容失败时、未登录时就下订单或支付时,遇到这些情况状态码或内容描述都应该怎么写,这些都是有严格要求的。
第一步:我们可以定义一个枚举,用于封装异常状态码和异常描述信息:
package com.leyou.common.enums;
@Getter
public enum ExceptionEnums {
ITEM_PRICE_NOT_NULL(501,"价格不能为空"),
UPLOAD_FILE_ERROR(502,"上传失败,请重试");
private int status;
private String message;
ExceptionEnums(int status, String message) {
this.status = status;
this.message = message;
}
}
第二步:修改ItemService抛异常的代码
第三步:在LyException中多添加一个构造方法
第四步:重启测试
1.3.异常枚举默认值
最后,这里给大家提供一个已经写好大量异常信息的枚举类:
package com.leyou.common.enums;
@Getter
public enum ExceptionEnum {
DATA_TRANSFER_ERROR(5000, "数据转换异常!"),
INSERT_OPERATION_FAIL(5000, "新增操作失败!"),
UPDATE_OPERATION_FAIL(5000, "更新操作失败!"),
DELETE_OPERATION_FAIL(5000, "删除操作失败!"),
UNAUTHORIZED(4001, "登录失效或未登录!");
private int status;
private String message;
ExceptionEnum(int status, String message) {
this.status = status;
this.message = message;
}
}
1.4
1、ResponseEntity 的优点:可以定义返回的状态码
2、抛异常时也想自定义状态码 需要自定义一个异常类LyException
3、发现项目没有捕获到LyException,所以定义统一的异常处理类BasicExceptionAdvice
4、使用BasicExceptionAdvice捕获后的结果错误信息和状态码没有在一起,
所以我们又定义一个返回结果类ExceptionResult包含状态码、错误信息、时间
5、返回的状态码和错误信息不能随便写,所以要定义枚举类