Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug?? Not resolving CtExecutableReference type and declared type depending on environment (Docker and local Spring Boot) #3926

Closed
sok82 opened this issue May 14, 2021 · 9 comments

Comments

@sok82
Copy link

sok82 commented May 14, 2021

Good day.

I encountered very strange issue using Spoon analysis library in my Java project.
I get different parsing results when running same code, that uses Spoon Launcher in different environments

  • 1 Environment - Spring Boot Project that is run locally from Intellij IDEA
  • 2 Environment - Same Spring Boot project that is run in a Docker Container

In locally run Spring Boot project - it's all OK, but when I run the same code in Docker - I get null values in CtExecutableReference.getType() and CtExecutableReference.getDeclaredType()

Here are the details
My Spoon version is 8.2.0. (from maven repo)

I am trying to parse (build AST) of code from this GitHub repository
And I have troubles parsing this class
There are following lines here

...
@Service
public class ValueServices {
	
	private ValuesRepository valuesRepository;
	
	private Queue<Values> queue;

	@Autowired
	public ValueServices(ValuesRepository valuesRepository) {
		super();
		this.valuesRepository = valuesRepository;
		this.queue = new LinkedList<Values>();
	}
	
	public List<Values> getAllValues() {
		List<Values> values = new ArrayList<>();
		this.valuesRepository.findAll().forEach(values::add);
		return values;
	}

...
}

When I run analysis and try to parse CtExecutableReference for findAll() method of this.valuesRepository.findAll().forEach(values::add) statement I get null values for getType() and getDeclaredType() when running my project in Docker.
When running locally both getType() and getDeclaredType() have non-null values

The same issue happens when parsing other similar code blocks in other projects.
For example here

@Service
public class BetService {

	public static final String DATE_FORMAT_NOW = "yyyy-MM-dd HH";
	public static final String DATE_FORMAT_NOW_WITH_HOUR_MIN = "yyyy-MM-dd HH:mm:ss";

	private BetRepository betRepository;

	@Autowired
	public BetService(BetRepository betRepository) {
		super();
		this.betRepository = betRepository;
	}

	public List<Bet> getAllBets() {
		List<Bet> bets = new ArrayList<Bet>();
		this.betRepository.findAll().forEach(bets::add);

		return bets;
	}
}

Statement with this.betRepository.findAll() has null in both getType and getDeclaredType when running in Docker, but ok in local environment.

At the same time following code is parsed well in both environments

public class BetRepositoryTest {
	
	@Autowired
    private TestEntityManager entityManager;
 
    @Autowired
    private BetRepository betRepository;

	@Test
	public void test() {
		Bet bet = new Bet("2018-07-06 12:56", "WIN", 103333, 1082, 500.5);
		entityManager.persist(bet);
	    entityManager.flush();
	   
	    List<Bet> bets = betRepository.findByCustomerId(bet.getCustomerId());	    
	    assertThat(bet.getCustomerId() == bets.get(0).getCustomerId());
	}
}

and statement betRepository.findByCustomerId() is parsed ok and has necessary type info both in Docker and n local Spring Boot run.

I double checked local tests - and all is ok - when running code in tests from IDE or starting Spring Boot project from IDE and initialising analysis by calling service from Web UI - it OK and works as expected.

But when I build Docker image - I get null in both type and declaredType.

I'm running Spoon analysis with the following code

private SourceCodeMetamodel buildMetamodelForFiles(Collection<File> javaFiles) {
        Launcher spoonAPI = new Launcher();
        log.debug("Spoon environment - {}",ToStringBuilder.reflectionToString(spoonAPI.getEnvironment()));
        log.debug("Spoon model builder - {}",ToStringBuilder.reflectionToString(spoonAPI.getModelBuilder()));
        Set<String> inputResources = new HashSet<>();
        for (File javaFile: javaFiles) {
            String javaDir = JavaFileUtils.getJavaFileStorageRootPath(javaFile);
            if (StringUtils.isNotBlank(javaDir) && !inputResources.contains(javaDir)) {
                spoonAPI.addInputResource(javaDir);
                inputResources.add(javaDir);
            }
            else if (StringUtils.isBlank(javaDir)) {
                spoonAPI.addInputResource(javaFile.getAbsolutePath());
            }
        }
        spoonAPI.buildModel();
        CtModel ctModel = spoonAPI.getModel();
        Collection<CtType<?>> modelTypes = ctModel.getAllTypes();
        return new SpoonSourceCodeMetamodel(modelTypes,false);
    }

Before running Launcher I tried to print it's settings. Here is what i got

2021-05-14 13:26:12.329 DEBUG 1 --- [         task-1] c.s.s.r.i.s.SpoonJavaSourceCodeAnalyzer  : Spoon environment - spoon.support.StandardEnvironment@4626a7ce[errorCount=0,processingStopped=false,prettyPrintingMode=FULLYQUALIFIED,warningCount=0,sourceClasspath=<null>,preserveLineNumbers=false,copyResources=true,enableComments=true,level=ERROR,shouldCompile=false,skipSelfChecks=false,complianceLevel=8,previewFeaturesEnabled=false,outputType=classes,noclasspath=true,compressionType=GZIP,sniperMode=false,ignoreDuplicateDeclarations=false,prettyPrinterCreator=<null>,useTabulations=false,tabulationSize=4,binaryOutputDirectory=/spooned-classes]

2021-05-14 13:26:12.333 DEBUG 1 --- [         task-1] c.s.s.r.i.s.SpoonJavaSourceCodeAnalyzer  : Spoon model builder - spoon.support.compiler.jdt.JDTBasedSpoonCompiler@2df98092[environment=<null>,probs=[],requestor=spoon.support.compiler.jdt.TreeBuilderRequestor@57050733,factory=spoon.reflect.factory.FactoryImpl@237d7ffa,javaCompliance=7,sources=<virtual folder>: spoon.support.compiler.VirtualFolder@42618617,templates=<virtual folder>: spoon.support.compiler.VirtualFolder@7fb32ea2,templateClasspath={},compilationUnitFilters=[],sortList=true]

I'm running project on Java8 - in both environments (details following).
To build docker I use following commands

FROM java:8
COPY maven /maven/
ENTRYPOINT java -Xverify:none -XX:TieredStopAtLevel=1 -XX:+TieredCompilation -XX:+UseSerialGC -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=40 -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Djava.security.egd=file:/dev/./urandom -Dspring.profiles.active=${PROFILE:-docker-dev} -jar /maven/skillcounters-sca-service-1.0-SNAPSHOT.jar

I tried to switch to different Docker base images (openjdk \ alpine etc.), but nothing helped.
I tried to exclude all java run options listed above (i.e. like -XXblabla) - didn't helped either.

To get ideas about what may go wrong i print all environment (including java) data on application start.

Here what is printed for local environment

Apple_PubSub_Socket_Render : /private/tmp/com.apple.launchd.xCrhs0tTMM/Render
COMMAND_MODE : unix2003
HOME : /Users/sk
JAVA_MAIN_CLASS_66239 : org.codehaus.classworlds.Launcher
JAVA_MAIN_CLASS_66249 : com.skillcounters.sca.SCAServiceApplication
LANG : ru_RU.UTF-8
LC_CTYPE : ru_RU.UTF-8
LOGNAME : sk
PATH : /Users/sk/Develop/d20/db/liquibase:/Users/sk/anaconda/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
PID : 66249
PWD : /Users/sk/Develop/d20/d20-git-repo/skillcounters-sca-service/skillcounters-sca-service-impl
SECURITYSESSIONID : 186a9
SHELL : /bin/bash
SSH_AUTH_SOCK : /private/tmp/com.apple.launchd.ItaWSltKcA/Listeners
TMPDIR : /var/folders/lz/gjd4j2t12_39qs0hpdjqd3sh0000gn/T/
USER : sk
XPC_FLAGS : 0x0
XPC_SERVICE_NAME : com.apple.xpc.launchd.oneshot.0x10000002.idea
__CF_USER_TEXT_ENCODING : 0x1F5:0x0:0x0
awt.toolkit : sun.lwawt.macosx.LWCToolkit
file.encoding : UTF-8
file.encoding.pkg : sun.io
file.separator : /
ftp.nonProxyHosts : local|*.local|169.254/16|*.169.254/16
gopherProxySet : false
http.nonProxyHosts : local|*.local|169.254/16|*.169.254/16
java.awt.graphicsenv : sun.awt.CGraphicsEnvironment
java.awt.headless : true
java.awt.printerjob : sun.lwawt.macosx.CPrinterJob
java.class.path : {all dependent jars go here - excluded them not to pollute issue...}
java.class.version : 52.0
java.endorsed.dirs : /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/endorsed
java.ext.dirs : /Users/sk/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
java.home : /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre
java.io.tmpdir : /var/folders/lz/gjd4j2t12_39qs0hpdjqd3sh0000gn/T/
java.library.path : /Users/sk/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.
java.runtime.name : Java(TM) SE Runtime Environment
java.runtime.version : 1.8.0_131-b11
java.specification.name : Java Platform API Specification
java.specification.vendor : Oracle Corporation
java.specification.version : 1.8
java.vendor : Oracle Corporation
java.vendor.url : http://java.oracle.com/
java.vendor.url.bug : http://bugreport.sun.com/bugreport/
java.version : 1.8.0_131
java.vm.info : mixed mode
java.vm.name : Java HotSpot(TM) 64-Bit Server VM
java.vm.specification.name : Java Virtual Machine Specification
java.vm.specification.vendor : Oracle Corporation
java.vm.specification.version : 1.8
java.vm.vendor : Oracle Corporation
java.vm.version : 25.131-b11

And here is what printed in Docker environment

CA_CERTIFICATES_JAVA_VERSION : 20140324
HOME : /root
HOSTNAME : e297584466e8
JAVA_DEBIAN_VERSION : 8u111-b14-2~bpo8+1
JAVA_HOME : /usr/lib/jvm/java-8-openjdk-amd64
JAVA_VERSION : 8u111
LANG : C.UTF-8
PATH : /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PID : 8
PROFILE : prod
PWD : /
awt.toolkit : sun.awt.X11.XToolkit
file.encoding : UTF-8
file.encoding.pkg : sun.io
file.separator : /
java.awt.graphicsenv : sun.awt.X11GraphicsEnvironment
java.awt.headless : true
java.awt.printerjob : sun.print.PSPrinterJob
java.class.path : /maven/skillcounters-skill-service-1.0-SNAPSHOT.jar
java.class.version : 52.0
java.endorsed.dirs : /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/endorsed
java.ext.dirs : /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/ext:/usr/java/packages/lib/ext
java.home : /usr/lib/jvm/java-8-openjdk-amd64/jre
java.io.tmpdir : /tmp
java.library.path : /usr/java/packages/lib/amd64:/usr/lib/x86_64-linux-gnu/jni:/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu:/usr/lib/jni:/lib:/usr/lib
java.protocol.handler.pkgs : org.springframework.boot.loader
java.runtime.name : OpenJDK Runtime Environment
java.runtime.version : 1.8.0_111-8u111-b14-2~bpo8+1-b14
java.security.egd : file:/dev/./urandom
java.specification.name : Java Platform API Specification
java.specification.vendor : Oracle Corporation
java.specification.version : 1.8
java.vendor : Oracle Corporation
java.vendor.url : http://java.oracle.com/
java.vendor.url.bug : http://bugreport.sun.com/bugreport/
java.version : 1.8.0_111
java.vm.info : mixed mode
java.vm.name : OpenJDK 64-Bit Server VM
java.vm.specification.name : Java Virtual Machine Specification
java.vm.specification.vendor : Oracle Corporation
java.vm.specification.version : 1.8
java.vm.vendor : Oracle Corporation
java.vm.version : 25.111-b14

Any help would be appreciated

@sok82
Copy link
Author

sok82 commented May 14, 2021

I found some clue when I enabled Spoon Launcher logging by setting it to 'ALL'

Here is what I got in Docker environment

...
The method findAll() is undefined for the type ValuesRepository at /opt/skillcounters/filestorage/github/vipinjo/assignment-consumer/work-tree/d4e050cf3566981ba386e336c5889f8d107abfbb/src/main/java/com/vipinjoseph/assignmentconsumer/service/ValueServices.java:32

And in local environment these warning are not shown...

Why is that?

Is there some issue with classpath i.e. spring classes (CrudRepository for example) is not available for Spoon when running in Docker?

How can I fix it?

@sok82
Copy link
Author

sok82 commented May 15, 2021

Next try.
I added directory containing all necessary jars into classpath but in logs I got

2021-05-15 07:58:28.426  WARN 7 --- [         task-1] c.s.s.r.i.s.SpoonJavaSourceCodeAnalyzer  : Added following directories to Spoon Environment Classpath : [/opt/skillcounters/sca/lib]
2021-05-15 07:58:29.685  WARN 7 --- [         task-1] spoon.Launcher                           : You specified the directory /opt/skillcounters/sca/lib in source classpath, please note that only class files will be considered. Jars and subdirectories will be ignored.

How can I include jars that are contained in that directory?

@sok82
Copy link
Author

sok82 commented May 15, 2021

Made another try with setting InputClassloader like this

...
 URLClassLoader urlClassLoader = URLClassLoader.newInstance(
                                                                existingDirs.stream()
                                                                    .map(this::toUrl)
                                                                    .toArray(URL[]::new)
                                                );
spoonAPI.getEnvironment().setInputClassLoader(urlClassLoader);
...

where existingDirs contains path to directory with all necessary libs in a form of *.jar files.

In logs I see that input classloader was set (classpath is initialized which is done by StandardEnvironment when I set InputClassloader)

spoon.support.StandardEnvironment@400e5d30[errorCount=0,processingStopped=false,prettyPrintingMode=FULLYQUALIFIED,warningCount=0,
sourceClasspath={/opt/skillcounters/sca/lib/}
,preserveLineNumbers=false,copyResources=true,enableComments=true,level=DEBUG,shouldCompile=false,skipSelfChecks=false,complianceLevel=8,previewFeaturesEnabled=false,outputType=classes,noclasspath=true,compressionType=GZIP,ignoreDuplicateDeclarations=false,prettyPrinterCreator=,useTabulations=false,tabulationSize=4,binaryOutputDirectory=/spooned-classes]

But no effect - again Spoon can't find necessary jars and\or include it to classpath.

BTW - I already switched version of spoon to 9.0.0. because in earlier versions InputClassLoader was not set if it's a URLClassLoader (fixed here - #3818)

Well, I'm stuck... (((

@sok82
Copy link
Author

sok82 commented May 15, 2021

At last I found a solution

  1. Use Spoon 9.0.0 or later with this fix
  2. Create classloader with a parent classloader from Spoon Launcher
  3. Directly include each *.jar file into classloader URL classpath (not a directory containing jars but each jar in a single URL passed to classloader)
  4. Set this classloader as an InputClassLoader
                URLClassLoader urlClassLoader = URLClassLoader.newInstance(
                                                                jarUrls.toArray(new URL[0])
                                                                ,spoonAPI.getClass().getClassLoader()
                                                );
                spoonAPI.getEnvironment().setInputClassLoader(urlClassLoader);

I strongly advice you to put this in docs for those like me are struggling with using spoon packaged in a jar application (for example Spring Boot)

@sok82 sok82 closed this as completed May 15, 2021
@slarse
Copy link
Collaborator

slarse commented May 20, 2021

Hi @sok82,

Thanks for the detailed summary of your findings, and sorry about the slowpoke response to your troubles.

You should not have to set a custom classloader just to resolve types. General classpath resolution is explained here under Resolution of elements and classpath. There are a few ways to get the correct classpath, I'll walk through some options.

First of all, to ensure that Spoon has all types in your project, you'll want to run Spoon in classpath mode. It will cause Spoon to scream bloody murder if there's an unresolved type.

Launcher launcher = new Launcher();
launcher.getEnvironment().setNoclasspath(false);

Then, you'll need to set the classpath. This can be done manually with Environment.setSourceClasspath(String[]). However, it seems like the project you're analyzing is a Maven project, and then I'd recommend using the MavenLauncher. It runs in classpath mode by default, and automatically resolves the classpath and sources for you.

String mavenProjectRoot = ...; // path to the directory with the pom.xml
MavenLauncher launcher = new MavenLauncher(mavenProjectRoot, MavenLauncher.SOURCE_TYPE.APP_SOURCE);
CtModel model = launcher.buildModel();

See The MavenLauncher class here for more details.

Hope that helps!

@sok82
Copy link
Author

sok82 commented May 31, 2021

Hi @slarse.

Thanks a lot for your comments.

Fortunately I managed to fix all that was necessary by myself - the way that I described above with custom classloader.

I'm sure, that solution that you provided suits some 'standard' scenario, but not my case.
I'm planning to analyze multiple projects, so I just can't be always sure that there would be a pom.xml file. At the same time most of the projects I analyze will depend on some 'standard' libs like Spring, etc. So having some bucket where I can place all necessary jars looks pretty good for me

I tried to use setNoClasspath(false), but that didn't help to resolve issue with dependencies in external jars.
Actually I examined Spoon source code and method like Environment.setSourceClasspath(String[]) are not intended to work with jar dependencies - there is a warning 'Jars and subdirectories will be ignored' thrown here concerning jars in that path.

It's here in StandardEnvironment.java (as of version 9)

    private void verifySourceClasspath(String[] sourceClasspath) throws InvalidClassPathException {
        String[] var2 = sourceClasspath;
        int var3 = sourceClasspath.length;

        for(int var4 = 0; var4 < var3; ++var4) {
            String classPathElem = var2[var4];
            File classOrJarFolder = new File(classPathElem);
            if (!classOrJarFolder.exists()) {
                throw new InvalidClassPathException(classPathElem + " does not exist, it is not a valid folder");
            }

            if (classOrJarFolder.isDirectory()) {
                SpoonFolder tmp = new FileSystemFolder(classOrJarFolder);
                List<SpoonFile> javaFiles = tmp.getAllJavaFiles();
                if (!javaFiles.isEmpty()) {
                    this.print("You're trying to give source code in the classpath, this should be given to addInputSource " + javaFiles, Level.WARN);
                }

                this.print("You specified the directory " + classOrJarFolder.getPath() + " in source classpath, please note that only class files will be considered. Jars and subdirectories will be ignored.", Level.WARN);
            } else if (classOrJarFolder.getName().endsWith(".class")) {
                throw new InvalidClassPathException(".class files are not accepted in source classpath.");
            }
        }

    }

In any case my solution with classloader works, so I'm happy with it)

Maybe be there is some caveat with it of which I do not know?

Thanks a lot for your collaboration

@slarse
Copy link
Collaborator

slarse commented May 31, 2021

Fortunately I managed to fix all that was necessary by myself - the way that I described above with custom classloader.

I'm sure, that solution that you provided suits some 'standard' scenario, but not my case.
I'm planning to analyze multiple projects, so I just can't be always sure that there would be a pom.xml file. At the same time most of the projects I analyze will depend on some 'standard' libs like Spring, etc. So having some bucket where I can place all necessary jars looks pretty good for me

Then I would use setSourceClasspath. It looks to me like your custom classloader just uses the URLClassLoader with paths to the JARs, which is roughly equivalent to just setting the classpath for Spoon's default loader (which is URLClassLoader).

I tried to use setNoClasspath(false), but that didn't help to resolve issue with dependencies in external jars.

This is a safety measure, it doesn't find the classpath for you. It's just to ensure that all types are actually available. If you require all types to be resolved, you should always run in classpath mode, because then you'll be alerted if a type is not resolved. In the default noclasspath mode, you'll just sporadically find null when trying to get a type, or other strangeness. Noclasspath mode is just inherently less stable than classpath mode.

Actually I examined Spoon source code and method like Environment.setSourceClasspath(String[]) are not intended to work with jar dependencies - there is a warning 'Jars and subdirectories will be ignored' thrown here concerning jars in that path.

You've taken the warning out of context: it will only appear if you add the path to a directory, that code path cannot be entered given the path directly to a JAR file. If you have a look at the docs, they say the following:

* Sets the source class path of the Spoon model.
* After the class path is set, it can be retrieved by
* {@link #getSourceClasspath()}. Only .jar files or directories with *.class files are accepted.
* The *.jar or *.java files contained in given directories are ignored.

In particular:

Only .jar files or directories with *.class files are accepted.
The *.jar or *.java files contained in given directories are ignored.

Which says that if you add the path to a directory containing jar files, those jar files are ignored. Jar files are only included if you add the full path to that jar. It's the same way providing the classpath to e.g. javac works.

In any case my solution with classloader works, so I'm happy with it)

You're free to keep using it, of course! I just wanted to add for completeness that setting the source classpath is the intended solution to your problem. I.e. this should work fine:

spoonAPI.getEnvironment().setSourceClasspath(urls.stream().map(URL::toString).toArray(String[]::new));

Maybe be there is some caveat with it of which I do not know?

Classloaders are kind of complicated, and I don't claim to have a full grasp on how the classloader inheritance works in practice (you're setting the default classloader as a parent), but there shouldn't be any problems with your solution.

@sok82
Copy link
Author

sok82 commented May 31, 2021

Thanks for clarification @slarse.

Now I see where I lost context and what I could do wrong.

I don't like playing with classloaders also) This seems like some magic for me sometimes - need to keep fingers crossed..
So when I have free time I'll try to use your advice - maybe I missed something when I was struggling with this before.

Thanks a lot and have a nice coding

@slarse
Copy link
Collaborator

slarse commented May 31, 2021

You're welcome, hope it all works out to your liking!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants