Spring MVCテストで「循環ビューパス」例外を回避する方法


117

コントローラの1つに次のコードがあります。

@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それが失敗したときにあなたが使用するものを投稿できます か?
Sotirios Delimanolis 2013

@SotiriosDelimanolis:Spring MVCテストでviewResolverが使用されているかどうかはわかりません。ドキュメント
balteo

8
私は同じ問題に直面していましたが、問題は依存関係の下に追加していないことでした。<dependency> <groupId> org.springframework.boot </ groupId> <artifactId> spring-boot-starter-thymeleaf </ artifactId> </ dependency>
aamir

@RestController代わりに使用@Controller
MozenRath

回答:


65

これは、Spring MVCテストとは関係ありません。

を宣言しない場合ViewResolver、Spring InternalResourceViewResolverJstlViewをレンダリングするためののインスタンスを作成するデフォルトを登録しますView

JstlViewクラスが拡張InternalResourceViewされています

同じWebアプリケーション内のJSPまたはその他のリソースのラッパー。モデルオブジェクトを要求属性として公開し、javax.servlet.RequestDispatcherを使用して、指定されたリソースURLに要求を転送します。

このビューのURLは、RequestDispatcherのforwardまたはincludeメソッドに適した、Webアプリケーション内のリソースを指定することになっています。

太字は私のものです。つまり、ビューはレンダリングの前に、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と、同じサーブレットが(前のものを処理したため)要求を処理し、無限ループに入ることがわかります。


特定のand でa ThymeleafViewResolverとa ServletContextTemplateResolverを宣言すると、ビルド方法が異なり、次のようなパスが与えられますprefixsuffixView

WEB-INF/web-templates/preference.html

ThymeleafViewインスタンスはServletContextServletContextResourceResolver

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

それは最終的に

return servletContext.getResourceAsStream(resourceName);

これは、ServletContextパスに関連するリソースを取得します。次に、を使用しTemplateEngineてHTMLを生成できます。ここで無限ループが発生する可能性はありません。


1
詳しい返信ありがとうございます。Thymeleafを使用するとループが発生しない理由と、Thymeleafビューリゾルバーを使用しないとループが発生する理由がわかります。ただし、アプリをテストできるように構成を変更する方法はまだわかりません...
balteo

1
あなたが使用@balteo に対する相対ファイルとして解決されると、あなたが提供します。その解決を使用しない場合、Springは。でリソースを検索するデフォルトを使用します。このリソースはにすることができます。この場合、パスがにマップされているためです。ThymleafViewResolverViewprefixsuffixInternalResourceViewResolverRequestDispatcherServlet/preferenceDispatcherServlet
Sotirios Delimanolis 2013

2
@balteoアプリをテストするには、正しいを提供しますViewResolver。いずれかのThymeleafViewResolverあなたの質問のように、あなた自身が設定されInternalResourceViewResolverたり、あなたのコントローラに戻ってきているビュー名を変更します。
Sotirios Delimanolis 2013

ありがとう、ありがとう、ありがとう!内部リソースビューリゾルバーが「インクルード」ではなく転送を選択した理由を理解できませんでしたが、今の説明では、名前に「リソース」を使用することは少しあいまいなようです。この説明は素晴らしいです。
Chris Thompson

2
@ShirgillFarhanAnsari 戻り値の型(なし)を持つ@RequestMapping注釈付きハンドラーメソッドの戻り値は、Stringをビュー名として解釈するによって処理され、それを使用して、私の回答で説明するプロセスを実行します。を使用すると、Spring MVCは代わりにこれを使用して、代わりに文字列を直接HTTP応答に書き込みます。ビューの解像度はありません。String@ResponseBodyViewNameMethodReturnValueHandler@ResponseBodyRequestResponseBodyMethodProcessor
Sotirios Delimanolis 2017

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

2
春のREST WebサービスのためのJSONレスポンスを戻しながらこれは私の問題を解決し...
ジョー・

いいですね、produces = {"application / json"}を指定しない場合でも機能します。デフォルトでjsonを生成しますか?
ジェイ

74

@Controller@RestController

同じ問題があり、コントローラーにもと注釈が付けられていることに気付きました@Controller。それを置き換えることで@RestController問題は解決しました。これはSpring Web MVCからの説明です:

@RestControllerは、それ自体が@Controllerおよび@ResponseBodyでメタ注釈が付けられた合成注釈であり、すべてのメソッドがタイプレベルの@ResponseBody注釈を継承するコントローラーを示します。そのため、応答本文とビューの解決に直接書き込み、HTMLテンプレートでレンダリングします。


1
@TodorTodorovそれは私のためにした
Igor Rodriguez

@TodorTodorovと私のために!

3
私も働いた。私が持っていた@ControllerAdvicehandleXyExceptionResponseEntityするのではなく、自分自身のオブジェクトを返され、その中に方法。アノテーションの@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
新しい単体テストの1つでこの問題のトラブルシューティングを手伝っていましたが、これがまさに私たちが探していたものです。
Bradford2000

私はこれを使用しましたが、テストでリゾルバーに間違った接頭辞と接尾辞を指定したにもかかわらず、それは機能しました。これの理由を説明できますか、なぜこれが必要なのですか?
dushyantashu 2017

この回答は、最も正確で具体的なものに投票する必要があります
Caffeine Coder

20

Spring Bootを使用して、テストではなくWebページをロードしようとしていますが、この問題がありました。私の解決策は、わずかに異なる状況を考慮して、上記のものとは少し異なっていました。(これらの答えは私が理解するのに役立ちましたが)。

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

「web」を「thymeleaf」に変更するだけで問題が解決しました。


1
私にとっては、スターターウェブを変更する必要はありませんでしたが、<scope> test </ scope>でthymeleafの依存関係がありました。「test」スコープを削除したところ、うまくいきました。手がかりをありがとう!
Georgina Diaz

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で使用しました。ありがとう
Matheus Araujo 2017

私は同じ問題を抱えていたので、これで修正されました。どうもありがとう!
ヨハン

これは、(1)コントローラを変更する必要がなく、(2)クラスごとに1つの単純なインポートですべてのテストクラスで再利用できる優れたソリューションです。+1
Nander Speerstra

オールディーだがゴールディー!私の日を救った。この回避策をありがとう+1
Raistlin

13

Spring Bootを使用している場合は、thymeleaf依存関係をpom.xmlに追加します。

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

1
賛成票。Thymeleaf依存関係の欠如が、プロジェクトでこのエラーを引き起こした原因です。あなたは春のブートを使用しているffをしかし、その後、依存関係ではなく、次のようになります<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
peterh


8

私の場合、Kotlin + Springブートを試してみましたが、Circular View Pathの問題が発生しました。以下を試すまで、オンラインで得たすべての提案は役に立ちませんでした:

もともと私は自分のコントローラに注釈を付けていました @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

ThymeleafでSpring Bootを使用しています。これは私のために働いたものです。JSPにも同様の答えがありますが、私はJSPではなくHTMLを使用していることに注意してください。これらは、ここで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を実行しているときにページが表示される場合:

ホワイトラベルエラーページこのアプリケーションには/エラーの明示的なマッピングがないため、これはフォールバックと見なされます。

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

男...私はあなたにビールを借りています。この名前を変更したため、1日中迷っていました。
julianobrasil

2

Thymeleafの場合:

私は春4とthymeleafを使い始めたばかりですが、このエラーが発生したとき、次のように追加することで解決しました。

<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 Webアプリを構成InternalResourceViewResolverします。この問題は、構成にBeanを追加することで解決されます。お役に立てれば幸いです。

@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.2.7からスプリングブート1.3.1にアップグレードした後、アプリが壊れ、この行だけがregistry.addViewController( "/ login")。setViewName( "login");に失敗しました。そのBeanを登録すると、アプリは再び動作しました...少なくともログインは成功しました。
le0diaz 2015

0

これは、Springが「設定」を削除し、「設定」を再度追加して、リクエストURIと同じパスを作成するために発生します。

このように発生:リクエストUri: "/ preference"

"preference"を削除: "/"

パスを追加: "/" + "preference"

終了文字列: "/ preference"

これは、Springが例外をスローして通知するループに入ります。

「preferenceView」などの別のビュー名を付けることをお勧めします。


0

Gradleファイルにcompile( "org.springframework.boot:spring-boot-starter-thymeleaf")依存関係を追加してみてください。Thymeleafはビューのマッピングに役立ちます。


0

私の場合、Springブートアプリケーションを使用してJSPページを提供しようとしたときにこの問題が発生しました。

これが私のために働いたものです:

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>

-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);
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.