Статьи

Создание версий статических ресурсов с помощью UrlRewriteFilter

Несколько недель назад сотрудник отправил мне интересное электронное письмо после разговора с генеральным директором Zoompf в JSConf .

Был упомянут один интересный совет: как мы запрашиваем версию наших скриптов и CSS. Очевидно, это не всегда кэширует, как мы ожидали (некоторые прокси никогда не кэшируют актив, если у него есть строка запроса). Рекомендация преподобного имени файла себя .

В этой статье объясняется, как мы реализовали систему «очистки кеша» в нашем приложении с помощью Maven и UrlRewriteFilter. Первоначально мы использовали строку запроса в нашей реализации, но переключились на имена файлов после прочтения рекомендации Соудерса. Эта часть была выяснена моим уважаемым коллегой Ноем Пачи .

Наши требования

  • Сделайте так, чтобы URL включал номер версии для каждого статического URL ресурса (JS, CSS и SWF), который служит для истечения срока действия клиентского кэша ресурса.
  • Вставьте номер версии в приложение, чтобы номер версии можно было включить в URL.
  • Use a random version number when in development mode (based on running without a packaged war) so that developers will not need to clear their browser cache when making changes to static resources. The random version number should match the production version number formats which is currently: x.y-SNAPSHOT-revisionNumber
  • When running in production, the version number/cachebust is computed once (when a Filter is initialized). In development, a new cachebust is computed on each request.

In our app, we’re using Maven, Spring and JSP, but the latter two don’t really matter for the purposes of this discussion.

Implementation Steps
1. First we added the buildnumber-maven-plugin to our project’s pom.xml so the build number is calculated from SVN.

<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>buildnumber-maven-plugin</artifactId>
<version>1.0-beta-4</version>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>create</goal>
</goals>
</execution>
</executions>
<configuration>
<doCheck>false</doCheck>
<doUpdate>false</doUpdate>
<providerImplementations>
<svn>javasvn</svn>
</providerImplementations>
</configuration>
</plugin>

2. Next we used the maven-war-plugin to add these values to our WAR’s MANIFEST.MF file.

<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>2.0.2</version>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
<manifestEntries>
<Implementation-Version>${project.version}</Implementation-Version>
<Implementation-Build>${buildNumber}</Implementation-Build>
<Implementation-Timestamp>${timestamp}</Implementation-Timestamp>
</manifestEntries>
</archive>
</configuration>
</plugin>

3. Then we configured a Filter to read the values from this file on startup. If this file doesn’t exist, a default version number of «1.0-SNAPSHOT-{random}» is used. Otherwise, the version is calculated as ${project.version}-${buildNumber}.

private String buildNumber = null;

...
@Override
public void initFilterBean() throws ServletException {
try {
InputStream is =
servletContext.getResourceAsStream("/META-INF/MANIFEST.MF");
if (is == null) {
log.warn("META-INF/MANIFEST.MF not found.");
} else {
Manifest mf = new Manifest();
mf.read(is);
Attributes atts = mf.getMainAttributes();
buildNumber = atts.getValue("Implementation-Version") + "-" + atts.getValue("Implementation-Build");
log.info("Application version set to: " + buildNumber);
}
} catch (IOException e) {
log.error("I/O Exception reading manifest: " + e.getMessage());
}
}

...

// If there was a build number defined in the war, then use it for
// the cache buster. Otherwise, assume we are in development mode
// and use a random cache buster so developers don't have to clear
// their browswer cache.
requestVars.put("cachebust", buildNumber != null ? buildNumber : "1.0-SNAPSHOT-" + new Random().nextInt(100000));

4. We then used the «cachebust» variable and appended it to static asset URLs as indicated below.

<c:set var="version" scope="request" 
value="${requestScope.requestConfig.cachebust}"/>
<c:set var="base" scope="request"
value="${pageContext.request.contextPath}"/>

<link rel="stylesheet" type="text/css"
href="${base}/v/${version}/assets/css/style.css" media="all"/>

<script type="text/javascript"
src="${base}/v/${version}/compressed/jq.js"></script>

The injection of /v/[CACHEBUSTINGSTRING]/(assets|compressed) eventually has to map back to the actual asset (that does not include the two first elements of the URI). The application must remove these two elements to map back to the actual asset. To do this, we use the UrlRewriteFilter. The UrlRewriteFilter is used (instead of Apache’s mod_rewrite) so when developers run locally (using mvn jetty:run) they don’t have to configure Apache.

5. In our application, «/compressed/» is mapped to wro4j‘s WroFilter. In order to get UrlRewriteFilter and WroFilter to work with this setup, the WroFilter has to accept FORWARD and REQUEST dispatchers.

<filter-mapping>
<filter-name>rewriteFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
<filter-name>WebResourceOptimizer</filter-name>
<url-pattern>/compressed/*</url-pattern>
<dispatcher>FORWARD</dispatcher>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>

Once this was configured, we added the following rules to our urlrewrite.xml to allow rewriting of any assets or compressed resource request back to its «correct» URL.

<rule match-type="regex">
<from>^/v/[0-9A-Za-z_.\-]+/assets/(.*)$</from>
<to>/assets/$1</to>
</rule>
<rule match-type="regex">
<from>^/v/[0-9A-Za-z_.\-]+/compressed/(.*)$</from>
<to>/compressed/$1</to>
</rule>
<rule>
<from>/compressed/**</from>
<to>/compressed/$1</to>
</rule>

Of course, you can also do this in Apache. This is what it might look like in your vhost.d file:

RewriteEngine    on
RewriteLogLevel  0!
RewriteLog       /srv/log/apache22/app_rewrite_log
RewriteRule      ^/v/[.A-Za-z0-9_-]+/assets/(.*) /assets/$1 [PT]
RewriteRule      ^/v/[.A-Za-z0-9_-]+/compressed/(.*) /compressed/$1 [PT]

Является ли хорошей идеей реализовать это в Apache или использовать UrlRewriteFilter — вопрос спорный. Если мы сможем сделать это с помощью UrlRewriteFilter, польза от этого в Apache будет сомнительной, тем более что он создает дубликат кода.

 

От http://raibledesigns.com/rd/entry/versioning_static_assets_with_urlrewritefilter