gluonhq / scenebuilder

Scene Builder is a visual, drag 'n' drop, layout tool for designing JavaFX application user interfaces.
https://gluonhq.com/products/scene-builder/
Other
732 stars 218 forks source link

ClassDefNotFoundError when importing custom component #281

Open dubaut opened 4 years ago

dubaut commented 4 years ago

As suggested on StackOverflow I build SceneBuilder from master and tried out the feature apparently merged with #228.

First, everything looked fine but after all, it did not load my custom component. I then debugged it in IntelliJ and found out that I get a ClassDefNotFoundError for all .class files in the folder I provided, even though these classes clearly exist.

Java
11.0.6+10-LTS, Azul Systems, Inc.

Operating System
Windows 10, amd64, 10.0

Does anyone have the same issue?

Plus: I am not sure if it is a good thing that the application fails silently without telling the user why loading a custom component failed.

dubaut commented 4 years ago

Okay, after playing around today I realized that the base folder must contain a directory hierarchy that represents the package structure of the controls to be imported. If not, nothing happens.

It would be great if the user would get an indication of what's going on and maybe hints on how to solve the problem.

nathanwonnacott commented 3 years ago

I agree that it would be nice to get some feedback on why a custom component was not loaded. I wrote a little utility that sometimes helps me debug these sorts of issues (though it doesn't catch every case):


 * This is a debugging utility to help troubleshoot issue when SceneBuilder won't load components
 * exported via {@link StatePropertyBeanInfoCodeGenerator}.
 * </br></br>
 * It will call SceneBuilder's loading code so that you can debug it and view what exceptions are thrown.
 * </br></br>
 * Currently, this calls the portion of the code that attempts to load a specific class, however, the loading
 * could fail before that step in the process (it may not even attempt to load that file). This class could
 * be expanded to run the code that searches for classes to load as well, but for now it doesn't simply because
 * I made this to debug one issue and I didn't need that capability yet.
 * 
 * </br></br>
 * Usage:
 * <pre>
 * SceneBuilderJarFileLoadTest pathToSceneBuilderDistJarFile pathToExportedControlsJarFile classNameToTest [classNameToTest ...]
 * </pre>
 * Note that if you are using an installed version of scene builder, then the jar file will be app/dist.jar in the SceneBuilder install folder

 * </br></br>
 * This must be run with a java version newer or equal to the version of java that was used to compile the scene builder
 * jar. Currently, our practice is to use a version of SceneBuilder which is newer than our current java version. You may need
 * to download a Java 8 version of the scene builder jar file. If you find that your class loads in Java 8 scene builder, but
 * not a newer version, then you'll have to build this class in Java 12. I haven't figured out exactly how to get Java 12 to work
 * with eclipse very well. I eventually got it to build (not sure how), but could only run it from the command line. I'll add the
 * command that I used to run it here just in case it's useful later, but there's certainly a better way to run it:
 * 
 * <pre>
 * alias java13=<path to java 13 java.exe file>
 * export PATH_TO_FX=<wherever you downloaded your new version of FX .jar files>
 * java13 --module-path $PATH_TO_FX --add-modules javafx.graphics,javafx.swing,javafx.controls,javafx.fxml -cp /c/Users/e306477/Eclipse2019_RCAT_workspace/Try3FullOnModuleStuff/bin/ com.lmco.adp.utility.statechange.beaninfo.SceneBuilderJarFileLoadTest /c/Users/e306477/AppData/Local/SceneBuilder10/app/dist.jar /c/Users/e306477/Documents/FxControls.jar com.lmco.adp.utility.statechange.viewcomponents.StatefulCheckBox com.lmco.adp.rcat.antenna.gui.viewer.AntennaPatternGraphicPanel com.lmco.adp.rcat.antenna.gui.lists.AntennaList com.lmco.adp.rcat.antenna.gui.lists.AntennaPatternList
 * </pre>
 * 
 * 
 * @author Nate Wonnacott
 */
public class SceneBuilderJarFileLoadTest {

    @SuppressWarnings("unused")
    /**
     * The JarExplorer object in the SceneBuilder jar file. This isn't actually currently used, but it would be 
     * needed if we ever implement debugging the process of searching for class files to try to load. Since I 
     * already did the work to instantiate this class, I'm going to leave this in in case we ever decide to use
     * it later.
     */
    private Object jarExplorer;

    /**
     * Method to instantiate a component via the FXML loader.
     */
    private Method instantiateWithFXMLLoaderMethod;

    /**
     * Class loader using the exported controls .jar file as it's classpath
     */
    private ClassLoader controlsJarFileClassLoader;

    /**
     * Instantiates the testing class
     * @param sceneBuilderJar path to the scene builder distribution jar file
     * @param controlsFile path to the controls jar file for importing into scenebuilder
     */
    public SceneBuilderJarFileLoadTest(Path sceneBuilderJar, Path controlsFile) {

        URLClassLoader sceneBuilderClassLoader = null;

        try {
            URL[] sceneBuilderUrlArray = new URL[]{sceneBuilderJar.toUri().toURL()};

            sceneBuilderClassLoader = new URLClassLoader(sceneBuilderUrlArray);

            URL[] controlsFileUrlArray = new URL[]{controlsFile.toUri().toURL()};
            controlsJarFileClassLoader = new URLClassLoader(controlsFileUrlArray);

            Class<?> jarExplorerClass = sceneBuilderClassLoader.loadClass("com.oracle.javafx.scenebuilder.kit.library.util.JarExplorer");

            instantiateWithFXMLLoaderMethod = 
                    jarExplorerClass.getMethod("instantiateWithFXMLLoader", Class.class, ClassLoader.class);

            jarExplorer = jarExplorerClass.getConstructor(Path.class).newInstance(controlsFile);

        }
        catch(Exception e) {
            //There are various things that can go wrong, lets just wrap them all in an unchecked exception
            throw new RuntimeException(e);
        }
        finally {
            if(sceneBuilderClassLoader != null) {
                try {
                    sceneBuilderClassLoader.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }   
    }

    public boolean testLoadClass(String className) {

        System.out.println("Attempting to load " + className);

        try {
            Class<?> classToLoad = controlsJarFileClassLoader.loadClass(className);

            instantiateWithFXMLLoaderMethod.invoke(null, classToLoad, controlsJarFileClassLoader);
        }
        catch(Throwable t) {
            t.printStackTrace();
            return false;
        }

        return true;
    }

    public static void main(String args[]) {
        if(args.length < 3) {
            printUsage();
            System.exit(-1);
        }

        //In order to instantiate FX components, the FX toolkit must be set up
        FxSwingThreadUtilities.ensureFxToolkitIsInitialized();

        //Setting stdout to stderr to avoid the interleaving issues
        System.setErr(System.out);

        SceneBuilderJarFileLoadTest tester = new SceneBuilderJarFileLoadTest(
                new File(args[0]).toPath(), 
                new File(args[1]).toPath());

        int failures = (int)Arrays.stream(args)
                            .sequential()
                            .skip(2) //first 2 args aren't classnames
                            .map(tester::testLoadClass)
                            .filter(success -> !success)
                            .count();

        System.out.println(failures + " classes failed");

        //Make sure we call system exit since we instantiated the FX toolkit and we're not in an FX application. Otherwise, the
        //process will hang forever.
        System.exit(failures);

    }

    private static void printUsage() {
        System.out.println("Usage: " + SceneBuilderJarFileLoadTest.class.getCanonicalName() + 
                " pathToSceneBuilderJarFile pathToFxControlsImportJarFile classNameToTest [classNameToTest ...]");
    }
}```

The `FxSwingThreadUtilities.ensureFxToolkitIsInitialized();` is another utility within my project that just initializes the FX toolkit. It involves several other utilities and it would be a bit much to paste all of them here, but basically it just ensures that the FX toolkit is initialized by creating a JFXPanel and ensuring that it is not disposed.

Ultimately, though, we need a way within SceneBuilder to see why a class failed to load. This is a recurring problem that has cost me quite a few development hours.
Oliver-Loeffler commented 1 year ago

Hi @dubaut,

with which control did you try this, can you provide an example (FXML with custom control)?

Thanks!