How do you create a MANIFEST.MF that's available when you're testing and running from a jar in production?

ε祈祈猫儿з 提交于 2019-11-29 00:18:00

问题


I've spent far too much time trying to figure this out. This should be the simplest thing and everyone who distributes Java applications in jars must have to deal with it.

I just want to know the proper way to add versioning to my Java app so that I can access the version information when I'm testing, e.g. debugging in Eclipse and running from a jar.

Here's what I have in my build.xml:

<target name="jar" depends = "compile">
    <property name="version.num" value="1.0.0"/>
    <buildnumber file="build.num"/>
    <tstamp>
        <format property="TODAY" pattern="yyyy-MM-dd HH:mm:ss" />
    </tstamp>

    <manifest file="${build}/META-INF/MANIFEST.MF">
        <attribute name="Built-By" value="${user.name}" />
        <attribute name="Built-Date" value="${TODAY}" />                   
        <attribute name="Implementation-Title" value="MyApp" />
        <attribute name="Implementation-Vendor" value="MyCompany" />                
        <attribute name="Implementation-Version" value="${version.num}-b${build.number}"/>                              
    </manifest>

    <jar destfile="${build}/myapp.jar" basedir="${build}" excludes="*.jar" />                   
</target>

This creates /META-INF/MANIFEST.MF and I can read the values when I'm debugging in Eclipse thusly:

public MyClass()
{
    try
    {                        
        InputStream stream = getClass().getResourceAsStream("/META-INF/MANIFEST.MF");
        Manifest manifest = new Manifest(stream);            

        Attributes attributes = manifest.getMainAttributes();

        String implementationTitle = attributes.getValue("Implementation-Title");
        String implementationVersion = attributes.getValue("Implementation-Version");
        String builtDate = attributes.getValue("Built-Date");
        String builtBy = attributes.getValue("Built-By");
   }
   catch (IOException e)
   {            
        logger.error("Couldn't read manifest.");
   }        

}

But, when I create the jar file, it loads the manifest of another jar (presumably the first jar loaded by the application - in my case, activation.jar).

Also, the following code doesn't work either although all the proper values are in the manifest file.

    Package thisPackage = getClass().getPackage();
    String implementationVersion = thisPackage.getImplementationVersion();

Any ideas?


回答1:


You can get the manifest for an arbitrary class in an arbitrary jar without parsing the class url (which could be brittle). Just locate a resource that you know is in the jar you want, and then cast the connection to JarURLConnection.

If you want the code to work when the class is not bundled in a jar, add an instanceof check on the type of URL connection returned. Classes in an unpacked class hierarchy will return a internal Sun FileURLConnection instead of the JarUrlConnection. Then you can load the Manifest using one of the InputStream methods described in other answers.

@Test
public void testManifest() throws IOException {
    URL res = org.junit.Assert.class.getResource(org.junit.Assert.class.getSimpleName() + ".class");
    JarURLConnection conn = (JarURLConnection) res.openConnection();
    Manifest mf = conn.getManifest();
    Attributes atts = mf.getMainAttributes();
    for (Object v : atts.values()) {
        System.out.println(v);
    }
}



回答2:


You want to use this:

Enumeration<URL> resources = Thread.currentThread().getContextClassLoader().getResources("META-INF/MANIFEST.MF");

You can parse the URL to figure out WHICH jar the manifest if from and then read the URL via getInputStream() to parse the manifest.




回答3:


Here's what I've found that works:

packageVersion.java:

package com.company.division.project.packageversion;

import java.io.IOException;
import java.io.InputStream;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

public class packageVersion
{
    void printVersion()
    {
        try
        {         
            InputStream stream = getClass().getResourceAsStream("/META-INF/MANIFEST.MF");

            if (stream == null)
            {
                System.out.println("Couldn't find manifest.");
                System.exit(0);
            }

            Manifest manifest = new Manifest(stream);

            Attributes attributes = manifest.getMainAttributes();

            String impTitle = attributes.getValue("Implementation-Title");
            String impVersion = attributes.getValue("Implementation-Version");
            String impBuildDate = attributes.getValue("Built-Date");
            String impBuiltBy = attributes.getValue("Built-By");

            if (impTitle != null)
            {
                System.out.println("Implementation-Title:   " + impTitle);
            }            
            if (impVersion != null)
            {
                System.out.println("Implementation-Version: " + impVersion);
            }
            if (impBuildDate != null)
            {
                System.out.println("Built-Date: " + impBuildDate);
            }
            if (impBuiltBy != null)
            {
                System.out.println("Built-By:   " + impBuiltBy);
            }

            System.exit(0);
        }
        catch (IOException e)
        {            
            System.out.println("Couldn't read manifest.");
        }        
    }

    /**
     * @param args
     */
    public static void main(String[] args)
    {
        packageVersion version = new packageVersion();
        version.printVersion();        
    }

}

Here's the matching build.xml:

<project name="packageVersion" default="run" basedir=".">

    <property name="src" location="src"/>
    <property name="build" location="bin"/>
    <property name="dist" location="dist"/>

    <target name="init">
        <tstamp>
            <format property="TIMESTAMP" pattern="yyyy-MM-dd HH:mm:ss" />
        </tstamp>
        <mkdir dir="${build}"/>
        <mkdir dir="${build}/META-INF"/>
    </target>

    <target name="compile" depends="init">
        <javac debug="on" srcdir="${src}" destdir="${build}"/>
    </target>

    <target name="dist" depends = "compile">        
        <mkdir dir="${dist}"/>      
        <property name="version.num" value="1.0.0"/>
        <buildnumber file="build.num"/>
        <manifest file="${build}/META-INF/MANIFEST.MF">
            <attribute name="Built-By" value="${user.name}" />
            <attribute name="Built-Date" value="${TIMESTAMP}" />                                
            <attribute name="Implementation-Vendor" value="Company" />
            <attribute name="Implementation-Title" value="PackageVersion" />
            <attribute name="Implementation-Version" value="${version.num} (b${build.number})"/>
            <section name="com/company/division/project/packageversion">
                <attribute name="Sealed" value="false"/>
            </section>          
        </manifest>     
        <jar destfile="${dist}/packageversion-${version.num}.jar" basedir="${build}" manifest="${build}/META-INF/MANIFEST.MF"/>                 
    </target>

    <target name="clean">
        <delete dir="${build}"/>
        <delete dir="${dist}"/>
    </target>

    <target name="run" depends="dist">      
        <java classname="com.company.division.project.packageversion.packageVersion">
            <arg value="-h"/>
            <classpath>
                <pathelement location="${dist}/packageversion-${version.num}.jar"/>
                <pathelement path="${java.class.path}"/>
            </classpath>
        </java>
    </target>

</project>



回答4:


You can access the manifest (or any other) file within a jar if you use the same class loader to as was used to load the classes.

this.getClass().getClassLoader().getResourceAsStream( ... ) ;

If you are multi-threaded use the following:

Thread.currentThread().getContextClassLoader().getResourceAsStream( ... ) ;

This is also a realy useful technique for including a default configuration file within the jar.




回答5:


ClassLoader.getResource(String) will load the first manifest it finds on the classpath, which may be the manifest for some other JAR file. Thus, you can either enumerate all the manifests to find the one you want or use some other mechanism, such as a properties file with a unique name.




回答6:


I've found the comment by McDowell to be true - which MANIFEST.MF file gets picked up depends on the classpath and might not be the one wanted. I use this

String cp = PCAS.class.getResource(PCAS.class.getSimpleName() + ".class").toString();
cp = cp.substring(0, cp.indexOf(PCAS.class.getPackage().getName())) 
              +  "META-INF/MANIFEST.MF";
Manifest mf = new Manifest((new URL(cp)).openStream());

which I adapted from link text




回答7:


Just don't use the manifest. Create a foo.properties.original file, with a content such as version=@VERSION@

And in ther same task you are jaring you can do a copy to copu foo.properties.original and then




回答8:


I will also usually use a version file. I will create one file per jar since each jar could have its own version.




回答9:


You can use a utility class Manifests from jcabi-manifests that automates finding and parsing of all MANIFEST.MF files available in classpath. Then, you read any attribute with a one liner:

final String name = Manifests.read("Build-By");
final String date = Manifests.read("Build-Date");

Also, check this out: http://www.yegor256.com/2014/07/03/how-to-read-manifest-mf.html



来源:https://stackoverflow.com/questions/84486/how-do-you-create-a-manifest-mf-thats-available-when-youre-testing-and-running

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!