Arthas从入门到精通(三) 理解的前提-ClassLoad机制应用

系列索引

Arthas从入门到精通(一) 整体概览

Arthas从入门到精通(二) 理解的前提-ClassLoad机制剖析

Arthas从入门到精通(三) 理解的前提-ClassLoad机制应用

Arthas从入门到精通(四) 熟练的前提-OGNL表达式

背景

  • 前面已经分析过java的classLoader模型以及双亲委派机制的实现。
  • 这里主要分析一下如何应用classLoader的简单应用,以及在一些主流框架上的实践分析。

自定义CLassLoader

可以注意到,之前分析的ClassLoader都是从某个目录或者jar包里,加载的class文件,如果需要本地硬盘或者从网络上动态加载一个class文件,就需要自己自定义一个ClassLoader实现。

三个步骤:

  • 编写一个类继承自ClassLoader抽象类。  
    • 一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。 
  • 复写它的findClass()方法。
    • 在loadClass中最后调用
  • 在findClass()方法中调用defineClass()。 
    • 这个方法在编写自定义classloader的时候非常重要,它能将class二进制内容转换成Class对象,如果不符合要求的会抛出各种异常。

测试类Test.java

package com.destiny;
public class Test {

    public void say() {
        System.out.println("hello world");
    }
}

对这个文件执行javac之后生成Test.class文件,放到~/class 目录下

DiskClassLoader.java

在findClass()方法中定义了查找class的方法,然后数据通过defineClass()生成了Class对象。  

public class DiskClassLoader extends ClassLoader {

    private String mLibPath;

    public DiskClassLoader(String path) {
        mLibPath = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = getFileName(name);

        File file = new File(mLibPath, fileName);

        // 读取file的二进制流
        try {
            FileInputStream is = new FileInputStream(file);

            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            int len = 0;
            try {
                while ((len = is.read()) != -1) {
                    bos.write(len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            byte[] data = bos.toByteArray();
            is.close();
            bos.close();

            return defineClass(name, data, 0, data.length);

        } catch (IOException e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }

    //获取要加载 的class文件名
    private String getFileName(String name) {
        // TODO Auto-generated method stub
        int index = name.lastIndexOf('.');
        if (index == -1) {
            return name + ".class";
        } else {
            return name.substring(index + 1) + ".class";
        }
    }
}

ClassLoaderTest

最终能正确动过Test.class文件,通过反射执行到say方法。

public class ClassLoaderTest {

    public static void main(String[] args) {
        //创建自定义classloader对象。
        DiskClassLoader diskLoader = new DiskClassLoader("~/class/");
        try {
            //加载class文件
            Class c = diskLoader.loadClass("com.destiny.Test");

            if (c != null) {
                try {
                    Object obj = c.newInstance();
                    Method method = c.getDeclaredMethod("say", null);
                    //通过反射调用Test类的say方法
                    method.invoke(obj, null);
                } catch (InstantiationException | IllegalAccessException
                        | NoSuchMethodException
                        | SecurityException |
                        IllegalArgumentException |
                        InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

对一个自定义的ClassLoader来说,最重要的是路径,即从哪里加载class文件,可以是磁盘, 网络,内存等。

自定义ClassLoader的一些妙用:

  • class文件加密,对.class执行一些加密操作,在findClass处理二进制的时候,加载回来,可以完成对.class文件的加密

Context ClassLoader 线程上下文类加载器

在thread中,可以设置当前线程自己的context ClassLoader,它只是一个线程私有的概念,并不和之前介绍的3个类加载器一样是一个具体的实现。

每个Thread都有一个相关联的ClassLoader,默认是AppClassLoader。并且子线程默认使用父线程的ClassLoader除非子线程特别设置。

Thread相关源码如下:

public class Thread implements Runnable {

/* The context ClassLoader for this thread */
   private ClassLoader contextClassLoader;

   public void setContextClassLoader(ClassLoader cl) {
       SecurityManager sm = System.getSecurityManager();
       if (sm != null) {
           sm.checkPermission(new RuntimePermission("setContextClassLoader"));
       }
       contextClassLoader = cl;
   }

   public ClassLoader getContextClassLoader() {
       if (contextClassLoader == null)
           return null;
       SecurityManager sm = System.getSecurityManager();
       if (sm != null) {
           ClassLoader.checkClassLoaderPermission(contextClassLoader,
                                                  Reflection.getCallerClass());
       }
       return contextClassLoader;
   }
}

下面演示如何利用线程的context classLoader 分别加载同包名,同类名的class,一般我们在同一个classLoader下,因为双亲委派模型的存在,是不可能实现的。

接口:

package com.destiny.test;

public interface ISpeak {
    public void speak();

}

实现类1:

package com.destiny.test;

// 生成.class文件后在 /user/destiny/Documents/speak/test/ 下
public class SpeakTest implements ISpeak {

    @Override
    public void speak() {
        System.out.println("Test");
    }

}

实现类2:

package com.destiny.test;

// 生成.class文件后在 /user/destiny/Documents/speak/ 下
public class SpeakTest implements ISpeak {

    @Override
    public void speak() {
        System.out.println("I\' destiny");
    }

}

在一个jvm中同时加载实现类1和实现类2:

public class ClassLoaderTest {

    public static void main(String[] args) {
        DiskClassLoader diskLoader1 = new DiskClassLoader("/Users/destiny/Documents/speak/test/");
        Class cls1 = null;
        try {
            //加载class文件
            cls1 = diskLoader1.loadClass("com.destiny.test.SpeakTest");
            System.out.println(cls1.getClassLoader().toString());
            if (cls1 != null) {
                try {
                    Object obj = cls1.newInstance();
                    Method method = cls1.getDeclaredMethod("speak", null);
                    //通过反射调用Test类的speak方法
                    method.invoke(obj, null);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        System.out.println("----------------------next thread--------------------------------------------");

        DiskClassLoader diskLoader = new DiskClassLoader("/Users/destiny/Documents/speak/");
        System.out.println("Thread " + Thread.currentThread().getName() + " classloader: " + Thread.currentThread().getContextClassLoader().toString());
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("Thread " + Thread.currentThread().getName() + " classloader: " + Thread.currentThread().getContextClassLoader().toString());

                try {
                    //方法1: 用新构造的classLoader加载class文件, 输出I\' destiny
                    Thread.currentThread().setContextClassLoader(diskLoader);
                    Class c = diskLoader.loadClass("com.destiny.test.SpeakTest");

                    //方法2: 直接用线程原来的上下文加载器, AppClassLoader,会直接找不到SpeakTest
                    //ClassLoader cl = Thread.currentThread().getContextClassLoader();
                    //Class c = cl.loadClass("com.destiny.test.SpeakTest");
                    System.out.println(c.getClassLoader().toString());
                    if (c != null) {
                        try {
                            Object obj = c.newInstance();
                            //SpeakTest1 speak = (SpeakTest1) obj;
                            //speak.speak();
                            Method method = c.getDeclaredMethod("speak", null);
                            //通过反射调用Test类的say方法
                            method.invoke(obj, null);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

可以看到DiskClassLoader1和DiskClassLoader分别加载了自己路径下的SpeakTest.class文件,并且它们的类名是一样的com.frank.test.SpeakTest,但是执行结果不一样,因为它们的实际class文件内容不一样。

Tomcat的ClassLoader机制简析

tomcat需要解决的问题:

  • 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。 
  • 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机,这是扯淡的。 
  • web容器也有自己依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。 
  • web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,否则要你何用? 所以,web容器需要支持 jsp 修改后不用重启。

tomcat加载设计:

我们看到,前面3个类加载和默认的一致,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/*/server/*/shared/*(在tomcat 6之后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/*中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。

  • commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  • catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;

Tomcat的类加载:

当tomcat启动时,会创建几种类加载器:

Bootstrap 引导类加载器 

  • 加载JVM启动所需的类,以及标准扩展类(位于jre/lib/ext下)

System 系统类加载器 

  • 加载tomcat启动的类,比如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。位于CATALINA_HOME/bin下。

Common 通用类加载器 

  • 加载tomcat使用以及应用通用的一些类,位于CATALINA_HOME/lib下,比如servlet-api.jar

webapp 应用类加载器

  • 每个应用在部署后,都会创建一个唯一的类加载器。该类加载器会加载位于 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件。

当应用需要到某个类时,则会按照下面的顺序进行类加载:

  1. 使用bootstrap引导类加载器加载
  2. 使用system系统类加载器加载
  3. 使用应用类加载器在WEB-INF/classes中加载
  4. 使用应用类加载器在WEB-INF/lib中加载
  5. 使用common类加载器在CATALINA_HOME/lib中加载 

 Tomcat7逻辑关系图

 具体实现逻辑见:   Bootstrap.initClassLoaders()

tomcat的类加载机制是违反了双亲委托原则的, 具体实现见WebAppClassLoaderBase.loadClass(),主要流程如下:

  1. 先在本地缓存中查找是否已经加载过该类(对于一些已经加载了的类,会被缓存在resourceEntries这个数据结构中),如果已经加载即返回,否则 继续下一步。
  2. 让系统类加载器(AppClassLoader)尝试加载该类,主要是为了防止一些基础类会被web中的类覆盖,如果加载到即返回,返回继续。
  3. 前两步均没加载到目标类,那么web应用的类加载器将自行加载,如果加载到则返回,否则继续下一步。
  4. 最后还是加载不到的话,则委托父类加载器(Common ClassLoader)去加载。
// 来自WebappClassLoaderBase.loadClass
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

        synchronized (getClassLoadingLock(name)) {
            if (log.isDebugEnabled())
                log.debug("loadClass(" + name + ", " + resolve + ")");
            Class<?> clazz = null;

            // Log access to stopped class loader
            checkStateForClassLoading(name);

            // (0) Check our previously loaded local class cache
            clazz = findLoadedClass0(name);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Returning class from cache");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }

            // (0.1) Check our previously loaded class cache
            clazz = findLoadedClass(name);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Returning class from cache");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }

            // (0.2) Try loading the class with the system class loader, to prevent
            //       the webapp from overriding Java SE classes. This implements
            //       SRV.10.7.2
            String resourceName = binaryNameToPath(name, false);

            ClassLoader javaseLoader = getJavaseClassLoader();
            boolean tryLoadingFromJavaseLoader;
            try {
                // Use getResource as it won't trigger an expensive
                // ClassNotFoundException if the resource is not available from
                // the Java SE class loader. However (see
                // https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 for
                // details) when running under a security manager in rare cases
                // this call may trigger a ClassCircularityError.
                tryLoadingFromJavaseLoader = (javaseLoader.getResource(resourceName) != null);
            } catch (ClassCircularityError cce) {
                // The getResource() trick won't work for this class. We have to
                // try loading it directly and accept that we might get a
                // ClassNotFoundException.
                tryLoadingFromJavaseLoader = true;
            }

            if (tryLoadingFromJavaseLoader) {
                try {
                    clazz = javaseLoader.loadClass(name);
                    if (clazz != null) {
                        if (resolve)
                            resolveClass(clazz);
                        return (clazz);
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }

            // (0.5) Permission to access this class when using a SecurityManager
            if (securityManager != null) {
                int i = name.lastIndexOf('.');
                if (i >= 0) {
                    try {
                        securityManager.checkPackageAccess(name.substring(0,i));
                    } catch (SecurityException se) {
                        String error = "Security Violation, attempt to use " +
                            "Restricted Class: " + name;
                        log.info(error, se);
                        throw new ClassNotFoundException(error, se);
                    }
                }
            }

            boolean delegateLoad = delegate || filter(name, true);

            // (1) Delegate to our parent if requested
            if (delegateLoad) {
                if (log.isDebugEnabled())
                    log.debug("  Delegating to parent classloader1 " + parent);
                try {
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {
                        if (log.isDebugEnabled())
                            log.debug("  Loading class from parent");
                        if (resolve)
                            resolveClass(clazz);
                        return (clazz);
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }

            // (2) Search local repositories
            if (log.isDebugEnabled())
                log.debug("  Searching local repositories");
            try {
                clazz = findClass(name);
                if (clazz != null) {
                    if (log.isDebugEnabled())
                        log.debug("  Loading class from local repository");
                    if (resolve)
                        resolveClass(clazz);
                    return (clazz);
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }

            // (3) Delegate to parent unconditionally
            if (!delegateLoad) {
                if (log.isDebugEnabled())
                    log.debug("  Delegating to parent classloader at end: " + parent);
                try {
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {
                        if (log.isDebugEnabled())
                            log.debug("  Loading class from parent");
                        if (resolve)
                            resolveClass(clazz);
                        return (clazz);
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }
        }

        throw new ClassNotFoundException(name);
    }

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值