记一次项目后期对静态资源的处理

项目背景

本项目采用传统的SSM + jsp架构,兼容IE8及以上主流浏览器,且已经的线上运行。本地开发使用tomcat容器,测试环境和生产环境使用nginx + Websphere。
一切似乎完美,然而客户体验却很糟糕。主要有两个问题:

问题一

用IE9打开网页报错,部分页面不能加载,需要客户自己更改浏览器设置。

问题二

每次上线后,浏览器访问页面达不到预期效果,甚至报错,需要客户自己清理浏览器缓存。

问题产生的原因

以上两个问题,有经验的开发人员一看就能猜到原因,然而项目组的人似乎并不在意这些问题,他们的目标只是实现正常情况下的功能。本开发进项目组后通过查看代码,发现:

问题一产生的原因

页面没有添加meta标签指定浏览器应该以哪种引擎渲染页面。

问题二产生的原因

DispatcherServlet没有拦截对静态资源请求的url,也就没有配置对静态资源缓存的处理。

解决问题

问题一的解决方案

在所有jsp页面的head标签内添加一行

<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">

好在所有jsp页面引用了公共的头部jsp,把上面的代码加在公共的jsp上就可以了。

问题二的解决方案

springmvc本身是自带静态资源处理方案的,只要配置就行,可选方案是指定资源版本和使用md5。本人更倾向使用md5,因为不用每次上线都改一次版本,md5可以做到对静态资源缓存的最细粒度的控制。
然而由于上面提到的原因,如果使用springmvc的提供的功能,必须修改项目的基础配置,风险太大。放弃?是的,本开发决定自己写个控制静态资源缓存的功能。指定资源版本?太简单,不再赘述,以下是使用md5的处理。

具体思路

项目启动时获取js、css文件的路径和md5值并存放到map中,在jsp引用这些资源的地方用el表达式获取md5值作为url参数拼接的引用路径的后面,当md5值改变时浏览器认为是新的资源,就会去服务器请求。

具体做法
第一次尝试
public class StaticWebConstants {
	private static final Logger logger = Logger.getLogger(StaticWebConstants.class);

	public static final Map<String, String> FILE_HASH_MAP = new HashMap<String, String>();
	
	static {
		try {
			String path = StaticWebConstants.class.getResource("/").getPath();
			File rootPath rootPath = new File(path);
			rootPath = rootPath.getParentFile().getParentFile();
			generateStatic(rootPath);
		} catch (Exception e) {
			logger.error("获取静态文件URI异常", e);
			throw new ServiceException("获取静态文件URI异常", e);
		}
	}
	
	private static void generateMap(File file) {
		InputStream is = null;
		try {
			is = new FileInputStream(file);
			String path = file.getAbsolutePath().replace("\\", "/");
			String filenameMd5Hex = DigestUtils.md5Hex(path);
			String fileMd5Hex = DigestUtils.md5Hex(is);
			logger.info(path + "----filenameMd5Hex-----" + filenameMd5Hex + "-----fileMd5Hex-----" + fileMd5Hex);
			FILE_HASH_MAP.put(filenameMd5Hex, fileMd5Hex);
		} catch (Exception e) {
			throw new ServiceException("生成静态文件Md5值异常", e);
		} finally {
			if (is != null) {
				try {
					is.close();
				} catch (IOException e) {
					logger.info("静态文件输入流关闭异常", e);
				}
			}
		}
	}
	private static void generateStatic(File file) {
		if (file.isFile()) {
			generateMap(file);
		} else {
			File[] listFiles = file.listFiles();
			for (File file2 : listFiles) {
				generateStatic(file2);
			}
		}
	}

	public static void main(String[] args) {}
}
public class MyServletContextListener implements ServletContextListener {
	
	@Override
	public void contextDestroyed(ServletContextEvent servletContextEvent) {
	}
	
	@Override
	public void contextInitialized(ServletContextEvent servletcontextevent) {
		// 浏览器静态资源缓存解决方案start
		@SuppressWarnings("unused")
		Map<String, String> map = StaticWebConstants.FILE_HASH_MAP;
		// 浏览器静态资源缓存解决方案end
	}
}
<%@ page import="com.abc.ssvf.common.StaticWebConstants" %>
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/css/public.css?v=${StaticWebConstants.FILE_HASH_MAP["1d034a56fffd5ee419e3852bbc070092"]}">

执行StaticWebConstants.main方法,就可以找到 public.css文件路径md5值。满心欢喜,开始本地测试。咦,打印出这么多日志,再看浏览器源

<link rel="stylesheet" type="text/css" href="/css/public.css?v=">

v=后面没有md5值。开是开始解决日志过多和找不到md5值的问题。

第二次尝试
public class StaticWebConstants {
	private static final Logger logger = Logger.getLogger(StaticWebConstants.class);

	public static final Map<String, String> FILE_HASH_MAP = new HashMap<String, String>();
	
	private static final String ROOT_PATH; // 存放项目绝对路径
	
	static {
		try {
			String path = StaticWebConstants.class.getResource("/").getPath();
			File rootPath rootPath = new File(path);
			rootPath = rootPath.getParentFile().getParentFile();
			ROOT_PATH = rootPath.getAbsolutePath().replace("\\", "/");
			
			// 只遍历需要管理缓存的js、css文件,解决日志过多的问题
			File[] listRootFiles = rootPath.listFiles(new FilenameFilter() {
				
				@Override
				public boolean accept(File dir, String name) {
					if ("css".equals(name) || "jslib".equals(name) || "resources".equals(name) || "static".equals(name)) {
						return true;
					}
					return false;
				}
			});
			for (File file : listRootFiles) {
				if (file.getName().equals("css")) {
					File[] listFiles = file.listFiles(new FilenameFilter() {
						
						@Override
						public boolean accept(File dir, String name) {
							if (name.endsWith(".js") || name.endsWith(".css")) {
								return true;
							}
							return false;
						}
					});
					for (File file2 : listFiles) {
						if (file2.isFile()) {
							generateMap(file2);
						}
					}
				} else if (file.getName().equals("jslib")) {
					File[] listFiles = file.listFiles(new FilenameFilter() {
						
						@Override
						public boolean accept(File dir, String name) {
							if ("manage".equals(name) || name.endsWith(".js") || name.endsWith(".css")) {
								return true;
							}
							return false;
						}
					});
					for (File file2 : listFiles) {
						if (file2.isFile()) {
							generateMap(file2);
						} else {
							File[] listFiles2 = file2.listFiles(new FilenameFilter() {
								
								@Override
								public boolean accept(File dir, String name) {
									if (name.endsWith(".js") || name.endsWith(".css")) {
										return true;
									}
									return false;
								}
							});
							for (File file3 : listFiles2) {
								if (file3.isFile()) {
									generateMap(file3);
								}
							}
						}
					}
				} else if (file.getName().equals("resources")) {
					File[] listFiles = file.listFiles(new FilenameFilter() {
						
						@Override
						public boolean accept(File dir, String name) {
							if ("base".equals(name) || "luck".equals(name) || "signin".equals(name)) {
								return true;
							}
							return false;
						}
					});
					for (File file2 : listFiles) {
						if (file2.isFile()) {
							generateMap(file2);
						} else {
							File[] listFiles2 = file2.listFiles(new FilenameFilter() {
								
								@Override
								public boolean accept(File dir, String name) {
									if ("css".equals(name) || "js".equals(name) || "luckJS".equals(name)) {
										return true;
									}
									return false;
								}
							});
							for (File file3 : listFiles2) {
								if (file3.isFile()) {
									generateMap(file3);
								} else {
									File[] listFiles3 = file3.listFiles(new FilenameFilter() {
										
										@Override
										public boolean accept(File dir, String name) {
											if ("app".equals(name) || "log".equals(name) || "comJs".equals(name)
													|| "wheel".equals(name) || name.endsWith(".js") || name.endsWith(".css")) {
												return true;
											}
											return false;
										}
									});
									for (File file4 : listFiles3) {
										if (file4.isFile()) {
											generateMap(file4);
										} else {
											File[] listFiles4 = file4.listFiles(new FilenameFilter() {
												
												@Override
												public boolean accept(File dir, String name) {
													if (name.endsWith(".js") || name.endsWith(".css")) {
														return true;
													}
													return false;
												}
											});
											for (File file5 : listFiles4) {
												if (file5.isFile()) {
													generateMap(file5);
												}
											}
										}
									}
								}
							}
						}
					}
				} else if (file.getName().equals("static")) {
					//以后新加的静态资源都放在static目录下
					generateStatic(file);
				}
			}
			
		} catch (Exception e) {
			logger.error("获取静态文件URI异常", e);
			throw new ServiceException("获取静态文件URI异常", e);
		}
	}
	
	private static void generateMap(File file) {
		InputStream is = null;
		try {
			is = new FileInputStream(file);
			// 将文件绝对路径前的项目绝对路径替换成/,用于统一main方法和各种环境下的文件路径md5值
			String path = file.getAbsolutePath().replace("\\", "/").replace(ROOT_PATH, "/");
			String filenameMd5Hex = DigestUtils.md5Hex(path);
			String fileMd5Hex = DigestUtils.md5Hex(is);
			logger.info(path + "----filenameMd5Hex-----" + filenameMd5Hex + "-----fileMd5Hex-----" + fileMd5Hex);
			FILE_HASH_MAP.put(filenameMd5Hex, fileMd5Hex);
		} catch (Exception e) {
			throw new ServiceException("生成静态文件Md5值异常", e);
		} finally {
			if (is != null) {
				try {
					is.close();
				} catch (IOException e) {
					logger.info("静态文件输入流关闭异常", e);
				}
			}
		}
	}
	
	//递归static目录下的文件
	private static void generateStatic(File file) {
		if (file.isFile()) {
			generateMap(file);
		} else {
			File[] listFiles = file.listFiles();
			for (File file2 : listFiles) {
				generateStatic(file2);
			}
		}
	}
	public static void main(String[] args) {}
}
public class MyServletContextListener implements ServletContextListener {
	
	@Override
	public void contextDestroyed(ServletContextEvent servletContextEvent) {
	}
	
	@Override
	public void contextInitialized(ServletContextEvent servletcontextevent) {
		// 浏览器静态资源缓存解决方案start
		@SuppressWarnings("unused")
		Map<String, String> map = StaticWebConstants.FILE_HASH_MAP;
		// 浏览器静态资源缓存解决方案end
	}
}
<%@ page import="com.abc.ssvf.common.StaticWebConstants" %>
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/css/public.css?v=${StaticWebConstants.FILE_HASH_MAP["6965a3d7b027fd5952025a8f91c93e10"]}">

再次满心欢喜,开始本地测试,一切正常。改变项目存放位置,一切正常。完美,有点沾沾自喜。上测试环境测试,咦,网页打不开了,报了个StaticWebConstants类的引用没找到,有一种不祥的预感。再看项目启动日志,一开始就报错了,报错代码:

rootPath = new File(path).getParentFile().getParentFile();
第三次尝试

由于没加太多日志,不知道报错原因。加日志看看

logger.info("init StaticWebConstants---->");
String path = StaticWebConstants.class.getResource("/").getPath();
logger.info("URL_PATH----->" + path);
File rootPath rootPath = new File(path);
logger.info("rootPath1----->" + rootPath.getAbsolutePath());
rootPath = rootPath.getParentFile().getParentFile();
logger.info("rootPath2----->" + rootPath.getAbsolutePath());
ROOT_PATH = rootPath.getAbsolutePath().replace("\\", "/");
logger.info("ROOT_PATH----->" + ROOT_PATH);

有点蒙,竟然打印的日志是
URL_PATH----->/
rootPath1----->/
这是linux系统根路径呀,难怪rootPath.getParentFile()会报错。

第四次尝试

于是开始尝试获取项目根路径的办法,功夫不负有心人,终于找到了,代码如下

String path = StaticWebConstants.class.getClassLoader().getResource("WEB-INF/classes").getPath();
logger.info("URL_PATH----->" + path);
rootPath = new File(path).getParentFile().getParentFile();
logger.info("rootPath----->" + rootPath.getAbsolutePath());
ROOT_PATH = rootPath.getAbsolutePath().replace("\\", "/");
logger.info("ROOT_PATH----->" + ROOT_PATH);

不知道是nginx的原因,还是Websphere的原因,还是测试环境配置的原因,总之找到办法了,不管那么多了。

第五次尝试

整理好代码后,继续。bug虐我千百遍,我待bug如初恋。
这次启动日志如预期一样正常,打开浏览器也正常,查看源,咦,我要崩溃了,竟然是这样

<link rel="stylesheet" type="text/css" href="/css/public.css?v=">

没有值,代码填写的md5值不对吗?看代码和日志,对的呀,我的天哪,崩溃了!

第六次尝试

又一次使用百度大法,得到的答案是Websphere从6.0才支持${ }写法,可是测试环境Websphere版本是9.0呀。再看项目中用到的${ },能获取到值呀,只是都是简单常规的写法。不管了,改成最原始的写法,如下

<%@ page import="com.abc.ssvf.common.StaticWebConstants" %>
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/css/public.css?v=<%=StaticWebConstants.FILE_HASH_MAP.get("6965a3d7b027fd5952025a8f91c93e10") %>">

启动项目,查看源,一气呵成,终于有值了。现在本地环境和测试生产环境用的是两套代码,于是整合开始

第七次尝试
public class StaticWebConstants {
	private static final Logger logger = Logger.getLogger(StaticWebConstants.class);

	public static final Map<String, String> FILE_HASH_MAP = new HashMap<String, String>();
	
	private static final String ROOT_PATH; // 存放项目绝对路径
	
	static {
		try {
			//在配置文件中加入标识项目环境的常量
			logger.info("init StaticWebConstants---->" + Constants.ENVIRONMENT);
			String path = "";
			if (Constants.ENVIRONMENT.equals(Constants.ENVIRONMENT_DEV)) {
				String path = StaticWebConstants.class.getResource("/").getPath();
			} else {
				String path = StaticWebConstants.class.getClassLoader().getResource("WEB-INF/classes").getPath();
			}
			logger.info("URL_PATH----->" + path);
			File rootPath = new File(path).getParentFile().getParentFile();
			ROOT_PATH = rootPath.getAbsolutePath().replace("\\", "/");
			
			// 只遍历需要管理缓存的js、css文件,解决日志过多的问题
			File[] listRootFiles = rootPath.listFiles(new FilenameFilter() {
				
				@Override
				public boolean accept(File dir, String name) {
					if ("css".equals(name) || "jslib".equals(name) || "resources".equals(name) || "static".equals(name)) {
						return true;
					}
					return false;
				}
			});
			for (File file : listRootFiles) {
				if (file.getName().equals("css")) {
					File[] listFiles = file.listFiles(new FilenameFilter() {
						
						@Override
						public boolean accept(File dir, String name) {
							if (name.endsWith(".js") || name.endsWith(".css")) {
								return true;
							}
							return false;
						}
					});
					for (File file2 : listFiles) {
						if (file2.isFile()) {
							generateMap(file2);
						}
					}
				} else if (file.getName().equals("jslib")) {
					File[] listFiles = file.listFiles(new FilenameFilter() {
						
						@Override
						public boolean accept(File dir, String name) {
							if ("manage".equals(name) || name.endsWith(".js") || name.endsWith(".css")) {
								return true;
							}
							return false;
						}
					});
					for (File file2 : listFiles) {
						if (file2.isFile()) {
							generateMap(file2);
						} else {
							File[] listFiles2 = file2.listFiles(new FilenameFilter() {
								
								@Override
								public boolean accept(File dir, String name) {
									if (name.endsWith(".js") || name.endsWith(".css")) {
										return true;
									}
									return false;
								}
							});
							for (File file3 : listFiles2) {
								if (file3.isFile()) {
									generateMap(file3);
								}
							}
						}
					}
				} else if (file.getName().equals("resources")) {
					File[] listFiles = file.listFiles(new FilenameFilter() {
						
						@Override
						public boolean accept(File dir, String name) {
							if ("base".equals(name) || "luck".equals(name) || "signin".equals(name)) {
								return true;
							}
							return false;
						}
					});
					for (File file2 : listFiles) {
						if (file2.isFile()) {
							generateMap(file2);
						} else {
							File[] listFiles2 = file2.listFiles(new FilenameFilter() {
								
								@Override
								public boolean accept(File dir, String name) {
									if ("css".equals(name) || "js".equals(name) || "luckJS".equals(name)) {
										return true;
									}
									return false;
								}
							});
							for (File file3 : listFiles2) {
								if (file3.isFile()) {
									generateMap(file3);
								} else {
									File[] listFiles3 = file3.listFiles(new FilenameFilter() {
										
										@Override
										public boolean accept(File dir, String name) {
											if ("app".equals(name) || "log".equals(name) || "comJs".equals(name)
													|| "wheel".equals(name) || name.endsWith(".js") || name.endsWith(".css")) {
												return true;
											}
											return false;
										}
									});
									for (File file4 : listFiles3) {
										if (file4.isFile()) {
											generateMap(file4);
										} else {
											File[] listFiles4 = file4.listFiles(new FilenameFilter() {
												
												@Override
												public boolean accept(File dir, String name) {
													if (name.endsWith(".js") || name.endsWith(".css")) {
														return true;
													}
													return false;
												}
											});
											for (File file5 : listFiles4) {
												if (file5.isFile()) {
													generateMap(file5);
												}
											}
										}
									}
								}
							}
						}
					}
				} else if (file.getName().equals("static")) {
					//以后新加的静态资源都放在static目录下
					generateStatic(file);
				}
			}
			
		} catch (Exception e) {
			logger.error("获取静态文件URI异常", e);
			throw new ServiceException("获取静态文件URI异常", e);
		}
	}
	
	private static void generateMap(File file) {
		InputStream is = null;
		try {
			is = new FileInputStream(file);
			// 将文件绝对路径前的项目绝对路径替换成/,用于统一main方法和各种环境下的文件路径md5值
			String path = file.getAbsolutePath().replace("\\", "/").replace(ROOT_PATH, "/");
			String filenameMd5Hex = DigestUtils.md5Hex(path);
			String fileMd5Hex = DigestUtils.md5Hex(is);
			logger.info(path + "----filenameMd5Hex-----" + filenameMd5Hex + "-----fileMd5Hex-----" + fileMd5Hex);
			FILE_HASH_MAP.put(filenameMd5Hex, fileMd5Hex);
		} catch (Exception e) {
			throw new ServiceException("生成静态文件Md5值异常", e);
		} finally {
			if (is != null) {
				try {
					is.close();
				} catch (IOException e) {
					logger.info("静态文件输入流关闭异常", e);
				}
			}
		}
	}
	
	//递归static目录下的文件
	private static void generateStatic(File file) {
		if (file.isFile()) {
			generateMap(file);
		} else {
			File[] listFiles = file.listFiles();
			for (File file2 : listFiles) {
				generateStatic(file2);
			}
		}
	}
	public static void main(String[] args) {}
}
public class MyServletContextListener implements ServletContextListener {
	
	@Override
	public void contextDestroyed(ServletContextEvent servletContextEvent) {
	}
	
	@Override
	public void contextInitialized(ServletContextEvent servletcontextevent) {
		// 浏览器静态资源缓存解决方案start
		// 只在项目启动时加载一次
		@SuppressWarnings("unused")
		Map<String, String> map = StaticWebConstants.FILE_HASH_MAP;
		// 浏览器静态资源缓存解决方案end
	}
}
<%@ page import="com.abc.ssvf.common.StaticWebConstants" %>
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/css/public.css?v=<%=StaticWebConstants.FILE_HASH_MAP.get("6965a3d7b027fd5952025a8f91c93e10") %>">

总结

虽然这种方法解决了静态资源的问题,但是没有springmvc自带的解决方案方便。所以的构建项目时就应该考虑到这些,才能少走些弯路。

完成

Springboot项目请移步
springboot控制静态资源的缓存

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值