龙空技术网

@ControllerAdvice注解使用及原理探究

京东云开发者 1016

前言:

此时同学们对“前端时间格式转换”大约比较着重,看官们都想要分析一些“前端时间格式转换”的相关文章。那么小编在网摘上网罗了一些对于“前端时间格式转换””的相关文章,希望兄弟们能喜欢,朋友们一起来学习一下吧!

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

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

1 ControllerAdvice介绍

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

@ExceptionHandler: 该注解作用于方法上,,可以捕获到controller中抛出的一些自定义异常,统一进行处理,一般用于进行一些特定的异常处理。@InitBinder:该注解作用于方法上,用于将前端请求的特定类型的参数在到达controller之前进行处理,从而达到转换请求参数格式的目的。@ModelAttribute:该注解作用于方法和请求参数上,在方法上时设置一个值,可以直接在进入controller后传入该参数。2 ControllerAdvice应用场景2.1@ExceptionHandler统一处理业务异常

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

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

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

@RestControllerAdvice@Slf4jpublic class GlobalExceptionHandler {// 将前端传入的字符串时间格式转换为LocalDate时间  @InitBinder    protected void initBinder(WebDataBinder binder) {//将前端传入的字符串格式时间数据转为LocalDate格式的数据        binder.registerCustomEditor(LocalDate.class, new PropertyEditorSupport() {            @Override            public void setAsText(String text) throws IllegalArgumentException {                setValue(LocalDate.parse(text, DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));            }        });//将前端传入的字符串格式时间数据转为LocalDateTime格式的数据        binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport() {            @Override            public void setAsText(String text) throws IllegalArgumentException {                setValue(LocalDateTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));            }        });//将前端传入的字符串格式时间数据转为LocalTim格式的数据        binder.registerCustomEditor(LocalTime.class, new PropertyEditorSupport() {            @Override            public void setAsText(String text) throws IllegalArgumentException {                setValue(LocalTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));            }        });    }}// controller进行参数绑定public ResponseData<List<WorkCalendarVo>> listWorkCalendar(@RequestParam LocalDate  date){}
2.3 ModelAttribute提前绑定全局user对象
// 这里@ModelAttribute("loginUser")标注的modelAttribute()方法表示会在Controller方法之前将user设置到contoller里的已绑定参数里    @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如何生效时,不得不提到springMvc绕不过的DispatcherServlet,这个类是SpringMVC统一的入口,所有的请求都通过它,里面的一些初始化方法如下。

public class DispatcherServlet extends FrameworkServlet {    // ......    protected void initStrategies(ApplicationContext context) {        initMultipartResolver(context);        initLocaleResolver(context);        initThemeResolver(context);        initHandlerMappings(context);//请求处理的adapter        initHandlerAdapters(context);// 异常响应处理的resolver        initHandlerExceptionResolvers(context);        initRequestToViewNameTranslator(context);        initViewResolvers(context);        initFlashMapManager(context);    }    // ......}
3.1@initBinder和@ModelAttribute的作用原理

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

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

@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注解标注的类        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);        }// ......日志处理    }
3.2@ExceptionHandler注解的作用原理

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

属性填充后会进行afterPropertiesSet方法,这个方法可以用在一些特殊情况中,也就是某个对象的某个属性需要经过外界得到,比如说查询数据库等方式,这时候可以用到spring的该特性,只需要实现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注解标注的方法进行缓存,后面就可以使用了            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注解的方法已经缓存

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

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

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

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

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

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;        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.// 找到执行链,根据请求路径匹配到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();                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. 真正进行处理的地方                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

@Nullable    protected 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()) {                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 获取之前项目启动缓存的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);    }

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

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

继续往下跟踪,进入InvocableHandlerMethod的invokeForRequest方法,里面有getMethodArgumentValues方法,会对请求参数进行处理。

最终使用AbstractNamedValueMethodArgumentResolver的resolveArgument()方法对请求字符串格式数据进行处理

// 请求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 {// 得到方法的参数列表        MethodParameter[] parameters = getMethodParameters();        if (ObjectUtils.isEmpty(parameters)) {            return EMPTY_ARGS;        }        Object[] args = new Object[parameters.length];// 循环如处理请求参数        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 {// 真正进行参数处理的地方                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来进行处理public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,            NativeWebRequest webRequest, @Nullable WebDataBinderFactory 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);            }            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进行处理        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 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;    }

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

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

参考:

作者:京东物流 付鹏嘎

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

标签: #前端时间格式转换