- 2023-04-20 10:14:19
- 368 热度
- 0 评论
[TOC]
在 Spring Boot 项目中,有的时候我们想返回一段 JSON,结果却忘了写 @ResponseBody
注解,像下面这样:
1 |
@Controller |
这个时候当项目跑起来,肯定会报错,具体报什么错,则要看用的什么视图解析器,如果用了 Freemarker,你可能会看到如下错误:
这个错误是说陷入到循环调用中了。
如果用了 Thymeleaf,你可能会看到如下错误:
这个是说一个名叫 01 的视图不存在。
我只是少加了一个 @ResponseBody
注解而已,为什么用不同的视图解析器会报不同的错误?并且这些错误实在看不出和 @ResponseBody
注解有什么关联。
無名今天就通过源码分析,来和大家把这个问题讲清楚。
1.方法入口
前面無名刚刚和大家分享了 DispatcherServlet 的源码,并且和大家细致分析了 doDispatch 方法的执行步骤,还没看的小伙伴可以先看看:
在这篇文章中,有一个小小细节,就是在 doDispatch 方法中,有如下一段代码:
1 |
applyDefaultViewName(processedRequest, mv); |
当这段代码执行的时候,接口方法已经通过反射调用完成了,并且将返回值封装成了一个 ModelAndView 对象(如果接口方法用到了 @ResponseBody
注解,则此时拿到的 ModelAndView 对象为 null),但是这个时候的 ModelAndView 对象还没有渲染,此时会调用 applyDefaultViewName 方法去判断返回的 ModelAndView 对象中有没有 view,如果没有,则给出一个默认的视图名。
这行代码就是切入点,接下来我们就来分析一下 applyDefaultViewName 方法。
2.applyDefaultViewName
1 |
private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception { |
可以看到,这里的判断逻辑很简单,首先检查 mv 是否为 null(如果用户添加了 @ResponseBody
注解,mv 就为 null),然后去判断 mv 中是否包含视图,如果不包含视图,则调用 getDefaultViewName 方法去获取默认的视图名,并将获取到的默认视图名交给 mv。
3.getDefaultViewName
1 |
@Nullable |
这里涉及到一个新的组件 viewNameTranslator,如果 viewNameTranslator 不为 null,则调用其 getViewName 方法获取默认的视图名。
viewNameTranslator 其实就是 RequestToViewNameTranslator,我们一起来看下:
1 |
public interface RequestToViewNameTranslator { |
这个接口很简单,里边就一个方法 getViewName 方法来返回视图名称。在 SpringMVC 中,RequestToViewNameTranslator 接口只有一个默认的实现类 DefaultRequestToViewNameTranslator,我们来看下实现类中的 getViewName 方法:
1 |
@Override |
在 getViewName 方法中,首先提取出来当前请求路径,如果请求地址是 http://localhost:8080/01
,那么这里提取出来的路径就是 /01
,然后通过 transformPath 方法对路径进行处理,再分别加上前后缀后返回,默认的前后缀都是空字符串(如有需要,也可以自行配置)。
transformPath 则主要干了如下几件事:
- 去掉路径开始的
/
。 - 去掉路径结尾的
/
。 - 如果请求路径有扩展名,则去掉扩展名,例如请求路径是
/01.txt
,经过这一步处理后,就变成了/01
。 - 如果 separator 与 SLASH 不同,则替换原来的分隔符(默认是相同的)。
好了,经过这一波处理后,正常情况下,我们就拿到了一个新的视图名,这个新的视图名就是你的请求路径。
例如请求路径是 http://localhost:8080/01
,那么获取到的默认视图名就是 01
。
现在大家就知道了,在没有写 @ResponseBody
的情况下,SpringMVC 会自动提取出一个默认的视图名,并且根据这个视图名去查找视图。
4.问题分析
要搞清楚这个问题,需要大家对视图解析器有一定了解,如果还不了解,可以先看看無名之前的文章:
看完视图解析器的分析之后,接下来的内容就很好理解了。
4.1 Freemarker
先来看使用了 Freemarker 后为什么报循环调用的错。
根据前面两篇文章的分析,现在我们在 Spring Boot 中默认使用的视图解析器是 ContentNegotiatingViewResolver,在这个视图解析器中会首先选出所有候选的 View,由于我们的代码中并不存在一个名为 01 的 Freemarker 视图(如果刚好存在一个名为 01 的 Freemarker 视图就不会报错了,就直接将该视图展示出来了),而 FreeMarkerViewResolver 的父类 UrlBasedViewResolver 中的 loadView 方法在加载视图的时候,会去检查视图是否存在,结果发现视图吧不存在,导致最终返回 null。所以当 01 这个视图不存在时,最终负责处理该视图的并不是 FreeMarkerViewResolver,而是否则兜底的 InternalResourceViewResolver,该视图解析器最终构建出来的视图就是 InternalResourceView。
InternalResourceView 在最终渲染之前,会有一个预处理,代码如下:
1 |
protected String prepareForRendering(HttpServletRequest request, HttpServletResponse response) |
这个地方的 getUrl 参数是在 buildView 方法中设置的(具体参见:SpringMVC 九大组件之 ViewResolver 深入分析),它返回的视图的完整路径名,也就是 prefix + viewName + suffix
,如果这个路径和当前请求路径一致,就抛出异常,抛出的异常就是我们一开始截图中看到的异常(其实异常中也说了,这个问题可能是由于自动生成 viewName 导致的)。
这就是为什么当我们使用 Freemarker 依赖时报循环请求的异常。
4.2 Thymeleaf
再来看 Thymeleaf,使用 Thymeleaf 时报的异常是模版不存在。
首先我们找到异常抛出的位置是在 TemplateManager#resolveTemplate 方法中:
1 |
private static TemplateResolution resolveTemplate( |
可以看到,这个方法在执行的过程中如果没能提前返回,最终就会抛出异常,抛出的异常也就是我们在控制台所看到的异常。执行到这一步的原因是前面获取到的 templateResolution 为 null,并且 failIfNotExists 参数为 true,failIfNotExists 参数在调用的时候固定传入,这个没啥好说的,问题的核心在于获取到的 templateResolution 是否为 null。
templateResolution 则是在 AbstractTemplateResolver#resolveTemplate 方法中获取到的,如下:
1 |
public final TemplateResolution resolveTemplate( |
可以看到,在拿到 templateResource 之后,会调用 templateResource.exists()
方法判断资源是否存在,也就是相应的模版文件是否存在,如果不存在就会返回 null,进而导致上一个方法抛出异常。
5.小结
好啦,今天主要和小伙伴们分享了一下 SpringMVC 中默认视图名的问题,不知道大家有没有 GET 到呢~
- Spring(403)
- Boot(208)
- Spring Boot(187)
- Java(82)
- Cloud(82)
- Spring Cloud(82)
- Security(60)
- Spring Security(54)
- Boot2(51)
- Spring Boot2(51)
- Redis(31)
- SQL(29)
- Mysql(25)
- IDE(24)
- Dalston(24)
- MVC(22)
- JDBC(22)
- IDEA(22)
- mongoDB(22)
- Web(21)
- CLI(20)
- SpringMVC(19)
- Alibaba(19)
- SpringBoot(17)
- Docker(17)
- Git(16)
- Eclipse(16)
- Vue(16)
- ORA(15)
- JPA(15)
- Apache(15)
- Mybatis(14)
- Oracle(14)
- jdk(14)
- Tomcat(14)
- Linux(14)
- HTTP(14)
- XML(13)
- JdbcTemplate(13)
- OAuth(13)
- Nacos(13)
- Pro(13)
- Data(12)
- JSON(12)
- OAuth2(12)
- stream(11)
- int(11)
- Myeclipse(11)
- Bug(10)
- not(10)
- ast(9)
- maven(9)
- Map(9)
- Hystrix(9)
- Swagger(8)
- APP(8)
- Bit(8)
- API(8)
- session(8)
- Window(8)
- HTML(7)
- Github(7)
- JavaMail(7)
- Cache(7)
- File(7)
- IntelliJ(7)
- mail(7)
- windows(7)
- too(7)
- RabbitMQ(6)
- and(6)
- star(6)
- Excel(6)
- Log4J(6)
- pushlet(6)
- apt(6)
- read(6)
- Freemarker(6)
- WebFlux(6)
- JSP(6)
- Bean(6)
- error(6)
- nginx(6)
- Server(6)
- jar(6)
- ueditor(6)
- ehcache(6)
- UDP(6)
- JWT(5)
- rdquo(5)
- PHP(5)
- Struts(5)
- string(5)
- Syntaxhighlighter(5)
- script(5)
- Tool(5)
- Controller(5)
- swagger2(5)
- ldquo(5)
- input(5)