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