Comment éviter l'exception «Chemin de vue circulaire» avec le test Spring MVC

117

J'ai le code suivant dans l'un de mes contrôleurs:

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

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

J'essaie simplement de le tester en utilisant le test Spring MVC comme suit:

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

Je reçois l'exception suivante:

Chemin de vue circulaire [préférence]: renvoie à nouveau à l'URL du gestionnaire actuel [/ préférence]. Vérifiez votre configuration ViewResolver! (Conseil: cela peut être le résultat d'une vue non spécifiée, en raison de la génération de nom de vue par défaut.)

Ce que je trouve étrange, c'est que cela fonctionne bien lorsque je charge la configuration de contexte "complète" qui comprend le modèle et les résolveurs de vue comme indiqué ci-dessous:

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

Je suis bien conscient que le préfixe ajouté par le résolveur de modèle garantit qu'il n'y a pas de "chemin de vue circulaire" lorsque l'application utilise ce résolveur de modèle.

Mais alors comment je suis censé tester mon application à l'aide du test Spring MVC?

balteo
la source
1
Pouvez-vous publier le que ViewResolvervous utilisez en cas d'échec?
Sotirios Delimanolis
@SotiriosDelimanolis: Je ne sais pas si un viewResolver est utilisé par Spring MVC Test. documentation
balteo
8
J'étais confronté au même problème, mais le problème était que je n'ai pas ajouté la dépendance ci-dessous. <dependency> <groupId> org.springframework.boot </groupId> <artifactId> spring-boot-starter-thymeleaf </artifactId> </dependency>
aamir
utiliser à la @RestControllerplace de@Controller
MozenRath

Réponses:

65

Cela n'a rien à voir avec les tests Spring MVC.

Lorsque vous ne déclarez pas a ViewResolver, Spring enregistre une valeur par défaut InternalResourceViewResolverqui crée des instances de JstlViewpour le rendu du View.

La JstlViewclasse s'étend InternalResourceViewqui est

Wrapper pour une JSP ou une autre ressource dans la même application Web. Expose les objets de modèle en tant qu'attributs de demande et transmet la demande à l'URL de ressource spécifiée à l'aide d'un javax.servlet.RequestDispatcher.

Une URL pour cette vue est censée spécifier une ressource dans l'application Web, adaptée à la méthode forward ou include de RequestDispatcher.

Le gras est à moi. En d'autres termes, la vue, avant le rendu, essaiera d'obtenir un RequestDispatchervers lequel forward(). Avant de faire cela, il vérifie les éléments suivants

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

pathest le nom de la vue, ce que vous avez renvoyé du @Controller. Dans cet exemple, c'est preference. La variable uricontient l'URI de la demande en cours de traitement, qui est /context/preference.

Le code ci-dessus se rend compte que si vous deviez transférer vers /context/preference, le même servlet (puisque le même a géré le précédent) gérerait la demande et vous entreriez dans une boucle sans fin.


Lorsque vous déclarez a ThymeleafViewResolveret a ServletContextTemplateResolveravec un prefixet spécifique suffix, il construit Viewdifféremment, en lui donnant un chemin comme

WEB-INF/web-templates/preference.html

ThymeleafViewles instances localisent le fichier par rapport au ServletContextchemin en utilisant un ServletContextResourceResolver

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

qui finalement

return servletContext.getResourceAsStream(resourceName);

Cela obtient une ressource relative au ServletContextchemin. Il peut ensuite utiliser le TemplateEnginepour générer le HTML. Il n'y a aucun moyen qu'une boucle sans fin puisse se produire ici.

Sotirios Delimanolis
la source
1
Merci pour votre réponse détaillée. Je comprends pourquoi la boucle ne se produit pas lorsque j'utilise Thymeleaf et pourquoi elle se produit lorsque je n'utilise pas le résolveur de vue Thymeleaf. Cependant, je ne sais toujours pas comment changer ma configuration pour pouvoir tester mon application ...
balteo
1
@balteo Lorsque vous utilisez ThymleafViewResolverle Viewest résolu comme un fichier relatif à prefixet que suffixvous fournissez. Lorsque vous n'utilisez pas cette résolution, Spring utilise une valeur par défaut InternalResourceViewResolverqui recherche les ressources avec un RequestDispatcher. Cette ressource peut être un fichier Servlet. Dans ce cas, c'est parce que le chemin /preferencecorrespond à votre DispatcherServlet.
Sotirios Delimanolis
2
@balteo Pour tester votre application, fournissez un fichier ViewResolver. Soit le ThymeleafViewResolvercomme dans votre question, votre propre configuré InternalResourceViewResolverou modifiez le nom de la vue que vous renvoyez dans votre contrôleur.
Sotirios Delimanolis
Merci merci merci! Je ne pouvais pas comprendre pourquoi le résolveur de vue des ressources internes préférait transférer plutôt que "inclure", mais maintenant avec votre explication, il semble que l'utilisation de "ressource" dans le nom soit un peu ambiguë. Cette explication est stellaire.
Chris Thompson
2
@ShirgillFarhanAnsari Une @RequestMappingméthode de gestionnaire annotée avec un Stringtype de retour (et non @ResponseBody) a sa valeur de retour gérée par un ViewNameMethodReturnValueHandlerqui interprète la chaîne comme un nom de vue et l'utilise pour suivre le processus que j'explique dans ma réponse. Avec @ResponseBody, Spring MVC utilisera à la place RequestResponseBodyMethodProcessorqui écrit la chaîne directement dans la réponse HTTP, c'est-à-dire. pas de résolution de vue.
Sotirios Delimanolis
97

J'ai résolu ce problème en utilisant @ResponseBody comme ci-dessous:

@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
la source
10
Ils veulent renvoyer du HTML en résolvant une vue, pas une version sérialisée d'un fichier List<DomainObject>.
Sotirios Delimanolis
2
Cela a résolu mon problème en renvoyant une réponse JSON pour le service Web Spring Rest.
Joe
Bien, si je ne spécifie pas produit = {"application / json"}, cela fonctionne toujours. Produit-il json par défaut?
Jay
74

@Controller@RestController

J'ai eu le même problème et j'ai remarqué que mon contrôleur était également annoté avec @Controller. Le remplacer par a @RestControllerrésolu le problème. Voici l'explication de Spring Web MVC :

@RestController est une annotation composée qui est elle-même méta-annotée avec @Controller et @ResponseBody indiquant un contrôleur dont chaque méthode hérite de l'annotation @ResponseBody au niveau du type et écrit donc directement dans le corps de la réponse par rapport à la résolution de la vue et au rendu avec un modèle HTML.

Boris
la source
1
@TodorTodorov Ça l'a fait pour moi
Igor Rodriguez
@TodorTodorov et pour moi!
couru le
3
A travaillé pour moi aussi. J'avais un @ControllerAdviceavec une handleXyExceptionméthode, qui a renvoyé mon propre objet au lieu d'un ResponseEntity. L'ajout @RestControlleren plus de l' @ControllerAdviceannotation a fonctionné et le problème est parti.
Igor
36

Voici comment j'ai résolu ce problème:

@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
la source
1
Ceci est pour les cas de test uniquement. Pas pour les contrôleurs.
cst1992
2
A aidé quelqu'un à résoudre ce problème dans l'un de ses nouveaux tests unitaires, c'est exactement ce que nous recherchions.
Bradford2000
J'ai utilisé cela, mais malgré le mauvais préfixe et suffixe pour mon résolveur dans le test, cela a fonctionné. Pouvez-vous expliquer cela, pourquoi est-ce nécessaire?
dushyantashu du
cette réponse devrait être votée pour être la plus correcte et la plus précise
Caffeine Coder
20

J'utilise Spring Boot pour essayer de charger une page Web, pas pour tester, et j'ai eu ce problème. Ma solution était un peu différente de celles ci-dessus compte tenu des circonstances légèrement différentes. (bien que ces réponses m'ont aidé à comprendre.)

J'ai simplement dû changer ma dépendance de démarrage Spring Boot dans Maven de:

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

à:

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

Le simple fait de changer le «web» en «thymeleaf» a résolu le problème pour moi.

Old Schooled
la source
1
Pour moi, il n'était pas nécessaire de changer le starter-web, mais j'avais la dépendance thymeleaf avec <scope> test </scope>. Lorsque j'ai supprimé la portée "test", cela a fonctionné. Merci pour l'indice!
Georgina Diaz
16

Voici une solution simple si vous ne vous souciez pas du rendu de la vue.

Créez une sous-classe de InternalResourceViewResolver qui ne vérifie pas les chemins de vue circulaire:

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

Ensuite, configurez votre test avec:

MockMvc mockMvc;

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

    mockMvc =
            MockMvcBuilders.standaloneSetup(controller)
                    .setViewResolvers(new StandaloneMvcTestViewResolver())
                    .build();
}
Dave Bower
la source
Cela a résolu mon problème. Je viens d'ajouter une classe StandaloneMvcTestViewResolver dans le même répertoire des tests et l'ai utilisée dans les MockMvcBuilders comme décrit ci-dessus. Merci
Matheus Araujo
J'ai eu le même problème et cela l'a résolu pour moi aussi. Merci beaucoup!
Johan
C'est une excellente solution qui (1) n'a pas besoin de changer les contrôleurs et (2) peut être réutilisée dans toutes les classes de test avec une simple importation par classe. +1
Nander Speerstra
Oldie mais goldie! J'ai sauvé ma journée. Merci pour cette solution de contournement +1
Raistlin
13

Si vous utilisez Spring Boot, ajoutez la dépendance thymeleaf dans votre pom.xml:

    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring4</artifactId>
        <version>2.1.6.RELEASE</version>
    </dependency>
Sarvar Nishonboev
la source
1
Vote positif. La dépendance Thymeleaf manquante est à l'origine de cette erreur dans mon projet. Cependant, si vous utilisez Spring Boot, la dépendance ressemblerait à ceci à la place:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
peterh
8

L'ajout /après avoir /preferencerésolu le problème pour moi:

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

Dans mon cas, j'essayais Kotlin + Spring Boot et je suis entré dans le problème du chemin de vue circulaire. Toutes les suggestions que j'ai reçues en ligne ne pouvaient pas aider, jusqu'à ce que j'essaye ce qui suit:

À l'origine, j'avais annoté ma manette en utilisant @Controller

import org.springframework.stereotype.Controller

J'ai ensuite remplacé @Controllerpar@RestController

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

Et ça a marché.

johnmilimo
la source
6

si vous n'avez pas utilisé de @RequestBody et que vous utilisez uniquement @Controller, le moyen le plus simple de résoudre ce problème consiste à utiliser @RestControllerau lieu de@Controller

MozenRath
la source
ce n'est pas une solution, maintenant il affichera le nom de votre fichier, à la place du modèle
Ashish Kamble
1
cela dépend du problème réel. cette erreur peut se produire pour plusieurs raisons
MozenRath
4

Ajoutez l'annotation @ResponseBodyà votre retour de méthode.

Ishaan Arora
la source
Veuillez inclure une explication de la manière et des raisons pour lesquelles cela résout le problème.
Android
3

J'utilise Spring Boot avec Thymeleaf. C'est ce qui a fonctionné pour moi. Il existe des réponses similaires avec JSP, mais notez que j'utilise HTML, pas JSP, et celles-ci se trouvent dans le dossier src/main/resources/templatescomme dans un projet Spring Boot standard, comme expliqué ici . Cela pourrait aussi être votre cas.

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

J'espère que cela t'aides.

Pedro Lopez
la source
3

Lors de l'exécution de Spring Boot + Freemarker si la page apparaît:

Page d'erreur Whitelabel Cette application n'a pas de mappage explicite pour / error, vous voyez donc cela comme une solution de secours.

Dans la version spring-boot-starter-parent 2.2.1.RELEASE, freemarker ne fonctionne pas:

  1. renommer les fichiers Freemarker de .ftl en .ftlh
  2. Ajouter à application.properties: spring.freemarker.expose-request-attributes = true

spring.freemarker.suffix = .ftl

Max
la source
1
Le simple fait de renommer les fichiers Freemarker de .ftl à .ftlh a résolu le problème pour moi.
jannnik
Mec ... je te dois une bière. J'ai perdu toute ma journée à cause de ce changement de nom.
julianobrasil le
2

Pour Thymeleaf:

Je viens de commencer à utiliser spring 4 et thymeleaf, lorsque j'ai rencontré cette erreur, elle a été résolue en ajoutant:

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

Lorsque vous utilisez l' @Controllerannotation, vous avez besoin @RequestMappinget @ResponseBodyannotations. Réessayez après avoir ajouté une annotation@ResponseBody

Gowri Ayyanar
la source
0

J'utilise l'annotation pour configurer l'application Web Spring, le problème résolu en ajoutant un InternalResourceViewResolverbean à la configuration. J'espère que ce serait 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;
    }
}
alijandro
la source
Merci, cela fonctionne bien pour moi. Mon application est tombée en panne après la mise à niveau vers Spring Boot 1.3.1 à partir de 1.2.7 et seule cette ligne échouait registry.addViewController ("/ login"). SetViewName ("login"); Lors de l'enregistrement de ce bean, l'application a fonctionné à nouveau ... au moins la connexion est passée wll.
le0diaz
0

Cela se produit parce que Spring supprime la «préférence» et ajoute à nouveau la «préférence» en effectuant à nouveau le même chemin que la requête Uri.

Se déroulant comme ceci: request Uri: "/ preference"

supprimer "préférence": "/"

ajouter le chemin: "/" + "préférence"

chaîne de fin: "/ préférence"

C'est entrer dans une boucle que le Spring vous notifie en lançant une exception.

Il est préférable dans votre intérêt de donner un nom de vue différent comme "préférenceView" ou tout ce que vous voulez.

xpioneer
la source
0

essayez d'ajouter une dépendance de compilation ("org.springframework.boot: spring-boot-starter-thymeleaf") à votre fichier gradle. Thymeleaf aide à mapper les vues.

aishwarya kore
la source
0

Dans mon cas, j'ai eu ce problème en essayant de servir des pages JSP à l'aide de l'application Spring Boot.

Voici ce qui a fonctionné pour moi:

application.properties

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

pom.xml

Pour activer la prise en charge des JSP, nous aurions besoin d'ajouter une dépendance sur tomcat-embed-jasper.

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

Une autre approche 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);
    }
}
Voyant édenté
la source