Servlet for serving static content

前端 未结 14 1698
天命终不由人
天命终不由人 2020-11-22 06:02

I deploy a webapp on two different containers (Tomcat and Jetty), but their default servlets for serving the static content have a different way of handling the URL structur

相关标签:
14条回答
  • 2020-11-22 06:21

    Abstract template for a static resource servlet

    Partly based on this blog from 2007, here's a modernized and highly reusable abstract template for a servlet which properly deals with caching, ETag, If-None-Match and If-Modified-Since (but no Gzip and Range support; just to keep it simple; Gzip could be done with a filter or via container configuration).

    public abstract class StaticResourceServlet extends HttpServlet {
    
        private static final long serialVersionUID = 1L;
        private static final long ONE_SECOND_IN_MILLIS = TimeUnit.SECONDS.toMillis(1);
        private static final String ETAG_HEADER = "W/\"%s-%s\"";
        private static final String CONTENT_DISPOSITION_HEADER = "inline;filename=\"%1$s\"; filename*=UTF-8''%1$s";
    
        public static final long DEFAULT_EXPIRE_TIME_IN_MILLIS = TimeUnit.DAYS.toMillis(30);
        public static final int DEFAULT_STREAM_BUFFER_SIZE = 102400;
    
        @Override
        protected void doHead(HttpServletRequest request, HttpServletResponse response) throws ServletException ,IOException {
            doRequest(request, response, true);
        }
    
        @Override
        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            doRequest(request, response, false);
        }
    
        private void doRequest(HttpServletRequest request, HttpServletResponse response, boolean head) throws IOException {
            response.reset();
            StaticResource resource;
    
            try {
                resource = getStaticResource(request);
            }
            catch (IllegalArgumentException e) {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST);
                return;
            }
    
            if (resource == null) {
                response.sendError(HttpServletResponse.SC_NOT_FOUND);
                return;
            }
    
            String fileName = URLEncoder.encode(resource.getFileName(), StandardCharsets.UTF_8.name());
            boolean notModified = setCacheHeaders(request, response, fileName, resource.getLastModified());
    
            if (notModified) {
                response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
                return;
            }
    
            setContentHeaders(response, fileName, resource.getContentLength());
    
            if (head) {
                return;
            }
    
            writeContent(response, resource);
        }
    
        /**
         * Returns the static resource associated with the given HTTP servlet request. This returns <code>null</code> when
         * the resource does actually not exist. The servlet will then return a HTTP 404 error.
         * @param request The involved HTTP servlet request.
         * @return The static resource associated with the given HTTP servlet request.
         * @throws IllegalArgumentException When the request is mangled in such way that it's not recognizable as a valid
         * static resource request. The servlet will then return a HTTP 400 error.
         */
        protected abstract StaticResource getStaticResource(HttpServletRequest request) throws IllegalArgumentException;
    
        private boolean setCacheHeaders(HttpServletRequest request, HttpServletResponse response, String fileName, long lastModified) {
            String eTag = String.format(ETAG_HEADER, fileName, lastModified);
            response.setHeader("ETag", eTag);
            response.setDateHeader("Last-Modified", lastModified);
            response.setDateHeader("Expires", System.currentTimeMillis() + DEFAULT_EXPIRE_TIME_IN_MILLIS);
            return notModified(request, eTag, lastModified);
        }
    
        private boolean notModified(HttpServletRequest request, String eTag, long lastModified) {
            String ifNoneMatch = request.getHeader("If-None-Match");
    
            if (ifNoneMatch != null) {
                String[] matches = ifNoneMatch.split("\\s*,\\s*");
                Arrays.sort(matches);
                return (Arrays.binarySearch(matches, eTag) > -1 || Arrays.binarySearch(matches, "*") > -1);
            }
            else {
                long ifModifiedSince = request.getDateHeader("If-Modified-Since");
                return (ifModifiedSince + ONE_SECOND_IN_MILLIS > lastModified); // That second is because the header is in seconds, not millis.
            }
        }
    
        private void setContentHeaders(HttpServletResponse response, String fileName, long contentLength) {
            response.setHeader("Content-Type", getServletContext().getMimeType(fileName));
            response.setHeader("Content-Disposition", String.format(CONTENT_DISPOSITION_HEADER, fileName));
    
            if (contentLength != -1) {
                response.setHeader("Content-Length", String.valueOf(contentLength));
            }
        }
    
        private void writeContent(HttpServletResponse response, StaticResource resource) throws IOException {
            try (
                ReadableByteChannel inputChannel = Channels.newChannel(resource.getInputStream());
                WritableByteChannel outputChannel = Channels.newChannel(response.getOutputStream());
            ) {
                ByteBuffer buffer = ByteBuffer.allocateDirect(DEFAULT_STREAM_BUFFER_SIZE);
                long size = 0;
    
                while (inputChannel.read(buffer) != -1) {
                    buffer.flip();
                    size += outputChannel.write(buffer);
                    buffer.clear();
                }
    
                if (resource.getContentLength() == -1 && !response.isCommitted()) {
                    response.setHeader("Content-Length", String.valueOf(size));
                }
            }
        }
    
    }
    

    Use it together with the below interface representing a static resource.

    interface StaticResource {
    
        /**
         * Returns the file name of the resource. This must be unique across all static resources. If any, the file
         * extension will be used to determine the content type being set. If the container doesn't recognize the
         * extension, then you can always register it as <code>&lt;mime-type&gt;</code> in <code>web.xml</code>.
         * @return The file name of the resource.
         */
        public String getFileName();
    
        /**
         * Returns the last modified timestamp of the resource in milliseconds.
         * @return The last modified timestamp of the resource in milliseconds.
         */
        public long getLastModified();
    
        /**
         * Returns the content length of the resource. This returns <code>-1</code> if the content length is unknown.
         * In that case, the container will automatically switch to chunked encoding if the response is already
         * committed after streaming. The file download progress may be unknown.
         * @return The content length of the resource.
         */
        public long getContentLength();
    
        /**
         * Returns the input stream with the content of the resource. This method will be called only once by the
         * servlet, and only when the resource actually needs to be streamed, so lazy loading is not necessary.
         * @return The input stream with the content of the resource.
         * @throws IOException When something fails at I/O level.
         */
        public InputStream getInputStream() throws IOException;
    
    }
    

    All you need is just extending from the given abstract servlet and implementing the getStaticResource() method according the javadoc.

    Concrete example serving from file system:

    Here's a concrete example which serves it via an URL like /files/foo.ext from the local disk file system:

    @WebServlet("/files/*")
    public class FileSystemResourceServlet extends StaticResourceServlet {
    
        private File folder;
    
        @Override
        public void init() throws ServletException {
            folder = new File("/path/to/the/folder");
        }
    
        @Override
        protected StaticResource getStaticResource(HttpServletRequest request) throws IllegalArgumentException {
            String pathInfo = request.getPathInfo();
    
            if (pathInfo == null || pathInfo.isEmpty() || "/".equals(pathInfo)) {
                throw new IllegalArgumentException();
            }
    
            String name = URLDecoder.decode(pathInfo.substring(1), StandardCharsets.UTF_8.name());
            final File file = new File(folder, Paths.get(name).getFileName().toString());
    
            return !file.exists() ? null : new StaticResource() {
                @Override
                public long getLastModified() {
                    return file.lastModified();
                }
                @Override
                public InputStream getInputStream() throws IOException {
                    return new FileInputStream(file);
                }
                @Override
                public String getFileName() {
                    return file.getName();
                }
                @Override
                public long getContentLength() {
                    return file.length();
                }
            };
        }
    
    }
    

    Concrete example serving from database:

    Here's a concrete example which serves it via an URL like /files/foo.ext from the database via an EJB service call which returns your entity having a byte[] content property:

    @WebServlet("/files/*")
    public class YourEntityResourceServlet extends StaticResourceServlet {
    
        @EJB
        private YourEntityService yourEntityService;
    
        @Override
        protected StaticResource getStaticResource(HttpServletRequest request) throws IllegalArgumentException {
            String pathInfo = request.getPathInfo();
    
            if (pathInfo == null || pathInfo.isEmpty() || "/".equals(pathInfo)) {
                throw new IllegalArgumentException();
            }
    
            String name = URLDecoder.decode(pathInfo.substring(1), StandardCharsets.UTF_8.name());
            final YourEntity yourEntity = yourEntityService.getByName(name);
    
            return (yourEntity == null) ? null : new StaticResource() {
                @Override
                public long getLastModified() {
                    return yourEntity.getLastModified();
                }
                @Override
                public InputStream getInputStream() throws IOException {
                    return new ByteArrayInputStream(yourEntityService.getContentById(yourEntity.getId()));
                }
                @Override
                public String getFileName() {
                    return yourEntity.getName();
                }
                @Override
                public long getContentLength() {
                    return yourEntity.getContentLength();
                }
            };
        }
    
    }
    
    0 讨论(0)
  • 2020-11-22 06:21

    try this

    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>*.js</url-pattern>
        <url-pattern>*.css</url-pattern>
        <url-pattern>*.ico</url-pattern>
        <url-pattern>*.png</url-pattern>
        <url-pattern>*.jpg</url-pattern>
        <url-pattern>*.htc</url-pattern>
        <url-pattern>*.gif</url-pattern>
    </servlet-mapping>    
    

    Edit: This is only valid for the servlet 2.5 spec and up.

    0 讨论(0)
  • 2020-11-22 06:21

    To serve all requests from a Spring app as well as /favicon.ico and the JSP files from /WEB-INF/jsp/* that Spring's AbstractUrlBasedView will request you can just remap the jsp servlet and default servlet:

      <servlet>
        <servlet-name>springapp</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
      </servlet>
    
      <servlet-mapping>
        <servlet-name>jsp</servlet-name>
        <url-pattern>/WEB-INF/jsp/*</url-pattern>
      </servlet-mapping>
    
      <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/favicon.ico</url-pattern>
      </servlet-mapping>
    
      <servlet-mapping>
        <servlet-name>springapp</servlet-name>
        <url-pattern>/*</url-pattern>
      </servlet-mapping>
    

    We can't rely on the *.jsp url-pattern on the standard mapping for the jsp servlet because the path pattern '/*' is matched before any extension mapping is checked. Mapping the jsp servlet to a deeper folder means it's matched first. Matching '/favicon.ico' exactly happens before path pattern matching. Deeper path matches will work, or exact matches, but no extension matches can make it past the '/*' path match. Mapping '/' to default servlet doesn't appear to work. You'd think the exact '/' would beat the '/*' path pattern on springapp.

    The above filter solution doesn't work for forwarded/included JSP requests from the application. To make it work I had to apply the filter to springapp directly, at which point the url-pattern matching was useless as all requests that go to the application also go to its filters. So I added pattern matching to the filter and then learned about the 'jsp' servlet and saw that it doesn't remove the path prefix like the default servlet does. That solved my problem, which was not exactly the same but common enough.

    0 讨论(0)
  • 2020-11-22 06:21

    Use org.mortbay.jetty.handler.ContextHandler. You don't need additional components like StaticServlet.

    At the jetty home,

    $ cd contexts

    $ cp javadoc.xml static.xml

    $ vi static.xml

    ...

    <Configure class="org.mortbay.jetty.handler.ContextHandler">
    <Set name="contextPath">/static</Set>
    <Set name="resourceBase"><SystemProperty name="jetty.home" default="."/>/static/</Set>
    <Set name="handler">
      <New class="org.mortbay.jetty.handler.ResourceHandler">
        <Set name="cacheControl">max-age=3600,public</Set>
      </New>
     </Set>
    </Configure>
    

    Set the value of contextPath with your URL prefix, and set the value of resourceBase as the file path of the static content.

    It worked for me.

    0 讨论(0)
  • 2020-11-22 06:24

    Checked for Tomcat 8.x: static resources work OK if root servlet map to "". For servlet 3.x it could be done by @WebServlet("")

    0 讨论(0)
  • 2020-11-22 06:25

    I came up with a slightly different solution. It's a bit hack-ish, but here is the mapping:

    <servlet-mapping>   
        <servlet-name>default</servlet-name>
        <url-pattern>*.html</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>*.jpg</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
     <servlet-name>default</servlet-name>
        <url-pattern>*.png</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>*.css</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>*.js</url-pattern>
    </servlet-mapping>
    
    <servlet-mapping>
        <servlet-name>myAppServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    

    This basically just maps all content files by extension to the default servlet, and everything else to "myAppServlet".

    It works in both Jetty and Tomcat.

    0 讨论(0)
提交回复
热议问题