![](https://8lovelife-1256398294.cos.ap-shanghai.myqcloud.com/photos/38.png)
使用了很多年的 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
Buildin ClassLoader
Java 提供了一些内置的 ClassLoader 类型
BootstrapClassLoader
由 C++ 编写,负责启动 JVM 以及加载 Java 应用程序所需的核心类,是最顶层的类加载器,在 Java 中的输出为 NULL
1
2
3
4
5ClassLoader hashMapListClassLoader = HashMap.class.getClassLoader();
System.out.println("HashMap ClassLoader: " + hashMapListClassLoader);
// 输出
HashMap ClassLoader: nullPlatformClassLoader
负责加载一些核心类的扩展包,比如供第三方实现的扩展包,如:java.sql.Driver
1
2
3
4
5ClassLoader driverManagerClassLoader = DriverManager.class.getClassLoader();
System.out.println("DriverManager ClassLoader: " + driverManagerClassLoader);
// 输出
DriverManager ClassLoader: jdk.internal.loader.ClassLoaders$PlatformClassLoader@5ca881b5SystemClassLoader
用于加载应用级别的类,加载器会从 CLASSPATH( -classpath, or -cp ) 路径下查找所需加载的类文件
1
2
3
4
5ClassLoader orikaClassLoader = OrikaClassLoaderTest.class.getClassLoader();
System.out.println("OrikaClassLoaderTest ClassLoader: " + orikaClassLoader);
// 输出
OrikaClassLoaderTest ClassLoader: jdk.internal.loader.ClassLoaders$AppClassLoader@5ffd2b27
Custom ClassLoader
用户可以按需定制自己的 ClassLoader
- 从远程服务器、自定义路径下加载字节码(如:Spring 中的 LaunchedURLClassLoader)
- 实现同名同包的不同 Class 定义(如:Tomcat 中的 WebappX ClassLoader)
加载模块
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 准则
委托机制
当前 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
6java.lang.ClassLoader.loadClass()
java.net.URLClassLoader.findClass()
java.net.URLClassLoader.defineClass()
类加载器的父子关系:
CustomClassLoader -> SystemClassLoader -> SystemClassLoader -> BootstrapClassLoader类的可见性
Child ClassLoader 可以看到所有 Parent ClassLoader 加载的 Class,相反的 Parent ClassLoader 看不到 Child ClassLoader 所加载的 Class
类的唯一性
确保 Class 不被重复加载,由于 Class 加载的委托机制,很容易保证 Class 的唯一性
SPI
SPI 是 Service Provider Interface 的简称,用于方便的扩展第三方实现,此功能包含在 Java SE 6 及以后的版本中。最让人熟知的是 java.sql.Driver 接口,通过 ServiceLoader 方法就可以加载 Driver 的实现,如:mysql driver 、clickhouse driver 等。先来看下面的例子:
1 | ClassLoader serviceLoaderClassLoader = ServiceLoader.class.getClassLoader(); |
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 | public static <S> ServiceLoader<S> load(Class<S> service) { |
1 | ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); |
这里的线程上下文加载器是 SystemClassLoader 通过 SystemClassLoader 的委托机制可以成功加载 com.mysql.cj.jdbc.Driver
Orika
Orika 是一个 Java Bean 映射框架,可以快速、高效的将一个对象映射到另一个对象,使用起来也非常简单。Orika 通过生成字节码的方式使得 Java Bean 之间的映射速度与手动 setXXX 速度相当,生成字节码部分使用了 Javassist 框架
使用方式
1
2
3
4
5
6
7
8
9
10MapperFactory 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);字节码生成
1
2
3
4
5JavassistCompilerStrategy#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
40public 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
implementation 'ma.glasnost.orika:orika-core:1.5.4'
使用方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BeanCopyConfig extends ConfigurableMapper {
protected void configure(MapperFactory factory) {
super.configure(factory);
factory.classMap(A.class, B.class)
.field("a", "b")
.byDefault()
.register();
}
}
// 使用
private MapperFacade mapperFacade;
B b = new B("B");
A a = mapperFacade.map(b, A.class);
CompletableFuture
将一个大任务拆分为多个小任务,并且使各个小任务并行异步执行,是常见的性能优化形式,Java 并发包中的 CompletableFuture 能够帮助程序很方便的实现这一优化
1 | List<CompletableFuture<Void>> completableFutures = new ArrayList<>(); |
Orika 在 CompletableFuture 中使用
1
2
3
4
5
6
7
8
9
10
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;
});异常
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
30Caused 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 omittedJarLauncher
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
16JarLauncher#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
}ForkJoinPool
CompletableFuture 默认使用的是 ForkJoinPool 线程池,线程上下文加载器为 SystemClassLoader
1
2
3
4
5
6
7
8
9
10
11ForkJoinWorkerThread(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
}原因
Orika 中的 Javassist 使用 ForkJoinPool 线程上下文加载器 SystemClassLoader 加载 Java Bean,而 Java Bean 是由 Spring 自定义的 LaunchedURLClassLoader 加载,类加载路线:SystemClassLoader -> LaunchedURLClassLoader,违反了加载器类的可见性
解决
使用 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
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;
}
....
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;
});