类加载器
虚拟机加载某个类所依赖的类的过程叫做类的解析,类加载机制可以加载磁盘中的文件或者网络中的文件。如果类A的域是其他的类,或者类A有父类,那么这些类也会被加载。然后虚拟机执行A的main方法,如果在main方法中和main方法调用的方法中用到了其他的类,那么接下来又会加载这些用到的类。每个java程序至少有3种类加载器:
类别 | 作用 |
---|---|
引导类加载器 | Bootstrap类加载器,负责加载系统类(jre/lib/rt.jar 中的类),C语言实现的,该加载器没有对应的ClassLoader对象,String.class.getClassLoader() 将返回null |
扩展类加载器 | 加载jre/lib.ext 中的类,Oracle的jdk中,使用java实现,URLClassLoader的实例 |
系统类加载器 | 也称应用类加载器,加载应用类,即classpath中的类,Oracle的jdk中,使用java实现,URLClassLoader的实例 |
- 层次结构
类加载器之间有一种父子关系,除了系统类加载器,每个类加载器都有一个父类加载器。当使用一个类加载器来加载类时,首先会委托它的父类加载器去加载,如果父类加载器加载失败,则自己加载。
使用类加载器加载指定URL中的类文件
URL url = new URL("file:///path/to/plugin.jar");
URL[] urls = {url};
URLClassLoader pluginLoader = new URLClassLoader(urls);
Class> c = pluginLoader.loadClass("mypackage.Myclass");
ClassLoader和URLClassLoader
看名字都带有ClassLoader,叫做类加载器,事实上是可以理解为动态的加载类,不过,也不是只能加载类,也可以加载其他形式的文件,比如说.properties属性文件。
区别:其实在两个类加载器有一点小区别,就在于能够加载的类存放的位置,从JDK源码上来看其实是URLClassLoader继承了ClassLoader,也就是说URLClassLoader把ClassLoader扩展了一下,所以可以理解成URLClassLoader功能要多点。
ClassLoader只能加载classpath下面的类,而URLClassLoader可以加载任意路径下的类。他们的继承关系如下
public class URLClassLoader extends SecureClassLoader {}
public class SecureClassLoader extends ClassLoader {}
URLClassLoader是在java.net包下的一个类。一般动态加载类都是直接用Class.forName()这个方法,但这个方法只能创建程序中已经引用的类,并且只能用包名的方法进行索引,比如Java.lang.String,不能对一个不在程序引用里的.jar包中的类进行创建。
URLClassLoader提供了这个功能,它让我们可以通过以下几种方式进行加载:
- 文件: (从文件系统目录加载)
- jar包: (从Jar包进行加载)
- Http: (从远程的Http服务进行加载)
当class文件或者resources资源文件更新后,我们需要重新加载这些类或者Jar。从理论上来说,当应用清理了对所加载的对象的引用,那么垃圾收集器就会将这些对象给收集掉,然后我们再重新加载新的JAR文件,并创建一个新的URLClassLoader来加载。可是这里有一个问题,就是我们不知道垃圾收集器什么时候将那些未被引用的对象给收集掉,特别是在Windows中,因为在Windows中打开的文件是不可以被删除或被替换的。
在Java7的Build 48版中,URLClassLoader提供了close()这个方法,可以将打开的资源全部释放掉,这个给开发者节省了大量的时间来精力来处理这方面的问题。
URL url = new URL("file:foo.jar");
URLClassLoader loader = new URLClassLoader (new URL[] {url});
Class cl = Class.forName ("Foo", true, loader);
Runnable foo = (Runnable) cl.newInstance();
foo.run();
loader.close ();
// foo.jar gets updated somehow
loader = new URLClassLoader (new URL[] {url});
cl = Class.forName ("Foo", true, loader);
foo = (Runnable) cl.newInstance();
// run the new implementation of Foo
foo.run();
- 线程的类加载器
每个线程都有一个对类加载器的引用,称为该线程的上下文类加载器。主线程的类加载器是系统类加载器,子线程的类加载器继承自父线程的类加载器,当然我们也可以设置/获取线程的类加载器
Thread t = Thread.currentThread();
t.setContextClassLoader(loader);
ClassLoader loader = t.getContextClassLoader();
Class c = loader.loadClass(className);
特别之处
程序在运行时,所有的类名都是全限定类名,即包括包名,所以打印的时候是全限定名。一个虚拟机中可以同时存在两个包名和类名都一样的类,因为类是由它的全名和加载它的类加载器来确定的。编写自己的类加载器
只需要继承ClassLoader类并重写findClass
方法,类加载器的loadClass
首先将任务委托给父类加载器,如果父类加载器无法加载,则调用findClass
方法。findClass的操作是,先读取类文件(.class,如果加密过了的话,也可能是其他后缀,那就需要读取到字节,解密后再传给defineClass方法)的字节,然后调用defineClass
方法
public MyClassLoader extends ClassLoader {
protected Class> findClass(String name) throws ClassNotFoundException {
try{
String cname = f(name);//根据类名name获取类文件路径
Bytes bytes[] = Files.readAllBytes(Paths.get(cname));
Class> c = defineClass(name, bytes, 0, bytes.length);
if(c = null) throw new ClassNotFoundException();
}catch(IOException e){
throw new ClassNotFoundException(name);
}
}
}
字节码校验
当类加载器将字节码传递给虚拟机时,字节码首先需要接受校验器(verifier)的校验,检验器负责检查那些无法执行的具有明显的破坏性的操作,除了系统类外,所有的类都要被校验,下面是一些检查,如果有一条没有通过,就不予加载。其实这些检查编译器就做了,但为何还要检验器呢?因为编译之后的类文件可能被了解字节码文件的人修改,可能会恶意插入一些破坏性的操作
- 变量要在使用之前初始化
- 方法调用要与对象引用类型匹配
- 访问私有数据与和方法的规则没有被违反
- 对本地变量的访问都落在运行时堆栈内
- 运行时堆栈没有溢出
安全管理器和访问权限
校验器是第一道防线,安全管理器是第二道防线。安全管理器类负责控制具体的操作是否允许执行。在java的早起版本,本地类具有全部权限,远程类只能在沙箱中运行,这种“要么全有,要么全都没有”的机制很不好,后来加入一种机制,建立代码来源和权限的映射,给不同的来源的代码分配不同的权限,这就涉及到权限类了。下面是一个典型的策略文件(给来自这个URL的代码给与在temp目录下读写的权限),这种方式有个缺点,当代码来源改变时,又需要重新配置,后面讲的代码签名的方式更好。
grant codeBase "http://www.xx.yy/classses"
{
permission java.io.FilePermission "/temp/*", "read,write";
}
消息摘要
消息摘要是数据块的数字指纹,任何一个给定的数据块(无论多大),根据一定的算法,压缩成一个唯一的字节序列。MD5算法是16个字节,SHA1(安全散列算法)是20个字节(虽然说理论上存在两条不同的消息的消息摘要会一样,但是2的160次方足够大了,足够目前的使用),消息摘要的作用是确保数据块没有被篡改或损坏。
InputStream in = Files.newInputStream(Paths.get("myfile"));
//根据算法名称获取MessageDigest对象
MessageDigest md = MessageDigest.getInstance("SHA-1");
int c;
while((c = in.read()) != -1) {
md.update((byte)c);//也可以先读取到一个字节数组bytes里,然后依次性update,如md.update(bytes)
}
byte[] hash = md.digest();//获取消息摘要
//转为十六进制形式
StringBuffer sb = new StringBuffer();
for(int i = 0;i < bytes.length;i++){
int v = byte[i] & oxFF;
if(v < 16) {sb.append("0");}
sb.append(Integer.toString(v, 16).toUpperCase());
}
System.out.println(sb.toString());
消息签名
公钥和私钥。A使用私钥签名,B使用A的公钥解密,如果能匹配(能用公钥打开),说明消息确实来自A。签名的作用是身份认证。广泛的使用情景是,A去权威认证机构注册一个数字证书(包含A的注册信息和A的公钥),一般我们在访问某个网站或者安装某个软件的时候,会有数字证书的问题。B安装A的数字证书的时候,B可以看到这是哪家认证机构认证的,是谁的数字证书,如果相信就安装,安装后就代表B信任A。
加密
加密分为对称加密(加密和解密的密码相同)和非对称加密(加密和解密的密码不同,一般是公钥加密,私钥解密;私钥加密,公钥解密这种情况的作用是身份认证,使用openssl ,keytools等工具生成一对公私钥对)。对称加密比非对称加密快,所以一般数据量大使用对称加密,数据量小使用非对称加密。一般是这样,比如A和B通信,A发送数据data给B
data生成消息摘要digest,使用密码key(对称加密)加密,使用B的公钥B-public-key对密码key加密,并使用A的私钥A-private-key进行签名,然后发送给B(包括数据data,数据摘要digest,和密码key),B使用A的公钥A-public-key进行身份认证,确定消息来自A,然后使用B的私钥B-private-key对密码key进行解密,然后使用密码key对data和digest进行解密,然后对data重新进行消息摘要生成digest1,对比digest和digest1就能确定data有没有被篡改和损坏。
//加密代码,解密代码一样,只是模式换成Cipher.DECRYPT_MODE
KeyGenerator kegen = new KeyGenerator("AES");//AES密码生成器
SecureRandom random = new SecureRandom();//SecureRandom类比Random更随机
kegen.init(random);
SecretKey key = kegen.generateKey();//生成AES加密方式的密码
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);//指定为加密模式,Cipher.DECRYPT_MODE为解密模式
InputStream in = ...;//要加密的文件
OutputStream out = ...;//加密后文件
int blockSize = cipher.getBlockSize();
int outputSize = cipher.getOutputSize(blockSize);
byte[] inBytes = new byte[blockSize];
byte[] outBytes = new byte[outputSize];
int inLength = 0;
boolean more = true;
while(more){
inLength = in.read(inBytes);
if(inLength == blockSize) {
int outlength = cipher.update(inBytes,0,blockSize,outBytes);
out.write(outBytes,0,outLength);
}else{more = false}
}
if(inLength > 0) {outBytes = cipher.doFinal(inBytes,0,inLength);}
else{outBytes = cipher.doFinal();}
out.write(outBytes);
//使用非对称加密对密码进行加密,解密将模式设为UNWRAP_MODE
KeyPairGenerator kegen = KeyPairGenerator.getInstance("RSA")//RSA算法方式
SecureRandom random = new SecureRandom();
int keySize = 512;
kegen.initialize(keySize,random);
PairKey keyPair = kegen.generateKeyPair();
Key publicKey = keyPair.getPublic();
Key privateKey = keyPair.getPrivate();
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.WRAP_MODE,publicKey);
Key key = ...;//密码
byte[] wrappedKey = cipher.wrap(key);//用公钥加密密码,解密为Key key = cipher.unwrap(keyBytes),其中cipher的模式为UNWRAP_MODE
SecureRandom
SecureRandom类产生的随机数远比Random产生的安全(更随机),需要提供一个种子(20位),以便在一个随机点上生成数字序列。最好的方式是从诸如白噪音发生器等设备哪里获取输入,或者从用户盲打的键盘输入中提取,收集完种子后传给setSeed方法
SecureRandom random = new SecureRandom();
byte[] b = new byte[20];
b = ...;//填充字节数组b
random.setSeed(b);
密码流
//密码输入流为CipherInputStream,操作一样,只是Cipher模式不一样
Cipher cipher = ...;
cipher.init(Cipher.ENCRYPT_MODE,key);
File file= new File("加密文件.txt");
CipherOutputStream out = new CipherOutputStream(new FileOutputStream(file), cipher);
InputStream in = new FileInputStream(new File("原始文件.txt"));
byte[] bytes = new byte[512];
int len = in.read(bytes);
while(len != -1) {
out.write(bytes,0,len);
}
out.flush();