目录
项目背景
本项目采用传统的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控制静态资源的缓存