package act.app;
/*-
* #%L
* ACT Framework
* %%
* Copyright (C) 2014 - 2017 ActFramework
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import act.Act;
import act.app.event.AppEventId;
import act.app.util.EnvMatcher;
import act.asm.AsmException;
import act.asm.ClassReader;
import act.asm.ClassWriter;
import act.boot.BootstrapClassLoader;
import act.boot.app.FullStackAppBootstrapClassLoader;
import act.cli.meta.CommanderClassMetaInfo;
import act.cli.meta.CommanderClassMetaInfoHolder;
import act.cli.meta.CommanderClassMetaInfoManager;
import act.conf.AppConfig;
import act.controller.meta.ControllerClassMetaInfo;
import act.controller.meta.ControllerClassMetaInfoHolder;
import act.controller.meta.ControllerClassMetaInfoManager;
import act.event.AppEventListenerBase;
import act.exception.EnvNotMatchException;
import act.job.meta.JobClassMetaInfo;
import act.job.meta.JobClassMetaInfoManager;
import act.mail.meta.MailerClassMetaInfo;
import act.mail.meta.MailerClassMetaInfoHolder;
import act.mail.meta.MailerClassMetaInfoManager;
import act.metric.Metric;
import act.metric.MetricInfo;
import act.util.*;
import act.view.ActErrorResult;
import org.osgl.$;
import org.osgl.exception.NotAppliedException;
import org.osgl.logging.L;
import org.osgl.logging.Logger;
import org.osgl.util.C;
import org.osgl.util.E;
import org.osgl.util.IO;
import org.osgl.util.S;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.util.*;
import static act.util.ClassInfoRepository.canonicalName;
import static org.osgl.$.notNull;
/**
* The top level class loader to load a specific application classes into JVM
*/
@ApplicationScoped
public class AppClassLoader
extends ClassLoader
implements
ControllerClassMetaInfoHolder,
CommanderClassMetaInfoHolder,
MailerClassMetaInfoHolder,
AppService<AppClassLoader>,
ActClassLoader {
private final static Logger logger = L.get(AppClassLoader.class);
private App app;
private Map<String, byte[]> libClsCache = C.newMap();
private ClassInfoRepository classInfoRepository;
private boolean destroyed;
protected ControllerClassMetaInfoManager controllerInfo;
protected MailerClassMetaInfoManager mailerInfo = new MailerClassMetaInfoManager();
protected CommanderClassMetaInfoManager commanderInfo = new CommanderClassMetaInfoManager();
protected JobClassMetaInfoManager jobInfo = new JobClassMetaInfoManager();
protected SimpleBean.MetaInfoManager simpleBeanInfo;
protected Metric metric = Act.metricPlugin().metric(MetricInfo.CLASS_LOADING);
@Inject
public AppClassLoader(final App app) {
super(Act.class.getClassLoader());
this.app = notNull(app);
ClassInfoRepository actClassInfoRepository = Act.classInfoRepository();
if (null != actClassInfoRepository) {
this.classInfoRepository = new AppClassInfoRepository(app, actClassInfoRepository);
}
controllerInfo = new ControllerClassMetaInfoManager(app);
if (null == app.eventBus()) {
return; // for unit test only
}
simpleBeanInfo = new SimpleBean.MetaInfoManager(this);
app.eventBus().bind(AppEventId.APP_CODE_SCANNED, new AppEventListenerBase() {
@Override
public String id() {
return "appClassLoader:controllerInfo:mergeActionMetaInfo";
}
@Override
public void on(EventObject event) throws Exception {
controllerInfo.mergeActionMetaInfo(app);
}
});
}
@Override
public final boolean isDestroyed() {
return destroyed;
}
@Override
public Class<? extends Annotation> scope() {
return ApplicationScoped.class;
}
@Override
public AppClassLoader app(App app) {
throw E.unsupport();
}
public final App app() {
return app;
}
@Override
public final void destroy() {
libClsCache.clear();
controllerInfo.destroy();
mailerInfo.destroy();
jobInfo.destroy();
simpleBeanInfo.destroy();
releaseResources();
destroyed = true;
}
@Override
public ClassInfoRepository classInfoRepository() {
return classInfoRepository;
}
protected void releaseResources() {
classInfoRepository.destroy();
}
public void detectChanges() {
// don't do anything when running in none-dev mode
}
public ControllerClassMetaInfo controllerClassMetaInfo(String controllerClassName) {
return controllerInfo.controllerMetaInfo(controllerClassName);
}
public ControllerClassMetaInfoManager controllerClassMetaInfoManager() {
return controllerInfo;
}
public CommanderClassMetaInfo commanderClassMetaInfo(String commanderClassName) {
return commanderInfo.commanderMetaInfo(commanderClassName);
}
public CommanderClassMetaInfoManager commanderClassMetaInfoManager() {
return commanderInfo;
}
public SimpleBean.MetaInfoManager simpleBeanInfoManager() {
return simpleBeanInfo;
}
@Override
public MailerClassMetaInfo mailerClassMetaInfo(String className) {
return mailerInfo.mailerMetaInfo(className);
}
public MailerClassMetaInfoManager mailerClassMetaInfoManager() {
return mailerInfo;
}
public JobClassMetaInfo jobClassMetaInfo(String jobClassName) {
return jobInfo.jobMetaInfo(jobClassName);
}
public JobClassMetaInfoManager jobClassMetaInfoManager() {
return jobInfo;
}
public SimpleBean.MetaInfo simpleBeanMetaInfo(String className) {
return simpleBeanInfo.get(className);
}
public boolean isSourceClass(String className) {
return false;
}
public Class<?> loadedClass(String name) {
Class<?> c = findLoadedClass(name);
if (null == c) {
ClassLoader p = getParent();
if (null != p && (p instanceof ActClassLoader || p instanceof BootstrapClassLoader)) {
return ((ActClassLoader) p).loadedClass(name);
}
}
return c;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
if (c != null) {
return c;
}
// ensure we can enhance the act classes specified below
if (name.startsWith("act.") && name.endsWith("Admin")) {
return super.loadClass(name, resolve);
}
c = loadAppClass(name, resolve);
if (null == c) {
return super.loadClass(name, resolve);
} else {
return c;
}
}
protected void scan() {
scanByteCode(libClsCache.keySet(), bytecodeLookup);
}
/**
* This method implement a event listener based scan process:
* <ol>
* <li>First loop: through all cached bytecode. Chain all scanner's bytecode visitor</li>
* <li>Rest loops: through dependencies. Thus if some bytecode missed by a certain scanner
* due to the context is not established can be captured eventually</li>
* </ol>
*/
protected void scanByteCode(Iterable<String> classes, $.Function<String, byte[]> bytecodeProvider) {
logger.debug("start to scan bytecode ...");
final AppCodeScannerManager scannerManager = app().scannerManager();
Map<String, List<AppByteCodeScanner>> dependencies = C.newMap();
for (String className : classes) {
logger.debug("scanning %s ...", className);
dependencies.remove(className);
byte[] ba = bytecodeProvider.apply(className);
if (null == ba) {
logger.warn("Cannot find any bytecode for class: %s. You might have an empty Java source file for that.", className);
continue;
}
libClsCache.put(className, ba);
act.metric.Timer timer = metric.startTimer("act:classload:scan:bytecode:" + className);
List<ByteCodeVisitor> visitors = C.newList();
List<AppByteCodeScanner> scanners = C.newList();
for (AppByteCodeScanner scanner : scannerManager.byteCodeScanners()) {
if (scanner.start(className)) {
//LOGGER.trace("scanner %s added to the list", scanner.getClass().getName());
visitors.add(scanner.byteCodeVisitor());
scanners.add(scanner);
}
}
if (visitors.isEmpty()) {
continue;
}
ByteCodeVisitor theVisitor = ByteCodeVisitor.chain(visitors);
EnvMatcher matcher = new EnvMatcher();
matcher.setDownstream(theVisitor);
ClassReader cr = new ClassReader(ba);
try {
cr.accept(matcher, 0);
} catch (EnvNotMatchException e) {
continue;
} catch (AsmException e) {
Throwable t = e.getCause();
if (t instanceof ClassNotFoundException) {
continue;
} else {
logger.error(e, "Error scanning bytecode at %s", e.context());
ActErrorResult error = ActErrorResult.scanningError(e);
if (Act.isDev()) {
app.setBlockIssue(error);
} else {
throw error;
}
}
}
for (AppByteCodeScanner scanner : scanners) {
scanner.scanFinished(className);
Map<Class<? extends AppByteCodeScanner>, Set<String>> ss = scanner.dependencyClasses();
if (ss.isEmpty()) {
//LOGGER.trace("no dependencies found for %s by scanner %s", className, scanner);
continue;
}
for (Class<? extends AppByteCodeScanner> scannerClass : ss.keySet()) {
AppByteCodeScanner scannerA = scannerManager.byteCodeScannerByClass(scannerClass);
for (String dependencyClass : ss.get(scannerClass)) {
logger.trace("dependencies[%s] found for %s by scanner %s", dependencyClass, className, scannerA);
List<AppByteCodeScanner> l = dependencies.get(dependencyClass);
if (null == l) {
l = C.newList();
dependencies.put(dependencyClass, l);
}
if (!l.contains(scanner)) l.add(scannerA);
}
}
}
timer.stop();
}
// loop through dependencies until it's all processed
while (!dependencies.isEmpty()) {
String className = dependencies.keySet().iterator().next();
act.metric.Timer timer = metric.startTimer("act:classload:scan:bytecode:" + className);
List<AppByteCodeScanner> scanners = dependencies.remove(className);
List<ByteCodeVisitor> visitors = C.newList();
for (AppByteCodeScanner scanner : scanners) {
scanner.start(className);
visitors.add(scanner.byteCodeVisitor());
}
ByteCodeVisitor theVisitor = ByteCodeVisitor.chain(visitors);
byte[] bytes = bytecodeProvider.apply(className);
libClsCache.put(className, bytes);
ClassReader cr = new ClassReader(bytes);
try {
cr.accept(theVisitor, 0);
} catch (AsmException e) {
throw ActErrorResult.of(e);
}
for (AppByteCodeScanner scanner : scanners) {
scanner.scanFinished(className);
Map<Class<? extends AppByteCodeScanner>, Set<String>> ss = scanner.dependencyClasses();
if (ss.isEmpty()) {
logger.trace("no dependencies found for %s by scanner %s", className, scanner);
continue;
}
for (Class<? extends AppByteCodeScanner> scannerClass : ss.keySet()) {
AppByteCodeScanner scannerA = scannerManager.byteCodeScannerByClass(scannerClass);
for (String dependencyClass : ss.get(scannerClass)) {
logger.trace("dependencies[%s] found for %s by scanner %s", dependencyClass, className, scannerA);
List<AppByteCodeScanner> l = dependencies.get(dependencyClass);
if (null == l) {
l = C.newList();
dependencies.put(dependencyClass, l);
}
if (!l.contains(scanner)) l.add(scannerA);
}
}
}
timer.stop();
}
}
protected void preload() {
preloadLib();
preloadClasses();
}
private void preloadLib() {
final Map<String, byte[]> bytecodeIdx = C.newMap();
final Map<String, Properties> jarConf = C.newMap();
final $.Function<String, Boolean> ignoredClassNames = app().config().appClassTester().negate();
Jars.F.JarEntryVisitor classNameIndexBuilder = Jars.F.classNameIndexBuilder(bytecodeIdx, ignoredClassNames);
Jars.F.JarEntryVisitor confIndexBuilder = Jars.F.appConfigFileIndexBuilder(jarConf);
List<File> jars = FullStackAppBootstrapClassLoader.jars(AppClassLoader.class.getClassLoader());
for (File jar : jars) {
Jars.scan(jar, classNameIndexBuilder, confIndexBuilder);
}
libClsCache.putAll(bytecodeIdx);
AppConfig config = app().config();
config.loadJarProperties(jarConf);
}
void loadClasses() {
for (String key : libClsCache.keySet()) {
try {
Class<?> c = loadClass(key, true);
cache(c);
} catch (Exception e) {
logger.warn(e, "error loading class");
}
}
}
protected void preloadClasses() {
File base = RuntimeDirs.classes(app);
List<File> files = Files.filter(base, _F.SAFE_CLASS);
for (File file : files) {
preloadClassFile(base, file);
}
}
protected void preloadClassFile(File base, File file) {
ByteArrayOutputStream baos = new ByteArrayOutputStream((int) file.length());
IO.copy(IO.is(file), baos);
byte[] bytes = baos.toByteArray();
libClsCache.put(ClassNames.sourceFileNameToClassName(base, file.getAbsolutePath().replace(".class", ".java")), bytes);
}
protected byte[] loadAppClassFromDisk(String name) {
File base = new File(app().layout().target(app().base()), "classes");
if (base.canRead() && base.isDirectory()) {
String path = ClassNames.classNameToClassFileName(name);
File classFile = new File(base, path);
if (classFile.canRead()) {
return IO.readContent(classFile);
}
}
return null;
}
public Class<?> defineClass(String name, byte[] b, int off, int len, boolean resolve) {
Class<?> c = super.defineClass(name, b, off, len, DOMAIN);
if (resolve) {
super.resolveClass(c);
}
return c;
}
private Class<?> loadAppClass(String name, boolean resolve) throws ClassNotFoundException {
byte[] bytecode = appBytecode(name);
// we need to skip getting byte code
if (null == bytecode) {
if (!(name.contains("$") && name.endsWith("MethodAccess") && !name.endsWith("$MethodAccess"))) {
// We need to skip getting byte code for reflectasm generated class for inner class method access
bytecode = loadAppClassFromDisk(name);
if (null == bytecode) return null;
} else {
return null;
}
}
if (!app().config().needEnhancement(name)) {
Class<?> c;
if (name.contains("$")) {
return super.loadClass(name, resolve);
} else {
c = super.defineClass(name, bytecode, 0, bytecode.length, DOMAIN);
if (resolve) {
super.resolveClass(c);
}
return c;
}
}
try {
byte[] baNew = enhance(name, bytecode);
try {
Class<?> c = super.defineClass(name, baNew, 0, baNew.length, DOMAIN);
if (resolve) {
super.resolveClass(c);
}
return c;
} catch (VerifyError e) {
File f = File.createTempFile(name, ".class");
IO.write(baNew, f);
throw e;
}
} catch (RuntimeException e) {
throw e;
} catch (Error e) {
throw e;
} catch (Exception e) {
throw E.unexpected("Error processing class " + name);
}
}
protected byte[] enhance(String className, byte[] bytecode) {
return asmEnhance(className, bytecode);
}
private byte[] asmEnhance(String className, byte[] bytecode) {
if (!enhanceEligible(className)) return bytecode;
$.Var<ClassWriter> cw = $.var(null);
ByteCodeVisitor enhancer = Act.enhancerManager().appEnhancer(app, className, cw);
if (null == enhancer) {
return bytecode;
}
EnvMatcher matcher = new EnvMatcher();
matcher.setDownstream(enhancer);
cw.set(new ClassWriter(ClassWriter.COMPUTE_FRAMES));
enhancer.commitDownstream();
ClassReader r = new ClassReader(bytecode);
try {
r.accept(matcher, 0);
} catch (EnvNotMatchException e) {
return bytecode;
} catch (AsmException e) {
logger.error(e, "error enhancing bytecode at %s", e.context());
throw ActErrorResult.enhancingError(e);
}
return cw.get().toByteArray();
}
protected byte[] appBytecode(String name) {
return appBytecode(name, true);
}
protected byte[] appBytecode(String name, boolean loadFromSource) {
return libClsCache.get(name);
}
protected byte[] bytecode(String name) {
return bytecode(name, true);
}
protected byte[] bytecode(String name, boolean compileSource) {
byte[] bytes = appBytecode(name, compileSource);
if (null != bytes) {
return bytes;
}
name = name.replace('.', '/') + ".class";
InputStream is = getParent().getResourceAsStream(name);
if (null == is) {
return null;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
IO.copy(is, baos);
return baos.toByteArray();
}
byte[] enhancedBytecode(String name) {
byte[] bytecode = bytecode(name, false);
return null == bytecode ? null : enhance(name, bytecode);
}
private synchronized ClassNode cache(Class<?> c) {
String cname = canonicalName(c);
if (null == cname) {
return null;
}
ClassInfoRepository repo = classInfoRepository();
if (repo.has(cname)) {
return repo.node(cname);
}
String name = c.getName();
ClassNode node = repo.node(name, cname);
node.modifiers(c.getModifiers());
Class[] ca = c.getInterfaces();
for (Class pc : ca) {
if (pc == Object.class) continue;
String pcname = canonicalName(pc);
if (null != pcname) {
cache(pc);
node.addInterface(pcname);
}
}
Class pc = c.getSuperclass();
if (null != pc && Object.class != pc) {
String pcname = canonicalName(pc);
if (null != pcname) {
cache(pc);
node.parent(pcname);
}
}
return node;
}
private $.F1<String, byte[]> bytecodeLookup = new $.F1<String, byte[]>() {
@Override
public byte[] apply(String s) throws NotAppliedException, $.Break {
return appBytecode(s);
}
};
private static java.security.ProtectionDomain DOMAIN;
static {
DOMAIN = (java.security.ProtectionDomain)
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction() {
public Object run() {
return AppClassLoader.class.getProtectionDomain();
}
});
}
private enum _F {
;
static $.Predicate<String> SYS_CLASS_NAME = new $.Predicate<String>() {
@Override
public boolean test(String s) {
return s.startsWith("java") || s.startsWith("org.osgl.");
}
};
static $.Predicate<String> SAFE_CLASS = S.F.endsWith(".class").and(SYS_CLASS_NAME.negate());
}
protected static boolean enhanceEligible(String name) {
boolean sys = name.startsWith("java") || name.startsWith("com.google") || name.startsWith("org.apache") || name.startsWith("org.springframework");
return !sys;
}
}