一、背景
最近正在编写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
>
|
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)
|
七、总结
这个Filter很好的解决了我在开发TagLib时遇到的资源引用问题,对我来说应该够用了。
我们项目中一般采用目录方式部署,我也更希望通过Web服务器来直接访问资源。
并没有采用maven来组织资源,因为我需要提供给非maven工程使用。
一些流行的mvc中也有类似的手法,可能是采用流方式读取(猜测),感兴趣的朋友可以查看这些mvc的代码。
八、源程序下载
下载包中提供了源代码以及打包后(target目录)的工程。