前言
大家你好! 这是我的第一篇博客 ,我会把我所学所悟的知识以最简单的语言描述清楚,让大家通俗易懂。
正文
下面我要对springboot(1.5.8.RELEASE)中异常拦截处理进行讲解。项目中我们是一定要碰到的情况就是无论在控制层,业务层还是Dao层都需要校验一些数据,无论是前端传过来的,还是经过业务处理判断的,如果不合法需要友好的提示给用户,否则用户收到一个NullPointerException这玩意,他可能会很懵逼,再说直接将错误的信息直接暴露给用户,这样的体验可想而知,且对黑客而言,详细异常信息往往会提供非常大的帮助…
下面我只讲解
一.校验Body中的参数校验友好提示给用户。
二.手动抛出异常信息友好提示给用户。
1.由于笔者用的是SpringCloud,首先要有一个Eureka服务,下面分别是eureka服务的启动类和配置,很简单,这里不做详细解释。
//eureka服务启动类
@SpringBootApplication
@EnableEurekaServer
public class EurekaApp {
public static void main(String[] args) {
SpringApplication.run(EurekaApp.class, args);
}
}
#注册中心端口号
server.port=1111
#注册中的地址
eureka.instance.hostname=localhost
#是否开启基本的鉴权,默认为true
security.basic.enabled=true
#指定默认的用户名,默认为user.
security.user.name=admin
#默认的用户密码.
security.user.password=admin
#表示是否将自己注册到Eureka Server,默认为true
eureka.client.register-with-eureka=false
#表示是否从Eureka Server获取注册信息,默认为true
eureka.client.fetch-registry=false
#设置与Eureka Server交互的地址,查询服务和注册服务都需要依赖这个地址。默认是http://localhost:8761/eureka ;多个地址可使用 , 分隔。
eureka.client.service-url.defaultZone=http://${security.user.name}:${security.user.password}@${eureka.instance.hostname}:${server.port}/eureka/
#实例名称显示IP配置
eureka.instance.perfer-ip-address=true
# 设为false,关闭自我保护
eureka.server.enable-self-preservation=false
# 清理间隔(单位毫秒,默认是60*1000)
#启用主动失效,并且每次主动失效检测间隔为3s
eureka.server.eviction-interval-timer-in-ms=10000
2.创建一个module,名字:eureka-client ,作为一个服务(为了方便讲解,我把所有的异常配置都写在这个项目里)
eureka-client服务的application.properties文件
eureka.client.service-url.defaultZone=http://admin:admin@localhost:1111/eureka/
server.port=8762
spring.application.name=eureka-client
management.security.enabled=false
下面是 eureka-client服务的启动类。
/**
* @author yanlin
* @version v1.2
* @date 2018-08-16 下午2:21
* @since v8.0
**/
@SpringBootApplication
@EnableDiscoveryClient
@RestController
public class EurekaClientApp {
public static void main(String[] args) {
SpringApplication.run(EurekaClientApp.class, args);
}
@PostMapping("/ClientService/body")
public String testBodyE(@Valid @RequestBody Student student) {
return "访问无异常:" + student.toString();
}
@GetMapping("/ClientService/{name}")
public String test(@PathVariable("name") String name) {
if (name.equals("1")) {
throw new ParameterServiceException("这里填写错误代码,规范应是一个枚举", "描述当前错误原因,例如参数不能为空等");
}
return name;
}
下面把注册中心启动,然后启动eureka-client,由于笔者很懒,所以get请求一律在浏览器进行
请求 http://localhost:8762/ClientService/2 是ok的
用postman请求 http://localhost:8762/ClientService/body
下面是重点内容
在当前的服务右键new一个class 名字叫 GlobalDefultExceptionHandler,利用@RestControllerAdvice对全局异常进行拦截,然后利用@ExceptionHandler根据自己特定需要,配置对什么样的异常进行拦截处理,我只写了两种。
/**
* 统一拦截异常
*
* @author yanlin
* @version v1.3
* @date 2018-10-18 下午2:27
* @since v8.0
**/
@RestControllerAdvice
public class GlobalDefultExceptionHandler {
/**
* 处理参数异常,一般用于校验body参数
*
* @param e
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ErrorMessage handleValidationBodyException(MethodArgumentNotValidException e) {
for (ObjectError s : e.getBindingResult().getAllErrors()) {
return new ErrorMessage("Invalid_Request_Parameter", s.getObjectName() + ": " + s.getDefaultMessage());
}
return new ErrorMessage("Invalid_Request_Parameter", "未知参数错误");
}
/**
* 主动throw的异常
*
* @param e
* @param response
* @return
*/
@ExceptionHandler(ServiceException.class)
public ErrorMessage handleUnProccessableServiceException(ServiceException e, HttpServletResponse response) {
response.setStatus(e.getStatusCode().value());
return new ErrorMessage(e.getErrorCode(), e.getMessage());
}
}
下面我回贴出来四个类,大家一看便知
/**
* @author yanlin
* @version v1.3
* @date 2018-10-18 下午2:36
* @since v8.0
**/
public class ErrorMessage implements Serializable {
private static final long serialVersionUID = 8065583911104112360L;
private String errorCode;
private String errorMessage;
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public ErrorMessage(String errorCode, String errorMessage) {
super();
this.errorCode = errorCode;
this.errorMessage = errorMessage;
}
public ErrorMessage() {
super();
}
}
/**
* @author yanlin
* @version v1.3
* @date 2018-10-18 下午2:50
* @since v8.0
**/
public abstract class ServiceException extends RuntimeException {
private static final long serialVersionUID = 8109469326798389194L;
protected HttpStatus statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
private String errorCode;
public HttpStatus getStatusCode() {
return statusCode;
}
public void setStatusCode(HttpStatus statusCode) {
this.statusCode = statusCode;
}
public ServiceException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
}
/**
* @author yanlin
* @version v1.3
* @date 2018-10-18 下午3:00
* @since v8.0
**/
public class ParameterServiceException extends ServiceException {
private static final long serialVersionUID = 8362753245631601878L;
public ParameterServiceException(String errorCode, String message) {
super(errorCode, message);
this.statusCode = HttpStatus.UNPROCESSABLE_ENTITY;
}
}
/**
* @author yanlin
* @version v1.3
* @date 2018-10-18 下午3:16
* @since v8.0
**/
public class Student implements Serializable {
private static final long serialVersionUID = 7003907324788760110L;
@NotBlank(message = "姓名不能为空")
private String name;
@Min(value = 2, message = "不能小于2")
private String age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public Student() {
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}
这四个类我值得解释的是ParameterServiceException ,这是可以扩展的,根据你的需要。我这里描述的是参数异常的类,也就是你判断参数不合法时 throw new的那个类,在eureka-client服务的启动类我已经写了。
下面我要利用我定义好的这几个类演示一下我请求有异常是返回给用户的效果
首先是get请求,测试手动抛出异常,当我请求参数是 1 触发了我手动抛出的异常。
然后我利用postman测试post请求,我利用了javax.validation.constraints下面的注解校验的参数,上面方法的参数前一定要@Valid,否则你实体类里写的所有类似@NotNull(message = "姓名不能为空")这样的注解全部不生效。
这样前端在响应体里找到,就可以输出具体异常信息里了。
总结说明
以下是笔者关于这方面的经验和技巧分享给大家。
1.参数校验非法是一般使用手动抛出异常的方式告知前端,上面有代码贴出,如:throw new ParameterServiceException("这里填写错误代码,规范应是一个枚举", "描述当前错误原因,例如参数不能为空等");
2.业务代码块里尽量少出现try存在大量代码的情况,这样一旦某一行出错,类会跟随一张 异常表(exception table),每一个try catch都会在这个表里添加行记录,每一个记录都有4个信息(try catch的开始地址,结束地址,异常的处理起始位,异常类名称)。当代码在运行时抛出了异常时,首先拿着抛出位置到异常表中查找是否可以被catch(例如看位置是不是处于任何一栏中的开始和结束位置之间),如果可以则跑到异常处理的起始位置开始处理,如果没有找到则原地return,并且copy异常的引用给父调用方,接着看父调用的异常表。。。以此类推。这样的操作太多是对jvm处理造成资源浪费的,当然,经过多年的改进,异常对jvm性能的影响越来越小。
3.大家都知道所有的异常例如 NullPointerException都是继承 RuntimeException 而RuntimeException 又继承 Exception,所以,catch(Exception e){
e.printStackTrace();
}
这种写法是很不负责任的。
try {
//大量代码
}catch(Exception e){
e.printStackTrace();
}finally {
}
4.大家在对对象内参数校验对时候尽量利用javax.validation.constraints的注解,避免校验更多的参数时出现大量的代码,不宜阅读。
注:对本文有异议或不明白的地方微信探讨,wx:15524579896