404 error redirect in Spring with Java config
Asked Answered
D

9

27

As you know, in XML, the way to configure this is:

<error-page>
    <error-code>404</error-code>
    <location>/my-custom-page-not-found.html</location>
</error-page>

But I haven't found a way to do it in Java config. The first way I tried was:

@RequestMapping(value = "/**")
public String Error(){
    return "error";
}

And it appeared to work, but it has conflicts retrieving the resources.

Is there a way to do it?

Divorce answered 9/5, 2014 at 22:24 Comment(4)
What are you trying to do with that handler method?Bronwynbronx
I'm trying to redirect to a custom error page if the user enter an invalid urlContiguity
Request handlers are meant to handle requests. /** matches all URIs.Bronwynbronx
Also check my post here for a proper error handling:stackoverflow.com/questions/41607802/…Ague
B
25

In Spring Framework, there are number of ways of handing exceptions (and particularly 404 error). Here is a documentation link.

  • First, you can still use error-page tag in web.xml, and customize error page. Here is an example.
  • Second, you can use one @ExceptionHandler for all controllers, like this:

    @ControllerAdvice
    public class ControllerAdvisor {
    
         @ExceptionHandler(NoHandlerFoundException.class)
         public String handle(Exception ex) {
    
            return "404";//this is view name
        }
    }
    

    For this to work, set throwExceptionIfNoHandlerFound property to true for DispatcherServlet in web.xml:

    <init-param>
        <param-name>throwExceptionIfNoHandlerFound</param-name>
        <param-value>true</param-value>
    </init-param>
    

    You can also pass some objects to error view, see javadoc for this.

Brownnose answered 10/5, 2014 at 0:37 Comment(8)
I'm using java config, no xml. How can I add the throwExceptionIfNoHandlerFound param?Contiguity
You are using application descriptor (web.xml) anyway.Brownnose
In web.xml, you declare spring org.springframework.web.servlet.DispatcherServlet as standart servlet. Add init parameter to this servlet, as I described above.Brownnose
this still involves xml configuration... -1Hooey
@Hooey there are people that don't trust in annotation configuration and have to rely on XML config instead...Dunham
@Luiggi the OP was asking specifically for java config based solutionHooey
@Hooey and Eugene shows how to use @ControllerAdvice which is annotation configuration. The XML configuration is for web.xml file, which you use when working on web environments and when you don't use Spring Boot or something similar.Dunham
@Luiggi: extending WebApplicationInitializer instead of web.xmlEhudd
D
14

The most clean solution since spring 4.2 RC3 is using the new createDispatcherServlet hook within the class extending AbstractDispatcherServletInitializer (or indirectly through extending AbstractAnnotationConfigDispatcherServletInitializer) like this:

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    }

    /* ... */

    @Override
    protected DispatcherServlet createDispatcherServlet(WebApplicationContext servletAppContext) {
        final DispatcherServlet dispatcherServlet = super.createDispatcherServlet(servletAppContext);
        dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);
        return dispatcherServlet;
    }
}

Then you can use a global @ControllerAdvice (a class that is annotated with @ControllerAdvice) as described in the reference docs. Within the advice you can handle the NoHandlerFoundException with an @ExceptionHandler as described here.

This could look something like this:

@ControllerAdvice
public class NoHandlerFoundControllerAdvice {

    @ExceptionHandler(NoHandlerFoundException.class)
    public ResponseEntity<String> handleNoHandlerFoundException(NoHandlerFoundException ex) {
        // prepare responseEntity
        return responseEntity;
    }

}
Decuple answered 15/7, 2015 at 16:56 Comment(3)
This looks to be the best current practice. One minor note: as of Spring 4.2.3, AbstractDispatcherServletInitializer.createDispatcherServlet returns a FrameworkServlet, so an additional cast to DispatcherServlet is required. Some additional considerations to implementing this approach may be found in this answer.Price
Can getServletConfigClasses also be used in WebAppInitializer?Eury
Yes it can. It is done in the full code of my answer above, but stripped away for brevity.Decuple
P
6

Simple answer for 100% free xml:

  1. Set properties for DispatcherServlet

    public class SpringMvcInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    
        @Override
        protected Class<?>[] getRootConfigClasses() {
            return new Class[] { RootConfig.class  };
        }
    
        @Override
        protected Class<?>[] getServletConfigClasses() {
            return new Class[] {AppConfig.class  };
        }
    
        @Override
        protected String[] getServletMappings() {
            return new String[] { "/" };
        }
    
        @Override
        protected void customizeRegistration(ServletRegistration.Dynamic registration) {
            boolean done = registration.setInitParameter("throwExceptionIfNoHandlerFound", "true"); // -> true
            if(!done) throw new RuntimeException();
        }
    
    }
    
  2. Create @ControllerAdvice:

    @ControllerAdvice
    public class AdviceController {
    
        @ExceptionHandler(NoHandlerFoundException.class)
        public String handle(Exception ex) {
            return "redirect:/404";
        }
    
        @RequestMapping(value = {"/404"}, method = RequestMethod.GET)
        public String NotFoudPage() {
            return "404";
    
        }
    }
    
Pathoneurosis answered 24/1, 2016 at 7:54 Comment(3)
This didn't work for me, my code never go in customizeRegistration and fail with ambigous error messageEpsilon
Best answer on this question, @Carlos Lopèz please validate itIntransigent
Where the RootConfig.class came from?Accommodating
W
5

Use code-based Servlet container initialization as described in the doc and override registerDispatcherServlet method to set throwExceptionIfNoHandlerFound property to true:

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[] { WebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }

    @Override
    protected void registerDispatcherServlet(ServletContext servletContext) {
        String servletName = getServletName();
        Assert.hasLength(servletName, "getServletName() may not return empty or null");

        WebApplicationContext servletAppContext = createServletApplicationContext();
        Assert.notNull(servletAppContext,
            "createServletApplicationContext() did not return an application " +
                    "context for servlet [" + servletName + "]");

        DispatcherServlet dispatcherServlet = new DispatcherServlet(servletAppContext);

        // throw NoHandlerFoundException to Controller
        dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);

        ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
        Assert.notNull(registration,
            "Failed to register servlet with name '" + servletName + "'." +
                    "Check if there is another servlet registered under the same name.");

        registration.setLoadOnStartup(1);
        registration.addMapping(getServletMappings());
        registration.setAsyncSupported(isAsyncSupported());

        Filter[] filters = getServletFilters();
        if (!ObjectUtils.isEmpty(filters)) {
            for (Filter filter : filters) {
                registerServletFilter(servletContext, filter);
            }
        }

        customizeRegistration(registration);
    }
}    

Then create an exception handler:

@ControllerAdvice
public class ExceptionHandlerController {
    @ExceptionHandler(Exception.class)
    public String handleException(Exception e) {
        return "404";// view name for 404 error
    }   
}

Don't forget about using @EnableWebMvc annotation on your Spring configuration file:

@Configuration
@EnableWebMvc
@ComponentScan(basePackages= {"org.project.etc"})
public class WebConfig extends WebMvcConfigurerAdapter {
    ...
}
Witty answered 1/7, 2014 at 11:49 Comment(1)
Is there a reason you chose this method to override rather than the customizeRegistration method? It seems like a lot more stuff needs to be there than just @Override protected void customizeRegistration(ServletRegistration.Dynamic registration){ registration.setInitParameter("throwExceptionIfNoHandlerFound","true"); }Gayle
V
5

In your web configuration class,

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter 

Declare a bean as follows,

@Bean
public EmbeddedServletContainerCustomizer containerCustomizer() {

  return new EmbeddedServletContainerCustomizer() {
    @Override
    public void customize(ConfigurableEmbeddedServletContainer container)       
    {
      ErrorPage error401Page = new ErrorPage(HttpStatus.UNAUTHORIZED, "/401.html");
      ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/404.html");
      ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500.html");

      container.addErrorPages(error401Page,error404Page,error500Page);
    }
  };
}

Add the mentioned html files(401.html .etc) to /src/main/resources/static/ folder.

Hope this helps

Virulent answered 19/8, 2016 at 4:31 Comment(4)
Is this only for Spring Boot? I'd love for this to work otherwise since you could also specify the non-404 error pages.Ehudd
It's for spring mvc. Doesn't matter whether it is spring boot or not... same configuration. You can add error pages for other http error codes as well.Virulent
Perhaps I'm missing something, but EmbeddedServletContainerCustomizer, ConfigurableEmbeddedServletContainer, and ErrorPage are not found in Spring Framework 4: docs.spring.io/spring/docs/current/javadoc-apiEhudd
Just to clarify, EmbeddedServletContainerCustomizer is part of Spring Boot, so this will only work if you're using Spring Boot.Polarization
I
3

For Java config there is a method setThrowExceptionIfNoHandlerFound(boolean throwExceptionIfNoHandlerFound) in DispatcherServlet. By settting it to true I guess you are doing same thing

<init-param>
    <param-name>throwExceptionIfNoHandlerFound</param-name>
    <param-value>true</param-value>
</init-param>

then you can can this NoHandlerFoundException.class in controller advice as stated in above answer

it will be like something

public class WebXml implements WebApplicationInitializer{

    public void onStartup(ServletContext servletContext) throws ServletException {
        WebApplicationContext context = getContext();
        servletContext.addListener(new ContextLoaderListener(context));


        DispatcherServlet dp =  new DispatcherServlet(context);
        dp.setThrowExceptionIfNoHandlerFound(true);

        ServletRegistration.Dynamic dispatcher = servletContext.addServlet("DispatcherServlet", dp);
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping(MAPPING_URL);
    }
}
Idona answered 1/11, 2014 at 10:40 Comment(0)
H
1

The solution proposed in comments above really works:

@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration)
{
  registration.setInitParameter("throwExceptionIfNoHandlerFound", "true");
}
Hooey answered 9/1, 2016 at 20:29 Comment(0)
D
0

A solution for Spring 5 and Thymeleaf 3.

In MyWebInitializer, enable exception throwing with setThrowExceptionIfNoHandlerFound(). We need to do casting to DispatcherServlet.

@Configuration
public class MyWebInitializer extends
        AbstractAnnotationConfigDispatcherServletInitializer {

        ...

        @Override
        protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) {
            var dispatcher = (DispatcherServlet) super.createDispatcherServlet(servletAppContext);
            dispatcher.setThrowExceptionIfNoHandlerFound(true);
            return dispatcher;
        }
    }

Create a controller advice with @ControllerAdvice and add error message to the ModealAndView.

@ControllerAdvice
public class ControllerAdvisor {

    @ExceptionHandler(NoHandlerFoundException.class)
    public ModelAndView handle(Exception ex) {

        var mv = new ModelAndView();
        mv.addObject("message", ex.getMessage());
        mv.setViewName("error/404");

        return mv;
    }
}

Create 404 error template, which displays the error message. Based on my configuration, the file is src/main/resources/templates/error/404.html.

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <title>Resource not found</title>
</head>
<body>

<h2>404 - resource not found</h2>

<p>
    <span th:text="${message}" th:remove="tag"></span>
</p>

</body>
</html>

For completeness, I add the Thymeleaf resolver configuration. We configure the Thymeleaf templates to be in templates directory on the classpath.

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.zetcode"})
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private ApplicationContext applicationContext;

    ...

    @Bean
    public SpringResourceTemplateResolver templateResolver() {

        var templateResolver = new SpringResourceTemplateResolver();

        templateResolver.setApplicationContext(applicationContext);
        templateResolver.setPrefix("classpath:/templates/");
        templateResolver.setSuffix(".html");

        return templateResolver;
    }
    ...
}
Drunken answered 17/1, 2019 at 15:18 Comment(0)
E
-1

In springboot it is even simplier. Because of Spring autoconfiguration stuff, spring creates a bean org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties. This class is annotated with @ConfigurationProperties(prefix = "spring.mvc") and therefore it will seek for properties with spring.mvc prefix.

Part from javadoc:

Annotation for externalized configuration. Add this to a class definition or a
* @Bean method in a @Configuration class if you want to bind and validate
* some external Properties (e.g. from a .properties file).

You just have to add to your i.e. application.properties file following properties:

 spring.mvc.throwExceptionIfNoHandlerFound=true
 spring.resources.add-mappings=false //this is for spring so it won't return default handler for resources that not exist

and add exception resolver as follows:

@ControllerAdvice
public class ExceptionResponseStatusHandler {
    @ExceptionHandler(NoHandlerFoundException.class)
    public ModelAndView handle404() {
        var out = new ModelAndView();
        out.setViewName("404");//you must have view named i.e. 404.html
        return out;
    }
}
Enterovirus answered 30/12, 2019 at 16:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.