设计目标:
实现网页的静态化。
设计思路:
实现方法有三种:
1. 通过动态模板引擎FreeMarker、Velocity渲染后,直接输出到一个静态文件中。
2. 通过Nginx的相关插件(lighttpd、squid)做静态化处理,具体可以参考:http://www.oschina.net/question/54100_9105
3. 通过Servlet的过滤器,在输出阶段,获取输出流,截取输出字段,继续输出流,并将输出字段写到一个html文件中。
代码实现:
1. Web.xml的配置
<!-- 静态页面 -->
<filter>
<filter-name>cacheHtmlFilter</filter-name>
<filter-class>com.wll.web.filter.CacheHtmlFilter</filter-class>
<init-param>
<param-name>isGenHtmlFile</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>cacheHtmlFilter</filter-name>
<url-pattern>/path1/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>cacheHtmlFilter</filter-name>
<url-pattern>/path2/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>cacheHtmlFilter</filter-name>
<url-pattern>/path3/*</url-pattern>
</filter-mapping>
2.Filter的实现
package net.wll.web.filter;
import net.xuele.common.security.SessionUtil;
import net.xuele.common.security.UserSession;
import net.xuele.common.utils.format.JsonUtils;
import net.xuele.teacheval.web.utils.EvalRedisUtil;
import net.xuele.teacheval.web.wrapper.StreamCharArrayWrapper;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.*;
/**
* 静态化过滤器 <br>
* 截获输出流,获取输出字节,做相关的静态化处理,并输出到页面
*
* @author wull
*/
public class CacheHtmlFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(CacheHtmlFilter.class);
/** 是否输出到静态页面 */
private static final boolean isGenHtmlFile = false;
/** 是否将输出字节保存到Cache中 */
private static final boolean isSaveStaticHtmlToCache = true;
private StringRedisTemplate cacheTeachevalRedisTemplate;
@Override
public void init(FilterConfig config) throws ServletException {
logger.debug("-----------------》初始化11111111111111111111111111111111111");
if(this.cacheTeachevalRedisTemplate == null) {
ServletContext servletContext = config.getServletContext();
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
cacheTeachevalRedisTemplate = (StringRedisTemplate) ctx.getBean("cacheTeachevalRedisTemplate");
}
}
/**
* 执行网页静态化处理
* @param request
* @param response
* @param chain
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
this.execFilter(SessionUtil.getUserSession(), req, resp, chain);
}
public void execFilter(UserSession userSession, HttpServletRequest req, HttpServletResponse resp,
FilterChain chain) throws ServletException, IOException {
// 1. 获取当前的url信息
String url = req.getRequestURI().substring(req.getContextPath().length());
String queryString = req.getQueryString();
String key = url;
try {
// 2. 通过request获取所有的parameter,用来组织key值
Map<String, String[]> reqParams = req.getParameterMap();
key = EvalRedisUtil.genCachePageKey(url, userSession, reqParams);
// 2.1 如果通过模糊查询的,则不进行过滤
String keyword = req.getParameter("keyword");
if(StringUtils.isNotBlank(keyword)){
chain.doFilter(req,resp);
return ;
}
String value = EvalRedisUtil.get(this.cacheTeachevalRedisTemplate, key);
// 3.看看缓存中是否有该缓存页面,有的话直接返回
if (StringUtils.isBlank(value)) {
// 3.1 如果缓存中找不到对应的value值,那么直接返回动态页面
logger.debug("缓存中key={}找不到对应的value值,那么直接返回动态页面", key);
} else {
// 3.2 如果缓存中找到对应的value值,那么直接输出流
logger.debug("缓存中key={}找到对应的value.size={},输出到客户端页面上", new String[]{key, String.valueOf(value.length())});
OutputStream finalOut = resp.getOutputStream();
//这句话的意思,使得放入流的数据是utf8格式
finalOut.write(value.getBytes("UTF-8"));
logger.debug("当前请求的输出内容输出到客户端页面,Finish...");
return;
}
}catch (Exception e){
// 如果中间出现异常,直接返回动态页面
logger.error("网页静态化--处理网页静态化时出现异常,直接返回动态页面,异常信息为:", e);
chain.doFilter(req,resp);
return ;
}
// 4. 调用资源,使用 StreamCharArrayWrapper 包装输出,并获取页面字节码
resp.setHeader("Content-Encoding", "text/html;charset=UTF-8");
resp.setCharacterEncoding("UTF-8");
StreamCharArrayWrapper responseWrapper = new StreamCharArrayWrapper(resp);
chain.doFilter(req, responseWrapper);
// 5. 取得存放输出数据的 char 型数组,并组织成String类型
char[] responseChars = responseWrapper.toCharArray();
final String finalStr = new String(responseChars);
logger.debug("当前请求的输出内容长度为:" + finalStr.length());
int status = resp.getStatus();
// 6. 只有相应返回码status正确的response才进行网页静态化
if((status >= 200 && status < 300) || status == 304 ){
try{
// correct do nothing
logger.info("当前请求的url:{},queryString:{},status={},OK...", new String[]{url, queryString,String.valueOf(status)});
// 6.1.是否输出到服务器本地静态页面
if(isGenHtmlFile == true){
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
// 临时输出到一个url地址
FileOutputStream tempOut = new FileOutputStream("C://"+new Random().nextInt(1000)+".html");
// 构建FileOutputStream对象,文件不存在会自动新建
OutputStreamWriter tempWriter = new OutputStreamWriter(tempOut, "UTF-8");
tempWriter.append(finalStr);
tempWriter.close();
// 关闭输出流
tempOut.close();
logger.debug("当前请求的输出服务器本地,内容长度为:" + finalStr.length());
}
// 6.2 是否将输出字节保存到Cache中
if(isSaveStaticHtmlToCache == true){
// 6.2 保存到缓存中,加入到缓存
EvalRedisUtil.set(this.cacheTeachevalRedisTemplate,key,finalStr);
logger.debug("当前请求的输出内容保存到缓存当中,key={},value.size={}" ,new String[]{key,String.valueOf(finalStr.length())});
}
}catch (Exception e){
// 如果中间出现异常,直接返回动态页面
logger.error("网页静态化--处理网页静态化时出现异常,直接返回动态页面,异常信息为:",e);
chain.doFilter(req,resp);
return ;
}
}else{
// 有错误的response,直接返回动态页面
logger.info("当前请求的url:{},queryString:{},status={},ERROR...", new String[]{url, queryString, String.valueOf(status)});
}
// 7. 最后,将页面再次输出到客户端的页面上
OutputStream finalOut = resp.getOutputStream();
//这句话的意思,使得放入流的数据是utf8格式
finalOut.write(finalStr.getBytes("UTF-8"));
logger.debug("当前请求的输出内容输出到客户端页面,Finish...");
}
@Override
public void destroy() {
}
/**
* 因为Filter在单元测试期间,无法
* @param cacheTeachevalRedisTemplate
*/
public void setCacheTeachevalRedisTemplate(StringRedisTemplate cacheTeachevalRedisTemplate) {
this.cacheTeachevalRedisTemplate = cacheTeachevalRedisTemplate;
logger.debug("-----------------》初始化222222222222222222222222222222222222");
}
}
3. StreamCharArrayWrapper将输出流变成char数组
package com.wll.web.wrapper;
import java.io.CharArrayWriter;
import java.io.PrintWriter;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
/**
* A response wrapper that takes everything the client would normally output and
* saves it in one big character array.
*/
public class StreamCharArrayWrapper extends HttpServletResponseWrapper {
private CharArrayWriter charWriter;
/**
* Initializes wrapper.
* <P>
* First, this constructor calls the parent constructor. That call is
* crucial so that the response is stored and thus setHeader, *setStatus,
* addCookie, and so forth work normally.
* <P>
* Second, this constructor creates a CharArrayWriter that will be used to
* accumulate the response.
*/
public StreamCharArrayWrapper(HttpServletResponse response) {
super(response);
charWriter = new CharArrayWriter();
}
/**
* When servlets or JSP pages ask for the Writer, don't give them the real
* one. Instead, give them a version that writes into the character array.
* The filter needs to send the contents of the array to the client (perhaps
* after modifying it).
*/
@Override
public PrintWriter getWriter() {
return new PrintWriter(charWriter);
}
/**
* Get a String representation of the entire buffer.
* <P>
* Be sure <B>not</B> to call this method multiple times on the same
* wrapper. The API for CharArrayWriter does not guarantee that it
* "remembers" the previous value, so the call is likely to make a new
* String every time.
*/
@Override
public String toString() {
return charWriter.toString();
}
/** Get the underlying character array. */
public char[] toCharArray() {
return charWriter.toCharArray();
}
}