本文記錄在基于Spring(Boot)框架(使用Java語言)和Grails框架(使用Groovy語言)下,開發Controller接口,對不存在的URL請求,接口返回404 not found,而不是拋出NoHandlerFoundException異常的問題,以及排查過程。
對于Spring (Boot)框架,請參考Spring 。
本文帶著對Grails的極大惡意,謹慎下翻。
Grails
對于Grails框架,使用Groovy開發的Controller接口,Postman請求不存在的index1
接口,給出如下響應信息:
切換到Preview:
經過分析,Postman上看到的preview頁面實際上是下圖中的notFound.gsp
文件:
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>
gsp文件就是Grails下的JSP頁面,實際上是XML文件。
console打印日志:WARN [nio-8895-exec-5] o.s.web.servlet.PageNotFound : No mapping for GET /index1
找不到這個類??
NoHandlerFoundException
添加配置:
spring:mvc:throw-exception-if-no-handler-found: trueweb: # 必須關閉靜態資源的默認處理,否則 /health1 可能被靜態資源處理器攔截或跳過resources:add-mappings: false # 謹慎使用,可能影響靜態資源
報錯:
Postman看到的還是上面的第二個圖。
添加配置類:
package com.abcd@Configuration
@EnableWebMvc
class NoHandlerConfig {@BeanServletRegistrationBean<DispatcherServlet> dispatcherServlet() {DispatcherServlet dispatcher = new DispatcherServlet()dispatcher.setThrowExceptionIfNoHandlerFound(true) // 關鍵設置ServletRegistrationBean<DispatcherServlet> registration = new ServletRegistrationBean<>(dispatcher, "/")registration.setName("dispatcherServlet")return registration}
}
結果應用啟動報錯:
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: {}
注釋404映射:
還是不行。
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')
再人工實現一個NotFoundController:
class NotFoundController {def handle() {throw new org.springframework.web.servlet.NoHandlerFoundException(request.method,request.forwardURI,new HttpHeaders())}
}
日志打印:
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
Postman渲染HTML錯誤信息:
上面代碼里寫的明明是new HttpHeaders()
,這里卻變成String???事實上,這個參數類型變更的問題,我后來又遇到過一次。
這特么太搞笑了。
GlobalExceptionHandler
想要實現的效果是,GlobalExceptionHandler.groovy
實現全局Controller接口接管。對于404 Not Found,使用ERROR級別來打印日志(忽視下面截圖里的錯誤,實際上應該是this.logError(e, "請求路徑不存在")
):
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')}
}
刪除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]
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;}}@Nullableprotected 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;}
}
當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.configossClient(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())
}
增加:
import org.springframework.web.servlet.DispatcherServletbeans = {dispatcherServlet(DispatcherServlet) {throwExceptionIfNoHandlerFound = true}
}
結果報錯:
console控制臺打印異常日志:
ERROR [nio-8867-exec-5] o.g.web.errors.GrailsExceptionResolver : NullPointerException occurred when processing request: [GET] /temp/all
Stacktrace follows:
java.lang.NullPointerException: nullat 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 requestat org.springframework.util.Assert.notNull(Assert.java:201)at org.grails.web.mapping.mvc.UrlMappingsHandlerMapping.getHandlerInternal(UrlMappingsHandlerMapping.groovy:130)
應用啟動時,報錯如上面的StackTrace所示,后續的接口請求,則報錯StackTrace頂部的空指針:
ERROR [nio-8867-exec-9] c.z.security.config.AdviceConfiguration : [Web][有異常被拋出] >> 異常類=[java.lang.NullPointerException], URI=[/temp/all], 消息=[null]
而且是所有的Controller接口都會報錯NPE!!!!
NPE
臭名昭著的空指針!為啥會NPE??
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}
}
DefaultMimeTypeResolver.groovy
源碼:
@CompileStatic
class DefaultMimeTypeResolver implements MimeTypeResolver {@OverrideMimeType resolveResponseMimeType(GrailsWebRequest webRequest= GrailsWebRequest.lookup()) {if (webRequest != null) {return HttpServletResponseExtension.getMimeType(webRequest.response)}return null}
}
HttpServletResponseExtension.groovy
源碼:
@CompileStatic
class HttpServletResponseExtension {@CompileStaticstatic 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}
}
WTF?webRequest是null的??
其他
網絡上有很多對Grails的吐槽
- stackoverflow-1