若要對傳播至 Servlet 容器的例外進行處理,可以在 web.xml 中設定 <error-page>
並指定 <exception-type>
等資訊。例如:
<web-app …>
<error-page>
<exception-type>java.lang.NullPointerException</exception-type>
<location>/report.jsp</location>
</error-page>
</web-app>
如果在 <location>
中設定的是 JSP 頁面,該頁面必須設定 isErrorPage
屬性為 true
,才可以使用 exception
隱含物件。
這類傳播至 Servlet 容器的例外,基本上應該是底層無法處理的執行時期例外,傳播至 Servlet 容器之目的,是希望頁面呈現出對使用者有用的資訊,知道到底發生了什麼事情。
在 web.xml 設定,顯然是個針對整個應用程式,在使用 Spring MVC 時,如果想要針對控制器呢?可以使用 @ControllerAdvice
,透過它的 basePackages
、basePackageClasses
、annotations
等屬性,指定要介入哪些控制器之中,如果不設定,那就是適用全部的控制器。例如:
package cc.openhome.aspect;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice
public class ExceptionCtrlAdvice {
@ExceptionHandler(NullPointerException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String nullPointExHandler(NullPointerException ex) {
// ... 做一些例外訊息的處理,像是日誌之類的 ...
// ...bla...bla
// 傳回顯示錯誤的頁面
return "report";
}
}
@ExceptionHandler
可標示特定的例外,被標示的方法將在例外發生時執行,可搭配 @ResponseStatus
指定回應狀態碼(@ResponseStatus
也可以個別地標示在控制器的處理器方法上)。
@ControllerAdvice
處理的範疇是整個控制器,如果你要進一步地,將例外處理的範疇,限定在某個控制器的處理器方法上,可以使用〈攔截處理器〉中談到的方式。
@ControllerAdvice
通常會用於例外處理,然而除了可以使用 @ExceptionHandler
之外,還可以使用 @ModelAttribute
、@InitBinder
。
@ModelAttribute
標註的方法,不需要傳回值,可以接受 Model
實例,會在控制器中每個處理器方法被執行前呼叫,如果處理器中使用的 Model
,在處理器執行前進行某些屬性設定,就可以集中至 @ModelAttribute
標註的方法中進行。
@InitBinder
標註的方法,可以接受 WebDataBinder
實例,這個實例擁有許多可以處理請求欄位的方法,像是常見的 setAllowedFields
、setRequiredFields
、checkAllowedFields
、checkRequiredFields
等。
透過 WebDataBinder
,也可以註冊自訂的屬性編輯器,自訂的屬性編輯器可以實作 PropertyEditor
,這也是個自 Spring 1.x 就存在的 API,在我舊版文件的〈使用 PropertyEditor〉也談過,通常實作時為了方便,會繼承 PropertyEditorSupport
,例如,若想實現一個從 Epoch 毫秒數轉 LocalDateTime
的屬性編輯器,可以如下:
...略
public class LocalDateTimeEditor extends PropertyEditorSupport {
private ZoneId taipei = ZoneId.of("Asia/Taipei");
@Override
public void setAsText(String millis) throws IllegalArgumentException {
setValue(
Instant.ofEpochMilli(Long.parseLong(millis))
.atZone(taipei)
.toLocalDateTime()
);
}
@Override
public String getAsText() {
LocalDateTime localDateTime = (LocalDateTime) getValue();
return String.valueOf(
localDateTime.atZone(taipei).toInstant().toEpochMilli()
);
}
}
當必須從指定的物件轉換為字串時,會呼叫 getAsText
方法,而接收到參數要將之轉換為指定的物件時,會呼叫 setAsText
方法,你可以在 @ControllerAdvice
中標示某個方法為 @InitBinder
並註冊屬性編輯器:
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(LocalDateTime.class, new LocalDateTimeEditor());
}
若某個控制器的處理器方法,想要從 millis
請求參數直接轉為 LocalDateTime
實例,可以如下標註:
@PostMapping("do_foo")
protected String doFoo(@RequestParam("millis") LocalDateTime localDateTime) {
/// ...做點事
}
實際上,Spring 本身也有 @DateTimeFormat
(針對舊式的 DateTime API,而不是 Java 8 新日期時間 API)、@NumberFormat
等可以直接使用,例如,若輸入的日期格式字串可符合 Date
格式轉換,可以如下轉為 Date
:
@PostMapping("do_foo")
protected String doFoo(@DateTimeFormat(iso=ISO.DATE) Date date) {
/// ...做點事
}
若不使用 @DateTimeFormat
,想要自定義格式轉換的話,可以在 @InitBinder
註冊 CustomDateEditor
:
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(
new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false)
);
}
WebDataBinder
也可以透過 setValidator
來設定自定義的驗證器,setValidator
接受 Validator
實作物件,這也是個自 Spring 1.x 就存在的 API,在我舊版文件的〈實作 Validator〉也談過:
package org.springframework.validation;
public interface Validator {
boolean supports(Class clazz);
void validate(Object obj, Errors errors);
}
supports
方法表示是否支援傳入類別的驗證,只有在傳回 true
的情況下,才會使用 validate
方法,在 validate
方法的參數中,obj
表示已從請求參數轉換過來的物件,可以對 validate
方法中進行一些驗證。
如果相對應的方法,並不需要在控制器之間共用,@ExceptionHandler
、@ModelAttribute
、@InitBinder
等,也可以直接標示在控制器之中。