Come evitare l'eccezione "Percorso vista circolare" con il test Spring MVC


117

Ho il seguente codice in uno dei miei controller:

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

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

Sto semplicemente cercando di testarlo usando il test Spring MVC come segue:

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

Ricevo la seguente eccezione:

Percorso di visualizzazione circolare [preferenza]: verrebbe inviato nuovamente all'URL del gestore corrente [/ preferenza]. Controlla la configurazione di ViewResolver! (Suggerimento: potrebbe essere il risultato di una vista non specificata, a causa della generazione del nome della vista predefinita.)

Quello che trovo strano è che funziona bene quando carico la configurazione del contesto "completo" che include il modello e visualizza i risolutori come mostrato di seguito:

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

Sono ben consapevole che il prefisso aggiunto dal risolutore di modelli garantisce che non ci sia un "percorso di visualizzazione circolare" quando l'app utilizza questo risolutore di modelli.

Ma allora come dovrei testare la mia app usando il test Spring MVC?


1
Puoi pubblicare quello ViewResolverche usi quando fallisce?
Sotirios Delimanolis

@SotiriosDelimanolis: Non sono sicuro che viewResolver venga utilizzato da Spring MVC Test. documentazione
balteo

8
Stavo affrontando lo stesso problema ma il problema era che non avevo aggiunto sotto la dipendenza. <dependency> <groupId> org.springframework.boot </groupId> <artifactId> spring-boot-starter-thymeleaf </artifactId> </dependency>
aamir

utilizzare al @RestControllerposto di@Controller
MozenRath

Risposte:


65

Questo non ha nulla a che fare con i test Spring MVC.

Quando non dichiari a ViewResolver, Spring registra un valore predefinito InternalResourceViewResolverche crea istanze di JstlViewper il rendering del file View.

La JstlViewclasse si estende InternalResourceViewche è

Wrapper per un JSP o un'altra risorsa all'interno della stessa applicazione web. Espone gli oggetti del modello come attributi della richiesta e inoltra la richiesta all'URL della risorsa specificato utilizzando un javax.servlet.RequestDispatcher.

Si suppone che un URL per questa vista specifichi una risorsa all'interno dell'applicazione web, adatta per il metodo forward o include di RequestDispatcher.

Il grassetto è mio. In altre parole, la vista, prima del rendering, tenterà di ottenere un RequestDispatchera cui forward(). Prima di fare ciò controlla quanto segue

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

dov'è pathil nome della vista, cosa hai restituito da @Controller. In questo esempio, cioè preference. La variabile uricontiene l'uri della richiesta che viene gestita, ovvero /context/preference.

Il codice sopra si rende conto che se si dovesse inoltrare a /context/preference, lo stesso servlet (poiché lo stesso ha gestito il precedente) gestirà la richiesta e si andrebbe in un ciclo infinito.


Quando dichiari a ThymeleafViewResolvere a ServletContextTemplateResolvercon una specifica prefixe suffix, costruisce Viewdiversamente, dandogli un percorso come

WEB-INF/web-templates/preference.html

ThymeleafViewle istanze individuano il file rispetto al ServletContextpercorso utilizzando un file ServletContextResourceResolver

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

che alla fine

return servletContext.getResourceAsStream(resourceName);

Ottiene una risorsa relativa al ServletContextpercorso. Può quindi utilizzare TemplateEngineper generare l'HTML. Non è possibile che qui avvenga un ciclo infinito.


1
Grazie per la tua risposta dettagliata. Capisco perché il loop non si verifica quando utilizzo Thymeleaf e perché si verifica quando non utilizzo il resolver della vista Thymeleaf. Tuttavia, non sono ancora sicuro di come modificare la mia configurazione in modo da poter testare la mia app ...
balteo

1
@balteo Quando usi ThymleafViewResolveril Viewviene risolto come un file relativo al prefixe suffixfornisci. Quando non usi che si risolve, Spring usa un valore predefinito InternalResourceViewResolverche trova risorse con estensione RequestDispatcher. Questa risorsa può essere un file Servlet. In questo caso è perché il percorso viene /preferencemappato al tuo DispatcherServlet.
Sotirios Delimanolis

2
@balteo Per testare la tua app, fornisci un file ViewResolver. O ThymeleafViewResolvercome nella tua domanda, il tuo configurato InternalResourceViewResolvero cambia il nome della vista che stai restituendo nel tuo controller.
Sotirios Delimanolis

Grazie, grazie, grazie! Non sono riuscito a capire perché il risolutore della vista delle risorse interne preferisse inoltrare piuttosto che "includere", ma ora con la tua spiegazione sembra che l'uso di "risorsa" nel nome sia un po 'ambiguo. Questa spiegazione è stellare.
Chris Thompson

2
@ShirgillFarhanAnsari Un @RequestMappingmetodo del gestore annotato con un Stringtipo di ritorno (e no @ResponseBody) ha il suo valore di ritorno gestito da un ViewNameMethodReturnValueHandlerche interpreta la stringa come un nome di visualizzazione e la usa per passare attraverso il processo che spiego nella mia risposta. Con @ResponseBody, Spring MVC utilizzerà invece RequestResponseBodyMethodProcessorche invece scrive la stringa direttamente nella risposta HTTP, ovvero. nessuna risoluzione di visualizzazione.
Sotirios Delimanolis

97

Ho risolto questo problema utilizzando @ResponseBody come di seguito:

@RequestMapping(value = "/resturl", method = RequestMethod.GET, produces = {"application/json"})
    @ResponseStatus(HttpStatus.OK)
    @Transactional(value = "jpaTransactionManager")
    public @ResponseBody List<DomainObject> findByResourceID(@PathParam("resourceID") String resourceID) {

10
Vogliono restituire HTML risolvendo una vista, non restituire una versione serializzata di un file List<DomainObject>.
Sotirios Delimanolis

2
Questo ha risolto il mio problema durante la restituzione di una risposta JSON per il servizio web di riposo primaverile ..
Joe

Bello, se non specifichi produce = {"application / json"}, funziona ancora. Produce json per impostazione predefinita?
Jay

74

@Controller@RestController

Ho avuto lo stesso problema e ho notato che anche il mio controller era annotato con @Controller. Sostituendolo con @RestControllerrisolto il problema. Ecco la spiegazione di Spring Web MVC :

@RestController è un'annotazione composta che è essa stessa meta-annotata con @Controller e @ResponseBody che indicano un controller il cui ogni metodo eredita l'annotazione @ResponseBody a livello di tipo e quindi scrive direttamente nel corpo della risposta rispetto alla risoluzione della vista e al rendering con un modello HTML.


1
@TodorTodorov Lo ha fatto per me
Igor Rodriguez

@TodorTodorov e per me!
Ha corso il

3
Ha funzionato anche per me. Avevo un @ControllerAdvicecon un handleXyExceptionmetodo in esso, che restituiva il mio oggetto invece di un ResponseEntity. L'aggiunta @RestControllerin cima @ControllerAdviceall'annotazione ha funzionato e il problema è scomparso.
Igor

36

Ecco come ho risolto questo 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();
    }

1
Questo è solo per i casi di prova. Non per i controllori.
cst1992

2
Stavo aiutando qualcuno a risolvere questo problema in uno dei loro nuovi unit test, questo è esattamente quello che stavamo cercando.
Bradford2000

L'ho usato, ma nonostante abbia fornito il prefisso e il suffisso errati per il mio risolutore nel test, ha funzionato. Potete fornire un ragionamento alla base di questo, perché è necessario?
dushyantashu

questa risposta dovrebbe essere votata per essere la più corretta e specifica
Caffeine Coder

20

Sto usando Spring Boot per provare a caricare una pagina web, non per testare, e ho riscontrato questo problema. La mia soluzione era leggermente diversa da quelle sopra, considerando le circostanze leggermente diverse. (anche se quelle risposte mi hanno aiutato a capire.)

Ho semplicemente dovuto modificare la dipendenza dello starter Spring Boot in Maven da:

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

per:

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

Basta cambiare il 'web' in 'thymeleaf' per me ha risolto il problema.


1
Per me, non era necessario cambiare il web di avviamento, ma avevo la dipendenza a foglia di timo con <scope> test </scope>. Quando ho rimosso l'ambito "test", ha funzionato. Grazie per l'indizio!
Georgina Diaz il

16

Ecco una soluzione semplice se in realtà non ti interessa rendere la vista.

Crea una sottoclasse di InternalResourceViewResolver che non controlla i percorsi della vista circolare:

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

Quindi imposta il tuo test con esso:

MockMvc mockMvc;

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

    mockMvc =
            MockMvcBuilders.standaloneSetup(controller)
                    .setViewResolvers(new StandaloneMvcTestViewResolver())
                    .build();
}

Questo ha risolto il mio problema. Ho appena aggiunto una classe StandaloneMvcTestViewResolver nella stessa directory dei test e l'ho usata in MockMvcBuilders come descritto sopra. Grazie
Matheus Araujo

Ho avuto lo stesso problema e questo lo ha risolto anche per me. Molte grazie!
Johan

Questa è un'ottima soluzione che (1) non necessita di cambiare i controller e (2) può essere riutilizzata in tutte le classi di test con una semplice importazione per classe. +1
Nander Speerstra

Oldie but goldie! Mi ha salvato la giornata. Grazie per questa soluzione alternativa +1
Raistlin

13

Se stai usando Spring Boot, aggiungi la dipendenza thymeleaf nel tuo pom.xml:

    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring4</artifactId>
        <version>2.1.6.RELEASE</version>
    </dependency>

1
Upvote. La mancanza di dipendenza da foglia di timo è stata la causa di questo errore nel mio progetto. Tuttavia, se stai usando Spring Boot, la dipendenza sarebbe invece questa:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
Peterh

8

Aggiunta /dopo aver /preferencerisolto il problema per me:

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

8

Nel mio caso, stavo provando Kotlin + Spring boot e sono entrato nel problema Circular View Path. Tutti i suggerimenti che ho ricevuto online non potevano essere d'aiuto, finché non ho provato quanto segue:

Inizialmente avevo annotato il mio controller usando @Controller

import org.springframework.stereotype.Controller

Ho quindi sostituito @Controllercon@RestController

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

E ha funzionato.


6

se non hai usato @RequestBody e stai usando solo @Controller, il modo più semplice per risolvere questo problema è usare @RestControllerinvece di@Controller


questo non è corretto, ora mostrerà il nome del file, invece di mostrare il modello
Ashish Kamble

1
dipende dal problema reale. questo errore può verificarsi a causa di molti motivi
MozenRath

4

Aggiungi l'annotazione @ResponseBodyal ritorno del metodo.


Si prega di includere una spiegazione di come e perché questo risolve il problema aiuterebbe davvero a migliorare la qualità del tuo post e probabilmente si tradurrebbe in più voti positivi.
Android

3

Sto usando Spring Boot con Thymeleaf. Questo è ciò che ha funzionato per me. Ci sono risposte simili con JSP, ma nota che sto usando HTML, non JSP, e queste sono nella cartella src/main/resources/templatescome in un progetto Spring Boot standard come spiegato qui . Questo potrebbe anche essere il tuo 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;
}

Spero che questo ti aiuti.


3

Quando si esegue Spring Boot + Freemarker se viene visualizzata la pagina:

Pagina di errore Whitelabel Questa applicazione non ha una mappatura esplicita per / error, quindi stai vedendo questo come un fallback.

Nella versione spring-boot-starter-parent 2.2.1.RELEASE il freemarker non funziona:

  1. rinominare i file Freemarker da .ftl a .ftlh
  2. Aggiungi ad application.properties: spring.freemarker.expose-request-attributes = true

spring.freemarker.suffix = .ftl


1
La semplice ridenominazione dei file Freemarker da .ftl a .ftlh ha risolto il problema per me.
jannnik,

Amico ... ti devo una birra. Ho perso la mia intera giornata a causa di questa cosa di rinominare.
julianobrasil

2

Per Thymeleaf:

Ho appena iniziato a utilizzare la molla 4 e la foglia di timo, quando ho riscontrato questo errore è stato risolto aggiungendo:

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

1

Quando si utilizza l' @Controllerannotazione, sono necessarie @RequestMappinge @ResponseBodyannotazioni. Riprova dopo aver aggiunto l'annotazione@ResponseBody


0

Uso l'annotazione per configurare l'app web di primavera, il problema è stato risolto aggiungendo un InternalResourceViewResolverbean alla configurazione. Spero possa essere utile.

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

Grazie, questo funziona bene per me. La mia app si è guastata dopo l'aggiornamento a Spring Boot 1.3.1 da 1.2.7 ed era solo questa riga che non funzionava Registry.addViewController ("/ login"). SetViewName ("login"); Durante la registrazione di quel bean, l'app ha funzionato di nuovo ... almeno l'accesso è andato a buon fine.
le0diaz

0

Questo accade perché Spring sta rimuovendo la "preferenza" e aggiungendo di nuovo la "preferenza" facendo lo stesso percorso della richiesta Uri.

Succede così: richiesta Uri: "/ preferenza"

rimuovi "preferenza": "/"

aggiungi percorso: "/" + "preferenza"

stringa finale: "/ preferenza"

Questo sta entrando in un ciclo che Spring ti avvisa lanciando un'eccezione.

È meglio nel tuo interesse dare un nome di visualizzazione diverso come "preferenzaView" o qualsiasi cosa tu voglia.


0

prova ad aggiungere la dipendenza compile ("org.springframework.boot: spring-boot-starter-thymeleaf") al tuo file gradle. Thymeleaf aiuta a mappare le viste.


0

Nel mio caso, ho riscontrato questo problema durante il tentativo di servire le pagine JSP utilizzando l'applicazione Spring Boot.

Ecco cosa ha funzionato per me:

application.properties

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

pom.xml

Per abilitare il supporto per JSP, dovremmo aggiungere una dipendenza da tomcat-embed-jasper.

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

-2

Un altro semplice approccio:

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);
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.