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 (EnhancementException e) { 195 enhanceContext.log(className, "Transform error " + e.getMessage()); 196 throw e; 197 } catch (IllegalArgumentException | IllegalStateException e) { 198 log(2, className, "No enhancement on class due to " + e); 199 return null; 200 } catch (Exception e) { 201 if (enhanceContext.isThrowOnError()) { 202 throw new IllegalStateException(e); 203 } 204 enhanceContext.log(className, "Error during transform " + e); 205 enhanceContext.log(e); 206 return null; 207 } finally { 208 logUnresolvedCommonSuper(className); 209 } 210 } 211 212 private boolean isQueryBeanCompanion(String className, ClassLoader classLoader) { 213 return className.endsWith("$Companion") && enhanceContext.isQueryBean(className, classLoader); 214 } 215 216 /** 217 * Perform entity and transactional enhancement. 218 */ 219 private void enhanceEntityAndTransactional(ClassLoader loader, TransformRequest request) { 220 try { 221 DetectEnhancement detect = detect(loader, request.getBytes()); 222 if (detect.isEntity()) { 223 if (detect.isEnhancedEntity()) { 224 detect.log(3, "already enhanced entity"); 225 } else { 226 entityEnhancement(loader, request); 227 } 228 } 229 if (enhanceContext.isEnableProfileLocation() || detect.isTransactional()) { 230 if (detect.isEnhancedTransactional()) { 231 detect.log(3, "already enhanced transactional"); 232 } else { 233 transactionalEnhancement(loader, request); 234 } 235 } 236 } catch (NoEnhancementRequiredException e) { 237 log(8, request.getClassName(), "No entity or transactional enhancement required " + e.getMessage()); 238 } 239 } 240 241 /** 242 * Log and common superclass classpath issues that defaulted to Object. 243 */ 244 private void logUnresolvedCommonSuper(String className) { 245 if (!keepUnresolved && !unresolved.isEmpty()) { 246 for (CommonSuperUnresolved commonUnresolved : unresolved) { 247 log(2, className, commonUnresolved.getMessage()); 248 } 249 unresolved.clear(); 250 } 251 } 252 253 /** 254 * Return the list of unresolved common superclass issues. This should be cleared 255 * after each use and can only be used with {@link #setKeepUnresolved()}. 256 */ 257 public List<CommonSuperUnresolved> getUnresolved() { 258 return unresolved; 259 } 260 261 /** 262 * Perform entity bean enhancement. 263 */ 264 private void entityEnhancement(ClassLoader loader, TransformRequest request) { 265 ClassReader cr = new ClassReader(request.getBytes()); 266 ClassWriterWithoutClassLoading cw = new ClassWriterWithoutClassLoading(ClassWriter.COMPUTE_FRAMES, loader); 267 ClassAdapterEntity ca = new ClassAdapterEntity(cw, loader, enhanceContext); 268 try { 269 cr.accept(ca, ClassReader.EXPAND_FRAMES); 270 if (ca.isLog(2)) { 271 ca.logEnhanced(); 272 unresolved.addAll(cw.getUnresolved()); 273 } 274 275 request.enhancedEntity(cw.toByteArray()); 276 277 } catch (AlreadyEnhancedException e) { 278 if (ca.isLog(3)) { 279 ca.log("already enhanced entity"); 280 } 281 request.enhancedEntity(null); 282 } catch (NoEnhancementRequiredException e) { 283 if (ca.isLog(4)) { 284 ca.log("skipped entity enhancement"); 285 } 286 } 287 } 288 289 /** 290 * Perform transactional enhancement and Finder profileLocation enhancement. 291 */ 292 private void transactionalEnhancement(ClassLoader loader, TransformRequest request) { 293 ClassReader cr = new ClassReader(request.getBytes()); 294 ClassWriterWithoutClassLoading cw = new ClassWriterWithoutClassLoading(ClassWriter.COMPUTE_FRAMES, loader); 295 ClassAdapterTransactional ca = new ClassAdapterTransactional(cw, loader, enhanceContext); 296 try { 297 cr.accept(ca, ClassReader.EXPAND_FRAMES); 298 if (ca.isLog(2)) { 299 ca.logEnhanced(); 300 } 301 302 request.enhancedTransactional(cw.toByteArray()); 303 304 } catch (AlreadyEnhancedException e) { 305 if (ca.isLog(3)) { 306 ca.log("already transactional enhanced"); 307 } 308 } catch (NoEnhancementRequiredException e) { 309 if (ca.isLog(4)) { 310 ca.log("skipped transactional enhancement"); 311 } 312 } finally { 313 unresolved.addAll(cw.getUnresolved()); 314 } 315 } 316 317 /** 318 * Perform enhancement. 319 */ 320 private void enhanceQueryBean(ClassLoader loader, TransformRequest request) { 321 ClassReader cr = new ClassReader(request.getBytes()); 322 ClassWriterWithoutClassLoading cw = new ClassWriterWithoutClassLoading(ClassWriter.COMPUTE_FRAMES, loader); 323 TypeQueryClassAdapter ca = new TypeQueryClassAdapter(cw, enhanceContext, loader); 324 try { 325 cr.accept(ca, ClassReader.EXPAND_FRAMES); 326 request.enhancedQueryBean(cw.toByteArray()); 327 } catch (AlreadyEnhancedException e) { 328 if (ca.isLog(3)) { 329 ca.log("already query bean enhanced"); 330 } 331 } catch (NoEnhancementRequiredException e) { 332 if (ca.isLog(4)) { 333 ca.log("skipped query bean enhancement"); 334 } 335 } finally { 336 unresolved.addAll(cw.getUnresolved()); 337 } 338 } 339 340 /** 341 * Helper method to split semi-colon separated class paths into a URL array. 342 */ 343 public static URL[] parseClassPaths(String extraClassPath) { 344 if (extraClassPath == null) { 345 return new URL[0]; 346 } 347 return UrlPathHelper.convertToUrl(extraClassPath.split(";")); 348 } 349 350 /** 351 * Read the bytes quickly trying to detect if it needs entity or transactional 352 * enhancement. 353 */ 354 private DetectEnhancement detect(ClassLoader classLoader, byte[] classfileBuffer) { 355 DetectEnhancement detect = new DetectEnhancement(classLoader, enhanceContext); 356 ClassReader cr = new ClassReader(classfileBuffer); 357 cr.accept(detect, ClassReader.SKIP_CODE + ClassReader.SKIP_DEBUG + ClassReader.SKIP_FRAMES); 358 return detect; 359 } 360}