Spring的@ControllerAdvice注解作用原理探究(轉發)


在Spring里,我們可以使用@ControllerAdvice來聲明一些全局性的東西,最常見的是結合@ExceptionHandler注解用於全局異常的處理。

@ControllerAdvice是在類上聲明的注解,其用法主要有三點:

  • @ExceptionHandler注解標注的方法:用於捕獲Controller中拋出的不同類型的異常,從而達到異常全局處理的目的;
  • @InitBinder注解標注的方法:用於請求中注冊自定義參數的解析,從而達到自定義請求參數格式的目的;
  • @ModelAttribute注解標注的方法:表示此方法會在執行目標Controller方法之前執行 。

看下具體用法:

// 這里@RestControllerAdvice等同於@ControllerAdvice + @ResponseBody
@RestControllerAdvice
public class GlobalHandler {
    private final Logger logger = LoggerFactory.getLogger(GlobalHandler.class);
    // 這里@ModelAttribute("loginUserInfo")標注的modelAttribute()方法表示會在Controller方法之前
    // 執行,返回當前登錄用戶的UserDetails對象
    @ModelAttribute("loginUserInfo")
    public UserDetails modelAttribute() {
        return (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
    // @InitBinder標注的initBinder()方法表示注冊一個Date類型的類型轉換器,用於將類似這樣的2019-06-10
    // 日期格式的字符串轉換成Date對象
    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
    } 
    // 這里表示Controller拋出的MethodArgumentNotValidException異常由這個方法處理
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result exceptionHandler(MethodArgumentNotValidException e) {
        Result result = new Result(BizExceptionEnum.INVALID_REQ_PARAM.getErrorCode(),
                BizExceptionEnum.INVALID_REQ_PARAM.getErrorMsg());
        logger.error("req params error", e);
        return result;
    }
    // 這里表示Controller拋出的BizException異常由這個方法處理
    @ExceptionHandler(BizException.class)
    public Result exceptionHandler(BizException e) {
        BizExceptionEnum exceptionEnum = e.getBizExceptionEnum();
        Result result = new Result(exceptionEnum.getErrorCode(), exceptionEnum.getErrorMsg());
        logger.error("business error", e);
        return result;
    }
    // 這里就是通用的異常處理器了,所有預料之外的Exception異常都由這里處理
    @ExceptionHandler(Exception.class)
    public Result exceptionHandler(Exception e) {
        Result result = new Result(1000, "網絡繁忙,請稍后再試");
        logger.error("application error", e);
        return result;
    }

}

@ExceptionHandler標注的多個方法分別表示只處理特定的異常。這里需要注意的是當Controller拋出的某個異常多個@ExceptionHandler標注的方法都適用時,Spring會選擇最具體的異常處理方法來處理,也就是說@ExceptionHandler(Exception.class)這里標注的方法優先級最低,只有當其它方法都不適用時,才會來到這里處理。

下面我們看看Spring是怎么實現的,首先前端控制器DispatcherServlet對象在創建時會初始化一系列的對象:

public class DispatcherServlet extends FrameworkServlet {
    // ......
	protected void initStrategies(ApplicationContext context) {
		initMultipartResolver(context);
		initLocaleResolver(context);
		initThemeResolver(context);
		initHandlerMappings(context);
		initHandlerAdapters(context);
		initHandlerExceptionResolvers(context);
		initRequestToViewNameTranslator(context);
		initViewResolvers(context);
		initFlashMapManager(context);
	}
    // ......
}

對於@ControllerAdvice 注解,我們重點關注initHandlerAdapters(context)和initHandlerExceptionResolvers(context)這兩個方法。

initHandlerAdapters(context)方法會取得所有實現了HandlerAdapter接口的bean並保存起來,其中就有一個類型為RequestMappingHandlerAdapter的bean,這個bean就是@RequestMapping注解能起作用的關鍵,這個bean在應用啟動過程中會獲取所有被@ControllerAdvice注解標注的bean對象做進一步處理,關鍵代碼在這里:

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
		implements BeanFactoryAware, InitializingBean {
    // ......
    private void initControllerAdviceCache() {
                // ......
		List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
		AnnotationAwareOrderComparator.sort(adviceBeans);

		List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();

		for (ControllerAdviceBean adviceBean : adviceBeans) {
			Class<?> beanType = adviceBean.getBeanType();
			if (beanType == null) {
				throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
			}
                        // 找到所有ModelAttribute標注的方法並緩存起來
			Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
			if (!attrMethods.isEmpty()) {
				this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
				if (logger.isInfoEnabled()) {
					logger.info("Detected @ModelAttribute methods in " + adviceBean);
				}
			}
                        // 找到所有InitBinder標注的方法並緩存起來
			Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
			if (!binderMethods.isEmpty()) {
				this.initBinderAdviceCache.put(adviceBean, binderMethods);
				if (logger.isInfoEnabled()) {
					logger.info("Detected @InitBinder methods in " + adviceBean);
				}
			}
                        // ......
		}
	}
    // ......
}

來看DispatcherServlet的initHandlerExceptionResolvers(context)方法,方法會取得所有實現了HandlerExceptionResolver接口的bean並保存起來,其中就有一個類型為ExceptionHandlerExceptionResolver的bean,這個bean在應用啟動過程中會獲取所有被@ControllerAdvice注解標注的bean對象做進一步處理,關鍵代碼在這里:  

public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
		implements ApplicationContextAware, InitializingBean {
    // ......
	private void initExceptionHandlerAdviceCache() {
		// ......
		List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
		AnnotationAwareOrderComparator.sort(adviceBeans);

		for (ControllerAdviceBean adviceBean : adviceBeans) {
			ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());
			if (resolver.hasExceptionMappings()) {
			    // 找到所有ExceptionHandler標注的方法並保存成一個ExceptionHandlerMethodResolver類型的對象緩存起來
				this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
				if (logger.isInfoEnabled()) {
					logger.info("Detected @ExceptionHandler methods in " + adviceBean);
				}
			}
			// ......
		}
	}
    // ......
}

當Controller拋出異常時,DispatcherServlet通過ExceptionHandlerExceptionResolver來解析異常,而ExceptionHandlerExceptionResolver又通過ExceptionHandlerMethodResolver 來解析異常, ExceptionHandlerMethodResolver 最終解析異常找到適用的@ExceptionHandler標注的方法是這里:  

public class ExceptionHandlerMethodResolver {
	// ......
	private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
		List<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>();
		// 找到所有適用於Controller拋出異常的處理方法,例如Controller拋出的異常
		// 是BizException(繼承自RuntimeException),那么@ExceptionHandler(BizException.class)和
		// @ExceptionHandler(Exception.class)標注的方法都適用此異常
		for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
			if (mappedException.isAssignableFrom(exceptionType)) {
				matches.add(mappedException);
			}
		}
		if (!matches.isEmpty()) {
		        // 這里通過排序找到最適用的方法,排序的規則依據拋出異常相對於聲明異常的深度,例如
			// Controller拋出的異常是BizException(繼承自RuntimeException),那么BizException
			// 相對於@ExceptionHandler(BizException.class)聲明的BizException.class其深度是0,
			// 相對於@ExceptionHandler(Exception.class)聲明的Exception.class其深度是2,所以
			// @ExceptionHandler(BizException.class)標注的方法會排在前面
			Collections.sort(matches, new ExceptionDepthComparator(exceptionType));
			return this.mappedMethods.get(matches.get(0));
		}
		else {
			return null;
		}
	}
    // ......
}

整個@ControllerAdvice處理的流程就是這樣,這個設計還是非常靈活的。  

  

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM