So vermeiden Sie die Ausnahme „Kreisansichtspfad“ beim Spring MVC-Test

117

Ich habe den folgenden Code in einem meiner Controller:

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

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

Ich versuche einfach, es mit dem Spring MVC-Test wie folgt zu testen :

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

Ich erhalte die folgende Ausnahme:

Kreisansichtspfad [Präferenz]: würde erneut an die aktuelle Handler-URL [/ Präferenz] zurücksenden. Überprüfen Sie Ihr ViewResolver-Setup! (Hinweis: Dies kann das Ergebnis einer nicht angegebenen Ansicht sein, da der Name der Standardansicht generiert wurde.)

Was ich seltsam finde, ist, dass es gut funktioniert, wenn ich die "vollständige" Kontextkonfiguration lade , die die Vorlage und die Ansichtsauflöser enthält, wie unten gezeigt:

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

Mir ist klar, dass das vom Vorlagen-Resolver hinzugefügte Präfix sicherstellt, dass es keinen "kreisförmigen Ansichtspfad" gibt, wenn die App diesen Vorlagen-Resolver verwendet.

Aber wie soll ich dann meine App mit dem Spring MVC-Test testen?

balteo
quelle
1
Können Sie das, was ViewResolverSie verwenden, posten , wenn es fehlschlägt?
Sotirios Delimanolis
@SotiriosDelimanolis: Ich bin nicht sicher, ob viewResolver von Spring MVC Test verwendet wird. Dokumentation
Balteo
8
Ich hatte das gleiche Problem, aber das Problem war, dass ich die Abhängigkeit nicht hinzugefügt habe. <dependency> <groupId> org.springframework.boot </ groupId> <artifactId> Spring-Boot-Starter-Thymeleaf </ifactId> </ dependency>
aamir
Verwenden Sie @RestControlleranstelle von@Controller
MozenRath

Antworten:

65

Dies hat nichts mit Spring MVC-Tests zu tun.

Wenn Sie a nicht deklarieren ViewResolver, registriert Spring einen Standardwert, InternalResourceViewResolverder Instanzen JstlViewfür das Rendern von erstellt View.

Die JstlViewKlasse erweitert InternalResourceViewsich

Wrapper für eine JSP oder eine andere Ressource in derselben Webanwendung. Macht Modellobjekte als Anforderungsattribute verfügbar und leitet die Anforderung mithilfe eines javax.servlet.RequestDispatcher an die angegebene Ressourcen-URL weiter.

Eine URL für diese Ansicht soll eine Ressource in der Webanwendung angeben, die für die Weiterleitungs- oder Include-Methode von RequestDispatcher geeignet ist.

Mutig ist meins. Mit anderen Worten, die Ansicht wird vor dem Rendern versuchen, eine RequestDispatcherzu bekommen, zu der forward(). Zuvor wird Folgendes überprüft

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

Wo pathist der Ansichtsname, was Sie von der zurückgegeben haben @Controller. In diesem Beispiel also preference. Die Variable urienthält die URL der zu bearbeitenden Anforderung /context/preference.

Der obige Code erkennt, dass bei einer Weiterleitung /context/preferencedasselbe Servlet (da dasselbe das vorherige behandelt hat) die Anforderung verarbeiten würde und Sie in eine Endlosschleife geraten würden.


Wenn Sie a ThymeleafViewResolverund a ServletContextTemplateResolvermit einem bestimmten prefixund deklarieren suffix, baut es das Viewanders auf und gibt ihm einen Pfad wie

WEB-INF/web-templates/preference.html

ThymeleafViewInstanzen suchen die Datei relativ zum ServletContextPfad mithilfe von a ServletContextResourceResolver

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

was schließlich

return servletContext.getResourceAsStream(resourceName);

Dadurch wird eine Ressource abgerufen, die relativ zum ServletContextPfad ist. Es kann dann das verwenden TemplateEngine, um den HTML-Code zu generieren. Hier kann keine Endlosschleife passieren.

Sotirios Delimanolis
quelle
1
Vielen Dank für Ihre ausführliche Antwort. Ich verstehe, warum die Schleife nicht auftritt, wenn ich Thymeleaf verwende, und warum sie auftritt, wenn ich den Thymeleaf-Ansichtsauflöser nicht verwende. Ich bin mir jedoch immer noch nicht sicher, wie ich meine Konfiguration ändern soll, damit ich meine App testen kann ...
balteo
1
@balteo Wenn Sie ThymleafViewResolverdas Viewals eine Datei in Bezug auf die aufgelöst prefixund suffixSie zur Verfügung stellen. Wenn Sie diese Auflösung nicht verwenden, verwendet Spring einen Standardwert, InternalResourceViewResolverder Ressourcen mit a findet RequestDispatcher. Diese Ressource kann eine sein Servlet. In diesem Fall liegt es daran, dass der Pfad /preferenceIhrem Pfad zugeordnet ist DispatcherServlet.
Sotirios Delimanolis
2
@balteo Um Ihre App zu testen, geben Sie eine korrekte an ViewResolver. Entweder ThymeleafViewResolverwie in Ihrer Frage, Ihren eigenen konfigurierten InternalResourceViewResolveroder den Ansichtsnamen ändern, den Sie in Ihrem Controller zurückgeben.
Sotirios Delimanolis
Danke danke danke! Ich konnte nicht herausfinden, warum der Resolver für die interne Ressourcenansicht lieber weiterleitete als "einzuschließen", aber jetzt mit Ihrer Erklärung scheint es, als ob die Verwendung von "Ressource" im Namen etwas mehrdeutig ist. Diese Erklärung ist hervorragend.
Chris Thompson
2
@ShirgillFarhanAnsari Bei einer mit @RequestMappingAnmerkungen versehenen Handlermethode mit einem StringRückgabetyp (und Nein @ResponseBody) wird der Rückgabewert von a behandelt, ViewNameMethodReturnValueHandlerder den String als Ansichtsnamen interpretiert und ihn verwendet, um den in meiner Antwort erläuterten Prozess zu durchlaufen. Mit @ResponseBodyverwendet RequestResponseBodyMethodProcessorstattdessen Spring MVC, das stattdessen den String direkt in die HTTP-Antwort schreibt, d. H. Keine Ansichtsauflösung.
Sotirios Delimanolis
97

Ich habe dieses Problem mit @ResponseBody wie folgt gelöst:

@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
quelle
10
Sie möchten HTML zurückgeben, indem sie eine Ansicht auflösen, und keine serialisierte Version von a zurückgeben List<DomainObject>.
Sotirios Delimanolis
2
Dies löste mein Problem, als ich eine JSON-Antwort für den Spring Rest-Webdienst zurückgab.
Joe
Schön, wenn ich nicht produziere = {"application / json"}, funktioniert es trotzdem. Produziert es standardmäßig JSON?
Jay
73

@Controller@RestController

Ich hatte das gleiche Problem und bemerkte, dass mein Controller auch mit Anmerkungen versehen war @Controller. Das Ersetzen durch hat @RestControllerdas Problem behoben. Hier ist die Erklärung von Spring Web MVC :

@RestController ist eine zusammengesetzte Annotation, die selbst mit @Controller und @ResponseBody meta-annotiert ist und einen Controller angibt, dessen jede Methode die Annotation @ResponseBody auf Typebene erbt und daher direkt in den Antworttext im Vergleich zur Ansichtsauflösung und zum Rendern mit einer HTML-Vorlage schreibt.

Boris
quelle
1
@TodorTodorov Es hat für mich getan
Igor Rodriguez
@TodorTodorov und für mich!
Ran
2
Hat auch für mich gearbeitet. Ich hatte ein @ControllerAdvicemit einem handleXyExceptionVerfahren darin, die anstelle eines ResponseEntity mein eigenes Objekt zurückgegeben. Das Hinzufügen @RestControllerzu der @ControllerAdviceAnmerkung hat funktioniert und das Problem ist behoben.
Igor
36

So habe ich dieses Problem gelöst:

@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
quelle
1
Dies gilt nur für Testfälle. Nicht für Controller.
cst1992
2
Hat jemandem bei der Behebung dieses Problems in einem seiner neuen Komponententests geholfen? Genau das haben wir gesucht.
Bradford2000
Ich habe dies verwendet, aber obwohl ich im Test das falsche Präfix und Suffix für meinen Resolver angegeben habe, hat es funktioniert. Können Sie eine Begründung dafür liefern, warum dies erforderlich ist?
Dushyantashu
Diese Antwort sollte als richtig und spezifisch eingestuft werden
DareDevil
20

Ich verwende Spring Boot, um eine Webseite zu laden, nicht zu testen, und hatte dieses Problem. Meine Lösung war etwas anders als die oben genannten, wenn man die etwas anderen Umstände berücksichtigt. (obwohl diese Antworten mir geholfen haben zu verstehen.)

Ich musste einfach meine Spring Boot-Starter-Abhängigkeit in Maven ändern von:

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

zu:

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

Nur das 'Web' in 'Thymeleaf' zu ändern, hat das Problem für mich behoben.

Old Schooled
quelle
1
Für mich war es nicht notwendig, das Starter-Web zu ändern, aber ich hatte die Thymeleaf-Abhängigkeit mit <scope> test </ scope>. Als ich den "Test" -Bereich entfernte, funktionierte es. Danke für den Hinweis!
Georgina Diaz
16

Hier ist eine einfache Lösung, wenn Sie das Rendern der Ansicht nicht wirklich interessieren.

Erstellen Sie eine Unterklasse von InternalResourceViewResolver, die nicht nach kreisförmigen Ansichtspfaden sucht:

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

Dann richten Sie Ihren Test damit ein:

MockMvc mockMvc;

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

    mockMvc =
            MockMvcBuilders.standaloneSetup(controller)
                    .setViewResolvers(new StandaloneMvcTestViewResolver())
                    .build();
}
Dave Bower
quelle
Dies hat mein Problem behoben. Ich habe gerade eine StandaloneMvcTestViewResolver-Klasse im selben Verzeichnis der Tests hinzugefügt und sie in den MockMvcBuilders wie oben beschrieben verwendet. Vielen Dank
Matheus Araujo
Ich hatte das gleiche Problem und das hat es auch für mich behoben. Vielen Dank!
Johan
Dies ist eine großartige Lösung, bei der (1) die Controller nicht geändert werden müssen und (2) in allen Testklassen mit einem einfachen Import pro Klasse wiederverwendet werden kann. +1
Nander Speerstra
Alt aber gut! Hat meinen Tag gerettet. Vielen Dank für diese
Problemumgehung
13

Wenn Sie Spring Boot verwenden, fügen Sie Ihrer pom.xml die Abhängigkeit von Thymeleaf hinzu:

    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring4</artifactId>
        <version>2.1.6.RELEASE</version>
    </dependency>
Sarvar Nishonboev
quelle
1
Upvote. Das Fehlen der Thymeleaf-Abhängigkeit hat diesen Fehler in meinem Projekt verursacht. <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
Wenn
8

Hinzufügen /nach /preferencegelöst das Problem für mich:

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

In meinem Fall habe ich Kotlin + Spring Boot ausprobiert und bin zum Thema Circular View Path gekommen. Alle Vorschläge, die ich online bekam, konnten nicht helfen, bis ich Folgendes versuchte:

Ursprünglich hatte ich meinen Controller mit kommentiert @Controller

import org.springframework.stereotype.Controller

Ich habe dann ersetzt @Controllermit@RestController

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

Und es hat funktioniert.

Johnmilimo
quelle
6

Wenn Sie keinen @RequestBody verwendet haben und nur verwenden @Controller, ist die einfachste Möglichkeit, dies zu beheben, die Verwendung von @RestControlleranstelle von@Controller

MozenRath
quelle
Dies ist nicht behoben, jetzt wird Ihr Dateiname angezeigt, stattdessen wird die Vorlage
angezeigt
1
das hängt vom eigentlichen Problem ab. Dieser Fehler kann aus vielen Gründen auftreten
MozenRath
4

Fügen Sie die Anmerkung @ResponseBodyIhrer Methodenrückgabe hinzu.

Ishaan Arora
quelle
Bitte geben Sie eine Erklärung an, wie und warum dies das Problem löst. Dies würde wirklich dazu beitragen, die Qualität Ihres Beitrags zu verbessern und wahrscheinlich zu mehr Up-Votes zu führen.
Android
3

Ich benutze Spring Boot mit Thymeleaf. Das hat bei mir funktioniert. Es gibt ähnliche Antworten mit JSP, aber beachten Sie, dass ich HTML verwende, nicht JSP, und diese befinden sich im Ordner src/main/resources/templateswie in einem Standard-Spring Boot-Projekt, wie hier erläutert . Dies könnte auch Ihr Fall sein.

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

Hoffe das hilft.

Pedro Lopez
quelle
3

Wenn Sie Spring Boot + Freemarker ausführen, wird folgende Seite angezeigt:

Whitelabel-Fehlerseite Diese Anwendung hat keine explizite Zuordnung für / error, daher sehen Sie dies als Fallback.

In der Version Spring-Boot-Starter-Parent 2.2.1.RELEASE funktioniert Freemarker nicht:

  1. Benennen Sie Freemarker-Dateien von .ftl in .ftlh um
  2. Zu application.properties hinzufügen: spring.freemarker.expose-request-attribute = true

spring.freemarker.suffix = .ftl

Max
quelle
1
Das einfache Umbenennen von Freemarker-Dateien von .ftl in .ftlh löste das Problem für mich.
Jannnik
Mann ... ich schulde dir ein Bier. Ich habe meinen ganzen Tag wegen dieser Umbenennung verloren.
Julianobrasil
2

Für Thymeleaf:

Ich habe gerade angefangen, Spring 4 und Thymeleaf zu verwenden. Als ich auf diesen Fehler stieß, wurde er durch Hinzufügen von:

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

Wenn Sie @ControllerAnmerkungen verwenden, benötigen Sie @RequestMappingund @ResponseBodyAnmerkungen. Versuchen Sie es erneut, nachdem Sie eine Anmerkung hinzugefügt haben@ResponseBody

Gowri Ayyanar
quelle
0

Ich verwende die Anmerkung, um die Spring Web App InternalResourceViewResolverzu konfigurieren. Das Problem wurde durch Hinzufügen einer Bean zur Konfiguration gelöst . Hoffe es wäre hilfreich.

@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
quelle
Danke, das funktioniert gut für mich. Meine App ist nach dem Upgrade von 1.2.7 auf Spring Boot 1.3.1 kaputt gegangen und es war nur diese Zeile, bei der registry.addViewController ("/ login") fehlgeschlagen ist. SetViewName ("login"); Bei der Registrierung dieser Bean funktionierte die App wieder ... zumindest ging der Login wll.
Le0diaz
0

Dies geschieht, weil Spring "Präferenz" entfernt und die "Präferenz" erneut anfügt, wobei derselbe Pfad wie bei der Anforderung Uri verwendet wird.

So passiert: Anfrage Uri: "/ Präferenz"

"Präferenz" entfernen: "/"

Pfad anhängen: "/" + "Voreinstellung"

Endzeichenfolge: "/ Präferenz"

Dies gerät in eine Schleife, die der Frühling durch Auslösen einer Ausnahme benachrichtigt.

Es ist am besten in Ihrem Interesse, einen anderen Ansichtsnamen wie "PreferenceView" oder etwas anderes anzugeben, das Sie mögen.

xpioneer
quelle
0

Versuchen Sie, Ihrer Gradle-Datei eine Kompilierungsabhängigkeit ("org.springframework.boot: spring-boot-Starter-thymeleaf") hinzuzufügen. Thymeleaf hilft beim Zuordnen von Ansichten.

Aishwarya Kore
quelle
0

In meinem Fall hatte ich dieses Problem beim Versuch, JSP-Seiten mit der Spring-Boot-Anwendung bereitzustellen.

Folgendes hat bei mir funktioniert:

application.properties

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

pom.xml

Um die Unterstützung für JSPs zu aktivieren, müssten wir eine Abhängigkeit von Tomcat-Embed-Jasper hinzufügen.

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

Ein weiterer einfacher Ansatz:

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);
    }
}
Zahnloser Seher
quelle