Как избежать исключения «Круговой путь просмотра» с помощью теста 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?

balteo
источник
1
Можете ли вы опубликовать то, ViewResolverчто используете, когда это не удается?
Сотириос Делиманолис
@SotiriosDelimanolis: я не уверен, используется ли какой-либо viewResolver в Spring MVC Test. документация
balteo
8
Я столкнулся с той же проблемой, но проблема заключалась в том, что я не добавил ниже зависимость. <dependency> <groupId> org.springframework.boot </groupId> <artifactId> spring-boot-starter-thymeleaf </artifactId> </dependency>
Аамир 05
использовать @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содержит uri обрабатываемого запроса, то есть /context/preference.

В приведенном выше коде понимается, что если бы вы перешли на /context/preferenceтот же сервлет (поскольку он обрабатывал предыдущий), он обработал бы запрос, и вы вошли бы в бесконечный цикл.


Когда вы объявляете a ThymeleafViewResolverи a ServletContextTemplateResolverс конкретным prefixand suffix, он строит по- Viewразному, давая ему путь вроде

WEB-INF/web-templates/preference.html

ThymeleafViewэкземпляры находят файл относительно ServletContextпути, используя ServletContextResourceResolver

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

что в конечном итоге

return servletContext.getResourceAsStream(resourceName);

Это получает ресурс, относящийся к ServletContextпути. Затем он может использовать TemplateEngineдля создания HTML. Здесь не может быть бесконечного цикла.

Сотириос Делиманолис
источник
1
Спасибо за подробный ответ. Я понимаю, почему цикл не возникает, когда я использую Thymeleaf, и почему он возникает, когда я не использую преобразователь представлений Thymeleaf. Однако я до сих пор не уверен, как изменить свою конфигурацию, чтобы я мог протестировать свое приложение ...
balteo
1
@balteo При использовании разрешено в качестве файла по отношению к и вы предоставите. Когда вы не используете это решение, Spring использует значение по умолчанию, которое находит ресурсы с расширением . Этот ресурс может быть . В данном случае это связано с тем, что путь соответствует вашему . ThymleafViewResolverViewprefixsuffixInternalResourceViewResolverRequestDispatcherServlet/preferenceDispatcherServlet
Сотириос Делиманолис
2
@balteo Чтобы протестировать приложение, укажите правильный ViewResolver. Либо, ThymeleafViewResolverкак в вашем вопросе, вы сами настроили InternalResourceViewResolverили измените имя представления, которое вы возвращаете в своем контроллере.
Сотириос Делиманолис
Спасибо Спасибо спасибо! Я не мог понять, почему внутренний преобразователь представления ресурсов предпочел пересылать, а не «включать», но теперь с вашим объяснением кажется, что использование «ресурса» в имени немного двусмысленно. Это звездное объяснение.
Крис Томпсон,
2
@ShirgillFarhanAnsari @RequestMappingАннотированный метод обработчика с Stringтипом возврата (и no @ResponseBody) имеет возвращаемое значение, обрабатываемое a, ViewNameMethodReturnValueHandlerкоторое интерпретирует String как имя представления и использует его для выполнения процесса, который я объясняю в своем ответе. С @ResponseBody, Spring MVC будет использовать вместо этого , RequestResponseBodyMethodProcessorкоторый вместо записывает строку непосредственно в ответ 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 для веб-службы Spring rest ..
Джо,
Хорошо, если я не укажу Produce = {"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
Я использовал это, но, несмотря на то, что в тесте я указывал неправильный префикс и суффикс для моего распознавателя, он работал. Можете ли вы обосновать это, почему это необходимо?
душянташу
этот ответ должен быть проголосован как наиболее правильный и конкретный
Caffeine Coder
20

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

Мне просто пришлось изменить свою стартовую зависимость Spring Boot в Maven с:

<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> test </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, как описано выше. Спасибо
Матеус Арауджо
У меня была такая же проблема, и это тоже исправило ее. Большое спасибо!
Johan
Это отличное решение, которое (1) не требует изменения контроллеров и (2) может быть повторно использовано во всех тестовых классах с одним простым импортом для каждого класса. +1
Нандер Спирстра
Олди, но Голди! Спас мой день. Спасибо за этот обходной путь. +1
Рейстлин
13

Если вы используете Spring Boot, добавьте зависимость тимелеафа в свой pom.xml:

    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring4</artifactId>
        <version>2.1.6.RELEASE</version>
    </dependency>
Сарвар Нишонбоев
источник
1
Upvote. Отсутствие зависимости Thymeleaf было причиной этой ошибки в моем проекте. Однако, если вы используете Spring Boot, зависимость будет выглядеть следующим образом:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
peterh
8

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

@Test
public void circularViewPathIssue() throws Exception {
    mockMvc.perform(get("/preference/"))
           .andDo(print());
}
Светлана Митрахович
источник
8

В моем случае я пробовал загрузку Kotlin + Spring и столкнулся с проблемой Circular View Path. Все предложения, которые я получил в Интернете, не помогли, пока я не попробовал следующее:

Изначально я аннотировал свой контроллер, используя @Controller

import org.springframework.stereotype.Controller

Затем я заменил @Controllerна@RestController

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

И это сработало.

johnmilimo
источник
7

если вы не использовали @RequestBody и используете только @Controller, самый простой способ исправить это - использовать @RestControllerвместо@Controller

MozenRath
источник
это не исправить, теперь вместо шаблона будет отображаться имя вашего файла
Ашиш Камбл
1
это зависит от реальной проблемы. эта ошибка может возникать по многим причинам
MozenRath
4

Добавьте аннотацию @ResponseBodyк вашему методу return.

Ишаан Арора
источник
Пожалуйста, включите объяснение того, как и почему это решает проблему, это действительно поможет улучшить качество вашего сообщения и, возможно, приведет к большему количеству голосов за.
Android
3

Я использую Spring Boot с Thymeleaf. Это то, что у меня сработало. Есть аналогичные ответы с 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 Это приложение не имеет явного сопоставления для / error, поэтому вы рассматриваете это как запасной вариант.

В версии spring-boot-starter-parent 2.2.1.RELEASE 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

Для тимелеафа:

Я только начал использовать Spring 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

Я использую аннотацию для настройки веб-приложения Spring, проблема решена путем добавления InternalResourceViewResolverbean-компонента в конфигурацию. Надеюсь, это будет полезно.

@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, и была только эта строка, в которой произошел сбой реестра .addViewController ("/ login"). SetViewName ("login"); При регистрации этого bean-компонента приложение снова заработало ... по крайней мере, вход в систему прошел.
le0diaz
0

Это происходит потому, что Spring удаляет «предпочтение» и снова добавляет «предпочтение», используя тот же путь, что и Uri запроса.

Происходит так: запрос Uri: "/ preference"

удалить "предпочтение": "/"

добавить путь: "/" + "предпочтение"

конец строки: "/ предпочтение"

Это входит в цикл, о котором Spring уведомляет вас, выбрасывая исключение.

Лучше всего в ваших интересах дать другое имя представления, например "preferenceView" или что угодно.

xpioneer
источник
0

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

айшвария коре
источник
0

В моем случае эта проблема возникла при попытке обслуживать страницы JSP с помощью загрузочного приложения Spring.

Вот что у меня сработало:

application.properties

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

pom.xml

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

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <scope>provided</scope>
</dependency>
Faouzi
источник
-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);
    }
}
Беззубый провидец
источник