Як уникнути винятку "Шлях кругового перегляду" за допомогою тесту Spring MVC


117

У мене в одному з контролерів є такий код:

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

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

Я просто намагаюся протестувати його за допомогою тесту Spring MVC наступним чином:

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

Я отримую таке виняток:

Шлях кругового перегляду [уподобання]: знову повернеться до поточної URL-адреси обробника [/ настройки]. Перевірте налаштування ViewResolver! (Підказка. Це може бути результатом не визначеного перегляду через генерування імен перегляду за замовчуванням.)

Що мені здається дивним, це те, що він працює чудово, коли я завантажую "повну" конфігурацію контексту, що включає шаблон і переглядає роздільну здатність, як показано нижче:

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

Мені добре відомо, що префікс, доданий до вирішальника шаблону, забезпечує відсутність "шляху кругового перегляду", коли програма використовує цей роздільник шаблону.

Але тоді як я повинен перевірити свій додаток за допомогою тесту Spring MVC?


1
Чи можете ви опублікувати ViewResolverвикористання, яке не використовується?
Сотіріос Деліманоліс

@SotiriosDelimanolis: Я не впевнений, чи використовується який-небудь viewResolver Spring Test MVC. документація
balteo

8
Я зіткнувся з тією ж проблемою, але проблема в тому, що я не додав нижче залежності. <dependency> <groupId> org.springframework.boot </groupId> <artifactId> весняний завантажувач-стартер-чебрець </artifactId> </dependency>
аамір

використовувати @RestControllerзамість@Controller
MozenRath

Відповіді:


65

Це не має нічого спільного з тестуванням Spring MVC.

Якщо ви не оголошуєте a ViewResolver, Spring реєструє за замовчуванням, InternalResourceViewResolverщо створює екземпляри JstlViewдля візуалізації View.

JstlViewКлас розширює InternalResourceViewякий

Обгортка для JSP або іншого ресурсу в межах одного веб-додатку. Виставляє об'єкти моделі як атрибути запиту і пересилає запит до вказаної URL-адреси ресурсу за допомогою javax.servlet.RequestDispatcher.

URL-адреса для цього представлення повинна визначати ресурс у веб-програмі, що підходить для пересилання або включення методу RequestDispatcher.

Сміливий - мій. Іншими словами, перегляд, перш ніж рендерінг, спробує домогтися того, RequestDispatcherдо якого потрібно forward(). Перед цим він перевіряє наступне

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

де pathназва перегляду, що ви повернули з @Controller. У цьому прикладі, тобто preference. Змінна uriмістить урі оброблюваного запиту, який є /context/preference.

Код вище розуміє, що якби ви пересилали /context/preference, той же сервлет (оскільки той же обробляв попередній) буде обробляти цей запит, і ви переходите до нескінченного циклу.


Коли ви оголошуєте a ThymeleafViewResolverі a ServletContextTemplateResolverз певним prefixі suffix, він будує Viewінакше, надаючи йому такий собі шлях

WEB-INF/web-templates/preference.html

ThymeleafViewекземпляри знаходять файл відносно ServletContextшляху за допомогою a ServletContextResourceResolver

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

які зрештою

return servletContext.getResourceAsStream(resourceName);

При цьому виходить ресурс, який відносно ServletContextшляху. Потім він може використовувати TemplateEngineдля створення HTML. Тут не може трапитися нескінченна петля.


1
Дякуємо за детальну відповідь. Я розумію, чому цикл не виникає, коли я використовую Thymeleaf, і чому він виникає, коли я не використовую роздільну здатність перегляду Thymeleaf. Однак я все ще не впевнений, як змінити конфігурацію, щоб я міг перевірити свою програму ...
balteo

1
@balteo При використанні дозволено в якості файлу по відношенню до і ви надасте. Якщо ви не використовуєте це рішення, Spring використовує типовий параметр, який знаходить ресурси з . Цей ресурс може бути . У цьому випадку це тому, що шлях відображається до вашого . ThymleafViewResolverViewprefixsuffixInternalResourceViewResolverRequestDispatcherServlet/preferenceDispatcherServlet
Sotirios Delimanolis

2
@balteo Щоб перевірити додаток, введіть правильний ViewResolver. Або ThymeleafViewResolverяк у вашому запитанні, власно налаштований InternalResourceViewResolverабо змініть ім'я перегляду, яке ви повертаєте у своєму контролері.
Сотіріос Деліманоліс

Дякую, дякую, дякую! Я не міг зрозуміти, чому вирішувач внутрішнього перегляду ресурсів вважає за краще пересилати, а не "включати", але тепер із вашим поясненням здається, що використання "ресурсу" в імені дещо неоднозначне. Це пояснення є зоряним.
Кріс Томпсон

2
@ShirgillFarhanAnsari Метод @RequestMappingанотованого обробника з Stringтипом повернення (і ні @ResponseBody) повертає його значення, ViewNameMethodReturnValueHandlerяке інтерпретує String як ім'я перегляду, і використовує його для проходження процесу, який я пояснюю у своїй відповіді. З @ResponseBody, Spring MVC замість цього використовуватиме RequestResponseBodyMethodProcessorякий записує String безпосередньо у відповідь HTTP, тобто. немає роздільної здатності перегляду.
Сотіріос Деліманоліс

97

Я вирішив цю проблему, використовуючи @ResponseBody, як показано нижче:

@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
Вони хочуть повернути HTML, вирішивши перегляд, а не повернути серіалізовану версію List<DomainObject>.
Сотіріос Деліманоліс

2
Це вирішило мою проблему під час повернення відповіді JSON для веб-сервісу Весняний відпочинок ..
Джо

Добре, якщо я не вказую, що виробляє = {"application / json"}, він все одно працює. Чи виробляє json за замовчуванням?
Джей

74

@Controller@RestController

У мене був такий самий випуск, і я помітив, що мій контролер також був позначений @Controller. Замінивши її @RestControllerвирішеною проблемою. Ось пояснення від Spring Web MVC :

@RestController - це складена анотація, яка сама мета-коментується за допомогою @Controller та @ResponseBody, що вказує на контролер, кожен метод якого успадковує анотацію @ResponseBody на рівні типу і тому записує безпосередньо в орган відповідей проти роздільної здатності та візуалізації з шаблоном HTML.


1
@TodorTodorov Це зробило для мене
Ігор Родрігес

@TodorTodorov і для мене!
Побіг

3
Працював і для мене. У мене був @ControllerAdviceз handleXyExceptionметодом в ньому, який повернув мій власний об'єкт замість ResponseEntity. Додавання @RestControllerзверху до @ControllerAdviceанотації спрацювало, і проблеми вже немає.
Ігор

36

Ось як я вирішив цю проблему:

@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
Це лише для тестів. Не для контролерів.
cst1992

2
Допомагав комусь у вирішенні цієї проблеми в одному з нових тестів блоку, саме це ми шукали.
Bradford2000

Я використовував це, але, незважаючи на те, що в тесті не було вказано неправильний префікс і суфікс для мого резолютора, він працював. Чи можете ви надати обґрунтування цього, чому це потрібно?
dushyantashu

ця відповідь повинна бути проголосована за найбільш правильну і конкретну
Кофеїн Кодер

20

Я використовую Spring Boot, щоб спробувати завантажити веб-сторінку, а не тестувати, і у мене була ця проблема. Моє рішення було дещо іншим, ніж вище, враховуючи трохи інші обставини. (хоча ці відповіді допомогли мені зрозуміти.)

Мені просто довелося змінити свою залежність від стартера "Spring Boot" в Мевені на:

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

Просто зміна "павутини" на "чебрець" вирішила для мене проблему.


1
Для мене не потрібно було змінювати мережу для початку, але я мав залежність від чебрецю за допомогою тесту <scope> </scope>. Коли я видалив область "тестування", вона спрацювала. Дякую за підказку!
Георгіна Діас

16

Ось просте виправлення, якщо ви насправді не дбаєте про те, щоб переглянути подання.

Створіть підклас InternalResourceViewResolver, який не перевіряє шляхи кругового перегляду:

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

Потім налаштуйте свій тест:

MockMvc mockMvc;

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

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

Це вирішило мою проблему. Я щойно додав клас StandaloneMvcTestViewResolver в той самий каталог тестів і використовував його в MockMvcBuilders, як описано вище. Дякую
Матей Арауджо

У мене була така ж проблема, і це вирішило і для мене. Дуже дякую!
Йоган

Це чудове рішення, що (1) не потребує зміни контролерів і (2) може бути повторно використаний у всіх тестових класах одним простим імпортом на клас. +1
Nander Speerstra

Олді, але золото! Врятував мій день. Дякуємо вам за цей шлях +1
Raistlin

13

Якщо ви використовуєте Spring Boot, додайте залежність від чебрецю до свого pom.xml:

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

1
Оновлення Відсутність залежності від чебрецю було причиною цієї помилки в моєму проекті. Однак, якщо ви використовуєте Spring Boot, тоді залежність буде виглядати <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
приблизно так

8

Додавання /після /preferenceвирішення проблеми для мене:

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

8

У моєму випадку я випробовував завантаження Kotlin + Spring і потрапив у проблему "Круговий погляд". Усі пропозиції, які я отримав в Інтернеті, не змогли допомогти, поки я не спробував нижче:

Спочатку я коментував свій контролер за допомогою @Controller

import org.springframework.stereotype.Controller

Потім я замінив @Controllerна@RestController

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

І це спрацювало.


6

якщо ви ще не використовували @RequestBody, а використовуєте лише @Controllerнайпростіший спосіб виправити це, @RestControllerзамість цього@Controller


це не виправлено, тепер він покаже ім'я вашого файлу, замість цього буде показаний шаблон
Ashish Kamble

1
це залежить від актуальної проблеми. ця помилка може статися з багатьох причин
MozenRath

4

Додайте примітку @ResponseBodyдо повернення методу.


Будь ласка, додайте пояснення, як і чому це вирішує проблему, справді допоможе покращити якість вашої публікації та, ймовірно, призведе до збільшення кількості голосів.
Android

3

Я використовую весняний черевик з чебрецем. Це те, що працювало для мене. Є подібні відповіді з JSP, але зауважте, що я використовую HTML, а не JSP, і вони знаходяться в папці, src/main/resources/templatesяк у стандартному проекті Spring Boot, як пояснено тут . Це також може бути ваш випадок.

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

Сподіваюся, це допомагає.


3

Під час запуску Spring Boot + Freemarker, якщо відображається сторінка:

Сторінка помилок Whitelabel У цій програмі немає чіткого відображення / помилки, тому ви бачите це як резервний.

У початковій версії завантажувача-стартера 2.2.1. ЗВІТКА версія Freemarker не працює:

  1. перейменуйте файли Freemarker з .ftl в .ftlh
  2. Додати до application.properties: spring.freemarker.expose-request-attributes = true

spring.freemarker.suffix = .ftl


1
Просто перейменування файлів Freemarker з .ftl в .ftlh вирішило проблему для мене.
jannnik

Людина ... я тобі завдячую пивом. Я втратив цілий день через цю перейменування.
julianobrasil

2

Для чебрецю:

Я щойно почав використовувати весну 4 і чебрець, коли зіткнувся з цією помилкою, її вирішили додавши:

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

1

Під час використання @Controllerанотацій потрібні @RequestMappingі @ResponseBodyпримітки. Повторіть спробу після додавання примітки@ResponseBody


0

Я використовую анотацію для налаштування весняного веб-додатка, проблему вирішено шляхом додавання InternalResourceViewResolverбіна до конфігурації. Сподіваюся, це буде корисним.

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

Дякую, це прекрасно працює для мене. Мій додаток вийшов з ладу після оновлення до весняного завантаження 1.3.1 з 1.2.7, і лише в цій лінії не було вдається register.addViewController ("/ login"). SetViewName ("login"); При реєстрації цього бобу додаток працював знову ... принаймні, логін пішов wll.
le0diaz

0

Це відбувається тому, що Spring видаляє "перевагу" і додає "перевагу" знову проходячи той самий шлях, що і запит Uri.

Так відбувається: запит на Uri: "/ preference"

видалити "налаштування": "/"

додати шлях: "/" + "налаштування"

кінцевий рядок: "/ preference"

Це потрапляння в цикл, про який Весна сповіщає вас, кидаючи виняток.

Найкраще в інтересах надати іншу назву перегляду, наприклад "preferenceView" або все, що вам подобається.


0

спробуйте додати залежність компіляції ("org.springframework.boot: spring-boot-starter-thymeleaf") до файлу gradle.Thymeleaf допомагає зіставити подання.


0

У моєму випадку у мене виникла ця проблема, намагаючись обслуговувати сторінки JSP за допомогою програми Spring boot.

Ось що для мене спрацювало:

застосування.властивості

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

пом.хмл

Щоб увімкнути підтримку JSP, нам потрібно було б додати залежність від tomcat-embed-jasper.

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

-2

Ще один простий підхід:

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);
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.