package org.hotswap.agent.plugin.spring;
import org.hotswap.agent.annotation.Init;
import org.hotswap.agent.annotation.OnClassLoadEvent;
import org.hotswap.agent.annotation.Plugin;
import org.hotswap.agent.command.Scheduler;
import org.hotswap.agent.javassist.*;
import org.hotswap.agent.logging.AgentLogger;
import org.hotswap.agent.plugin.spring.getbean.ProxyReplacerTransformer;
import org.hotswap.agent.plugin.spring.scanner.ClassPathBeanDefinitionScannerTransformer;
import org.hotswap.agent.plugin.spring.scanner.ClassPathBeanRefreshCommand;
import org.hotswap.agent.util.HotswapTransformer;
import org.hotswap.agent.util.IOUtils;
import org.hotswap.agent.util.PluginManagerInvoker;
import org.hotswap.agent.util.classloader.*;
import org.hotswap.agent.watch.WatchFileEvent;
import org.hotswap.agent.watch.WatchEventListener;
import org.hotswap.agent.watch.Watcher;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.net.URL;
import java.security.ProtectionDomain;
import java.util.Enumeration;
/**
* Spring plugin.
*
* @author Jiri Bubnik
*/
@Plugin(name = "Spring", description = "Reload Spring configuration after class definition/change.",
testedVersions = {"All between 3.0.1 - 4.2.6"}, expectedVersions = {"3x", "4x"},
supportClass = {ClassPathBeanDefinitionScannerTransformer.class, ProxyReplacerTransformer.class})
public class SpringPlugin {
private static AgentLogger LOGGER = AgentLogger.getLogger(SpringPlugin.class);
/**
* If a class is modified in IDE, sequence of multiple events is generated -
* class file DELETE, CREATE, MODIFY, than Hotswap transformer is invoked.
* ClassPathBeanRefreshCommand tries to merge these events into single command.
* Wait this this timeout after class file event.
*/
private static final int WAIT_ON_CREATE = 600;
@Init
HotswapTransformer hotswapTransformer;
@Init
Watcher watcher;
@Init
Scheduler scheduler;
@Init
ClassLoader appClassLoader;
public void init() {
LOGGER.info("Spring plugin initialized");
}
public void init(String version) {
LOGGER.info("Spring plugin initialized - Spring core version '{}'", version);
}
/**
* Register both hotswap transformer AND watcher - in case of new file the file is not known
* to JVM and hence no hotswap is called. The file may even exist, but until is loaded by Spring
* it will not be known by the JVM. File events are processed only if the class is not known to the
* classloader yet.
*
* @param basePackage only files in a basePackage
*/
public void registerComponentScanBasePackage(final String basePackage) {
LOGGER.info("Registering basePackage {}", basePackage);
final SpringChangesAnalyzer analyzer = new SpringChangesAnalyzer(appClassLoader);
hotswapTransformer.registerTransformer(appClassLoader, basePackage + ".*", new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (classBeingRedefined != null) {
if (analyzer.isReloadNeeded(classBeingRedefined, classfileBuffer)) {
scheduler.scheduleCommand(new ClassPathBeanRefreshCommand(classBeingRedefined.getClassLoader(),
basePackage, className, classfileBuffer));
}
}
return classfileBuffer;
}
});
Enumeration<URL> resourceUrls = null;
try {
resourceUrls = appClassLoader.getResources(basePackage.replace(".", "/"));
} catch (IOException e) {
LOGGER.error("Unable to resolve base package {} in classloader {}.", basePackage, appClassLoader);
return;
}
// for all application resources watch for changes
while (resourceUrls.hasMoreElements()) {
URL basePackageURL = resourceUrls.nextElement();
if (!IOUtils.isFileURL(basePackageURL)) {
LOGGER.debug("Spring basePackage '{}' - unable to watch files on URL '{}' for changes (JAR file?), limited hotswap reload support. " +
"Use extraClassPath configuration to locate class file on filesystem.", basePackage, basePackageURL);
continue;
} else {
watcher.addEventListener(appClassLoader, basePackageURL, new WatchEventListener() {
@Override
public void onEvent(WatchFileEvent event) {
if (event.isFile() && event.getURI().toString().endsWith(".class")) {
// check that the class is not loaded by the classloader yet (avoid duplicate reload)
String className;
try {
className = IOUtils.urlToClassName(event.getURI());
} catch (IOException e) {
LOGGER.trace("Watch event on resource '{}' skipped, probably Ok because of delete/create event sequence (compilation not finished yet).", e, event.getURI());
return;
}
if (!ClassLoaderHelper.isClassLoaded(appClassLoader, className)) {
// refresh spring only for new classes
scheduler.scheduleCommand(new ClassPathBeanRefreshCommand(appClassLoader,
basePackage, event), WAIT_ON_CREATE);
}
}
}
});
}
}
}
/**
* Plugin initialization is after Spring has finished its startup and freezeConfiguration is called.
*
* This will override freeze method to init plugin - plugin will be initialized and the configuration
* remains unfrozen, so bean (re)definition may be done by the plugin.
*/
@OnClassLoadEvent(classNameRegexp = "org.springframework.beans.factory.support.DefaultListableBeanFactory")
public static void register(CtClass clazz) throws NotFoundException, CannotCompileException {
StringBuilder src = new StringBuilder("{");
src.append("setCacheBeanMetadata(false);");
src.append(PluginManagerInvoker.buildInitializePlugin(SpringPlugin.class));
src.append(PluginManagerInvoker.buildCallPluginMethod(SpringPlugin.class, "init",
"org.springframework.core.SpringVersion.getVersion()", String.class.getName()));
src.append("}");
for (CtConstructor constructor : clazz.getDeclaredConstructors()) {
constructor.insertBeforeBody(src.toString());
}
// freezeConfiguration cannot be disabled because of performance degradation
// instead call freezeConfiguration after each bean (re)definition and clear all caches.
// WARNING - allowRawInjectionDespiteWrapping is not safe, however without this
// spring was not able to resolve circular references correctly.
// However, the code in AbstractAutowireCapableBeanFactory.doCreateBean() in debugger always
// showed that exposedObject == earlySingletonReference and hence everything is Ok.
// if (exposedObject == bean) {
// exposedObject = earlySingletonReference;
// The waring is because I am not sure what is going on here.
CtMethod method = clazz.getDeclaredMethod("freezeConfiguration");
method.insertBefore(
"org.hotswap.agent.plugin.spring.ResetSpringStaticCaches.resetBeanNamesByType(this); " +
"setAllowRawInjectionDespiteWrapping(true); ");
}
@OnClassLoadEvent(classNameRegexp = "org.springframework.aop.framework.CglibAopProxy")
public static void cglibAopProxyDisableCache(CtClass ctClass) throws NotFoundException, CannotCompileException {
CtMethod method = ctClass.getDeclaredMethod("createEnhancer");
method.setBody("{" +
"org.springframework.cglib.proxy.Enhancer enhancer = new org.springframework.cglib.proxy.Enhancer();" +
"enhancer.setUseCache(false);" +
"return enhancer;" +
"}");
LOGGER.debug("org.springframework.aop.framework.CglibAopProxy - cglib Enhancer cache disabled");
}
}