2017-02-19 98 views
2

我正在使用ByteBuddy重新綁定另一個庫的類以向其添加Spring依賴注入。問題是我不能實例化用作攔截器的類,這意味着我不能使用Spring將ApplicationContext注入攔截器。在運行時生成的類中使用Kotlin對象

要解決這個問題,我創建了一個對象StaticAppContext,其獲取實現ApplicationContextAware注入ApplicationContext

@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 <[email protected]>" 
    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的問題)。而且,似乎可能有種族的假設是正確的。

我現在有一個不同的問題,我會做一些研究,看看我能否自己解決它。

回答

0

聲明:我是一個業餘愛好程序員,並沒有與春季工作。這裏有一些猜測是基於我對聽說有關Spring的。


我有一種預感,這可能是一個類加載器的問題 - 你可以在2級不同的類裝載器裝載2 StaticAppContext類由於你在D4JModuleLoaderReplacer.replaceModuleLoader()ClassLoader.getSystemClassLoader()使用。

要確認這一點,請在init { ... }塊中記錄創建StaticAppContext對象。例如:

@Component 
object StaticAppContext : ApplicationContextAware { 
    private val LOGGER = getLogger(StaticAppContext::class) 

    init { 
     LOGGER.info("StaticAppContext created. Classloader: ${javaClass.classLoader}") 
    } 

    ... 
} 

如果我的理論是正確的,你應該得到2個創建日誌消息。

如果是這種情況,我相信你應該使用當前的上下文類加載器(Thread.currentThread().getContextClassLoader())來代替。

1

甲科特林object被編譯成如下的佈局:

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

的類是隱含一個單。因此我想知道這個問題是否是一場類加載的比賽。很有可能已經調用了靜態初始化器。你確定你正在收到正確的日誌消息嗎?