SpringBoot中一次请求会走两次Controller的问题

FBIWarning

大神勿进,写的啰嗦且浅显,免得浪费您的时间。

 

前言

维护同事的一段代码,发现他使用的事务隔离级别为SERIALIZABLE,读了代码,发现完全没必要,遂问之。
答曰,前端会莫名其妙发送两个一模一样的请求过来导致数据会增加两条,让前端查过找不到原因,所以就提升隔离级别。
顿时,内心犹如一万只草泥马奔腾而过,本着码农应有的探索精神(闲的蛋疼),我决定一探究竟。

正题

异常

would dispatch back to the current handler URL [/xxx.html] again. Check your ViewResolver setup! (Hint: This may be the result of an unspecified view, due to default view name generation.)

导致“一次请求多次调用”这个现象可能有各种各样意想不到的原因,但如果控制台打印了类似上面的错误信息,那基本没跑了,就是咱今天要讲的这个问题导致的。

 

复现问题

这个问题咋一听很诡异,那先复现问题再说吧。于是打断点,Debug。好家伙!果然走了两次!

秋多麻袋!但前端并没有发送两次请求呀,所以这问题基本上锁定了是后端哪儿个地方没弄对导致的。

 

伪代码说明

@Controller
@RequestMapping("api/xxx")
public class XXXController {

    @Autowired
    XXXService xxxService;

    @RequestMapping(value = "xxx", method = RequestMethod.GET)
    public XXX doXXX(Integer xxxId, ...) throws Exception {
        ...

        XXX xxx = xxxService.doXXX(xxxId, ...);
        
        ...

        return xxx;
    }
}


public class XXXServiceImpl implements XXXService {

    @Autowired
    XXXMapper xxxMapper;

    @Transactional(rollbackFor = Exception.class, isolation = Isolation.SERIALIZABLE)
    public XXX doXXX(Integer xxxId, ...) throws Exception {
        ...
        
        XXXDbo xxxDbo = xxxMapper.selectById(xxxId);
        if (xxxDbo != null) {
            throw new XXXException("data already exists");
        }

        ...
    }
}

 

单步调试

不知道各位大神看了如上代码之后能看出问题吗?

emmmm…好吧,开始说正题。

这个就要从盘古开天辟地之时说起…咳咳,给我回来!

这个就要从@Controller和@RestController注解说起,一般(我个人)在开发API的时候都使用后者,开发未前后端分离的管理系统的时候都用前者,为什么呢?

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
    ...
}

看到没,RestController相当于Controller和ResponseBody注解的组合,换言之,两者之间不外乎差了一个ResponseBody,那这个ResponseBody小哥哥又是干什么用的呢?

这个就要稍微涉及SpringMVC的处理流程了,而这个话题怎么都绕不开DispatcherServlet,所有的请求都得经过他的手进行“分发”,才会最终走到我们的Controller里面。

好了,不废话,直接贴代码,看看关键的调用链吧!

//DispatcherServlet.java
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    ...

    // Actually invoke the handler.
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

    ...

    //为什么要贴这句代码?留个伏笔有木有[阴笑]
    applyDefaultViewName(processedRequest, mv);

    ...
}

//AbstractHandlerMethodAdapter.java
@Override
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
    return handleInternal(request, response, (HandlerMethod) handler);
}

//RequestMappingHandlerAdapter.java
@Override
protected ModelAndView handleInternal(HttpServletRequest request,
			HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
    ...
    
    mav = invokeHandlerMethod(request, response, handlerMethod);

    ...
}
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
			HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
    ...

    invocableMethod.invokeAndHandle(webRequest, mavContainer);

    ...

    //伏笔+1
    return getModelAndView(mavContainer, modelFactory, webRequest);

    //还有finally块
    ...
}

//ServletInvocableHandlerMethod.java
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer  mavContainer, Object... providedArgs) throws Exception {
    //这段接着走,会最终走到我们的Controller里面
    Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
    
    ...

    //这段就是开始处理我们Controller里面的返回值
    this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);

    ...
}

最后那个returnValue就是我们Controller里面返回的xxx,也就说走到这儿,我们的业务代码实际上就执行完了,而接下来才是重头戏,接着看代码!

//HandlerMethodReturnValueHandlerComposite.java
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws  Exception {
    HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);

    ...
}
private HandlerMethodReturnValueHandler selectHandler(Object value, MethodParameter returnType) {
    ...

    //这里的returnValueHandlers是spring给我们提供的针对各种类型返回数据的处理器
    //如果没有业务需要的,也可以实现其接口然后注入即可使用自定义的处理器
    for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
        ...

        //终于要说到ResponseBody了
        if (handler.supportsReturnType(returnType)) {
            return handler;
        }
    }
    return null;
}

看到handler.supportsReturnType(returnType)这句代码了吗,这就是在判断返回的数据类型需要由哪个处理器来进行处理,Spring预设了有好几个,我们重点关注其中两个:RequestResponseBodyMethodProcessor和ModelAttributeMethodProcessor,为什么呢?

我们分别来看他们的supportsReturnType方法

//RequestResponseBodyMethodProcessor.java
@Override
public boolean supportsReturnType(MethodParameter returnType) {
    return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || returnType.hasMethodAnnotation(ResponseBody.class));
}

//ModelAttributeMethodProcessor.java
@Override
public boolean supportsReturnType(MethodParameter parameter) {
    return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
				(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
}

很激动有木有,看了辣么久的代码,总算见到了我们可爱又帅气的ResponseBody小哥哥了,这句代码意思也很明显了吧:

只要所在的class或者所在的method添加了ResponseBody注解,那么就用我来作为处理器!

而下面那个则是:

没有相应的注解,且不是常规的数据类型,那么就选我!

(其实这里还涉及先后顺序的问题,就不展开说了,你懂得[滑稽])

如果到这里就结束了,你可能要给差评了,这扯了一大堆,其实就说了一点,有没有ResponseBody其实就是对应的返回数据Processor不同而已,为什么这个不同会导致再发起一次请求还没说呀!别急,接下来就要说到。(其实急的很是吧[吐舌])

细心点的同学可能看到前面RequestMappingHandlerAdapter中的getModelAndView(mavContainer, modelFactory, webRequest)方法,看看里面是怎么来构造的吧。

private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
			ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {
    ...

    ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());
    if (!mavContainer.isViewReference()) {
	mav.setView((View) mavContainer.getView());
    }

    ...
}

很好,很强大,直接new出来的。紧接着就会对ModelAndView设置View属性(如/login.jsp这种,很熟悉吧?),很不幸,经过咱ModelAttributeMethodProcessor大哥处理的后的,这个属性是null。那么null又会导致什么问题呢?

又要回到我们熟悉的DispatcherServlet中,看到applyDefaultViewName(processedRequest, mv)方法没

//DispatcherServlet.java
private void applyDefaultViewName(HttpServletRequest request, ModelAndView mv) throws Exception {
    if (mv != null && !mv.hasView()) {
        mv.setViewName(getDefaultViewName(request));
    }
}
protected String getDefaultViewName(HttpServletRequest request) throws Exception {
    return this.viewNameTranslator.getViewName(request);
}

//DefaultRequestToViewNameTranslator.java
public String getViewName(HttpServletRequest request) {
		String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
    return (this.prefix + transformPath(lookupPath) + this.suffix);
}

看到没,他给咱的ModelAndView设置了ViewName,而且值就是我们请求的路径,所以后面处理的时候当然就会再调用一次啦!因为他把原来的接口当做处理页面来进行调用了。(这句描述可能不准确,海涵)

 

结语

罗里吧嗦讲了一大堆,不知道各位看官看明白没有?其实就很简单一事,只是为了搞清来龙去脉所以才贴了这么多代码的。

最后来一句可能不是很准确的结论吧:

没有加ResponseBody会导致返回对象的Processor使用ModelAttributeMethodProcessor,而他会构造一个ModelAndView对象,且对象会最终设置调用接口的路径为view属性。

加了ResponseBody就会使用RequestResponseBodyMethodProcessor,他不会构造ModelAndView(另外的逻辑,不展开了),也就不会有这个问题。

 

最后再抛个问题,知道为什么同事通过提升事务的隔离级别也能解决一次请求会增加两条数据的问题吗?且待下回(再次闲的蛋疼时)分解[滑稽]

    原文作者:hoew
    原文地址: https://blog.csdn.net/aa54682002/article/details/99646175
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞