javaspring-mvc

(Not a) Bug in spring framework forward


I believe I've uncovered a bug in spring framework with the forward: notation. This is happening with XML based spring applications.

I found this while upgrading my existing spring framework application from Spring 3 to Spring 6. I have created a project to reproduce.

Starting with build.gradle

plugins {
    id 'java'
    id 'war'  // Add war plugin for web application
}

group = 'com.avenuinsights'
version = '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    def springVersion = '6.2.7'
    implementation "org.springframework:spring-webmvc:${springVersion}"
    implementation "org.springframework:spring-context:${springVersion}"
    implementation group: 'jakarta.servlet', name: 'jakarta.servlet-api', version: '6.1.0'
    implementation group: 'jakarta.servlet.jsp', name: 'jakarta.servlet.jsp-api', version: '3.1.1'
    implementation group: 'jakarta.servlet.jsp.jstl', name: 'jakarta.servlet.jsp.jstl-api', version: '3.0.2'
    implementation group: 'org.glassfish.web', name: 'jakarta.servlet.jsp.jstl', version: '3.0.1'
    testImplementation platform('org.junit:junit-bom:5.10.0')
    testImplementation 'org.junit.jupiter:junit-jupiter'
}

test {
    useJUnitPlatform()
}

The web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!-- Configure the Spring DispatcherServlet -->
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!-- Map all requests to the DispatcherServlet -->
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>

and the spring-mvc.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!-- Enable Spring MVC features -->
    <mvc:annotation-driven />

    <!-- Configure controllers as beans -->
    <bean id="firstController" class="org.example.forwardxml.controller.FirstController" />
    <bean id="secondController" class="org.example.forwardxml.controller.SecondController" />
    <bean id="thirdController" class="org.example.forwardxml.controller.ThirdController" />

    <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <value>
                /first=firstController
                /second=secondController
                /third=thirdController
            </value>
        </property>
    </bean>

    <!-- Define the View Resolver -->
    <bean class="org.example.forwardxml.resolver.TranslatingViewResolver"/>

    <bean id="urlViewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
        <property name="viewClass" value="org.springframework.web.servlet.view.InternalResourceView"/>
    </bean>

</beans>

FirstController.java

public class FirstController extends AbstractController {

    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("FirstController: Processing request...");
        // Forward to the second controller
        return new ModelAndView("second.one");
    }
}

SecondController.java

public class SecondController extends AbstractController {

    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("SecondController: Processing request...");
        // Forward to the third controller
        return new ModelAndView("third.one");
    }
}

and ThirdController.java

public class ThirdController extends AbstractController {

    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("ThirdController: Processing request...");

        // Create a model and view to display "Hello, World!"
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("hello.view");
        modelAndView.addObject("message", "Hello, World!");

        return modelAndView;
    }
}

and finally the TranslatingViewResolver.java

public class TranslatingViewResolver extends AbstractCachingViewResolver {
    public static final String HELLO = "hello.view";
    public static final String SECOND = "second.one";
    public static final String THIRD = "third.one";

    @Override
    protected View loadView(String viewName, Locale locale) throws Exception {
        if (SECOND.equals(viewName)) {
            // Forward to the second controller
            return new InternalResourceView("forward:/second");
        } else if (THIRD.equals(viewName)) {
            // Forward to the third controller
            return new InternalResourceView("forward:/third");
        } else if (HELLO.equals(viewName)) {
            // Forward to the hello JSP
            return new JstlView("/WEB-INF/views/hello.jsp");
        }
        return null;
    }
}

Now, I believe that what should happen is that I should go to

localhost:8080/forwardxml/first

And it should return the jsp saying "Hello, World!" What I'm getting is:

enter image description here

If I've made a mistake, please let me know, but this looks like a bug according to the documentation. If you recreate and think this is an issue, please comment as well.


Solution

  • The forward: prefix is a magic prefix for the UrlBasedViewResolver (and subclasses) which will then turn it into an InternalResourceView with the prefix removed. The default for the InternalResourceView is to forward anyway.

    There is no bug related to this in Spring MVC, the problem is in your TranslatingViewResolver. Instead of new InternalResourceView("forward:/second") you should do new InternalResourceView("/second"). As you include the forward: prefix in the name of the view, Spring will then try to resolve something that listens for forward:/second instead of `/second. It fails hence a 404.

    This also has nothing to do with XML config or a bug in Spring, the same would happen with Java config and annotation based controllers etc.