Spring MVC之@ControllerAdvice詳解
對於@ControllerAdvice,我們比較熟知的用法是結合@ExceptionHandler用於全域性異常的處理,但其作用不僅限於此。ControllerAdvice拆分開來就是Controller Advice,關於Advice,前面我們講解Spring Aop時講到,其是用於封裝一個切面所有屬性的,包括切入點和需要織入的切面邏輯。這裡ContrllerAdvice也可以這麼理解,其抽象級別應該是用於對Controller進行“切面”環繞的,而具體的業務織入方式則是通過結合其他的註解來實現的。@ControllerAdvice是在類上宣告的註解,其用法主要有三點:
- 結合方法型註解@ExceptionHandler,用於捕獲Controller中丟擲的指定型別的異常,從而達到不同型別的異常區別處理的目的;
- 結合方法型註解@InitBinder,用於request中自定義引數解析方式進行註冊,從而達到自定義指定格式引數的目的;
- 結合方法型註解@ModelAttribute,表示其標註的方法將會在目標Controller方法執行之前執行。
從上面的講解可以看出,@ControllerAdvice的用法基本是將其宣告在某個bean上,然後在該bean的方法上使用其他的註解來指定不同的織入邏輯。不過這裡@ControllerAdvice並不是使用AOP的方式來織入業務邏輯的,而是Spring內建對其各個邏輯的織入方式進行了內建支援。本文將對@ControllerAdvice的這三種使用方式分別進行講解。
1. @ExceptionHandler
@ExceptionHandler的作用主要在於宣告一個或多個型別的異常,當符合條件的Controller丟擲這些異常之後將會對這些異常進行捕獲,然後按照其標註的方法的邏輯進行處理,從而改變返回的檢視資訊。如下是@ExceptionHandler的屬性結構:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ExceptionHandler { // 指定需要捕獲的異常的Class型別 Class<? extends Throwable>[] value() default {}; }
如下是我們使用@ExceptionHandler捕獲RuntimeException異常的例子:
@ControllerAdvice(basePackages = "mvc")
public class SpringControllerAdvice {
@ExceptionHandler(RuntimeException.class)
public ModelAndView runtimeException(RuntimeException e) {
e.printStackTrace();
return new ModelAndView("error");
}
}
這裡我們模擬一個訪問user detail的介面,在該介面中丟擲了RuntimeException,那麼理論上,這裡的異常捕獲器就會捕獲該異常,然後返回預設的error試圖。如下是UserController的程式碼:
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(value = "/detail", method = RequestMethod.GET)
public ModelAndView detail(@RequestParam("id") long id) {
ModelAndView view = new ModelAndView("user");
User user = userService.detail(id);
view.addObject("user", user);
throw new RuntimeException("mock user detail exception.");
}
}
2. @InitBinder
對於@InitBinder,該註解的主要作用是繫結一些自定義的引數。一般情況下我們使用的引數通過@RequestParam,@RequestBody或者@ModelAttribute等註解就可以進行綁定了,但對於一些特殊型別引數,比如Date,它們的繫結Spring是沒有提供直接的支援的,我們只能為其宣告一個轉換器,將request中字串型別的引數通過轉換器轉換為Date型別的引數,從而供給@RequestMapping標註的方法使用。如下是@InitBinder的宣告:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InitBinder {
// 這裡value引數用於指定需要繫結的引數名稱,如果不指定,則會對所有的引數進行適配,
// 只有是其指定的型別的引數才會被轉換
String[] value() default {};
}
如下是使用@InitBinder註冊Date型別引數轉換器的實現:
@ControllerAdvice(basePackages = "mvc")
public class SpringControllerAdvice {
@InitBinder
public void globalInitBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
}
}
這裡@InitBinder標註的方法註冊的Formatter在每次request請求進行引數轉換時都會呼叫,用於判斷指定的引數是否為其可以轉換的引數。如下是我們宣告的包含Date型別引數的介面:
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(value = "/detail", method = RequestMethod.GET)
public ModelAndView detail(@RequestParam("id") long id, Date date) {
System.out.println(date);
ModelAndView view = new ModelAndView("user");
User user = userService.detail(id);
view.addObject("user", user);
return view;
}
}
Tue Oct 02 00:00:00 CST 2018
可以看到,這裡我們對request引數進行了轉換,並且在介面中成功接收了該引數。
3. @ModelAttribute
關於@ModelAttribute的用法,處理用於介面引數可以用於轉換物件型別的屬性之外,其還可以用來進行方法的宣告。如果宣告在方法上,並且結合@ControllerAdvice,該方法將會在@ControllerAdvice所指定的範圍內的所有介面方法執行之前執行,並且@ModelAttribute標註的方法的返回值還可以供給後續會呼叫的介面方法使用。如下是@ModelAttribute註解的宣告:
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ModelAttribute {
// 該屬性與name屬性的作用一致,用於指定目標引數的名稱
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
// 與name屬性一起使用,如果指定了binding為false,那麼name屬性指定名稱的屬性將不會被處理
boolean binding() default true;
}
這裡@ModelAttribute的各個屬性值主要是用於其在介面引數上進行標註時使用的,如果是作為方法註解,其name或value屬性則指定的是返回值的名稱。如下是使用@ModelAttribute進行方法標註的一個例子:
@ControllerAdvice(basePackages = "mvc")
public class SpringControllerAdvice {
@ModelAttribute(value = "message")
public String globalModelAttribute() {
System.out.println("global model attribute.");
return "this is from model attribute";
}
}
這裡需要注意的是,該方法提供了一個String型別的返回值,而@ModelAttribute中指定了該屬性名稱為message,這樣在Controller層就可以接收該引數了,如下是Controller層的程式碼:
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(value = "/detail", method = RequestMethod.GET)
public ModelAndView detail(@RequestParam("id") long id,
@ModelAttribute("message") String message) {
System.out.println(message);
ModelAndView view = new ModelAndView("user");
User user = userService.detail(id);
view.addObject("user", user);
return view;
}
}
global model attribute.
this is from model attribute
可以看到,這裡使用@ModelAttribute註解標註的方法確實在目標介面執行之前執行了。需要說明的是,@ModelAttribute標註的方法的執行是在所有攔截器的preHandle()方法執行之後才會執行。
4. 小結
本文首先講解了@ControllerAdvice註解的作用,然後結合@ControllerAdvice講解了能夠與其結合的三個註解的使用方式。關於這三種使用方式,需要說明的是,這三種註解如果應用於@ControllerAdvice註解所標註的類中,那麼它們表示會對@ControllerAdvice所指定的範圍內的介面都有效;如果單純的將這三種註解應用於某個Controller中,那麼它們將只會對該Controller中所有的介面有效,並且此時是不需要在該Controller上標註@ControllerAdvice的。