嵌入式 Jetty HttpServer 不接受 mutipart/form-data

Embedded Jetty HttpServer not accepting mutipart/form-data

我正在为我们的 REST API 的嵌入式 Http 服务器从 Eclipse Jersey/Grizzly (2.33) 迁移到 Eclipse/Jetty (10.0.6),我可以'为了我的生活,让 multipart/form-data 上传正常工作。我承认我 精通 Jetty 配置,也不 Jersey/Grizzly 配置,我正在将旧代码与 Jetty 食谱中的最少样板拼凑在一起。

在这一点上,我很高兴能让服务器接受请求。我可以自己研究如何处理这些文件。我目前的主要目标是 而不是 必须立即重写数十个 servlets/handlers(因此使用 Jersey ServletContainer)。

这是服务器代码:

    public static void start() throws Exception {
    
    httpConfig = new HttpConfiguration();

    HttpConnectionFactory http11 = new HttpConnectionFactory(httpConfig);
    
    server = new Server();
    ServerConnector connector = new ServerConnector(server, http11);
    connector.setPort((cmdOptions.port < 0 ? 9998 : cmdOptions.port));
    server.setConnectors(new Connector[] {connector});
    
    ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
    context.setContextPath("/");

    HandlerList handlers = new HandlerList();
    ServletHandler servletHandler = new ServletHandler();
    
    // Set up the resources.common package as the handlers for the servlet
    ServletHolder servletHolder = context.addServlet(ServletContainer.class, "/*");
    servletHolder.setInitOrder(0);
    servletHolder.setInitParameter("jersey.config.server.provider.packages", "resources.grizzly;resources.common");
    servletHandler.addServlet(servletHolder);
    
    // MultiPartConfig setup - to allow for ServletRequest.getParts() usage
    Path multipartTmpDir = Paths.get("target", "multipart-tmp");
    multipartTmpDir = CommonResFileManager.ensureDirExists(multipartTmpDir);

    String location = multipartTmpDir.toString();
    long maxFileSize = 10 * 1024 * 1024; // 10 MB
    long maxRequestSize = 10 * 1024 * 1024; // 10 MB
    int fileSizeThreshold = 64 * 1024; // 64 KB
    MultipartConfigElement multipartConfig = new MultipartConfigElement(location, maxFileSize, maxRequestSize, fileSizeThreshold);
    
    FilterHolder filterHolder;

    filterHolder = context.addFilter(resources.jetty.SecurityFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
    filterHolder.setAsyncSupported(true);

    CorsHandler corsHandler = new CorsHandler();
    corsHandler.setHandler(context);

    UploadHandler uploadHandler = new UploadHandler("/G/uploadFolder", multipartConfig, multipartTmpDir);
    
    handlers.addHandler(corsHandler);
    handlers.addHandler(uploadHandler);
    handlers.addHandler(servletHandler);
    
    server.setHandler(handlers);
    server.start();
}

感兴趣的资源是:

public class CommonResProject extends CommonResBase {

...

    @POST @Path("uploadFolder")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    public String uploadFolder(final FormDataMultiPart multiPart)            
    {
        Collection<Part> parts = null;
        try {
            parts = ((HttpServletRequest)request).getParts();
        } catch (IOException | ServletException ex) {
            Logger.getLogger(CommonResProject.class.getName()).log(Level.SEVERE, null, ex);
        }
        if(parts != null){
            parts.stream().forEach(p -> System.out.println(p.getName() + " ["+p.getContentType()+"]: "+p.getSize()+" bytes"));
        }
        // projects is a POJO that actually does the fiddly bits with the uploaded files
        boolean retVal = projects.uploadFolder(getDB(), getUserId(), multiPart);
        return "{\"retVal\" : " + String.valueOf(retVal) + "}";
    }   

...

扩展了:

@Path("/GProject")
public class GProject extends CommonResProject
{
    public GProject()
    {
        super();
        resInterface = new GBaseRes();  // Must always do
    }
    
    public static void processParts(HttpServletRequest request, HttpServletResponse response, java.nio.file.Path outputDir) throws ServletException, IOException
    {
        response.setContentType("text/plain");
        response.setCharacterEncoding("utf-8");

        PrintWriter out = response.getWriter();

        for (Part part : request.getParts())
        {
            out.printf("Got Part[%s].size=%s%n", part.getName(), part.getSize());
            out.printf("Got Part[%s].contentType=%s%n", part.getName(), part.getContentType());
            out.printf("Got Part[%s].submittedFileName=%s%n", part.getName(), part.getSubmittedFileName());
            String filename = part.getSubmittedFileName();
            if (StringUtil.isNotBlank(filename))
            {
                // ensure we don't have "/" and ".." in the raw form.
                filename = URLEncoder.encode(filename, "utf-8");

                java.nio.file.Path outputFile = outputDir.resolve(filename);
                try (InputStream inputStream = part.getInputStream();
                     OutputStream outputStream = Files.newOutputStream(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))
                {
                    IO.copy(inputStream, outputStream);
                    out.printf("Saved Part[%s] to %s%n", part.getName(), outputFile.toString());
                }
            }
        }
    }
    
    public static ServletContextHandler newServletUploadHandler(MultipartConfigElement multipartConfig, java.nio.file.Path outputDir) throws IOException
    {
        ServletContextHandler context = new ServletContextHandler();

        SaveUploadServlet saveUploadServlet = new SaveUploadServlet(outputDir);
        ServletHolder servletHolder = new ServletHolder(saveUploadServlet);
        servletHolder.getRegistration().setMultipartConfig(multipartConfig);

        context.addServlet(servletHolder, "/uploadFolder");

        return context;
    }   
    
    public static class SaveUploadServlet extends HttpServlet
    {
        private final java.nio.file.Path outputDir;

        public SaveUploadServlet(java.nio.file.Path outputDir) throws IOException
        {
            this.outputDir = outputDir.resolve("servlet");
            ensureDirExists(this.outputDir);
        }

        @Override
        protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
        {
            processParts(request, response, outputDir);
        }
    }
    
    
    public static class UploadHandler extends AbstractHandler
    {
        private final String contextPath;
        private final MultipartConfigElement multipartConfig;
        private final java.nio.file.Path outputDir;

        public UploadHandler(String contextPath, MultipartConfigElement multipartConfig, java.nio.file.Path outputDir) throws IOException
        {
            super();
            this.contextPath = contextPath;
            this.multipartConfig = multipartConfig;
            this.outputDir = outputDir.resolve("handler");
            CommonResFileManager.ensureDirExists(this.outputDir);
        }

        @Override
        public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
        {
            if (!target.startsWith(contextPath))
            {
                // not meant for us, skip it.
                return;
            }

            if (!request.getMethod().equalsIgnoreCase("POST"))
            {
                response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
                return;
            }

            // Ensure request knows about MultiPartConfigElement setup.
            request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, multipartConfig);
            // Process the request
            processParts(request, response, outputDir);
            //baseRequest.setHandled(true);
        }
    }
}

当我尝试上传一组文件时,整个过程会生成以下堆栈跟踪:

2021-09-13 12:58:17 SEVERE - resources.common.ResponseExceptionMapper toResponse -- HTTP 415 Unsupported Media Type
javax.ws.rs.NotSupportedException: HTTP 415 Unsupported Media Type
    at org.glassfish.jersey.server.spi.internal.ParameterValueHelper.getParameterValues(ParameterValueHelper.java:75)
    at org.glassfish.jersey.server.model.internal.JavaResourceMethodDispatcherProvider$AbstractMethodParamInvoker.getParamValues(JavaResourceMethodDispatcherProvider.java:109)
    at org.glassfish.jersey.server.model.internal.JavaResourceMethodDispatcherProvider$TypeOutInvoker.doDispatch(JavaResourceMethodDispatcherProvider.java:219)
    at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.dispatch(AbstractJavaResourceMethodDispatcher.java:79)
    at org.glassfish.jersey.server.model.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:475)
    at org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:397)
    at org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:81)
    at org.glassfish.jersey.server.ServerRuntime.run(ServerRuntime.java:255)
    at org.glassfish.jersey.internal.Errors.call(Errors.java:248)
    at org.glassfish.jersey.internal.Errors.call(Errors.java:244)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:292)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:274)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:244)
    at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:265)
    at org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:234)
    at org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:680)
    at org.glassfish.jersey.servlet.WebComponent.serviceImpl(WebComponent.java:394)
    at org.glassfish.jersey.servlet.WebComponent.service(WebComponent.java:346)
    at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:366)
    at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:319)
    at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:205)
    at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:764)
    at org.eclipse.jetty.servlet.ServletHandler$ChainEnd.doFilter(ServletHandler.java:1619)
    at resources.jetty.SecurityFilter.doFilter(SecurityFilter.java:232)
    at org.eclipse.jetty.servlet.FilterHolder.doFilter(FilterHolder.java:202)
    at org.eclipse.jetty.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1594)
    at org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter.doFilter(WebSocketUpgradeFilter.java:164)
    at org.eclipse.jetty.servlet.FilterHolder.doFilter(FilterHolder.java:202)
    at org.eclipse.jetty.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1594)
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:506)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:221)
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1571)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:221)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1372)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:176)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:463)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1544)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:174)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1294)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:129)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)
    at resources.jetty.CorsHandler.handle(CorsHandler.java:30)
    at org.eclipse.jetty.server.handler.HandlerList.handle(HandlerList.java:51)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)
    at org.eclipse.jetty.server.Server.handle(Server.java:562)
    at org.eclipse.jetty.server.HttpChannel.lambda$handle[=14=](HttpChannel.java:406)
    at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:663)
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:398)
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:282)
    at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:319)
    at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:100)
    at org.eclipse.jetty.io.SocketChannelEndPoint.run(SocketChannelEndPoint.java:101)
    at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.runTask(AdaptiveExecutionStrategy.java:412)
    at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.consumeTask(AdaptiveExecutionStrategy.java:381)
    at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.tryProduce(AdaptiveExecutionStrategy.java:268)
    at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.lambda$new[=14=](AdaptiveExecutionStrategy.java:138)
    at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:378)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:894)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1038)
    at java.base/java.lang.Thread.run(Thread.java:832)
Caused by: org.glassfish.jersey.message.internal.MessageBodyProviderNotFoundException: MessageBodyReader not found for media type=multipart/form-data;boundary=----WebKitFormBoundary4K5nPFIDDwLPZAnk, type=class org.glassfish.jersey.media.multipart.FormDataMultiPart, genericType=class org.glassfish.jersey.media.multipart.FormDataMultiPart.
    at org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$TerminalReaderInterceptor.aroundReadFrom(ReaderInterceptorExecutor.java:208)
    at org.glassfish.jersey.message.internal.ReaderInterceptorExecutor.proceed(ReaderInterceptorExecutor.java:132)
    at org.glassfish.jersey.server.internal.MappableExceptionWrapperInterceptor.aroundReadFrom(MappableExceptionWrapperInterceptor.java:49)
    at org.glassfish.jersey.message.internal.ReaderInterceptorExecutor.proceed(ReaderInterceptorExecutor.java:132)
    at org.glassfish.jersey.message.internal.MessageBodyFactory.readFrom(MessageBodyFactory.java:1072)
    at org.glassfish.jersey.message.internal.InboundMessageContext.readEntity(InboundMessageContext.java:885)
    at org.glassfish.jersey.server.ContainerRequest.readEntity(ContainerRequest.java:282)
    at org.glassfish.jersey.server.internal.inject.EntityParamValueParamProvider$EntityValueSupplier.apply(EntityParamValueParamProvider.java:73)
    at org.glassfish.jersey.server.internal.inject.EntityParamValueParamProvider$EntityValueSupplier.apply(EntityParamValueParamProvider.java:56)
    at org.glassfish.jersey.server.spi.internal.ParamValueFactoryWithSource.apply(ParamValueFactoryWithSource.java:50)
    at org.glassfish.jersey.server.spi.internal.ParameterValueHelper.getParameterValues(ParameterValueHelper.java:68)

首先,不要直接使用ServletHandler

仅使用 ServletContetHandlerServletHolder 配置您需要的内容。

ServletHandler 是一个内部 class 并不意味着可以像那样直接使用。 特别是对于您试图强加的所有配置。

接下来,将 UploadHandler 转换为 normal/formal HttpServlet 并将其正确添加到 ServletContextHandler (您甚至可以使用相同的 url-pattern就像你现在一样)。 ServletContext 在这里很重要(对于多部分),而您的 raw/naked UploadHandler 实际上并没有像您想象的那样处理多部分。

stacktrace 表明您在生成 stacktrace 的时间点没有将 Jetty 用于 multipart,这意味着它绕过了 UploadHandler 并且 Jersey 本身正在尝试处理 multipart 内容。这可能意味着您在 Jersey servlet 上指定了 MultiPartConfigElement


ServletHolder servletHolder = context.addServlet(ServletContainer.class, "/*");
servletHolder.setInitOrder(0);
servletHolder.setInitParameter("jersey.config.server.provider.packages",
   "resources.grizzly;resources.common");

Path multipartTmpDir = Paths.get("target", "multipart-tmp");
multipartTmpDir = CommonResFileManager.ensureDirExists(multipartTmpDir);

String location = multipartTmpDir.toString();
long maxFileSize = 10 * 1024 * 1024; // 10 MB
long maxRequestSize = 10 * 1024 * 1024; // 10 MB
int fileSizeThreshold = 64 * 1024; // 64 KB
MultipartConfigElement multipartConfig = new MultipartConfigElement(location,
   maxFileSize, maxRequestSize, fileSizeThreshold);

servletHolder.getRegistration().setMultipartConfig(multipartConfig);