Cara menghindari pengecualian "Jalur tampilan melingkar" dengan pengujian Spring MVC

117

Saya memiliki kode berikut di salah satu pengontrol saya:

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

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

Saya hanya mencoba mengujinya menggunakan tes Spring MVC sebagai berikut:

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

Saya mendapatkan pengecualian berikut:

Jalur tampilan melingkar [preferensi]: akan mengirimkan kembali ke URL penangan saat ini [/ preferensi] lagi. Periksa penyiapan ViewResolver Anda! (Petunjuk: Ini mungkin hasil dari tampilan yang tidak ditentukan, karena pembuatan nama tampilan default.)

Yang saya anggap aneh adalah bahwa ini berfungsi dengan baik ketika saya memuat konfigurasi konteks "lengkap" yang menyertakan template dan resolver tampilan seperti yang ditunjukkan di bawah ini:

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

Saya sangat menyadari bahwa awalan yang ditambahkan oleh pemecah templat memastikan bahwa tidak ada "jalur tampilan melingkar" saat aplikasi menggunakan pemecah templat ini.

Tapi kemudian bagaimana saya bisa menguji aplikasi saya menggunakan tes Spring MVC?

balteo
sumber
1
Bisakah Anda memposting yang ViewResolverAnda gunakan saat gagal?
Sotirios Delimanolis
@SotiriosDelimanolis: Saya tidak yakin apakah viewResolver digunakan oleh Spring MVC Test. dokumentasi
balteo
8
Saya menghadapi masalah yang sama tetapi masalahnya adalah saya belum menambahkan di bawah ketergantungan. <dependency> <groupId> org.springframework.boot </groupId> <artifactId> spring-boot-starter-thymeleaf </artifactId> </dependency>
aamir
gunakan @RestControllersebagai pengganti@Controller
MozenRath

Jawaban:

65

Ini tidak ada hubungannya dengan pengujian Spring MVC.

Saat Anda tidak mendeklarasikan a ViewResolver, Spring mendaftarkan default InternalResourceViewResolveryang membuat instance JstlViewuntuk merender View.

The JstlViewkelas meluas InternalResourceViewyang

Pembungkus untuk JSP atau sumber daya lain dalam aplikasi web yang sama. Mengekspos objek model sebagai atribut permintaan dan meneruskan permintaan ke URL sumber daya yang ditentukan menggunakan javax.servlet.RequestDispatcher.

URL untuk tampilan ini seharusnya menentukan sumber daya dalam aplikasi web, cocok untuk metode penyertaan atau penyertaan RequestDispatcher.

Bold adalah milikku. Dalam otherwords, pandangan, sebelum rendering, akan mencoba untuk mendapatkan RequestDispatcheryang ke forward(). Sebelum melakukan ini, periksa hal-hal berikut

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

di mana pathnama tampilan, apa yang Anda kembalikan dari @Controller. Dalam contoh ini, itu preference. Variabel urimenyimpan uri dari permintaan yang sedang ditangani, yaitu /context/preference.

Kode di atas menyadari bahwa jika Anda meneruskan ke /context/preference, servlet yang sama (karena yang sama menangani sebelumnya) akan menangani permintaan dan Anda akan masuk ke loop tanpa akhir.


Ketika Anda mendeklarasikan a ThymeleafViewResolverdan a ServletContextTemplateResolverdengan spesifik prefixdan suffix, itu membangun secara Viewberbeda, memberinya jalur seperti

WEB-INF/web-templates/preference.html

ThymeleafViewContoh menemukan file relatif ke ServletContextjalur dengan menggunakan file ServletContextResourceResolver

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

yang akhirnya

return servletContext.getResourceAsStream(resourceName);

Ini mendapatkan sumber daya yang relatif terhadap ServletContextjalur. Kemudian dapat menggunakan TemplateEngineuntuk menghasilkan HTML. Tidak mungkin loop tanpa akhir bisa terjadi di sini.

Sotirios Delimanolis
sumber
1
Terima kasih atas jawaban rinci Anda. Saya mengerti mengapa loop tidak terjadi ketika saya menggunakan Thymeleaf dan mengapa itu terjadi ketika saya tidak menggunakan resolver tampilan Thymeleaf. Namun, saya masih tidak yakin bagaimana mengubah konfigurasi saya sehingga saya dapat menguji aplikasi saya ...
balteo
1
@balteo Bila Anda menggunakan ThymleafViewResolveryang Viewdiselesaikan sebagai file relatif terhadap prefixdan suffixAnda berikan. Saat Anda tidak menggunakan penyelesaian itu, Spring menggunakan default InternalResourceViewResolveryang mencari sumber daya dengan file RequestDispatcher. Sumber daya ini dapat berupa Servlet. Dalam hal ini itu karena jalur /preferencememetakan ke Anda DispatcherServlet.
Sotirios Delimanolis
2
@balteo Untuk menguji aplikasi Anda, berikan yang benar ViewResolver. Baik ThymeleafViewResolverseperti dalam pertanyaan Anda, Anda sendiri yang dikonfigurasi InternalResourceViewResolveratau mengubah nama tampilan yang Anda kembalikan di pengontrol Anda.
Sotirios Delimanolis
Terima kasih terima kasih terima kasih! Saya tidak dapat memahami mengapa resolver tampilan sumber daya internal lebih memilih untuk meneruskan daripada "menyertakan" tetapi sekarang dengan penjelasan Anda, tampaknya penggunaan "sumber daya" dalam nama agak ambigu. Penjelasan ini luar biasa.
Chris Thompson
2
@ShirgillFarhanAnsari @RequestMappingMetode penangan beranotasi dengan Stringtipe kembalian (dan tidak @ResponseBody) memiliki nilai kembaliannya ditangani oleh ViewNameMethodReturnValueHandleryang menafsirkan String sebagai nama tampilan, dan menggunakannya untuk melalui proses yang saya jelaskan dalam jawaban saya. Dengan @ResponseBody, Spring MVC akan menggunakan RequestResponseBodyMethodProcessoryang menulis String langsung ke respon HTTP, yaitu. tidak ada resolusi tampilan.
Sotirios Delimanolis
97

Saya memecahkan masalah ini dengan menggunakan @ResponseBody seperti di bawah ini:

@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
sumber
10
Mereka ingin mengembalikan HTML dengan menyelesaikan tampilan, bukan mengembalikan versi serial dari a List<DomainObject>.
Sotirios Delimanolis
2
Ini menyelesaikan masalah saya saat mengembalikan respons JSON untuk layanan web Spring rest ..
Joe
Bagus, jika saya tidak menentukan produksi = {"application / json"}, tetap berfungsi. Apakah itu menghasilkan json secara default?
Jay
74

@Controller@RestController

Saya memiliki masalah yang sama dan saya perhatikan bahwa pengontrol saya juga diberi anotasi @Controller. Menggantinya dengan @RestControllermemecahkan masalah. Berikut penjelasan dari Spring Web MVC :

@RestController adalah anotasi tersusun yang dianotasi secara meta dengan @Controller dan @ResponseBody yang menunjukkan pengontrol yang setiap metodenya mewarisi anotasi @ResponseBody level-tipe dan oleh karena itu menulis langsung ke isi respons vs resolusi tampilan dan rendering dengan template HTML.

Boris
sumber
1
@TodorTodorov Itu terjadi untuk saya
Igor Rodriguez
@TodorTodorov dan untuk saya!
Berlari
3
Bekerja untuk saya juga. Saya memiliki @ControllerAdvicedengan handleXyExceptionmetode di dalamnya, yang mengembalikan objek saya sendiri alih-alih ResponseEntity. Menambahkan @RestControllerdi atas @ControllerAdviceanotasi berfungsi dan masalah hilang.
Igor
36

Beginilah cara saya memecahkan masalah ini:

@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
sumber
1
Ini hanya untuk kasus pengujian. Bukan untuk pengontrol.
cst1992
2
Sedang membantu seseorang memecahkan masalah ini di salah satu pengujian unit baru mereka, inilah yang kami cari.
Bradford2000
Saya menggunakan ini, tetapi meskipun memberikan awalan dan akhiran yang salah untuk resolver saya dalam pengujian, itu berhasil. Bisakah Anda memberikan alasan di balik ini, mengapa ini diperlukan?
dushyantashu
jawaban ini harus dipilih sebagai yang paling benar dan spesifik
Caffeine Coder
20

Saya menggunakan Spring Boot untuk mencoba dan memuat halaman web, bukan menguji, dan mengalami masalah ini. Solusi saya sedikit berbeda dari yang di atas mengingat keadaan yang sedikit berbeda. (meskipun jawaban itu membuat saya mengerti.)

Saya hanya perlu mengubah ketergantungan starter Spring Boot saya di Maven dari:

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

untuk:

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

Hanya mengubah 'web' menjadi 'thymeleaf' telah memperbaiki masalah saya.

Sekolah Tua
sumber
1
Bagi saya, web-pemula tidak perlu diubah, tetapi saya memiliki ketergantungan timeleaf dengan <scope> test </scope>. Saat saya menghapus cakupan "test", itu berhasil. Terima kasih atas petunjuknya!
Georgina Diaz
16

Berikut ini perbaikan mudah jika Anda tidak benar-benar peduli tentang rendering tampilan.

Buat subclass InternalResourceViewResolver yang tidak memeriksa jalur tampilan melingkar:

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

Kemudian siapkan pengujian Anda dengannya:

MockMvc mockMvc;

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

    mockMvc =
            MockMvcBuilders.standaloneSetup(controller)
                    .setViewResolvers(new StandaloneMvcTestViewResolver())
                    .build();
}
Dave Bower
sumber
Ini memperbaiki masalah saya. Saya baru saja menambahkan kelas StandaloneMvcTestViewResolver di direktori pengujian yang sama dan menggunakannya di MockMvcBuilders seperti dijelaskan di atas. Terima kasih
Matheus Araujo
Saya memiliki masalah yang sama dan ini memperbaikinya untuk saya juga. Terima kasih banyak!
Johan
Ini adalah solusi hebat yang (1) tidak perlu mengubah pengontrol dan (2) dapat digunakan kembali di semua kelas pengujian dengan satu impor sederhana per kelas. +1
Nander Speerstra
Oldie tapi goldie! Menyelamatkan hariku. Terima kasih untuk solusi ini +1
Raistlin
13

Jika Anda menggunakan Spring Boot, tambahkan dependensi thymeleaf ke pom.xml Anda:

    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring4</artifactId>
        <version>2.1.6.RELEASE</version>
    </dependency>
Sarvar Nishonboev
sumber
1
Suara positif. Ketergantungan Thymeleaf yang hilang adalah penyebab kesalahan ini dalam proyek saya. Namun, jika Anda menggunakan Spring Boot, dependensinya akan terlihat seperti ini:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
peterh
8

Menambahkan /setelah /preferencememecahkan masalah untuk saya:

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

Dalam kasus saya, saya mencoba boot Kotlin + Spring dan saya mengalami masalah Circular View Path. Semua saran yang saya dapatkan secara online tidak dapat membantu, sampai saya mencoba yang di bawah ini:

Awalnya saya telah menjelaskan pengontrol saya menggunakan @Controller

import org.springframework.stereotype.Controller

Saya kemudian diganti @Controllerdengan@RestController

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

Dan itu berhasil.

johnmilimo
sumber
6

jika Anda belum pernah menggunakan @RequestBody dan hanya menggunakan @Controller, cara paling sederhana untuk memperbaiki ini menggunakan @RestControllerbukannya@Controller

MozenRath
sumber
ini tidak diperbaiki, sekarang ini akan menampilkan nama file Anda, alih-alih menampilkan templat
Ashish Kamble
1
itu tergantung pada masalah sebenarnya. kesalahan ini dapat terjadi karena berbagai alasan
MozenRath
4

Tambahkan anotasi @ResponseBodyke pengembalian metode Anda.

Ishaan Arora
sumber
Harap sertakan penjelasan tentang bagaimana dan mengapa hal ini menyelesaikan masalah akan sangat membantu meningkatkan kualitas posting Anda, dan mungkin menghasilkan lebih banyak suara.
Android
3

Saya menggunakan Spring Boot dengan Thymeleaf. Inilah yang berhasil bagi saya. Ada jawaban yang mirip dengan JSP tetapi perhatikan bahwa saya menggunakan HTML, bukan JSP, dan ini ada di folder src/main/resources/templatesseperti dalam proyek Spring Boot standar seperti yang dijelaskan di sini . Ini juga bisa menjadi kasus Anda.

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

Semoga ini membantu.

Pedro Lopez
sumber
3

Saat menjalankan Spring Boot + Freemarker jika halaman muncul:

Halaman Kesalahan Label Putih Aplikasi ini tidak memiliki pemetaan eksplisit untuk / error, jadi Anda melihatnya sebagai fallback.

Di spring-boot-starter-parent 2.2.1.RELEASE version freemarker tidak berfungsi:

  1. ganti nama file Freemarker dari .ftl menjadi .ftlh
  2. Tambahkan ke application.properties: spring.freemarker.expose-request-atribut = true

spring.freemarker.suffix = .ftl

Max
sumber
1
Cukup mengganti nama file Freemarker dari .ftl menjadi .ftlh telah memecahkan masalah bagi saya.
jannnik
Man ... Aku berhutang budi padamu. Saya kehilangan seluruh hari saya karena penggantian nama ini.
julianobrasil
2

Untuk Thymeleaf:

Saya baru saja mulai menggunakan spring 4 dan thymeleaf, ketika saya menemukan kesalahan ini, itu diatasi dengan menambahkan:

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

Saat menggunakan @Controlleranotasi, Anda membutuhkan @RequestMappingdan @ResponseBodyanotasi. Coba lagi setelah menambahkan anotasi@ResponseBody

Gowri Ayyanar
sumber
0

Saya menggunakan anotasi untuk mengonfigurasi aplikasi web musim semi, masalah diselesaikan dengan menambahkan InternalResourceViewResolverkacang ke konfigurasi. Semoga bermanfaat.

@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.dll
sumber
Terima kasih, ini berfungsi dengan baik untuk saya. Aplikasi saya rusak setelah memutakhirkan ke boot musim semi 1.3.1 dari 1.2.7 dan hanya baris ini yang gagal registri.addViewController ("/ login"). SetViewName ("login"); Saat mendaftarkan kacang itu, aplikasi bekerja lagi ... setidaknya login berjalan lancar.
le0diaz
0

Ini terjadi karena Spring menghapus "preferensi" dan menambahkan "preferensi" lagi dengan membuat jalur yang sama seperti Uri permintaan.

Terjadi seperti ini: request Uri: "/ preference"

hapus "preferensi": "/"

tambahkan jalur: "/" + "preferensi"

string akhir: "/ preferensi"

Ini memasuki loop yang Spring memberi tahu Anda dengan melemparkan pengecualian.

Sebaiknya berikan nama tampilan yang berbeda seperti "preferenceView" atau apa pun yang Anda suka.

xpioneer
sumber
0

coba tambahkan ketergantungan compile ("org.springframework.boot: spring-boot-starter-thymeleaf") ke file gradle Anda. Thymeleaf membantu memetakan tampilan.

aishwarya kore
sumber
0

Dalam kasus saya, saya mengalami masalah ini saat mencoba menyajikan halaman JSP menggunakan aplikasi Spring boot.

Inilah yang berhasil untuk saya:

application.properties

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

pom.xml

Untuk mengaktifkan dukungan untuk JSP, kita perlu menambahkan ketergantungan pada tomcat-embed-jasper.

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

Pendekatan sederhana lainnya:

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);
    }
}
Peramal Toothless
sumber