关注微信公众号【
Java之言
】,更多干货文章
和学习资料
,助你放弃编程之路!
在平常项目开发过程中,程序难免会出现运行时异常,或者业务异常。难道要针对每一处可能出现的异常进行编写代码进行处理?或者直接不处理异常,将一大屏堆满英文的异常信息显示给用户?那用户体验性是何等极差。
所以,当程序抛异常时,为了日志的可读性
,排查 Bug简单
,以及更好的用户体验性
,所以我们要对全局异常进行处理。
- JDK 1.8 或者1.8以上
- Springboot (此演示版本为 Springboot 2.1.18.RELEASE)
- Gradle (当然也可用Maven,其实目的都是为构建项目,管理依赖等)
plugins {
id "org.springframework.boot" version "2.1.18.RELEASE"
id "io.spring.dependency-management" version "1.0.10.RELEASE"
id "java"
}
group = 'com.nobody'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
configurations {
developmentOnly
runtimeClasspath {
extendsFrom developmentOnly
}
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenLocal()
maven {
url "http://maven.aliyun.com/nexus/content/groups/public/" }
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
// 添加lombok,主要为程序中通过注解,不用编写getter和setter等代码
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
}
在我们项目开发中,肯定会有跟业务相关的异常,例如添加用户的业务,系统要求用户名不能为空,但是添加用户的请求接口,用户名值为空,这时我们程序要报
用户名不能为空
的异常错误;或者查询用户信息的接口,可能会报用户不存在
的错误异常等等。
因为要做成通用性,所以我们定义一个异常基础接口类,自定义的异常枚举类需实现该接口。
package com.nobody.exception;
/**
* @Description 自定义异常基础接口类,自定义的异常信息枚举类需实现该接口。
* @Author Mr.nobody
* @Date 2021/2/6
* @Version 1.0
*/
public interface BaseErrorInfo {
/**
* 获取错误码
*
* @return 错误码
*/
String getErrorCode();
/**
* 获取错误信息
*
* @return 错误信息
*/
String getErrorMsg();
}
通用异常信息枚举类,这里定义的所有异常信息是整个程序通用的。
package com.nobody.exception;
import lombok.Getter;
/**
* @Description 自定义通用异常信息枚举类
* @Author Mr.nobody
* @Date 2020/10/23
* @Version 1.0
*/
@Getter
public enum CommonErrorEnum implements BaseErrorInfo {
/**
* 成功
*/
SUCCESS("200", "成功!"),
/**
* 请求的数据格式不符!
*/
BODY_NOT_MATCH("400", "请求的数据格式不符!"),
/**
* 未找到该资源!
*/
NOT_FOUND("404", "未找到该资源!"),
/**
* 服务器内部错误!
*/
INTERNAL_SERVER_ERROR("500", "服务器内部错误!"),
/**
* 服务器正忙,请稍后再试!
*/
SERVER_BUSY("503", "服务器正忙,请稍后再试!");
private String errorCode;
private String errorMsg;
CommonErrorEnum(String errorCode, String errorMsg) {
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
}
如果程序中异常信息太多,可以针对每个模块功能定义业务异常枚举类,方便维护,例如和用户相关的异常信息枚举类如下。
package com.nobody.exception;
import lombok.Getter;
/**
* @Description 自定义用户相关异常信息枚举类
* @Author Mr.nobody
* @Date 2020/10/23
* @Version 1.0
*/
@Getter
public enum UserErrorEnum implements BaseErrorInfo {
/**
* 用户不存在
*/
USER_NOT_FOUND("1001", "用户不存在!");
private String errorCode;
private String errorMsg;
UserErrorEnum(String errorCode, String errorMsg) {
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
}
业务异常类,主要用于业务错误,或者异常时手动抛出的异常。
package com.nobody.exception;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.MDC;
/**
* @Description 自定义业务异常类
* @Author Mr.nobody
* @Date 2020/10/23
* @Version 1.0
*/
@Getter
@Setter
public class BizException extends RuntimeException {
private static final long serialVersionUID = 5564446583860234738L;
// 错误码
private String errorCode;
// 错误信息
private String errorMsg;
// 日志追踪ID
private String traceId = MDC.get("traceId");
public BizException(BaseErrorInfo errorInfo) {
super(errorInfo.getErrorMsg());
this.errorCode = errorInfo.getErrorCode();
this.errorMsg = errorInfo.getErrorMsg();
}
public BizException(BaseErrorInfo errorInfo, String errorMsg) {
super(errorMsg);
this.errorCode = errorInfo.getErrorCode();
this.errorMsg = errorMsg;
}
public BizException(BaseErrorInfo errorInfo, Throwable cause) {
super(errorInfo.getErrorMsg(), cause);
this.errorCode = errorInfo.getErrorCode();
this.errorMsg = errorInfo.getErrorMsg();
}
public BizException(String errorCode, String errorMsg) {
super(errorMsg);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
public BizException(String errorCode, String errorMsg, Throwable cause) {
super(errorMsg, cause);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
}
为方便前端对接口返回的数据进行处理,也是规范问题,所以我们要定义接口返回统一格式。
package com.nobody.pojo.vo;
import lombok.Getter;
import lombok.Setter;
/**
* @Description 接口返回统一格式
* @Author Mr.nobody
* @Date 2021/2/6
* @Version 1.0
*/
@Getter
@Setter
public class GeneralResult<T> {
private boolean success;
private String errorCode;
private String message;
private T data;
private String traceId;
private GeneralResult(boolean success, T data, String message, String errorCode) {
this.success = success;
this.data = data;
this.message = message;
this.errorCode = errorCode;
}
public static <T> GeneralResult<T> genResult(boolean success, T data, String message) {
return genResult(success, data, message, null);
}
public static <T> GeneralResult<T> genSuccessResult(T data) {
return genResult(true, data, null, null);
}
public static <T> GeneralResult<T> genErrorResult(String message) {
return genResult(false, null, message, null);
}
public static <T> GeneralResult<T> genSuccessResult() {
return genResult(true, null, null, null);
}
public static <T> GeneralResult<T> genErrorResult(String message, String errorCode) {
return genResult(false, null, message, errorCode);
}
public static <T> GeneralResult<T> genResult(boolean success, T data, String message,
String errorCode) {
return new GeneralResult<>(success, data, message, errorCode);
}
public static <T> GeneralResult<T> genErrorResult(String message, String errorCode,
String traceId) {
GeneralResult<T> result = genResult(false, null, message, errorCode);
result.setTraceId(traceId);
return result;
}
}
此类是对全局异常的处理,根据自己情况,是否对不同种类的异常进行处理。例如以下是单独对业务异常,接口参数异常,以及剩余的所有异常进行处理,并生成接口统一格式信息,返回给调用接口的客户端,进行展示。
首先我们需要在处理全局异常的类上面,加上@ControllerAdvice
或者@RestControllerAdvice
注解。@ControllerAdvice 注解能处理@Controller
和@RestController
类型的接口调用时产生的异常,而 @RestControllerAdvice 注解只能处理@RestController
类型接口调用时产生的异常。我们一般用 @ControllerAdvice 注解。
@ExceptionHandler
只能注解在方法上,表示这是一个处理异常的方法,value
属性可以填写需要处理的异常类,可以是数组。
@ResponseBody
注解表示我们返回的信息是响应体数据。
package com.nobody.exception;
import javax.servlet.http.HttpServletRequest;
import com.nobody.pojo.vo.GeneralResult;
import org.slf4j.MDC;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @Description 统一异常处理
* @Author Mr.nobody
* @Date 2020/10/23
* @Version 1.0
*/
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 处理自定义的业务异常
@ExceptionHandler(value = BizException.class)
@ResponseBody
public GeneralResult<Object> restErrorHandler(HttpServletRequest request, BizException e) {
String err = "requestURI:" + request.getRequestURI() + ",errorCode:" + e.getErrorCode()
+ ",errorMsg:" + e.getErrorMsg();
log.error(err, e);
return GeneralResult.genErrorResult(e.getMessage(), e.getErrorCode(), e.getTraceId());
}
// 处理接口参数数据格式错误异常
@ExceptionHandler(value = MethodArgumentNotValidException.class)
@ResponseBody
public GeneralResult<Object> errorHandler(HttpServletRequest request,
MethodArgumentNotValidException e) {
StringBuilder message = new StringBuilder();
String err = null;
e.getBindingResult().getAllErrors()
.forEach(error -> message.append(error.getDefaultMessage()).append(";"));
String des = message.toString();
if (!StringUtils.isEmpty(des)) {
err = des.substring(0, des.length() - 1);
}
log.error(err + ",requestURI:" + request.getRequestURI(), e);
return GeneralResult.genErrorResult(CommonErrorEnum.BODY_NOT_MATCH.getErrorMsg(),
CommonErrorEnum.BODY_NOT_MATCH.getErrorCode(), MDC.get("traceId"));
}
// 处理其他异常
@ExceptionHandler(value = Exception.class)
@ResponseBody
public GeneralResult<Object> errorHandler(HttpServletRequest request, Exception e) {
log.error("internal server error,requestURI:" + request.getRequestURI(), e);
return GeneralResult.genErrorResult(CommonErrorEnum.INTERNAL_SERVER_ERROR.getErrorMsg(),
CommonErrorEnum.INTERNAL_SERVER_ERROR.getErrorCode(), MDC.get("traceId"));
}
}
测试会针对不同情况进行验证,以下是一些测试需要用到的类。
package com.nobody.pojo.entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
/**
* @Description 用户实体类
* @Author Mr.nobody
* @Date 2021/2/6
* @Version 1.0
*/
@AllArgsConstructor
@Getter
@Setter
public class UserEntity implements Serializable {
private static final long serialVersionUID = 5564446583860234738L;
private String id;
private String name;
private int age;
}
package com.nobody.pojo.dto;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
/**
* @Description 添加用户时参数类
* @Author Mr.nobody
* @Date 2021/2/6
* @Version 1.0
*/
@Getter
@Setter
public class UserDTO {
@NotEmpty(message = "用户名不能为空")
private String name;
@Min(value = 0, message = "年龄最小不能低于0")
private int age;
}
以下简单模拟 User 相关业务,然后产生不同的异常。
package com.nobody.service;
import com.nobody.pojo.dto.UserDTO;
import com.nobody.pojo.entity.UserEntity;
/**
* @Description
* @Author Mr.nobody
* @Date 2021/2/6
* @Version 1.0
*/
public interface UserService {
UserEntity add(UserDTO userDTO);
UserEntity getById(String id);
void marry(String age);
}
package com.nobody.service.impl;
import com.nobody.exception.BizException;
import com.nobody.exception.UserErrorEnum;
import com.nobody.pojo.dto.UserDTO;
import com.nobody.pojo.entity.UserEntity;
import com.nobody.service.UserService;
import org.springframework.stereotype.Service;
import java.util.Objects;
import java.util.UUID;
/**
* @Description
* @Author Mr.nobody
* @Date 2021/2/6
* @Version 1.0
*/
@Service
public class UserServiceImpl implements UserService {
@Override
public UserEntity add(UserDTO userDTO) {
String userId = UUID.randomUUID().toString();
return new UserEntity(userId, userDTO.getName(), userDTO.getAge());
}
@Override
public UserEntity getById(String id) {
// 模拟业务异常
if (Objects.equals(id, "000")) {
throw new BizException(UserErrorEnum.USER_NOT_FOUND);
}
return new UserEntity(id, "Mr.nobody", 18);
}
@Override
public void marry(String age) {
// 当age不是数字字符串时,抛出异常
Integer integerAge = Integer.valueOf(age);
System.out.println(integerAge);
}
}
接口类定义,根据不同参数调用接口,可产生不同的异常错误。
package com.nobody.controller;
import com.nobody.pojo.dto.UserDTO;
import com.nobody.pojo.entity.UserEntity;
import com.nobody.pojo.vo.GeneralResult;
import com.nobody.service.UserService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
* @Description
* @Author Mr.nobody
* @Date 2021/2/6
* @Version 1.0
*/
@RestController
@RequestMapping("user")
public class UserController {
private UserService userService;
public UserController(final UserService userService) {
this.userService = userService;
}
@PostMapping("add")
public GeneralResult<UserEntity> add(@RequestBody @Valid UserDTO userDTO) {
UserEntity user = userService.add(userDTO);
return GeneralResult.genSuccessResult(user);
}
@GetMapping("find/{userId}")
public GeneralResult<UserEntity> find(@PathVariable String userId) {
UserEntity user = userService.getById(userId);
return GeneralResult.genSuccessResult(user);
}
@GetMapping("marry/{age}")
public GeneralResult<UserEntity> marry(@PathVariable String age) {
userService.marry(age);
return GeneralResult.genSuccessResult();
}
}
启动服务,进行接口调用,本此演示用的 IDEA 自带的
HTTP Client
工具进行调用,当然你也可以使用Postman
进行调用。
首先演示正常的接口调用,服务没有报错,接口也返回正常数据。
还是调用查询用户接口,演示用户不存在情况,服务报错打印日志,接口也返回错误信息。
再演示添加用户操作,用户名不填值,程序报错打印日志,接口也返回错误信息。
再演示其他异常情况,例如解析数字出错。
此演示项目已上传到Github,如有需要可自行下载,欢迎
Star
。
https://github.com/LucioChn/springboot-global-exception-handler
关注微信公众号【
Java之言
】,更多干货文章
和学习资料
,助你放弃编程之路!