7

I'm trying to handle 404 error using an @ControllerAdvice in a Spring MVC application totally configured using Javaconfig.

Spring MVC version is 4.1.5

I have read this:

But unfortunately it does not work for me.

Here you have my conf:

SpringConfigurationInitializer

public class SpringConfigurationInitializer extends
        AbstractAnnotationConfigDispatcherServletInitializer {



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

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return null;
    }

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

    @Override
    public void customizeRegistration(ServletRegistration.Dynamic registration) {
        registration.setInitParameter("throwExceptionIfNoHandlerFound", "true");
    }
}

Note that i'm using

registration.setInitParameter("throwExceptionIfNoHandlerFound", "true");

And

GlobalExceptionHandler (version 1)

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(NoHandlerFoundException.class)
    public ModelAndView handleError404(HttpServletRequest request, Exception e) {
        System.out.println("handled!!!");
        ModelAndView mav = new ModelAndView("/errors/404");
        mav.addObject("exception", e);
        return mav;
    }
}

GlobalExceptionHandler (version 2)

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    public ResponseEntity handleNoHandlerFoundException(NoHandlerFoundException ex,
            HttpHeaders headers, HttpStatus status, WebRequest request) {
        System.out.println("handled¡¡¡");
        return null;
    }
}

Keep in mind that i'm not using any kind of xml config file and i'm trying to build a web application (not REST)

AppConfiguration

@Configuration
@ComponentScan({ "org.moyanojv.opendata.*" })
@Import({ MvcConfiguration.class, RepositoryConfiguration.class, SecurityConfig.class })
public class AppConfiguration extends WebMvcConfigurerAdapter{

}

MvcConfiguration

@EnableWebMvc
@Configuration
public class MvcConfiguration extends WebMvcConfigurerAdapter {

    @Bean
    public UrlBasedViewResolver viewResolver() {
        UrlBasedViewResolver viewResolver = new UrlBasedViewResolver();
        viewResolver.setViewClass(TilesView.class);
        return viewResolver;
    }

    @Bean
    public TilesConfigurer tilesConfigurer() {
        TilesConfigurer tilesConfigurer = new TilesConfigurer();
        tilesConfigurer.setDefinitions(new String[] { "/WEB-INF/tiles.xml" });
        tilesConfigurer.setCheckRefresh(true);
        return tilesConfigurer;
    }



    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
         registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
    }

    @Override
    public void configureDefaultServletHandling(
            DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }


    /* Localization section is started */

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor(){
        LocaleChangeInterceptor localeChangeInterceptor=new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        return localeChangeInterceptor;
    }

    @Bean(name = "localeResolver")
    public LocaleResolver getLocaleResolver(){
        return new CookieLocaleResolver();
    }

    @Bean
    public ResourceBundleMessageSource messageSource() {
        ResourceBundleMessageSource source = new ResourceBundleMessageSource();
        source.setBasename("i18n/messages");
        source.setUseCodeAsDefaultMessage(true);
        return source;
    }
}

RepositoryConfiguration

@Configuration
public class RepositoryConfiguration {

}

SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth)
            throws Exception {
        auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN");
        auth.inMemoryAuthentication().withUser("user").password("user").roles("USER");
    }

    @Override
    public void configure( WebSecurity web ) throws Exception
    {
        // This is here to ensure that the static content (JavaScript, CSS, etc)
        // is accessible from the login page without authentication
        web
            .ignoring()
                .antMatchers( "/resources/**" );
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        // access-denied-page: this is the page users will be
        // redirected to when they try to access protected areas.
        .exceptionHandling()
            .accessDeniedPage( "/403" )
            .and()
        // The intercept-url configuration is where we specify what roles are allowed access to what areas.
        // We specifically force the connection to https for all the pages, although it could be sufficient
        // just on the login page. The access parameter is where the expressions are used to control which
        // roles can access specific areas. One of the most important things is the order of the intercept-urls,
        // the most catch-all type patterns should at the bottom of the list as the matches are executed
        // in the order they are configured below. So /** (anyRequest()) should always be at the bottom of the list.
        .authorizeRequests()
            .antMatchers( "/admin" ).hasRole("ADMIN")
            .antMatchers("/login**").permitAll()
            .antMatchers("/home").permitAll()
            .antMatchers("/404").permitAll()
            .anyRequest().authenticated()
            .and()
        // This is where we configure our login form.
        // login-page: the page that contains the login screen
        // login-processing-url: this is the URL to which the login form should be submitted
        // default-target-url: the URL to which the user will be redirected if they login successfully
        // authentication-failure-url: the URL to which the user will be redirected if they fail login
        // username-parameter: the name of the request parameter which contains the username
        // password-parameter: the name of the request parameter which contains the password
        .formLogin()
            .loginPage( "/login" )
            .failureUrl( "/login?err=1" )
            .defaultSuccessUrl("/private")
            .usernameParameter( "username" )
            .passwordParameter( "password" )
            .permitAll()
            .and()
        // This is where the logout page and process is configured. The logout-url is the URL to send
        // the user to in order to logout, the logout-success-url is where they are taken if the logout
        // is successful, and the delete-cookies and invalidate-session make sure that we clean up after logout
        .logout()
            .logoutUrl("/logout")
            .logoutSuccessUrl("/login?logout=1")
            .invalidateHttpSession(true)
            //.deleteCookies("JSESSIONID,SPRING_SECURITY_REMEMBER_ME_COOKIE")
            .and()
        .csrf()
            .disable()
        // The session management is used to ensure the user only has one session. This isn't
        // compulsory but can add some extra security to your application.
        .sessionManagement()
            .maximumSessions(1);
    }

    @Override
    @Bean
    public UserDetailsService userDetailsServiceBean() throws Exception
    {
        return super.userDetailsServiceBean();
    }
}

SpringSecurityInitializer

public class SpringSecurityInitializer extends AbstractSecurityWebApplicationInitializer{
    //do nothing
}

With this config i'm not able to handle 404 error code.

Thanks in advance.

Updated to add more information about config files

Community
  • 1
  • 1
moyanojv
  • 81
  • 1
  • 3
  • And you never will be able to do so. An `@ExceptionHandler` will only work for exceptions occurring during the execution of a handler. This isn't the execution of a handler but fails before that. Instead use an `error-page` in web.xml (yes in web.xml as there is no java equivalent for that). – M. Deinum May 28 '15 at 12:49
  • In this case what is the porpouse of registration.setInitParameter("throwExceptionIfNoHandlerFound", "true");? – moyanojv May 28 '15 at 12:57
  • By default it sends a 404 this lets it throw an exception which you could handle in a filter for instance. To register error pages you have to use a web.xml, as that isn't available in java configuration. (An oversight in the 3.x release of the servlet API I suspect). – M. Deinum May 28 '15 at 13:06
  • What about? http://stackoverflow.com/questions/18322279/spring-mvc-spring-security-and-error-handling or http://stackoverflow.com/questions/13356549/handle-error-404-with-spring-controller?lq=1 This code is not working for me. – moyanojv May 28 '15 at 13:08
  • Oki. Thanks @M. Deinum – moyanojv May 28 '15 at 13:11
  • Hmm. Just looked at the code for the `DispatcherServlet` and it could work (So I was wrong :) ). Is your `GlobalExceptionHandler` registered in the context that is being loaded by the `DispatcherServlet`? And do you have `@EnableWebMvc` in your configuration... – M. Deinum May 28 '15 at 13:15
  • @EnableWebMvc is used in my MvcConfiguration (extends WebMvcConfigurerAdapter). Regarding if my GlobalExceptionHandler is registered i'm not sure. I will add more details to the main question. – moyanojv May 28 '15 at 14:23
  • GlobalExceptionHandler is in the same package as my other controllers so i think it is correctly registered. – moyanojv May 28 '15 at 14:42
  • But not in the context loaded by the `DispatcherServlet`. Remove `MvcConfiguration` from the import on the `AppConfiguration` class and let that be loaded by the `DispatcherServlet`. Return this class from `getServletConfigClasses`. – M. Deinum May 29 '15 at 05:32
  • Many thanks for your help, but sadly this changes don't solve the problem. – moyanojv May 29 '15 at 07:56
  • You don't have the `@ExceptionHandler` annotation on the method... – M. Deinum May 29 '15 at 08:15
  • It was my fault. In my code the method is correctly annotated. Still not works (I have modified the question (GlobalExceptionHandler (version 1)) – moyanojv May 29 '15 at 08:43
  • Which one are you using as currently your question is confusing rather than providing the right amount of information. Enable debug/trace logging and see what is happening. Also are you sure the exception is thrown? This will only work for requests that are actually handled by the `DispatcherServlet`, as you have a enabled the `DefaultServletHandlerConfigurer` this means everything that cannot be handled by the `DispatcherServlet` is passed on to the default servlet, so I would say everything is always handled and the exception isn't occurring... – M. Deinum May 29 '15 at 08:53
  • Right now all my config is in the question. Regarding the "problem": If i try to load a page without controller, for example /bla, i can't see the system.out message and of course i'm not getting the correct jsp page defined in the GlobalExceptionHandler method handleError404. Thanks for your help. – moyanojv May 29 '15 at 10:21
  • If all else fails, you could use a similar approach they use in Spring-Boot - They have just developed a Filter class that handles all exceptions/non-2XX response codes and supports mapping to an error controller (basically replicating the errorPage type support you would have in a web.xml) - You can see their implementation here: https://github.com/spring-projects/spring-boot/blob/master/spring-boot/src/main/java/org/springframework/boot/context/web/ErrorPageFilter.java – rhinds May 29 '15 at 13:14
  • @moyanojv: Did you find a solution for your problem? I have pretty much the same problem and tried all kind of suggestions, but haven't managed to display a personal 404 nohandlerfound page. – user1583209 Mar 14 '17 at 14:18
  • @M.Deinum: Does a solution exist if I have enabled the 'DefaultServletHandlerConfigurer' but still I want to catch the 404? – Jannik Mar 16 '18 at 10:43

3 Answers3

11

Conclusion seems to be that setting throwExceptionIfNoHandlerFound to true does not throw an exception when no handler is found.

The solution is quite simple. From the javadoc @ DispatcherServlet.setThrowExceptionIfNoHandlerFound. It states here that a NoHandlerFoundException will never be thrown if DefaultServletHttpRequestHandler is used.

Solution hence is to remove the line

   configurer.enable();

from your MvcConfiguration. The exception should fire now and your GlobalExceptionHandler should do the rest!

s.ijpma
  • 930
  • 1
  • 11
  • 23
2

Workaround: Add @RequestMapping("/**")

@Controller
@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @RequestMapping("/**")
    public String handlerNotMappingRequest(HttpServletRequest request, HttpServletResponse response, HttpHeaders httpHeaders)
            throws NoHandlerFoundException {
        throw new NoHandlerFoundException("No handler mapping found.", request.getRequestURL().toString(), httpHeaders);
    }

    @ExceptionHandler(Throwable.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ModelAndView handleControllerException(Throwable ex) {
        logger.error("ErrorLog: ", ex);
        return new ModelAndView("error/exception", "exceptionMsg", "ExceptionHandler msg: " + ex.toString());
    }

    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ModelAndView handleNoHandlerFoundException(NoHandlerFoundException ex) {
        logger.error("ErrorLog: ", ex);
        return new ModelAndView("error/exception", "exceptionMsg", "NoHandlerFoundException msg: " + ex.toString());
    }
}
KhanhLV
  • 31
  • 2
0

The solution is to extend AbstractAnnotationConfigDispatcherServletInitializer and override this method:

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

OR this one:

@Override
public void customizeRegistration(ServletRegistration.Dynamic registration) {
    registration.setInitParameter("throwExceptionIfNoHandlerFound", "true");
}

And finally in your ControlerAdvice use this:

@ExceptionHandler(NoHandlerFoundException.class)
public String error404(Exception ex) {

    return new ModelAndView("404");
}
zygimantus
  • 3,649
  • 4
  • 39
  • 54