轉載:https://my.oschina.net/zhangxufeng/blog/2222434
對於@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."); } }
啟動上述服務,在瀏覽器中訪問http://localhost:8080/user/detail?id=1之后,可以看到頁面展示的是我們定義的異常視圖。
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; } }
在瀏覽器輸入http://localhost:8080/user/detail?id=1&date=2018-10-2,可以看到控制台進行了如下打印:
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; } }
可以看到,這里使用@ModelAttribute注解接收名稱為message的參數,從而獲取了前面綁定的參數。運行上述代碼並且訪問http://localhost:8080/user/detail?id=1,可以看到頁面進行了正常的展示,控制台也進行了如下打印:
global model attribute.
this is from model attribute
可以看到,這里使用@ModelAttribute注解標注的方法確實在目標接口執行之前執行了。需要說明的是,@ModelAttribute標注的方法的執行是在所有攔截器的preHandle()方法執行之后才會執行。
4. 小結
本文首先講解了@ControllerAdvice注解的作用,然后結合@ControllerAdvice講解了能夠與其結合的三個注解的使用方式。關於這三種使用方式,需要說明的是,這三種注解如果應用於@ControllerAdvice注解所標注的類中,那么它們表示會對@ControllerAdvice所指定的范圍內的接口都有效;如果單純的將這三種注解應用於某個Controller中,那么它們將只會對該Controller中所有的接口有效,並且此時是不需要在該Controller上標注@ControllerAdvice的。