简介

        如果没有JAR (Java Archive)的存在,Java也许不至于像今天这样大行天下。JAR 是一个将其他文件或JAR捆绑在一起,并以.jar做为扩展名的文件。它使用与Zip相同的格式进行压缩,所以适用于任何支持Zip压缩的系统环境。由于是压缩格式,这使得它在网络上的传输十分迅速。JAR的manifest文件使得我们可以对JAR包进行数字签名,版本控制,包密封(Seal),和设置程序的执行入口等,另外,从Java 5 开始在META-INF文件夹下生成索引文件INDEX.LIST,可以大大缩短了JAR文件中类的加载时间。

       这篇文章主要探讨一下JAR中的Package Sealing功能。这个功能是在Java 1.2引入的。在生成Jar文件时我们可以指定是否将整个Jar或者其中某几个Package进行密封,如果是将Jar文件整个进行密封,那就意味着其内所有的Package都被密封了。Package一旦密封,那么Java 虚拟机一旦成功装载密封Package中的某个类后,其后所有装载的带有相同Package名的类必须来自同一个Jar文件,否则将触发Sealing Violation安全异常。闲话不多说,让我们在如下几个方面开始Package Sealing的探秘之旅吧。

  1. Manifest的Package Sealing 配置
  2. Java源代码解析 Sealing Violation 
  3. Package Sealing 的益处
  4. Sealing Violation实例

 

Manifest的Package Sealing 配置

我们先看看 JAR文件是如何通过manifest文件来对Jar文件中的Package进行密封。主要有两种密封方式

Manifest-Version: 1.0

Name: com/tr/algorithm/test/box/
Sealed: true

Name: com/tr/algorithm/search/
Sealed: true

Name: com/tr/multiThreads/
Sealed: true

Name: com/tr/algorithm/BinaryTree/
Sealed: true



上面的manifest文件中

针对某几个package进行了密封操作,但下面的manifest文件内容是对Jar文件中

所有的package进行密封


Manifest-Version: 1.0
Sealed: true


Java源代码解析 Sealing Violation 

知道了如何对Package进行密封后,我们一起来看看Java源代码中是如何触发Sealing Violation。我使用的JDK版本为1.6

java version "1.6.0_24"
Java(TM) SE Runtime Environment (build 1.6.0_24-b50)
Java HotSpot(TM) Client VM (build 19.1-b02, mixed mode)



下面是URLClassLoader中对Package Sealing进行安全校验的代码. 我添加了一些注释,方便理解.



String pkgname = name.substring(0, i);
	    Package pkg = getPackage(pkgname);
	    Manifest man = res.getManifest();
		
	    if (pkg != null) { 
             /*
                   * 如果当前试图加载的类,例如com.seal.util.DateUtil.java,所对应的Package
	           *  例如com.seal.util,已经被ClassLoader装载。
                   */		
		if (pkg.isSealed()) {
		    /*
			*如果当前装载的类,例如com.seal.util.DateUtil.java,所对应的Package com.seal.util 已经被装载且密封,
			*但当前装载的类的URI和已经装载的Package不同,通俗一点就是来自不同的Jar文件的话
			*将触发安全异常。
			*/
		    if (!pkg.isSealed(url)) {
			throw new SecurityException(
			    "sealing violation: package " + pkgname + " is sealed");
		    }

		} else { //如果这个Package已经被装载,但没有被密封。
			/*
			*查看当前正在装载的类,例如com.seal.util.DateUtil.java ,所对应的Package com.seal.util 是否需要密封。
			*如果是的话,将触发安全异常。因为,JVM不允许对已经装载的Package再进行密封。
			*/
		    if ((man != null) && isSealed(pkgname, man)) { 
			throw new SecurityException(
			    "sealing violation: can't seal package " + pkgname + 
			    ": already loaded");
		    }
		}
	    } else {
		/*
		*如果当前试图加载的类对应的Package没有被ClassLoader装载,则试图加载该Package。
		*如果Manifest文件非空,则会使用Manifest来定义这个Package,当然,这包含是否对Package进行密封。
		*/
		if (man != null) {
		    definePackage(pkgname, man, url);
		} else {
                    definePackage(pkgname, null, null, null, null, null, null, null);
                }
	    }





总的来说,有两种情况触发关于Sealing的安全异常


  1. 检查当前试图加载的类对应的Package是否已经被JVM装载且密封。如果已经被装载且密封了,但被密封的Package与当前加载的类对应的Package不是来自同一个Jar文件,将触发安全异常
  2. 检查当前试图加载的类对应的 Package是否已经被JVM装载且密封。如果已经被装载但没有被密封,但当前试图加载的类对应的Package确要试图进行密封操作,将触发安全异常。JVM不允许对已经装载但未密封的Package再进行密封操作。

Package Sealing 的益处

Package Sealing所能带来的好处主要是版本一致性. 我们知道Java 在运行时是严格按照classpath中定义的顺序进行装载和检查,尤其是现在Java开源包满天飞, 很有可能你的Java应用程序或者中间件的classpath中会在不同的Jar文件中包含同一个Package的不同版本。这会使得程序运行产生不一致性结果,很难发现。


Sealing Violation实例



我们来看看下面两个针对Sealing Violation的例子,就很容易明白。  



版本升级例子

第一例子的大背景是对发布的Jar包进行版本升级给用户使用。在版本一中我们有DateUtil和StringUtil 两个类


package com.seal.util;

public class DateUtil {
	public void DoDateStuff(){
		System.out.println("doing date stuffs in version 1.0");
	}
}



package com.seal.util;

public class StringUtil {
	public void DoStringStuff(){
		System.out.println("doing string stuffs in version 1.0");
	}
}



这两个类我们发布为sealed_v1.jar。 该Jar文件已经被密封,其Manifest文件内容如下


Manifest-Version: 1.0
Sealed: true


在版本二中,我们增加了一个类NumberUtil,同时更改了DateUtil和StringUtil的逻辑。版本二所发布的Jar文件为sealed_v2.jar。Manifest文件的内容和版本一相同。Java类代码如下



package com.seal.util;

public class DateUtil {
	public void DoDateStuff(){
		System.out.println("doing date stuffs in version 2.0");
	}
}



package com.seal.util;

public class StringUtil {
	public void DoStringStuff(){
		System.out.println("doing string stuffs in version 2.0");
	}
}



package com.seal.util;

public class NumberUtil {
	public void DoNumberStuff(){
		System.out.println("doing number stuffs in version 2.0");
	}
}



这时候,对于客户而言,可用的Jar版本有版本一sealed_v1.jar和版本二sealed_v2.jar。用户有个测试类如下


import com.seal.util.DateUtil;
import com.seal.util.NumberUtil;
import com.seal.util.StringUtil;


public class SealTestCase1 {
  
  public static void main(String[] args){
	 DateUtil du = new DateUtil();
	 du.DoDateStuff();
	 StringUtil su = new StringUtil();
	 su.DoStringStuff();
	 
	 NumberUtil nu = new NumberUtil();
	 nu.DoNumberStuff();
  }
}



我们执行Java命令编译并执行上面的测试类,假设上述两个Jar文件sealed_v1.jar和sealed_v2.jar,以及SealTestCase1类都存放在目录D:\sealingTest\case_1中。


执行如下命令编译SealTestCase1类。

javac -classpath .;D:\sealingTest\case_1\sealed_v1.jar;D:\sealingTest\case_1\sealed_v2.jar SealTestCase1.java

执行如下命令运行SealTestCase1类。

java -classpath .;D:\sealingTest\case_1\sealed_v1.jar;D:\sealingTest\case_1\sealed_v2.jar SealTestCase1

doing date stuffs in version 1.0
doing string stuffs in version 1.0
Exception in thread "main" java.lang.SecurityException: sealing violation: package com.seal.util is sealed
        at java.net.URLClassLoader.defineClass(URLClassLoader.java:234)
        at java.net.URLClassLoader.access$000(URLClassLoader.java:58)
        at java.net.URLClassLoader$1.run(URLClassLoader.java:197)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:305)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:246)
        at SealTestCase1.main(SealTestCase1.java:14)





public class SealTestCase1 {

  public static void main(String[] args){
	 /*
	  * JVM 装载 com.seal.util.DateUtil类,由于Classpath 中sealed_v1.jar在最前面,
	  * 所以,com.seal.util.DateUtil类来自于sealed_v1.jar。我们知道sealed_v1.jar是被整个密封的。
	  * JVM 会将Package com.seal.util 进行装载并密封。 
	  */
	 DateUtil du = new DateUtil();
	 du.DoDateStuff();

         /*
          * 同样,sealed_v1.jar在classpath中比sealed_v2.jar靠前,
          * 类 com.seal.util.StringUtil 也会在sealed_v1.jar中进行装载。
          * 由于Package com.seal.util之前已经被装载并密封,而且类com.seal.util.StringUtil与
          * 类com.seal.util.DateUtil 来自于相同的Jar文件。所以,不会出现什么问题。顺利通过安全管理器的校验。
          */
	 StringUtil su = new StringUtil();
	 su.DoStringStuff();

	 /*
	  * JVM试图装载类com.seal.util.NumberUtil时,发现该类根本没在sealed_v1.jar中
	  * 所以,继续查找sealed_v2.jar,并找到该类。
	  * 但JVM发现之前装载的Package com.seal.util 已经被装载并密封,而且来自sealed_v1.jar,
	  * 而这个类确来自sealed_v2.jar。这是JVM会报安全异常错误。
	  */
	 NumberUtil nu = new NumberUtil();
	 nu.DoNumberStuff();
  }
}



了解到上面的原因后,我们可以

将sealed_v1.jar和sealed_v2.jar在classpath中的位置调换,然后再执行一次

java -classpath .;D:\sealingTest\case_1\sealed_v2.jar;D:\sealingTest\case_1\sealed_v1.jar SealTestCase1

doing date stuffs in version 2.0
doing string stuffs in version 2.0
doing number stuffs in version 2.0



我们看到结果正常显示。我们可以看到Package Sealing 保证了版本的一致性。


第三方发布 vs官方发布



第二个例子是某官方发布了一个密封的Jar文件 sealed_official_release.jar。其类如下


package com.seal.util;

public class DateUtil {
	public void DoDateStuff(){
		System.out.println("doing date stuffs officially");
	}
}



package com.seal.util;

public class NumberUtil {
	public void DoNumberStuff(){
		System.out.println("doing number stuffs officially");
	}
}



package com.seal.util;

public class StringUtil {
	public void DoStringStuff(){
		System.out.println("doing string stuffs officially");
	}
}



然后,该官方发布的Jar文件,被某第三方组织更改和发布。Jar文件的名称为 unsealed_3rdparty_release.jar。请注意,与原来官方发布的Jar的另一个不同是

这个Jar文件是一个未密封的Jar文件。内容如下,


package com.seal.util;

public class DateUtil {
	public void DoDateStuff(){
		System.out.println("doing modified date stuffs by third party");
	}
}



package com.seal.util;

public class StringUtil {
	public void DoStringStuff(){
		System.out.println("doing modified string stuffs by third party");
	}
}




那这时,用户手里可能有这两个不同的Jar文件,并且需要运行如下程序


import com.seal.util.DateUtil;
import com.seal.util.NumberUtil;
import com.seal.util.StringUtil;




public class SealTestCase2 {
	  public static void main(String[] args){
			 DateUtil du = new DateUtil();
			 du.DoDateStuff();
			 StringUtil su = new StringUtil();
			 su.DoStringStuff();
			 
			 NumberUtil nu = new NumberUtil();
			 nu.DoNumberStuff();
		  }
}



我们假设上述两个Jar文件sealed_official_release.jar, unsealed_3rdparty_release.jar和类SealTestCase2.java 都在目录D:\sealingTest\case_2中


运行如下命令,编译类SealTestCase2.java。

javac -classpath .;D:\sealingTest\case_2\unsealed_3rdparty_release.jar;D:\sealingTest\case_2\sealed_official_release.jar SealTestCase2.java

运行如下命令运行SealTestCase2.java。

java -classpath .;D:\sealingTest\case_2\unsealed_3rdparty_release.jar;D:\sealingTest\case_2\sealed_official_release.jar SealTestCase2

doing modified date stuffs by third party
doing modified string stuffs by third party
Exception in thread "main" java.lang.SecurityException: sealing violation: can't
 seal package com.seal.util: already loaded
        at java.net.URLClassLoader.defineClass(URLClassLoader.java:242)
        at java.net.URLClassLoader.access$000(URLClassLoader.java:58)
        at java.net.URLClassLoader$1.run(URLClassLoader.java:197)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:305)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:246)
        at SealTestCase2.main(SealTestCase2.java:15)





这个Sealing Violation 正是JDK中定义的第二种情况。请看如下的代码注释来进一步理解这个Sealing Violation的产生的根本原因。





public class SealTestCase2 {
	  public static void main(String[] args){
			 /*
			  * JVM 装载 com.seal.util.DateUtil类,由于Classpath 中unsealed_3rdparty_release.jar在最前面,
			  * 所以,com.seal.util.DateUtil类来自于unsealed_3rdparty_release.jar。
			  * 我们知道unsealed_3rdparty_release.jar是没有被密封的。
			  * JVM 会将Package com.seal.util 进行装载但不会密封它。 
			  */
			 DateUtil du = new DateUtil();
			 du.DoDateStuff();
		         /*
		          * 同样,unsealed_3rdparty_release.jar在classpath中比sealed_official_release.jar靠前,
		          * 类 com.seal.util.StringUtil 也会在unsealed_3rdparty_release.jar中进行装载。
		          * 由于Package com.seal.util之前已经被装载但没有密封,
		          * 所以,不会出现什么问题。顺利通过安全管理器的校验。
		          */
		     
			 StringUtil su = new StringUtil();
			 su.DoStringStuff();
			 /*
			  * JVM试图装载类com.seal.util.NumberUtil时,发现该类根本没在unsealed_3rdparty_release.jar中
			  * 所以,继续查找sealed_official_release.jar,并找到该类。请注意sealed_official_release.jar
			  * 是被密封的。所以,JVM试图对Package com.seal.util 进行密封。
			  * 但JVM发现Package com.seal.util 已经被装载。
			  * 这时,JVM会报安全异常错误。
			  */
			 NumberUtil nu = new NumberUtil();
			 nu.DoNumberStuff();	
		  }
  
}







让我们将Jar文件sealed_official_release.jar, unsealed_3rdparty_release.jar的顺序在classpath中调换位置,运行如下命令



java -classpath .;D:\sealingTest\case_2\sealed_official_release.jar;D:\sealingTest\case_2\unsealed_3rdparty_release.jar SealTestCase2


doing date stuffs officially
doing string stuffs officially
doing number stuffs officially



我们看到客户类SealTestCase2执行的所有类是来自官方发布的Jar文件。从而,保证了版本的一致性。