文档章节

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

NoahX
 NoahX
发布于 2013/06/27 00:53
字数 2129
阅读 12227
收藏 205

一、背景

最近正在编写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

© 著作权归作者所有

NoahX
粉丝 136
博文 59
码字总数 48419
作品 0
海淀
程序员
私信 提问
加载中

评论(10)

NoahX
NoahX 博主

引用来自“琴瑟琵琶”的评论

貌似资源文件放到jar文件的/META-INF/resources里 就能直接被访问吧
是的Servlet 3以上就可以,低版不行
BUG多野结衣
BUG多野结衣
貌似资源文件放到jar文件的/META-INF/resources里 就能直接被访问吧
付学良
付学良

引用来自“xautlx”的评论

Servlet3规范好像已经有标准支持:https://blogs.oracle.com/alexismp/entry/web_inf_lib_jar_meta

Spring MVC和Servlet3的确都支持,其实就是webjar的功能。不过这篇blog应该意思是不用这两个的情况下是怎么解决的吧?
EntDIY
EntDIY
Servlet3规范好像已经有标准支持:https://blogs.oracle.com/alexismp/entry/web_inf_lib_jar_meta
ahnan
ahnan
www.webjars.org
wj2699
wj2699
spring的mvc确实有这么个功能,但是如果和apache整合的时候,apache就处理不了静态资源了
10000011
10000011

引用来自“monolithic”的评论

你可以看下http://www.webjars.org/ 这个实现
常用的js等可以以jar包形式加载
<dependencies>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>

然后你的中间件、web框架以静态资源的形式读取
比如我用springmvc,只需配置如下
<mvc:resources mapping="/webjars/**" location="classpath:/META-INF/resources/webjars/"/>

在页面引入
<spring:url value="/webjars/bootstrap/2.3.0/css/bootstrap.min.css" var="bootstrapCss"/>
即可

这个不错!
x
xuningnb
你可以看下http://www.webjars.org/ 这个实现
常用的js等可以以jar包形式加载
<dependencies>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>

然后你的中间件、web框架以静态资源的形式读取
比如我用springmvc,只需配置如下
<mvc:resources mapping="/webjars/**" location="classpath:/META-INF/resources/webjars/"/>

在页面引入
<spring:url value="/webjars/bootstrap/2.3.0/css/bootstrap.min.css" var="bootstrapCss"/>
即可
大漠真人
大漠真人
收藏了~
l
lintghi
应该可以用maven overlay解决. 将打包类型由原来的jar变成war.
优点: 不用自己写代码解决 ;
缺点: 构建环境必须使用maven.
Java使用POM一JAR包的形式管理JavaScript文件-WebJars

说明:原来JS框架还可以使用POM进行管理的。WebJars是一个很神奇的东西,可以让大家以JAR包的形式来使用前端的各种框架、组件。 什么是WebJars 什么是WebJars?WebJars是将客户端(浏览器)资...

easonjim
2018/01/18
0
0
让您的 web 应用程序飞起来

本文向您介绍如何通过在您的 CSS 和 JavaScript 文件中 — 两种易于优化的常见资源,使用社区中提供的工具即可完成优化 — 优化空间使用来实现更高的性能。然而,在继续之前,有一点是很重要...

IBMdW
2011/09/21
2.7K
2
Spring Boot + WebJars 整合运用

Spring Boot + WebJars 整合运用 #### 示例:SpringBoot 第十八章:web应用开发之WebJars使用 WebJars(https://www.webjars.org/)介绍及使用  什么是WebJars  为什么使用  原理 如何使用 ...

近在咫尺远在天涯
05/06
49
0
前端性能优化指南

在“A Node.JS Holiday Season”系列丛书的这一章中,我们将会讨论一些前端性能和介绍一些基于Mozilla的快速开发工具。 我们将要使用前段性能自动化中非常重要的工具之一的connect-cachify。...

Jeky
2013/01/17
5K
4
Webpack 4 构建大型项目实践 / 处理图片、样式和字体

本文所用示例的仓库地址: gayhub 上文使用 生成了一个 文件,并且插件自动把打包后的资源添加到 文件中,使我们可以打开 在浏览器看到 js 的执行效果。本节我们将用 Webpack Loaders 来处理...

莫得盐
06/26
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Mybatis Plus删除

/** @author beth @data 2019-10-17 00:30 */ @RunWith(SpringRunner.class) @SpringBootTest public class DeleteTest { @Autowired private UserInfoMapper userInfoMapper; /** 根据id删除......

一个yuanbeth
今天
4
0
总结

一、设计模式 简单工厂:一个简单而且比较杂的工厂,可以创建任何对象给你 复杂工厂:先创建一种基础类型的工厂接口,然后各自集成实现这个接口,但是每个工厂都是这个基础类的扩展分类,spr...

BobwithB
今天
5
0
java内存模型

前言 Java作为一种面向对象的,跨平台语言,其对象、内存等一直是比较难的知识点。而且很多概念的名称看起来又那么相似,很多人会傻傻分不清楚。比如本文我们要讨论的JVM内存结构、Java内存模...

ls_cherish
今天
4
0
友元函数强制转换

友元函数强制转换 p522

天王盖地虎626
昨天
5
0
js中实现页面跳转(返回前一页、后一页)

本文转载于:专业的前端网站➸js中实现页面跳转(返回前一页、后一页) 一:JS 重载页面,本地刷新,返回上一页 复制代码代码如下: <a href="javascript:history.go(-1)">返回上一页</a> <a h...

前端老手
昨天
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部