Jetty 10迁移至Jetty 12后嵌入式服务无法提供openapi.json
我维护一个基于嵌入式Jetty+JAX-RS的Java应用,用Swagger v3注解生成接口文档。在Jetty 10时,通过io.swagger.v3.jaxrs2.integration.resources.OpenApiResource可以正常在/cva/api/openapi.json路径返回openapi.json文件。但迁移到Jetty 12.0.12后,Swagger UI页面能访问,但请求/cva/api/openapi.json返回404,导致Swagger UI提示"Failed to load API definition",无法正常工作。
服务器初始化代码如下:
// called like this new CvaServer("/cva", getPort()).run() CvaServer(String contextPath, int port) throws Exception { server = new Server(port); // Set up resource config ResourceConfig resourceConfig = new ResourceConfig(); resourceConfig.packages("io.swagger.v3.jaxrs2.integration.resources", "org.gel.cva.server.rest", // This is the package name for my APIs "org.gel.cva.server.exceptionmapper", "com.jersey.jaxb", "com.fasterxml.jackson.jaxrs.json"); resourceConfig.register(MultiPartFeature.class); resourceConfig.register(RolesAllowedDynamicFeature.class); resourceConfig.register(AuthenticatingRequestFilter.class); resourceConfig.register(AuditLoggingFilter.class); resourceConfig.register(StringNormalisationConverterProvider.class); resourceConfig.register(EntityFilteringFeature.class); resourceConfig.register(AuthMethodFilter.class); EncodingFilter.enableFor(resourceConfig, GZipEncoder.class); // Set up ServletContextHandler ServletContextHandler servletContextHandler = new ServletContextHandler(); servletContextHandler.setContextPath(contextPath); servletContextHandler.addServlet(new ServletHolder(new ServletContainer(resourceConfig)), "/api/*"); servletContextHandler.addEventListener(new CvaContextListener()); // Set up ResourceHandler ResourceHandler resourceHandler = new ResourceHandler(); resourceHandler.setWelcomeMode(ResourceService.WelcomeMode.REDIRECT); servletContextHandler.setContextPath("/"); servletContextHandler.setWelcomeFiles(new String[] { "index.html" }); servletContextHandler.setBaseResource( ResourceFactory.of(resourceHandler).newClassLoaderResource("static", true) ); // Set up CrossOriginHandler CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); crossOriginHandler.setAllowedOriginPatterns(Set.of("*")); crossOriginHandler.setAllowedMethods(Set.of("GET","POST","PUT","DELETE","OPTIONS","HEAD")); crossOriginHandler.setAllowCredentials(true); crossOriginHandler.setAllowedHeaders(Set.of("*")); // Set up OpenAPI models OpenAPI oas = new OpenAPI(); oas.setServers(List.of(new io.swagger.v3.oas.models.servers.Server().url("/cva/api"))); Info info = new Info() .title("title") .description("") .version(CvaConfiguration.CVA_VERSION); oas.info(info); SwaggerConfiguration oasConfig = new SwaggerConfiguration() .openAPI(oas) .prettyPrint(true) .resourcePackages(Set.of("org.gel.cva.server.rest")); // Set up JaxrsOpenApiContextBuilder try { new JaxrsOpenApiContextBuilder() .application(resourceConfig) .openApiConfiguration(oasConfig) .buildContext(true) .read(); } catch (OpenApiConfigurationException e) { throw new RuntimeException(e.getMessage(), e); } crossOriginHandler.setHandler(servletContextHandler); servletContextHandler.addServlet(new ServletHolder(new DefaultServlet()), "/*"); server.setHandler(servletContextHandler); // Set up Connectors ServerConnector serverConnector = new ServerConnector( server, new HttpConnectionFactory(), new HTTP2CServerConnectionFactory(new HttpConfiguration()) ); serverConnector.setPort(port); server.setConnectors(new ServerConnector[]{serverConnector}); }
预期:openapi.json能正常访问,Swagger页面正常工作。
问题根源
- ContextPath被覆盖:代码先将
ServletContextHandler的contextPath设为传入的/cva,后续又重置为/,导致Jersey的/api/*映射到根路径,而非预期的/cva/api/*,所以/cva/api/openapi.json无法匹配路由。 - 跨域处理器未生效:创建
CrossOriginHandler后未将其设为服务器根处理器,跨域配置实际未起作用。 - 路由冲突:静态资源与API接口共用同一个
ServletContextHandler,且DefaultServlet拦截/*,覆盖了Jersey的API路由。
修复步骤
1. 拆分处理器,分离API与静态资源
创建两个独立的ServletContextHandler:一个处理API请求(contextPath设为/cva),另一个处理静态资源(contextPath设为/),通过HandlerList组合两者。
2. 保留API的ContextPath
确保API对应的ServletContextHandler的contextPath始终为传入的/cva,避免被后续代码覆盖。
3. 正确配置跨域处理器
将CrossOriginHandler作为服务器根处理器,内部包含组合后的HandlerList,确保所有请求都经过跨域校验。
4. 调整静态资源配置
静态资源的ServletContextHandler中正确设置ResourceHandler和DefaultServlet,避免与API路由冲突。
修复后的完整代码
// called like this new CvaServer("/cva", getPort()).run() CvaServer(String contextPath, int port) throws Exception { server = new Server(port); // -------------------------- // 1. 配置JAX-RS资源(API接口) // -------------------------- ResourceConfig resourceConfig = new ResourceConfig(); resourceConfig.packages("io.swagger.v3.jaxrs2.integration.resources", "org.gel.cva.server.rest", "org.gel.cva.server.exceptionmapper", "com.jersey.jaxb", "com.fasterxml.jackson.jaxrs.json"); resourceConfig.register(MultiPartFeature.class); resourceConfig.register(RolesAllowedDynamicFeature.class); resourceConfig.register(AuthenticatingRequestFilter.class); resourceConfig.register(AuditLoggingFilter.class); resourceConfig.register(StringNormalisationConverterProvider.class); resourceConfig.register(EntityFilteringFeature.class); resourceConfig.register(AuthMethodFilter.class); EncodingFilter.enableFor(resourceConfig, GZipEncoder.class); // API对应的ServletContextHandler,contextPath保持为传入的/cva ServletContextHandler apiContext = new ServletContextHandler(ServletContextHandler.SESSIONS); apiContext.setContextPath(contextPath); apiContext.addServlet(new ServletHolder(new ServletContainer(resourceConfig)), "/api/*"); apiContext.addEventListener(new CvaContextListener()); // -------------------------- // 2. 配置静态资源(Swagger UI等) // -------------------------- ServletContextHandler staticContext = new ServletContextHandler(ServletContextHandler.SESSIONS); staticContext.setContextPath("/"); staticContext.setWelcomeFiles(new String[]{"index.html"}); ResourceHandler resourceHandler = new ResourceHandler(); resourceHandler.setWelcomeMode(ResourceService.WelcomeMode.REDIRECT); resourceHandler.setBaseResource(ResourceFactory.of(resourceHandler).newClassLoaderResource("static", true)); staticContext.setHandler(resourceHandler); // 添加DefaultServlet处理静态资源默认请求 staticContext.addServlet(new ServletHolder("default", new DefaultServlet()), "/*"); // -------------------------- // 3. 组合API和静态资源处理器 // -------------------------- HandlerList handlerList = new HandlerList(); handlerList.addHandler(apiContext); handlerList.addHandler(staticContext); // -------------------------- // 4. 配置跨域处理器 // -------------------------- CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); crossOriginHandler.setAllowedOriginPatterns(Set.of("*")); crossOriginHandler.setAllowedMethods(Set.of("GET","POST","PUT","DELETE","OPTIONS","HEAD")); crossOriginHandler.setAllowCredentials(true); crossOriginHandler.setAllowedHeaders(Set.of("*")); crossOriginHandler.setHandler(handlerList); // -------------------------- // 5. 配置OpenAPI // -------------------------- OpenAPI oas = new OpenAPI(); oas.setServers(List.of(new io.swagger.v3.oas.models.servers.Server().url(contextPath + "/api"))); Info info = new Info() .title("title") .description("") .version(CvaConfiguration.CVA_VERSION); oas.info(info); SwaggerConfiguration oasConfig = new SwaggerConfiguration() .openAPI(oas) .prettyPrint(true) .resourcePackages(Set.of("org.gel.cva.server.rest")); try { new JaxrsOpenApiContextBuilder() .application(resourceConfig) .openApiConfiguration(oasConfig) .buildContext(true) .read(); } catch (OpenApiConfigurationException e) { throw new RuntimeException(e.getMessage(), e); } // -------------------------- // 6. 设置服务器根处理器和连接器 // -------------------------- server.setHandler(crossOriginHandler); ServerConnector serverConnector = new ServerConnector( server, new HttpConnectionFactory(), new HTTP2CServerConnectionFactory(new HttpConfiguration()) ); serverConnector.setPort(port); server.setConnectors(new ServerConnector[]{serverConnector}); }
验证
启动服务器后,访问/cva/api/openapi.json即可正常获取OpenAPI文档,Swagger UI也能加载该文档并正常显示接口信息。
内容的提问来源于stack exchange,提问作者user28027519

