Версия для печати темы
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум программистов > Java EE (J2EE) и Spring > Хранение ресурсов web-приложения в jar'e


Автор: Maksym 11.2.2007, 12:09
Собственно, сабж.
Хочется запаковать все ресурсы web-приложения в jar и положить этот jar в dependencies нескольких проектов с одинаковой идеей интерфейса.
Подскажите, пожалуйста, можно ли в jsp сослаться на css или изображение, спрятанное в jar'e, и если можно то как?
ЗЫ.
Используется ли такая практика? или, может быть, есть другие приемы хранения и предоставления в web-интерфейсы общих ресурсов?

Автор: MisterCleric 12.2.2007, 13:09
Собственно можно. Как универсально сделать не подскажу. Но покажу пример. 
Я использую webwork. В его jar'e запакован FCKeditor он лежит по таком пути: webwork-2.2.2.jar\com\opensymphony\webwork\static\richtexteditor\

на странице к его js & css обращаюсь так:
 <script language="JavaScript" type="text/javascript"
        src="<%=request.getContextPath()%>/webwork/richtexteditor/fckeditor.js"></script>

Как я понимаю путь ищеться от: 
<filter>
        <filter-name>webwork</filter-name>
        <filter-class>com.opensymphony.webwork.dispatcher.FilterDispatcher</filter-class>
</filter>

В общем и все. Других вариантов не пробовал. И как-то мысли даже не было. 
Пробуй

Автор: Maksym 12.2.2007, 14:16
MisterCleric
Большое спасибо. Формат не очень нравится (<%=request.getContextPath()%>/webwork/richtexteditor/fckeditor.js>), но конечно попробую.
All
Нет ли еще идей? 
(честно говоря я рассчитывал на прямой путь, заложенный в технологию, а не на трюк)

Автор: Stampede 12.2.2007, 15:49
Maksym, идея как бы понятна: распространять модуль в виде бинарника, в котором уже запакованы все элементы его вебного интерфейса. Подтыкнул к проекту - и сразу пользуйся.

Может, и не лишено смысла. Реализовать тоже можно, и даже не особенно трудно. Щас раскажу как. Мне видятся два пути. Оба связаны с написанием кастомного сервлета.

1. Сервлет, выдающий содержимое архива.

Напишем сервлет, который, скажем, по адресу "/archive/yourfile.jar/img/button.gif" понимает, что нужно пойти в стандартную папку WEB-INF/lib, найти там yourfile.jar, открыть его как архив, прочитать содержимое файла img/button.gif и выдать его в поток вывода.

2. Сервлет, выдающий ресурсы

Здесь предлагается написать сервлет, который использует метод Class.getResourceAsStream(String path). Опять-таки, надо продумать шаблон пути, который будет однозначно мапиться на даный сервлет, и часть которого будет указывать на путь внутри архива. Для все того же файла кнопки в yourfile.jar УРЛ мог бы выглядеть так: "/resources/img/buton.gif".

Теперь сравним варианты. Второй быстрее в реализации, поскольку поиск и разрешение пути к ресурсу за тебя сделает система, используя стандартный механизм лукапа загрузчиками классов. Кроме того, УРЛ'ы при этом получаются компактными, и в них не надо явно прописывать имя архива.

С другой стороны, первый сервлет может быть более универсальным, так как теоретически мы могли бы запрограммировать его таким образом, чтобы он доставал нам статику из любого архива, который мы укажем - и не обязательно лежащего на classpath. А уж как это указать - это целиком дело фантазии разработчиков. Можно задавать через параметры инициализации сервлета, можно через собственную конфигурацию, можно через часть УРЛ, а можно все вместе.

Так что дерзай. Интересно будет услышать, что и как получается.



Автор: Maksym 12.2.2007, 17:20
Stampede
Спасибо. Первый вариант кажется более интересным с точки зрения масштабируемости и реиспользования.
Удивляет отсутствие готовых решений.

Автор: Maksym 19.2.2007, 17:59
Выкроил время и написал коротенькое решение. Работает. Прошу о code-review.

Код

package test.framework.ui.utils;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class ResourcesStorageServlet extends HttpServlet {
    
    private static String RESOURCES_LOCATION = "/test/framework/ui/resources";
    
    public void init() throws ServletException {
        // TODO Initialization actions        
    }
    
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        
        InputStream is = ResourcesStorageServlet.class.getResourceAsStream(RESOURCES_LOCATION + request.getPathInfo());
        
        if (is != null) {
            try {
                OutputStream os = response.getOutputStream();
                
                int _byte = is.read(); 
                while (_byte != -1) {
                    os.write(_byte);
                    _byte = is.read();            
                }
                
                os.flush();        
                os.close();
                is.close();
            } catch (IOException exc) {
//                TODO IOException processing
            }
        } else {
//             TODO MissingResource processing
        }
        
        super.doGet(request, response);
    }
}


В пакет test.framework.ui.resources.img кладутся все общие изображения, в test.framework.ui.resources.css -- общие стили и т.п.

Все это запаковывается в framework.jar (в Eclipse удобно создать для этих целей Utility Project и подключать его к зависимым приложениям), который подключается в j2ee dependencies тех web-приложений, которые будут использовать эти ресурсы.
В web.xml этих приложений не забываем указать:
Код

...
    <servlet>
        <servlet-name>Resources Storage Servlet</servlet-name>
        <servlet-class>test.framework.ui.utils.ResourcesStorageServlet</servlet-class>
        <load-on-startup>1</load-on-startup>        
    </servlet>    
    <servlet-mapping>
        <servlet-name>Resources Storage Servlet</servlet-name>
        <url-pattern>/allresources/*</url-pattern>
    </servlet-mapping>
...


И после этого в проектах запросто можно пользоваться общими ресурсам:
Код

...
<img src="allresources/img/logo.gif" width="200" height="60" border="0" />
...


Автор: Tony 19.2.2007, 18:34
Код

        InputStream input = this.getClass().getResourceAsStream("");
        OutputStream out = response.getOutputStream();
        byte buffer [] = new byte [4096];
        int size = 0 ;
        while ((size = input.read(buffer))!=-1)
            out.write(buffer, 0, size);


A ещё можно 4ерз NIO.

Автор: Maksym 19.2.2007, 18:50
Tony
Спасибо. С буфером, конечно, будет быстрее. А что даст использование nio?

Добавлено @ 18:51 
Размер буфера 4096 чем подсказан? исходя из чего его стоит вычислять?

Автор: Tony 19.2.2007, 20:43
NIO (new IO) побыстрее работает с input/output, чем класси4еская модель in/out.
Код

Размер буфера 4096 чем подсказан? исходя из чего его стоит вычислять?

Зависит от коли4ества запросов к ресурсу. Если мало запросов и маленкие файлики(цсс,иконки...) то можно сразу весь файл загрузить в массив и выдать его, не исползуя for (как в примере выше).
Нас4ёт NIO.(вариант 2)
Код

        ReadableByteChannel in = Channels.newChannel(this.getClass().getResourceAsStream(""));
        WritableByteChannel out = Channels.newChannel(response.getOutputStream());
                ByteBuffer buffer = ByteBuffer.allocate(4096); 
        while (in.read(buffer)!=-1){
            out.write(buffer);
            buffer.flip();
         }
        in.close();

 

Автор: Maksym 19.2.2007, 22:34
Tony
Apache IO API предлагает сделать все вызовом одного метода -- http://jakarta.apache.org/commons/io/api-release/org/apache/commons/io/IOUtils.html (вариант 3)
Какой способ эффективнее? Все таки речь идет о многократно повторяющейся операции smile 

Автор: Tony 19.2.2007, 23:42
Да знаю 4то есть у Jakarti. Если ты хо4ешь ложить либу ради одного метода флаг в руки. Нас4ёт производительности надо сорсы смотреть. Сорри нету времени(завтра).

Автор: Maksym 24.2.2007, 16:58
Tony
Исходник org.apache.commons.io.IOUtils.copy(InputStream input, OutputStream output):
Код

    /**
     * The default buffer size to use.
     */
    private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;

   /**
     * Copy bytes from an <code>InputStream</code> to an
     * <code>OutputStream</code>.
     * <p>
     * This method buffers the input internally, so there is no need to use a
     * <code>BufferedInputStream</code>.
     * <p>
     * Large streams (over 2GB) will return a bytes copied value of
     * <code>-1</code> after the copy has completed since the correct
     * number of bytes cannot be returned as an int. For large streams
     * use the <code>copyLarge(InputStream, OutputStream)</code> method.
     * 
     * @param input  the <code>InputStream</code> to read from
     * @param output  the <code>OutputStream</code> to write to
     * @return the number of bytes copied
     * @throws NullPointerException if the input or output is null
     * @throws IOException if an I/O error occurs
     * @throws ArithmeticException if the byte count is too large
     * @since Commons IO 1.1
     */
    public static int copy(InputStream input, OutputStream output) throws IOException {
        long count = copyLarge(input, output);
        if (count > Integer.MAX_VALUE) {
            return -1;
        }
        return (int) count;
    }

    /**
     * Copy bytes from a large (over 2GB) <code>InputStream</code> to an
     * <code>OutputStream</code>.
     * <p>
     * This method buffers the input internally, so there is no need to use a
     * <code>BufferedInputStream</code>.
     * 
     * @param input  the <code>InputStream</code> to read from
     * @param output  the <code>OutputStream</code> to write to
     * @return the number of bytes copied
     * @throws NullPointerException if the input or output is null
     * @throws IOException if an I/O error occurs
     * @since Commons IO 1.3
     */
    public static long copyLarge(InputStream input, OutputStream output)
            throws IOException {
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        long count = 0;
        int n = 0;
        while (-1 != (n = input.read(buffer))) {
            output.write(buffer, 0, n);
            count += n;
        }
        return count;
    }

Разницы с твоим вариантом для io, кажется, нет. Дополнительно вычисляется никому не нужный count (the number of bytes copied) и все.

All
Не работал с nio. Действительно ли его предпочтительнее изпользовать в подобных ситуациях? В чем заключается его оптимизация по сравнению со стандартной моделью?

Автор: Tony 24.2.2007, 20:45
Код

Не работал с nio. Действительно ли его предпочтительнее изпользовать в подобных ситуациях? В чем заключается его оптимизация по сравнению со стандартной моделью?

Полно имфы (и на русском языке).
http://www.javable.com/javaworld/04_03/01/

Автор: Maksym 20.3.2007, 20:55
Окончательный вариант (на данный момент). Используется в серии проектов с большим количеством общих ресурсов. Пока проблем не было.
Код

package test.framework.utils;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ResourcesStorageServlet extends HttpServlet {

    private static final String RESOURCES_LOCATION = "/test/framework/ui/resources";
    private static final String SERVLET_PARAM_NAME_BUFFER_SIZE = "buffer_size";

    private int bufferSize = 1024;

    public void init() throws ServletException {
        String bufferSizeStr = getServletConfig().getInitParameter(SERVLET_PARAM_NAME_BUFFER_SIZE);
        if (bufferSizeStr != null) {
            try {
                bufferSize = Integer.parseInt(bufferSizeStr);
            } catch (NumberFormatException exp) {
                UILogger.log().warn("Buffer_size param can't be read from servlet parameter. Default is using.");
            }
        }
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        String requestPath = request.getRequestURI().replaceAll(request.getContextPath(), "");

        InputStream is = ResourcesStorageServlet.class.getResourceAsStream(RESOURCES_LOCATION + requestPath);

        if (is != null) {
            try {

                OutputStream os = response.getOutputStream();
                byte buffer[] = new byte[bufferSize];
                int size = 0;
                while ((size = is.read(buffer)) != -1)
                    os.write(buffer, 0, size);
                os.flush();
                os.close();
                is.close();

            } catch (IOException exc) {
                UILogger.log().warn("IOException during resource (reading from file)/(sending to response).", exc);
            }
        } else {
            UILogger.log().warn("Missing resource: " + requestPath);
        }

        super.doGet(request, response);
    }
}
 
Размер буфера параметризуется в зависимости от приложения. Для логирования используется внешний класс UILogger (helper для использования log4j).
Интересно, что реалзиация через nio не заработала... Код молча отработал и в response все ушло, но к пользователю ничего не попало...  smile 

Автор: Tony 22.3.2007, 17:41
Код

Интересно, что реалзиация через nio не заработала... Код молча отработал и в response все ушло, но к пользователю ничего не попало...  

Вот это интерсно

Автор: Maksym 22.3.2007, 19:59
Tony
Есть ли InputStream'ы, с которыми nio работать не может? Например, если они порождаются налету внутри jvm и никак средствами операционной системы к ним не достучаться..

Автор: Maksym 18.4.2007, 00:26
По ходу использования тема получила свое логическое продолжение.
Теперь наш сервлет не только отдает common ресурсы из .jar, но и:
- пакует их gzip для браузеров, поддерживающих компрессию
- устанавливает expiration date, last modified, content length в response
В результате значительно уменшилось количество IOException'ов выпадающих из этого сервлета в лог, похоже это связано с тем, что браузеры запрашивают ресурсы меньшее количество раз, а если запрашивают то точно знают объем данных которых нужно дождаться (кроме того сам объем данных уменьшился за счет компрессии).
Код

package test.framework.utils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.GZIPOutputStream;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ResourcesStorageServlet extends HttpServlet {

    private static final String RESOURCES_LOCATION = "/test/framework/ui/resources";
    private static final String SERVLET_PARAM_NAME_BUFFER_SIZE = "buffer_size";
    
    private static final long STARTUP_TIME = System.currentTimeMillis();
    private static final long EXPIRATION_TIME = STARTUP_TIME + 2629743830l;//startup time + one month     

    private static int bufferSize = 4096;

    public void init() throws ServletException {
        String bufferSizeStr = getServletConfig().getInitParameter(SERVLET_PARAM_NAME_BUFFER_SIZE);
        if (bufferSizeStr != null) {
            try {
                bufferSize = Integer.parseInt(bufferSizeStr);
            } catch (NumberFormatException exp) {
                UILogger.log().warn("Buffer_size param can't be read from servlet parameter. Default is using.");
            }
        }
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        String requestPath = request.getRequestURI().replaceAll(request.getContextPath(), "");
        InputStream resource = ResourcesStorageServlet.class.getResourceAsStream(RESOURCES_LOCATION + requestPath);

        if (resource != null) {
            try {
                long ifModifiedSince = request.getDateHeader("If-Modified-Since");
                if (ifModifiedSince == -1 || STARTUP_TIME - ifModifiedSince > 1000) {
                 response.setHeader("Cache-Control", "private, max-age=86400");
                 response.setDateHeader("Expires", EXPIRATION_TIME);
                 response.setDateHeader("Last-Modified", STARTUP_TIME);

                 if(requestPath.endsWith(".gif"))
                     response.setContentType("image/gif");
                 else if(requestPath.endsWith(".jpeg"))
                     response.setContentType("image/jpeg");
                 else if(requestPath.endsWith(".js"))
                     response.setContentType("text/javascript");
                 else if(requestPath.endsWith(".css"))
                     response.setContentType("text/css");
                         
                 ByteArrayOutputStream output = new ByteArrayOutputStream();
                 boolean compress = request.getHeader("Accept-Encoding").indexOf("gzip") > -1;
                 if (compress) {
                     response.setHeader("Content-Encoding", "gzip");
                     GZIPOutputStream gzip = new GZIPOutputStream(output);
                     copy(resource, gzip);
                     gzip.finish();
                 } else {
                     copy(resource, output);
                 }
                 byte[] resourceBytes = output.toByteArray();

                 response.setContentLength(resourceBytes.length);
                 response.getOutputStream().write(resourceBytes);
                } else {
                 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                 response.setDateHeader("Expires", EXPIRATION_TIME);
                }
                response.flushBuffer();       
            } catch (IOException exc) {
                UILogger.log().debug("IOException during resource (reading from file)/(sending to response): " + (RESOURCES_LOCATION + requestPath), exc);
            }
        } else {
            UILogger.log().warn("Missing resource: " + requestPath);
        }
    }
    
    private static void copy(InputStream input, OutputStream output) throws IOException {
    byte[] buf = new byte[bufferSize];
    int len = 0;
    while ((len = input.read(buf)) > -1)
        output.write(buf, 0, len);
    }     
}

Автор: Stampede 18.4.2007, 01:07
Отлично, Maksym - держи плюса!

Не совсем понятна несогласованная логика выставления Expires и max-age (через месяц безостановочной работы сервера все ресурсы будут отдаваться как уже "испарившиеся"), но да ладно. Все равно полезная вещь, а подправить под себя всегда можно при желании.

Автор: Maksym 18.4.2007, 12:02
Цитата(Stampede @  18.4.2007,  01:07 Найти цитируемый пост)
Не совсем понятна несогласованная логика выставления Expires и max-age (через месяц безостановочной работы сервера все ресурсы будут отдаваться как уже "испарившиеся"), но да ладно.

Да, точно. Спасибо за замечание, исправим smile

Powered by Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)