侧边栏壁纸
博主头像
落叶人生博主等级

走进秋风,寻找秋天的落叶

  • 累计撰写 130562 篇文章
  • 累计创建 28 个标签
  • 累计收到 9 条评论
标签搜索

目 录CONTENT

文章目录

以Jar形式为Web项目提供资源文件(JS、CSS与图片)

2022-06-22 星期三 / 0 评论 / 0 点赞 / 56 阅读 / 20392 字

一、背景 最近正在编写TagLib,在开发的过程中遇到一个资源文件引用问题。因为我开发的TagLib最终是以Jar包的形式提供给项目来用的,所以Jar包中必须包含我开发TagLib所需的JS、CS

一、背景

最近正在编写TagLib,在开发的过程中遇到一个资源文件引用问题。因为我开发的TagLib最终是以Jar包的形式提供给项目来用的,所以Jar包中必须包含我开发TagLib所需的JS、CSS与图片等资源。问题就是Tag是在项目的Web工程中运行,如何访问到jar中的资源。

二、分析

我想了一下,应该有两种方式:

1、把我需要的JS、CSS与图片等资源copy到Web工程中。

    好处:

  • 通过原生的Web服务器来访问,速度与性能上讲会好一些。

    缺点:

  • Web工程必须以目录方式部署。(非war)
  • 存放资源的目录名需要与Web工程明确约定。(防止对原Web项目文件进行覆盖)

2、通过程序采用流的方式读取Jar中的资源流再输出到页面流。

    好处:

  • 不依赖Web工程的部署方式。
  • 不会复制文件到Web工程。

    缺点:

  • 以流的方式实时从Jar中读取。(速度与性能上讲并非最优)
  • 页面流输出时需要指定内容类型Content-Type。(前者会由Web服务器来维护)

三、分析结果

最终我准备将1、2两种情况接合使用,默认会采用1复制文件到Web工程的方式。如果发现Web工程无法复制文件则采用2流读取方式。

四、核心代码开发(Jar端)

为了进行两种方式的切换定义一个Filter非常适合,可以拦截请求干扰行为,又可以在init初始化时进行资源文件的复制。

从下面的目录结构可以看出主程序就是一个Filter类,org.noahx.jarresource.resource包下的内容就是我需要的资源目录。Filter会自动判断Web工程的部署方式(目录与War)来决定复制资源目录还是直接流读取。

1、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。

2、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第三方类包

五、核心代码开发(Web端)

作为Jar文件的使用端,只需要在web.xml中配置一个filter,就可以访问到Jar中的资源。

1、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工程下创建资源目录(目录部署)。

2、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工程中所提供的资源。(下图为显示效果)

3、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>

六、Web工程两种模式的Filter日志

1、目录部署方式

[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资源文件的过程

2、War包部署方式

[JAR-RES] 2013-06-26 13:12:25,287 DEBUG [org.noahx.jarresource.TagLibResourceFilter] - ResourceMode:Jar (TagLibResourceFilter.java:111)
从Jar中直接读取,并没有copy资源的过程。

七、总结

这个Filter很好的解决了我在开发TagLib时遇到的资源引用问题,对我来说应该够用了。

我们项目中一般采用目录方式部署,我也更希望通过Web服务器来直接访问资源。

并没有采用maven来组织资源,因为我需要提供给非maven工程使用。

一些流行的mvc中也有类似的手法,可能是采用流方式读取(猜测),感兴趣的朋友可以查看这些mvc的代码。

八、源程序下载

下载包中提供了源代码以及打包后(target目录)的工程。

http://sdrv.ms/11Mp5gF

广告 广告

评论区