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

一、背景

最近正在编写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(程序内逻辑详见注释)
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
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
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<? 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
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
< 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与图片)
?
1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ 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
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<? 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、目录部署方式
?
1
2
3
4
5
6
7
[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包部署方式
?
1
[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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JEECG低代码平台

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值