这标题不知道怎么写才能表达到我想表达的意思。。。。
基于此文章(前端工程精粹(一):静态资源版本更新与缓存)的hash版本冗余更新静态资源
摘抄:
基于query的形式修改链接
如果先覆盖index.html,后覆盖a.js,用户在这个时间间隙访问,会得到新的index.html配合旧的a.js的情况,从而出现错误的页面。
如果先覆盖a.js,后覆盖index.html,用户在这个间隙访问,会得到旧的index.html配合新的a.js的情况,从而也出现了错误的页面。
静态资源文件版本更新是“覆盖式”的,而页面需要通过修改query来更新,对于使用CDN缓存的web产品来说,还可能面临CDN缓存攻击的问题
对付这个问题,目前来说最优方案就是基于文件内容的hash版本冗余机制了。也就是说,我们希望工程师源码是这么写的:
但是线上代码是这样的:
目前百度也是这种做法,不过oschina还是停留在第一种方法。oschina的做法不是不好,只是觉得不优雅。这也是普遍做法。虽然理解一间公司需要盈利,但一间有创新的科技公司更受人尊重。既然这是一个程序员的网站,为何不尝试使用一些比较前沿的技术(这涉及的技术选型不谈),毕竟用户都是程序员,大家对bug接受能力还是大(也许)。
---------------------------------------我的分割线------------------------------------------------------
我们希望得到的是,修改了的文件的hash码是改变了,并且与现在和未修改前的文件的hash码不重复.Hash码并且越短越好.
MD5生成的16进制长度是32位,SHA-256的更长,是64位.最后确定是选CRC32(介绍) ,不过看到JDK(8) 里面还有个Adler32(介绍).
理论上来讲,CRC64的碰撞概率大约是每18×10^18个CRC码出现一次。这里(链接)有人测试过CRC32的碰撞概率,数据显示是1820W数据,冲突数量是38638个,还是比较可观。还有Adler32也可以选择,不过保守点还是选CRC32.
当中主要出现两个问题
maven-war-plugin对warSourceDirectory资源复制后就是打成war,不能在中间插入步骤,修改复制后的资源
对静态资源的hash冗余,然后修改使用这资源的文件(html)
第一个问题我是在package生命流程钱前将warSourceDirectory的资源复制出来修改,最后配置maven-war-plugin的warSourceDirectory指向这个复制出来的文件夹就可以.
第二个问题只能通过写maven插件来解决
先贴插件的代码
@Mojo(name = "hash", defaultPhase = LifecyclePhase.VALIDATE) public class HashResourceMojo extends AbstractMojo { @Parameter(defaultValue = "${project.build.directory}/prepareWarSource") private File warSourceDirectory; @Parameter private String[] excludes; @Parameter private String[] includes; @Parameter private String[] includesHtml; @Parameter private String[] excludesHtml; @Parameter(defaultValue = "UTF-8") private String encode; @Parameter(defaultValue = "CRC32") private String algorithm; public void execute() throws MojoExecutionException { Map<String, String> resourceMap = checksumResource(); Pattern pattern = Pattern.compile("<(link|script)\\b[^<]*(src|href)=[\"'](?<uri>[\\w-\\./]+?)[\"'][^<]*>"); List<File> files = getFiles(warSourceDirectory, getIncludesHtml(), excludesHtml); int processFileCount = 0; for (File file : files) { try { String source = FileUtils.readFileToString(file, encode); StringBuilder sb = new StringBuilder(); int count = 0; while (true) { Matcher matcher = pattern.matcher(source); if (matcher.find()) { String uri = matcher.group("uri"); File uriFile = new File(warSourceDirectory, uri); String absPath = convertToWebPath(uriFile); if (resourceMap.containsKey(absPath)) { String hashFile = resourceMap.get(absPath); int index = absPath.lastIndexOf("/"); sb.append(source.substring(0, matcher.start("uri"))).append(absPath.substring(0, index + 1)).append(hashFile); count++; } else { sb.append(source.substring(0, matcher.end("uri"))); } source = source.substring(matcher.end("uri")); } else { break; } } sb.append(source); if (count > 0) { processFileCount++; FileUtils.write(file, sb.toString(), encode); getLog().info(convertToWebPath(file) + " change " + count + " uri"); } } catch (IOException e) { throw new RuntimeException(e); } } getLog().info("Hash " + resourceMap.keySet().size() + " files"); getLog().info("Process " + processFileCount + " files"); } private Map<String, String> checksumResource() { List<File> files = getFiles(warSourceDirectory, getIncludes(), excludes); return files.parallelStream().map(file -> { String[] str = new String[2]; FileReader in = null; try { in = new FileReader(file); byte[] bytes = IOUtils.toByteArray(in); IOUtils.closeQuietly(in); Checksum checksum = getChecksum(); checksum.reset(); checksum.update(bytes, 0, bytes.length); String hexCode = Long.toHexString(checksum.getValue()); String fileName = file.getName(); int suffixIndex = fileName.lastIndexOf("."); String hashFileName = fileName.substring(0, suffixIndex) + "_" + hexCode + fileName.substring(suffixIndex); File outFile = new File(file.getParent(), hashFileName); if (file.renameTo(outFile)) { String srcPath = convertToWebPath(file); str[0] = srcPath; str[1] = hashFileName; } else { throw new RuntimeException("File rename failed"); } } catch (IOException e) { throw new RuntimeException(e); } finally { IOUtils.closeQuietly(in); } return str; }).collect(Collectors.toMap(obj -> obj[0], obj -> obj[1])); } private List<File> getFiles(File parent, String[] in, String[] ex) { DirectoryScanner scanner = new DirectoryScanner(); scanner.setBasedir(parent); scanner.setIncludes(in); scanner.setExcludes(ex); scanner.scan(); return Arrays.stream(scanner.getIncludedFiles()).parallel().map(fileName -> new File(parent, fileName)).collect(Collectors.toList()); } private String convertToWebPath(File file) throws IOException { return file.getCanonicalPath().replace(warSourceDirectory.getPath(), "").replace("\\", "/"); } public String[] getIncludes() { if (includes == null || includes.length < 1) { return new String[]{"**/*.js", "**/*.css"}; } return includes; } public String[] getIncludesHtml() { if (includesHtml == null || includesHtml.length < 1) { return new String[]{"**/*.jsp", "**/*.html"}; } return includesHtml; } private Checksum getChecksum() { Checksum checksum; if (algorithm.equalsIgnoreCase("Adler32")) { checksum = new Adler32(); } else if (algorithm.equalsIgnoreCase("CRC32")) { checksum = new CRC32(); } else { throw new IllegalArgumentException("Algorithm value is " + algorithm); } return checksum; }
难就难在写正则表达式,这正则试过几条数据都没有问题.生成环境也试过OK。不过就不适用带EL表达的uri,可自行改造。
项目的pom的配置
<plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>2.7</version> <executions> <execution> <phase>validate</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/prepareWarSource</outputDirectory> <resources> <resource> <directory>${basedir}/webapp</directory> </resource> </resources> </configuration> </execution> </executions> </plugin> <plugin> <groupId>com.chaodongyue.maven</groupId><!--自己写的插件--> <artifactId>hashresource-maven-plugin</artifactId>< <version>1.0</version> <executions> <execution> <goals> <goal>hash</goal> </goals> <configuration> <warSourceDirectory>${project.build.directory}/prepareWarSource</warSourceDirectory> <includesHtml> <include>**/*.jsp</include> <include>**/*.html</include> <include>/WEB-INF/tags/*.tag</include> </includesHtml> </configuration> </execution> </executions> </plugin> <!--压缩js和css <plugin> <groupId>net.alchim31.maven</groupId> <artifactId>yuicompressor-maven-plugin</artifactId> <version>1.5.1</version> <executions> <execution> <goals> <goal>compress</goal> </goals> </execution> </executions> <configuration> <gzip>true</gzip> <encoding>UTF-8</encoding> <nosuffix>true</nosuffix> <jswarn>false</jswarn> <warSourceDirectory>${project.build.directory}/prepareWarSource</warSourceDirectory> <webappDirectory>${project.build.directory}/prepareWarSource</webappDirectory> <includes> <include>**/*.js</include> <include>**/*.css</include> </includes> </configuration> </plugin> --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <configuration> <warSourceDirectory>${project.build.directory}/prepareWarSource</warSourceDirectory> </configuration> </plugin> </plugins>
在validate阶段,将原本web根目录下的文件全部复制到prepareWarSource目录下,然后在对js,css,jsp等进行hash处理。注意顺序,应该将maven-resources-plugin摆在hash插件之前。跟住可以对hash之后的文件进行压缩。
压缩后的文件的hash值肯定和未压缩之前的不同,但这不影响我们的需求。也可以修改yuicompressor-maven-plugin的pash,然后放hash插件之前,这样就是先压缩后计算hash。如果还是启用gzip的话,上述插件的代码是不支持的,还是建议先hash后压缩。
最后maven-war-plugin设置warSourceDirectory打包的源文件夹的路径就可以