Spring Boot2 实战系列之 exception 配置

前言

在实际项目中,通常会有各种异常抛出,如果我们不加以捕捉并自定义统一的response(返回信息),那么用户就可能看到莫名奇怪的错误。比如常见的 NullPointerException, 你直接返回这个错误信息,相信用户也是一头雾水。

还有就是统一捕捉异常并自定义报错信息也有利于我们排查,迅速定位问题所在。

我们先来看下如果不做全局异常配置会是怎样,如下例所示,在 controller 层的请求里加个 try catch,然后根据不同异常处理,如果有很多请求,那么每个请求都像这样加个 try catch 层,就比较繁杂,而且要捕捉的异常可能很多都相同,这样代码重复就比较多。

@RestController
@RequestMapping("/api")
public class LoginController {
	@PostMapping("/login")
	public ResponseCode login(@RequestBody User User) {
		JSONObject jsonObject = null;
		try {
			logger.info("Request data:" + User.toString());
			jsonObject = loginService.valifyLogin(User);
		} catch (RestInputException e) {
			// Exception 处理1
			return ResponseCode.fail(400, "Bad request.");
		} catch (Exception e) {
			// Exception 处理2
			return ResponseCode.error("System error");
		}

		return ResponseCode.success("Login success!", jsonObject);
	}
}

那怎样优雅地捕捉异常呢,这就需要用到 Spring 的两个注解 @ExceptionHandler 和 @ControllerAdvice。

@ExceptionHandler 作用于方法之上,捕捉 controller 类抛出的指定的异常,例如上面的登陆请求可以简化为如下例子,这样就避免了写重复的try catch 代码。

@RestController
@RequestMapping("/api")
public class LoginController {
    @PostMapping("/login")
	public ResponseCode login(@RequestBody User User) {
		JSONObject jsonObject = loginService.valifyLogin(User);
        return ResponseCode.success("Login success!", jsonObject);
    }

    @ExceptionHandler(RestInputException.class)
    @ResponseBody
    public ApiResponse handleRestInputException(RestInputException e) {
        log.info("Business error: [{}]", e.getMessage());
        return ApiResponse.error(ApiStatus.BAD_REQUEST.getCode(), e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ApiResponse handleExceptions(Exception e) {
        log.error("System error", e);
        return ApiResponse.error(ApiStatus.INTERNAL_SERVER_ERROR.getCode(), ApiStatus.INTERNAL_SERVER_ERROR.getMsg());
    } 
}

但如果有多个 controller 类,每个类还是可能要写重复的 @ExceptionHandler 注解方法,这时可以写一个 BaseController 类,其他 controller 类继承这个类即可。

public class BaseController {
    @ExceptionHandler(RestInputException.class)
    @ResponseBody
    public ApiResponse handleRestInputException(RestInputException e) {
        log.info("Business error: [{}]", e.getMessage());
        return ApiResponse.error(ApiStatus.BAD_REQUEST.getCode(), e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ApiResponse handleExceptions(Exception e) {
        log.error("System error", e);
        return ApiResponse.error(ApiStatus.INTERNAL_SERVER_ERROR.getCode(), ApiStatus.INTERNAL_SERVER_ERROR.getMsg());
    } 
}


@RestController
@RequestMapping("/api")
public class LoginController extends BaseController {/***/}

像上面这样可以为不同业务的 controller 继承一个对应的 BaseController, 里面写相关业务异常的处理就行了 但如果想要一个全局的异常配置呢,这样就要用到 @ControllerAdvice + @ExceptionHandler,两者搭配使用,如下所示

@Slf4j
@ControllerAdvice
public class ApiExceptionConfig {
    @ExceptionHandler(RestInputException.class)
    @ResponseBody
    public ApiResponse handleRestInputException(RestInputException e) {
        log.info("Business error: [{}]", e.getMessage());
        return ApiResponse.error(ApiStatus.BAD_REQUEST.getCode(), e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ApiResponse handleExceptions(Exception e) {
        log.error("System error", e);
        return ApiResponse.error(ApiStatus.INTERNAL_SERVER_ERROR.getCode(), ApiStatus.INTERNAL_SERVER_ERROR.getMsg());
    }     
}

值得注意的是它们间的优先级,@Controller + @ExceptionHandler 优先级高,@ControllerAdvice + @ExceptionHandler 次之,如果它们定义了相同的异常,优先级高先捕捉异常,而且被捕捉处理了,优先级低的就不再执行。

下面使用 @ControllerAdvice + @ExceptionHandler 方式创建一个全局异常配置项目。

创建项目

项目结构图如下:
在这里插入图片描述
pom 依赖如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.5.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>top.yekongle</groupId>
	<artifactId>springboot-exception-sample</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>springboot-exception-sample</name>
	<description>Exception project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

代码编写

异常相关
RestInputException.java

package top.yekongle.exception.exception;

/**
 * @Description 自定义异常
 * */
public class RestInputException extends RuntimeException {
	private static final long serialVersionUID = 1L;

	public RestInputException(String message) {
		super(message);
	}
}

ApiExceptionConfig.java, 全局异常配置

package top.yekongle.exception.config;

import java.util.stream.Collectors;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.ObjectError;
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 top.yekongle.exception.exception.RestInputException;
import top.yekongle.exception.response.ApiResponse;
import top.yekongle.exception.response.ApiStatus;

/**
 * @Description 全局异常配置类
 * @Slf4j lombok 注解,自动生成 logger 类
 * */
@Slf4j
@ControllerAdvice
public class ApiExceptionConfig {

    //处理请求参数格式错误 @RequestBody上validate失败后抛出的异常是MethodArgumentNotValidException异常。
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public ApiResponse MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
	    	/*String message = null;
	    	for (ObjectError objectError : e.getBindingResult().getAllErrors()) {
				if (message == null) {
					message = objectError.getDefaultMessage();
				} else {
					message = message + objectError.getDefaultMessage();
				}
			}*/
        String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
        log.info("Bad request: [{}]", message);
        return ApiResponse.error(ApiStatus.BAD_REQUEST.getCode(), message);
    }

    @ExceptionHandler(RestInputException.class)
    @ResponseBody
    public ApiResponse handleRestInputException(RestInputException e) {
        log.info("Business error: [{}]", e.getMessage());
        return ApiResponse.error(ApiStatus.BAD_REQUEST.getCode(), e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ApiResponse handleExceptions(Exception e) {
        log.error("System error", e);
        return ApiResponse.error(ApiStatus.INTERNAL_SERVER_ERROR.getCode(), ApiStatus.INTERNAL_SERVER_ERROR.getMsg());
    }
}

Bean相关

TestDto.java

package top.yekongle.exception.dto;

import lombok.Data;
import javax.validation.constraints.NotEmpty;

/**
 * 请求实体
 * @Data lombok 注解,自动生成 getter setter 方法
 * @NotEmpty 参数校验注解
 * */
@Data
public class TestDto {
	@NotEmpty(message = "名字不能为空!")
	private String name;
	@NotEmpty(message = "地址不能为空!")
	private String address;
}

Person.java

package top.yekongle.exception.model;

import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

/**
 * @Description 业务实体类
 * */
@Data
public class Person {
	@NotEmpty(message = "姓名不能为空!")
	private String name;
	@NotNull(message = "年龄不能为空!")
	private Integer age;
}

下面是结果返回相关类

ApiStatus.java

package top.yekongle.exception.response;

/**
 * @Description 自定义返回状态
 */
public enum ApiStatus {

    SUCCESS(200, "Success"),
    INTERNAL_SERVER_ERROR(500, "Internal Server Error"),
    BAD_REQUEST(400, "Bad Request");

    private final int code;
    private final String msg;

    ApiStatus(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    /**
     * Return the integer value of this status code.
     */
    public int getCode() {
        return this.code;
    }

    /**
     * Return the reason phrase of this status code.
     */
    public String getMsg() {
        return this.msg;
    }
}

ApiResponse.java

package top.yekongle.exception.response;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * @Description 自定义response实体类
 */
@JsonInclude(Include.NON_NULL)
@Data
@AllArgsConstructor
public class ApiResponse {
	private Integer code;
	private String msg;
	private Object data;
	
	public static ApiResponse success(Object data) {
		return new ApiResponse(ApiStatus.SUCCESS.getCode(), ApiStatus.SUCCESS.getMsg(), data);
	}
	
	public static ApiResponse error(Integer code, String msg) {
		return new ApiResponse(code, msg, null);
	}
}

业务逻辑类
PersonService.java

package top.yekongle.exception.service;

import top.yekongle.exception.model.Person;

import java.util.Map;

/**
 * @Description 业务接口
 */
public interface PersonService {
	Map<String, Object> register(Person person);
}

PersonServiceImpl.java

package top.yekongle.exception.service.impl;

import java.util.HashMap;
import java.util.Map;

import org.springframework.stereotype.Service;

import top.yekongle.exception.exception.RestInputException;
import top.yekongle.exception.model.Person;
import top.yekongle.exception.service.PersonService;

/**
 * @Description 业务实现类
 */
@Service
public class PersonServiceImpl implements PersonService {
    @Override
    public Map<String, Object> register(Person person) {
        Map<String, Object> resultMap = null;
        if (person.getAge().intValue() < 18) {
            throw new RestInputException("未成年人不能注册哦!");
        }

        resultMap = new HashMap<>();
        resultMap.put("ID", 666);
        return resultMap;
    }
}

控制类
TestController.java

package top.yekongle.exception.controller;

import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.yekongle.exception.dto.TestDto;
import top.yekongle.exception.model.Person;
import top.yekongle.exception.response.ApiResponse;
import top.yekongle.exception.service.PersonService;

/**
 * @Description 测试异常控制类
 */
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
    @Autowired
    private PersonService personService;

    /**
     * 测试捕捉参数验证 exception
     * */
	@PostMapping("/validate")
	public ApiResponse testValidate(@RequestBody @Validated TestDto testDto) {
		return ApiResponse.success(testDto);
	}

    /**
     * 测试捕捉业务验证 exception
     * */
    @PostMapping("/business")
    public ApiResponse init(@RequestBody Person person) {
        Map resultMap = personService.register(person);
        return ApiResponse.success(resultMap);
    }

    /**
     * 测试捕捉系统 exception
     * */
    @PostMapping("/error")
    public ApiResponse testValidate() {
        TestDto testDto = null;
        log.info("Result:" + testDto.toString());
        return ApiResponse.success(null);
    }
}

运行演示

运行项目,用 postman 测试
访问 http://127.0.0.1:8080/test/validate
在这里插入图片描述
访问 http://localhost:8080/test/business
在这里插入图片描述

访问 http://localhost:8080/test/error
在这里插入图片描述

可见,全局异常配置生效,并返回了自定义的异常信息。

项目已上传至 Github: https://github.com/yekongle/springboot-code-samples/tree/master/springboot-exception-sample , 希望对小伙伴们有帮助哦。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值