Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a Java decompiler (be it standalone or an Eclipse plugin) that is able to decompile code woven by AspectJ?

I have scanned the various related questions on the Internet (like http://www.java-decompiler.com/) and SO in particular. So far, I could only find two Java decompilers - JD-GUI and DJ Java Decompiler that claim to be up-to-date.

All the rest are either unavailable for download or discontinued.

So, I took a .class file containing AspectJ weaven code and decompiled it using both of the available decompilers. Observe the results:

  1. JD-GUI:

    enter image description here

  2. DJ Java Decompiler:

    enter image description here

As you can see both tools fail to decompile the Java code weaven with AspectJ.

Now I am not being too picky, I am just used to the .NET Reflector and looking for the same quality in a Java decompiler, be it stand-alone or Eclipse plugin, free or commercial.

I am looking for one that actually works, and is convenient to use.

Edit

The general tenor of the responses to my question is something like this - "Well, what do you want? Although, AspectJ creates valid JVM bytecode, this bytecode cannot be translated into valid Java". All I can say is that I do not subscribe to this point of view.

Let me present you some more context and I hope you will agree that the tools can and should do better.

The decompiled Java class was weaven using the following aspect:

public abstract aspect LoggingAspect {
  declare parents: (@LogMe *) implements ILoggable;

  public Logger ILoggable.getLogger() {
    LoggerHolderAspect holder = LoggerHolderAspect.aspectOf(this.getClass());
    return holder.getLogger();
  }

  abstract pointcut loggedMethods();

  before(ILoggable o): loggedMethods() && this(o) {
    logBefore(o.getLogger(), thisJoinPoint);
  }

  after(ILoggable o) returning (Object result): loggedMethods() && this(o) {
    logAfterReturning(o.getLogger(), thisJoinPoint, result);
  }

  after(ILoggable o) throwing (Exception e): loggedMethods() && this(o) {
    logAfterThrowing(o.getLogger(), thisJoinPoint, e);
  }

  protected void logBefore(Logger l, JoinPoint jp) { ... }
  protected void logAfterReturning(Logger l, JoinPoint jp, Object result) { ... }
  protected void logAfterThrowing(Logger l, JoinPoint jp, Exception e) { ... }
}

Now, the class being weaven is this:

@Path("user")
public class UserHandler {
  ...    

  @GET
  @Path("{id}")
  @Produces({ "application/json", "application/xml" })
  public User getUser(@PathParam("id") int id) { ... }

  @DELETE
  @Path("{id}")
  public void deleteUser(@PathParam("id") int id) { ... }

  @PUT
  @Path("{id}")
  public void putUser(@PathParam("id") int id, User entity) { ... }

  @POST
  @Produces({ "application/json", "application/xml" })
  public Response postUser(User entity) { ... }
}

Now, the JD-GUI simply fails to decompile each and every instrumented method correctly. The produced output looks like a badly done SVN merge. Here is the link to the complete file for the curious - http://pastebin.com/raw.php?i=WEmMNCPS

The output produced by DJ Java Decompiler is slightly better. Seems like DJ has a problem with non void methods. Indeed, observe how it decompiles a void method:

@DELETE
@Path(value="{id}")
public void deleteUser(@PathParam(value="id") int id)
{
    int i = id;
    org.aspectj.lang.JoinPoint joinpoint = Factory.makeJP(ajc$tjp_1, this, this, Conversions.intObject(i));
    try
    {
        ResourceHandlerLoggingAspect.aspectOf().ajc$before$com_shunra_poc_logging_LoggingAspect$1$51e061ae(this, joinpoint);
        if(!securityContext.isUserInRole(Enroler.ADMINISTRATOR.getName()))
        {
            throw new WebApplicationException(javax.ws.rs.core.Response.Status.UNAUTHORIZED);
        } else
        {
            m_userService.delete(id);
            Object obj = null;
            ResourceHandlerLoggingAspect.aspectOf().ajc$afterReturning$com_shunra_poc_logging_LoggingAspect$2$51e061ae(this, obj, joinpoint);
            return;
        }
    }
    catch(Exception exception)
    {
        ResourceHandlerLoggingAspect.aspectOf().ajc$afterThrowing$com_shunra_poc_logging_LoggingAspect$3$51e061ae(this, exception, joinpoint);
        throw exception;
    }
}

This is fine, but if a method returns something, then DJ fails, for instance:

@POST
@Produces(value={"application/json", "application/xml"})
public Response postUser(User entity)
{
    org.aspectj.lang.JoinPoint joinpoint;
    User user = entity;
    joinpoint = Factory.makeJP(ajc$tjp_3, this, this, user);
    ResourceHandlerLoggingAspect.aspectOf().ajc$before$com_shunra_poc_logging_LoggingAspect$1$51e061ae(this, joinpoint);
    entity.Id = 0;
    m_userService.post(entity);
    Response response;
    Response response1 = response = Response.created(postedUserLocation(entity.Id)).entity(new EntityPostResult(entity.Id)).build();
    ResourceHandlerLoggingAspect.aspectOf().ajc$afterReturning$com_shunra_poc_logging_LoggingAspect$2$51e061ae(this, response1, joinpoint);
    return response;
    Exception exception;
    exception;
    ResourceHandlerLoggingAspect.aspectOf().ajc$afterThrowing$com_shunra_poc_logging_LoggingAspect$3$51e061ae(this, exception, joinpoint);
    throw exception;
}

Again, here is the complete output for the curious - http://pastebin.com/raw.php?i=Qnwjm16y

What exactly is being done by AspectJ, that these decompilers cannot cope with? All I want is a Java decompiler that is on a par with the .NET Reflector, which has absolutely no problem decompiling instrumented C# code, except really special cases and we are not there.

Edit 2

Following the suggestion of Andy Clement I have created a small application just to test how AspectJ instruments the code, compare it against manual instrumentation and see how JD-GUI and DJ decompile it. Below are my findings:

The Java source code is:

public class Program {
  private static boolean doThrow;

  public static void main(String[] args) {
    run();
    doThrow = true;
    run();
  }

  private static void run() {
    System.out.println("===============================================");
    System.out.println("doThrow = " + doThrow);
    System.out.println("===============================================");
    System.out.println("autoInstrumented:");
    try {
      System.out.println(autoInstrumented());
    } catch (Exception e) {
      System.out.println(e.getMessage());
    }
    System.out.println("===============================================");
    System.out.println("manuallyInstrumented:");
    try {
      System.out.println(manuallyInstrumented());
    } catch (Exception e) {
      System.out.println(e.getMessage());
    }
    System.out.println("===============================================");
  }

  public static void before() {
    System.out.println("before(f)");
  }

  public static void afterReturning(int x) {
    System.out.println("afterReturning(f) = " + x);
  }

  public static void afterThrowing(Exception e) {
    System.out.println("afterThrowing(f) = " + e.getMessage());
  }

  public static int f() throws Exception {
    if (doThrow) {
      throw new Exception("*** EXCEPTION !!! ***");
    }
    return 10;
  }

  public static int autoInstrumented() throws Exception {
    return f();
  }

  public static int manuallyInstrumented() throws Exception {
    before();
    try {
      int result = f();
      afterReturning(result);
      return result;
    } catch (Exception e) {
      afterThrowing(e);
      throw e;
    }
  }
}

The aspect code is:

public aspect Weave {
  pointcut autoInstrumented() : execution(int Program.autoInstrumented());

  before() : autoInstrumented() {
    Program.before();
  }

  after() returning (int result) : autoInstrumented() {
    Program.afterReturning(result);
  }

  after() throwing (Exception e) : autoInstrumented() {
    Program.afterThrowing(e);
  }
}

The output produced by DJ is:

public static int autoInstrumented()
    throws Exception
{
    Weave.aspectOf().ajc$before$Weave$1$be1609d6();
    int i;
    int j = i = f();
    Weave.aspectOf().ajc$afterReturning$Weave$2$be1609d6(j);
    return i;
    Exception exception;
    exception;
    Weave.aspectOf().ajc$afterThrowing$Weave$3$be1609d6(exception);
    throw exception;
}

Putting aside the names of the AspectJ created methods, the produced Java code is both wrong and invalid. It is wrong, because there is no try-catch statement. It is invalid because exception; is not a valid Java statement.

Next comes JD-GUI:

public static int autoInstrumented() throws Exception {
  try {
    Weave.aspectOf().ajc$before$Weave$1$be1609d6();
    int i;
    int j = i = f(); Weave.aspectOf().ajc$afterReturning$Weave$2$be1609d6(j); return i; } catch (Exception localException) { Weave.aspectOf().ajc$afterThrowing$Weave$3$be1609d6(localException); } throw localException;
}

I have to take my words about the JD-GUI producing corrupted output back. It so happens that the code is more or less correct, but all of the method tail is output on a single line! When viewing inside the GUI it looks like the method is truncated. Only after copying the code, pasting in Eclipse and reformatting can one see, that it is almost OK:

public static int autoInstrumented() throws Exception {
  try {
    Weave.aspectOf().ajc$before$Weave$1$be1609d6();
    int i;
    int j = i = f();
    Weave.aspectOf().ajc$afterReturning$Weave$2$be1609d6(j);
    return i;
  } catch (Exception localException) {
    Weave.aspectOf().ajc$afterThrowing$Weave$3$be1609d6(localException);
  }
  throw localException;
}

Almost, because how come the throw localException; statement finds itself outside the catch block?

Now, as for the actual JVM byte code. I have used the ByteCode Outline Eclipse extension, here are the results:

The manually instrumented method manuallyInstrumented:

public static manuallyInstrumented()I throws java/lang/Exception 
ATTRIBUTE org.aspectj.weaver.MethodDeclarationLineNumber : unknown
  TRYCATCHBLOCK L0 L1 L2 java/lang/Exception
 L3
  INVOKESTATIC Program.before()V
 L0
  INVOKESTATIC Program.f()I
  ISTORE 0
 L4
  ILOAD 0
  INVOKESTATIC Program.afterReturning(I)V
 L5
  ILOAD 0
 L1
  IRETURN
 L2
 FRAME SAME1 java/lang/Exception
  ASTORE 0
 L6
  ALOAD 0
  INVOKESTATIC Program.afterThrowing(Ljava/lang/Exception;)V
 L7
  ALOAD 0
  ATHROW
 L8
  LOCALVARIABLE result I L4 L2 0
  LOCALVARIABLE e Ljava/lang/Exception; L6 L8 0
  MAXSTACK = 1
  MAXLOCALS = 1

The auto instrumented method autoInstrumented:

public static autoInstrumented()I throws java/lang/Exception 
ATTRIBUTE org.aspectj.weaver.MethodDeclarationLineNumber : unknown
  TRYCATCHBLOCK L0 L1 L1 java/lang/Exception
 L0
  INVOKESTATIC Weave.aspectOf()LWeave;
  INVOKEVIRTUAL Weave.ajc$before$Weave$1$be1609d6()V
  INVOKESTATIC Program.f()I
  DUP
  ISTORE 0
  DUP
  ISTORE 1
  INVOKESTATIC Weave.aspectOf()LWeave;
  ILOAD 1
  INVOKEVIRTUAL Weave.ajc$afterReturning$Weave$2$be1609d6(I)V
  ILOAD 0
  IRETURN
 L1
  ASTORE 2
  INVOKESTATIC Weave.aspectOf()LWeave;
  ALOAD 2
  INVOKEVIRTUAL Weave.ajc$afterThrowing$Weave$3$be1609d6(Ljava/lang/Exception;)V
  ALOAD 2
  ATHROW
  MAXSTACK = 3
  MAXLOCALS = 3

I am not a JVM guru (mildly put), so I cannot tell whether the JVM byte code of the autoInstrumented is "bad". Can you?

Summary:

  • DJ is no good
  • JD-GUI is almost there, but still produces bad Java and usability is awful - need to copy, paste and reformat in Eclipse to understand what is going on.

Bottomline

No love.

like image 890
mark Avatar asked Dec 08 '11 12:12

mark


1 Answers

Java bytecode can express things that literally can't be translated directly into Java, and as a result, class files created by tools that generate bytecode directly can't necessarily be decompiled. It's possible for a human to write analogous Java code, but that's an AI problem, and not at all the same thing as decompiling.

like image 71
Ernest Friedman-Hill Avatar answered Sep 17 '22 12:09

Ernest Friedman-Hill