2、开发环境在日常项目开发过程当中,程序不免会出现运行时异常,或者业务异常。难道要针对每一处可能出现的异常进行编写代码进行处理?或者直接不处理异常,将一大屏堆满英文的异常信息显示给用户?那用户体验性是何等极差。
因此,当程序抛异常时,为了日志的可读性
,排查 Bug简单
,以及更好的用户体验性
,因此咱们要对全局异常进行处理。前端
3、添加依赖
- 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' }4、自定义异常错误类
在咱们项目开发中,确定会有跟业务相关的异常,例如添加用户的业务,系统要求用户名不能为空,可是添加用户的请求接口,用户名值为空,这时咱们程序要报
用户名不能为空
的异常错误;或者查询用户信息的接口,可能会报用户不存在
的错误异常等等。java
由于要作成通用性,因此咱们定义一个异常基础接口类,自定义的异常枚举类需实现该接口。web
package com.nobody.exception; /** * @Description 自定义异常基础接口类,自定义的异常信息枚举类需实现该接口。 * @Author Mr.nobody * @Date 2021/2/6 * @Version 1.0 */ public interface BaseErrorInfo { /** * 获取错误码 * * @return 错误码 */ String getErrorCode(); /** * 获取错误信息 * * @return 错误信息 */ String getErrorMsg(); }
通用异常信息枚举类,这里定义的全部异常信息是整个程序通用的。spring
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; } }5、接口返回统一格式
为方便前端对接口返回的数据进行处理,也是规范问题,因此咱们要定义接口返回统一格式。app
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> genSucce***esult(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> genSucce***esult() { 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; } }6、全局异常处理
此类是对全局异常的处理,根据本身状况,是否对不一样种类的异常进行处理。例如如下是单独对业务异常,接口参数异常,以及剩余的全部异常进行处理,并生成接口统一格式信息,返回给调用接口的客户端,进行展现。
首先咱们须要在处理全局异常的类上面,加上@ControllerAdvice
或者@RestControllerAdvice
注解。@ControllerAdvice 注解能处理@Controller
和@RestController
类型的接口调用时产生的异常,而 @RestControllerAdvice 注解只能处理@RestController
类型接口调用时产生的异常。咱们通常用 @ControllerAdvice 注解。
@ExceptionHandler
只能注解在方法上,表示这是一个处理异常的方法,value
属性能够填写须要处理的异常类,能够是数组。@ResponseBody
注解表示咱们返回的信息是响应体数据。dom
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")); } }7、测试
测试会针对不一样状况进行验证,如下是一些测试须要用到的类。maven
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 相关业务,而后产生不一样的异常。ide
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.genSucce***esult(user); } @GetMapping("find/{userId}") public GeneralResult<UserEntity> find(@PathVariable String userId) { UserEntity user = userService.getById(userId); return GeneralResult.genSucce***esult(user); } @GetMapping("marry/{age}") public GeneralResult<UserEntity> marry(@PathVariable String age) { userService.marry(age); return GeneralResult.genSucce***esult(); } }
启动服务,进行接口调用,本此演示用的 IDEA 自带的
HTTP Client
工具进行调用,固然你也可使用Postman
进行调用。
首先演示正常的接口调用,服务没有报错,接口也返回正常数据。
仍是调用查询用户接口,演示用户不存在状况,服务报错打印日志,接口也返回错误信息。
再演示添加用户操做,用户名不填值,程序报错打印日志,接口也返回错误信息。
再演示其余异常状况,例如解析数字出错。