Jak uniknąć wyjątku „Okrągła ścieżka widoku” w teście Spring MVC

117

Mam następujący kod w jednym z moich kontrolerów:

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

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

Po prostu próbuję to przetestować za pomocą testu Spring MVC w następujący sposób:

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

Otrzymuję następujący wyjątek:

Okrągła ścieżka widoku [preferencja]: spowoduje ponowne wysłanie z powrotem do bieżącego adresu URL modułu obsługi [/ preferencje]. Sprawdź konfigurację ViewResolver! (Wskazówka: może to być wynikiem nieokreślonego widoku, z powodu domyślnego generowania nazwy widoku).

Wydaje mi się dziwne, że działa dobrze, gdy ładuję „pełną” konfigurację kontekstu, która zawiera szablon i narzędzia do rozpoznawania widoku, jak pokazano poniżej:

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

Doskonale zdaję sobie sprawę, że przedrostek dodany przez program do rozpoznawania szablonów zapewnia, że ​​nie ma „okrągłej ścieżki widoku”, gdy aplikacja używa tego narzędzia do rozpoznawania szablonów.

Ale jak mam przetestować moją aplikację za pomocą testu Spring MVC?

balteo
źródło
1
Czy możesz opublikować plik, ViewResolverktórego używasz, gdy się nie powiedzie?
Sotirios Delimanolis
@SotiriosDelimanolis: Nie jestem pewien, czy jakikolwiek viewResolver jest używany przez Spring MVC Test. dokumentacja
balteo
8
Miałem ten sam problem, ale problem polegał na tym, że nie dodałem poniżej zależności. <dependency> <groupId> org.springframework.boot </groupId> <artifactId> spring-boot-starter-thymeleaf </artifactId> </dependency>
aamir
użyj @RestControllerzamiast@Controller
MozenRath

Odpowiedzi:

65

Nie ma to nic wspólnego z testowaniem Spring MVC.

Gdy nie deklarujesz ViewResolver, Spring rejestruje wartość domyślną, InternalResourceViewResolverktóra tworzy instancje JstlViewdo renderowania View.

JstlViewKlasa rozszerza InternalResourceViewktóry jest

Opakowanie dla strony JSP lub innego zasobu w tej samej aplikacji internetowej. Udostępnia obiekty modelu jako atrybuty żądania i przekazuje żądanie do określonego adresu URL zasobu za pomocą javax.servlet.RequestDispatcher.

Adres URL tego widoku ma określać zasób w aplikacji internetowej, odpowiedni dla metody przesyłania dalej lub dołączania RequestDispatcher.

Bold jest mój. Innymi słowy, widok, przed renderowaniem, spróbuje pobrać element RequestDispatcherdo forward(). Zanim to zrobisz, sprawdza, co następuje

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

gdzie pathjest nazwa widoku, co zwróciłeś z @Controller. W tym przykładzie to jest preference. Zmienna urizawiera identyfikator URI obsługiwanego żądania, którym jest /context/preference.

Powyższy kod zdaje sobie sprawę, że jeśli miałbyś przekazać dalej /context/preference, ten sam aplet (ponieważ ten sam obsługiwał poprzedni) obsłużyłby żądanie i wszedłbyś w nieskończoną pętlę.


Kiedy deklarujesz a ThymeleafViewResolveri a ServletContextTemplateResolverz konkretnym prefixi suffix, buduje on Viewinaczej, nadając mu ścieżkę podobną do

WEB-INF/web-templates/preference.html

ThymeleafViewinstancje lokalizują plik względem ServletContextścieżki przy użyciu rozszerzeniaServletContextResourceResolver

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

które ostatecznie

return servletContext.getResourceAsStream(resourceName);

Pobiera zasób, który jest względny w stosunku do ServletContextścieżki. Następnie może użyć TemplateEnginedo wygenerowania kodu HTML. Nie ma możliwości, aby doszło tutaj do nieskończonej pętli.

Sotirios Delimanolis
źródło
1
Dziękuję za szczegółową odpowiedź. Rozumiem, dlaczego pętla nie występuje, gdy używam Thymeleaf i dlaczego występuje, gdy nie używam programu do rozpoznawania widoku Thymeleaf. Jednak nadal nie jestem pewien, jak zmienić konfigurację, aby móc przetestować moją aplikację ...
balteo
1
@balteo Podczas korzystania uchwala się, co złożyć względem i podać. Jeśli tego nie użyjesz, Spring używa wartości domyślnej, która znajduje zasoby z rozszerzeniem . Ten zasób może być plikiem . W tym przypadku dzieje się tak, ponieważ ścieżka jest mapowana do twojego . ThymleafViewResolverViewprefixsuffixInternalResourceViewResolverRequestDispatcherServlet/preferenceDispatcherServlet
Sotirios Delimanolis
2
@balteo Aby przetestować swoją aplikację, podaj poprawny plik ViewResolver. Albo ThymeleafViewResolverjak w pytaniu, samodzielnie skonfigurowany InternalResourceViewResolverlub zmień nazwę widoku, który zwracasz w kontrolerze.
Sotirios Delimanolis
Dziekuję Dziekuję Dziękuję! Nie mogłem zrozumieć, dlaczego wewnętrzny program rozpoznawania zasobów wolał przekazywać dalej niż „dołączać”, ale teraz, po Twoim wyjaśnieniu, wydaje się, że użycie słowa „zasób” w nazwie jest nieco niejednoznaczne. To wyjaśnienie jest niesamowite.
Chris Thompson
2
@ShirgillFarhanAnsari @RequestMappingMetoda obsługi z adnotacjami z Stringtypem zwracanym (i nie @ResponseBody) ma wartość zwracaną obsługiwaną przez a, ViewNameMethodReturnValueHandlerktóra interpretuje ciąg jako nazwę widoku i używa go do przejścia przez proces, który wyjaśniam w mojej odpowiedzi. W przypadku @ResponseBodySpring MVC zamiast tego użyje on, RequestResponseBodyMethodProcessorktóry zamiast tego zapisuje ciąg bezpośrednio w odpowiedzi HTTP, tj. brak rozdzielczości widoku.
Sotirios Delimanolis
97

Rozwiązałem ten problem używając @ResponseBody jak poniżej:

@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
źródło
10
Chcą zwrócić kod HTML, rozwiązując widok, a nie zwracając serializowaną wersję pliku List<DomainObject>.
Sotirios Delimanolis
2
To rozwiązało mój problem podczas zwracania odpowiedzi JSON dla usługi sieciowej Spring rest ..
Joe
Nieźle, jeśli nie podam wartości wydajność = {"application / json"}, to nadal działa. Czy domyślnie produkuje json?
Jay
74

@Controller@RestController

Miałem ten sam problem i zauważyłem, że mój kontroler również był oznaczony @Controller. Zastąpienie go @RestControllerrozwiązało problem. Oto wyjaśnienie Spring Web MVC :

@RestController to złożona adnotacja, która sama jest meta-adnotowana za pomocą @Controller i @ResponseBody wskazujących kontroler, którego każda metoda dziedziczy adnotację @ResponseBody na poziomie typu i dlatego zapisuje bezpośrednio do treści odpowiedzi w porównaniu z rozdzielczością widoku i renderowaniem za pomocą szablonu HTML.

Boris
źródło
1
@TodorTodorov Zrobiło to dla mnie
Igor Rodriguez
@TodorTodorov i dla mnie!
Biegał
3
Dla mnie też zadziałał. Miałem metodę @ControllerAdvicez handleXyExceptionmetodą, która zwróciła mój własny obiekt zamiast ResponseEntity. Dodawanie @RestControllerna górze @ControllerAdviceadnotacji zadziałało i problem zniknął.
Igor
36

Oto jak rozwiązałem ten problem:

@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
źródło
1
Dotyczy to tylko przypadków testowych. Nie dla kontrolerów.
cst1992
2
Pomagałem komuś w rozwiązaniu tego problemu w jednym z nowych testów jednostkowych, właśnie tego szukaliśmy.
Bradford2000
Użyłem tego, ale pomimo podania niewłaściwego przedrostka i sufiksu dla mojego resolwera w teście, zadziałało. Czy możesz podać uzasadnienie tego, dlaczego jest to wymagane?
dushyantashu
ta odpowiedź powinna zostać uznana za najbardziej poprawną i konkretną
Caffeine Coder
20

Używam Spring Boot, aby spróbować załadować stronę internetową, a nie testować, i mam ten problem. Moje rozwiązanie było nieco inne niż te powyżej, biorąc pod uwagę nieco inne okoliczności. (chociaż te odpowiedzi pomogły mi zrozumieć.)

Po prostu musiałem zmienić moją zależność startera Spring Boot w Maven z:

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

do:

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

Sama zmiana „sieci” na „tymianek” rozwiązała problem.

Old Schooled
źródło
1
Dla mnie nie było potrzeby zmieniać sieci startowej, ale miałem zależność thymeleaf z <scope> test </scope>. Kiedy usunąłem zakres „testowy”, zadziałało. Dzięki za wskazówkę!
Georgina Diaz
16

Oto łatwe rozwiązanie, jeśli nie zależy Ci na renderowaniu widoku.

Utwórz podklasę InternalResourceViewResolver, która nie sprawdza ścieżek widoków kołowych:

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

Następnie skonfiguruj swój test za jego pomocą:

MockMvc mockMvc;

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

    mockMvc =
            MockMvcBuilders.standaloneSetup(controller)
                    .setViewResolvers(new StandaloneMvcTestViewResolver())
                    .build();
}
Dave Bower
źródło
To rozwiązało mój problem. Właśnie dodałem klasę StandaloneMvcTestViewResolver w tym samym katalogu testów i użyłem jej w MockMvcBuilders, jak opisano powyżej. Dzięki
Matheus Araujo,
Miałem ten sam problem i to rozwiązało go również dla mnie. Wielkie dzięki!
Johan
Jest to świetne rozwiązanie, które (1) nie wymaga zmiany kontrolerów i (2) może być ponownie użyte we wszystkich klasach testowych z jednym prostym importem na klasę. +1
Nander Speerstra
Oldie but goldie! Uratowałem mój dzień. Dziękujemy za obejście problemu +1
Raistlin
13

Jeśli używasz Spring Boot, dodaj zależność thymeleaf do swojego pom.xml:

    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring4</artifactId>
        <version>2.1.6.RELEASE</version>
    </dependency>
Sarvar Nishonboev
źródło
1
Głosuj za. Brak zależności Thymeleaf był przyczyną tego błędu w moim projekcie. Jeśli jednak używasz Spring Boot, zależność będzie wyglądać następująco:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
peterh
8

Dodanie /po /preferencerozwiązaniu problemu dla mnie:

@Test
public void circularViewPathIssue() throws Exception {
    mockMvc.perform(get("/preference/"))
           .andDo(print());
}
Svetlana Mitrakhovich
źródło
8

W moim przypadku wypróbowałem but Kotlin + Spring i wpadłem w problem Circular View Path. Wszystkie sugestie, które otrzymałem online, nie pomogły, dopóki nie wypróbowałem poniższych:

Pierwotnie dodałem adnotację do mojego kontrolera za pomocą @Controller

import org.springframework.stereotype.Controller

Następnie otrzymuje @Controllersię z@RestController

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

I zadziałało.

johnmilimo
źródło
6

jeśli nie korzystałeś z @RequestBody i używasz tylko @Controller, najprostszym sposobem naprawienia tego jest użycie @RestControllerzamiast@Controller

MozenRath
źródło
to nie jest naprawione, teraz wyświetli się nazwa pliku, zamiast szablonu
Ashish Kamble
1
to zależy od rzeczywistego problemu. ten błąd może wystąpić z wielu powodów
MozenRath
4

Dodaj adnotację @ResponseBodydo zwrotu metody.

Ishaan Arora
źródło
Proszę dołączyć wyjaśnienie, w jaki sposób i dlaczego to rozwiązuje problem, naprawdę pomogłoby poprawić jakość Twojego posta i prawdopodobnie zaowocowałoby większą liczbą pozytywnych głosów.
Android
3

Używam Spring Boot z Thymeleaf. To właśnie zadziałało dla mnie. Istnieją podobne odpowiedzi w przypadku JSP, ale zauważ, że używam HTML, a nie JSP, i znajdują się one w folderze src/main/resources/templatesjak w standardowym projekcie Spring Boot, jak wyjaśniono tutaj . To może być również Twój przypadek.

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

Mam nadzieję że to pomoże.

Pedro Lopez
źródło
3

Podczas uruchamiania Spring Boot + Freemarker, jeśli pojawi się strona:

Strona błędu białej etykiety Ta aplikacja nie ma wyraźnego mapowania błędu /, więc widzisz to jako rezerwę.

W spring-boot-starter-parent 2.2.1.RELEASE wersja freemarker nie działa:

  1. zmień nazwy plików Freemarker z .ftl na .ftlh
  2. Dodaj do application.properties: spring.freemarker.expose-request-attributes = true

spring.freemarker.suffix = .ftl

Maks
źródło
1
Po prostu zmiana nazw plików Freemarker z .ftl na .ftlh rozwiązała problem za mnie.
jannnik
Człowieku ... Jestem ci winien piwo. Straciłem cały dzień z powodu tej zmiany nazwy.
julianobrasil
2

W przypadku tymianku:

Właśnie zacząłem używać sprężyny 4 i tymianku, kiedy napotkałem ten błąd, rozwiązano go dodając:

<bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
  <property name="templateEngine" ref="templateEngine" />
  <property name="order" value="0" />
</bean> 
Carlos H. Raymundo
źródło
1

Podczas korzystania z @Controlleradnotacji potrzebujesz @RequestMappingi @ResponseBodyadnotacji. Spróbuj ponownie po dodaniu adnotacji@ResponseBody

Gowri Ayyanar
źródło
0

Używam adnotacji do konfiguracji wiosennej aplikacji internetowej, problem rozwiązany przez dodanie InternalResourceViewResolverbeana do konfiguracji. Mam nadzieję, że to byłoby pomocne.

@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
źródło
Dzięki temu dla mnie działa dobrze. Moja aplikacja zepsuła się po aktualizacji do wiosennego rozruchu 1.3.1 z wersji 1.2.7 i tylko ta linia zawodziła register.addViewController ("/ login"). SetViewName ("login"); Podczas rejestracji tego ziarna aplikacja znów działała ... przynajmniej logowanie poszło wll.
le0diaz
0

Dzieje się tak, ponieważ Spring usuwa „preferencję” i ponownie dołącza „preferencję”, tworząc tę ​​samą ścieżkę, co identyfikator Uri żądania.

Dzieje się tak: żądanie Uri: „/ preference”

usuń „preferencję”: „/”

dołącz ścieżkę: „/” + „preferencja”

end string: „/ preference”

To jest wejście w pętlę, o której Spring informuje cię, rzucając wyjątek.

W Twoim interesie jest nadanie innej nazwy widoku, np. „PreferenceView”, lub dowolnej innej.

xpioneer
źródło
0

spróbuj dodać zależność kompilacji ("org.springframework.boot: spring-boot-starter-thymeleaf") do pliku gradle.Thymeleaf pomaga w mapowaniu widoków.

aishwarya kore
źródło
0

W moim przypadku miałem ten problem podczas próby obsługi stron JSP za pomocą aplikacji rozruchowej Spring.

Oto, co zadziałało dla mnie:

application.properties

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

pom.xml

Aby włączyć obsługę stron JSP, musielibyśmy dodać zależność od tomcat-embed-jasper.

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <scope>provided</scope>
</dependency>
Faouzi
źródło
-2

Kolejne proste podejście:

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);
    }
}
Bezzębny Widz
źródło