背景知识:
最近项目组遇到一个问题就是改了一个new theme之后导致某些css文件不起作用了,这也激起了我的好奇心,让我有机会去研究下Liferay Dynamic CSS Filter的原理。
引入:
这个Filter 和一般的Filter一样,会配置在portal-web.xml中,并且声明了对于.css文件和.jsp资源文件请求时候会触发:
然后执行它的processFilter 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
protected
void
processFilter(
HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain)
throws
Exception {
Object parsedContent = getDynamicContent(
request, response, filterChain);
if
(parsedContent ==
null
) {
processFilter(
DynamicCSSFilter.
class
, request, response, filterChain);
}
else
{
if
(parsedContent
instanceof
File) {
ServletResponseUtil.write(response, (File)parsedContent);
}
else
if
(parsedContent
instanceof
String) {
ServletResponseUtil.write(response, (String)parsedContent);
}
}
}
|
调试场景:
比如当我们刚加载 liferay首页,因为上面有许多css资源文件,所以会自动触发这个调用,走入processFilter方法,而它会调用getDynamicContent()方法来获取jRuby解析Sass后的变成的普通css文件。这方法是我们这文章研究的重点。
getDynamicContent()的代码如下:
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
|
protected
Object getDynamicContent(
HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain)
throws
Exception {
String requestURI = request.getRequestURI();
String requestPath = requestURI;
String contextPath = request.getContextPath();
if
(!contextPath.equals(StringPool.SLASH)) {
requestPath = requestPath.substring(contextPath.length());
}
String realPath = ServletContextUtil.getRealPath(
_servletContext, requestPath);
if
(realPath ==
null
) {
return
null
;
}
realPath = StringUtil.replace(
realPath, CharPool.BACK_SLASH, CharPool.SLASH);
File file =
new
File(realPath);
String cacheCommonFileName = getCacheFileName(request);
File cacheContentTypeFile =
new
File(
cacheCommonFileName +
"_E_CONTENT_TYPE"
);
File cacheDataFile =
new
File(cacheCommonFileName +
"_E_DATA"
);
if
((cacheDataFile.exists()) &&
(cacheDataFile.lastModified() >= file.lastModified())) {
if
(cacheContentTypeFile.exists()) {
String contentType = FileUtil.read(cacheContentTypeFile);
response.setContentType(contentType);
}
return
cacheDataFile;
}
String dynamicContent =
null
;
String content =
null
;
try
{
if
(realPath.endsWith(_CSS_EXTENSION) && file.exists()) {
if
(_log.isInfoEnabled()) {
_log.info(
"Parsing SASS on CSS "
+ file);
}
content = FileUtil.read(file);
dynamicContent = DynamicCSSUtil.parseSass(
request, realPath, content);
response.setContentType(ContentTypes.TEXT_CSS);
FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS);
}
else
if
(realPath.endsWith(_JSP_EXTENSION) || !file.exists()) {
if
(_log.isInfoEnabled()) {
_log.info(
"Parsing SASS on JSP or servlet "
+ realPath);
}
StringServletResponse stringResponse =
new
StringServletResponse(response);
processFilter(
DynamicCSSFilter.
class
, request, stringResponse,
filterChain);
CacheResponseUtil.setHeaders(
response, stringResponse.getHeaders());
response.setContentType(stringResponse.getContentType());
content = stringResponse.getString();
dynamicContent = DynamicCSSUtil.parseSass(
request, realPath, content);
FileUtil.write(
cacheContentTypeFile, stringResponse.getContentType());
}
else
{
return
null
;
}
}
catch
(Exception e) {
_log.error(
"Unable to parse SASS on CSS "
+ realPath, e);
if
(_log.isDebugEnabled()) {
_log.debug(content);
}
response.setHeader(
HttpHeaders.CACHE_CONTROL,
HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
}
if
(dynamicContent !=
null
) {
FileUtil.write(cacheDataFile, dynamicContent);
}
else
{
dynamicContent = content;
}
return
dynamicContent;
}
|
我们附上调试信息:
从上述调试信息一目了然,主要是第5行获取当前请求的URI,从调试信息看,它的内容是:
html/portlet/login/css/main.css , 这个也正符合我们的猜想,因为当前请求的资源文件main.css符合CSS扩展名的模式,所以才被这个Dynamic CSS Filter所过滤到并且进入这个断点。
第16行获取这个资源文件的真实路径realPath(疑问1: 如何获取这个真实的path的? 答案在以后讨论中给出) ,这里给出的路径是
/app/Liferay/RI/liferay-portal-6.1.0-ce-ga1/tomcat-7.0.23/webapps/ROOT/html/portlet/login/css/main.css
,这也正符合我们当初的设想,因为这个main.css我们的确是一年前把它手动复制到了该目录下。
然后第19行计算出这个文件对应的缓存base文件名,因为一个缓存文件总是由2部分组成,一个是内容类型文件,一个是数据文件,他们的各自名字都是由base名字加上指定后缀拼接而成。内容类型文件的名字是<cacheCommonFileName>_E_CONTENT_TYPE,而缓存数据文件的名字是<cacheCommonFileName>_E_DATA. (疑问2:如何计算得到这个缓存base文件名?答案也在后续讨论中给出) ,所以我们通过计算得到的缓存base文件名为:
/app/Liferay/RI/liferay-portal-6.1.0-ce-ga1/tomcat-7.0.23/temp/liferay/css/portal/623927847558055413
下面既然得到了缓存base文件名,并且依照后缀定则拼接了相应的内容类型文件名和数据文件名,那么下面的工作就是第20行和第21行在相应位置创建相应的File对象了。因为new File()按照我们对于java的语义,就是如果这个文件不存在,那么则创建新文件,如果存在,只File对象指向已知文件。
我们到服务器目录下看到了这个2个文件:
当缓存内容类型文件和缓存内容文件都固定下来后,下面就考虑到更新或者填入内容到这些文件了。
首先,从第35行开始,还是从原始的带有Sass的css文件入手:
1
2
3
4
5
6
7
8
9
10
|
if
(realPath.endsWith(_CSS_EXTENSION) && file.exists()) {
if
(_log.isInfoEnabled()) {
_log.info(
"Parsing SASS on CSS "
+ file);
}
content = FileUtil.read(file);
dynamicContent = DynamicCSSUtil.parseSass(
request, realPath, content);
response.setContentType(ContentTypes.TEXT_CSS);
FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS);
}
|
它先判断原始文件是不是CSS扩展名的文件,如果是,那么就读取这个原始的css文件到一个字符串变量content中,见以下的调试信息:
然后调用DynamicCSSUtil的parseSass()方法吧这个带Sass的css文件解析成一个不带Sass的普通css文件,并且结果存放在dynamicContent变量中,比如上述content被解析后存放到的dynamicContent的值如下:
读者很容易看出这个新的样式文件是和原来不一样了,不仅仅是排版格式还有语法。
最后,把相应的内容写入到刚才最早的生成的内容类型文件和数据文件中。
源代码的第64行在内容类型文件(<cacheCommonFileName>_E_CONTENT_TYPE)中写入内容为 text/css:
FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS);
而第82行在数据文件中(<cacheCommonFileName>_E_DATA)写入刚才生成Sass解析后生成的普通css文件内容:
FileUtil.write(cacheContentTypeFile, stringResponse.getContentType()
(疑问3:这个写入过程细节是这样的呢?比如文件为空和文件中已经有内容各是如何处理的? 这个答案也在以后讨论中给出)
最后在第88行中返回的动态生成的普通css文件字符串。
现在我们返回到processFilter方法中,既然已经得到了Sass解析后生成的普通css字符串,所以最后就是把这个字符串返回到客户端,所以在processFilter()方法的行末:
ServletResponseUtil.write(response, (String)parsedContent);
这样我们访问页面时候就可以正确的看到和使用这里的样式了。
总结:
从非常宏观的角度,我们至少有以下几点收获:
(1)DynamicCssFilter的调用时机是在站点请求响应的资源文件的时候触发的。
(2)访问资源文件时,它会从原始含有Sass语法的css文件中获取原始内容,然后用jRuby引擎进行解析从而获得新的解析后的普通css文件。
(3)解析后的css文件总会最终被写入到缓存内容文件中,这个缓存数据文件的后缀总是_E_DATA,并且它总是对应一个内容类型文件,这个内容类型文件的格式总是_E_CONTENT_TYPE.
(4)解析后的文件会被服务器写到最终输出流中,从而你在浏览器中可以看到并且使用这个被解析后的普通css文件。
我们还留着几个疑点,会在接下来的文章中得到解决。