@ControllerAdvice注解使用及原理探究
最近在新項(xiàng)目的開發(fā)過程中,遇到了個(gè)問題,需要將一些異常的業(yè)務(wù)流程返回給前端,需要提供給前端不同的響應(yīng)碼,前端再在次基礎(chǔ)上做提示語言的國際化適配。這些異常流程涉及業(yè)務(wù)層和控制層的各個(gè)地方,如果每個(gè)地方都寫一些重復(fù)代碼顯得很冗余。
然后查詢解決方案時(shí)發(fā)現(xiàn)了@ControllerAdvice這個(gè)注解,可以對業(yè)務(wù)異常進(jìn)行統(tǒng)一處理。經(jīng)過仔細(xì)了解后,發(fā)現(xiàn)這個(gè)注解還有更多的用處,都很實(shí)用。
1 ControllerAdvice介紹
@ControllerAdvice一般和三個(gè)以下注解一塊使用,起到不同的作用,
- @ExceptionHandler: 該注解作用于方法上,,可以捕獲到controller中拋出的一些自定義異常,統(tǒng)一進(jìn)行處理,一般用于進(jìn)行一些特定的異常處理。
- @InitBinder:該注解作用于方法上,用于將前端請求的特定類型的參數(shù)在到達(dá)controller之前進(jìn)行處理,從而達(dá)到轉(zhuǎn)換請求參數(shù)格式的目的。
- @ModelAttribute:該注解作用于方法和請求參數(shù)上,在方法上時(shí)設(shè)置一個(gè)值,可以直接在進(jìn)入controller后傳入該參數(shù)。
2 ControllerAdvice應(yīng)用場景
2.1@ExceptionHandler統(tǒng)一處理業(yè)務(wù)異常
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 這里就是對各個(gè)層返回的異常進(jìn)行統(tǒng)一捕獲處理
@ExceptionHandler(value = BusinessException.class)
public ResponseData<Void> bizException(BusinessException e){
log.error("業(yè)務(wù)異常記錄",e);
return ResponseData.error(e.getCode(),e.getMessage());
}
}
//業(yè)務(wù)異常處代碼示例:
if(CollectionUtil.isNotEmpty(companies)){
// 通過BusinessExceptionEnum枚舉對業(yè)務(wù)異常進(jìn)行統(tǒng)一管理
throw new BusinessException(BusinessExceptionEnum.ERROR_10003);
}
需要注意的是,如果這里有多個(gè)ExceptionHandler,按照異常類的層次體系,越高層的異常,優(yōu)先級越低。
2.2@InitBinder做日期格式的統(tǒng)一處理
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 將前端傳入的字符串時(shí)間格式轉(zhuǎn)換為LocalDate時(shí)間
@InitBinder
protected void initBinder(WebDataBinder binder) {
//將前端傳入的字符串格式時(shí)間數(shù)據(jù)轉(zhuǎn)為LocalDate格式的數(shù)據(jù)
binder.registerCustomEditor(LocalDate.class, new PropertyEditorSupport() {
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(LocalDate.parse(text, DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
}
});
//將前端傳入的字符串格式時(shí)間數(shù)據(jù)轉(zhuǎn)為LocalDateTime格式的數(shù)據(jù)
binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport() {
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(LocalDateTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
}
});
//將前端傳入的字符串格式時(shí)間數(shù)據(jù)轉(zhuǎn)為LocalTim格式的數(shù)據(jù)
binder.registerCustomEditor(LocalTime.class, new PropertyEditorSupport() {
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(LocalTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
}
});
}
}
// controller進(jìn)行參數(shù)綁定
public ResponseData<List<WorkCalendarVo>> listWorkCalendar(@RequestParam LocalDate date){}
2.3 ModelAttribute提前綁定全局user對象
// 這里@ModelAttribute("loginUser")標(biāo)注的modelAttribute()方法表示會在Controller方法之前將user設(shè)置到contoller里的已綁定參數(shù)里
@ModelAttribute("loginUser")
public User setLoginUser(HttpServletRequest request) {
return LoginContextUtils.getLoginUser(request);
}
// 使用
@PostMapping("/list")
public ResponseData<IPage<EmployeeVo>> listEmployee(@ModelAttribute("loginUser") User user, @RequestBody EmployeeSearch employeeSearch){
return ResponseData.success(employeeService.listEmployee(user, employeeSearch));
}
3 ControllerAdvice作用原理探究
在探究ControllerAdvice如何生效時(shí),不得不提到springMvc繞不過的DispatcherServlet,這個(gè)類是SpringMVC統(tǒng)一的入口,所有的請求都通過它,里面的一些初始化方法如下。
public class DispatcherServlet extends FrameworkServlet {
// ......
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
//請求處理的adapter
initHandlerAdapters(context);
// 異常響應(yīng)處理的resolver
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
// ......
}
3.1@initBinder和@ModelAttribute的作用原理
@initBinder和@ModelAttribute都是請求過程中的處理,我們知道springMvc通過HandlerApapter定位到具體的方法進(jìn)行請求處理,因此查看HandlerHaper的實(shí)現(xiàn)類,發(fā)現(xiàn)RequestMappingHandlerAdapter比較符合我們的目標(biāo)
點(diǎn)進(jìn)去RequestMappingHandlerAdapter后發(fā)現(xiàn)里面的一個(gè)方法如下
@Override
public void afterPropertiesSet() {
// Do this first, it may add ResponseBody advice beans
// 這里會添加ResponseBody advice beans
initControllerAdviceCache();
if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.initBinderArgumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}
// 這里找到contollerAdvice注解的類,緩存里面的方法
private void initControllerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
// 找到@ControllerAdvice注解標(biāo)注的類
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
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標(biāo)注的方法進(jìn)行緩存,就可以使用了
Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
if (!attrMethods.isEmpty()) {
this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
}
// 找到所有initBinder注解標(biāo)注的方法進(jìn)行緩存,就可以使用了
Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
if (!binderMethods.isEmpty()) {
this.initBinderAdviceCache.put(adviceBean, binderMethods);
}
if (RequestBodyAdvice.class.isAssignableFrom(beanType) || ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
requestResponseBodyAdviceBeans.add(adviceBean);
}
}
if (!requestResponseBodyAdviceBeans.isEmpty()) {
this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
}
// ......日志處理
}
3.2@ExceptionHandler注解的作用原理
相同的思路,@ExceptionHandler是響應(yīng)時(shí)的處理,因此需要找到對應(yīng)的Resolver,進(jìn)入initHandlerExceptionResolvers(context)方法,
屬性填充后會進(jìn)行afterPropertiesSet方法,這個(gè)方法可以用在一些特殊情況中,也就是某個(gè)對象的某個(gè)屬性需要經(jīng)過外界得到,比如說查詢數(shù)據(jù)庫等方式,這時(shí)候可以用到spring的該特性,只需要實(shí)現(xiàn)InitializingBean。
@Override
public void afterPropertiesSet() {
// Do this first, it may add ResponseBodyAdvice beans
initExceptionHandlerAdviceCache();
if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}
private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
// 這里找到ExceptionHandler注解標(biāo)注的方法進(jìn)行緩存,后面就可以使用了
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
this.responseBodyAdvice.add(adviceBean);
}
}
// ......日志處理
}
在啟動spring時(shí)debug發(fā)現(xiàn)最終也會走到這里對@ExceptionHander注解的方法已經(jīng)緩存
當(dāng)Controller拋出異常時(shí),DispatcherServlet通過ExceptionHandlerExceptionResolver來解析異常,而ExceptionHandlerExceptionResolver又通過ExceptionHandlerMethodResolver 來解析異常, ExceptionHandlerMethodResolver 最終解析異常找到適用的@ExceptionHandler標(biāo)注的方法是這里:
@Nullable
public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
Method method = this.exceptionLookupCache.get(exceptionType);
if (method == null) {
method = getMappedMethod(exceptionType);
this.exceptionLookupCache.put(exceptionType, method);
}
return (method != NO_MATCHING_EXCEPTION_HANDLER_METHOD ? method : null);
}
4 用具體的調(diào)用過程,驗(yàn)證上面的推測
本部分通過對DispatcherServlet的調(diào)用過程跟蹤,梳理出ControllerAdvice的作用原理,以@InitBinder主節(jié)點(diǎn)生效過程為例。
首選是dispathServlet在初始化過程中,初始化RequestMappingHandlerAdapter過程中打斷點(diǎn)發(fā)現(xiàn),initBinder已經(jīng)緩存進(jìn)來了。
然后是dispatcherServlet的調(diào)用流程圖,驗(yàn)證下是initBinder注解是否生效。
DispatcherServlet 通過doService()方法開始調(diào)用,主要邏輯包括 設(shè)置 request ,通過doDispatch() 進(jìn)行請求分發(fā)處理。
doDispatch() 的主要過程是通過 HandlerMapping 獲取 Handler,再找到用于執(zhí)行它的 HandlerAdapter,執(zhí)行 Handler 后得到 ModelAndView ,ModelAndView 是連接“業(yè)務(wù)邏輯層”與“視圖展示層”的橋梁。
4.1 DispathcerServlet的doDispatch方法
在入口處找到要執(zhí)行的HandlerAdapter,調(diào)用handle方法繼續(xù)
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
// 找到執(zhí)行鏈,根據(jù)請求路徑匹配到controller的方法
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
// 找到對應(yīng)的HandlerAdapter,執(zhí)行鏈中的handler類型為HandlerMethod的.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler. 真正進(jìn)行處理的地方
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
..........
}
4.2 RequestmappingHanderApapter對@initBInder注解緩存方法進(jìn)行處理
找到對應(yīng)的handlerAdapter后進(jìn)入invokeHandlerMethod()方法,在這里通過構(gòu)建WebDataBinderFactory對initBinder注解進(jìn)行構(gòu)建,供后續(xù)使用,具體邏輯如下。
通過getDataBinderFactory()方法從之前緩存的Map> initBinderAdviceCache中生成binderFactory
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
//根據(jù)initBinder注解,獲取對應(yīng)的factory,主要成員是InvocableHandlerMethod,就包括之前緩存的。
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
// 創(chuàng)建可調(diào)用的對象,進(jìn)行調(diào)用邏輯處理
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
// binderFactory設(shè)置進(jìn)invocableMethod,
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
asyncManager.clearConcurrentResult();
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(result, !traceOn);
return "Resume with async result [" + formatted + "]";
});
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}
// 繼續(xù)進(jìn)行處理
invocableMethod.invokeAndHandle(webRequest, mavContainer);
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}
return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}
// 生成WebDataBinderFactory的具體邏輯
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
Class<?> handlerType = handlerMethod.getBeanType();
Set<Method> methods = this.initBinderCache.get(handlerType);
if (methods == null) {
methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
this.initBinderCache.put(handlerType, methods);
}
List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
// Global methods first 獲取之前項(xiàng)目啟動緩存的initMethod
this.initBinderAdviceCache.forEach((controllerAdviceBean, methodSet) -> {
if (controllerAdviceBean.isApplicableToBeanType(handlerType)) {
Object bean = controllerAdviceBean.resolveBean();
for (Method method : methodSet) {
initBinderMethods.add(createInitBinderMethod(bean, method));
}
}
});
for (Method method : methods) {
Object bean = handlerMethod.getBean();
initBinderMethods.add(createInitBinderMethod(bean, method));
}
return createDataBinderFactory(initBinderMethods);
}
經(jīng)過上面的處理,發(fā)現(xiàn)initBinder標(biāo)注的注解方法已經(jīng)成功緩存進(jìn)bindFactory。
4.3 繼續(xù)調(diào)用getMethodArgumentValues進(jìn)行后續(xù)處理
繼續(xù)往下跟蹤,進(jìn)入InvocableHandlerMethod的invokeForRequest方法,里面有g(shù)etMethodArgumentValues方法,會對請求參數(shù)進(jìn)行處理。
最終使用AbstractNamedValueMethodArgumentResolver的resolveArgument()方法對請求字符串格式數(shù)據(jù)進(jìn)行處理
// 請求Controller方法如下
public ResponseData<IPage<CompanyVo>> listCompany(HttpServletRequest servletRequest, @RequestBody CompanySearch companySearch, @RequestParam LocalDate localDate){
getLoginUser(servletRequest);
return ResponseData.success(companyService.listCompany(companySearch));
}
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 得到方法的參數(shù)列表
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
Object[] args = new Object[parameters.length];
// 循環(huán)如處理請求參數(shù)
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
// 真正進(jìn)行參數(shù)處理的地方
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}
// 最終會使用AbstractNamedValueMethodArgumentResolver來進(jìn)行處理
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
// 得到請求參數(shù)名稱為"localdate"
Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException(
"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
}
// 獲取請求的locadate的值,此時(shí)為字符串格式"yyyy-mm-dd"
Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
if (arg == null) {
if (namedValueInfo.defaultValue != null) {
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
}
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
// 這里就會使用bindFactory進(jìn)行處理
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
try {
// 經(jīng)過這里進(jìn)行處理,輸入的string類型就會轉(zhuǎn)為LocalDate了
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
}
catch (ConversionNotSupportedException ex) {
throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
namedValueInfo.name, parameter, ex.getCause());
}
catch (TypeMismatchException ex) {
throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
namedValueInfo.name, parameter, ex.getCause());
}
// Check for null value after conversion of incoming argument value
if (arg == null && namedValueInfo.defaultValue == null &&
namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValueAfterConversion(namedValueInfo.name, nestedParameter, webRequest);
}
}
handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
return arg;
}
最后附上上面調(diào)用過程中一些類的介紹
以上就是ControllerAdivce的全介紹。通過對源碼的學(xué)習(xí),加深了對HTTP請求過程的理解。
參考:https://blog.csdn.net/zmm__1377445292/article/details/116158554
作者:京東物流 付鵬嘎
來源:京東云開發(fā)者社區(qū) 自猿其說Tech