Como evitar a exceção do "caminho de visão circular" com o teste Spring MVC

117

Eu tenho o seguinte código em um dos meus controladores:

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

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

Estou simplesmente tentando testá-lo usando o teste Spring MVC da seguinte maneira:

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

Estou recebendo a seguinte exceção:

Caminho de visualização circular [preferência]: seria despachado de volta para a URL do manipulador atual [/ preferência] novamente. Verifique a configuração do ViewResolver! (Dica: isso pode ser o resultado de uma exibição não especificada, devido à geração de nome de exibição padrão.)

O que acho estranho é que funciona bem quando carrego a configuração de contexto "completa" que inclui o modelo e os resolvedores de visualização, conforme mostrado abaixo:

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

Estou bem ciente de que o prefixo adicionado pelo resolvedor de modelo garante que não haja "caminho de visualização circular" quando o aplicativo usa esse resolvedor de modelo.

Mas então como devo testar meu aplicativo usando o teste Spring MVC?

Balteo
fonte
1
Você pode postar o que ViewResolvervocê usa quando está falhando?
Sotirios Delimanolis
@SotiriosDelimanolis: Não tenho certeza se algum viewResolver é usado pelo Spring MVC Test. documentação
balteo
8
Eu estava enfrentando o mesmo problema, mas o problema é que não adicionei a dependência abaixo. <dependency> <groupId> org.springframework.boot </groupId> <artifactId> spring-boot-starter-thymeleaf </artifactId> </dependency>
aamir
usar em @RestControllervez de@Controller
MozenRath

Respostas:

65

Isso não tem nada a ver com o teste Spring MVC.

Quando você não declara a ViewResolver, o Spring registra um padrão InternalResourceViewResolverque cria instâncias de JstlViewpara renderizar o View.

A JstlViewclasse estende o InternalResourceViewque é

Wrapper para um JSP ou outro recurso no mesmo aplicativo da web. Expõe objetos de modelo como atributos de solicitação e encaminha a solicitação para a URL de recurso especificada usando um javax.servlet.RequestDispatcher.

Um URL para essa visualização deve especificar um recurso dentro do aplicativo da web, adequado para o método de encaminhamento ou inclusão do RequestDispatcher.

Ousado é meu. Em outras palavras, a visualização, antes da renderização, tentará obter um RequestDispatcherpara o qual forward(). Antes de fazer isso, ele verifica o seguinte

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

onde pathestá o nome da visualização, o que você retornou do @Controller. Neste exemplo, é isso preference. A variável uricontém o uri da solicitação que está sendo tratada, que é /context/preference.

O código acima percebe que se você fosse encaminhar para /context/preference, o mesmo servlet (já que o mesmo tratou do anterior) trataria a solicitação e você entraria em um loop infinito.


Quando você declara a ThymeleafViewResolvere a ServletContextTemplateResolvercom um prefixe específico suffix, ele constrói o Viewdiferente, dando a ele um caminho como

WEB-INF/web-templates/preference.html

ThymeleafViewinstâncias localizam o arquivo em relação ao ServletContextcaminho usando um ServletContextResourceResolver

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

que eventualmente

return servletContext.getResourceAsStream(resourceName);

Isso obtém um recurso que é relativo ao ServletContextcaminho. Ele pode então usar o TemplateEnginepara gerar o HTML. Não há como um loop infinito acontecer aqui.

Sotirios Delimanolis
fonte
1
Obrigado pela sua resposta detalhada. Eu entendo por que o loop não ocorre quando eu uso o Thymeleaf e por que ocorre quando eu não uso o resolvedor de visualização do Thymeleaf. No entanto, ainda não tenho certeza de como mudar minha configuração para que eu possa testar meu aplicativo ...
balteo
1
@balteo Quando você usa ThymleafViewResolvero Viewé resolvido como um arquivo relativo a prefixe suffixvocê fornece. Quando você não usa isso resolve, o Spring usa um padrão InternalResourceViewResolverque encontra recursos com um RequestDispatcher. Este recurso pode ser um Servlet. Nesse caso, é porque o caminho é /preferencemapeado para o seu DispatcherServlet.
Sotirios Delimanolis
2
@balteo Para testar seu aplicativo, forneça um correto ViewResolver. Tanto o ThymeleafViewResolvercomo em sua pergunta, seu próprio configurado InternalResourceViewResolverou mude o nome da visão que você está retornando em seu controlador.
Sotirios Delimanolis
Obrigado, obrigado, obrigado! Não consegui descobrir por que o resolvedor de visualização de recurso interno preferiu encaminhar em vez de "incluir", mas agora com sua explicação, parece que o uso de "recurso" no nome é um pouco ambíguo. Esta explicação é estelar.
Chris Thompson
2
@ShirgillFarhanAnsari Um @RequestMappingmétodo de manipulador anotado com um Stringtipo de retorno (e não @ResponseBody) tem seu valor de retorno manipulado por um ViewNameMethodReturnValueHandlerque interpreta String como um nome de visualização e o usa para passar pelo processo que explico em minha resposta. Com @ResponseBody, Spring MVC irá usar o RequestResponseBodyMethodProcessorque, em vez disso, grava a String diretamente na resposta HTTP, ou seja. sem resolução de visualização.
Sotirios Delimanolis
97

Resolvi esse problema usando @ResponseBody como abaixo:

@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
fonte
10
Eles desejam retornar HTML resolvendo uma visualização, não retornando uma versão serializada de um List<DomainObject>.
Sotirios Delimanolis
2
Isso resolveu meu problema ao retornar uma resposta JSON para o serviço da web Spring Rest.
Joe
Legal, se eu não especificar o makes = {"application / json"}, ainda funciona. Ele produz JSON por padrão?
Jay
74

@Controller@RestController

Eu tive o mesmo problema e notei que meu controlador também foi anotado com @Controller. Substituí-lo por @RestControllerresolveu o problema. Aqui está a explicação do Spring Web MVC :

@RestController é uma anotação composta que é meta-anotada com @Controller e @ResponseBody, indicando um controlador cujo cada método herda a anotação @ResponseBody de nível de tipo e, portanto, grava diretamente no corpo de resposta vs resolução de visualização e renderização com um modelo HTML.

Boris
fonte
1
@TodorTodorov Foi por mim
Igor Rodriguez
@TodorTodorov e para mim!
Executado em
3
Funcionou para mim também. Eu tinha um @ControllerAdvicecom um handleXyExceptionmétodo nele, que retornou meu próprio objeto em vez de um ResponseEntity. Adicionar @RestControllerem cima da @ControllerAdviceanotação funcionou e o problema desapareceu.
Igor
36

Foi assim que resolvi esse 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
fonte
1
Isso é apenas para casos de teste. Não para controladores.
cst1992 de
2
Estava ajudando alguém a solucionar esse problema em um de seus novos testes de unidade, isso é exatamente o que estávamos procurando.
Bradford2000
Usei isso, mas apesar de fornecer o prefixo e sufixo incorretos para meu resolvedor no teste, funcionou. Você pode fornecer um raciocínio por trás disso, por que isso é necessário?
dushyantashu
esta resposta deve ser votada por ser a mais correta e específica
Caffeine Coder
20

Estou usando o Spring Boot para tentar carregar uma página da web, não para testar, e tive esse problema. Minha solução foi um pouco diferente das anteriores, considerando as circunstâncias ligeiramente diferentes. (embora essas respostas tenham me ajudado a entender.)

Eu simplesmente tive que mudar minha dependência inicial do Spring Boot no Maven de:

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

para:

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

Apenas mudar a 'web' para 'thymeleaf' resolveu o problema para mim.

Old Schooled
fonte
1
Para mim, não foi necessário mudar o starter-web, mas eu tinha a dependência do thymeleaf com <scope> test </scope>. Quando removi o escopo de "teste", ele funcionou. Obrigado pela pista!
Georgina Diaz
16

Aqui está uma solução fácil se você realmente não se importa em renderizar a visualização.

Crie uma subclasse de InternalResourceViewResolver que não verifica os caminhos de visualização 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;
    }
}

Em seguida, configure seu teste com ele:

MockMvc mockMvc;

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

    mockMvc =
            MockMvcBuilders.standaloneSetup(controller)
                    .setViewResolvers(new StandaloneMvcTestViewResolver())
                    .build();
}
Dave Bower
fonte
Isso resolveu meu problema. Acabei de adicionar uma classe StandaloneMvcTestViewResolver no mesmo diretório dos testes e usei-a no MockMvcBuilders conforme descrito acima. Obrigado
Matheus Araujo
Eu tive o mesmo problema e isso resolveu para mim também. Muito obrigado!
Johan
Esta é uma ótima solução que (1) não precisa mudar os controladores e (2) pode ser reutilizada em todas as classes de teste com uma simples importação por classe. +1
Nander Speerstra de
Oldie mas goldie! Salvou meu dia. Obrigado por esta solução alternativa +1
Raistlin de
13

Se você estiver usando Spring Boot, adicione a dependência thymeleaf em seu pom.xml:

    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring4</artifactId>
        <version>2.1.6.RELEASE</version>
    </dependency>
Sarvar Nishonboev
fonte
1
Voto positivo. A ausência de dependência do Thymeleaf foi o que causou esse erro em meu projeto. No entanto, se você estiver usando Spring Boot, a dependência será assim:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
peterh
8

Adicionar /depois /preferenceresolveu o problema para mim:

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

No meu caso, eu estava experimentando a inicialização Kotlin + Spring e entrei no problema Circular View Path. Todas as sugestões que recebi online não puderam ajudar, até que tentei o seguinte:

Originalmente, anotei meu controlador usando @Controller

import org.springframework.stereotype.Controller

Eu então substituí @Controllerpor@RestController

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

E funcionou.

johnmilimo
fonte
6

se você não usou um @RequestBody e está usando apenas @Controller, a maneira mais simples de corrigir isso é usando em @RestControllervez de@Controller

MozenRath
fonte
isso não foi corrigido, agora ele mostrará o nome do seu arquivo, em vez do modelo
Ashish Kamble
1
isso depende do problema real. este erro pode ocorrer por vários motivos
MozenRath
4

Adicione a anotação @ResponseBodyao seu método return.

Ishaan Arora
fonte
Inclua uma explicação de como e por que isso resolve o problema realmente ajudaria a melhorar a qualidade da sua postagem e provavelmente resultaria em mais votos positivos.
Android de
3

Estou usando o Spring Boot com Thymeleaf. Isto é o que funcionou para mim. Existem respostas semelhantes com JSP, mas observe que estou usando HTML, não JSP, e elas estão na pasta src/main/resources/templatescomo em um projeto Spring Boot padrão, conforme explicado aqui . Este também pode ser o seu 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 isto ajude.

Pedro Lopez
fonte
3

Ao executar Spring Boot + Freemarker se a página aparecer:

Whitelabel Error Page Este aplicativo não possui mapeamento explícito para / error, então você está vendo isso como um fallback.

No spring-boot-starter-parent 2.2.1.RELEASE versão freemarker não funciona:

  1. renomear arquivos do Freemarker de .ftl para .ftlh
  2. Adicione a application.properties: spring.freemarker.expose-request-attribute = true

spring.freemarker.suffix = .ftl

Max
fonte
1
Simplesmente renomear os arquivos do Freemarker de .ftl para .ftlh resolveu o problema para mim.
jannnik
Cara ... eu te devo uma cerveja. Perdi meu dia inteiro por causa dessa coisa de renomeação.
julianobrasil 01 de
2

Para Thymeleaf:

Acabei de começar a usar spring 4 e thymeleaf, quando encontrei este erro, ele foi resolvido adicionando:

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

Ao usar @Controlleranotação, você precisa @RequestMappinge @ResponseBodyanotações. Tente novamente depois de adicionar anotação@ResponseBody

Gowri Ayyanar
fonte
0

Eu uso a anotação para configurar o aplicativo da web do spring, o problema resolvido adicionando um InternalResourceViewResolverbean à configuração. Espero que seja útil.

@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
fonte
Obrigado, isso funciona bem para mim. Meu aplicativo quebrou após a atualização para o Spring boot 1.3.1 de 1.2.7 e foi apenas esta linha que estava falhando em registry.addViewController ("/ login"). SetViewName ("login"); Ao registrar esse bean, o aplicativo funcionou novamente ... pelo menos o login funcionou.
le0diaz
0

Isso está acontecendo porque o Spring está removendo "preferência" e acrescentando a "preferência" novamente fazendo o mesmo caminho que o Uri da solicitação.

Acontecendo assim: solicitar Uri: "/ preferência"

remova "preferência": "/"

acrescentar caminho: "/" + "preferência"

string final: "/ preferência"

Isso é entrar em um loop que o Spring notifica lançando uma exceção.

É melhor do seu interesse dar um nome de visualização diferente, como "preferênciaView" ou qualquer coisa que você desejar.

xpioneer
fonte
0

tente adicionar a dependência de compilação ("org.springframework.boot: spring-boot-starter-thymeleaf") ao seu arquivo gradle.Thymeleaf ajuda a mapear visualizações.

Aishwarya Kore
fonte
0

No meu caso, tive esse problema ao tentar servir páginas JSP usando o aplicativo Spring boot.

Aqui está o que funcionou para mim:

application.properties

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

pom.xml

Para habilitar o suporte para JSPs, precisaríamos adicionar uma dependência em tomcat-embed-jasper.

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

Outra abordagem simples:

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