本文记录在基于Spring(Boot)框架(使用Java语言)和Grails框架(使用Groovy语言)下,开发Controller接口,对不存在的URL请求,接口返回404 not found,而不是抛出NoHandlerFoundException异常的问题,以及排查过程。

对于Spring (Boot)框架,请参考Spring 。

本文带着对Grails的极大恶意,谨慎下翻。

Grails

对于Grails框架,使用Groovy开发的Controller接口,Postman请求不存在的index1接口,给出如下响应信息:

Grails(Groovy)框架抛出NoHandlerFoundException而不是返回404 Not Found_java

切换到Preview:

Grails(Groovy)框架抛出NoHandlerFoundException而不是返回404 Not Found_mvc_02

经过分析,Postman上看到的preview页面实际上是下图中的notFound.gsp文件:

Grails(Groovy)框架抛出NoHandlerFoundException而不是返回404 Not Found_spring_03

notFound.gsp文件如下:

<!doctype html>
<html>
	<head>
		<title>Page Not Found</title>
		<meta name="layout" content="main">
		<g:if env="development"><asset:stylesheet src="errors.css"/></g:if>
	</head>
	<body>
		<ul class="errors">
			<li>Error: Page Not Found (404)</li>
			<li>Path: ${request.forwardURI}</li>
		</ul>
	</body>
</html>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

gsp文件就是Grails下的JSP页面,实际上是XML文件。

console打印日志:WARN [nio-8895-exec-5] o.s.web.servlet.PageNotFound : No mapping for GET /index1

Grails(Groovy)框架抛出NoHandlerFoundException而不是返回404 Not Found_spring_04

找不到这个类??

NoHandlerFoundException

添加配置:

spring:
  mvc:
    throw-exception-if-no-handler-found: true
  web: # 必须关闭静态资源的默认处理,否则 /health1 可能被静态资源处理器拦截或跳过
    resources:
      add-mappings: false # 谨慎使用,可能影响静态资源
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

报错:

Grails(Groovy)框架抛出NoHandlerFoundException而不是返回404 Not Found_java_05

Postman看到的还是上面的第二个图。

添加配置类:

package com.abcd

@Configuration
@EnableWebMvc
class NoHandlerConfig {
    @Bean
    ServletRegistrationBean<DispatcherServlet> dispatcherServlet() {
        DispatcherServlet dispatcher = new DispatcherServlet()
        dispatcher.setThrowExceptionIfNoHandlerFound(true)  // 关键设置

        ServletRegistrationBean<DispatcherServlet> registration = new ServletRegistrationBean<>(dispatcher, "/")
        registration.setName("dispatcherServlet")
        return registration
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

结果应用启动报错:

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
  • 1.

注释404映射:

Grails(Groovy)框架抛出NoHandlerFoundException而不是返回404 Not Found_spring_06

Grails(Groovy)框架抛出NoHandlerFoundException而不是返回404 Not Found_java_07

还是不行。

WTF??

一个很简单的技术需求,在Grails框架体系下实现起来怎么这么困难???

原理

Grails使用自己的UrlMappings路由系统,它基于AbstractController和动态调度。当访问/health1

  • Spring MVC层确实找不到Handler;
  • 但Grails的NotFoundController或默认的grails.web.mapping.filter.UrlMappingFilter拦截请求;
  • 最终返回404,根本不经过Spring的DispatcherServlet抛异常逻辑;
  • 所以:NoHandlerFoundException根本不会被抛出,无论你怎么配置throwExceptionIfNoHandlerFound。

经过各种折腾,终于有了一个将就的解决方法:
UrlMappings.groovy最后面添加如下配置:

"/*"(controller: 'notFound', action: 'handle')
  • 1.

再人工实现一个NotFoundController:

class NotFoundController {
    def handle() {
        throw new org.springframework.web.servlet.NoHandlerFoundException(
                request.method,
                request.forwardURI,
                new HttpHeaders()
        )
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

日志打印:

o.g.web.errors.GrailsExceptionResolver   : NoHandlerFoundException occurred when processing request: [GET] /health1
No handler found for GET /health1. Stacktrace follows: 
java.lang.reflect.InvocationTargetException: null
Caused by: org.springframework.web.servlet.NoHandlerFoundException: No handler found for GET /health1
  • 1.
  • 2.
  • 3.
  • 4.

Postman渲染HTML错误信息:

Grails(Groovy)框架抛出NoHandlerFoundException而不是返回404 Not Found_#开发语言_08

上面代码里写的明明是new HttpHeaders(),这里却变成String???事实上,这个参数类型变更的问题,我后来又遇到过一次。

这特么太搞笑了。

GlobalExceptionHandler

想要实现的效果是,GlobalExceptionHandler.groovy实现全局Controller接口接管。对于404 Not Found,使用ERROR级别来打印日志(忽视下面截图里的错误,实际上应该是this.logError(e, "请求路径不存在")):

Grails(Groovy)框架抛出NoHandlerFoundException而不是返回404 Not Found_mvc_09

UrlMappings.groovy

UrlMappings.groovy文件如下:

class UrlMappings {
    static mappings = {
        "/$controller/$action?/$id?(.$format)?" {
            constraints {
                // apply constraints here
            }
        }
        // 分组接口
        group "/doc", {
            "/"(controller: "document", method: "GET", action: "index") // 文档列表
            "/$doc_id"(controller: "document", method: "GET", action: "detail") // 文档详情
        }
		// 单独接口
        "/opts"(controller: "options", method: "GET", action: "index")
        // 省略其他若干
        "/"(view: "/index")
        "500"(view: '/error')
        "404"(view: '/notFound')
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

删除notFound.gsp文件(并没有注释UrlMappings文件里的404配置),请求/index1接口,报错:

javax.servlet.ServletException: Could not resolve view with name '/notFound' in servlet with name 'grailsDispatcherServlet'
at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1385) ~[spring-webmvc-5.3.33.jar:5.3.33]
at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1150) ~[spring-webmvc-5.3.33.jar:5.3.33]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-5.3.33.jar:5.3.33]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:965) ~[spring-webmvc-5.3.33.jar:5.3.33]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.33.jar:5.3.33]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.33.jar:5.3.33]

2024-12-31 14:00:57.501  --> ERROR [nio-8995-exec-4] o.a.c.c.C.[Tomcat].[localhost]           : Exception Processing ErrorPage[errorCode=404, location=/error]
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

PageNotFound

o.s.web.servlet.PageNotFound这个类到底是在哪个GAV里的?

这个类不存在,至少在spring-webmvc-5.x下面并不存在。

经过各种排查,

public class DispatcherServlet extends FrameworkServlet {

	public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound";

	protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY);

	protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
		if (pageNotFoundLogger.isWarnEnabled()) {
			pageNotFoundLogger.warn("No mapping for " + request.getMethod() + " " + getRequestUri(request));
		}
		if (this.throwExceptionIfNoHandlerFound) {
			throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request),
					new ServletServerHttpRequest(request).getHeaders());
		} else {
			response.sendError(HttpServletResponse.SC_NOT_FOUND);
		}
	}

	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		mappedHandler = getHandler(processedRequest);
		if (mappedHandler == null) {
			noHandlerFound(processedRequest, response);
			return;
		}
	}
	
	@Nullable
	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {
			for (HandlerMapping mapping : this.handlerMappings) {
				HandlerExecutionChain handler = mapping.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.

Grails(Groovy)框架抛出NoHandlerFoundException而不是返回404 Not Found_spring_10

当Spring框架的DispatcherServlet无法找到处理请求的处理器(Handler)时,它会返回null,从而导致404 Not Found错误。

resources.groovy

resources.groovy文件如下:

import com.aliyun.oss.OSSClient
import io.minio.MinioAsyncClient
import io.minio.MinioClient

// Place your Spring DSL code here
beans = {
    def grailsConfig = grailsApplication.config

    ossClient(OSSClient, grailsConfig.oss?.endpoint, grailsConfig.oss?.accessKeyId, grailsConfig.oss?.accessKeySecret)
    minioClient(MinioClient, MinioAsyncClient.builder()
            .endpoint(grailsConfig.minio?.endpoint as String)
            .credentials(grailsConfig.minio?.accessKey as String, grailsConfig.minio?.secretKey as String)
            .build())
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

增加:

import org.springframework.web.servlet.DispatcherServlet

beans = {
    dispatcherServlet(DispatcherServlet) {
        throwExceptionIfNoHandlerFound = true
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

结果报错:

Grails(Groovy)框架抛出NoHandlerFoundException而不是返回404 Not Found_#开发语言_11

console控制台打印异常日志:

ERROR [nio-8867-exec-5] o.g.web.errors.GrailsExceptionResolver   : NullPointerException occurred when processing request: [GET] /temp/all
Stacktrace follows: 
java.lang.NullPointerException: null
  at org.grails.web.mime.HttpServletResponseExtension.getMimeTypeForRequest(HttpServletResponseExtension.groovy:131) ~[grails-plugin-mimetypes-6.2.1.jar:6.2.1]
  at org.grails.web.mime.HttpServletResponseExtension.getMimeType(HttpServletResponseExtension.groovy:127) ~[grails-plugin-mimetypes-6.2.1.jar:6.2.1]
  at org.grails.web.mime.DefaultMimeTypeResolver.resolveResponseMimeType(DefaultMimeTypeResolver.groovy:41) ~[grails-plugin-mimetypes-6.2.1.jar:6.2.1]
  at org.grails.web.mapping.mvc.UrlMappingsHandlerMapping.findRequestedVersion(UrlMappingsHandlerMapping.groovy:184) ~[grails-web-url-mappings-6.2.1.jar:6.2.1]
  at org.grails.web.mapping.mvc.UrlMappingsHandlerMapping.getHandlerInternal(UrlMappingsHandlerMapping.groovy:132) ~[grails-web-url-mappings-6.2.1.jar:6.2.1]
  at org.springframework.web.servlet.handler.AbstractHandlerMapping.getHandler(AbstractHandlerMapping.java:499) ~[spring-webmvc-5.3.39.jar:5.3.39]
  at org.springframework.web.servlet.DispatcherServlet.getHandler(DispatcherServlet.java:1266) ~[spring-webmvc-5.3.39.jar:5.3.39]
  at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1048) ~[spring-webmvc-5.3.39.jar:5.3.39]

ERROR [nio-8867-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ClassCastException: class org.springframework.web.context.request.ServletRequestAttributes cannot be cast to class org.grails.web.servlet.mvc.GrailsWebRequest (org.springframework.web.context.request.ServletRequestAttributes and org.grails.web.servlet.mvc.GrailsWebRequest are in unnamed module of loader 'app')] with root cause 
java.lang.ClassCastException: class org.springframework.web.context.request.ServletRequestAttributes cannot be cast to class org.grails.web.servlet.mvc.GrailsWebRequest (org.springframework.web.context.request.ServletRequestAttributes and org.grails.web.servlet.mvc.GrailsWebRequest are in unnamed module of loader 'app')
  at org.grails.web.mapping.AbstractUrlMappingInfo.evaluateNameForValue(AbstractUrlMappingInfo.java:119) ~[grails-web-url-mappings-6.2.1.jar:6.2.1]
  at org.grails.web.mapping.DefaultUrlMappingInfo.getNamespace(DefaultUrlMappingInfo.java:185) ~[grails-web-url-mappings-6.2.1.jar:6.2.1]
  at org.grails.web.mapping.mvc.AbstractGrailsControllerUrlMappings.collectControllerMapping(AbstractGrailsControllerUrlMappings.groovy:206) ~[grails-web-url-mappings-6.2.1.jar:6.2.1]
  at org.grails.web.mapping.mvc.AbstractGrailsControllerUrlMappings.matchStatusCode(AbstractGrailsControllerUrlMappings.groovy:120) ~[grails-web-url-mappings-6.2.1.jar:6.2.1]

WARN [nio-8867-exec-5] c.z.security.config.AdviceConfiguration  : [Web][有Warn被抛出] >> Warn类=[java.lang.IllegalArgumentException], URI=[/error], 消息=[不合法的参数异常], Warn=[java.lang.IllegalArgumentException: HandlerMapping requires a Grails web request
  at org.springframework.util.Assert.notNull(Assert.java:201)
  at org.grails.web.mapping.mvc.UrlMappingsHandlerMapping.getHandlerInternal(UrlMappingsHandlerMapping.groovy:130)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

应用启动时,报错如上面的StackTrace所示,后续的接口请求,则报错StackTrace顶部的空指针:

ERROR [nio-8867-exec-9] c.z.security.config.AdviceConfiguration  : [Web][有异常被抛出] >> 异常类=[java.lang.NullPointerException], URI=[/temp/all], 消息=[null]
  • 1.

而且是所有的Controller接口都会报错NPE!!!!

NPE

臭名昭著的空指针!为啥会NPE??

Grails(Groovy)框架抛出NoHandlerFoundException而不是返回404 Not Found_spring_12

UrlMappingsHandlerMapping.groovy源码:

@CompileStatic
class UrlMappingsHandlerMapping extends AbstractHandlerMapping {
	protected String findRequestedVersion(GrailsWebRequest currentRequest) {
		String version = currentRequest.getHeader(HttpHeaders.ACCEPT_VERSION)
		if(!version && mimeTypeResolver) {
		    MimeType mimeType = mimeTypeResolver.resolveResponseMimeType(currentRequest)
		    version = mimeType.version
		}
		return version
	}
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

DefaultMimeTypeResolver.groovy源码:

@CompileStatic
class DefaultMimeTypeResolver implements MimeTypeResolver {
	@Override
	MimeType resolveResponseMimeType(GrailsWebRequest webRequest= GrailsWebRequest.lookup()) {
	    if (webRequest != null) {
	        return HttpServletResponseExtension.getMimeType(webRequest.response)
	    }
	    return null
	}
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

HttpServletResponseExtension.groovy源码:

@CompileStatic
class HttpServletResponseExtension {
	@CompileStatic
	static MimeType getMimeType(HttpServletResponse response) {
		final webRequest = GrailsWebRequest.lookup()
		return getMimeTypeForRequest(webRequest)
	}
	
	private static MimeType getMimeTypeForRequest(GrailsWebRequest webRequest) {
		HttpServletRequest request = webRequest.getCurrentRequest()
		MimeType result = (MimeType) request.getAttribute(GrailsApplicationAttributes.RESPONSE_MIME_TYPE)
		if (!result) {
			// 省略代码
		}
		return result
	}
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

WTF?webRequest是null的??

其他

网络上有很多对Grails的吐槽