| /* |
| * Copyright (c) 2016 Mockito contributors |
| * This program is made available under the terms of the MIT License. |
| */ |
| package org.mockito.internal.creation.bytebuddy; |
| |
| import net.bytebuddy.ByteBuddy; |
| import net.bytebuddy.description.method.MethodDescription; |
| import net.bytebuddy.description.modifier.SynchronizationState; |
| import net.bytebuddy.description.modifier.Visibility; |
| import net.bytebuddy.dynamic.DynamicType; |
| import net.bytebuddy.dynamic.loading.MultipleParentClassLoader; |
| import net.bytebuddy.dynamic.scaffold.TypeValidation; |
| import net.bytebuddy.implementation.FieldAccessor; |
| import net.bytebuddy.implementation.Implementation; |
| import net.bytebuddy.implementation.attribute.MethodAttributeAppender; |
| import net.bytebuddy.matcher.ElementMatcher; |
| import org.mockito.exceptions.base.MockitoException; |
| import org.mockito.internal.creation.bytebuddy.ByteBuddyCrossClassLoaderSerializationSupport.CrossClassLoaderSerializableMock; |
| import org.mockito.internal.creation.bytebuddy.MockMethodInterceptor.DispatcherDefaultingToRealMethod; |
| import org.mockito.mock.SerializableMode; |
| |
| import java.io.IOException; |
| import java.io.ObjectInputStream; |
| import java.lang.annotation.Annotation; |
| import java.lang.reflect.Modifier; |
| import java.lang.reflect.Type; |
| import java.util.ArrayList; |
| import java.util.Random; |
| |
| import static java.lang.Thread.currentThread; |
| import static net.bytebuddy.description.modifier.Visibility.PRIVATE; |
| import static net.bytebuddy.dynamic.Transformer.ForMethod.withModifiers; |
| import static net.bytebuddy.implementation.MethodDelegation.to; |
| import static net.bytebuddy.implementation.attribute.MethodAttributeAppender.ForInstrumentedMethod.INCLUDING_RECEIVER; |
| import static net.bytebuddy.matcher.ElementMatchers.*; |
| import static org.mockito.internal.util.StringUtil.join; |
| |
| class SubclassBytecodeGenerator implements BytecodeGenerator { |
| |
| private static final String CODEGEN_PACKAGE = "org.mockito.codegen."; |
| |
| private final SubclassLoader loader; |
| |
| private final ByteBuddy byteBuddy; |
| private final Random random; |
| |
| private final Implementation readReplace; |
| private final ElementMatcher<? super MethodDescription> matcher; |
| |
| private final Implementation dispatcher = to(DispatcherDefaultingToRealMethod.class); |
| private final Implementation hashCode = to(MockMethodInterceptor.ForHashCode.class); |
| private final Implementation equals = to(MockMethodInterceptor.ForEquals.class); |
| private final Implementation writeReplace = to(MockMethodInterceptor.ForWriteReplace.class); |
| |
| public SubclassBytecodeGenerator() { |
| this(new SubclassInjectionLoader()); |
| } |
| |
| public SubclassBytecodeGenerator(SubclassLoader loader) { |
| this(loader, null, any()); |
| } |
| |
| public SubclassBytecodeGenerator(Implementation readReplace, ElementMatcher<? super MethodDescription> matcher) { |
| this(new SubclassInjectionLoader(), readReplace, matcher); |
| } |
| |
| protected SubclassBytecodeGenerator(SubclassLoader loader, Implementation readReplace, ElementMatcher<? super MethodDescription> matcher) { |
| this.loader = loader; |
| this.readReplace = readReplace; |
| this.matcher = matcher; |
| byteBuddy = new ByteBuddy().with(TypeValidation.DISABLED); |
| random = new Random(); |
| } |
| |
| @Override |
| public <T> Class<? extends T> mockClass(MockFeatures<T> features) { |
| String name = nameFor(features.mockedType); |
| DynamicType.Builder<T> builder = |
| byteBuddy.subclass(features.mockedType) |
| .name(name) |
| .ignoreAlso(isGroovyMethod()) |
| .annotateType(features.stripAnnotations |
| ? new Annotation[0] |
| : features.mockedType.getAnnotations()) |
| .implement(new ArrayList<Type>(features.interfaces)) |
| .method(matcher) |
| .intercept(dispatcher) |
| .transform(withModifiers(SynchronizationState.PLAIN)) |
| .attribute(features.stripAnnotations |
| ? MethodAttributeAppender.NoOp.INSTANCE |
| : INCLUDING_RECEIVER) |
| .method(isHashCode()) |
| .intercept(hashCode) |
| .method(isEquals()) |
| .intercept(equals) |
| .serialVersionUid(42L) |
| .defineField("mockitoInterceptor", MockMethodInterceptor.class, PRIVATE) |
| .implement(MockAccess.class) |
| .intercept(FieldAccessor.ofBeanProperty()); |
| if (features.serializableMode == SerializableMode.ACROSS_CLASSLOADERS) { |
| builder = builder.implement(CrossClassLoaderSerializableMock.class) |
| .intercept(writeReplace); |
| } |
| if (readReplace != null) { |
| builder = builder.defineMethod("readObject", void.class, Visibility.PRIVATE) |
| .withParameters(ObjectInputStream.class) |
| .throwing(ClassNotFoundException.class, IOException.class) |
| .intercept(readReplace); |
| } |
| ClassLoader classLoader = new MultipleParentClassLoader.Builder() |
| .append(features.mockedType) |
| .append(features.interfaces) |
| .append(currentThread().getContextClassLoader()) |
| .append(MockAccess.class, DispatcherDefaultingToRealMethod.class) |
| .append(MockMethodInterceptor.class, |
| MockMethodInterceptor.ForHashCode.class, |
| MockMethodInterceptor.ForEquals.class).build(MockMethodInterceptor.class.getClassLoader()); |
| if (classLoader != features.mockedType.getClassLoader()) { |
| assertVisibility(features.mockedType); |
| for (Class<?> iFace : features.interfaces) { |
| assertVisibility(iFace); |
| } |
| builder = builder.ignoreAlso(isPackagePrivate() |
| .or(returns(isPackagePrivate())) |
| .or(hasParameters(whereAny(hasType(isPackagePrivate()))))); |
| } |
| return builder.make() |
| .load(classLoader, loader.resolveStrategy(features.mockedType, classLoader, name.startsWith(CODEGEN_PACKAGE))) |
| .getLoaded(); |
| } |
| |
| private static ElementMatcher<MethodDescription> isGroovyMethod() { |
| return isDeclaredBy(named("groovy.lang.GroovyObjectSupport")); |
| } |
| |
| // TODO inspect naming strategy (for OSGI, signed package, java.* (and bootstrap classes), etc...) |
| private String nameFor(Class<?> type) { |
| String typeName = type.getName(); |
| if (isComingFromJDK(type) |
| || isComingFromSignedJar(type) |
| || isComingFromSealedPackage(type)) { |
| typeName = CODEGEN_PACKAGE + type.getSimpleName(); |
| } |
| return String.format("%s$%s$%d", typeName, "MockitoMock", Math.abs(random.nextInt())); |
| } |
| |
| private boolean isComingFromJDK(Class<?> type) { |
| // Comes from the manifest entry : |
| // Implementation-Title: Java Runtime Environment |
| // This entry is not necessarily present in every jar of the JDK |
| return type.getPackage() != null && "Java Runtime Environment".equalsIgnoreCase(type.getPackage().getImplementationTitle()) |
| || type.getName().startsWith("java.") |
| || type.getName().startsWith("javax."); |
| } |
| |
| private boolean isComingFromSealedPackage(Class<?> type) { |
| return type.getPackage() != null && type.getPackage().isSealed(); |
| } |
| |
| private boolean isComingFromSignedJar(Class<?> type) { |
| return type.getSigners() != null; |
| } |
| |
| private static void assertVisibility(Class<?> type) { |
| if (!Modifier.isPublic(type.getModifiers())) { |
| throw new MockitoException(join("Cannot create mock for " + type, |
| "", |
| "The type is not public and its mock class is loaded by a different class loader.", |
| "This can have multiple reasons:", |
| " - You are mocking a class with additional interfaces of another class loader", |
| " - Mockito is loaded by a different class loader than the mocked type (e.g. with OSGi)", |
| " - The thread's context class loader is different than the mock's class loader")); |
| } |
| } |
| } |