静的コンテンツを提供するためのサーブレット
質問
2 つの異なるコンテナー (Tomcat と Jetty) に Web アプリをデプロイしますが、静的コンテンツを提供するためのデフォルトのサーブレットでは、使用したい URL 構造を処理する方法が異なります (詳細).
したがって、独自の静的コンテンツ (画像、CSS など) を提供するために、Web アプリに小さなサーブレットを含めることを検討しています。サーブレットには次のプロパティが必要です。
- 外部依存関係なし
- シンプルで信頼性の高い
- へのサポート
If-Modified-Since
ヘッダー (すなわち、カスタムgetLastModified
方法) - (オプション) gzip エンコーディング、etag などのサポート
そのようなサーブレットはどこかにありますか?私が見つけることができる最も近いのは 例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>
したがって、すべてのリクエストは、 static
パス。問題は、Tomcat のデフォルトのサーブレットは ServletPath を考慮しない (つまり、メイン フォルダー内の静的ファイルを検索する) のに対し、Jetty は考慮する (つまり、 static
フォルダ)。
解決 5
結局自分で巻きました StaticServlet
. 。サポートします If-Modified-Since
, 、gzip エンコーディングを使用しており、war ファイルからの静的ファイルも提供できるはずです。これはそれほど難しいコードではありませんが、完全に簡単なコードでもありません。
コードは次の場所から入手できます。 静的サーブレット.java. 。お気軽にコメントしてください。
アップデート: クルラムはそれについて尋ねます ServletUtils
で参照されるクラス StaticServlet
. 。これは、私のプロジェクトで使用した補助メソッドを備えた単なるクラスです。必要な唯一の方法は、 coalesce
(これは SQL 関数と同じです COALESCE
)。これはコードです:
public static <T> T coalesce(T...ts) {
for(T t: ts)
if(t != null)
return t;
return null;
}
他のヒント
少し異なる解決策を思いつきました。少しハックっぽいですが、マッピングは次のとおりです。
<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>
これは基本的に、拡張子によってすべてのコンテンツ ファイルをデフォルトのサーブレットにマップし、その他すべてを「myAppServlet」にマップするだけです。
Jetty と Tomcat の両方で動作します。
この場合、デフォルトのサーブレットを完全にカスタム実装する必要はありません。この単純なサーブレットを使用してリクエストをコンテナの実装にラップできます。
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);
}
}
良い結果が出ました ファイルサーブレット, ほぼすべての HTTP (etag、チャンキングなど) をサポートしているためです。
静的リソース サーブレットの抽象テンプレート
部分的には以下に基づいています このブログ 2007 年以降、キャッシュを適切に処理するサーブレット用の最新化され再利用性の高い抽象テンプレートがここにあります。 ETag
, If-None-Match
そして If-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><mime-type></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;
}
必要なのは、指定された抽象サーブレットを拡張して、 getStaticResource()
javadoc によるメソッド。
ファイル システムから提供する具体的な例:
以下は、次のような 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();
}
};
}
}
データベースから提供する具体的な例:
以下は、次のような URL を介してサービスを提供する具体的な例です。 /files/foo.ext
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();
}
};
}
}
上記の情報例から判断すると、この記事全体は Tomcat 6.0.29 以前のバグのある動作に基づいていると思います。見る 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>
編集:これは、サーブレット 2.5 仕様以降でのみ有効です。
私も同じ問題を抱えていましたが、Tomcat コードベースの「デフォルト サーブレット」のコードを使用して解決しました。
http://svn.apache.org/repos/asf/tomcat/trunk/java/org/apache/catalina/servlets/DefaultServlet.java
の デフォルトのサーブレット Tomcat で静的リソース (jpg、html、css、gif など) を提供するサーブレットです。
このサーブレットは非常に効率的で、上で定義したいくつかのプロパティを備えています。
このソース コードは、不要な機能や依存関係を開始して削除するのに適した方法だと思います。
- org.apache.naming.resources パッケージへの参照は、削除するか、java.io.File コードに置き換えることができます。
- org.apache.catalina.util パッケージへの参照は、おそらくソース コード内で複製できるユーティリティ メソッド/クラスのみです。
- org.apache.catalina.Globals クラスへの参照は、インライン化または削除できます。
いくつかの回避策についての素晴らしいチュートリアルをウェブ上で見つけました。これはシンプルで効率的で、REST URL スタイルのアプローチを使用していくつかのプロジェクトで使用しました。
http://www.kuligowski.pl/java/rest-style-urls-and-url-mapping-for-static-content-apache-tomcat,5
Tomcatを拡張してこれを行いました デフォルトのサーブレット (送信元) そして 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-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>
Spring アプリ、/favicon.ico、および Spring の AbstractUrlBasedView が要求する /WEB-INF/jsp/* の JSP ファイルからのすべてのリクエストを処理するには、jsp サーブレットとデフォルトのサーブレットを再マップするだけです。
<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 サーブレットの標準マッピングの *.jsp URL パターンに依存することはできません。JSP サーブレットをより深いフォルダーにマッピングすると、それが最初に一致することを意味します。「/favicon.ico」の一致は、パス パターンの一致の前に正確に行われます。より深いパス一致または完全一致は機能しますが、「/*」パス一致を超える拡張子一致はありません。「/」をデフォルトのサーブレットにマッピングすると機能しないようです。正確な '/' は springapp の '/*' パス パターンよりも優れていると思われるでしょう。
上記のフィルター ソリューションは、アプリケーションから転送/組み込まれた JSP リクエストに対しては機能しません。これを機能させるには、フィルターを springapp に直接適用する必要がありましたが、その時点では、アプリケーションに送信されるすべてのリクエストもそのフィルターに送信されるため、URL パターンのマッチングは役に立ちませんでした。そこで、フィルターにパターン マッチングを追加し、「jsp」サーブレットについて学び、デフォルトのサーブレットのようにパス プレフィックスを削除しないことを確認しました。これで私の問題は解決しました。まったく同じではありませんでしたが、十分に一般的なものでした。
Tomcat 8.x を確認しました:ルート サーブレットが "" にマップされている場合、静的リソースは正常に動作します。サーブレット 3.x の場合は次のように実行できます。 @WebServlet("")
org.mortbay.jetty.handler.ContextHandler を使用します。StaticServlet などの追加コンポーネントは必要ありません。
桟橋のホームで、
$ cd コンテキスト
$ cp javadoc.xml static.xml
$ vi 静的.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>
contextPath の値を URL プレフィックスで設定し、resourceBase の値を静的コンテンツのファイル パスとして設定します。
それは私にとってはうまくいきました。
JSOS の StaticFile を参照してください。 http://www.servletitsuite.com/servlets/staticfile.htm