一、背景 最近正在编写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