以Jar形式爲Web項目提供資源文件(JS、CSS與圖片)

1、背景

最近正在編寫TagLib,在開發的過程當中遇到一個資源文件引用問題。由於我開發的TagLib最終是以Jar包的形式提供給項目來用的,因此Jar包中必須包含我開發TagLib所需的JS、CSS與圖片等資源。問題就是Tag是在項目的Web工程中運行,如何訪問到jar中的資源。 javascript

2、分析

我想了一下,應該有兩種方式: css

一、把我須要的JS、CSS與圖片等資源copy到Web工程中。

    好處: html

  • 經過原生的Web服務器來訪問,速度與性能上講會好一些。

    缺點: java

  • Web工程必須以目錄方式部署。(非war)
  • 存放資源的目錄名須要與Web工程明確約定。(防止對原Web項目文件進行覆蓋)

二、經過程序採用流的方式讀取Jar中的資源流再輸出到頁面流。

    好處: web

  • 不依賴Web工程的部署方式。
  • 不會複製文件到Web工程。

    缺點: shell

  • 以流的方式實時從Jar中讀取。(速度與性能上講並不是最優)
  • 頁面流輸出時須要指定內容類型Content-Type。(前者會由Web服務器來維護)

3、分析結果

最終我準備將一、2兩種狀況接合使用,默認會採用1複製文件到Web工程的方式。若是發現Web工程沒法複製文件則採用2流讀取方式。 apache

4、核心代碼開發(Jar端)

爲了進行兩種方式的切換定義一個Filter很是適合,能夠攔截請求干擾行爲,又能夠在init初始化時進行資源文件的複製。 api

從下面的目錄結構能夠看出主程序就是一個Filter類,org.noahx.jarresource.resource包下的內容就是我須要的資源目錄。Filter會自動判斷Web工程的部署方式(目錄與War)來決定複製資源目錄仍是直接流讀取。 安全

一、org.noahx.jarresource.TagLibResourceFilter(程序內邏輯詳見註釋)

package org.noahx.jarresource;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sun.net.www.protocol.file.FileURLConnection;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * Created with IntelliJ IDEA.
 * User: noah
 * Date: 6/24/13
 * Time: 8:18 PM
 * To change this template use File | Settings | File Templates.
 */
public class TagLibResourceFilter implements Filter {

    private static final String RESOURCE_PACKAGE_PATH = "/org/noahx/jarresource/resource";

    private static final String RESOURCE_CHARSET = "UTF-8";

    private static final String DEFAULT_MINE_TYPE = "application/octet-stream";

    private static String resourcePath;

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private ResourceMode resourceMode;

    private static enum ResourceMode {
        Dir, Jar
    }

    private static final Map<String, String> MINE_TYPE_MAP;

    static {
        MINE_TYPE_MAP = new HashMap<String, String>();
        MINE_TYPE_MAP.put("js", "application/javascript;charset=" + RESOURCE_CHARSET);
        MINE_TYPE_MAP.put("css", "text/css;charset=" + RESOURCE_CHARSET);
        MINE_TYPE_MAP.put("gif", "image/gif");
        MINE_TYPE_MAP.put("jpg", "image/jpeg");
        MINE_TYPE_MAP.put("jpeg", "image/jpeg");
        MINE_TYPE_MAP.put("png", "image/png");

    }

    public static String getResourcePath() {
        return TagLibResourceFilter.resourcePath;
    }

    private static void setResourcePath(String resourcePath) {
        TagLibResourceFilter.resourcePath = resourcePath;
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        String resPath = filterConfig.getInitParameter("resourcePath");

        if (!resPath.startsWith("/")) {
            resPath = "/" + resPath;
        }

        setResourcePath(resPath);

        String rootPath = filterConfig.getServletContext().getRealPath("/");
        if (rootPath != null) {   //若是web工程是目錄方式運行

            String dirPath = filterConfig.getServletContext().getRealPath(resPath);
            File dir = null;
            try {
                dir = new File(dirPath);
                FileUtils.deleteQuietly(dir); //清除老資源
                FileUtils.forceMkdir(dir);   //從新建立資源目錄

                if(logger.isDebugEnabled()){
                    logger.debug("create dir '"+dirPath+"'");
                }
            } catch (Exception e) {
                logger.error("Error creating TagLib Resource dir", e);
            }

            try {
                copyResourcesRecursively(this.getClass().getResource(RESOURCE_PACKAGE_PATH), dir); //複製classpath中的資源到目錄
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            }

            resourceMode = ResourceMode.Dir;   //設置爲目錄模式
        } else {
            resourceMode = ResourceMode.Jar;    //設置爲jar包模式
        }

        if(logger.isDebugEnabled()){
            logger.debug("ResourceMode:"+resourceMode);
        }
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        switch (resourceMode) {
            case Dir:
                chain.doFilter(request, response);
                break;
            case Jar: {
                HttpServletRequest req = (HttpServletRequest) request;
                String path = req.getRequestURI().substring(req.getContextPath().length());  //uri去掉web上下文

                HttpServletResponse rep = (HttpServletResponse) response;

                if (path.startsWith(getResourcePath() + "/")) {  //resourcePath必須與url-pattern一致

                    path = path.substring(getResourcePath().length());     //uri去掉resourcePath

                    try {
                        URL resource = this.getClass().getResource(RESOURCE_PACKAGE_PATH + path);    //可能存在潛在安全問題
                        if (resource == null) {      //若是在類路徑中沒有找到資源->404
                            rep.sendError(HttpServletResponse.SC_NOT_FOUND);
                        } else {
                            InputStream inputStream = readResource(resource);
                            if (inputStream != null) {  //有inputstream說明已經讀到jar中內容
                                String ext = FilenameUtils.getExtension(path).toLowerCase();
                                String contentType = MINE_TYPE_MAP.get(ext);
                                if (contentType == null) {
                                    contentType = DEFAULT_MINE_TYPE;
                                }
                                rep.setContentType(contentType);    //設置內容類型

                                ServletOutputStream outputStream = rep.getOutputStream();
                                try {
                                    int size = IOUtils.copy(inputStream, outputStream);  //向輸出流輸出內容
                                    rep.setContentLength(size);
                                } finally {
                                    IOUtils.closeQuietly(inputStream);
                                    IOUtils.closeQuietly(outputStream);
                                }
                            } else {   //沒有inputstream->404
                                rep.sendError(HttpServletResponse.SC_NOT_FOUND);
                            }
                        }

                    } catch (Exception e) {
                        logger.error(e.getMessage(), e);
                        rep.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                    }
                } else {
                    logger.error("MUST set url-pattern=\"" + resourcePath + "/*\"!!");
                    rep.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                }

            }
            break;
        }


    }

    @Override
    public void destroy() {
    }


    private InputStream readResource(URL originUrl) throws Exception {
        InputStream inputStream = null;
        URLConnection urlConnection = originUrl.openConnection();
        if (urlConnection instanceof JarURLConnection) {
            inputStream = readJarResource((JarURLConnection) urlConnection);
        } else if (urlConnection instanceof FileURLConnection) {
            File originFile = new File(originUrl.getPath());
            if (originFile.isFile()) {
                inputStream = originUrl.openStream();
            }
        } else {
            throw new Exception("URLConnection[" + urlConnection.getClass().getSimpleName() +
                    "] is not a recognized/implemented connection type.");
        }

        return inputStream;
    }

    private InputStream readJarResource(JarURLConnection jarConnection) throws Exception {
        InputStream inputStream = null;
        JarFile jarFile = jarConnection.getJarFile();
        if (!jarConnection.getJarEntry().isDirectory()) { //若是jar中內容爲目錄則不返回inputstream
            inputStream = jarFile.getInputStream(jarConnection.getJarEntry());
        }
        return inputStream;
    }

    private void copyResourcesRecursively(URL originUrl, File destination) throws Exception {

        URLConnection urlConnection = originUrl.openConnection();
        if (urlConnection instanceof JarURLConnection) {
            copyJarResourcesRecursively(destination, (JarURLConnection) urlConnection);
        } else if (urlConnection instanceof FileURLConnection) {
            FileUtils.copyDirectory(new File(originUrl.getPath()), destination); //若是不是jar則採用目錄copy
            if(logger.isDebugEnabled()){
                logger.debug("copy dir '"+originUrl.getPath()+"' --> '"+destination.getPath()+"'");
            }
        } else {
            throw new Exception("URLConnection[" + urlConnection.getClass().getSimpleName() +
                    "] is not a recognized/implemented connection type.");
        }
    }

    private void copyJarResourcesRecursively(File destination, JarURLConnection jarConnection) throws IOException {
        JarFile jarFile = jarConnection.getJarFile();
        Enumeration<JarEntry> entries = jarFile.entries();
        while (entries.hasMoreElements()) {    //遍歷jar內容逐個copy
            JarEntry entry = entries.nextElement();
            if (entry.getName().startsWith(jarConnection.getEntryName())) {
                String fileName = StringUtils.removeStart(entry.getName(), jarConnection.getEntryName());
                File destFile = new File(destination, fileName);
                if (!entry.isDirectory()) {
                    InputStream entryInputStream = jarFile.getInputStream(entry);
                    FileUtils.copyInputStreamToFile(entryInputStream, destFile);
                    if(logger.isDebugEnabled()){
                        logger.debug("copy jarfile to file '"+entry.getName()+"' --> '"+destination.getPath()+"'");
                    }
                } else {
                    FileUtils.forceMkdir(destFile);
                    if(logger.isDebugEnabled()){
                        logger.debug("create dir '"+destFile.getPath()+"'");
                    }
                }
            }
        }
    }
}

補充:Filter中提供了靜態方法getResourcePath()來得到當前的資源路徑,個人TagLib中就能夠經過該方法得到資源URI。 服務器

二、pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.noahx.jarresource</groupId>
    <artifactId>resource</artifactId>
    <version>1.0-SNAPSHOT</version>

    <packaging>jar</packaging>

    <dependencies>

        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.4</version>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.5</version>
        </dependency>

        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
        </dependency>

    </dependencies>
</project>

使用了commons-io與commons-lang第三方類包

5、核心代碼開發(Web端)

做爲Jar文件的使用端,只須要在web.xml中配置一個filter,就能夠訪問到Jar中的資源。

一、web.xml

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
	      http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         version="2.5">
    <display-name>jar resource web</display-name>

    <filter>
        <filter-name>tagLibResourceFilter</filter-name>
        <filter-class>org.noahx.jarresource.TagLibResourceFilter</filter-class>
        <init-param>
            <param-name>resourcePath</param-name>
            <param-value>/tagres</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>tagLibResourceFilter</filter-name>
        <url-pattern>/tagres/*</url-pattern>
    </filter-mapping>

    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>
注意:因爲Servlet 3.0如下沒法經過程序得到url-pattern,因此在filter的參數中指定了一個同名路徑來使用。filter會用這個路徑名稱在Web工程下建立資源目錄(目錄部署)。

二、index.jsp(資源使用樣例,JS、CSS與圖片)

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title></title>
    <link rel="stylesheet" type="text/css" href="tagres/css.css" />
    <script  type="text/javascript" src="tagres/example.js"></script>
</head>
<body>
     <img src="tagres/imgs/star-hover4.png" />star-hover4.png<br/>
     <button onclick="example();" >example.js (example)</button><br/>
     <div class="redbox">css.css redbox</div>
</body>
</html>

tagres/中的內容就是Jar工程中所提供的資源。(下圖爲顯示效果)

三、pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.noahx.jarresource</groupId>
    <artifactId>web</artifactId>
    <version>1.0-SNAPSHOT</version>

    <packaging>war</packaging>

    <dependencies>
        <dependency>
            <groupId>org.noahx.jarresource</groupId>
            <artifactId>resource</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.5</version>
            <scope>runtime</scope>
        </dependency>

    </dependencies>

</project>

6、Web工程兩種模式的Filter日誌

一、目錄部署方式

[JAR-RES] 2013-06-26 13:11:13,132 DEBUG [org.noahx.jarresource.TagLibResourceFilter] - create dir '/nautilus/develop/jar-resource/web/target/web-1.0-SNAPSHOT/tagres' (TagLibResourceFilter.java:93)
[JAR-RES] 2013-06-26 13:11:13,146 DEBUG [org.noahx.jarresource.TagLibResourceFilter] - create dir '/nautilus/develop/jar-resource/web/target/web-1.0-SNAPSHOT/tagres/' (TagLibResourceFilter.java:240)
[JAR-RES] 2013-06-26 13:11:13,147 DEBUG [org.noahx.jarresource.TagLibResourceFilter] - create dir '/nautilus/develop/jar-resource/web/target/web-1.0-SNAPSHOT/tagres/imgs' (TagLibResourceFilter.java:240)
[JAR-RES] 2013-06-26 13:11:13,152 DEBUG [org.noahx.jarresource.TagLibResourceFilter] - copy jarfile to file 'org/noahx/jarresource/resource/imgs/star-hover4.png' --> '/nautilus/develop/jar-resource/web/target/web-1.0-SNAPSHOT/tagres' (TagLibResourceFilter.java:235)
[JAR-RES] 2013-06-26 13:11:13,153 DEBUG [org.noahx.jarresource.TagLibResourceFilter] - copy jarfile to file 'org/noahx/jarresource/resource/example.js' --> '/nautilus/develop/jar-resource/web/target/web-1.0-SNAPSHOT/tagres' (TagLibResourceFilter.java:235)
[JAR-RES] 2013-06-26 13:11:13,154 DEBUG [org.noahx.jarresource.TagLibResourceFilter] - copy jarfile to file 'org/noahx/jarresource/resource/css.css' --> '/nautilus/develop/jar-resource/web/target/web-1.0-SNAPSHOT/tagres' (TagLibResourceFilter.java:235)
[JAR-RES] 2013-06-26 13:11:13,154 DEBUG [org.noahx.jarresource.TagLibResourceFilter] - ResourceMode:Dir (TagLibResourceFilter.java:111)

能夠看到copy資源文件的過程

二、War包部署方式

[JAR-RES] 2013-06-26 13:12:25,287 DEBUG [org.noahx.jarresource.TagLibResourceFilter] - ResourceMode:Jar (TagLibResourceFilter.java:111)
從Jar中直接讀取,並無copy資源的過程。

7、總結

這個Filter很好的解決了我在開發TagLib時遇到的資源引用問題,對我來講應該夠用了。

咱們項目中通常採用目錄方式部署,我也更但願經過Web服務器來直接訪問資源。

並無採用maven來組織資源,由於我須要提供給非maven工程使用。

一些流行的mvc中也有相似的手法,多是採用流方式讀取(猜想),感興趣的朋友能夠查看這些mvc的代碼。

8、源程序下載

下載包中提供了源代碼以及打包後(target目錄)的工程。

http://sdrv.ms/11Mp5gF

相關文章
相關標籤/搜索