12

I'm attempting to retrieve the source code of any class (if available) from within a Java program for debugging purposes. Let's say I have the Class[_]'s reference to which I would like to retrieve the source code.

What I have attempted so far - in Scala:

val clazz = classOf[ClassDefinedInSeparateFile]

  1. clazz.getProtectionDomain.getCodeSource.getLocation.toString + "/" + clazz.getPackage.getName.replaceAll("\\.","/") + "/" + clazz.getSimpleName + ".scala" - looks OK, the JAR is there and contains the .scala file, but could not open using Source.fromFile(...).
  2. "/" + clazz.getPackage.getName.replaceAll("\\.","/") + "/" + clazz.getSimpleName + ".scala" - looks OK, but could not open using Source.fromInputStream(...)

Remarks:

  • There is no IDE available in production or staging environments.
  • In our setting JARs contain the source code .java or .scala files, therefore a decompiler is not necessary. (At least for the source code of the application, but not the dependencies. If a snippet is accessible of the application source code, that is enough - most exceptions are caught at the application level and relevant there.)

Thanks.

Bhargav Rao
  • 50,140
  • 28
  • 121
  • 140
Dyin
  • 5,815
  • 8
  • 44
  • 69
  • 2
    Could you not just use an IDE? – Andy Turner Dec 12 '18 at 18:18
  • For this use-case, unfortunately, it is not an option. There is no GUI available most of the times in production or staging environments. – Dyin Dec 12 '18 at 18:50
  • Since when do *"Most JARs contain the source code .java or .scala files"*? Who or what is supposed to put them there? I think you should describe your build process in more detail. – Andrey Tyukin Dec 12 '18 at 22:35
  • Well in our case they contain, at least the main program code - not the dependencies -, thus catching an exception at the main program source, one could print out the corresponding code snippet. The build process may be irrelevant, but it will include the source files as well, it just happens that as a resource they can not be read. Probably missing something... – Dyin Dec 13 '18 at 07:31
  • I improved my question based on your suggestions. – Dyin Dec 13 '18 at 07:33

3 Answers3

3

If the source is inside the jar, which is in classpath, you need to find out where it is exactly.

clazz.getName.replaceAll("\\.", "/") + ".scala" is an ok guess, but: (1) source code may not be in the same place as the classes - there could be a prefix (like src/ or whatever), or it could even be in a different jar, and (2) scala classes do not have to be in files with the same name - you can have several classes in one file, the file can be called foo.scala, some classes are generated on the fly etc. Also, a package is not always a directory in scala (it could be a package object for instance).

If you know the location inside the jar (and the jar is in the classpath), the way to open it is: clazz.getClassLoader.getResourceAsStream ... but, like I said above, the trick is to figure out the location. It is not easy (and there is no single standard way to do it).

Your best bet is indeed to use an IDE. I understand that you don't have it available in production environment, but you don't really need that. What you need is the production source code available on some machine where you have an IDE, and that you can achieve with a simple scp command.

Dima
  • 39,570
  • 6
  • 44
  • 70
  • I think `"/" + clazz.getPackage.getName.replaceAll("\\.","/") + "/" + clazz.getSimpleName + ".scala"` is more accurate, although the JAR is in the classpath, the resource is not found. I have checked the file is at the correctly guessed location. Isn't it that only "resources" could be opened that way? Could you reproduce it? – Dyin Dec 13 '18 at 07:27
  • I still could not reproduce it. I can see the JAR with sources in classpath, and `.scala` files are on path `/org/whatever/Class.scala`, but `getClassLoader.getResourceAsStream("/org/whatever/Class.scala")` throws an NPE. – Dyin Dec 13 '18 at 13:55
1

If I simply placed the source in the same folder as the class the below code worked fine for both code in the classpath and in jars

Note that there is no reason to prefix the name with "/", but you should scan for top level class.

I apologize for it being in core java but I didn't want to add any additional dependencies and wanted to be as clear as possible.

package com.stackoverflow.q53749060;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.stream.Stream;

import org.junit.Test;

import com.stackoverflow.q53749060.Answer.Result.Found;
import com.stackoverflow.q53749060.Answer.Result.NotFound;
import com.stackoverflow.q53749060.MyTopLevelClass.MyNestedClass;
import com.stackoverflow.q53749060.MyTopLevelClassInAnotherJar.MyNestedClassInAnotherJar;

@SuppressWarnings("javadoc")
public class Answer {

    static final String[] EXTENSIONS = { "java", "scala" };

    @Test
    public void test() {

        Arrays.stream(EXTENSIONS)
            .flatMap(ext -> toSource(ext, MyTopLevelClass.class, MyNestedClass.class,MyTopLevelClassInAnotherJar.class,MyNestedClassInAnotherJar.class, String.class))
            .forEach(System.out::println);

    }

    public Stream<Result> toSource(final String extension, final Class<?>... classes) {

        return Arrays.stream(classes)
            .map(clazz -> toSource(extension, clazz));
    }

    public Result toSource(final String extension, final Class<?> clazz) {

        Class<?> topLevelClass = clazz;

        while (topLevelClass.getEnclosingClass() != null) {
            topLevelClass = topLevelClass.getEnclosingClass();
        }

        final String name = topLevelClass.getName()
            .replaceAll("\\.", "/") + "." + extension;

        final Thread currentThread = Thread.currentThread();

        final ClassLoader contextClassLoader = currentThread.getContextClassLoader();

        if (contextClassLoader.getResource(name) == null) {
            return new NotFound(clazz);
        }

        final String source = toSource(name, contextClassLoader);

        return new Found(clazz, name, source);
    }

    public String toSource(final String name, final ClassLoader contextClassLoader) {

        try (final InputStream resourceInputStream = contextClassLoader.getResourceAsStream(name);
                final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {

            int length;
            byte[] data = new byte[1024];

            while ((length = resourceInputStream.read(data, 0, data.length)) != -1) {
                byteArrayOutputStream.write(data, 0, length);
            }

            byteArrayOutputStream.flush();

            byte[] byteArray = byteArrayOutputStream.toByteArray();

            return new String(byteArray);

        } catch (IOException ioe) {
            throw new UncheckedIOException("Failed to read source file: " + name, ioe);
        }
    }

    static class Result {

        final Class<?> clazz;

        Result(Class<?> clazz) {
            super();
            this.clazz = clazz;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((this.clazz == null) ? 0 : this.clazz.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            Result other = (Result) obj;
            if (this.clazz == null) {
                if (other.clazz != null) {
                    return false;
                }
            } else if (!this.clazz.equals(other.clazz)) {
                return false;
            }
            return true;
        }

        @Override
        public String toString() {
            return "Result [clazz=" + this.clazz + "]";
        }

        static class Found extends Result {

            final String source;

            final String path;

            Found(Class<?> clazz, String path, String source) {
                super(clazz);
                this.path = path;
                this.source = source;
            }

            @Override
            public int hashCode() {
                final int prime = 31;
                int result = super.hashCode();
                result = prime * result + ((this.source == null) ? 0 : this.source.hashCode());
                return result;
            }

            @Override
            public boolean equals(Object obj) {
                if (this == obj) {
                    return true;
                }
                if (!super.equals(obj)) {
                    return false;
                }
                if (getClass() != obj.getClass()) {
                    return false;
                }
                Found other = (Found) obj;
                if (this.source == null) {
                    if (other.source != null) {
                        return false;
                    }
                } else if (!this.source.equals(other.source)) {
                    return false;
                }
                return true;
            }

            @Override
            public String toString() {
                return "Found [source=" + this.source + ", clazz=" + this.clazz + "]";
            }

        }

        static class NotFound extends Result {

            NotFound(Class<?> clazz) {
                super(clazz);

            }

            @Override
            public String toString() {
                return "NotFound [clazz=" + this.clazz + "]";
            }

        }
    }
}
Jeff
  • 3,712
  • 2
  • 22
  • 24
0

You need a Java Decompiler if you want to get something similar to the original source code in Java.

You won't be able to access the source code (your executable program is made of Java bytecode, not Java Source code).

Here's a link to my favorite Java Decompiler.

Pablo Santa Cruz
  • 176,835
  • 32
  • 241
  • 292