001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.weaver.normalizer; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.io.OutputStream; 022import java.lang.annotation.ElementType; 023import java.lang.annotation.Target; 024import java.nio.charset.Charset; 025import java.security.MessageDigest; 026import java.security.NoSuchAlgorithmException; 027import java.text.MessageFormat; 028import java.util.HashMap; 029import java.util.LinkedHashMap; 030import java.util.LinkedHashSet; 031import java.util.Map; 032import java.util.Set; 033 034import javax.activation.DataSource; 035 036import org.apache.commons.io.IOUtils; 037import org.apache.commons.lang3.ArrayUtils; 038import org.apache.commons.lang3.CharEncoding; 039import org.apache.commons.lang3.Conversion; 040import org.apache.commons.lang3.Validate; 041import org.apache.commons.lang3.mutable.MutableBoolean; 042import org.apache.commons.lang3.tuple.MutablePair; 043import org.apache.commons.lang3.tuple.Pair; 044import org.apache.commons.weaver.model.ScanRequest; 045import org.apache.commons.weaver.model.ScanResult; 046import org.apache.commons.weaver.model.Scanner; 047import org.apache.commons.weaver.model.WeavableClass; 048import org.apache.commons.weaver.model.WeaveEnvironment; 049import org.apache.commons.weaver.spi.Weaver; 050import org.objectweb.asm.AnnotationVisitor; 051import org.objectweb.asm.ClassReader; 052import org.objectweb.asm.ClassVisitor; 053import org.objectweb.asm.ClassWriter; 054import org.objectweb.asm.MethodVisitor; 055import org.objectweb.asm.Opcodes; 056import org.objectweb.asm.Type; 057import org.objectweb.asm.commons.GeneratorAdapter; 058import org.objectweb.asm.commons.Method; 059import org.objectweb.asm.commons.Remapper; 060import org.objectweb.asm.commons.RemappingClassAdapter; 061import org.objectweb.asm.commons.SimpleRemapper; 062 063/** 064 * Handles the work of "normalizing" anonymous class definitions. 065 */ 066public class Normalizer { 067 private static final String INIT = "<init>"; 068 069 private static final Type OBJECT_TYPE = Type.getType(Object.class); 070 071 /** 072 * Marker annotation. 073 */ 074 @Target(ElementType.TYPE) 075 private @interface Marker { 076 } 077 078 private static class ClassWrapper { 079 final Class<?> wrapped; 080 final boolean mustRewriteConstructor; 081 082 ClassWrapper(final Class<?> wrapped, final boolean mustRewriteConstructor) { 083 this.wrapped = wrapped; 084 this.mustRewriteConstructor = mustRewriteConstructor; 085 } 086 } 087 088 private class WriteClass extends ClassVisitor { 089 private String className; 090 091 WriteClass() { 092 super(Opcodes.ASM4, new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS)); 093 } 094 095 WriteClass(final ClassReader reader) { 096 super(Opcodes.ASM4, new ClassWriter(reader, 0)); 097 } 098 099 @Override 100 public void visit(final int version, final int access, final String name, final String signature, 101 final String superName, final String[] intrfces) { 102 super.visit(version, access, name, signature, superName, intrfces); 103 className = name; 104 } 105 106 @Override 107 public void visitEnd() { 108 super.visitEnd(); 109 final byte[] bytecode = ((ClassWriter) cv).toByteArray(); 110 111 final DataSource classfile = env.getClassfile(className); 112 env.debug("Writing class %s to %s", className, classfile.getName()); 113 OutputStream outputStream = null; 114 try { 115 outputStream = classfile.getOutputStream(); 116 IOUtils.write(bytecode, outputStream); 117 } catch (final IOException e) { 118 throw new RuntimeException(e); 119 } finally { 120 IOUtils.closeQuietly(outputStream); 121 } 122 } 123 } 124 125 private enum IneligibilityReason { 126 NOT_ANONYMOUS, TOO_MANY_CONSTRUCTORS, IMPLEMENTS_METHODS, TOO_BUSY_CONSTRUCTOR; 127 } 128 129 /** 130 * Configuration prefix for this {@link Weaver}. 131 */ 132 public static final String CONFIG_WEAVER = "normalizer."; 133 134 /** 135 * Property name referencing a comma-delimited list of types whose subclasses/implementations should be normalized, 136 * e.g. {@code javax.enterprise.util.TypeLiteral}. 137 */ 138 public static final String CONFIG_SUPER_TYPES = CONFIG_WEAVER + "superTypes"; 139 140 /** 141 * Property name referencing a package name to which merged types should be added. 142 */ 143 public static final String CONFIG_TARGET_PACKAGE = CONFIG_WEAVER + "targetPackage"; 144 145 private static final Charset UTF8 = Charset.forName(CharEncoding.UTF_8); 146 147 private final WeaveEnvironment env; 148 149 private final Set<Class<?>> normalizeTypes; 150 private final String targetPackage; 151 152 /** 153 * Create a new {@link Normalizer} instance. 154 * @param env {@link WeaveEnvironment} 155 */ 156 public Normalizer(final WeaveEnvironment env) { 157 this.env = env; 158 159 this.targetPackage = 160 Utils.validatePackageName(Validate.notBlank(env.config.getProperty(CONFIG_TARGET_PACKAGE), 161 "missing target package name")); 162 this.normalizeTypes = 163 Utils.parseTypes( 164 Validate.notEmpty(env.config.getProperty(CONFIG_SUPER_TYPES), "no types specified for normalization"), 165 env.classLoader); 166 } 167 168 /** 169 * Normalize the classes found using the specified {@link Scanner}. 170 * @param scanner to scan with 171 * @return whether any work was done 172 */ 173 public boolean normalize(final Scanner scanner) { 174 boolean result = false; 175 for (final Class<?> supertype : normalizeTypes) { 176 final Set<Class<?>> subtypes = getBroadlyEligibleSubclasses(supertype, scanner); 177 try { 178 final Map<Pair<String, String>, Set<ClassWrapper>> segregatedSubtypes = segregate(subtypes); 179 for (final Map.Entry<Pair<String, String>, Set<ClassWrapper>> entry : segregatedSubtypes.entrySet()) { 180 final Set<ClassWrapper> likeTypes = entry.getValue(); 181 if (likeTypes.size() > 1) { 182 result = true; 183 rewrite(entry.getKey(), likeTypes); 184 } 185 } 186 } catch (final RuntimeException e) { 187 throw e; 188 } catch (final Exception e) { 189 throw new RuntimeException(e); 190 } 191 } 192 return result; 193 } 194 195 /** 196 * Map a set of classes by their enclosing class. 197 * @param sort values 198 * @return {@link Map} of enclosing classname to {@link Map} of internal name to {@link ClassWrapper} 199 */ 200 private Map<String, Map<String, ClassWrapper>> byEnclosingClass(final Set<ClassWrapper> sort) { 201 final Map<String, Map<String, ClassWrapper>> result = new HashMap<String, Map<String, ClassWrapper>>(); 202 for (final ClassWrapper wrapper : sort) { 203 final String outer = wrapper.wrapped.getEnclosingClass().getName(); 204 Map<String, ClassWrapper> map = result.get(outer); 205 if (map == null) { 206 map = new LinkedHashMap<String, Normalizer.ClassWrapper>(); 207 result.put(outer, map); 208 } 209 map.put(wrapper.wrapped.getName().replace('.', '/'), wrapper); 210 } 211 return result; 212 } 213 214 /** 215 * Rewrite classes as indicated by one entry of {@link #segregate(Iterable)}. 216 * @param key {@link String} {@link Pair} indicating supertype and constructor signature 217 * @param toMerge matching classes 218 * @throws IOException on I/O error 219 * @throws ClassNotFoundException if class not found 220 */ 221 private void rewrite(final Pair<String, String> key, final Set<ClassWrapper> toMerge) throws IOException, 222 ClassNotFoundException { 223 final String target = copy(key, toMerge.iterator().next()); 224 env.info("Merging %s identical %s implementations with constructor %s to type %s", toMerge.size(), 225 key.getLeft(), key.getRight(), target); 226 227 final Map<String, Map<String, ClassWrapper>> byEnclosingClass = byEnclosingClass(toMerge); 228 for (final Map.Entry<String, Map<String, ClassWrapper>> entry : byEnclosingClass.entrySet()) { 229 final String outer = entry.getKey(); 230 env.debug("Normalizing %s inner classes of %s", entry.getValue().size(), outer); 231 final Map<String, String> classMap = new HashMap<String, String>(); 232 for (final String merged : entry.getValue().keySet()) { 233 classMap.put(merged, target); 234 } 235 final Remapper remapper = new SimpleRemapper(classMap); 236 237 InputStream enclosingBytecode = null; 238 try { 239 enclosingBytecode = env.getClassfile(outer).getInputStream(); 240 final ClassReader reader = new ClassReader(enclosingBytecode); 241 242 final ClassVisitor cv = // NOPMD 243 new RemappingClassAdapter(new WriteClass(reader), remapper) { 244 245 @Override 246 public void visitInnerClass(final String name, final String outerName, final String innerName, 247 final int access) { 248 if (!classMap.containsKey(name)) { 249 super.visitInnerClass(name, outerName, innerName, access); 250 } 251 } 252 253 @Override 254 public MethodVisitor visitMethod(final int access, final String name, final String desc, 255 final String signature, final String[] exceptions) { 256 final MethodVisitor mv = // NOPMD 257 super.visitMethod(access, name, desc, signature, exceptions); 258 return new MethodVisitor(Opcodes.ASM4, mv) { 259 @Override 260 public void visitMethodInsn(final int opcode, final String owner, final String name, 261 final String desc) { 262 String useDescriptor = desc; 263 if (INIT.equals(name)) { 264 final ClassWrapper wrapper = entry.getValue().get(owner); 265 if (wrapper != null && wrapper.mustRewriteConstructor) { 266 // simply replace first argument type with OBJECT_TYPE: 267 final Type[] args = Type.getArgumentTypes(desc); 268 args[0] = OBJECT_TYPE; 269 useDescriptor = new Method(INIT, Type.VOID_TYPE, args).getDescriptor(); 270 } 271 } 272 super.visitMethodInsn(opcode, owner, name, useDescriptor); 273 } 274 }; 275 } 276 }; 277 278 reader.accept(cv, 0); 279 } finally { 280 IOUtils.closeQuietly(enclosingBytecode); 281 } 282 for (final String merged : entry.getValue().keySet()) { 283 if (env.deleteClassfile(merged)) { 284 env.debug("Deleted class %s", merged); 285 } else { 286 env.warn("Unable to delete class %s", merged); 287 } 288 } 289 } 290 291 } 292 293 /** 294 * <p>Find subclasses/implementors of {code supertype} that: 295 * <ul> 296 * <li>are anonymous</li> 297 * <li>declare a single constructor (probably redundant in the case of an anonymous class)</li> 298 * <li>do not implement any methods</li> 299 * </ul> 300 * </p><p> 301 * Considered "broadly" eligible because the instructions in the implemented constructor may remove the class from 302 * consideration later on. 303 * </p> 304 * @param supertype whose subtypes are sought 305 * @param scanner to use 306 * @return {@link Set} of {@link Class} 307 * @see #segregate(Iterable) 308 */ 309 private Set<Class<?>> getBroadlyEligibleSubclasses(final Class<?> supertype, final Scanner scanner) { 310 final ScanResult scanResult = scanner.scan(new ScanRequest().addSupertypes(supertype)); 311 final Set<Class<?>> result = new LinkedHashSet<Class<?>>(); 312 for (final WeavableClass<?> cls : scanResult.getClasses()) { 313 final Class<?> subtype = cls.getTarget(); 314 final IneligibilityReason reason; 315 if (!subtype.isAnonymousClass()) { 316 reason = IneligibilityReason.NOT_ANONYMOUS; 317 } else if (subtype.getDeclaredConstructors().length != 1) { 318 reason = IneligibilityReason.TOO_MANY_CONSTRUCTORS; 319 } else if (subtype.getDeclaredMethods().length > 0) { 320 reason = IneligibilityReason.IMPLEMENTS_METHODS; 321 } else { 322 result.add(subtype); 323 continue; 324 } 325 env.debug("Removed %s from consideration due to %s", subtype, reason); 326 } 327 return result; 328 } 329 330 /** 331 * <p>Segregate a number of classes (presumed subclasses/implementors of a 332 * common supertype/interface). The keys of the map consist of the important 333 * parts for identifying similar anonymous types: the "signature" and the 334 * invoked superclass constructor. For our purposes, the signature consists 335 * of the first applicable item of: 336 * <ol> 337 * <li>The generic signature of the class</li> 338 * <li>The sole implemented interface</li> 339 * <li>The superclass</li> 340 * </ol> 341 * </p><p> 342 * The class will be considered ineligible if its constructor is too "busy" as its side effects cannot be 343 * anticipated; the normalizer will err on the side of caution. 344 * </p><p> 345 * Further, we will here avail ourselves of the opportunity to discard any types we have already normalized. 346 * </p> 347 * @param subtypes 348 * @return Map of Pair<String, String> to Set of Classes 349 * @throws IOException 350 */ 351 private Map<Pair<String, String>, Set<ClassWrapper>> segregate(final Iterable<Class<?>> subtypes) 352 throws IOException { 353 final Map<Pair<String, String>, Set<ClassWrapper>> classMap = 354 new LinkedHashMap<Pair<String, String>, Set<ClassWrapper>>(); 355 for (final Class<?> subtype : subtypes) { 356 final MutablePair<String, String> key = new MutablePair<String, String>(); 357 final MutableBoolean ignore = new MutableBoolean(false); 358 final MutableBoolean valid = new MutableBoolean(true); 359 final MutableBoolean mustRewriteConstructor = new MutableBoolean(); 360 InputStream bytecode = null; 361 362 try { 363 bytecode = env.getClassfile(subtype).getInputStream(); 364 new ClassReader(bytecode).accept(new ClassVisitor(Opcodes.ASM4) { 365 String superName; 366 367 @Override 368 public void visit(final int version, final int access, final String name, final String signature, 369 final String superName, final String[] interfaces) { 370 super.visit(version, access, name, signature, superName, interfaces); 371 this.superName = superName; 372 final String left; 373 if (signature != null) { 374 left = signature; 375 } else if (ArrayUtils.getLength(interfaces) == 1) { 376 left = interfaces[0]; 377 } else { 378 left = superName; 379 } 380 key.setLeft(left); 381 } 382 383 @Override 384 public AnnotationVisitor visitAnnotation(final String desc, final boolean visible) { 385 if (Type.getType(Marker.class).getDescriptor().equals(desc)) { 386 ignore.setValue(true); 387 } 388 return null; 389 } 390 391 @Override 392 public MethodVisitor visitMethod(final int access, final String name, final String desc, 393 final String signature, final String[] exceptions) { 394 if (INIT.equals(name)) { 395 return new MethodVisitor(Opcodes.ASM4) { 396 @Override 397 public void visitMethodInsn(final int opcode, final String owner, final String name, 398 final String desc) { 399 if (INIT.equals(name) && owner.equals(superName)) { 400 key.setRight(desc); 401 } else { 402 valid.setValue(false); 403 } 404 } 405 406 @Override 407 public void visitFieldInsn(final int opcode, final String owner, final String name, 408 final String desc) { 409 if ("this$0".equals(name) && opcode == Opcodes.PUTFIELD) { 410 mustRewriteConstructor.setValue(true); 411 return; 412 } 413 valid.setValue(false); 414 } 415 }; 416 } 417 return null; 418 } 419 }, 0); 420 } finally { 421 IOUtils.closeQuietly(bytecode); 422 } 423 if (ignore.booleanValue()) { 424 continue; 425 } 426 if (valid.booleanValue()) { 427 Set<ClassWrapper> set = classMap.get(key); 428 if (set == null) { 429 set = new LinkedHashSet<ClassWrapper>(); 430 classMap.put(key, set); 431 } 432 set.add(new ClassWrapper(subtype, mustRewriteConstructor.booleanValue())); 433 } else { 434 env.debug("%s is ineligible for normalization due to %s", subtype, 435 IneligibilityReason.TOO_BUSY_CONSTRUCTOR); 436 } 437 } 438 return classMap; 439 } 440 441 /** 442 * Create the normalized version of a given class in the configured target package. The {@link Normalizer} will 443 * gladly do so in a package from which the normalized class will not actually be able to reference any types upon 444 * which it relies; in such a situation you must specify the target package as the package of the supertype. 445 * @param key used to generate the normalized classname. 446 * @param classWrapper 447 * @return the generated classname. 448 * @throws IOException 449 * @throws ClassNotFoundException 450 */ 451 private String copy(final Pair<String, String> key, final ClassWrapper classWrapper) throws IOException, 452 ClassNotFoundException { 453 final MessageDigest md5; 454 try { 455 md5 = MessageDigest.getInstance("MD5"); 456 } catch (final NoSuchAlgorithmException e) { 457 throw new RuntimeException(e); 458 } 459 md5.update(key.getLeft().getBytes(UTF8)); 460 md5.update(key.getRight().getBytes(UTF8)); 461 462 final long digest = Conversion.byteArrayToLong(md5.digest(), 0, 0L, 0, Long.SIZE / Byte.SIZE); 463 464 final String result = MessageFormat.format("{0}/$normalized{1,number,0;_0}", targetPackage, digest); 465 466 env.debug("Copying class %s to %s", classWrapper.wrapped.getName(), result); 467 468 InputStream bytecode = null; 469 470 try { 471 bytecode = env.getClassfile(classWrapper.wrapped).getInputStream(); 472 final ClassReader reader = new ClassReader(bytecode); 473 474 final ClassVisitor writeClass = new WriteClass(); 475 476 // we're doing most of this by hand; we only read the original class to hijack signature, ctor exceptions, 477 // etc.: 478 479 reader.accept(new ClassVisitor(Opcodes.ASM4) { 480 Type supertype; 481 482 @Override 483 public void visit(final int version, final int access, final String name, final String signature, 484 final String superName, final String[] interfaces) { 485 supertype = Type.getObjectType(superName); 486 writeClass.visit(version, Opcodes.ACC_PUBLIC, result, signature, superName, interfaces); 487 488 visitAnnotation(Type.getType(Marker.class).getDescriptor(), false); 489 } 490 491 @Override 492 public MethodVisitor visitMethod(final int access, final String name, final String desc, 493 final String signature, final String[] exceptions) { 494 if (INIT.equals(name)) { 495 496 final Method staticCtor = new Method(INIT, key.getRight()); 497 final Type[] argumentTypes = staticCtor.getArgumentTypes(); 498 final Type[] exceptionTypes = toObjectTypes(exceptions); 499 500 { 501 final GeneratorAdapter mgen = 502 new GeneratorAdapter(Opcodes.ACC_PUBLIC, staticCtor, signature, exceptionTypes, 503 writeClass); 504 mgen.visitCode(); 505 mgen.loadThis(); 506 for (int i = 0; i < argumentTypes.length; i++) { 507 mgen.loadArg(i); 508 } 509 mgen.invokeConstructor(supertype, staticCtor); 510 mgen.returnValue(); 511 mgen.endMethod(); 512 } 513 /* 514 * now declare a dummy constructor that will match, and discard, 515 * any originally inner-class bound constructor i.e. that set up a this$0 field. 516 * By doing this we can avoid playing with the stack that originally 517 * invoked such a constructor and simply rewrite the method 518 */ 519 { 520 final Method instanceCtor = 521 new Method(INIT, Type.VOID_TYPE, ArrayUtils.add(argumentTypes, 0, OBJECT_TYPE)); 522 final GeneratorAdapter mgen = 523 new GeneratorAdapter(Opcodes.ACC_PUBLIC, instanceCtor, signature, exceptionTypes, 524 writeClass); 525 mgen.visitCode(); 526 mgen.loadThis(); 527 for (int i = 0; i < argumentTypes.length; i++) { 528 mgen.loadArg(i + 1); 529 } 530 mgen.invokeConstructor(supertype, staticCtor); 531 mgen.returnValue(); 532 mgen.endMethod(); 533 } 534 return null; 535 } 536 return null; 537 } 538 539 @Override 540 public void visitEnd() { 541 writeClass.visitEnd(); 542 } 543 }, 0); 544 } finally { 545 IOUtils.closeQuietly(bytecode); 546 } 547 return result; 548 } 549 550 /** 551 * Translate internal names to Java type names. 552 * @param types to translate 553 * @return {@link Type}[] 554 * @see Type#getObjectType(String) 555 */ 556 private static Type[] toObjectTypes(final String[] types) { 557 if (types == null) { 558 return null; 559 } 560 final Type[] result = new Type[types.length]; 561 for (int i = 0; i < types.length; i++) { 562 result[i] = Type.getObjectType(types[i]); 563 } 564 return result; 565 } 566}