001package io.ebean.enhance; 002 003import io.ebean.enhance.asm.ClassReader; 004import io.ebean.enhance.asm.ClassWriter; 005import io.ebean.enhance.asm.Opcodes; 006import io.ebean.enhance.common.AgentManifest; 007import io.ebean.enhance.common.AlreadyEnhancedException; 008import io.ebean.enhance.common.ClassBytesReader; 009import io.ebean.enhance.common.ClassWriterWithoutClassLoading; 010import io.ebean.enhance.common.CommonSuperUnresolved; 011import io.ebean.enhance.common.DetectEnhancement; 012import io.ebean.enhance.common.EnhanceContext; 013import io.ebean.enhance.common.NoEnhancementRequiredException; 014import io.ebean.enhance.common.TransformRequest; 015import io.ebean.enhance.common.UrlPathHelper; 016import io.ebean.enhance.entity.ClassAdapterEntity; 017import io.ebean.enhance.entity.ClassPathClassBytesReader; 018import io.ebean.enhance.entity.MessageOutput; 019import io.ebean.enhance.querybean.TypeQueryClassAdapter; 020import io.ebean.enhance.transactional.ClassAdapterTransactional; 021import org.avaje.agentloader.AgentLoader; 022 023import java.io.IOException; 024import java.io.InputStream; 025import java.lang.instrument.ClassFileTransformer; 026import java.lang.instrument.IllegalClassFormatException; 027import java.lang.instrument.Instrumentation; 028import java.net.URL; 029import java.security.ProtectionDomain; 030import java.util.ArrayList; 031import java.util.List; 032import java.util.Properties; 033 034/** 035 * A Class file Transformer that performs Ebean enhancement of entity beans, 036 * transactional methods and query bean enhancement. 037 * <p> 038 * This is used as both a java agent or via Maven and Gradle plugins etc. 039 * </p> 040 */ 041public class Transformer implements ClassFileTransformer { 042 043 public static final int EBEAN_ASM_VERSION = Opcodes.ASM9; 044 private static String version; 045 046 /** 047 * Return the version of the ebean-agent or "unknown" if the version can not be determined. 048 */ 049 public static synchronized String getVersion() { 050 if (version == null) { 051 try (InputStream in = Transformer.class.getResourceAsStream("/META-INF/maven/io.ebean/ebean-agent/pom.properties")) { 052 if (in != null) { 053 Properties prop = new Properties(); 054 prop.load(in); 055 version = prop.getProperty("version"); 056 } 057 } catch (IOException e) { 058 System.err.println("Could not determine ebean-agent version: " + e.getMessage()); 059 } 060 if (version == null) { 061 version = "unknown"; 062 } 063 } 064 return version; 065 } 066 067 public static void agentmain(String agentArgs, Instrumentation inst) { 068 premain(agentArgs, inst); 069 } 070 071 public static void premain(String agentArgs, Instrumentation inst) { 072 instrumentation = inst; 073 transformer = new Transformer(null, agentArgs); 074 inst.addTransformer(transformer); 075 } 076 077 private static Instrumentation instrumentation; 078 private static Transformer transformer; 079 080 private final EnhanceContext enhanceContext; 081 private final List<CommonSuperUnresolved> unresolved = new ArrayList<>(); 082 private boolean keepUnresolved; 083 084 public Transformer(ClassLoader classLoader, String agentArgs) { 085 if (classLoader == null) { 086 classLoader = getClass().getClassLoader(); 087 } 088 ClassBytesReader reader = new ClassPathClassBytesReader(null); 089 AgentManifest manifest = new AgentManifest(classLoader); 090 this.enhanceContext = new EnhanceContext(reader, agentArgs, manifest); 091 } 092 093 /** 094 * Create with an EnhancementContext (for IDE Plugins mainly) 095 */ 096 public Transformer(EnhanceContext enhanceContext) { 097 this.enhanceContext = enhanceContext; 098 } 099 100 /** 101 * Create a transformer for entity bean enhancement and transactional method enhancement. 102 * 103 * @param bytesReader reads resources from class path for related inheritance and interfaces 104 * @param agentArgs command line arguments for debug level etc 105 */ 106 public Transformer(ClassBytesReader bytesReader, String agentArgs, AgentManifest manifest) { 107 this.enhanceContext = new EnhanceContext(bytesReader, agentArgs, manifest); 108 } 109 110 /** 111 * Return the Instrumentation instance. 112 */ 113 public static Instrumentation instrumentation() { 114 verifyInitialization(); 115 return instrumentation; 116 } 117 118 /** 119 * Return the Transformer instance. 120 */ 121 public static Transformer get() { 122 verifyInitialization(); 123 return transformer; 124 } 125 126 /** 127 * Use agent loader if necessary to initialise the transformer. 128 */ 129 public static void verifyInitialization() { 130 if (instrumentation == null) { 131 if (!AgentLoader.loadAgentFromClasspath("ebean-agent", "debug=0")) { 132 throw new IllegalStateException("ebean-agent not found in classpath - not dynamically loaded"); 133 } 134 } 135 } 136 137 /** 138 * Set this to keep and report unresolved explicitly. 139 */ 140 public void setKeepUnresolved() { 141 this.keepUnresolved = true; 142 } 143 144 /** 145 * Change the logout to something other than system out. 146 */ 147 public void setLogout(MessageOutput logout) { 148 this.enhanceContext.setLogout(logout); 149 } 150 151 public void log(int level, String msg) { 152 log(level, null, msg); 153 } 154 155 private void log(int level, String className, String msg) { 156 enhanceContext.log(level, className, msg); 157 } 158 159 public int getLogLevel() { 160 return enhanceContext.logLevel(); 161 } 162 163 public EnhanceContext getEnhanceContext() { 164 return enhanceContext; 165 } 166 167 @Override 168 public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { 169 try { 170 enhanceContext.withClassLoader(loader); 171 // ignore JDK and JDBC classes etc 172 if (enhanceContext.isIgnoreClass(className) || isQueryBeanCompanion(className, loader)) { 173 log(9, className, "ignore class"); 174 return null; 175 } 176 TransformRequest request = new TransformRequest(className, classfileBuffer); 177 if (enhanceContext.detectEntityTransactionalEnhancement(className)) { 178 enhanceEntityAndTransactional(loader, request); 179 } 180 if (enhanceContext.detectQueryBeanEnhancement(className)) { 181 enhanceQueryBean(loader, request); 182 } 183 if (request.isEnhanced()) { 184 return request.getBytes(); 185 } 186 187 log(9, className, "no enhancement on class"); 188 return null; 189 190 } catch (NoEnhancementRequiredException e) { 191 // the class is an interface 192 log(8, className, "No Enhancement required " + e.getMessage()); 193 return null; 194 } catch (IllegalArgumentException | IllegalStateException e) { 195 log(2, className, "No enhancement on class due to " + e); 196 return null; 197 } catch (Exception e) { 198 if (enhanceContext.isThrowOnError()) { 199 throw new IllegalStateException(e); 200 } 201 enhanceContext.log(className, "Error during transform " + e); 202 enhanceContext.log(e); 203 return null; 204 } finally { 205 logUnresolvedCommonSuper(className); 206 } 207 } 208 209 private boolean isQueryBeanCompanion(String className, ClassLoader classLoader) { 210 return className.endsWith("$Companion") && enhanceContext.isQueryBean(className, classLoader); 211 } 212 213 /** 214 * Perform entity and transactional enhancement. 215 */ 216 private void enhanceEntityAndTransactional(ClassLoader loader, TransformRequest request) { 217 try { 218 DetectEnhancement detect = detect(loader, request.getBytes()); 219 if (detect.isEntity()) { 220 if (detect.isEnhancedEntity()) { 221 detect.log(3, "already enhanced entity"); 222 } else { 223 entityEnhancement(loader, request); 224 } 225 } 226 if (enhanceContext.isEnableProfileLocation() || detect.isTransactional()) { 227 if (detect.isEnhancedTransactional()) { 228 detect.log(3, "already enhanced transactional"); 229 } else { 230 transactionalEnhancement(loader, request); 231 } 232 } 233 } catch (NoEnhancementRequiredException e) { 234 log(8, request.getClassName(), "No entity or transactional enhancement required " + e.getMessage()); 235 } 236 } 237 238 /** 239 * Log and common superclass classpath issues that defaulted to Object. 240 */ 241 private void logUnresolvedCommonSuper(String className) { 242 if (!keepUnresolved && !unresolved.isEmpty()) { 243 for (CommonSuperUnresolved commonUnresolved : unresolved) { 244 log(0, className, commonUnresolved.getMessage()); 245 } 246 unresolved.clear(); 247 } 248 } 249 250 /** 251 * Return the list of unresolved common superclass issues. This should be cleared 252 * after each use and can only be used with {@link #setKeepUnresolved()}. 253 */ 254 public List<CommonSuperUnresolved> getUnresolved() { 255 return unresolved; 256 } 257 258 /** 259 * Perform entity bean enhancement. 260 */ 261 private void entityEnhancement(ClassLoader loader, TransformRequest request) { 262 ClassReader cr = new ClassReader(request.getBytes()); 263 ClassWriterWithoutClassLoading cw = new ClassWriterWithoutClassLoading(ClassWriter.COMPUTE_FRAMES, loader); 264 ClassAdapterEntity ca = new ClassAdapterEntity(cw, loader, enhanceContext); 265 try { 266 cr.accept(ca, ClassReader.EXPAND_FRAMES); 267 if (ca.isLog(2)) { 268 ca.logEnhanced(); 269 unresolved.addAll(cw.getUnresolved()); 270 } 271 272 request.enhancedEntity(cw.toByteArray()); 273 274 } catch (AlreadyEnhancedException e) { 275 if (ca.isLog(3)) { 276 ca.log("already enhanced entity"); 277 } 278 request.enhancedEntity(null); 279 } catch (NoEnhancementRequiredException e) { 280 if (ca.isLog(4)) { 281 ca.log("skipped entity enhancement"); 282 } 283 } 284 } 285 286 /** 287 * Perform transactional enhancement and Finder profileLocation enhancement. 288 */ 289 private void transactionalEnhancement(ClassLoader loader, TransformRequest request) { 290 ClassReader cr = new ClassReader(request.getBytes()); 291 ClassWriterWithoutClassLoading cw = new ClassWriterWithoutClassLoading(ClassWriter.COMPUTE_FRAMES, loader); 292 ClassAdapterTransactional ca = new ClassAdapterTransactional(cw, loader, enhanceContext); 293 try { 294 cr.accept(ca, ClassReader.EXPAND_FRAMES); 295 if (ca.isLog(2)) { 296 ca.logEnhanced(); 297 } 298 299 request.enhancedTransactional(cw.toByteArray()); 300 301 } catch (AlreadyEnhancedException e) { 302 if (ca.isLog(3)) { 303 ca.log("already transactional enhanced"); 304 } 305 } catch (NoEnhancementRequiredException e) { 306 if (ca.isLog(4)) { 307 ca.log("skipped transactional enhancement"); 308 } 309 } finally { 310 unresolved.addAll(cw.getUnresolved()); 311 } 312 } 313 314 /** 315 * Perform enhancement. 316 */ 317 private void enhanceQueryBean(ClassLoader loader, TransformRequest request) { 318 ClassReader cr = new ClassReader(request.getBytes()); 319 ClassWriterWithoutClassLoading cw = new ClassWriterWithoutClassLoading(ClassWriter.COMPUTE_FRAMES, loader); 320 TypeQueryClassAdapter ca = new TypeQueryClassAdapter(cw, enhanceContext, loader); 321 try { 322 cr.accept(ca, ClassReader.EXPAND_FRAMES); 323 request.enhancedQueryBean(cw.toByteArray()); 324 } catch (AlreadyEnhancedException e) { 325 if (ca.isLog(3)) { 326 ca.log("already query bean enhanced"); 327 } 328 } catch (NoEnhancementRequiredException e) { 329 if (ca.isLog(4)) { 330 ca.log("skipped query bean enhancement"); 331 } 332 } finally { 333 unresolved.addAll(cw.getUnresolved()); 334 } 335 } 336 337 /** 338 * Helper method to split semi-colon separated class paths into a URL array. 339 */ 340 public static URL[] parseClassPaths(String extraClassPath) { 341 if (extraClassPath == null) { 342 return new URL[0]; 343 } 344 return UrlPathHelper.convertToUrl(extraClassPath.split(";")); 345 } 346 347 /** 348 * Read the bytes quickly trying to detect if it needs entity or transactional 349 * enhancement. 350 */ 351 private DetectEnhancement detect(ClassLoader classLoader, byte[] classfileBuffer) { 352 DetectEnhancement detect = new DetectEnhancement(classLoader, enhanceContext); 353 ClassReader cr = new ClassReader(classfileBuffer); 354 cr.accept(detect, ClassReader.SKIP_CODE + ClassReader.SKIP_DEBUG + ClassReader.SKIP_FRAMES); 355 return detect; 356 } 357}