1. 需要将应用程序拆分为多个独立组件的原因:
a) 不同应用程序可以共享相同组件。不必为每个应用程序都不熟组件的一个独立副本。
b) 应用程序随时间而改变。如果对应用程序的一个组件做了微笑更改,就应该能在不重新部署整个应用程序的情况下,重新部署该组件。
c) 应用程序是动态的。根据运行时的条件,应用程序可能以开发期间完全未料到的方式加入新资源(数据和代码)
d) 应用程序使用诸如图形图像的资源。这些资源不由编程人员管理,不需要编程人员介入即可配置。
e) 应用程序一般包含部署人员指定的配置设置。这些设置是应用程序的逻辑部分,但只在运行时才可用。
2. 在Java平台上,要在运行时组装应用程序,将会遇到这些问题:
a) 组件平台应该实现一个基础结构来定位应用程序的各个组件,以重新进行组装。
b) 在找到组件后,平台应验证这个组件是否真能与应用程序共同工作。(配置问题,安全问题)
3. 类加载器体系结构的目标是实现透明性,可扩展性,功能丰富和可配置性。另外,需要适当处理命名冲突和版本不兼容的问题,同时不影响共享从不同位置,按不同方式加载的资源的能力。最后,必须定义和实施一些安全措施,以确定一些特定的控制机制,说明动态加载的类可以做什么,不可以做什么。
4. 隐式类加载:当一个类引用另一个类时,则使用加载引用类的同一个类加载器隐式加载引用。触发隐式加载的是引用的编译时类型。引用类型(Class,Object)和所引用类(LoadMeThree)之间的微妙区别允许隐式和显式类加载共存。
代码事例:
public class LoadMe extends LoadMeBase{
static LoadMeToo lmt = new LoadMeToo();
static LoadMeAlso lma;
static ClassLoader ldr = getSomeLoader();
static Class cls = ldr.loadClass("LoadMeThree");
static Object obj = cls.newInstance();
}
注:所有的红色的类,都将会使用装载LoadMe的类装载器加载,因为,他们都被LoadMe所引用。而
LoadMeAlso由于只被初始化为null,因此将不会得到加载。而LoadMeThree,由于不是LoadMe所直
接引用的编译时类型,因此可以被其他的类加载器(ldr)加载。
5. 类加载器规则:
a) 一致性规则:类加载器不能多次加载同一个类。
b) 委托规则:在加载一个类之前,类加载器总参考父类加载器。
c) 可见性规则:类只能看到由其类加载器所委托加载的其他类,委托是类的加载器及其所有父类加载器的递归集。
6. 委托用作命名空间
类加载器委托构成了命名空间。当考虑类加载,则命名空间的概念不仅包含类的包名,还包括加载该类的类加载器。
public class LoadMe{
static{
System.out.println(LoadMe.class + " loadded");
}
public LoadMe(){
System.out.println("New Instance Created");
}
}
import java.net.*;
public class LoadDemo{
public static void main(String[] args) throws Exception{
URL url = new URL("file:sub/");
URL[] urls = new URL[]{url};
URLClassLoader loader1 = new URLClassLoader(urls);
URLClassLoader loader2 = new URLClassLoader(urls);
Class c1 = loader1.loadClass("LoadMe");
Class c2 = loader2.loadClass("LoadMe");
Object o1 = c1.newInstance();
Object o2 = c2.newInstance();
System.out.println("(c1 == c2) is "+(c1==c2));
}
}
第一次运行:
设定:CLASSPATH=.;
输出: class LoadMe loadded
New Instance Created
class LoadMe loadded
New Instance Created
(c1 == c2) is false
说明:当需要加载LoadMe时,装载LoadMeDemo的类装载器将先委托她的父类加载器,即系统类加载
器进行装载,而由于她在当前的类路径下没有找到该类,于是类的装载将分别由loader1和loader2
完成,于是,我们就能在输出中看到LoadMe被装载了两次。而且c1与c2不是同一个。这里之所以
不是同一个是因为把类加载器也作为了名字空间的一部分。
第二次运行:CLASSPATH=.;…/sub/;
输出: class LoadMe loadded
New Instance Created
New Instance Created
(c1 == c2) is true
说明:这个与上文同,当委托到系统类加载器时,她在…/sub/下加载到了该类,因此并没有使用
loader1和loader2对LoadMe进行加载,因此输出中就只有一句“class LoadMe loadded”,而且
c1与c2也是相同的。
小结:从上面的情况可以看到,由于委托模型的存在,当我们希望使用自己的类加载器对类进行加载
时,绝对不能把类放到类路径下,否则,类的加载将会被所委托的其他父类加载器所完成。
7. 热部署
热部署是指在不关闭虚拟机的情况下重新部署类的少许更改。
public interface Point{
public void emit();
}
public class PointImpl implements Point{
public void emit(){
System.out.println("Impl 1");
}
}
import java.net.*;
public class PointServer{
private ClassLoader loader;
private Class cl;
public Point getPoint(Object o) throws Exception{
if(o == null)
reload();
Point p = (Point)cl.newInstance();
return p;
}
public void reload() throws Exception{
URL[] urls = new URL[]{new URL("file:sub/")};
loader = new URLClassLoader(urls);
cl = loader.loadClass("PointImpl");
}
}
import java.io.*;
public class PointClient{
public static Point pt;
public static void main(String[] args) throws Exception{
PointServer server = new PointServer();
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
pt = server.getPoint(null);
System.out.println(pt);
while(true){
System.out.println("exit,emit,reload");
String cmd = br.readLine();
if(cmd.equals("exit")){
return;
}else if(cmd.equals("emit")){
pt.emit();
}else if(cmd.equals("reload")){
server.reload();
pt = server.getPoint(pt);
System.out.println(pt);
}
}
}
}
运行:
设定:CLASSPATH=.;
Point,PointServer和PointClient在同一个目录,PointImpl放在下一层目录。
输出:PointImpl@3e 25a5
exit,emit,reload
emit
Impl 1
exit,emit,reload (修改PointImpl中emit()方法的实现,并重新装载)
reload
PointImpl@42e816
exit,emit,reload
emit
Impl 2
exit,emit,reload
exit
说明:为了使得PointClient能够使用PointImpl的不同版本,我们在定义pt的类型时不能定义为
PointImpl类,因为这样会触发隐式加载,使得加载PointClient的类加载器加载了PointImpl,从
而受一致性规则所限,不能加载新PointImpl版本。
8. 使用热部署
在自己的应用程序当中使用热部署时,要注意以下几项:
a) 客户程序决不能引用需要动态替换的类型,否则,将导致两种后果之一:客户程序将使用类加载器隐式加载类(类未加载的情况),或根本不能加载类(类已加载的情况)。
b) 客户程序不能使用实现类型的引用,必须使用基类或者接口类型的引用。
c) 实现类必须能找到查找客户程序正在使用的基类接口的相同版本。换言之,实现类的加载器必须委托到客户程序的类加载器。(由于PoingClient和Point都在类路径上,所以都是由系统类加载器加载的,当使用URLClassLoader加载新的PointImpl时,由于他实现了Point接口,因此为了验证PointImpl是Point的有效扩展,就需要加载Point,而由于委托规则,这将有URLClassLoader的父类加载器,即系统类加载器完成,而由于Point已经早已被系统类加载器加载,因此这部可以成功通过。)
9. 引导类路径,扩展路径和类路径
引导类加载器从引导类路径(bootclasspath)加载内核系统包,扩展类加载器从扩展路径加载内核API的扩展,系统类加载器从类路径加载应用程序代码。
委托到 |
委托到 |
引导类加载器 |
扩展加载器 |
系统类加载器 |
内核APIs(如rt.jar) java.lang java.io等 |
默认扩展(如jre/lib/ext) |
类路径类 nick.ui等 |
加载 |
加载 |
加载 |
10. 类路径
设置 |
解释 |
-jar选项 |
只列出单个JAR |
-cp选项 |
列出JAR和目录 |
CLASSPATH环境变量 |
不推荐使用 |
当前目录 |
不推荐使用 |
若设置CLASSPATH环境变量,则总可能有另一个应用程序或用户将其重新设置为指向其他位置。同样,也不能依赖于隐式使用当前目录,如果系统上的任何参与者设置了CLASSPATH,则将忽略当前目录。
11. 扩展路径
将类放在扩展路径与放在类路径有所不同。主要体现在
a) 易于一次性指定大量JAR文件。
b) 扩展可通过所有安全检查。
在命令行设置java.ext.dirs属性可覆盖默认设置,如:
java –Djava.ext.dirs=mypackage1;mypackage2 MyMainClass
将使用mypackage1和mypackage2作为扩展路径。
12. 引导类路径
引导类加载器有两个不同于其他所有类加载器的显著特征,二者都与安全性相关:
a) 引导类加载器加载的类不经过有效性检验;换言之,虚拟机假设他们是结构完好的二进制类。
b) 引导类加载器加载的类不必经过安全检查;这与扩展类加载器加载的己加载可选包存在微妙差别;程序从不检查引导类的权限;但已安装可选包要求具备权限,不过,已安装可选包默认的具有相关权限。
在命令行设置-Xbootclasspath可以修改默认的引导类路径,如:
java –Xbootclasspath:nrt.jar MyMainClass
将使用nrt.jar作为引导类。
13. 调试类加载结构
我们可以使用三种调试类加载问题的策略:
a) 改编应用程序。
b) 使用-verbose:class标志。
c) 改变内核API
对于第一种方法,是指我们可以通过XX.class.getClassLoader()的方法,得到装载某个类的类装载器的引用,然后在通过递归调用getParent()的方法,获取整个委托链。
ClassLoader cl = XX.class.getClassLoader();
while(cl != null){
System.out.println(cl);
cl = cl.getParent();
}
对于第二种方法,是指在通过java命令运行程序时,增加一个参数,从而观察输出的消息。
java –verbose:class XX
对于第三种方法,是指修改内核API的源代码,然后通过增加备用引导类路径,从而实现内核API代码的替换。其中修改后的内核API放到somewhere目录下。
java –Xbootclasspath/p:somewhere XX
14. 倒置问题和上下文类加载器
根据可见性规则,如果类A的加载器是类B的加载器的子类。那么类A可以看到所有由类B的加载器所加载的类,但相反的,类B并不能看到由类A所加载的类。
不会引起导致的合法置换
类A和类B之间的关系 |
有效的类加载器关系 |
无关系 |
任意 |
A包含B类型的字段/变量 |
CLa等于/源于CLb |
A扩展B |
CLa等于/源于CLb |
A,B互相引用 |
CLa等于CLb |
注:A源于B是指B是A的父类加载器。
产生倒置引用的情况:当扩展类路径中的类需要引用在类路径当中的类。例如:
public class PointServer{
public static Point getPoint(String classname) throws Exception{
Class c = Class.forName(classname);
return (Point)c.newInstance();
}
}
public class PoingClient{
public static void main() throws Exception{
Point p = PointServer.getPoint("PointImpl");
}
}
在上述代码当中,假设我们把PointServer放到扩展路径当中,而把PointClient和PointImpl放到类路径当中,那么当PointClient调用getPoint()方法时,将会触发PointServer当中的Class.forName()方法去装载类,由于这个方法会使用调用该方法的类(PointServer)的类装载器(扩张类装载器)去加载PointImpl类,由此将导致倒置问题的产生。
为了解决这个问题,一种方法是,我们可以在调用getPoint()时,把装载PointClient的类加载器传过去,然后在PointServer加载类时,使用该类加载器加载。但是,如果Client和Server有平凡的交互的话,这个方法将会显得非常笨重和麻烦。第二种方法是使用上下文类加载器,上下文类加载器位于当前的线程当中。修改后的版本为这样。
public class PointServer{
public static Point getPoint(String classname) throws Exception{
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class c = Class.forName(classname,true,cl);
return (Point)c.newInstance();
}
}
public class PointClient{
public static void main() throws Exception {
ClassLoader cl = PointClient.class.getClassLoader();
Thread.currentThread().setContextClassLoader(cl);
Point p = PointServer.getPoint("PointImpl");
}
}
15. 元数据,即描述二进制类结构的数据,大多数元数据都是类型信息,列出类的基类,超接口,字段和方法。类型信息通过在运行时验证客户程序和服务器共享用来通信的公共类视图,使代码的动态链接更可靠。
16. 二进制类元数据
17. 反射字段
Java语言通过Field类来表征类的字段。要获取类的字段,我们可以调用Class上的getFields()和getDeclaredFields()等方法。这两个方法的区别是:getFields()能够返回递归到基类的所有字段,但是他只能返回public修饰的字段。getDeclaredFields()只返回当前类的字段,而且是类中所有的字段,包括private,protected,public等。我们也可以通过指定字段名的方式,来获取某一个特定的字段。在获得Field的引用后,我们可以调用Field上的getType(),getName()和getModifiers()等方法来进一步获取字段的详细信息。例如:
public class Example2{
public int e;
}
public class Example extends Example2{
private int a;
protected int b;
public int c;
int d;
}
public class ExampleExplorer{
public static void main(String[] args) throws Exception{
Class c = Class.forName("Example");
Field[] fs = c.getDeclaredFields();
for(Field f:fs){
emitField(f);
}
System.out.println("============");
fs = c.getFields();
for(Field f:fs){
emitField(f);
}
}
public static void emitField(Field f){
switch(f.getModifiers()){
case Modifier.PUBLIC:
System.out.print("public ");
break;
case Modifier.PRIVATE:
System.out.print("private ");
break;
case Modifier.PROTECTED:
System.out.print("protected ");
break;
}
System.out.println(f.getType()+" "+f.getName()+";");
}
}
输出: private int a;
protected int b;
public int c;
int d;
============
public int c;
public int e;
18. 反射方法:
Java语言通过Method类来表征类的方法。要获取类的方法,我们可以调用Class上的getMethods()和getDeclaredMethods()等方法。这两个方法的区别与字段获取的类似。当然,我们也可以通过指定方法名的方式,来获取某一特定的方法,但是由于方法允许重载,因此我们除了需要传递方法名以外,还需要传递变量的类型。对于引用类型,我们可以传入他们的.class字段,例如,对于String类型,我们可以传入String.class。对于基本类型我们可以传入他们对应包装类的.TYPE字段,例如,对于int类型,我们可以传入Integer.TYPE,而对于Integer类型,我们可以传入Integer.class。在获得Method的引用后,我们可以调用getName(),getModifiers(),getReturnType()等方法来获取方法的信息。
19. 反射调用:
反射API能够报告类的字段,方法和构造函数,而反射API的调用部分使用Field,Method,Constructor类在运行时查询和修改类(和实例)的状态,包括实例化,修改和获取字段的值,调用类或实例的方法。这里需要注意的是,对类和实例的字段和方法进行操作时,要区别对待。当我们对实例的字段和方法进行操作时,我们需要传入一个对实例的引用,也就是在一般情况下,被隐式传入的this引用。而当对类的字段和方法执行操作时,我们只需要传入null即可。例如:
字段操作:
public class Example{
private int a;
private String b;
}
public class ExampleExplorer{
public static void main(String[] args) throws Exception{
Class c = Class.forName("Example");
Object o = c.newInstance();
Field fa = c.getDeclaredField("a");
Field fb = c.getDeclaredField("b");
fa.setAccessible(true);
fb.setAccessible(true);
fa.setInt(o,1);
int a = fa.getInt(o);
fb.set(o,"hello world");
String b = (String)fb.get(o);
System.out.println("a's value is "+a+" b's value is "+b);
}
}
输出: a's value is 1 b's value is hello world
注:一般情况下,我们并不能对类的私有字段进行操作,利用反射也不例外,但有的时候,例如要序列化的时候,我们又必须有能力去处理这些字段,这时候,我们就需要调用AccessibleObject上的setAccessible()方法来允许这种访问,而由于反射类中的Field,Method和Constructor继承自AccessibleObject,因此,通过在这些类上调用setAccessible()方法,我们可以实现对这些字段的操作。但有的时候这将会成为一个安全隐患,为此,我们可以启用java.security.manager来判断程序是否具有调用setAccessible()的权限。默认情况下,内核API和扩展目录的代码具有该权限,而类路径或通过URLClassLoader加载的应用程序不拥有此权限。例如:当我们以这种方式来执行上述程序时将会抛出异常
>java -Djava.security.manager ExampleExplorer
Exception in thread "main" java.security.AccessControlException: access denied (
java.lang.reflect.ReflectPermission suppressAccessChecks)
at java.security.AccessControlContext.checkPermission(Unknown Source)
…
方法操作:
public class Example{
public void a(){
System.out.println("Method a");
}
public static void b(){
System.out.println("Method b");
}
}
public class ExampleExplorer{
public static void main(String[] args) throws Exception{
Class c = Class.forName("Example");
Object o = c.newInstance();
Method m = c.getMethod("a");
m.invoke(o);
m = c.getMethod("b");
m.invoke(null);
}
}
输出: Method a
Method b
20. 利用反射修改final字段
我们可以模范修改private字段的方式来修改final字段,即使用反射的方式,但是与private不同,对于final字段,编译器一般都会把他内嵌到代码当中,因此如果只通过反射修改了定义处的值,而未修改随后的引用值,将没有真正达到修改final字段的目标。而这种通过反射来修改final字段的方法,现在也已经被Sun所取消了,因此现在还没有一个有效的用于修改final字段的方法。
21. 反射调用引发的异常
通过反射调用方法时,可能抛出的异常一般有三种:
a) 抛出未检测异常:如Error或RuntimeException等,此类异常将通过反射层继续往外抛。
b) 抛出声明的检测异常:由于在书写反射代码时,可能尚未知道对应的方法签名,因此也不可能通过try-catch块进行对应的捕获,为此,在实际调用中如果抛出了这类异常时,VM将会把他们包装为InvocationTargetException,我们可以在使用反射的地方捕获这个异常,然后通过InvocationTargetException的getTargetException()方法,获取实际抛出的异常,重新抛出。
c) 抛出未声明的检测异常:这类情况很少见,因为如果类抛出了未声明的异常,将不能通过编译,之所以出现这种情况可能是因局部重新编译而引发的版本不匹配或已受损的二进制类,这类异常将会被包装为UndeclaredThrowedException。
22. 动态代理
在使用反射调用时,客户程序使用通用API,调用在编译时并不了解得方法(在服务器类中)。而在使用动态代理时,服务器使用通用API实现服务器类上的方法,在运行时生成,目的是满足客户程序的规范。例如,假设我们有三个接口A,B,C,有的时候,我们需要某个实现类实现A,B接口,有的时候,我们需要某个实现类实现A,C接口…这样,为了达到我们的目标,我们可能要产生2的3次方个实现类。为了解决这个问题,我们引入动态代理。在动态代理当中我们主要需要两个类,Proxy和InvocationHandler,其中这里最重要的是Proxy的newProxyInstance()方法。例如:
public class LoggingHandler implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if (method.getName().equals("toString")) {
return super.toString();
}
System.out.println("Method " + method + " called on " + proxy);
return null;
}
}
public class LoggingDemo {
public static void main(String[] args) throws Exception {
ClassLoader cl = LoggingDemo.class.getClassLoader();
DataOutput d = (DataOutput) Proxy.newProxyInstance(cl,
new Class[] { DataOutput.class }, new LoggingHandler());
d.writeChar('a');
d.writeUTF("Hello World");
}
}
输出:
Method public abstract void java.io.DataOutput.writeChar(int) throws java.io.IOException
called on t3.LoggingHandler@a90653
Method public abstract void java.io.DataOutput.writeUTF(java.lang.String) throws
java.io.IOException called on t3.LoggingHandler@a90653
说明:从上述的运行情况我们可以看到,所有对d的调用实际上都被转发到LoggingHandler中了。
通过Proxy.newProxyInstance(),我们建立了我们所需的代理,这个方法有三个参数,第一个是类装
载器,用于装载第二个参数所指定的Class数组。第二个参数是接口列表,也就是要被代理的对象列
表,例如,如果我们需要支持接口A,B,则可以在这里填入A.class和B.class。通过这个接口列表,
就使得新生成的代理,能够同时支持接口A和B上的方法。第三个参数是动态代理类,他只需实现
invoke()方法,当在代理上调用某个方法时,这个方法调用将会被转到这个代理的invoke()方法上。
这里要注意的是在InvokeHandler实现类的内部,对于来自Object的方法,toString(),hashCode()
和equals(),我们必须要把他们转发到super的处理程序,否则将会导致堆栈溢出。
23. 实现动态代理的转发处理程序
为了合理的模拟任何接口,动态代理需要了解接口的语义,或需要将方法调用转发到其他一些了解语义的对象。因为代理是为了通用性,所以并不需要了解接口的具体特性,大多数动态代理一般都将调用转发到其他对象。
动态代理的作用体现在方法调用转发,动态代理可以截获方法调用,分析或修改参数,将调用传递给其他一些对象,分析或修改结果,并将此结果返回给调用方。若经正确配置,动态代理可透明的工作,不需要客户程序或服务器代码的信息。例如:
public class LoggingHandler implements InvocationHandler {
private DataOutputStream os;
public LoggingHandler(DataOutputStream os) {
this.os = os;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if (method.getName().equals("toString")) {
return super.toString();
}
System.out.println("Method " + method + " called on " + proxy);
method.invoke(os, args);
System.out.println();
return null;
}
}
public class LoggingDemo {
public static void main(String[] args) throws Exception {
ClassLoader cl = LoggingDemo.class.getClassLoader();
DataOutput d = (DataOutput) Proxy.newProxyInstance(cl,
new Class[] { DataOutput.class }, new LoggingHandler(
new DataOutputStream(System.out)));
d.writeChar('a');
d.writeUTF("Hello World");
}
}
输出:
Method public abstract void java.io.DataOutput.writeChar(int) throws
java.io.IOException called on t3.LoggingHandler@a90653
a
Method public abstract void java.io.DataOutput.writeUTF(java.lang.String) throws
java.io.IOException called on t3.LoggingHandler@a90653
Hello World
说明:我们现在把客户端的请求,转发给了其他对象(os)。
24. 处理InvocationHandler中的异常
由于我们是使用反射的方式,把请求转发到其他对象上,因此我们就可以按照之前的方式,把调用放到一个try-cathc块当中,并捕捉InvocationTargetException。这种方式还有另外一个好处就是,由于现在不是由客户端直接调用服务器端的服务,而是通过代理,因此当出现异常情况时,可以在代理一级处理,而不会扩散到客户端。
25. 自定义元数据
通过现有类文件当中的元数据,我们可以很灵活的调用一个类的方法,修改他的某个字段,但是这些元数据并没有足够。上述的元数据并没有包括以下两种:数值参数的正确单位,以及接口允许的状态转换表。其中数值参数的正确单位是指,假设我们现在某个接口当中有一个方法public void addWater(int liquit),虽然通过反射我们可以准确定位到这个方法,但是所有已有的元数据并不能准确的指出,为调用这个方法而传入的参数的单位应该是升还是毫升等。对于第二个状态转换表是指,如果接口当中有两个方法a()和b(),且a()方法应该在b()之前被调用,这种关系在元数据中也是未能表现的。为了解决这个问题,J2SE 5.0引入了Annotation类,他为用户添加自定义元数据提供了一个的方法。
26. 串行化处理和元数据
在串行化处理中,元数据起到两个关键作用。首先,串行化依靠类元数据和反射来提取实例状态。元数据的第二个作用是确保通过流让接收者获取正确的类。
27. 串行化基础知识
若反射访问可以处理任何对象的任何字段,就不存在防止语言设计人员使所有类可串行化的限制机制。但事实并非如此,原因有两点。首先,一些类的实例包含VM,进程或计算机的本地资源。这些类包括线程,文件,套接字和数据库连接等。现在,还没有一个比较好的方法能够根据这些资源的状态而重新实例化一个对象。第二个原因与安全性相关。一些类非常注意私有字段的私有性。但串行化提供了后门,通过后门,可将字段写入串行化流,分析流内容,从而访问这些字段。在Java语言中,并不要求开发人员针对每个类考虑这些问题,而是认为:对象在默认情况下不可串行化。
28. 串行化忽略的一些字段
有三种情况,串行化不一定会读取所有字段并将他们写入流:
a) 只有基类本身可串行化,才处理基类字段。
b) 串行化忽略静态字段,因为静态字段不是任何特殊实例状态的一部分。
c) 可使用transient关键字来禁用特定字段的串行化。
public class Person {
public String name;
public Person() {
}
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Man extends Person implements Serializable {
private String age;
public static int count;
public transient String country;
public Man(String name) {
super(name);
}
public static int getCount() {
return count;
}
public static void setCount(int count) {
Man.count = count;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String toString() {
return "Name: " + name + " Age:" + age + " Country:" + country
+ " Count:" + count;
}
}
public class SerDemo {
public static void main(String[] args) throws Exception {
Man m = new Man("Nick");
m.setAge("25");
m.setCountry(" China");
Man.count = 1;
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
"p.txt"));
oos.writeObject(m);
oos.flush();
oos.close();
}
}
public class DeSerDemo {
public static void main(String[] args) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
"p.txt"));
Man m = (Man) ois.readObject();
System.out.println(m);
}
}
输出:Name: null Age:25 Country:null Count:0
说明:因为name字段是属于基类(Person)的字段,而Person没有声明为可串行化,因此在串行化过程中被忽略了,在反串行化回来时就只有该字段的默认值null了。而由于Country是transient字段,count是static字段,因此也被忽略了。反串行化之后也只有默认值。
29. 串行化与类构造函数
若将一个类标记为Serializable,那么,类的任何不可串行化的基类必须有默认构造函数。
public class Person {
public Person() {
System.out.println("Person Constructor.");
}
}
public class Man extends Person implements Serializable {
public Man() {
System.out.println("Man Constructor.");
}
}
输出:Person Constructor.
说明:在进行反序列化Man时,系统只调用了Person的构造函数,并没有调用Man的构造函数。因为,
串行化计划从流中指派Man的字段,运行Man的构造函数实属多余。
30. 使用readObject和writeObject
在从流读取对象的状态之前,ObjectInputStream使用反射来检查对象的类是否实现了readObject()方法。若是,ObjectInputStream只调用readObject(),而不执行普通反串行化。
public class Man extends Person implements Serializable {
public static int count;
public Man() {
count = 1;
}
private void readObject(ObjectInputStream ois) throws IOException,
ClassNotFoundException {
ois.defaultReadObject();
Man.count = 1;
}
}
public class DeSerDemo {
public static void main(String[] args) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
"p.txt"));
Man m = (Man) ois.readObject();
System.out.println("m's count is " + Man.count);
}
}
输出:m's count is 1
说明:在实现readObject()时,一般要执行两件事情:一是回调ObjectInputStream的
defaultReadObject()方法,按照普通串行化的方式读取字段;二是执行任何准备加到反串行化
过程的自定义步骤。从上面的讨论知道,在反串行化过程中,可串行类的构造函数不会被执行,
但是我们可以通过实现readObject()方法,从而进行必要的初始化工作,例如,初始化串行化过
程中被忽略掉的static字段和transient字段等。
31. serialVersionUID
在进行反序列化时,VM需要检查流中的类和加载的类是否一致,这通过在流中加入一个serialVersionUID实现。通过比较流和类中的ID值,VM就可以知道是否加载了合适的类。我们也可以显式指派该字段的值,从而可以避开这个一致性检查。
public class Man implements Serializable {
private static final long serialVersionUID = 1L;
public int count;
public Man() {
count = 1;
}
}
public class Man implements Serializable {
private static final long serialVersionUID = 1L;
public Man() {}
}
说明:我们在序列化前使用第一个Man类,在反序列化的时候使用第二个Man类,在一般情况下这是会导致异常的,但由于我们显式指派了serialVersionUID字段,因此就不会抛出异常了。
32. 兼容和不兼容的更改
一旦设置了serialVersionUID,则需要程序员自行确保类的新,旧版本的兼容。
更改类型 |
例子 |
兼容更改 |
添加字段;添加/删除类;添加/删除writeObject()/readObject();添加Serializable;更改访问修饰符;从字段中删除static/transient |
不兼容更改 |
删除字段;在层次结构中移动类;为字段添加static/transient;更改基本类型;切换Serializable/Externalizable,删除Serializable/Externalizable,更改writeObject()/readObject()是否处理默认字段数据的设置;添加产生与旧版本不兼容对象的writeReplace或readResolbe |
33. 显式管理可串化字段
除了上述的方法以外,我们可以使用ObjectInputStream.GetField来显式管理如何从流拖出字段,同时使用ObjectOutputStream.PutField来显式将字段插入流。
public class Man implements Serializable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private void writeObject(ObjectOutputStream oos) throws IOException {
ObjectOutputStream.PutField pf = oos.putFields();
pf.put("name", "cen");
oos.writeFields();
}
}
public class SerDemo {
public static void main(String[] args) throws Exception {
Man m = new Man();
m.setName("nick");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
"p.txt"));
oos.writeObject(m);
}
}
public class DeSerDemo {
public static void main(String[] args) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
"p.txt"));
Man m = (Man) ois.readObject();
System.out.println("m's name is " + m.getName());
}
}
输出:m's name is cen
说明:通过ObjectOutputStream.PutField我们可以绕开原有的反射机制,而对需要进行串行化
的字段和字段值进行定制。
34. 重写类员数据
在上述的例子当中,我们在writeObject时只写入了类中已定义的字段,这是因为类已定义的字段将会作为元数据写到流当中,然后在反序列化时,系统通过反射机制才能成功执行。以下演示如何在序列化时写入类没有定义的字段。
public class Man implements Serializable {
private static final ObjectStreamField[] serialPersistentFields = {
new ObjectStreamField("name", String.class),
new ObjectStreamField("age", Integer.TYPE) };
private void readObject(ObjectInputStream ois) throws IOException,
ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
String name = (String) gf.get("name", null);
int age = gf.get("age", 0);
System.out.println(name + " is " + age + " years old.");
}
private void writeObject(ObjectOutputStream oos) throws IOException {
ObjectOutputStream.PutField pf = oos.putFields();
pf.put("name", "cen");
pf.put("age", 25);
oos.writeFields();
}
}
输出:cen is 25 years old.
说明:如果在流当中写入类中没有定义的字段,那么我们需要添加这部分字段的元信息,这主要
包括定义一个serialPersistentFields数组。并且在writeObject()内部写入这部分字段和字段
值。对应的在raedObject()内部,我们也需要手工的对这部分值进行提取。
35. 停用元数据
串行化机制还提供了几种方法,允许不发送元数据。这包括在defaultWriteObject()后添加数据,使对象外部化(Externalizable),以及完全替换defaultWriteObject()的方法。
36. 在defaultWriteObject()后添加数据
由于ObjectInputStream和ObjectOutputStream分别实现了DataInput和DataOutput接口,而这些接口为读写基本类型提供了辅助方法,因此我们可以利用这些方法,写入自定义数据。
public class Man implements Serializable {
private void readObject(ObjectInputStream ois) throws IOException,
ClassNotFoundException {
ois.defaultReadObject();
String name = ois.readUTF();
int age = ois.readInt();
System.out.println(name + " is " + age + " years old.");
}
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
oos.writeUTF("cen");
oos.writeInt(25);
}
}
输出:cen is 25 years old.
说明:通过DataInput和DataOutput提供的辅助方法,我们可以很容易的添加自定义字段。这
里有几点需要注意,首先就是自定义字段的写入和读取,一定是在defaultXXObject()操作之后。
第二是,由于自定义数据没有元数据,因此使用不同类版本的用户无法确定流中的自定义字段的
含义,这与之前的使用serialPersistentFields的方法相比,在灵活性上有所不足。
37. 外部化
Externalizable扩展了Serializable,在实现Externalizable时,程序员必须负责为该类传输数据,以及为任何基类传输数据。
public class Man implements Externalizable {
public Man() {
System.out.println("Man's Constructor");
}
public void readExternal(ObjectInput in) throws IOException {
String name = in.readUTF();
int age = in.readInt();
System.out.println(name + " is " + age + " years old.");
}
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF("cen");
out.writeInt(25);
}
}
输出: Man's Constructor
cen is 25 years old.
说明:从输出我们可以看到,通过外部化的方式实现时,构造函数将会被调用。因此,当我们
需要使用外部化机制时,需要注意以下几点:Man类显示的处理本身及其基类的所有字段;因为
writeExternal()方法必须声明为公有,恶意或错误的代码可能随时调用writeExternal()方法,
将任意一些状态带入对象;Externalizable类必须有一个公有构造函数;不写入元数据,只将
字段的实际值写入流。
38. 用writeObject只写入原始数据
这种方法是指在实现writeObject()时不先调用defaultWriteObject(),直接使用DataOutput的辅助方法写自定义数据。
39. 三种方法的比较:
这三种忽略元数据的方法,较差的是“在defaultWriteObject()后添加数据”,因为这使得串行化流的内容是一个奇怪的混合;差的是“使用外部化”,因为这将丧失了元数据的所有优势;最差的就是:“用writeObject只写入原始数据”,因为这样做是违反规范宗旨的,且普通工具无法可靠的从流中读取数据。
40. 对象替换
对象流和对象本身都能够在串行化时执行对象替换。对象替换特性有多种用法。如果对象图包含不可串行化的对象,则可以用可串行化对象加以替换。如果正在编写分布式程序,则可使用对象的占位程序(Stub)来替换对象的状态,让接收者得到对远程对象的引用,而不是对象的本机副本。
41. 流控制替换
为了进行对象替换,我们需要重新实现ObjectOutputStream,并调用enableReplaceObject(true)以允许启用对象替换功能,并实现replaceObject()方法进行具体的替换工作。而对于ObjectInputStream,我们则需要调用enableResolveObject(true)和实现resolveObject()方法。
public class Man {
private String name;
public Man() {}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class SerMan implements Serializable {
private String name;
public SerMan(Man m) {
this.name = m.getName();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class OutputReplacer extends ObjectOutputStream {
public OutputReplacer(OutputStream oo) throws IOException {
super(oo);
enableReplaceObject(true);
}
protected Object replaceObject(Object obj) throws IOException {
if (obj instanceof Man) {
System.out.println("HERE");
SerMan sm = new SerMan((Man) obj);
return sm;
}
return super.replaceObject(obj);
}
}
public class InputReplacer extends ObjectInputStream {
public InputReplacer(InputStream is) throws IOException {
super(is);
enableResolveObject(true);
}
protected Object resolveObject(Object obj) throws IOException {
if (obj instanceof SerMan) {
System.out.println("THERE");
Man m = new Man();
m.setName(((SerMan) obj).getName());
return m;
}
return super.resolveObject(obj);
}
}
public class SerDemo {
public static void main(String[] args) throws Exception {
Man m = new Man();
m.setName("nick");
ObjectOutputStream oos = new OutputReplacer(new FileOutputStream(
"p.txt"));
oos.writeObject(m);
}
}
public class DeSerDemo {
public static void main(String[] args) throws Exception {
ObjectInputStream ois = new InputReplacer(new FileInputStream("p.txt"));
Man m = (Man) ois.readObject();
System.out.println(m.getName());
}
}
输出:nick
说明:由于Man类不能被串行化,所以,我们需要使用一个可串行化的SerMan类来对他进行替
换。通过引入类替换机制,我们可以对需要进行串行化的类进行过滤,或者对类的某个字段进行
过滤。
42. 类控制替换
类通过实现writeReplace()和readResolve()方法,可以进行类控制替换。他与流控制的区别是,仅在有权访问被替换类的源代码时,才可以使用类级别替换。类使用类级别替换机制,可以将其串行化形式与内存表示形式分离开来。如果类包含复杂的串行化代码,则显式编写分离的串行化代码可能更有效果。对于依赖对象标识的设计(如Singleton模式),由于反序列化的行为类似于公有构造函数,所以多次进行序列化,可能会破坏了Singleton的设计。
public class Man implements Serializable {
private String name;
private static Man m = new Man("nick");
private Man(String name) {
this.name = name;
}
public static Man getMan() {
return m;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class DeSerDemo {
public static void main(String[] args) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
"p.txt"));
Man m1 = (Man) ois.readObject();
ois = new ObjectInputStream(new FileInputStream("p.txt"));
Man m2 = (Man) ois.readObject();
System.out.println("m1's name is " + m1.getName());
System.out.println("m2's name is " + m2.getName());
System.out.println("m1 == m2 is " + (m1 == m2));
}
}
输出:m1's name is nick
m2's name is nick
m1 == m2 is false
说明:通过上述的输出,我们可以看到通过反序列化,我们已经破坏了原有的Singleton设计。
把Man类修改一下,增加readResolve()的实现:
public class Man implements Serializable {
…
private Object readResolve() throws IOException {
if (name.equals("nick"))
return m;
else
throw new InvalidObjectException("no man call " + name);
}
}
输出:m1's name is nick
m2's name is nick
m1 == m2 is true
说明:通过这次的输出,我们可以看到,即使经过了反序列化,原有的设计并未改变。对于
readResolve()方法,他是在反序列化以后被调用的,即反序列化先新建了一个实例,然后调用其
readResolve()方法,如果返回的值不是新生成的这个实例,则这个新实例将会被回收。
43. 替换的排序规则
对象替换包含以下的排序规则:
a) 类级别替换在流级别替换前调用。类级别解释出现在流级别解释之前。
b) 类级别替换是递归的,而类级别解释不递归执行。
c) 流级别替换不递归。流只有一次替换/解释对象的机会。
d) 在串行化期间,在遇到对象时替换对象。在反串行化期间,在完全构造对象后再替换对象。
在这里,第二和第四条规则是非对称的,这给使用readResolve()和writeReplace()的递归使用带来非常大的麻烦,因此如果没有特殊情况,不要使用递归的方式。
44. Java2安全性
public class Destruct {
public static void main(String[] args) {
try {
File f = new File("a.txt");
System.out.println("delete file?" + f.delete());
} catch (Exception e) {
e.printStackTrace();
}
}
}
a) 直接运行:
i. 命令行:java Destruct
ii. 输出:
delete file?true
iii. 说明:当我们直接运行该程序时,文件被成功的删除。
b) 增加安全选项:
i. 命令行:java -Djava.security.manager Destruct
ii. 输出:
java.security.AccessControlException: access denied
(java.util.PropertyPermission user.dir read)
at java.security.AccessControlContext.checkPermission(Unknown Source)
at java.security.AccessController.checkPermission(Unknown Source)
…
at t5.Destruct.main(Destruct.java:22)
iii. 说明:当我们添加安全选项后,安全管理器将检查程序所具有的权限,而由于一般的应用程序只具有最小的权限,因此他不能对文件进行删除。
c) 增加策略文件:
i. 命令行:java -Djava.security.manager -Djava.security.policy=Destruct.policy Destruct
ii. 输出:
delete file?true
iii. 说明:通过引入策略文件,我们可以对程序的权限进行定义。从而修改默认的行为。
iv. 策略文件:
grant{
permission java.io.FilePermission "<
};
45. 自定义类加载器
我们可以通过扩展已有的类加载器来实现自定义的类加载行为。一般来说,我们应该只重写当中的findClass()方法。
public interface IResourceTransformer {
public byte[] transformClassBytes(byte[] inout,int start,int len) ;
public URL transformResourceURL(URL resource);
public Enumeration transformResources(Enumeration resources);
}
public class ResourceTransformerAdapter implements IResourceTransformer {
public byte[] transformClassBytes(byte[] inout, int start, int len) {
return null;
}
public Enumeration transformResources(Enumeration resources) {
return null;
}
public URL transformResourceURL(URL resource) {
return null;
}
}
public class ClassNotter extends ResourceTransformerAdapter {
public byte[] transformClassBytes(byte[] inout, int start, int len) {
int end = start + len;
for (int i = start; i < end; i++) {
inout[i] = (byte) ~inout[i];
}
return inout;
}
}
public class TransformClassLoader extends URLClassLoader {
private IResourceTransformer rt;
public TransformClassLoader(URL[] urls, IResourceTransformer rt) {
super(urls);
this.rt = rt;
}
private URL getURLBase(URL url) {
URL[] urls = getURLs();
for (URL u : urls) {
if (url.toExternalForm().startsWith(u.toExternalForm()))
return u;
}
return null;
}
protected Class< ? > findClass(String name) throws ClassNotFoundException {
System.out.println("My findClass");
String className = name.replace(".", "/") + ".class";
URL url = super.getResource(className);
if (url == null) {
return null;
}
URL urlBase = getURLBase(url);
if (urlBase == null)
throw new Error("url has no base");
InputStream is = null;
try {
is = url.openStream();
if (is == null)
return null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
for (int ch = 0; (ch = (is.read())) != -1;) {
baos.write(ch);
}
byte[] classbytes = baos.toByteArray();
rt.transformClassBytes(classbytes, 0, classbytes.length);
return defineClass(name, classbytes, 0, classbytes.length,
new CodeSource(urlBase, new Certificate[0]));
} catch (IOException ioe) {
ioe.printStackTrace();
}
return null;
}
}
public class TCLDemo {
public static void main(String[] args) {
try {
URL url1 = new URL("file:/E:/Java/Study/Java%20Component/");
URLClassLoader cl = new TransformClassLoader(new URL[] { url1 },
new ClassNotter());
System.out.println(cl);
Class c1 = Class.forName("Test", true, cl);
System.out.println(c1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出: t5.TransformClassLoader@ 1a46e30
My findClass
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic
value 889275713 in class file Test
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(Unknown Source)
…
at t5.TCLDemo.main(TCLDemo.java:27)
说明:由于默认情况下,VM会先使用默认的装载策略去装载我们需要加载的类。因此,我们需
要把Test.class放到一个加载器不能直接加载的位置,然后VM才会调用findClass()方法来查找
类。而由于我们使用ClassNotter把二进制码进行了求反操作,因此系统将会抛出一个
ClassFormatError。
46. 协议处理程序
从上面的例子可以看到,我们所需要做的就是实现findClass()方法,以进行类文件的定位。为此,我们可以采用另外一种称为协议处理程序的方法来解决这个问题。协议处理程序将定位资源分成以下不同片断:协议处理程序(Handler)分析URL,并返回连接;连接(Connection)管理通信并返回流;流读写数据,并可能应用转换。这里我们定义一个称为“not”的新协议来演示该应用。
a) 结构图:
URLStreamHandlerFactory |
URLStreamHandler |
URLConnection |
根据协议类型返回合适的Handler |
根据协议类型返回合适的Handler |
把进行连接的具体细节转交给Connection |
b) 代码:
public class NotURLReader implements URLStreamHandlerFactory {
public URLStreamHandler createURLStreamHandler(String protocol) {
if (protocol.equals("not")) {
return new NotHandler();
}
return null;
}
public static void main(String[] args) throws Exception {
URL.setURLStreamHandlerFactory(new NotURLReader());
URL url = new URL("not:Test.class");
InputStream is = url.openStream();
int b;
int i = 0;
while ((b = is.read()) != -1) {
System.out.printf("%h/t",b);
if (i % 16 == 15) {
System.out.println();
}
i++;
}
}
}
public class NotHandler extends URLStreamHandler {
protected URLConnection openConnection(URL url) throws IOException {
return new NotConnection(url);
}
}
public class NotConnection extends URLConnection {
private InputStream is;
public NotConnection(URL url) {
super(url);
}
public void connect() throws IOException {}
public InputStream getInputStream() throws IOException {
if (is == null) {
String file = getURL().getFile();
is = new FileInputStream(file);
}
return is;
}
}
c) 输出:
ca fe ba be 0 0 0 31 0 1e 1 0 4 54 65 73
74 7 0 1 1 0 10 6a 61 76 61 2f 6c 61 6e 67
2f 4f 62 6a 65 63 74 7 0 3 1 0 6 3c 69 6e
69 74 3e 1 0 3 28 29 56 1 0 4 43 6f 64 65
…
47. 自定义加载器和协议处理程序之间的选择
流处理程序可用来连接到任何资源,但自定义类加载器只能加载类和其他协同定位的资源。流处理程序利用已内置到URLClassLoader的安全特性。而且,因为可在命令行安装他们,所以可使流处理程序的存在对其他代码完全透明。流处理程序只有一个缺点,就是当试图处理的协议格式与http具有完全不同的语法。这时我们可能需要额外的实现Handler中的parseURL()方法。
48. 使用生成式编程(Generative Programming,GP)的原因
使用生成式编程的原因很简单:实现有效的捕获和重用问题域的知识。域分析标识相关软件系统系列的共性及变性。共性是编写到系统中的标准特性,由该系统所有序列共享。变性随不同产品和同一产品的不同应用而改变。在系统生存期的某个时间点,有时必须针对各种变性做出选择和指定规范。做出选择的时间点成为绑定时间。
49. 按绑定时间进行分类
在传统的面向对象设计中,通过综合继承和参数化操作来建立变性模型。在使用基于继承的解决方案中,将各个不同规范实例化为不同具体类。在开发期间,规范被绑定,这称为编译时绑定。参数化解决方案通过传入参数,在运行时绑定规范。这意味着,如果程序生成参数化控制,终端用户可在运行时选择规范。Java开发领域有四个明显的绑定时间:当编译器运行时,出现编译期绑定;当设计人员配置已编译组件的初始状态时,出现设计时绑定;在将组件安装到要使用他们的网络时,出现部署时绑定;在应用程序开始执行后,出现运行时绑定。
50. 用Java生成代码的原因
l 高质量的类型信息是极具价值的隐式规范文档。
l 灵活的类加载支持绑定时间和绑定模式的任意组合。
l Java源文件易于读取和生成。
l Java字节码文件易于读取和生成。
l 生成的代码可提供极大性能提供,可消除VM的开销。
51. 绑定时间和模式的分类
|
静态绑定模式 |
动态绑定模式 |
开发时 |
IDE向导,rmic,JavaBeans |
默认串行化 |
设计时 |
|
JavaBeans |
部署时/运行时 |
JSP,EJB |
EJB,动态代理 |
输入/输出 |
源文件 |
类二进制 |
Java类二进制数据 |
RMI占位程序,IDE向导 |
RMI占位程序,动态代理 |
非Java数据 |
SOAP |
|
混合数据 |
JSP,EJB |
EJB |