Elegantní způsob ukládání verzi v Java archívech

Existují situace, kdy aplikaci neinstalujete sami, ale instaluje ji třetí strana - ať už je třetí stranou myšlen technik zákazníka nebo kolega z jiného oddělení firmy. Vy posléze přijdete už k nainstalované aplikaci, u které si nikdy tak úplně stoprocentně nemůžete být jisti verzí neřkuli verzemi knihoven, které daná aplikace používá. Přesto tato znalost může být pro řešení některých problémů zásadní (např. proto, že oprava může spočívat v pouhé instalaci nové verze knihovny / modulu). Můžete se s tím setkat i v daleko prostším případě - pokud vyvíjíte nějaký produkt s velkým množstvím instalací - chvíli vám může trvat než zjistíte jakou verzi má daný zákazník, u kterého řešíte nahlášené problémy.

Přímočarým řešením je vytvoření nějaké info stránky se seznamem knihoven / modulů a jejich verzí, které jsou použity v aplikaci, která by vám umožnila všechny potřebné informace zjistit během vteřiny. V tu chvíli už se ale dostáváte k druhému problému - jak zajistit (nelépe automatické) verzování knihoven, aby bylo zajistěno, že se knihovny budou pravidelně verzovat, a že u všech knihoven bude tato informace jednoduše přístupná (a opět nejlépe jednotným způsobem, aby info stránka neměla s vyhledáváním této informace problém).

Problém se zdá možná jednoduchý, ale můžete narazit na celou řadu nepříjemností jako se to stalo třeba nám. Řešení může být ale opravdu velmi prosté ...

Naše původní řešení nebylo nijak elegantní

Pro pravidelné verzování jsme používali standardní releasovací proces Mavenu. Vkládání a čtení verzí knihoven jsme si už ale museli zajistit sami. Donedávna jsme toto nechávali v kompetenci programátorů jednotlivých modulů, což mělo několik negativních efektů:

  • každý si to řešil tak nějak po svém - i když ve většině případů se jednalo o nějaký property file na classpath, kde Maven nahrazoval proměnné ${pom.artifactId} a ${pom.version}
  • s každou novou knihovnou / modulem to bylo nutné řešit znovu
  • díky výše uvedeným dvou bodům v několika knihovnách prostě přístup k verzím chyběl úplně

A pak přišla inspirace ...

... jak jinak než ze zdrojových kódů Springu (konkrétně z SpringVersion). Při brouzdání jejich kódem na lovu naprosto jiné informace jsem narazil na velmi zajímavou deklaraci sestávající se z těchto dvou řádků:


Package pkg = someClass.getPackage();
String version = (pkg != null ? pkg.getImplementationVersion() : "");

Na úrovni Java Package objektu jsou totiž dostupné některé informace, které se zapisují do souboru MANIFEST.MF ve složce META-INF každého JARu. Konkrétně se jedná o atributy:


Specification-Title: Forms module
Specification-Version: 2.0-SNAPSHOT
Specification-Vendor: FG Forrest, a.s.
Implementation-Title: Forms module
Implementation-Version: 2.0-SNAPSHOT
Implementation-Vendor: FG Forrest, a.s.

O vložení těchto atributů se samozřejmě musíte postarat. Ani Maven build ve svém standardním nastavení vám tyto atributy nepřidá. Nicméně po chvilce pátrání objevíte, že dostat je tam je velice prosté. V následujícím XML snippetu uvádím nastavení pro všechny standardní Java archívy (jar, war, ear):


<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-jar-plugin</artifactId>
   <configuration>
      <archive>
         <manifest>
            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
            <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
         </manifest>
      </archive>
   </configuration>
 </plugin>
<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-war-plugin</artifactId>
   <configuration>
      <archive>
         <manifest>
            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
            <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
         </manifest>
      </archive>
   </configuration>
 </plugin>
<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-ear-plugin</artifactId>
   <configuration>
      <archive>
         <manifest>
            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
            <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
         </manifest>
      </archive>
   </configuration>
</plugin>

Pokud toto nastavení pluginu umístíte do firemních parent POM deklarací, nebude muset žádný z vašich kolegů tento problém řešit. Knihovny nebo moduly pak v sobě mohou obsahovat třídu podobnou té Springové, ale i pokud ji obsahovat nebudou, můžete kdykoliv danou verzi zjistit i zvenčí. Stačí vám k tomu znát třídu zevnitř dané knihovny (JARu) a přes reflexi se dostat na objekt Package a odtud verzi bez problémů zjistit.

Výše uvedený odstavec platí pouze pro MANIFEST.MF umístěný v JAR souborech. Přestože s pomocí správného nastavení war a ear pluginu se vám tyto informace promítnou i do manifestů umístěných v těchto archívech, přístup k informacím v těchto archívech již není tak přímočarý. Nicméně myslím, že v těchto případech lze problém vždycky obejít extrakcí těchto dat do vybraného JARu umístěného ve WARu či EARu.

Jiří Pressfreund v komentářích přidal i alternativní konfiguraci pro buildování Antem (díky ;-) ):


<!–-
**********************************************************************
* Increase build number
**********************************************************************
-->
<target name="usage">
<propertyfile file="${config.dir}/version.properties">
      <entry key="build.number" type="int" operation="+" value="1” pattern="00”/>
   </propertyfile>
</target>
<!–-
**********************************************************************
* Build JAR library
**********************************************************************
-–>
<target name="build" depends="compile, usage" description="Build library">
   <tstamp/>
   <jar destfile="${dist.dir}/${ant.project.name}.jar">
      <fileset dir="${build.classes.dir}">
         <include name="**/*.*"/>
         <exclude name="**/*Test*.class"/>
      </fileset>
      <manifest>
         <attribute name="Built-By" value="${user.name}"/>
         <attribute name="Date" value="${TODAY}"/>
         <attribute name="Implementation-Version" value="Build ${major.number}.${minor.number}.${build.number}"/>
      </manifest>
   </jar>
</target>

Nestačí vám pouze informace o verzi?

Pokud by vás zajímala informace o číslu buildu a ne pouze verzi knihovny (například pokud byste potřebovali rozeznat jeden SNAPSHOTový build od druhého), musíte jít ještě dál. Kdysi jsme zkoušeli Maven BuildNumber Plugin od Codehausu, ale protože prozatím pořád používáme CVS mohli jsme využít pouze "offline" způsob generování build numberů, což při více vývojářích působilo víc problémů než užitku. Proto jsme si napsali vlastní plugin, který žádá o nové číslo buildu centrální server, kde je jednoduchý servlet přidělující nová čísla. Tím jsme schopni jednoduše zajistit unikátní čísla buildu i mezi více vývojáři.

Konfigurace našeho pluginu vypadá v pom.xml takto:


<plugin>
	<groupId>com.fg.maven</groupId>
	<artifactId>maven-buildservice-plugin</artifactId>
	<version>1.0</version>
	<configuration>
	   <buildNumberServiceUrl>http://naseUrl</buildNumberServiceUrl>
	   <buildNumberMinimalValue>1</buildNumberMinimalValue>
	</configuration>
	<executions>
	   <execution>
<phase>validate</phase>
		  <goals>
			 <goal>create</goal>
		  </goals>
	   </execution>
	</executions>
 </plugin>

Číslo buildu je potom možné také propagovat do manifestu. Stačí konfiguraci maven-jar-pluginu rozšířit takto:


<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-jar-plugin</artifactId>
   <configuration>
      <archive>
         <manifest>
            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
            <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
         </manifest>
         <manifestEntries>
            <Build-Number>${buildNumber}</Build-Number>
         </manifestEntries>
      </archive>
   </configuration>
</plugin>

Čtení takovýchto custom dat už není tak jednoduché jako v případě verzí. K tomuto účelu už budeme muset najít a přečíst MANIFEST.MF sami. Abyste nemuseli dávat kód dohromady, uvádím ten náš:


/**
 * Returns build number if it could be found - otherwise returns ? character.
 * @param moduleClass
 * @return
 */
public static String getBuildNumber(Class moduleClass) {
   String buildNumber = "?";
   try {
      ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
      String className = moduleClass.getName().replaceAll("\\.", "/") + ".class";
      if(log.isDebugEnabled()) {
         log.debug("ClassName: " + className);
      }
      URL resource = classLoader.getResource(className);
      if (resource != null) {
         String path = resource.getPath();
         if(log.isDebugEnabled()) {
            log.debug("Path to class: " + path);
         }
         int index = path.indexOf("!");
         if (index > -1) {
            String jarPath = path.substring(0, index);
            if (jarPath.startsWith("file:")) {
               jarPath = jarPath.substring(5);
            }
            if(log.isDebugEnabled()) {
               log.debug("Normalized path to jar: " + jarPath);
            }
            JarFile jar = new JarFile(jarPath);
            Manifest manifest = jar.getManifest();
            buildNumber = manifest.getMainAttributes().getValue("Build-Number");
         }
      }
   } catch (Exception ex) {
      log.error("Build number cannot be found! Due to: " + ex.getMessage(), ex);
   }
   return buildNumber;
}

Nuže a to je všechno. Vyřešili jsme další rutinní nepříjemnost vývoje.

Užitečné odkazy