Cómo evitar la excepción de "ruta de vista circular" con la prueba Spring MVC

117

Tengo el siguiente código en uno de mis controladores:

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

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

Simplemente estoy tratando de probarlo usando la prueba Spring MVC de la siguiente manera:

@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());
    }
}

Recibo la siguiente excepción:

Ruta de vista circular [preferencia]: se enviaría de nuevo a la URL del controlador actual [/ preferencia] nuevamente. ¡Compruebe la configuración de ViewResolver! (Sugerencia: esto puede ser el resultado de una vista no especificada, debido a la generación de nombre de vista predeterminada).

Lo que encuentro extraño es que funciona bien cuando cargo la configuración de contexto "completa" que incluye la plantilla y los resolutores de vista como se muestra a continuación:

<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>

Soy consciente de que el prefijo agregado por el solucionador de plantillas garantiza que no haya una "ruta de vista circular" cuando la aplicación utiliza este solucionador de plantillas.

Pero entonces, ¿cómo se supone que debo probar mi aplicación usando la prueba Spring MVC?

balteo
fuente
1
¿Puedes publicar el ViewResolverque usas cuando está fallando?
Sotirios Delimanolis
@SotiriosDelimanolis: No estoy seguro de si Spring MVC Test utiliza viewResolver. documentación
balteo
8
Estaba enfrentando el mismo problema, pero el problema era que no había agregado la dependencia a continuación. <dependency> <groupId> org.springframework.boot </groupId> <artifactId> spring-boot-starter-thymeleaf </artifactId> </dependency>
aamir
uso en @RestControllerlugar de@Controller
MozenRath

Respuestas:

65

Esto no tiene nada que ver con las pruebas Spring MVC.

Cuando no declara a ViewResolver, Spring registra un valor predeterminado InternalResourceViewResolverque crea instancias de JstlViewpara representar el View.

La JstlViewclase se extiende InternalResourceViewque es

Wrapper para una JSP u otro recurso dentro de la misma aplicación web. Expone objetos de modelo como atributos de solicitud y reenvía la solicitud a la URL del recurso especificado mediante un javax.servlet.RequestDispatcher.

Se supone que una URL para esta vista especifica un recurso dentro de la aplicación web, adecuado para el método de envío o inclusión de RequestDispatcher.

Atrevido es mío. En otras palabras, la vista, antes de renderizar, intentará obtener un RequestDispatcherdestino forward(). Antes de hacer esto, verifica lo siguiente

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.)");
}

donde pathestá el nombre de la vista, lo que devolvió del @Controller. En este ejemplo, eso es preference. La variable uricontiene el uri de la solicitud que se está manejando, que es /context/preference.

El código anterior se da cuenta de que si tuviera que reenviar /context/preference, el mismo servlet (ya que el mismo manejó el anterior) manejaría la solicitud y entraría en un bucle sin fin.


Cuando declaras a ThymeleafViewResolvery a ServletContextTemplateResolvercon un prefixy específico suffix, se construye de manera Viewdiferente, dándole una ruta como

WEB-INF/web-templates/preference.html

ThymeleafViewinstancias localizan el archivo en relación con la ServletContextruta mediante un ServletContextResourceResolver

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

que eventualmente

return servletContext.getResourceAsStream(resourceName);

Esto obtiene un recurso que es relativo a la ServletContextruta. Luego puede usar el TemplateEnginepara generar el HTML. No hay forma de que ocurra un ciclo sin fin aquí.

Sotirios Delimanolis
fuente
1
Gracias por tu minuciosa respuesta. Entiendo por qué el bucle no ocurre cuando uso Thymeleaf y por qué ocurre cuando no uso el solucionador de vistas de Thymeleaf. Sin embargo, todavía no estoy seguro de cómo cambiar mi configuración para poder probar mi aplicación ...
balteo
1
@balteo Cuando usa ThymleafViewResolverel Viewse resuelve como un archivo relativo al prefixy suffixusted proporciona. Cuando no usa esa resolución, Spring usa un valor predeterminado InternalResourceViewResolverque busca recursos con una extensión RequestDispatcher. Este recurso puede ser un Servlet. En este caso es porque la ruta se /preferenceasigna a su DispatcherServlet.
Sotirios Delimanolis
2
@balteo Para probar su aplicación, proporcione un ViewResolver. Ya sea ThymeleafViewResolvercomo en su pregunta, la suya propia configurada InternalResourceViewResolvero cambie el nombre de la vista que está devolviendo en su controlador.
Sotirios Delimanolis
¡Gracias, gracias, gracias! No pude entender por qué el solucionador de vista de recursos interno prefirió reenviar en lugar de "incluir", pero ahora, con su explicación, parece que el uso de "recurso" en el nombre es un poco ambiguo. Esta explicación es estelar.
Chris Thompson
2
@ShirgillFarhanAnsari Un @RequestMappingmétodo de controlador anotado con un Stringtipo de retorno (y no @ResponseBody) tiene su valor de retorno manejado por un ViewNameMethodReturnValueHandlerque interpreta la Cadena como un nombre de vista y lo usa para pasar por el proceso que explico en mi respuesta. Con @ResponseBody, Spring MVC usará en su lugar, RequestResponseBodyMethodProcessorque en su lugar escribe la cadena directamente en la respuesta HTTP, es decir. sin resolución de vista.
Sotirios Delimanolis
97

Resolví este problema usando @ResponseBody como se muestra a continuación:

@RequestMapping(value = "/resturl", method = RequestMethod.GET, produces = {"application/json"})
    @ResponseStatus(HttpStatus.OK)
    @Transactional(value = "jpaTransactionManager")
    public @ResponseBody List<DomainObject> findByResourceID(@PathParam("resourceID") String resourceID) {
Deepti Kohli
fuente
10
Quieren devolver HTML resolviendo una vista, no devolver una versión serializada de un List<DomainObject>.
Sotirios Delimanolis
2
Esto resolvió mi problema al devolver una respuesta JSON para el servicio web Spring rest ..
Joe
Bien, si no especifico produce = {"application / json"}, aún funciona. ¿Produce json de forma predeterminada?
Jay
74

@Controller@RestController

Tuve el mismo problema y noté que mi controlador también estaba anotado con @Controller. Reemplazarlo con @RestControllerresolvió el problema. Aquí está la explicación de Spring Web MVC :

@RestController es una anotación compuesta que está a su vez meta-anotada con @Controller y @ResponseBody indicando un controlador cuyos métodos heredan la anotación @ResponseBody de nivel de tipo y, por lo tanto, escribe directamente en el cuerpo de la respuesta frente a la resolución de la vista y la representación con una plantilla HTML.

Boris
fuente
1
@TodorTodorov Lo hizo por mí
Igor Rodríguez
@TodorTodorov y para mí!
Corrió el
3
También funcionó para mí. Tenía un método @ControllerAdvicecon un handleXyException, que devolvía mi propio objeto en lugar de un ResponseEntity. Agregar @RestControllerencima de la @ControllerAdviceanotación funcionó y el problema desapareció.
Igor
36

Así es como resolví este problema:

@Before
    public void setup() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/jsp/view/");
        viewResolver.setSuffix(".jsp");

        mockMvc = MockMvcBuilders.standaloneSetup(new HelpController())
                                 .setViewResolvers(viewResolver)
                                 .build();
    }
Piotr Sagalara
fuente
1
Esto es solo para casos de prueba. No para controladores.
cst1992
2
Estaba ayudando a alguien a solucionar este problema en una de sus nuevas pruebas unitarias, esto es exactamente lo que estábamos buscando.
Bradford2000
Usé esto, pero a pesar de dar el prefijo y sufijo incorrectos para mi resolutor en la prueba, funcionó. ¿Puede proporcionar un razonamiento detrás de esto, por qué es necesario?
dushyantashu
esta respuesta debería ser votada por ser la más correcta y específica
Codificador de cafeína
20

Estoy usando Spring Boot para intentar cargar una página web, no para probar, y tuve este problema. Mi solución fue un poco diferente a las anteriores considerando las circunstancias ligeramente diferentes. (aunque esas respuestas me ayudaron a entender).

Simplemente tuve que cambiar mi dependencia de arranque Spring Boot en Maven de:

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
</dependency>

a:

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Simplemente cambiando la 'web' a 'thymeleaf' solucionó el problema.

Viejo escolarizado
fuente
1
Para mí, no fue necesario cambiar la starter-web, pero tenía la dependencia de thymeleaf con <scope> test </scope>. Cuando quité el alcance de "prueba", funcionó. ¡Gracias por la pista!
Georgina Diaz
16

Aquí hay una solución fácil si realmente no le importa renderizar la vista.

Cree una subclase de InternalResourceViewResolver que no compruebe las rutas de vista circular:

public class StandaloneMvcTestViewResolver extends InternalResourceViewResolver {

    public StandaloneMvcTestViewResolver() {
        super();
    }

    @Override
    protected AbstractUrlBasedView buildView(final String viewName) throws Exception {
        final InternalResourceView view = (InternalResourceView) super.buildView(viewName);
        // prevent checking for circular view paths
        view.setPreventDispatchLoop(false);
        return view;
    }
}

Luego configure su prueba con él:

MockMvc mockMvc;

@Before
public void setUp() {
    final MyController controller = new MyController();

    mockMvc =
            MockMvcBuilders.standaloneSetup(controller)
                    .setViewResolvers(new StandaloneMvcTestViewResolver())
                    .build();
}
Dave Bower
fuente
Esto solucionó mi problema. Acabo de agregar una clase StandaloneMvcTestViewResolver en el mismo directorio de las pruebas y la usé en MockMvcBuilders como se describe anteriormente. Gracias
Matheus Araujo
Tuve el mismo problema y esto también me lo solucionó. ¡Muchas gracias!
Johan
Esta es una gran solución que (1) no necesita cambiar los controladores y (2) se puede reutilizar en todas las clases de prueba con una simple importación por clase. +1
Nander Speerstra
¡Viejo pero valioso! Salvó mi día. Gracias por esta solución alternativa +1
Raistlin
13

Si está utilizando Spring Boot, agregue la dependencia de thymeleaf en su pom.xml:

    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring4</artifactId>
        <version>2.1.6.RELEASE</version>
    </dependency>
Sarvar Nishonboev
fuente
1
Voto a favor. La falta de dependencia de Thymeleaf fue lo que causó este error en mi proyecto. Sin embargo, si está utilizando Spring Boot, entonces la dependencia se vería así:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
peterh
8

Agregar /después de /preferenceresolver el problema para mí:

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

En mi caso, estaba probando Kotlin + Spring boot y me metí en el problema de Circular View Path. Todas las sugerencias que recibí en línea no pudieron ayudar, hasta que intenté lo siguiente:

Originalmente había anotado mi controlador usando @Controller

import org.springframework.stereotype.Controller

Luego reemplacé @Controllercon@RestController

import org.springframework.web.bind.annotation.RestController

Y funcionó.

johnmilimo
fuente
6

si no ha usado un @RequestBody y lo está usando solo @Controller, la forma más sencilla de solucionar esto es usar en @RestControllerlugar de@Controller

MozenRath
fuente
esto no es correcto, ahora mostrará el nombre de su archivo, en lugar de mostrar la plantilla
Ashish Kamble
1
eso depende del problema real. este error puede ocurrir por muchas razones
MozenRath
4

Agregue la anotación @ResponseBodya su método return.

Ishaan Arora
fuente
Por favor, incluya una explicación de cómo y por qué esto resuelve el problema realmente ayudaría a mejorar la calidad de su publicación y probablemente resultaría en más votos a favor.
Android
3

Estoy usando Spring Boot con Thymeleaf. Esto es lo que funcionó para mí. Hay respuestas similares con JSP, pero tenga en cuenta que estoy usando HTML, no JSP, y están en la carpeta src/main/resources/templatescomo en un proyecto Spring Boot estándar como se explica aquí . Este también podría ser tu caso.

@InjectMocks
private MyController myController;

@Before
public void setup()
{
    MockitoAnnotations.initMocks(this);

    this.mockMvc = MockMvcBuilders.standaloneSetup(myController)
                    .setViewResolvers(viewResolver())
                    .build();
}

private ViewResolver viewResolver()
{
    InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();

    viewResolver.setPrefix("classpath:templates/");
    viewResolver.setSuffix(".html");

    return viewResolver;
}

Espero que esto ayude.

Pedro lopez
fuente
3

Al ejecutar Spring Boot + Freemarker si aparece la página:

Página de error de etiqueta blanca Esta aplicación no tiene un mapeo explícito para / error, por lo que está viendo esto como una alternativa.

En spring-boot-starter-parent 2.2.1.RELEASE versión freemarker no funciona:

  1. cambiar el nombre de los archivos Freemarker de .ftl a .ftlh
  2. Agregar a application.properties: spring.freemarker.expose-request-attributes = true

spring.freemarker.suffix = .ftl

Max
fuente
1
Simplemente cambiar el nombre de los archivos de Freemarker de .ftl a .ftlh me solucionó el problema.
Jannnik
Hombre ... te debo una cerveza. Perdí mi día entero debido a este cambio de nombre.
julianobrasil
2

Para Thymeleaf:

Acabo de comenzar a usar Spring 4 y Thymeleaf, cuando encontré este error, se resolvió agregando:

<bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
  <property name="templateEngine" ref="templateEngine" />
  <property name="order" value="0" />
</bean> 
Carlos H. Raymundo
fuente
1

Al usar la @Controlleranotación, necesita @RequestMappingy @ResponseBodyanotaciones. Vuelva a intentarlo después de agregar una anotación@ResponseBody

Gowri Ayyanar
fuente
0

Utilizo la anotación para configurar la aplicación web Spring, el problema se resuelve agregando un InternalResourceViewResolverbean a la configuración. Espero que sea de ayuda.

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.example.springmvc" })
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Bean
    public InternalResourceViewResolver internalResourceViewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/jsp/");
        resolver.setSuffix(".jsp");
        return resolver;
    }
}
alijandro
fuente
Gracias, esto funciona bien para mí. Mi aplicación se rompió después de actualizar a Spring Boot 1.3.1 desde 1.2.7 y era solo esta línea la que fallaba. Registration.addViewController ("/ login"). SetViewName ("login"); Al registrar ese bean, la aplicación funcionó nuevamente ... al menos el inicio de sesión fue correcto.
le0diaz
0

Esto sucede porque Spring está eliminando la "preferencia" y agregando la "preferencia" nuevamente haciendo la misma ruta que la solicitud Uri.

Sucediendo así: solicitud Uri: "/ preferencia"

eliminar "preferencia": "/"

añadir ruta: "/" + "preferencia"

cadena final: "/ preferencia"

Esto se está metiendo en un bucle que Spring le notifica lanzando una excepción.

Es mejor para su interés dar un nombre de vista diferente, como "ver preferencia" o cualquier cosa que desee.

xpioneer
fuente
0

intente agregar la dependencia compile ("org.springframework.boot: spring-boot-starter-thymeleaf") a su archivo gradle. Thhymeleaf ayuda a mapear vistas.

aishwarya corea
fuente
0

En mi caso, tuve este problema al intentar servir páginas JSP usando la aplicación Spring Boot.

Esto es lo que funcionó para mí:

application.properties

spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

pom.xml

Para habilitar el soporte para JSP, necesitaríamos agregar una dependencia en tomcat-embed-jasper.

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <scope>provided</scope>
</dependency>
Faouzi
fuente
-2

Otro enfoque simple:

package org.yourpackagename;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;

@SpringBootApplication
public class Application extends SpringBootServletInitializer {

      @Override
        protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
            return application.sources(PreferenceController.class);
        }


    public static void main(String[] args) {
        SpringApplication.run(PreferenceController.class, args);
    }
}
Vidente sin dientes
fuente