1. 程式人生 > 其它 >後端筆記08-Exception Handler

後端筆記08-Exception Handler

SpringBoot8-Exception Handler

學習如何在Spring Boot中進行統一的異常處理,其中包括了兩種方式的處理:第一種對API形式的介面進行異常處理,統一封裝了返回格式;第二種是對模板頁面請求的異常處理,統一處理錯誤頁面

pom.xml

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

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

Status列舉

public enum Status {
    /**
     * 操作成功
     */
    OK(200, "操作成功"),

    /**
     * 未知異常
     */
    UNKNOWN_ERROR(500,"伺服器出錯啦");
    /**
     * 狀態碼
     */
    private Integer code;
    /**
     * 內容
     */
    private String message;
    Status(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public Integer getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

Exception

@Data
@EqualsAndHashCode(callSuper = true)
public class BaseException extends RuntimeException{
    private Integer code;
    private String message;

    public BaseException(Status status) {
        super(status.getMessage());
        this.code = status.getCode();
        this.message = status.getMessage();
    }

    public BaseException(Integer code, String message) {
        super(message);
        this.code = code;
        this.message = message;
    }
}
@EqualsAndHashCode
  1. 此註解會生成equals(Object other)hashCode()方法。
  2. 它預設使用非靜態,非瞬態的屬性
  3. 可通過引數exclude排除一些屬性
  4. 可通過引數of指定僅使用哪些屬性
  5. 它預設僅使用該類中定義的屬性且不呼叫父類的方法

當啟動@EqualsAndHashCode時,預設不呼叫父類的equals方法,當做型別相等判斷時,會遇到麻煩,例如:

@Data
public class People {
    private Integer id;
}

@Data
public class User extends People {
    private String name;
    private Integer age;
}

public static void main(String[] args) {
    User user1 = new User();
    user1.setName("jiangxp");
    user1.setAge(18);
    user1.setId(1);

    User user2 = new User();
    user2.setName("jiangxp");
    user2.setAge(18);
    user2.setId(2);

    System.out.println(user1.equals(user2));
}

輸出結果:true

注意:兩條user資料,ID完全不一樣,結果明顯是錯的,沒有做id的equals判斷

需要將@EqualsAndHashCode修改為@EqualsAndHashCode(callSuper = true)才能得到正確結果。

另外因為@Data相當於@Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode這5個註解的合集,所以程式碼中@Data@EqualsAndHashCode(callSuper = true)聯合使用。

@Getter
public class JsonException extends BaseException{
    public JsonException(Status status) {
        super(status);
    }

    public JsonException(Integer code, String message) {
        super(code, message);
    }
}
@Getter
public class PageException extends BaseException{
    public PageException(Status status) {
        super(status);
    }

    public PageException(Integer code, String message) {
        super(code, message);
    }
}

ApiResponse.java

統一的API格式返回封裝

@Data
public class ApiResponse {
    /**
     * 狀態碼
     */
    private Integer code;

    /**
     * 返回內容
     */
    private String message;

    /**
     * 返回資料
     */
    private Object data;

    /**
     * 無參建構函式
     */
    private ApiResponse() {

    }

    /**
     * 全參建構函式
     *
     * @param code 狀態碼
     * @param message 返回內容
     * @param data 返回資料
     */
    private ApiResponse(Integer code, String message, Object data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    /**
     * 構造一個自定義的API返回
     *
     * @param code    狀態碼
     * @param message 返回內容
     * @param data    返回資料
     * @return ApiResponse
     */
    public static ApiResponse of(Integer code, String message, Object data) {
        return new ApiResponse(code, message, data);
    }

    /**
     * 構造一個有狀態的API返回
     *
     * @param status 狀態 {@link Status}
     * @return ApiResponse
     */
    public static ApiResponse ofStatus(Status status) {
        return ofStatus(status, null);
    }

    /**
     * 構造一個有狀態且帶資料的API返回
     *
     * @param status 狀態 {@link Status}
     * @param data   返回資料
     * @return ApiResponse
     */
    public static ApiResponse ofStatus(Status status, Object data) {
        return of(status.getCode(), status.getMessage(), data);
    }


    /**
     * 構造一個成功且帶資料的API返回
     *
     * @param data 返回資料
     * @return ApiResponse
     */
    public static ApiResponse ofSuccess(Object data) {
        return ofStatus(Status.OK, data);
    }

    /**
     * 構造一個成功且自定義訊息的API返回
     *
     * @param message 返回內容
     * @return ApiResponse
     */
    public static ApiResponse ofMessage(String message) {
        return of(Status.OK.getCode(), message, null);
    }

    /**
     * 構造一個異常且帶資料的API返回
     *
     * @param t    異常
     * @param data 返回資料
     * @param <T>  {@link BaseException} 的子類
     * @return ApiResponse
     */
    public static <T extends BaseException> ApiResponse ofException(T t, Object data) {
        return of(t.getCode(), t.getMessage(), data);
    }

    /**
     * 構造一個異常的API返回
     *
     * @param t   異常
     * @param <T> {@link BaseException} 的子類
     * @return ApiResponse
     */
    public static <T extends BaseException> ApiResponse ofException(T t) {
        return ofException(t, null);
    }
    
}

DemoExceptionHandler.java

@ControllerAdvice
@Slf4j
public class DemoExceptionHandler {
    private static final String DEFAULT_ERROR_VIEW = "error";
    /**
     * 統一 json 異常處理
     *
     * @param exception JsonException
     * @return 統一返回 json 格式
     */
    @ExceptionHandler(value = JsonException.class)
    @ResponseBody
    public ApiResponse jsonErrorHandler(JsonException exception) {
        log.error("【JsonException】:{}", exception.getMessage());
        return ApiResponse.ofException(exception);
    }

    /**
     * 統一 頁面 異常處理
     *
     * @param exception PageException
     * @return 統一跳轉到異常頁面
     */
    @ExceptionHandler(value = PageException.class)
    public ModelAndView pageErrorHandler(PageException exception) {
        log.error("【DemoPageException】:{}", exception.getMessage());
        ModelAndView view = new ModelAndView();
        view.addObject("message", exception.getMessage());
        view.setViewName(DEFAULT_ERROR_VIEW);
        return view;
    }

    @ModelAttribute
    public void addMyAttribute(Model model) {
        model.addAttribute("user", "miles");
    }

    @InitBinder
    public void initBinder(WebDataBinder binder) {
//        GenericConversionService genericConversionService = (GenericConversionService) binder.getConversionService();
//        if (genericConversionService != null) {
//            genericConversionService.addConverter(new DateConverter());
//        }

        binder.registerCustomEditor(String.class,
                new StringTrimmerEditor(true));

        binder.registerCustomEditor(Date.class,
                new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));
    }
}

@ControllerAdvice是一個特殊的@Component,用於標識一個類,這個類中可以使用三種註解標識的方法:@ExceptionHandler@InitBinder@ModelAttribute,將作用於所有的@Controller類的介面上。

@InitBinder

作用:註冊屬性編輯器,對HTTP請求引數進行處理,再繫結到對應的介面,比如格式化的時間轉換等。應用於單個@Controller類的方法上時,僅對該類裡的介面有效。與@ControllerAdvice組合使用可全域性生效。

@ControllerAdvice
public class ActionAdvice {
    //@InitBinder標註的方法必須有一個引數WebDataBinder
    @InitBinder
    public void handleException(WebDataBinder binder) {
        binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd HH:mm:ss"));
    }
}

@ExceptionHandler

作用:統一異常處理,也可以指定要處理的異常型別

@ControllerAdvice
public class ActionAdvice {
    
    @ExceptionHandler(Exception.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public Map handleException(Exception ex) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", 400);
        map.put("msg", ex.toString());
        return map;
    }
}

@ModelAttribute

作用:繫結資料

@ControllerAdvice
public class ActionAdvice {
    
    @ModelAttribute
    public void handleException(Model model) {
        model.addAttribute("user", "zfh");
    }
}

在介面中獲取前面繫結的引數:

@RestController
public class BasicController {
    
    @GetMapping(value = "index")
    public Map index(@ModelAttribute("user") String user) {
        //...
    }
}

完整示例程式碼:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.format.datetime.DateFormatter;
import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.validation.Validator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;

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

@ControllerAdvice
public class ControllerExceptionHandler {

    private Logger logger = LoggerFactory.getLogger(ControllerExceptionHandler.class);

    @InitBinder
    public void initMyBinder(WebDataBinder binder) {
        binder.registerCustomEditor(String.class,
                new StringTrimmerEditor(true));

        binder.registerCustomEditor(Date.class,
                new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));
    }

    @ModelAttribute
    public void addMyAttribute(Model model) {
        model.addAttribute("user", "miles"); // 在@RequestMapping的介面中使用@ModelAttribute("name") Object name獲取
    }

    @ExceptionHandler(value = Exception.class)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody // 如果使用了@RestControllerAdvice,這裡就不需要@ResponseBody了
    public Map handler(Exception ex) {
        logger.error("統一異常處理", ex);
        Map<String, Object> map = new HashMap<>();
        map.put("code", 400);
        map.put("msg", ex);
        return map;
    }
}

測試介面:

@RestController
public class TestAction {

    @GetMapping(value = "testAdvice")
    public JsonResult testAdvice(@ModelAttribute("user") String user, Date date) throws Exception {
        System.out.println("user: " + user);
        System.out.println("date: " + date);
        throw new Exception("直接丟擲異常");
    }
}

使用@InitBinder來對頁面資料進行解析繫結

介面資料的一些處理,比如時間字串轉化為Date格式等

way1:
@InitBinder
public void initMyBinder(WebDataBinder binder) {
    //字串處理
    binder.registerCustomEditor(String.class,
            new StringTrimmerEditor(true));// spring自帶的PropertyEditor可以在此註冊 也可以使用自定的型別轉化器
	
    //時間轉化
    binder.registerCustomEditor(Date.class,
            new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));
}

Controller方法的引數型別可以是基本型別,也可以是封裝後的普通Java型別。若這個普通Java型別沒有宣告任何註解,則意味著它的每一個屬性都需要到Request中去查詢對應的請求引數。眾所周知,無論客戶端傳入的是什麼型別的請求引數,最終都要以位元組的形式傳給服務端。而服務端通過RequestgetParameter方法取到的引數也都是字串形式的結果。所以,需要有一個把字串形式的引數轉換成服務端真正需要的型別的轉換工具,在spring中這個轉換工具為WebDataBinder

WebDataBinder不需要我們自己去建立,我們只需要向它註冊引數型別對應的屬性編輯器PropertyEditorPropertyEditor可以將字串轉換成其真正的資料型別,它的void setAsText(String text)方法實現資料轉換的過程。

具體的做法是,在Controller中宣告一個InitBinder方法,方法中利用WebDataBinder將自己實現的或者spring自帶的PropertyEditor進行註冊,如上程式碼。

參考:https://blog.csdn.net/hongxingxiaonan/article/details/50282001

way2:

自定義時間型別轉換器

public class DateConverter implements Converter<String, Date> {
    private static final String dateFormat = "yyyy-MM-dd HH:mm:ss";
    private static final String shortDateFormat = "yyyy-MM-dd";
    private static final String timeStampFormat = "^\\d+$";

    @Override
    public Date convert(String value) {
        if (StringUtils.isEmpty(value)) {
            return null;
        }
        value = value.trim();
        try {
            if (value.contains("-")) {
                SimpleDateFormat formatter;
                if (value.contains(":")) {
                    formatter = new SimpleDateFormat(dateFormat);
                } else {
                    formatter = new SimpleDateFormat(shortDateFormat);
                }
                return formatter.parse(value);
            } else if (value.matches(timeStampFormat)) {
                Long lDate = new Long(value);
                return new Date(lDate);
            }
        } catch (Exception e) {
            throw new RuntimeException(String.format("parser %s to Date fail", value));
        }
        throw new RuntimeException(String.format("parser %s to Date fail", value));
    }
}
    @InitBinder
    public void initBinder(WebDataBinder binder) {
        //型別轉化器的註冊
        GenericConversionService genericConversionService = (GenericConversionService) binder.getConversionService();
       if (genericConversionService != null) {
            genericConversionService.addConverter(new DateConverter());
        }
    }

擴充套件例子:

	@InitBinder
    public void initBinder(WebDataBinder binder) {
        // 方法1,註冊converter
        GenericConversionService genericConversionService = (GenericConversionService) binder.getConversionService();
        if (genericConversionService != null) {
            genericConversionService.addConverter(new DateConverter());
        }

        // 方法2,定義單格式的日期轉換,可以通過替換格式,定義多個dateEditor,程式碼不夠簡潔
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        CustomDateEditor dateEditor = new CustomDateEditor(df, true);
        binder.registerCustomEditor(Date.class, dateEditor);


        // 方法3,同樣註冊converter
        binder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                setValue(new DateConverter().convert(text));
            }
        });

    }

參考:https://my.oschina.net/sugarZone/blog/706417

參考:https://www.javazhiyin.com/56535.html

測試介面

@GetMapping("/json")
@ResponseBody
public ApiResponse jsonException(@ModelAttribute("user") String user, Date date) {
    log.info("【user】:{}", user);
    log.info("【date】:{}", date);
    throw new JsonException(Status.UNKNOWN_ERROR);
}

注意的是介面中的date是Date型別的

@RestControllerAdvice = @ControllerAdvice + @ResponseBody

參考:https://juejin.im/post/6844903826412011533

error.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head lang="en">
    <meta charset="UTF-8"/>
    <title>統一頁面異常處理</title>
</head>
<body>
<h1>統一頁面異常處理</h1>
<div th:text="${message}"></div>
</body>
</html>

TestController.java

@Controller
@Slf4j
public class TestController {
    @GetMapping("/json")
    @ResponseBody
    public ApiResponse jsonException(@ModelAttribute("user") String user, Date date) {
        log.info("【user】:{}", user);
        log.info("【date】:{}", date);
        throw new JsonException(Status.UNKNOWN_ERROR);
    }

    @GetMapping("/page")
    public ModelAndView pageException() {
        throw new PageException(Status.UNKNOWN_ERROR);
    }
}