@ControllerAdvice注解使用及原理探究

@initBinder和@ModelAttribute都是请求过程中的处理,我们知道springMv
首页 新闻资讯 行业资讯 @ControllerAdvice注解使用及原理探究

最近在新项目的开发过程中,遇到了个问题,需要将一些异常的业务流程返回给前端,需要提供给前端不同的响应码,前端再在次基础上做提示语言的国际化适配。这些异常流程涉及业务层和控制层的各个地方,如果每个地方都写一些重复代码显得很冗余。

然后查询解决方案时发现了@ControllerAdvice这个注解,可以对业务异常进行统一处理。经过仔细了解后,发现这个注解还有更多的用处,都很实用。

1 ControllerAdvice介绍

@ControllerAdvice一般和三个以下注解一块使用,起到不同的作用,

  • @ExceptionHandler: 该注解作用于方法上,,可以捕获到controller中抛出的一些自定义异常,统一进行处理,一般用于进行一些特定的异常处理。

  • @InitBinder:该注解作用于方法上,用于将前端请求的特定类型的参数在到达controller之前进行处理,从而达到转换请求参数格式的目的。

  • @ModelAttribute:该注解作用于方法和请求参数上,在方法上时设置一个值,可以直接在进入controller后传入该参数。

2 ControllerAdvice应用场景

2.1@ExceptionHandler统一处理业务异常

@RestControllerAdvice@Slf4jpublicclass GlobalExceptionHandler {// 这里就是对各个层返回的异常进行统一捕获处理@ExceptionHandler(value=BusinessException.class)publicResponseData<Void>bizException(BusinessException e){
        log.error("业务异常记录",e);returnResponseData.error(e.getCode(),e.getMessage());}
}//业务异常处代码示例:if(CollectionUtil.isNotEmpty(companies)){// 通过BusinessExceptionEnum枚举对业务异常进行统一管理throw new BusinessException(BusinessExceptionEnum.ERROR_10003);}

需要注意的是,如果这里有多个ExceptionHandler,按照异常类的层次体系,越高层的异常,优先级越低。

2.2@InitBinder做日期格式的统一处理

@RestControllerAdvice@Slf4jpublicclass GlobalExceptionHandler {// 将前端传入的字符串时间格式转换为LocalDate时间@InitBinderprotected void initBinder(WebDataBinder binder){//将前端传入的字符串格式时间数据转为LocalDate格式的数据binder.registerCustomEditor(LocalDate.class,new PropertyEditorSupport(){@Overridepublicvoid setAsText(Stringtext)throws IllegalArgumentException {
                setValue(LocalDate.parse(text,DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));}
        });//将前端传入的字符串格式时间数据转为LocalDateTime格式的数据binder.registerCustomEditor(LocalDateTime.class,new PropertyEditorSupport(){@Overridepublicvoid setAsText(Stringtext)throws IllegalArgumentException {
                setValue(LocalDateTime.parse(text,DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));}
        });//将前端传入的字符串格式时间数据转为LocalTim格式的数据binder.registerCustomEditor(LocalTime.class,new PropertyEditorSupport(){@Overridepublicvoid setAsText(Stringtext)throws IllegalArgumentException {
                setValue(LocalTime.parse(text,DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));}
        });}
}// controller进行参数绑定publicResponseData<List<WorkCalendarVo>>listWorkCalendar(@RequestParamLocalDatedate){}

2.3 ModelAttribute提前绑定全局user对象

// 这里@ModelAttribute("loginUser")标注的modelAttribute()方法表示会在Controller方法之前将user设置到contoller里的已绑定参数里@ModelAttribute("loginUser")publicUsersetLoginUser(HttpServletRequest request){returnLoginContextUtils.getLoginUser(request);}// 使用@PostMapping("/list")publicResponseData<IPage<EmployeeVo>>listEmployee(@ModelAttribute("loginUser")Useruser,@RequestBodyEmployeeSearch employeeSearch){returnResponseData.success(employeeService.listEmployee(user,employeeSearch));}

3 ControllerAdvice作用原理探究

在探究ControllerAdvice如何生效时,不得不提到springMvc绕不过的DispatcherServlet,这个类是SpringMVC统一的入口,所有的请求都通过它,里面的一些初始化方法如下。

publicclass DispatcherServlet extends FrameworkServlet {// ......protected void initStrategies(ApplicationContext context){
        initMultipartResolver(context);initLocaleResolver(context);initThemeResolver(context);initHandlerMappings(context);//请求处理的adapterinitHandlerAdapters(context);// 异常响应处理的resolverinitHandlerExceptionResolvers(context);initRequestToViewNameTranslator(context);initViewResolvers(context);initFlashMapManager(context);}// ......}

3.1@initBinder和@ModelAttribute的作用原理

@initBinder和@ModelAttribute都是请求过程中的处理,我们知道springMvc通过HandlerApapter定位到具体的方法进行请求处理,因此查看HandlerHaper的实现类,发现RequestMappingHandlerAdapter比较符合我们的目标

254b2c1098d53ee65e8781b96caa0fceebc433.png

点进去RequestMappingHandlerAdapter后发现里面的一个方法如下

@Overridepublicvoid afterPropertiesSet(){// Do this first, it may add ResponseBody advice beans// 这里会添加ResponseBody advice beansinitControllerAdviceCache();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注解标注的类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标注的方法进行缓存,就可以使用了Set<Method>attrMethods=MethodIntrospector.selectMethods(beanType,MODEL_ATTRIBUTE_METHODS);if(!attrMethods.isEmpty()){
                this.modelAttributeAdviceCache.put(adviceBean,attrMethods);}// 找到所有initBinder注解标注的方法进行缓存,就可以使用了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);}// ......日志处理}

81f33fb23e22d564c929480986a99d61f048cc.png

3.2@ExceptionHandler注解的作用原理

相同的思路,@ExceptionHandler是响应时的处理,因此需要找到对应的Resolver,进入initHandlerExceptionResolvers(context)方法,

d9d9d1a34e7dd3d0dca63408b965d44547c39a.png

属性填充后会进行afterPropertiesSet方法,这个方法可以用在一些特殊情况中,也就是某个对象的某个属性需要经过外界得到,比如说查询数据库等方式,这时候可以用到spring的该特性,只需要实现InitializingBean。

@Overridepublicvoid afterPropertiesSet(){// Do this first, it may add ResponseBodyAdvice beansinitExceptionHandlerAdviceCache();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注解标注的方法进行缓存,后面就可以使用了ExceptionHandlerMethodResolver resolver=new ExceptionHandlerMethodResolver(beanType);if(resolver.hasExceptionMappings()){
                this.exceptionHandlerAdviceCache.put(adviceBean,resolver);}if(ResponseBodyAdvice.class.isAssignableFrom(beanType)){
                this.responseBodyAdvice.add(adviceBean);}
        }// ......日志处理}

在启动spring时debug发现最终也会走到这里对@ExceptionHander注解的方法已经缓存

d54a87d660f68c62a4011531002c6f64c1676f.png

当Controller抛出异常时,DispatcherServlet通过ExceptionHandlerExceptionResolver来解析异常,而ExceptionHandlerExceptionResolver又通过ExceptionHandlerMethodResolver 来解析异常, ExceptionHandlerMethodResolver 最终解析异常找到适用的@ExceptionHandler标注的方法是这里:

@NullablepublicMethod 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 用具体的调用过程,验证上面的推测

本部分通过对DispatcherServlet的调用过程跟踪,梳理出ControllerAdvice的作用原理,以@InitBinder主节点生效过程为例。

首选是dispathServlet在初始化过程中,初始化RequestMappingHandlerAdapter过程中打断点发现,initBinder已经缓存进来了。

63ae9d966a697e0fc16679d5a099d8f9dff7ea.png

然后是dispatcherServlet的调用流程图,验证下是initBinder注解是否生效。

49cb405802a419f4d4029207233fd09e4501bd.png

DispatcherServlet 通过doService()方法开始调用,主要逻辑包括 设置 request ,通过doDispatch() 进行请求分发处理。

doDispatch() 的主要过程是通过 HandlerMapping 获取 Handler,再找到用于执行它的 HandlerAdapter,执行 Handler 后得到 ModelAndView ,ModelAndView 是连接“业务逻辑层”与“视图展示层”的桥梁。

4.1 DispathcerServlet的doDispatch方法

在入口处找到要执行的HandlerAdapter,调用handle方法继续

protected void doDispatch(HttpServletRequest request,HttpServletResponse response)throws Exception {
        HttpServletRequest processedRequest=request;HandlerExecutionChain mappedHandler=null;booleanmultipartRequestParsed=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.// 找到执行链,根据请求路径匹配到controller的方法mappedHandler=getHandler(processedRequest);if(mappedHandler==null){
                    noHandlerFound(processedRequest,response);return;}// Determine handler adapter for the current request.// 找到对应的HandlerAdapter,执行链中的handler类型为HandlerMethod的.HandlerAdapter ha=getHandlerAdapter(mappedHandler.getHandler());// Process last-modified header, if supported by the handler.String method=request.getMethod();booleanisGet=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. 真正进行处理的地方mv=ha.handle(processedRequest,response,mappedHandler.getHandler());if(asyncManager.isConcurrentHandlingStarted()){return;}

                applyDefaultViewName(processedRequest,mv);mappedHandler.applyPostHandle(processedRequest,response,mv);..........}

4.2 RequestmappingHanderApapter对@initBInder注解缓存方法进行处理

找到对应的handlerAdapter后进入invokeHandlerMethod()方法,在这里通过构建WebDataBinderFactory对initBinder注解进行构建,供后续使用,具体逻辑如下。
通过getDataBinderFactory()方法从之前缓存的Map> initBinderAdviceCache中生成binderFactory

@Nullableprotected ModelAndView invokeHandlerMethod(HttpServletRequest request,HttpServletResponse response,HandlerMethod handlerMethod)throws Exception {

        ServletWebRequest webRequest=new ServletWebRequest(request,response);try {//根据initBinder注解,获取对应的factory,主要成员是InvocableHandlerMethod,就包括之前缓存的。WebDataBinderFactory binderFactory=getDataBinderFactory(handlerMethod);ModelFactory modelFactory=getModelFactory(handlerMethod,binderFactory);// 创建可调用的对象,进行调用逻辑处理ServletInvocableHandlerMethod invocableMethod=createInvocableHandlerMethod(handlerMethod);if(this.argumentResolvers!=null){
                invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);}if(this.returnValueHandlers!=null){
                invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);}// binderFactory设置进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);}// 继续进行处理invocableMethod.invokeAndHandle(webRequest,mavContainer);if(asyncManager.isConcurrentHandlingStarted()){returnnull;}returngetModelAndView(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 获取之前项目启动缓存的initMethodthis.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));}returncreateDataBinderFactory(initBinderMethods);}

经过上面的处理,发现initBinder标注的注解方法已经成功缓存进bindFactory。

b30e3bd72e2a22e73ba627567f5a340dbfae96.png

4.3 继续调用getMethodArgumentValues进行后续处理

继续往下跟踪,进入InvocableHandlerMethod的invokeForRequest方法,里面有getMethodArgumentValues方法,会对请求参数进行处理。
最终使用AbstractNamedValueMethodArgumentResolver的resolveArgument()方法对请求字符串格式数据进行处理

// 请求Controller方法如下publicResponseData<IPage<CompanyVo>>listCompany(HttpServletRequest servletRequest,@RequestBodyCompanySearch companySearch,@RequestParamLocalDate localDate){
       getLoginUser(servletRequest);returnResponseData.success(companyService.listCompany(companySearch));}

protected Object[]getMethodArgumentValues(NativeWebRequest request,@NullableModelAndViewContainer mavContainer,Object...providedArgs)throws Exception {// 得到方法的参数列表MethodParameter[]parameters=getMethodParameters();if(ObjectUtils.isEmpty(parameters)){returnEMPTY_ARGS;}

        Object[]args=new Object[parameters.length];// 循环如处理请求参数for(inti=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 {// 真正进行参数处理的地方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;}
        }returnargs;}// 最终会使用AbstractNamedValueMethodArgumentResolver来进行处理publicfinal Object resolveArgument(MethodParameter parameter,@NullableModelAndViewContainer mavContainer,NativeWebRequest webRequest,@NullableWebDataBinderFactory binderFactory)throws Exception {

        NamedValueInfo namedValueInfo=getNamedValueInfo(parameter);MethodParameter nestedParameter=parameter.nestedIfOptional();// 得到请求参数名称为"localdate"Object resolvedName=resolveEmbeddedValuesAndExpressions(namedValueInfo.name);if(resolvedName==null){
            throw new IllegalArgumentException("Specified name must not resolve to null: ["+namedValueInfo.name+"]");}// 获取请求的locadate的值,此时为字符串格式"yyyy-mm-dd"Object arg=resolveName(resolvedName.toString(),nestedParameter,webRequest);if(arg==null){if(namedValueInfo.defaultValue!=null){
                arg=resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);}elseif(namedValueInfo.required&&!nestedParameter.isOptional()){
                handleMissingValue(namedValueInfo.name,nestedParameter,webRequest);}
            arg=handleNullValue(namedValueInfo.name,arg,nestedParameter.getNestedParameterType());}elseif("".equals(arg)&&namedValueInfo.defaultValue!=null){
            arg=resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);}// 这里就会使用bindFactory进行处理if(binderFactory!=null){
            WebDataBinder binder=binderFactory.createBinder(webRequest,null,namedValueInfo.name);try {// 经过这里进行处理,输入的string类型就会转为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 valueif(arg==null&&namedValueInfo.defaultValue==null&&namedValueInfo.required&&!nestedParameter.isOptional()){
                handleMissingValueAfterConversion(namedValueInfo.name,nestedParameter,webRequest);}
        }

        handleResolvedValue(arg,namedValueInfo.name,parameter,mavContainer,webRequest);returnarg;}

最后附上上面调用过程中一些类的介绍

54130633528da9e04f070395135f102cdf0633.jpg

以上就是ControllerAdivce的全介绍。通过对源码的学习,加深了对HTTP请求过程的理解。

参考:https://blog.csdn.net/zmm__1377445292/article/details/116158554

作者:京东物流 付鹏嘎

来源:京东云开发者社区 自猿其说Tech