And finally I got it! As I said, one step at time.
The trick was to use the AnalyzerAdapter class and keep a track about the name of the field in the stack so at any time you can know the field name who is referenced.
Here is the main code:
/** * * @author Marcelo D. Ré {@literal <[email protected]>} */ public class WriteAccessActivatorAdapter extends AnalyzerAdapter implements ITransparentDirtyDetectorDef, IJavaCollections { private final static Logger LOGGER = Logger.getLogger(WriteAccessActivatorAdapter.class.getName()); private boolean activate = false; private String owner; private List<String> ignoreFields; private List<String> collectionFields; HashSet<String> lastCollectionModifiedFields = new HashSet<>(); // mapea la posición de la pila con el nombre del campo asociado private Map<String,String> stackToField = new HashMap<>(); static { if (LOGGER.getLevel() == null) { LOGGER.setLevel(LogginProperties.WriteAccessActivatorAdapter); } } public WriteAccessActivatorAdapter(int api, String owner, int access, String name, String descriptor, MethodVisitor methodVisitor, List<String> ignoreFields, List<String> collectionFields ) { super(api, owner, access, name, descriptor, methodVisitor); this.ignoreFields = ignoreFields; this.collectionFields = collectionFields; this.owner = owner; } /** * Add a call to setDirty in every method that has a PUTFIELD in its code. * @param opcode código a analizar */ @Override public synchronized void visitInsn(int opcode) { LOGGER.log(Level.FINEST, "Activate: {0} - opcode: {1} ", new Object[]{this.activate,Printer.OPCODES[opcode]}); // analizar las listas if ((this.activate)&&((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW )) { // si hay colleciones agregadas, incluirlas como dirty antes de retornar. if (lastCollectionModifiedFields.size()>0) { insertDirtyCollectionsFields(); lastCollectionModifiedFields.clear(); } LOGGER.log(Level.FINEST, "Agregando llamada a setDirty..."); mv.visitVarInsn(Opcodes.ALOAD, 0); // mv.visitInsn(Opcodes.ICONST_1); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, owner, SETDIRTY, "()V", false); //mv.visitFieldInsn(Opcodes.PUTFIELD, owner, "__ogm__dirtyMark", "Z"); } super.visitInsn(opcode); LOGGER.log(Level.FINEST, "fin --------------------------------------------------"); } @Override public synchronized void visitFieldInsn(int opcode, String owner, String name, String desc) { super.visitFieldInsn(opcode, owner, name, desc); LOGGER.log(Level.FINEST, "opcode: {0} - owner: {1} - name: {2} - desc: {3} - transient: {4}", new Object[]{Printer.OPCODES[opcode], owner, name, desc, ignoreFields.contains(name)}); printStack(); if ((opcode ==Opcodes.GETFIELD)||(opcode == Opcodes.GETSTATIC)) { // si se está accediendo a un field, preservar el nombre para futuras referencias. this.stackToField.put(""+(this.stack==null? 0:(this.stack.size()-1)),name); // this.owner = owner; } if ((opcode == Opcodes.PUTFIELD) && (!ignoreFields.contains(name))) { LOGGER.log(Level.FINEST, "Modificación detectada!! Agregar el campo \"{0}\" a la lista.",new Object[]{name}); this.activate = true; // this.owner = owner; printStack(); insertDirtyField(name); } LOGGER.log(Level.FINEST, "fin --------------------------------------------------"); } @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { LOGGER.log(Level.FINEST, "opcode: {0} - owner: {1} - name: {2} - desc: {3} - isInterface: {4}", new Object[]{Printer.OPCODES[opcode], owner, name, descriptor, isInterface}); printStack(); // si el método coincide con una de las clases y métodos a monitorear, revisar el stack para verificar // que el campo sea un field. LOGGER.log(Level.FINEST, "activable object?: "+getJavaCollections().contains("L"+owner+";") +" - method: "+ name + "> activable? : " +getJavaCollectionsDirtyMethods().contains(name) ); if ((getJavaCollections().contains("L"+owner+";")) && (getJavaCollectionsDirtyMethods().contains(name))) { // calcular la posición de la pila a acceder int stackOffset = descriptor.equals("()V")?0:descriptor.substring(1, descriptor.indexOf(")")) .split(";").length; int stackIdx = this.stack == null ? 0 : this.stack.size() - 1 - stackOffset; String field = this.stackToField.get(""+stackIdx); LOGGER.log(Level.FINEST, "modificación de una colección detectada! stack idx: "+stackIdx+" field: "+field); if (this.collectionFields.contains(field)) { lastCollectionModifiedFields.add(field); this.activate = true; // this.owner = owner; } } super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } @Override public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); LOGGER.log(Level.FINEST, "\n\n\n\n\nname: "+name+" - desc: "+descriptor+" bs: "+ Arrays.toString(bootstrapMethodArguments)); printStack(); for (Object bsMthArg : bootstrapMethodArguments) { String bsMth = bsMthArg.toString(); int dot = bsMth.indexOf('.'); int bracket = bsMth.indexOf("("); if (dot > 0 && bracket > 0) { String cls = "L"+bsMth.substring(0, dot)+";"; String mth = bsMth.substring(dot+1, bracket); LOGGER.log(Level.FINEST, "cls: "+cls + " - method: "+mth); if (getJavaCollections().contains(cls) && getJavaCollectionsDirtyMethods().contains(mth)) { int stackIdx = this.stack.size() - 1 ; String field = this.stackToField.get(""+stackIdx); LOGGER.log(Level.FINEST, "modificación de una colección detectada! stack idx: "+stackIdx+" field: "+field); if (this.collectionFields.contains(field)) { lastCollectionModifiedFields.add(field); this.activate = true; } } } } LOGGER.log(Level.FINEST, "\n\n\n\n\n"); } @Override public void visitLabel(Label label) { LOGGER.log(Level.FINEST, "Label: "+label); if (lastCollectionModifiedFields.size()>0){ // si se ha agregado un collectionModifiedField, instrumentar add del campo LOGGER.log(Level.FINEST, "Modificaciones detectadas!! Agregar los campos a la lista."); printStack(); insertDirtyCollectionsFields(); // resetear el campo lastCollectionModifiedFields.clear(); LOGGER.log(Level.FINEST, " --------------------------------------------------"); } super.visitLabel(label); } @Override public void visitJumpInsn(int opcode, Label label) { if (this.activate && opcode == Opcodes.GOTO) { // si hay colleciones agregadas, incluirlas como dirty antes de retornar. if (lastCollectionModifiedFields.size()>0) { insertDirtyCollectionsFields(); lastCollectionModifiedFields.clear(); } } super.visitJumpInsn(opcode, label); } @Override public void visitEnd() { LOGGER.log(Level.FINEST, "fin MethodVisitor -------------------------------------"); // mv.visitMaxs(0, 0); super.visitEnd(); } private void printStack() { if (LOGGER.isLoggable(Level.FINEST)) { if (this.stack != null) { System.out.println("stack size:"+this.stack.size()); for (int i = 0; i < this.stack.size(); i++) { Object o = this.stack.get(i); System.out.println(""+o.getClass()+" : "+o + " --> "+ this.stackToField.get(""+i)); } System.out.println("--------------"); } else { System.out.println("stack size: NULL <<<<<<<<<<<<<<<<<<<<<<< "); } } } /** * Insert all field registered in the lastCollectionFields hashset. */ private void insertDirtyCollectionsFields() { for (String lastCollectionModifiedField : lastCollectionModifiedFields) { mv.visitVarInsn(Opcodes.ALOAD, 0); mv.visitFieldInsn(Opcodes.GETFIELD, owner, MODIFIEDFIELDS, "Ljava/util/Set;"); mv.visitLdcInsn(lastCollectionModifiedField); mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/Set", "add", "(Ljava/lang/Object;)Z", true); mv.visitInsn(Opcodes.POP); // Descartar el resultado booleano de add } } private void insertDirtyField(String name) { mv.visitVarInsn(Opcodes.ALOAD, 0); mv.visitFieldInsn(Opcodes.GETFIELD, owner, MODIFIEDFIELDS, "Ljava/util/Set;"); mv.visitLdcInsn(name); mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/Set", "add", "(Ljava/lang/Object;)Z", true); mv.visitInsn(Opcodes.POP); // Descartar el resultado booleano de add } }
Of course, there's a lot of situation that it will not work. You must respect the "Tell, don`t ask" rule and "Law of Demeter" !!!
The full code is at github.
adddoes not necessarily modify the collection. In case of aSet, for example, a return value offalseimplies that the set has not been modified. Further, it seems you are focusing on the easier scenario, where the code modifying the collection is inside the class whose “dirty” flag you want to set. But this doesn’t have to be the case. The collection might be returned by a method and the caller may one of the dozens modification methods, perhaps one not yet defined, as the API can evolve. You should probably rethink your approach…