Reputation: 44104
I need to change a string constant in a deployed Java program, i.e. the value inside the compiled .class
-files. It can be restarted, but not easily recompiled (though it's an inconvenient option if this question yields no answers). Is this possible?
Update: I just looked at the file with a hex editor and it looks like I can easily change the string there. Would that work, i.e. won't that invalidate some kind of signature of the file? The old and new string are both alphanumeric, and can be the same length if needed.
Update 2: I fixed it. Because the specific class I needed to change is very small and didn't change in the new version of the project, I could just compile that and take the new class from there. Still interested in an answer that doesn't involve compilation though, for educational purposes.
Upvotes: 15
Views: 15767
Reputation: 651
I had a similar issue in the past. My solution was to use one of the mentioned bytecode engineering libraries. I could not find javaassist, however there is a great tool called dirtyJOE that allows you (among many things) to edit constants in your .class file.
Here is a screenshot
You just import the .class file and click on the constant
Upvotes: 0
Reputation: 1954
I recently wrote my own ConstantPool mapper because ASM and JarJar had the following issues:
I ended up with the following:
public void process(DataInputStream in, DataOutputStream out, Function mapper) throws IOException {
int magic = in.readInt();
if (magic != 0xcafebabe) throw new ClassFormatError("wrong magic: " + magic);
out.writeInt(magic);
copy(in, out, 4); // minor and major
int size = in.readUnsignedShort();
out.writeShort(size);
for (int i = 1; i < size; i++) {
int tag = in.readUnsignedByte();
out.writeByte(tag);
Constant constant = Constant.constant(tag);
switch (constant) {
case Utf8:
out.writeUTF(mapper.apply(in.readUTF()));
break;
case Double:
case Long:
i++; // "In retrospect, making 8-byte constants take two constant pool entries was a poor choice."
// See http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.4.5
default:
copy(in, out, constant.size);
break;
}
}
Streams.copyAndClose(in, out);
}
private final byte[] buffer = new byte[8];
private void copy(DataInputStream in, DataOutputStream out, int amount) throws IOException {
in.readFully(buffer, 0, amount);
out.write(buffer, 0, amount);
}
And then
public enum Constant {
Utf8(1, -1),
Integer(3, 4),
Float(4, 4),
Long(5, 8),
Double(6,8),
Class(7, 2),
String(8, 2),
Field(9, 4),
Method(10, 4),
InterfaceMethod(11, 4),
NameAndType(12, 4),
MethodHandle(15, 3),
MethodType(16, 2),
InvokeDynamic(18, 4);
public final int tag, size;
Constant(int tag, int size) { this.tag = tag; this.size = size; }
private static final Constant[] constants;
static{
constants = new Constant[19];
for (Constant c : Constant.values()) constants[c.tag] = c;
}
public static Constant constant(int tag) {
try {
Constant constant = constants[tag];
if(constant != null) return constant;
} catch (IndexOutOfBoundsException ignored) { }
throw new ClassFormatError("Unknown tag: " + tag);
}
Just thought I'd show alternatives without libraries as it's quite a nice place to start hacking from. My code is was inspired by javap source code
Upvotes: 4
Reputation: 39451
The only extra data required when modifying a string (technically a Utf8 item) in the constant pool is the length field (2 bytes big endian preceding the data). There are no additional checksums or offsets that require modification.
There are two caveats:
If you plan to do this a lot, it's better to get a class file editor tool, but the hex editor is useful for quick changes.
Upvotes: 6
Reputation: 9295
You can modify .class using many bytecode engineering libraries. For e.g., using javaassist.
However, if you're trying to replace a static final member, it may not give you the desired effect, because the compiler would inline this constant wherever it is used.
Sample code using javaassist.jar
//ConstantHolder.java
public class ConstantHolder {
public static final String HELLO="hello";
public static void main(String[] args) {
System.out.println("Value:" + ConstantHolder.HELLO);
}
}
//ModifyConstant.java
import java.io.IOException;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.NotFoundException;
//ModifyConstant.java
public class ModifyConstant {
public static void main(String[] args) {
modifyConstant();
}
private static void modifyConstant() {
ClassPool pool = ClassPool.getDefault();
try {
CtClass pt = pool.get("ConstantHolder");
CtField field = pt.getField("HELLO");
pt.removeField(field);
CtField newField = CtField.make("public static final String HELLO=\"hell\";", pt);
pt.addField(newField);
pt.writeFile();
} catch (NotFoundException e) {
e.printStackTrace();System.exit(-1);
} catch (CannotCompileException e) {
e.printStackTrace();System.exit(-1);
} catch (IOException e) {
e.printStackTrace();System.exit(-1);
}
}
}
In this case, the program successfully modifies the value of HELLO from "Hello" to "Hell". However, when you run ConstantHolder class, it would still print "Value:Hello" because of inlining by the compiler.
Hope it helps.
Upvotes: 3
Reputation: 328614
If you have the sources for this class, then my approach is:
-source
and -target
.jar u
or an Ant taskExample for an Ant task:
<jar destfile="${jar}"
compress="true" update="true" duplicate="preserve" index="true"
manifest="tmp/META-INF/MANIFEST.MF"
>
<fileset dir="build/classes">
<filter />
</fileset>
<zipfileset src="${origJar}">
<exclude name="META-INF/*"/>
</zipfileset>
</jar>
Here I also update the manifest. Put the new classes first and then add all the files from the original JAR. duplicate="preserve"
will make sure that the new code will not be overwritten.
If the code isn't signed, you can also try to replace the bytes if the new string has the exact same length as the old one. Java does some checks on the code but there is no checksum in the .class files.
You must preserve the length; otherwise the class loader will get confused.
Upvotes: 8