用于提供静态内容的 Servlet

我将一个 webapp 部署在两个不同的容器(Tomcat 和 Jetty)上,但是它们用于提供静态内容的默认 servlet 对于处理我想要使用的 URL 结构(详情)有不同的方式。

因此,我希望在 webapp 中包含一个小的 servlet 来提供它自己的静态内容(图片、 CSS 等)。Servlet 应该具有以下属性:

这样的 servlet 在哪里可用吗? 我能找到的最接近的是 servlet 书中的 例子4-10

更新: 我想使用的 URL 结构——如果你想知道的话——很简单:

    <servlet-mapping>
<servlet-name>main</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/static/*</url-pattern>
</servlet-mapping>

因此,所有请求都应该传递给主 servlet,除非它们是针对 static路径的。问题是 Tomcat 的默认 servlet 没有考虑 ServletPath (因此它在主文件夹中查找静态文件) ,而 Jetty 考虑了(因此它在 static文件夹中查找静态文件)。

179175 次浏览

我也遇到了同样的问题,我使用 Tomcat 代码库中的“默认 servlet”代码解决了这个问题。

Https://github.com/apache/tomcat/blob/master/java/org/apache/catalina/servlets/defaultservlet.java

默认 Servlet是服务于 Tomcat 中静态资源(jpg、 html、 css、 gif 等)的 servlet。

这个 servlet 非常高效,并且具有您在上面定义的一些属性。

我认为这个源代码,是一个很好的开始和删除你不需要的功能或依赖的方法。

  • 对 org.apache.naming.resources 包的引用可以删除,或者用 java.io. File 代码替换。
  • 对 org.apache.catalina.util 包的引用可能只是可以在源代码中复制的实用工具方法/类。
  • 可以内联或删除对 org.apache.catalina.Globals 类的引用。

最后我自己卷了一卷 StaticServlet。它支持 If-Modified-Since,gzip 编码,它应该能够提供来自 war 文件的静态文件。它不是非常困难的代码,但它也不完全是琐碎的。

代码可用: Java。请随意注释。

更新: Khurram 询问 StaticServlet中引用的 ServletUtils类。它只是一个带有辅助方法的类,我在项目中使用了这些方法。您需要的唯一方法是 coalesce(与 SQL 函数 COALESCE相同)。这是密码:

public static <T> T coalesce(T...ts) {
for(T t: ts)
if(t != null)
return t;
return null;
}

你不需要额外的组件,比如 StaticServlet。

在码头的家里,

$cd 上下文

$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>

使用 URL 前缀设置 contextPath 的值,并将 resources Base 的值设置为静态内容的文件路径。

这招对我很管用。

在这种情况下,不需要完全自定义默认 servlet 的实现,您可以使用这个简单的 servlet 将请求封装到容器的实现:


package com.example;


import java.io.*;


import javax.servlet.*;
import javax.servlet.http.*;


public class DefaultWrapperServlet extends HttpServlet
{
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
RequestDispatcher rd = getServletContext().getNamedDispatcher("default");


HttpServletRequest wrapped = new HttpServletRequestWrapper(req) {
public String getServletPath() { return ""; }
};


rd.forward(wrapped, resp);
}
}

我在网上找到了一些关于变通方法的很棒的教程。它简单而有效,我在几个项目中使用了 REST urls 样式方法:

Http://www.kuligowski.pl/java/rest-style-urls-and-url-mapping-for-static-content-apache-tomcat,5

为此,我扩展了 tomcat 默认 Servlet(Src)并重写了 getRelativePath ()方法。

package com.example;


import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import org.apache.catalina.servlets.DefaultServlet;


public class StaticServlet extends DefaultServlet
{
protected String pathPrefix = "/static";


public void init(ServletConfig config) throws ServletException
{
super.init(config);


if (config.getInitParameter("pathPrefix") != null)
{
pathPrefix = config.getInitParameter("pathPrefix");
}
}


protected String getRelativePath(HttpServletRequest req)
{
return pathPrefix + super.getRelativePath(req);
}
}

这是我的 servlet 映射

<servlet>
<servlet-name>StaticServlet</servlet-name>
<servlet-class>com.example.StaticServlet</servlet-class>
<init-param>
<param-name>pathPrefix</param-name>
<param-value>/static</param-value>
</init-param>
</servlet>


<servlet-mapping>
<servlet-name>StaticServlet</servlet-name>
<url-pattern>/static/*</url-pattern>
</servlet-mapping>

我使用 FileServlet得到了很好的结果,因为它几乎支持所有 HTTP (标签、块等)。

要满足 Spring 应用程序以及/Favicon.ico 和/WEB-INF/JSP/* 中的 JSP 文件的所有请求,只需重新映射 JSP servlet 和默认 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>

我们不能依靠 * 。Jsp servlet 的标准映射上的 jsp url-pattern,因为在检查任何扩展映射之前,路径模式‘/*’是匹配的。将 jsp servlet 映射到更深的文件夹意味着它首先匹配。在路径模式匹配之前完全匹配“/Favicon.ico”。更深层的路径匹配可以工作,或者精确匹配,但是没有扩展匹配可以超过“/*”路径匹配。将’/’映射到默认 servlet 似乎不起作用。您可能会认为,在 Springapp 上,精确的“/”会打败“/*”路径模式。

上面的过滤器解决方案不适用于来自应用程序的转发/包含的 JSP 请求。为了让它工作,我必须直接将过滤器应用到 Springapp,这时 URL 模式匹配就没有用了,因为所有发送到应用程序的请求都会转到它的过滤器。因此,我在过滤器中添加了模式匹配,然后学习了“ jsp”servlet,发现它不像默认 servlet 那样删除路径前缀。这解决了我的问题,虽然不完全一样,但也足够普遍。

我想到了一个稍微不同的解决方案,有点像黑客,但是这里是地图:

<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>

这基本上只是通过扩展将所有内容文件映射到默认的 servlet,并将其他所有内容映射到“ myAppServlet”。

它在 Jetty 和 Tomcat 都适用。

从上面的示例信息判断,我认为整篇文章都是基于 Tomcat 6.0.29及更早版本中的一个 bug 行为。参见 https://issues.apache.org/bugzilla/show_bug.cgi?id=50026。升级到 Tomcat 6.0.30后,应该合并(Tomcat | Jetty)之间的行为。

试试这个

<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>

编辑: 这只对 servlet 2.5规范及以上版本有效。

静态资源 servlet 的抽象模板

部分基于2007年的 这个博客,这是一个现代化的、高度可重用的 servlet 抽象模板,它可以很好地处理缓存、 ETagIf-None-MatchIf-Modified-Since(但是不支持 Gzip 和 Range,只是为了保持简单,Gzip 可以通过过滤器或容器配置完成)。

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));
}
}
}


}

与下面表示静态资源的接口一起使用。

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;


}

您所需要的只是从给定的抽象 servlet 进行扩展,并根据 javadoc 实现 getStaticResource()方法。

从文件系统服务的具体例子:

下面是一个具体的例子,它通过本地磁盘文件系统中的一个 URL (如 /files/foo.ext)提供服务:

@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();
}
};
}


}

数据库服务的具体例子:

下面是一个具体的例子,它通过一个类似 /files/foo.ext的 URL 从数据库中提供服务,通过一个 EJB 服务调用返回具有 byte[] content属性的实体:

@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();
}
};
}


}

检查 Tomcat8.x: 如果根 servlet 映射到“”,静态资源工作正常。 对于 servlet 3. x,它可以由 @WebServlet("")完成