springspring-mvccircular-referencethymeleafspring-mvc-test

How to avoid the "Circular view path" exception with Spring MVC test


I have the following code in one of my controllers:

@Controller
@RequestMapping("/preference")
public class PreferenceController {

    @RequestMapping(method = RequestMethod.GET, produces = "text/html")
    public String preference() {
        return "preference";
    }
}

I am simply trying to test it using Spring MVC test as follows:

@ContextConfiguration
@WebAppConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class PreferenceControllerTest {

    @Autowired
    private WebApplicationContext ctx;

    private MockMvc mockMvc;
    @Before
    public void setup() {
        mockMvc = webAppContextSetup(ctx).build();
    }

    @Test
    public void circularViewPathIssue() throws Exception {
        mockMvc.perform(get("/preference"))
               .andDo(print());
    }
}

I am getting the following exception:

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

What I find strange is that it works fine when I load the "full" context configuration that includes the template and view resolvers as shown below:

<bean class="org.thymeleaf.templateresolver.ServletContextTemplateResolver" id="webTemplateResolver">
    <property name="prefix" value="WEB-INF/web-templates/" />
    <property name="suffix" value=".html" />
    <property name="templateMode" value="HTML5" />
    <property name="characterEncoding" value="UTF-8" />
    <property name="order" value="2" />
    <property name="cacheable" value="false" />
</bean>

I am well aware that the prefix added by the template resolver ensures that there is not "circular view path" when the app uses this template resolver.

But then how I am supposed to test my app using Spring MVC test?


Solution

  • This has nothing to do with Spring MVC testing.

    When you don't declare a ViewResolver, Spring registers a default InternalResourceViewResolver which creates instances of JstlView for rendering the View.

    The JstlView class extends InternalResourceView which is

    Wrapper for a JSP or other resource within the same web application. Exposes model objects as request attributes and forwards the request to the specified resource URL using a javax.servlet.RequestDispatcher.

    A URL for this view is supposed to specify a resource within the web application, suitable for RequestDispatcher's forward or include method.

    Emphasis mine. In other words, the view, before rendering, will try to get a RequestDispatcher to which to forward(). Before doing this it checks the following

    if (path.startsWith("/") ? uri.equals(path) : uri.equals(StringUtils.applyRelativePath(uri, path))) {
        throw new ServletException("Circular view path [" + path + "]: would dispatch back " +
                            "to the current handler URL [" + uri + "] again. Check your ViewResolver setup! " +
                            "(Hint: This may be the result of an unspecified view, due to default view name generation.)");
    }
    

    where path is the view name, what you returned from the @Controller. In this example, that is preference. The variable uri holds the uri of the request being handled, which is /context/preference.

    The code above realizes that if you were to forward to /context/preference, the same servlet (since the same handled the previous) would handle the request and you would go into an endless loop.


    When you declare a ThymeleafViewResolver and a ServletContextTemplateResolver with a specific prefix and suffix, it builds the View differently, giving it a path like

    WEB-INF/web-templates/preference.html
    

    ThymeleafView instances locate the file relative to the ServletContext path by using a ServletContextResourceResolver

    templateInputStream = resourceResolver.getResourceAsStream(templateProcessingParameters, resourceName);`
    

    which eventually

    return servletContext.getResourceAsStream(resourceName);
    

    This gets a resource that is relative to the ServletContext path. It can then use the TemplateEngine to generate the HTML. There's no way an endless loop can happen here.