Вопрос-ответ

Servlet for serving static content

Сервлет для обслуживания статического содержимого

Я развертываю веб-приложение в двух разных контейнерах (Tomcat и Jetty), но их сервлеты по умолчанию для обслуживания статического контента имеют другой способ обработки структуры URL, которую я хочу использовать (подробности).

Поэтому я хочу включить в веб-приложение небольшой сервлет для обслуживания собственного статического контента (изображений, CSS и т.д.). Сервлет должен обладать следующими свойствами:


  • Никаких внешних зависимостей

  • Простой и надежный

  • Поддержка If-Modified-Since заголовка (т.е. пользовательского getLastModified метода)

  • (Необязательно) поддержка кодировки gzip, etags,...

Доступен ли где-нибудь такой сервлет? Ближайший, который я могу найти, - это пример 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 папке).

Переведено автоматически
Ответ 1

Я придумал немного другое решение. Это немного халтурно, но вот сопоставление:

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

Ответ 2

В этом случае нет необходимости в полностью пользовательской реализации сервлета по умолчанию, вы можете использовать этот простой сервлет для переноса запроса в реализацию контейнера:


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);
}
}
Ответ 3

У меня были хорошие результаты с файловым сервлетом, поскольку он поддерживает практически все HTTP (etags, разбиение на блоки и т.д.).

Ответ 4

Абстрактный шаблон для сервлета статического ресурса

Частично основанный на этом блоге 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>&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;

}

Все, что вам нужно, это просто расширить данный абстрактный сервлет и реализовать 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();
}
};
}

}
java jsp servlets jakarta-ee