سؤال

أقوم بنشر تطبيق ويب على حاويتين مختلفتين (Tomcat وJetty)، لكن servlets الافتراضية الخاصة بهما لخدمة المحتوى الثابت لها طريقة مختلفة للتعامل مع بنية عنوان URL التي أرغب في استخدامها (تفاصيل).

ولذلك فإنني أتطلع إلى تضمين servlet صغير في تطبيق الويب لخدمة المحتوى الثابت الخاص به (الصور، CSS، وما إلى ذلك).يجب أن يتمتع servlet بالخصائص التالية:

  • لا تبعيات خارجية
  • بسيطة وموثوقة
  • الدعم ل If-Modified-Since رأس (أي.مخصص getLastModified طريقة)
  • (اختياري) دعم ترميز gzip، وetags،...

هل مثل هذا servlet متاح في مكان ما؟أقرب ما يمكن أن أجده هو مثال 4-10 من كتاب servlet.

تحديث: بنية عنوان 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 طريق.المشكلة هي أن servlet الافتراضي الخاص بـ Tomcat لا يأخذ ServletPath في الاعتبار (لذا فهو يبحث عن الملفات الثابتة في المجلد الرئيسي)، بينما يفعل Jetty (لذا فهو يبحث في المجلد الرئيسي) static مجلد).

هل كانت مفيدة؟

المحلول 5

انتهى بي الأمر بالتدحرج بنفسي StaticServlet.انه يدعم If-Modified-Since, وترميز gzip ويجب أن يكون قادرًا على خدمة الملفات الثابتة من ملفات الحرب أيضًا.إنها ليست تعليمات برمجية صعبة للغاية، ولكنها ليست تافهة تمامًا أيضًا.

الكود متاح: StaticServlet.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>

يقوم هذا بشكل أساسي بتعيين جميع ملفات المحتوى بالامتداد إلى servlet الافتراضي، وكل شيء آخر إلى "myAppServlet".

إنه يعمل في كل من Jetty وTomcat.

ليست هناك حاجة إلى تنفيذ مخصص تمامًا لـ 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);
    }
}

لقد حصلت على نتائج جيدة مع FileServlet, ، لأنه يدعم إلى حد كبير جميع بروتوكولات HTTP (العلامات الإلكترونية، والتقطيع، وما إلى ذلك).

قالب مجردة لServlet مورد ثابت

يعتمد جزئيا على هذه المدونة منذ عام 2007، إليك قالب تجريدي حديث وقابل لإعادة الاستخدام بدرجة كبيرة لـ servlet الذي يتعامل بشكل صحيح مع التخزين المؤقت، 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;

}

كل ما تحتاجه هو فقط التوسع من servlet الملخص المحدد وتنفيذ 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>    

يحرر:هذا صالح فقط لمواصفات servlet 2.5 وما فوق.

واجهت نفس المشكلة وقمت بحلها باستخدام كود "servlet الافتراضي" من قاعدة بيانات Tomcat.

http://svn.apache.org/repos/asf/tomcat/trunk/java/org/apache/catalina/servlets/DefaultServlet.java

ال DefaultServlet هو servlet الذي يخدم الموارد الثابتة (jpg، html، css، gif وما إلى ذلك) في Tomcat.

هذا servlet فعال للغاية ويحتوي على بعض الخصائص التي حددتها أعلاه.

أعتقد أن كود المصدر هذا هو وسيلة جيدة لبدء وإزالة الوظائف أو التبعيات التي لا تحتاج إليها.

  • يمكن إزالة المراجع إلى حزمة org.apache.naming.resources أو استبدالها بكود java.io.File.
  • من المحتمل أن تكون المراجع إلى الحزمة org.apache.catalina.util هي فقط طرق/فئات فائدة يمكن تكرارها في كود المصدر الخاص بك.
  • يمكن تضمين المراجع إلى فئة org.apache.catalina.Globals أو إزالتها.

لقد وجدت برنامجًا تعليميًا رائعًا على الويب حول بعض الحلول البديلة.إنه بسيط وفعال، وقد استخدمته في العديد من المشاريع باستخدام أسلوب REST urls:

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

لقد فعلت ذلك عن طريق تمديد القط DefaultServlet (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>  

لخدمة جميع الطلبات من تطبيق Spring بالإضافة إلى /favicon.ico وملفات JSP من /WEB-INF/jsp/* التي سيطلبها Spring's AbstractUrlBasedView، يمكنك فقط إعادة تعيين 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>

لا يمكننا الاعتماد على نمط عنوان url *.jsp في التعيين القياسي لـ jsp servlet لأن نمط المسار '/*' مطابق قبل التحقق من أي تعيين ملحق.إن تعيين jsp servlet إلى مجلد أعمق يعني أنه مطابق أولاً.تتم مطابقة "/favicon.ico" تمامًا قبل مطابقة نمط المسار.ستعمل مطابقات المسار الأعمق، أو المطابقات التامة، ولكن لا يمكن لمطابقات الامتداد أن تتجاوز مطابقة المسار '/*'.يبدو أن تعيين "/" إلى servlet الافتراضي لا يعمل.كنت تعتقد أن "/" الدقيق سيتفوق على نمط المسار "/*" في Springapp.

لا يعمل حل التصفية أعلاه مع طلبات JSP المُعاد توجيهها/المضمنة من التطبيق.ولإنجاحه، كان عليّ تطبيق الفلتر على Springapp مباشرة، وعند هذه النقطة كانت مطابقة نمط عنوان URL عديمة الفائدة حيث أن جميع الطلبات التي تذهب إلى التطبيق تذهب أيضًا إلى عوامل التصفية الخاصة به.لذلك أضفت نمطًا مطابقًا للمرشح ثم تعرفت على servlet "jsp" ورأيت أنه لا يزيل بادئة المسار كما يفعل servlet الافتراضي.لقد أدى ذلك إلى حل مشكلتي، التي لم تكن هي نفسها تمامًا ولكنها شائعة بدرجة كافية.

تم التحقق من Tomcat 8.x:تعمل الموارد الثابتة بشكل جيد إذا تم تعيين servlet الجذر إلى "".بالنسبة لـ servlet 3.x يمكن القيام بذلك عن طريق @WebServlet("")

استخدم org.mortbay.jetty.handler.ContextHandler.لا تحتاج إلى مكونات إضافية مثل StaticServlet.

في رصيف المنزل،

سياقات القرص المضغوط $

$ cp javadoc.xml static.xml

$ السادس 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>

قم بتعيين قيمة contextPath باستخدام بادئة URL الخاصة بك، وقم بتعيين قيمة ResourcesBase كمسار ملف للمحتوى الثابت.

عملت معي.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top