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