在运行时生成的类中使用Kotlin对象

我正在使用ByteBuddy重新绑定另一个库的类,以便向其添加Spring依赖注入。 问题是我不能实例化用作拦截器的类,这意味着我不能使用Spring将ApplicationContext注入到拦截器中。

为了解决这个问题,我创建了一个对象StaticAppContext ,它通过实现ApplicationContextAware来获得注入的ApplicationContextAware

 @Component object StaticAppContext : ApplicationContextAware { private val LOGGER = getLogger(StaticAppContext::class) @Volatile @JvmStatic lateinit var context: ApplicationContext override fun setApplicationContext(applicationContext: ApplicationContext?) { context = applicationContext!! LOGGER.info("ApplicationContext injected") } } 

这是得到注入就好(我可以看到日志消息),但是当我尝试从拦截器访问ApplicationContext ,我得到kotlin.UninitializedPropertyAccessException: lateinit property context has not been initialized

在这个类中定义了重定义类和incerceptor的类:

 package nu.peg.discord.d4j import net.bytebuddy.ByteBuddy import net.bytebuddy.dynamic.ClassFileLocator import net.bytebuddy.dynamic.loading.ClassLoadingStrategy import net.bytebuddy.implementation.MethodDelegation import net.bytebuddy.implementation.SuperMethodCall import net.bytebuddy.implementation.bind.annotation.* import net.bytebuddy.matcher.ElementMatchers import net.bytebuddy.pool.TypePool import nu.peg.discord.config.BeanNameRegistry.STATIC_APP_CONTEXT import nu.peg.discord.config.StaticAppContext import nu.peg.discord.util.getLogger import org.springframework.beans.BeansException import org.springframework.beans.factory.config.AutowireCapableBeanFactory import org.springframework.context.annotation.DependsOn import org.springframework.stereotype.Component import sx.blah.discord.api.IDiscordClient import sx.blah.discord.modules.Configuration import sx.blah.discord.modules.IModule import sx.blah.discord.modules.ModuleLoader import java.lang.reflect.Constructor import java.util.ArrayList import javax.annotation.PostConstruct /** * TODO Short summary * * @author Joel Messerli @15.02.2017 */ @Component @DependsOn(STATIC_APP_CONTEXT) class D4JModuleLoaderReplacer : IModule { companion object { private val LOGGER = getLogger(D4JModuleLoaderReplacer::class) } @PostConstruct fun replaceModuleLoader() { val pool = TypePool.Default.ofClassPath() ByteBuddy().rebase<Any>( pool.describe("sx.blah.discord.modules.ModuleLoader").resolve(), ClassFileLocator.ForClassLoader.ofClassPath() ).constructor( ElementMatchers.any() ).intercept( SuperMethodCall.INSTANCE.andThen(MethodDelegation.to(pool.describe("nu.peg.discord.d4j.SpringInjectingModuleLoaderInterceptor").resolve())) ).make().load(ClassLoader.getSystemClassLoader(), ClassLoadingStrategy.Default.INJECTION) LOGGER.info("The D4J ModuleLoader has been replaced with ByteBuddy to allow for Spring injection") } override fun getName() = "Spring Injecting Module Loader" override fun enable(client: IDiscordClient?) = true override fun getVersion() = "1.0.0" override fun getMinimumDiscord4JVersion() = "1.7" override fun getAuthor() = "Joel Messerli <hi.github@peg.nu>" override fun disable() {} } class SpringInjectingModuleLoaderInterceptor { companion object { private val LOGGER = getLogger(SpringInjectingModuleLoaderInterceptor::class) @Suppress("UNCHECKED_CAST") @JvmStatic fun <T> intercept( @This loader: ModuleLoader, @Origin ctor: Constructor<T>, @Argument(0) discordClient: IDiscordClient?, @FieldValue("modules") modules: List<Class<out IModule>>, @FieldValue("loadedModules") loadedModules: MutableList<IModule> ) { LOGGER.debug("Intercepting $ctor") val loaderClass = loader.javaClass val clientField = loaderClass.getDeclaredField("client") clientField.isAccessible = true clientField.set(loader, discordClient) val canModuleLoadMethod = loaderClass.getDeclaredMethod("canModuleLoad", IModule::class.java) canModuleLoadMethod.isAccessible = true val factory = StaticAppContext.context.autowireCapableBeanFactory for (moduleClass in modules) { try { val wired = factory.autowire(moduleClass, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false) as IModule LOGGER.info("Loading autowired module {}@{} by {}", wired.name, wired.version, wired.author) if (canModuleLoadMethod.invoke(loader, wired) as Boolean) { loadedModules.add(wired) } else { LOGGER.info("${wired.name} needs at least version ${wired.minimumDiscord4JVersion} to be loaded (skipped)") } } catch (e: BeansException) { LOGGER.info("Spring could not create bean", e) } } if (Configuration.AUTOMATICALLY_ENABLE_MODULES) { // Handles module load order and loads the modules val toLoad = ArrayList<IModule>(loadedModules) val loadModuleMethod = loaderClass.getDeclaredMethod("loadModule", IModule::class.java) while (toLoad.size > 0) { toLoad.filter { loadModuleMethod.invoke(loader, it) as Boolean }.forEach { toLoad.remove(it) } } } LOGGER.info("Module loading complete") } } } 

当我调试这个,IntelliJ显示当拦截器试图访问StaticAppContext时创建一个新的StaticAppContext实例,这是有道理的,因为引发异常。

从生成的代码调用时,Kotlin对象是不是真正的Singletons,或者我做错了什么? 什么是解决这个问题的方法?

该项目也可以在Github上找到: https : //github.com/jmesserli/discord-bernbot/tree/master/src/main/kotlin/nu/peg/discord


编辑
我能够通过删除spring-boot-devtools来修复这个问题,并添加他们自己的ClassLoader 。 当我尝试建议使用Thread.currentThread().contextClassLoader ,我得到了一个不同的异常告诉我,它已经加载了一个不同的ClassLoader (这确认这是一个ClassLoader的问题)。 而且,似乎可能有种族的假设是正确的。

我现在有一个不同的问题,我会做一些研究,看看我能否自己解决。

免责声明:我是一个业余爱好程序员,还没有与春季工作。 这里有一些猜测是基于我所听到的有关Spring的。


我有一个预感,这可能是一个类加载器的问题 – 由于您在D4JModuleLoaderReplacer.replaceModuleLoader()使用了ClassLoader.getSystemClassLoader() ,您可能会在2个不同的类加载器中加载2个StaticAppContext类。

要确认这一点,请在init { ... }块中记录StaticAppContext对象的创建。 例:

 @Component object StaticAppContext : ApplicationContextAware { private val LOGGER = getLogger(StaticAppContext::class) init { LOGGER.info("StaticAppContext created. Classloader: ${javaClass.classLoader}") } ... } 

如果我的理论是正确的,你应该得到2个创建日志消息。

如果是这样的话,我相信你应该使用当前上下文classloader( Thread.currentThread().getContextClassLoader() )来代替。

Kotlin object被编译为以下布局:

 public final class StaticAppContext { public static final StaticAppContext INSTANCE; private StaticAppContext(); static {} } 

这个类是隐式的单例。 所以我想知道这个问题是否是一个类加载的竞赛。 有一个很好的机会,已经调用静态初始化器。 你确定你正在得到正确的日志消息吗?