Keywords: Spring MVC | Circular View Path | MockMvc Testing | View Resolver | Thymeleaf
Abstract: This article provides an in-depth analysis of the "Circular View Path" exception commonly encountered in Spring MVC testing. It explains the working mechanism of default view resolvers and the differences with Thymeleaf view resolvers. By comparing various solutions, it offers practical testing configuration methods to help developers understand Spring MVC's view resolution process and effectively avoid common testing pitfalls.
Problem Background and Phenomenon Description
During unit testing of Spring MVC applications, developers often encounter a typical exception: "Circular view path [preference]: would dispatch back to the current handler URL [/preference] again. Check your ViewResolver setup!" This exception typically occurs when using MockMvc for controller testing, especially when the view name returned by the controller method has some relationship with the request path.
Root Cause of the Exception
To understand the mechanism behind this exception, it's essential to first comprehend Spring MVC's default view resolver configuration. When an application doesn't explicitly configure a ViewResolver, Spring automatically registers a default InternalResourceViewResolver. This resolver is responsible for creating JstlView instances to render views.
The JstlView class extends InternalResourceView, designed to wrap JSP or other resources within the same web application. The key characteristic is that it exposes model objects as request attributes and forwards the request to the specified resource URL using javax.servlet.RequestDispatcher. This means the view attempts to obtain a RequestDispatcher for forward() operation before rendering.
Before forwarding, the system performs crucial security checks:
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.)");
}
In this check, the path variable represents the view name returned from the @Controller method (e.g., "preference"), while the uri variable holds the request URI being processed (e.g., "/context/preference"). When the system detects that forwarding to "/context/preference" would cause the same servlet (since it handled the previous request) to process the request again, it throws the circular view path exception to prevent an infinite loop.
Differences with Thymeleaf View Resolver
When the application configures ThymeleafViewResolver and ServletContextTemplateResolver with specific prefix and suffix settings, the view construction process is entirely different. Thymeleaf builds complete paths like "WEB-INF/web-templates/preference.html".
ThymeleafView instances locate files through ServletContextResourceResolver:
templateInputStream = resourceResolver.getResourceAsStream(templateProcessingParameters, resourceName);
Ultimately calling:
return servletContext.getResourceAsStream(resourceName);
This approach retrieves resources relative to the ServletContext path, then uses TemplateEngine to generate HTML. Since the resource path is completely different from the request URL, there's no possibility of creating a circular path.
Solution Comparison and Analysis
Solution 1: Using @RestController Annotation
Replacing @Controller with @RestController is an effective solution. @RestController is a composed annotation that is itself meta-annotated with @Controller and @ResponseBody, indicating that every method of the controller inherits the type-level @ResponseBody annotation. This means methods write directly to the response body instead of going through view resolution and HTML template rendering.
The advantage of this approach is simplicity, particularly suitable for RESTful API development. However, the drawback is that if view rendering functionality is genuinely needed, this method becomes unsuitable.
Solution 2: Adding @ResponseBody Annotation
Adding @ResponseBody annotation to controller methods achieves a similar effect:
@RequestMapping(value = "/resturl", method = RequestMethod.GET, produces = {"application/json"})
@ResponseStatus(HttpStatus.OK)
@Transactional(value = "jpaTransactionManager")
public @ResponseBody List<DomainObject> findByResourceID(@PathParam("resourceID") String resourceID) {
// method implementation
}
This method offers more flexibility, allowing developers to choose between view rendering and direct response body output as needed.
Solution 3: Proper View Resolver Configuration
The most fundamental solution is to correctly configure the view resolver in the testing environment. When using MockMvcBuilders.standaloneSetup(), Spring configuration isn't loaded, so ViewResolver needs to be manually set up:
@Before
public void setup() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/jsp/view/");
viewResolver.setSuffix(".jsp");
mockMvc = MockMvcBuilders.standaloneSetup(new PreferenceController())
.setViewResolvers(viewResolver)
.build();
}
Or define it in XML configuration:
<bean id = "viewResolver" class = "org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/view/"/>
<property name="suffix" value=".jsp"/>
</bean>
Best Practices for Testing Environment Configuration
For Spring MVC testing, it's recommended to use @WebAppConfiguration and webAppContextSetup to load the complete application context:
@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 testPreference() throws Exception {
mockMvc.perform(get("/preference"))
.andDo(print());
}
}
This approach ensures consistency between the testing environment and production environment, including all view resolver configurations.
Conclusion and Recommendations
The root cause of the circular view path exception lies in the security check mechanism of the default view resolver. Understanding Spring MVC's view resolution process is crucial for avoiding such issues. In practical development, it's recommended to:
- Choose appropriate solutions based on actual requirements: use @RestController for RESTful APIs, and properly configure ViewResolver when view rendering is needed
- Ensure correct view resolver configuration in testing environments, either through standaloneSetup manual configuration or webAppContextSetup for complete configuration loading
- Understand the working principle differences between various view technologies (JSP, Thymeleaf, etc.)
- Establish unified testing configuration standards in team development
By deeply understanding Spring MVC's view resolution mechanism, developers can confidently write reliable unit tests and avoid common testing pitfalls.