Grails集成springdoc swagger时出现Ambiguous mapping(模糊映射)错误
Grails集成springdoc swagger时出现Ambiguous mapping(模糊映射)错误
问题场景
你写的这个获取认证Token的接口逻辑很清晰,但启动时遇到的模糊映射错误确实让人头疼。先把你的代码和错误日志整理清楚,方便分析:
你的AuthenticationApiController代码
package example.infra.adapters.input.api.v1.endpoints import example.aplication.services.authentication.LoginAuthenticator import example.aplication.dtos.authentication.CredencialsDTO import example.aplication.dtos.authentication.AuthenticationTokenDTO import example.aplication.exceptions.authentication.AuthenticationErrorException import org.springframework.web.bind.annotation.* import org.springframework.http.ResponseEntity import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.ExampleObject import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.parameters.RequestBody @RestController @RequestMapping("/api/v1/authentication") @Tag(name = "Authentication", description = "User authentication") class AuthenticationApiController { LoginAuthenticator loginAuthenticator @PostMapping(value = "/token") @Operation( summary = "Get authentication token", requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( required = true, content = @Content( mediaType = "application/json", schema = @Schema(implementation = CredencialsDTO), examples = [ @ExampleObject( value = '{"login": "loginExample", "senha": "senhaExample"}' ) ] ) ), responses = [ @ApiResponse( responseCode = "200", description = "Success", content = @Content( mediaType = "application/json", schema = @Schema(implementation = AuthenticationTokenDTO), examples = [ @ExampleObject( value = '{"token": "abc123", "expiration": "2025-09-03T18:00:00Z"}' ) ] ) ), @ApiResponse( responseCode = "401", description = "Incorrect login or password", content = @Content() ) ] ) ResponseEntity<?> token(@org.springframework.web.bind.annotation.RequestBody CredencialsDTO credencials) { try { AuthenticationTokenDTO tokenDto = loginAuthenticator.authenticate(credencials) return JsonResponse.success(tokenDto) } catch (AuthenticationErrorException e) { return JsonResponse.unauthorized() } } }
启动时的错误日志
| Running application... Configuring Spring Security Core ... ... finished configuring Spring Security Core 2025-09-04 14:58:23.510 ERROR --- [ restartedMain] o.s.boot.SpringApplication : Application run failed org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'authenticationApiController' method example.infra.adapters.input.api.v1.endpoints.AuthenticationApiController#token(CredencialsDTO) to {POST [/api/v1/authentication/token]}: There is already 'authenticationApiController' bean method example.infra.adapters.input.api.v1.endpoints.AuthenticationApiController#token() mapped. at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1804) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:955) at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:731) at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) at grails.boot.GrailsApp.run(GrailsApp.groovy:99) at grails.boot.GrailsApp.run(GrailsApp.groovy:485) at grails.boot.GrailsApp.run(GrailsApp.groovy:472) at example.Application.main(Application.groovy:8)
问题分析
从错误日志能明确看到:Spring发现你的AuthenticationApiController里有两个token方法都映射到了POST /api/v1/authentication/token路径——一个是你写的带CredencialsDTO参数的token(CredencialsDTO),另一个是无参的token()。
为什么会凭空多出一个无参的token()方法?大概率是这几个原因:
- Groovy语法解析问题:你最初粘贴的代码没有正确换行,Groovy在编译时错误解析了代码结构,生成了额外的无参方法;
- Groovy自动方法生成特性:你用了字段注入
LoginAuthenticator loginAuthenticator(没有加private final),Groovy会自动生成getter/setter,结合Swagger注解的处理可能触发了意外的方法重载; - Swagger注解冲突:你在
@Operation里用了全限定类名的@io.swagger.v3.oas.annotations.parameters.RequestBody,同时方法参数又用了全限定的@org.springframework.web.bind.annotation.RequestBody,双重注解可能导致Spring的映射处理器误判。
解决方案
针对这些可能的原因,按以下步骤修改代码,应该能解决问题:
1. 修正代码格式,确保语法解析正确
每个字段、注解、方法都单独占一行,避免Groovy编译时的歧义。
2. 使用构造注入代替字段注入(Spring官方推荐)
构造注入能避免Groovy自动生成getter/setter的潜在问题,同时让依赖关系更清晰:
// 替换原来的字段注入 private final LoginAuthenticator loginAuthenticator // 增加构造方法 AuthenticationApiController(LoginAuthenticator loginAuthenticator) { this.loginAuthenticator = loginAuthenticator }
3. 统一注解的使用方式
既然已经导入了注解包,就不要用全限定类名,减少冲突:
// 方法参数上直接用@RequestBody,不用写全限定名 ResponseEntity<?> token(@RequestBody CredencialsDTO credencials) { // ... 原有逻辑 } // @Operation里的requestBody也简化 requestBody = @RequestBody( required = true, content = @Content( // ... 原有内容 ) )
4. 最终修正后的完整代码
package example.infra.adapters.input.api.v1.endpoints import example.aplication.services.authentication.LoginAuthenticator import example.aplication.dtos.authentication.CredencialsDTO import example.aplication.dtos.authentication.AuthenticationTokenDTO import example.aplication.exceptions.authentication.AuthenticationErrorException import org.springframework.web.bind.annotation.* import org.springframework.http.ResponseEntity import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.ExampleObject import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.parameters.RequestBody @RestController @RequestMapping("/api/v1/authentication") @Tag(name = "Authentication", description = "User authentication") class AuthenticationApiController { private final LoginAuthenticator loginAuthenticator AuthenticationApiController(LoginAuthenticator loginAuthenticator) { this.loginAuthenticator = loginAuthenticator } @PostMapping("/token") @Operation( summary = "Get authentication token", requestBody = @RequestBody( required = true, content = @Content( mediaType = "application/json", schema = @Schema(implementation = CredencialsDTO), examples = [ @ExampleObject( value = '{"login": "loginExample", "senha": "senhaExample"}' ) ] ) ), responses = [ @ApiResponse( responseCode = "200", description = "Success", content = @Content( mediaType = "application/json", schema = @Schema(implementation = AuthenticationTokenDTO), examples = [ @ExampleObject( value = '{"token": "abc123", "expiration": "2025-09-03T18:00:00Z"}' ) ] ) ), @ApiResponse( responseCode = "401", description = "Incorrect login or password", content = @Content() ) ] ) ResponseEntity<?> token(@RequestBody CredencialsDTO credencials) { try { AuthenticationTokenDTO tokenDto = loginAuthenticator.authenticate(credencials) return JsonResponse.success(tokenDto) } catch (AuthenticationErrorException e) { return JsonResponse.unauthorized() } } }
验证修改
修改后重新启动应用grails run-app --port 8080,应该不会再出现模糊映射的错误了。如果还有问题,可以检查:
- 有没有其他控制器类映射了相同的路径;
- Grails的组件扫描配置是否正确,避免重复扫描同一个包;
- springdoc-openapi的版本是否和Grails/Spring Boot版本兼容。




