主流的 Java Web 服务器,如 Tomcat、 Jetty、 WebLogic、 WebSphere 以及其他服务器,都实现了自己定义的类加载器(一般都不止一个)。因为一个功能健全的 Web 服务器,其类加载器要解决如下几个问题:
在部署 Web 应用时,单独的一个 ClassPath 就无法满足需求了,所以各种 Web 服务器都“不约而同”地提供了好几个 ClassPath 路径供用户存放第三方类库。在 Tomcat 目录结构中,有 3 组目录("/common/*
"、"/server/*
" 和"/shared/*
") 可以存放 Java 类库,另外还可以加上 Web 应用程序自身的目录"/ WEB- INF/*", 一共 4 组。CommonClassLoader
、 CatalinaClassLoader
、 SharedClassLoader
和WebappClassLoader
JSP类加载器
则是 Tomcat 自己定义的类加载器。灰色的启动类加载器、扩展类加载器、应用程序类加载器是jdk默认提供的类加载器。
Tomcat热部署原理:JasperLoader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class, 它出现的目的就是为了被丢弃:当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能。
对于 Tomcat 的 6. x 版本,只有指定了 tomcat/conf/ catalina. properties 配置文件的 server.loader 和 share.loader 项后才会真正建立 CatalinaClassLoader 和 SharedClassLoader 的实例,否则会用到这两个类加载器的地方都会用 CommonClassLoader 的实例代替,而默认的配置文件中没有设置这两个 loader 项,所以 Tomcat 6.x 顺理成章地把/common、/server 和/shared 三个目录默认合并到一起变成一个/lib 目录。
java程序社区流传着这么一个观点,“学习 JEE 规范,去看 JBoss 源码;学习类加载器,就去看 OSGi 源码”。OSGi 在 Java 程序员中最著名的应用案例就是Eclipse IDE。
OSGi中的每个模块(成为Bundle)与普通的Java类库区别并不太大,两者一般都以JAR格式进行封装,并且内部存储的都是Java Package和Class。但是一个模块(Bundle)可以声明它所依赖的Java Package(通过Import-Package描述),也可以声明它允许导出发布的Java Package(通过Export-Package描述)。在OSGi里面,Bundle之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖,而且类库的可见性能得到非常精确地控制,一个模块里只有被Export过的package才可能被外界访问,其他的Pacakge和Class将会被隐藏起来。除了更精确地模块划分和可见性控制外,引入OSGi的另外一个重要理由是,基于OSGi的程序可以实现模块级的热插拔功能,当程序升级更新或调试出错时,可以只停用、重新安装后启动程序的其中的一部分,这对企业级程序开发来说是一个非常有诱惑以的特性。
OSGi之所以能有上述诱人的特点,要归功于它灵活的类加载器架构。OSGi Bundle类加载器之间只有规则,没有固定的委派关系。例如,某个Bundle声明了一个它依赖的Package,如果有其它Bundle声明发布了这个package后,那么对这个package的所有类加载动作都会委派给它发布的Bundle类加载器去完成。不涉及某个具体的Package时,各个Bundle加载器都是平级的关系,只有具体使用到某个Package和Class的时候,才会根据Package导入导出定义来构造Bundle间的委派和依赖。
另外,一个Bundle类加载器为其它Bundle提供服务时,会根据Export-Package列表严格控制访问范围。如果一个类存在Bundle的类库中但是没有被Export,那么这个Bundle的类加载器能找到这个类,但不会提供给其它Bundle使用,而且OSGi平台也不会把其它Bundle的类加载请求分配给这个Bundle来处理。
在OSGi里,加载一个类可能发生的查找行为和委派关系会比上图中显示的复杂得多,类加载时可能进行的查找规则如下:
将以java.*开头的类,委派给父类加载器加载。 否则,将委派列表名单内的类,委派给父类加载器加载。
否则,将import列表中的类,委派给Export这个类的Bundle的类加载器加载。
否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。 否则,查找类是否在自己的Fragment
Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。 否则,查找Dynamic
Import列表的Bundle,委派给对应Bundle的类加载器加载。 否则,类查找失败。
我们使用代理类最初的目的,就只是为了拦截某个方法的调用,在其前后执行一些额外逻辑,额外逻辑和目标方法并不具有强关联。方法调用会形成一个调用栈,可以把代理想象为是在这个栈上切了一刀,在切口上做的逻辑,我们叫切面逻辑。一个类从编写,到运行时调用,中间大概会经过这几个步骤:
所以生成代理可以有三个思路,一,在编译期修改源代码;二,在字节码加载前修改字节码;三,在字节码加载后动 态创建代理类的字节码。
类别 | 机制 | 原理 | 优点 | 缺点 | 技术 |
---|---|---|---|---|---|
静态AOP | 在编译期,切面直接以字节码的形式编译到目标字节码文件中 | 对系统无性能影响 | 灵活性不够 | AspectJ | |
动态AOP | 动态代理 | 在运行期,目标类加载后,为接口动态生成代理类,将切面植入到代理类中 | 相对于静态AOP更加灵活 | 切入的关注点需要实现接口。对系统有一点性能影响 | JDK dynamic proxy |
动态字节码生成 | 在运行期,目标类加载后,动态构建字节码文件生成目标类的子类,将切面逻辑加入到子类中 | 没有接口也可以织入 | 扩展类的实例方法为final时,则无法进行织入 | cglib | |
自定义类加载器 | 在运行期,目标加载前,将切面逻辑加到目标字节码里 | 可以对绝大部分类进行织入 | 代码中如果使用了其他类加载器,则这些类将不会被织入 | – | |
字节码转换 | 在运行期,所有类加载器加载字节码前,前进行拦截 | 可以对所有类进行织入 |
在java的动态代理中,有两个重要的类或者接口,一个是InvocationHandler(Interface)、另一个是Proxy(Class),需要代理的类实现动态代理要先实现InvocationHandler接口,(为要代理的类关联一个handler),再使用Proxy类的静态方法newProxyInstance创建代理对象。
动态代理类定义:Java.lang.reflect包下Proxy类
InvocationHandler:每一个需要动态代理的类都必须实现InvocationHandler这个接口,并且每个需要代理类的实例都关联到了一个handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由InvocationHandler这个接口的invoke方法来进行调用。我们来看看invoke这个方法:
Object invoke(Object proxy,Method method,Object[] args)
proxy:指代我们所代理的真实对象
method:指代的是我们所要调用真实对象的某个方法的Method对象
args:指代的是调用真实对象某个方法时接收的参数
Proxy:这个类的newProxyInstance静态方法作用就是用来动态的创建一个代理对象,其接受三个参数
public static Object newProxyInstance(ClassLoader loader,Class>[] interfaces,InvocationHandler h) throws IllegalArgumentException
loader:一个ClassLoader对象,定义了由哪个ClassLoader对象来生成的代理对象进行加载
interfaces:一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了。
h:一个InvocationHandler对象,表示的是当我这个动态代理在调用方法的时候,会关联到哪一个InvocationHandler对象上
使用动态代理的过程:首先,定义一个Subject接口,并且定义hello()方法:
public interface Subject {
public void hello(String str);
}
接着,定义一个类来实现上面的接口,这个类就是我们的真实对象:
public class RealSubject implements Subject{
@Override
public void hello(String str) {
System.out.println("执行RealSubject中的hello方法:"+str);
}
}
下一步,为需要代理的类关联一个Handler,并在实现InvocationHandler接口的类中添加需要的业务逻辑:
public class Dynamicproxy implements InvocationHandler{
//这就是我们代理的真实对象
private Object subject;
//构造方法,给我们要代理的真实对象赋值
public Dynamicproxy(Object subject){
this.subject = subject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//在代理真实对象前我们可以添加一些自己的操作
System.out.println("before 执行hello方法 可在invoke方法中增加自己业务逻辑");
System.out.println("当然可以获得需要代理类其中的 Method:"+method);
//当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用
method.invoke(subject, args);
//在代理真实对象后我们可以添加一些自己的操作
System.out.println("after执行hello方法 在执行invoke方法后再增加业务逻辑");
return null;
}
}
最后是使用时,结合动态代理类Proxy的静态方法与之前的handler,创建动态代理对象
public class TestDynamicproxy {
public static void main(String[] args) {
//我们要代理的真实对象
Subject realSubject = new RealSubject();
//我们要代理哪个真实对象,就将该对象传进去,最后通过该真实的对象调用该方法
InvocationHandler handler = new Dynamicproxy(realSubject);
Subject subject = (Subject)Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject.getClass().getInterfaces(), handler);
System.out.println("代理对象类名是:"+subject.getClass().getName());
subject.hello("world");
}
}
执行结果:
使用Proxy.newProxyInstance()创建的代理对象是在JVM上运行时动态产生的一个对象,它并不是InvocationHandler类型,也不是我们定义的那组接口的类型,而是在运行时动态产生的一个对象。并且命名方式是以$开头的,proxy中最后一个数字表示对象的标号。
动态代理的作用:主要用来做方法的增强,让你可以在不修改源码的情况下,增强一些方法,在方法执行前后做任何你想做的事情(甚至根本不去执行这个方法),因为在InvocationHandler的invoke这个方法中,你可以直接获取正在调用方法对应的Method对象,具体应用,比如,添加日志,做事物控制等。
动态代理存在一定的缺点:代理的类必须实现了接口,因为动态代理类在实现的时候继承了Proxy类,java不支持多继承,因此动态代理类只能根据接口来定义方法。除了动态代理还可以通过字节码修改技术,目前字节码修改技术有ASM,javassist等。cglib就是基于封装的Asm. Spring 就是使用cglib代理库。
ASM 是一个 Java 字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。不过ASM在创建class字节码的过程中,操纵的级别是底层JVM的汇编指令级别,这要求ASM使用者要对class组织结构和JVM汇编指令有一定的了解。