Reputation: 41
I have the following class:
package some.clazz.client;
import some.clazz.SomeClass;
public class SomeClassClient {
...
public SomeClass getProc();
...
}
I've removed/shrunk/deleted this getProc()
Java method from SomeClassClient
class bytecode
by using new MemberRemoval().stripMethods(ElementMatcher);
ByteBuddy transformation
in net.bytebuddy:byte-buddy-maven-plugin
Maven Plugin.
But import some.clazz.SomeClass;
statement is still present and shown by CFR Java Decompiler
!
There are no any another reference to SomeClass
class in SomeClassClient
class.
How can I remove this import statement from bytecode (really I'm assuming it's located in constant pool)? Because I'm still getting ClassNotFoundException when trying to use 'SomeClassClient' class.
My class
public class MethodsRemover implements net.bytebuddy.build.Plugin {
...
@Override
public DynamicType.Builder<?> apply(DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassFileLocator classFileLocator) {
try{
return builder.visit(new MemberRemoval().stripMethods(
ElementMatchers.any().and(
isAnnotatedWith(Transient.class)
.and(
t -> {
log.info(
"ByteBuddy transforming class: {}, strip method: {}",
typeDescription.getName(),
t
);
return true;
}
)
).or(
target -> Arrays.stream(STRIP_METHODS).anyMatch(
m -> {
Class<?> methodReturnType = getMethodReturnType(m);
String methodName = getMethodName(m);
Class<?>[] methodParameters = getMethodParameters(m);
return
isPublic()
.and(returns(
isVoid(methodReturnType)
? is(TypeDescription.VOID)
: isSubTypeOf(methodReturnType)
))
.and(named(methodName))
.and(isNoParams(m)
? takesNoArguments()
: takesArguments(methodParameters)
)
.and(t -> {
log.info(
"ByteBuddy transforming class: {}, strip method: {}",
typeDescription.getName(),
t
);
return true;
}).matches(target)
;
}
)
)
));
...
}
I've added the following EntryPoint and configured it in bytebuddy plugin to use:
public static class EntryPoint implements net.bytebuddy.build.EntryPoint {
private net.bytebuddy.build.EntryPoint typeStrategyEntryPoint = Default.REDEFINE;
public EntryPoint() {
}
public EntryPoint(net.bytebuddy.build.EntryPoint typeStrategyEntryPoint) {
this.typeStrategyEntryPoint = typeStrategyEntryPoint;
}
@Override
public ByteBuddy byteBuddy(ClassFileVersion classFileVersion) {
return typeStrategyEntryPoint
.byteBuddy(classFileVersion)
.with(ClassWriterStrategy.Default.CONSTANT_POOL_DISCARDING)
.ignore(none()); // Traverse through all (include synthetic) methods of type
}
@Override
public DynamicType.Builder<?> transform(TypeDescription typeDescription,
ByteBuddy byteBuddy,
ClassFileLocator classFileLocator,
MethodNameTransformer methodNameTransformer) {
return typeStrategyEntryPoint
.transform(typeDescription, byteBuddy, classFileLocator, methodNameTransformer);
}
}
Upvotes: 1
Views: 299
Reputation: 41
Eventually I've invented a workaround that allows to handle the synthetic bridge methods and at the same time still to use ElementMatcher-s to select methods to remove... As mentioned @Rafael Winterhalter (author) above in its comment: Byte-Buddy lib at its current (v1.10.22 at the moment) version does not handle bridge methods by using its existing MemberRemoval class. So just extend it to remove/strip methods in the following manner:
package com.pany.of.yours.byte.buddy;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.ClassFileVersion;
import net.bytebuddy.asm.MemberRemoval;
import net.bytebuddy.build.Plugin;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.field.FieldList;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.method.MethodList;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.ClassFileLocator;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.scaffold.ClassWriterStrategy;
import net.bytebuddy.dynamic.scaffold.MethodGraph;
import net.bytebuddy.dynamic.scaffold.inline.MethodNameTransformer;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.jar.asm.ClassVisitor;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.pool.TypePool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static net.bytebuddy.matcher.ElementMatchers.is;
import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;
import static net.bytebuddy.matcher.ElementMatchers.isBridge;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.isSubTypeOf;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.none;
import static net.bytebuddy.matcher.ElementMatchers.returns;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments;
...
public class MethodsRemover implements Plugin {
private static final Logger log = LoggerFactory.getLogger(MethodsRemover.class);
private static final Object[][] STRIP_METHODS = {
{SomeClass.class, "getProc", void.class} //,
// other methods here
};
public MethodsRemover() {
}
@Override
public boolean matches(TypeDescription typeDefinitions) {
// return typeDefinitions.getName().equals("pkg.SomeClass");
return typeDefinitions.isAssignableTo(SomeClassSuper.class) }
@Override
public DynamicType.Builder<?> apply(DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassFileLocator classFileLocator) {
try{
log.info(" ByteBuddy processing type =========> {}", typeDescription);
return builder.visit(new MemberRemovalEx().stripMethods(
ElementMatchers.none()// <= or you can use ElementMatchers.any();
.or(t -> { // <= + .and(..) - as a start point instead.
log.debug("ByteBuddy processing method --> {}", t);
return false;
})
.or(
isAnnotatedWith(Transient.class)
.and(t -> {
log.info(
" ByteBuddy strip transient method ++> {}",
t
);
return true;
})
)
.or(
target -> Arrays.stream(STRIP_METHODS).anyMatch(
m -> {
Class<?> methodReturnType = getMethodReturnType(m);
String methodName = getMethodName(m);
Class<?>[] methodParameters = getMethodParameters(m);
return
isPublic()
.and(returns(
isVoid(methodReturnType)
? is(TypeDescription.VOID)
: isSubTypeOf(methodReturnType)
))
.and(named(methodName))
.and(isNoParams(m)
? takesNoArguments()
: takesArguments(methodParameters)
)
.and(t -> {
log.info(
" ByteBuddy strip signature method ++> {}",
t
);
return true;
}).matches(target)
;
}
)
)
));
} catch (Exception e) {
log.error("ByteBuddy error: ", e);
throw e;
}
}
...
public static class EntryPoint implements net.bytebuddy.build.EntryPoint {
private net.bytebuddy.build.EntryPoint typeStrategyEntryPoint = Default.REDEFINE;
public EntryPoint() {
}
public EntryPoint(net.bytebuddy.build.EntryPoint typeStrategyEntryPoint) {
this.typeStrategyEntryPoint = typeStrategyEntryPoint;
}
@Override
public ByteBuddy byteBuddy(ClassFileVersion classFileVersion) {
return typeStrategyEntryPoint
.byteBuddy(classFileVersion)
.with(MethodGraph.Compiler.Default.forJVMHierarchy()) // Change hashCode/equals by including a return type
.with(ClassWriterStrategy.Default.CONSTANT_POOL_DISCARDING) // Recreate constants pool
.ignore(none()); // Traverse through all (include synthetic) methods of type
}
@Override
public DynamicType.Builder<?> transform(TypeDescription typeDescription,
ByteBuddy byteBuddy,
ClassFileLocator classFileLocator,
MethodNameTransformer methodNameTransformer) {
return typeStrategyEntryPoint
.transform(typeDescription, byteBuddy, classFileLocator, methodNameTransformer);
}
}
private class MemberRemovalEx extends MemberRemoval {
private final Junction<FieldDescription.InDefinedShape> fieldMatcher;
private final Junction<MethodDescription> methodMatcher;
public MemberRemovalEx() {
this(ElementMatchers.none(), ElementMatchers.none());
}
public MemberRemovalEx(Junction<FieldDescription.InDefinedShape> fieldMatcher,
Junction<MethodDescription> methodMatcher) {
super(fieldMatcher, methodMatcher);
this.fieldMatcher = fieldMatcher;
this.methodMatcher = methodMatcher;
}
@Override
public MemberRemoval stripInvokables(ElementMatcher<? super MethodDescription> matcher) {
return new MemberRemovalEx(this.fieldMatcher, this.methodMatcher.or(matcher));
}
@Override
public ClassVisitor wrap(TypeDescription instrumentedType,
ClassVisitor classVisitor,
Implementation.Context implementationContext,
TypePool typePool,
FieldList<FieldDescription.InDefinedShape> fields,
MethodList<?> methods,
int writerFlags,
int readerFlags) {
MethodList<MethodDescription.InDefinedShape> typeBridgeMethods =
instrumentedType.getDeclaredMethods().filter(isBridge());
int bridgeMethodCount = typeBridgeMethods.size();
if (bridgeMethodCount > 0) {
List<MethodDescription> methodsPlusBridges = new ArrayList<>(
methods.size() + bridgeMethodCount
);
methodsPlusBridges.addAll(typeBridgeMethods);
methodsPlusBridges.addAll(methods);
methods = new MethodList.Explicit<>(methodsPlusBridges);
}
return super.wrap(
instrumentedType,
classVisitor,
implementationContext,
typePool,
fields,
methods,
writerFlags,
readerFlags
);
}
}
}
And also here is the used byte-buddy Maven plugin configuration:
<build>
<plugins>
<plugin>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-maven-plugin</artifactId>
<version>${byte-buddy-maven-plugin.version}</version>
<executions>
<execution>
<id>byte.buddy.strip.methods</id>
<phase>process-classes</phase>
<goals>
<goal>transform</goal>
</goals>
<configuration>
<transformations>
<transformation>
<!-- Next plugin transformer removes @Transient annotated and some predefined methods from entities -->
<plugin>com.pany.of.yours.byte.buddy.MethodsRemover</plugin>
<!-- Optionally, specify groupId, artifactId, version of the class -->
</transformation>
</transformations>
<!-- Optionally, add 'initialization' block with EntryPoint class -->
<initialization>
<entryPoint>
com.pany.of.yours.byte.buddy.MethodsRemover$EntryPoint
</entryPoint>
</initialization>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>some.your.aux.dependency.group</groupId>
<artifactId>dependency-artifact</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
Upvotes: 0
Reputation: 298499
In an attempt to reproduce your issue, I used the following program using ASM (the library which is also used by Byte-Buddy):
ClassWriter cw = new ClassWriter(0);
cw.visit(52, ACC_ABSTRACT, "Invalid", null, "java/lang/Object", null);
MethodVisitor mv = cw.visitMethod(
ACC_ABSTRACT|ACC_PUBLIC, "test", "()Lnon/existent/Class;", null, null);
mv.visitEnd();
cw.visitEnd();
byte[] invalidclassBytes = cw.toByteArray();
cw = new ClassWriter(new ClassReader(invalidclassBytes), 0);
cw.visit(52, ACC_ABSTRACT|ACC_INTERFACE, "Test", null, "java/lang/Object", null);
mv = cw.visitMethod(ACC_STATIC|ACC_PUBLIC, "test", "()V", null, null);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello from generated class");
mv.visitMethodInsn(INVOKEVIRTUAL,
"java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
cw.visitEnd();
byte[] classBytes = cw.toByteArray();
MethodHandles.lookup().defineClass(classBytes);
Class.forName("Test").getDeclaredMethod("test").invoke(null);
System.out.println();
Path p = Files.write(Files.createTempFile("Class", "Test.class"), classBytes);
ToolProvider.findFirst("javap")
.ifPresent(javap -> javap.run(System.out, System.err, "-c", "-v", p.toString()));
Files.delete(p);
try {
Class<?> cl = MethodHandles.lookup().defineClass(invalidclassBytes);
System.out.println("defined " + cl);
cl.getMethods();
}
catch(Error e) {
System.out.println("got expected error " + e);
}
It first generates bytecode for a class named Invalid
containing a method with a return type non.existent.Class
. It then generates a class Test
using a ClassReader
reading the bytecode of first as input to the ClassWriter
, which will copy the entire constant pool, including the references to non-existing classes.
This second class, Test
, is turned into a runtime class and its test
method invoked. Further, the bytecode is dumped to a temporary file and javap
run over it, to show the constant pool. Only after these steps, an attempt to create a runtime class for Invalid
is made, to provoke an error.
On my machine, it prints:
Hello from generated class
Classfile /C:/Users/███████████/AppData/Local/Temp/Class10921011438737096460Test.class
Last modified 29.03.2021; size 312 bytes
SHA-256 checksum 63df4401143b4fb57b4815fc193f3e47fdd4c301cd76fa7f945edb415e14330a
interface Test
minor version: 0
major version: 52
flags: (0x0600) ACC_INTERFACE, ACC_ABSTRACT
this_class: #8 // Test
super_class: #4 // java/lang/Object
interfaces: 0, fields: 0, methods: 1, attributes: 0
Constant pool:
#1 = Utf8 Invalid
#2 = Class #1 // Invalid
#3 = Utf8 java/lang/Object
#4 = Class #3 // java/lang/Object
#5 = Utf8 test
#6 = Utf8 ()Lnon/existent/Class;
#7 = Utf8 Test
#8 = Class #7 // Test
#9 = Utf8 ()V
#10 = Utf8 java/lang/System
#11 = Class #10 // java/lang/System
#12 = Utf8 out
#13 = Utf8 Ljava/io/PrintStream;
#14 = NameAndType #12:#13 // out:Ljava/io/PrintStream;
#15 = Fieldref #11.#14 // java/lang/System.out:Ljava/io/PrintStream;
#16 = Utf8 Hello from generated class
#17 = String #16 // Hello from generated class
#18 = Utf8 java/io/PrintStream
#19 = Class #18 // java/io/PrintStream
#20 = Utf8 println
#21 = Utf8 (Ljava/lang/String;)V
#22 = NameAndType #20:#21 // println:(Ljava/lang/String;)V
#23 = Methodref #19.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
#24 = Utf8 Code
{
public static void test();
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=0
0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #17 // String Hello from generated class
5: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
defined class Invalid
got expected error java.lang.NoClassDefFoundError: non/existent/Class
It shows that the signature of the first class’ method ()Lnon/existent/Class;
is present in the second class file but since there is no method definition pointing to it, it’s just an unused UTF-8 type entry without any hint about containing type references, so it can’t cause any harm.
But it’s even shown that with the widespread Hotspot JVM, having a real class entry pointing to the yet-not-defined class Invalid
doesn’t prevent us from loading and using the class Test
.
Even more interestingly, the attempt to define a runtime class for Invalid
succeeded too, as the message “defined class Invalid” has been printed. It required an actual operation stumbling over the absent non/existent/Class
, like cl.getMethods()
to provoke an error.
I did another step and fed the generated bytecode to CFR on www.javadecompilers.com. It produced
/*
* Decompiled with CFR 0.150.
*/
interface Test {
public static void test() {
System.out.println("Hello from generated class");
}
}
showing that those dangling entries of the constant pool did not led to the generation of import
statements.
It all indicates that your assumption that there is no active use of the class SomeClass
in your transformed class is wrong. There must be an active use of the class that causes the exception and the generation of the import
statement.
It’s also worth noting that in the other direction, compiling source code containing import
statements of otherwise unused classes, no reference to those classes will appear in the class file.
The information given in this comment is crucial:
I've forgot to specify that
SomeClassClient
has a super class and also some interface in its hierarchy which(interface) defines thisTProc getProc()
method with generic return type which in turn extendsAbstractSomeClass
and is passed asSomeClass
to super class definition.javap displays:
- before instrumentation:
SomeClass getProc()
- after instrumentation:
AbstractSomeClass getProc()
Where as CFR disassembler shows only import statement.
I added formatting to the comment text
What you have here, is a bridge method. Since the original class implemented the method with a more specific return type, the compiler added a synthetic method overriding the AbstractSomeClass getProc()
method and delegating to the SomeClass getProc()
.
You removed the SomeClass getProc()
but not the bridge method. The bridge method is the code that still has references to the SomeClass
. The decompiler produced the import
statement as it encountered the reference to SomeClass
when processing the bridge method but did not generate source code for the bridge method as for normal code that would be unnecessary as generating source code for the actual target method is sufficient to reproduce the bridge method.
To eliminate the SomeClass
reference completely, you must remove both methods from the bytecode. For ordinary Java code, you can simply relax the return type checking, as the Java language doesn’t allow to define multiple methods with the same name and parameter types. So when the template’s return type is a reference type, you may simply match any reference return type, to match any overriding method and all of its bridge methods. You could add a check for the bridge method flag when the return type is a super type of the template’s return type, but, as said, for ordinary Java code, this is not necessary.
Upvotes: 6