Jetty            

Class Loading 

机制浅析

内容

  • 返回事故现场
  • 背后原因分析:jetty的Class Loading机制
  • 学到了什么?

返回事故现场

迁移PhpUtils

public static List arrayUnique(List rl) {
        int size = rl.size();
        for (int i = size - 1; i >= 0; i--) {
            Object r1 = rl.get(i);
            boolean found = false;
            for (int j = size - 1; j >= 0; j--) {
                if (j == i) {
                    continue;
                }
                if (r1.equals(rl.get(j))) {
                    found = true;
                    break;
                }
            }
            if (found) {
                rl.remove(i);
                size = rl.size();
            }
        }
        return rl;
}
@TestsUtil.RefactorTest(
        classRef = "io.conndots.util.PhpUtils",
        methodName = "arrayUnique"
)
public static <T> List<T> getDuplicatesRemovedList(List<T> list) {
        if (list == null) {
            return null;
        }
        if (CollectionUtils.isEmpty(list)) {
            return Lists.newArrayList();
        }

        Set<T> addedSet = Sets.newHashSet();
        List<T> nonDuplicatesList = Lists.newArrayList();
        for (T element : list) {
            if (addedSet.contains(element)) {
                continue;
            }
            nonDuplicatesList.add(element);
            addedSet.add(element);
        }
        return nonDuplicatesList;
}

TestsUtil

public static <T> T decorateFunctionWithRefactorTest(Class<?> cls, String method, Object ... params) ;
List<String> result = TestsUtil.decorateFunctionWithRefactorTest(CollectionsUtil.class, 
        "getDuplicatesRemovedList", list) ;

Example:

@TestsUtil.RefactorTest(
        classRef = "io.conndots.util.PhpUtils",
        methodName = "arrayUnique"
)

ClassLoader to load the ref class

Use Reflection and designed mechanism to

find the specified method

Use Reflection and designed mechanism to

find the specified method

new method

previous method

executed

result

executed

result

比较结果,若不等,写入日志;

返回新方法的结果。

后果

灰度上线后,日志中记录了大量的错误信息(这类信息应当写入DEBUG级别日志),表示ClassLoader找不到对应的类

立即回滚。

出问题的代码

解决方案

RefactorTest refactorAnno = refactorTo
        .getAnnotation(RefactorTest.class);
String refactorFromCls =  refactorAnno.classRef();
String refactorFromMethod = refactorAnno.methodName();
int[] paramClassesIndex = refactorAnno
        .paramClassIndex2ThisParams();

Class<?> refactorFromClass = ClassLoader
        .getSystemClassLoader().loadClass(refactorFromCls);
RefactorTest refactorAnno = refactorTo
        .getAnnotation(RefactorTest.class);
String refactorFromCls =  refactorAnno.classRef();
String refactorFromMethod = refactorAnno.methodName();
int[] paramClassesIndex = refactorAnno.paramClassIndex2ThisParams();

Class<?> refactorFromClass = TestsUtil.class.getClassLoader()
.loadClass(refactorFromCls);

在线下可以通过单元测试,部署到服务器后出现问题。

说明:部署在servlet容器中的应用中的ClassLoading机制与线下Java Runtime的ClassLoading机制是不同的,不能想当然。

背后原因分析

getSystemClassLoader()得到的是SystemClassLoader。

TestsUtil.class.getClassLoader()得到的是载入当前类的ClassLoader,由于在问题的项目中,待载入的类与TestsUtil在一个jar包之中,所以不会出同样的问题。

为什么问题被解决了?

Java SE与JAVA EE(jetty)的Class Loading机制不同

本机单元测试通过,而在Jetty中出了问题

Class Loading的delegation机制

ClassLoader A

ClassLoader B

ClassLoader C

ClassLoader D

Class Loading Delegation Hierarchy

Not a inheritance hierarchy

Parent-first:

A在载入特定的类之前,先委托给委托关系的父类B载入.

B会先委托给父类D载入。

如若D无法载入,B再看自己能不能载入,如不可以,返回A来载入。

Child-first:

A先载入特定类,找不到后,再委托父类载入,以此类推。

Assurance:

一个类只会被载入一次。

对A来说,C、D载入的类是可见的。反过来,指定D载入一个类,它无法委托给它的子类去载入。

 

JAVA SE的class loading Delegation Hierarchy

Bootstrap ClassLoader

Extensions ClassLoader

System ClassLoader

core java libs(<JAVA_HOME>/jre/lib)

extention libs(<JAVA_HOME>/jre/lib/ext)

libs in classpath

ClassLoader.getSystemClassLoader()得到SystemClassLoader根据Delegation hierarchy和其中的规则必然能够找到指定的类。

JaVA EE中的CLass Loading Delegation Hierarchy

System Class Loader主要用于load 应用服务器相关的类。

 

Web Class Loader载入当前特定应用的servlet类与其他类和包。

 

根据可见性原则,Web Class Loader能载入的类对System Class Loader不可见,反之却是可见的。

所以,之前的问题的发生就可解释了。

 

具体到jetty

public void preConfigure() throws Exception{
        // Setup configurations
        loadConfigurations();

        // Setup system classes
        loadSystemClasses();

        // Setup server classes
        loadServerClasses();

        // Configure classloader
        _ownClassLoader=false;
        if (getClassLoader()==null)
        {
            WebAppClassLoader classLoader = new 
                    WebAppClassLoader(this);
            setClassLoader(classLoader);
            _ownClassLoader=true;
        }

        ...
}
org.eclipse.jetty.webapp.WebAppContext

jetty的loadclass方法,逻辑比较好理解:

  • 如果属于system_class或者_parentLoaderPriority=true,并且不是server_class,优先采用parent first进行装载

 

 

 

  • 否则采用child first,先由WebAppClassLoader进行装载,没找到class再交由父ClassLoader装载
(_context.isParentLoaderPriority() || system_class) 
        && !server_class) 

所以

RefactorTest refactorAnno = refactorTo
        .getAnnotation(RefactorTest.class);
String refactorFromCls =  refactorAnno.classRef();
String refactorFromMethod = refactorAnno.methodName();
int[] paramClassesIndex = refactorAnno.paramClassIndex2ThisParams();

Class<?> refactorFromClass = TestsUtil.class.getClassLoader()
.loadClass(refactorFromCls);
RefactorTest refactorAnno = refactorTo
        .getAnnotation(RefactorTest.class);
String refactorFromCls =  refactorAnno.classRef();
String refactorFromMethod = refactorAnno.methodName();
int[] paramClassesIndex = refactorAnno
        .paramClassIndex2ThisParams();

Class<?> refactorFromClass = ClassLoader
        .getSystemClassLoader().loadClass(refactorFromCls);

在JavaSE中得到的是SystemCL,

在委托关系的最下层。

在Jetty中得到的CL在委托关系的top结构中。由于可见性原则,无法载入应用内的类。

在JavaSE中得到的是SystemCL,

在委托关系的最下层。

在Jetty中得到的CL是Web Class Loader,而且是child-first,优先使用Web Class Loader载入指定类,找不到时再去找父CL载入。

One More Thing

Delegation Hierarchy与热部署

默认情况下,JVM在启动时候加载一次类。如果运行时类被修改了,JVM并不会监控类的改动,对其重新编译成字节码替换原来的.class。

修改虚拟机的源代码,改变 classloader 的加载行为,使虚拟机能监听 class 文件的更新,重新加载 class 文件。

OR

创建自己的 classloader 来加载需要监听的 class,这样就能控制类加载的时机,从而实现热部署。

理解Class loading的委托机制,让自己订制的ClassLoader抢先加载需要监控改变的类。

Jetty支持Hot Deploy

通过配置WebAppProvider的属性实现:

  1. monitoredDirName
  2. scanInterval

学到了什么?

背后一定隐藏了巨大的“阴谋”,线索被遗漏了。

对于计算机里发生的事情,不能想当然。

程序的Runtime environment很重要。

做技术,要知其然,更要知其所以然。

短期内是要解决问题,长期内更要通过经历与研究预防未来可能的问题。

Thanks

Jetty ClassLoader机制浅析

By Xiangqian Lee

Jetty ClassLoader机制浅析

  • 1,636