Orika ClassLoader | 8lovelife's life
0%

Orika ClassLoader

使用了很多年的 Java Bean 对象映射框架 Orika,最近运行报错了,异常出现在某些特定的使用场景下,具体场景在 Spring 容器中使用 CompletableFuture,异步任务中进行一些 Java Bean 的对象映射,异常信息:

1
Caused by: ma.glasnost.orika.impl.generator.CompilerStrategy$SourceCodeGenerationException: Error compiling ma.glasnost.orika.generated.Orika_XXX_YYY_Mapper383167261886761$8

本篇博客将分析记录 Orika 使用异常产生的原因,同时回顾一些相关知识

ClassLoader

在程序运行中,ClassLoader 负责动态的按需将字节码加载到 JVM 中,字节码可以是 .java 文件编译后获得的 .class 文件,程序也可以按照字节码的定义规范直接生成字节码,比如通过 ASM,Javassis 字节码生成工具直接编辑字节码。Java 提供了一些内置的 ClassLoader 类型,用户也可以按需定制自己的 ClassLoader

  1. Buildin ClassLoader

    Java 提供了一些内置的 ClassLoader 类型

    • BootstrapClassLoader

      由 C++ 编写,负责启动 JVM 以及加载 Java 应用程序所需的核心类,是最顶层的类加载器,在 Java 中的输出为 NULL

      1
      2
      3
      4
      5
      ClassLoader hashMapListClassLoader = HashMap.class.getClassLoader();
      System.out.println("HashMap ClassLoader: " + hashMapListClassLoader);

      // 输出
      HashMap ClassLoader: null
    • PlatformClassLoader

      负责加载一些核心类的扩展包,比如供第三方实现的扩展包,如:java.sql.Driver

      1
      2
      3
      4
      5
      ClassLoader driverManagerClassLoader = DriverManager.class.getClassLoader();
      System.out.println("DriverManager ClassLoader: " + driverManagerClassLoader);

      // 输出
      DriverManager ClassLoader: jdk.internal.loader.ClassLoaders$PlatformClassLoader@5ca881b5
    • SystemClassLoader

      用于加载应用级别的类,加载器会从 CLASSPATH( -classpath, or -cp ) 路径下查找所需加载的类文件

      1
      2
      3
      4
      5
      ClassLoader orikaClassLoader = OrikaClassLoaderTest.class.getClassLoader();
      System.out.println("OrikaClassLoaderTest ClassLoader: " + orikaClassLoader);

      // 输出
      OrikaClassLoaderTest ClassLoader: jdk.internal.loader.ClassLoaders$AppClassLoader@5ffd2b27
  2. Custom ClassLoader

    用户可以按需定制自己的 ClassLoader

    • 从远程服务器、自定义路径下加载字节码(如:Spring 中的 LaunchedURLClassLoader)
    • 实现同名同包的不同 Class 定义(如:Tomcat 中的 WebappX ClassLoader)
  3. 加载模块

    Java SE 17 中各内置加载器负责加载的模块

    BootstrapClassLoader PlatformClassLoader SystemClassLoader
    java.base java.compiler jdk.attach
    java.datatransfer java.net.http jdk.compiler
    java.desktop java.scripting jdk.editpad
    java.instrument java.security.jgss jdk.internal.ed
    java.logging java.smartcardio jdk.internal.jvmstat
    java.management java.sql jdk.internal.le
    java.management.rmi java.sql.rowset jdk.internal.opt
    java.naming java.transaction.xa jdk.jartool
    java.prefs java.xml.crypto jdk.javadoc
    java.rmi jdk.accessibility jdk.jconsole
    java.security.sasl jdk.charsets jdk.jdeps
    java.xml jdk.crypto.cryptoki jdk.jdi
    jdk.jfr jdk.crypto.ec jdk.jdwp.agent
    jdk.management jdk.dynalink jdk.jlink
    jdk.management.agent jdk.httpserver jdk.jpackage
    jdk.management.jfr jdk.jsobject jdk.jshell
    jdk.naming.rmi jdk.localedata jdk.jstatd
    jdk.net jdk.naming.dns jdk.random
    jdk.nio.mapmode jdk.security.auth jdk.unsupported.desktop
    jdk.sctp jdk.security.jgss
    jdk.unsupported jdk.xml.dom
    jdk.zipfs

ClassLoader 准则

  1. 委托机制

    当前 ClassLoader 加载 Class 调用 java.lang.ClassLoader.loadClass() 方法,首先会确定此 Class 是否已经被加载,避免重复加载,如果 Class 未被加载过,则会委托 Parent ClassLoader 进行此 Class 的加载 java.lang.ClassLoader.loadClass(),递归进行,一直到 loadClass() 成功返回 Class。如果未成功返回 Class,则 Child ClassLoader 会调用 java.net.URLClassLoader.findClass() 查找字节码并通过 defineClass() 方法将字节码转换成 Class 对象,成功则返回,否则加载失败抛出 java.lang.NoClassDefFoundError 或 java.lang.ClassNotFoundException 异常。核心方法:

    1
    2
    3
    4
    5
    6
    java.lang.ClassLoader.loadClass()
    java.net.URLClassLoader.findClass()
    java.net.URLClassLoader.defineClass()

    类加载器的父子关系:
    CustomClassLoader -> SystemClassLoader -> SystemClassLoader -> BootstrapClassLoader
  2. 类的可见性

    Child ClassLoader 可以看到所有 Parent ClassLoader 加载的 Class,相反的 Parent ClassLoader 看不到 Child ClassLoader 所加载的 Class

  3. 类的唯一性

    确保 Class 不被重复加载,由于 Class 加载的委托机制,很容易保证 Class 的唯一性

SPI

SPI 是 Service Provider Interface 的简称,用于方便的扩展第三方实现,此功能包含在 Java SE 6 及以后的版本中。最让人熟知的是 java.sql.Driver 接口,通过 ServiceLoader 方法就可以加载 Driver 的实现,如:mysql driver 、clickhouse driver 等。先来看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ClassLoader serviceLoaderClassLoader = ServiceLoader.class.getClassLoader();
ClassLoader mysqlClassLoader = com.mysql.cj.jdbc.Driver.class.getClassLoader();
ClassLoader sqlClassLoader = Driver.class.getClassLoader();
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(java.sql.Driver.class);
Driver driver = loadedDrivers.findFirst().get();
System.out.println("Loaded Driver is " + driver);
System.out.println("Sql Driver ClassLoader is " + sqlClassLoader);
System.out.println("Mysql Driver ClassLoader is " + mysqlClassLoader);
System.out.println("ServiceLoader ClassLoader: " + serviceLoaderClassLoader);

// 输出
Loaded Driver is com.mysql.cj.jdbc.Driver@34a245ab
Sql Driver ClassLoader is jdk.internal.loader.ClassLoaders$PlatformClassLoader@24d46ca6
Mysql Driver ClassLoader is jdk.internal.loader.ClassLoaders$AppClassLoader@5ffd2b27
ServiceLoader ClassLoader: null

Mysql Driver 通过 ServiceLoader 的 load 方法被成功加载,但这里是不是违背了 ClassLoader 准则中类的可见性?ServiceLoader.class 是由 BootstrapClassLoader 加载的,java.sql.Driver.class 是由 PlatformClassLoader 加载的,com.mysql.cj.jdbc.Driver.class 是由 AppClassLoader 加载的,加载路线 BootstrapClassLoader -> PlatformClassLoader -> AppClassLoader,与类的可见性矛盾。如何解决 SPI 类加载可见性的矛盾?答案是:线程上下文类加载器

线程上下文类加载器

查看 ServiceLoader#load(..) 方法的实现可以发现,ServiceLoader 使用的是线程上下文加载器来加载 SPI 实现类

1
2
3
4
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}

1
2
3
4
5
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
System.out.println("Thread Context ClassLoader: " + contextClassLoader);

// 输出
Thread Context ClassLoader: jdk.internal.loader.ClassLoaders$AppClassLoader@5ffd2b27

这里的线程上下文加载器是 SystemClassLoader 通过 SystemClassLoader 的委托机制可以成功加载 com.mysql.cj.jdbc.Driver

Orika

Orika 是一个 Java Bean 映射框架,可以快速、高效的将一个对象映射到另一个对象,使用起来也非常简单。Orika 通过生成字节码的方式使得 Java Bean 之间的映射速度与手动 setXXX 速度相当,生成字节码部分使用了 Javassist 框架

  1. 使用方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    MapperFactory mapperFactory = new DefaultMapperFactory.    Builder().build();
    mapperFactory.classMap(A.class, B.class)
    .field("a", "b")
    .byDefault()
    .register();

    // 使用
    MapperFacade mapperFacade = mapperFactory.getMapperFacade();
    B b = new B("B");
    A a = mapperFacade.map(b, A.class);
  2. 字节码生成

    1
    2
    3
    4
    5
    JavassistCompilerStrategy#compileClass(...){
    ...
    compiledClass = byteCodeClass.toClass(Thread.currentThread().getContextClassLoader(), this.getClass().getProtectionDomain ());
    ...
    }

    在程序运行中,Javassist 会生成字节码并编译为类似如下 Class 对象,并进行 mapping 操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    public class Orika_B_A_Mapper347584518546208$0 extends ma.glasnost.orika.impl.GeneratedMapperBase {

    public void mapAtoB(java.lang.Object a, java.lang.Object b, ma.glasnost.orika.MappingContext mappingContext) {

    super.mapAtoB(a, b, mappingContext);

    // sourceType: A
    orika.A source = ((orika.A)a);
    // destinationType: B
    orika.B destination = ((orika.B)b);

    destination.setB(((java.lang.String)source.getA()));
    if(customMapper != null) {
    customMapper.mapAtoB(source, destination, mappingContext);
    }
    }
    ...

    public class B_A_ObjectFactory7711776903783377242228996375$1 extends ma.glasnost.orika.impl.GeneratedObjectFactory {

    public Object create(Object s, ma.glasnost.orika.MappingContext mappingContext)
    {if(s == null) throw new java.lang.IllegalArgumentException("source object must be not null");
    if (s instanceof orika.A) {
    orika.A source = (orika.A) s;
    try {
    java.lang.String arg0 = null;
    arg0 = ((java.lang.String)source.getA());
    return new orika.B(arg0);
    } catch (java.lang.Exception e) {
    if (e instanceof RuntimeException) {
    throw (RuntimeException)e;} else {
    throw new java.lang.RuntimeException("Error while constructing new B instance", e);
    }
    }
    }
    return new orika.B();
    }
    ...
    }

Spring 集成

  1. 引入依赖

    1
    implementation 'ma.glasnost.orika:orika-core:1.5.4'
  2. 使用方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Configuration
    public class BeanCopyConfig extends ConfigurableMapper {

    @Override
    protected void configure(MapperFactory factory) {
    super.configure(factory);
    factory.classMap(A.class, B.class)
    .field("a", "b")
    .byDefault()
    .register();
    }
    }

    // 使用
    @Resource
    private MapperFacade mapperFacade;

    B b = new B("B");
    A a = mapperFacade.map(b, A.class);

CompletableFuture

将一个大任务拆分为多个小任务,并且使各个小任务并行异步执行,是常见的性能优化形式,Java 并发包中的 CompletableFuture 能够帮助程序很方便的实现这一优化

1
2
3
4
5
6
7
8
9
10
List<CompletableFuture<Void>> completableFutures = new ArrayList<>();
for (:) {
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> doSomething())
.exceptionally(ex -> {
log.error("something wrong", ex);
return null;
});
completableFutures.add(completableFuture);
}
CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0])).join();
  1. Orika 在 CompletableFuture 中使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Resource
    private MapperFacade mapperFacade;

    CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
    B b = new B("B");
    A a = mapperFacade.map(b, A.class);
    }).exceptionally(ex -> {
    log.error("something wrong", ex);
    return null;
    });
  2. 异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    Caused by: ma.glasnost.orika.impl.generator.CompilerStrategy$SourceCodeGenerationException: Error compiling ma.glasnost.    orika.generated.Orika_XXX_YYY_Mapper383167261886761$8
    at ma.glasnost.orika.impl.generator.JavassistCompilerStrategy.compileClass(JavassistCompilerStrategy.java:253)
    at ma.glasnost.orika.impl.generator.SourceCodeContext.compileClass(SourceCodeContext.java:228)
    at ma.glasnost.orika.impl.generator.SourceCodeContext.getInstance(SourceCodeContext.java:244)
    at ma.glasnost.orika.impl.generator.MapperGenerator.build(MapperGenerator.java:73)
    ... 17 common frames omitted
    Caused by: javassist.CannotCompileException: by java.lang.reflect.InvocationTargetException
    at javassist.util.proxy.DefineClassHelper$JavaOther.defineClass(DefineClassHelper.java:220)
    at javassist.util.proxy.DefineClassHelper$Java11.defineClass(DefineClassHelper.java:52)
    at javassist.util.proxy.DefineClassHelper.toClass(DefineClassHelper.java:260)
    at javassist.ClassPool.toClass(ClassPool.java:1240)
    at javassist.CtClass.toClass(CtClass.java:1392)
    at ma.glasnost.orika.impl.generator.JavassistCompilerStrategy.compileClass(JavassistCompilerStrategy.java:246)
    ... 20 common frames omitted
    Caused by: java.lang.reflect.InvocationTargetException: null
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at javassist.util.proxy.DefineClassHelper$JavaOther.defineClass(DefineClassHelper.java:214)
    ... 25 common frames omitted
    Caused by: java.lang.NoClassDefFoundError: ma/glasnost/orika/impl/GeneratedMapperBase
    at java.base/java.lang.ClassLoader.defineClass1(Native Method)
    at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1012)
    ... 30 common frames omitted
    Caused by: java.lang.ClassNotFoundException: ma.glasnost.orika.impl.GeneratedMapperBase
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
    ... 32 common frames omitted
  3. JarLauncher

    Spring 应用被打包为 FatJar 的形式,并通过 JarLauncher 来启动 FatJar 应用,JarLauncher 自定义了类加载器 org.springframework.boot. loader.LaunchedURLClassLoader@25f38edc,其 Parent ClassLoader 被设置为 SystemClassLoader

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    JarLauncher#launch(..)

    protected void launch(String[] args) throws Exception {
    if (!isExploded()) {
    JarFile.registerUrlProtocolHandler();
    }
    ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
    String jarMode = System.getProperty("jarmode");
    String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
    launch(args, launchClass, classLoader);
    }

    protected ClassLoader createClassLoader(URL[] urls) throws Exception {
    return new LaunchedURLClassLoader(isExploded(), getArchive(), urls, getClass().getClassLoader());
    // Parent ClassLoader 是 getClass().getClassLoader(),即 SystemClassLoader
    }
  4. ForkJoinPool

    CompletableFuture 默认使用的是 ForkJoinPool 线程池,线程上下文加载器为 SystemClassLoader

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ForkJoinWorkerThread(ThreadGroup group, ForkJoinPool pool,
    boolean useSystemClassLoader, boolean isInnocuous) {
    super(group, null, pool.nextWorkerThreadName(), 0L);
    UncaughtExceptionHandler handler = (this.pool = pool).ueh;
    this.workQueue = new ForkJoinPool.WorkQueue(this, isInnocuous);
    super.setDaemon(true);
    if (handler != null)
    super.setUncaughtExceptionHandler(handler);
    if (useSystemClassLoader)
    super.setContextClassLoader(ClassLoader.getSystemClassLoader()); // 线程上下文加载器被设置为 SystemClassLoader
    }
  5. 原因

    Orika 中的 Javassist 使用 ForkJoinPool 线程上下文加载器 SystemClassLoader 加载 Java Bean,而 Java Bean 是由 Spring 自定义的 LaunchedURLClassLoader 加载,类加载路线:SystemClassLoader -> LaunchedURLClassLoader,违反了加载器类的可见性

  6. 解决

    使用 ThreadPoolTaskExecutor 线程池运行 CompletableFuture

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @Bean(name = "asyncServiceExecutor")
    public Executor asyncServiceExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(10);
    executor.setMaxPoolSize(10);
    executor.setThreadNamePrefix("-async-");
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    executor.initialize();
    return executor;
    }

    ....

    @Resource
    private Executor asyncServiceExecutor;

    CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
    B b = new B("B");
    A a = mapperFacade.map(b, A.class);
    },asyncServiceExecutor).exceptionally(ex -> {
    log.error("something wrong", ex);
    return null;
    });