gwtproject / gwt

GWT Open Source Project
http://www.gwtproject.org
1.52k stars 374 forks source link

Classes with static initializers are not included in compiled output #9510

Closed Flamenco closed 7 years ago

Flamenco commented 7 years ago

GWT version:2.8 Browser (with version):All *Operating System:All*


Description

I have created several classes that have a static initializer and register themselves by interface in a service manager. They are consumed by calling the service manager and using the registered instantiation.

The classes are not getting included in the compile because I have essentially removed dependencies to them.

It seems an annotation to force these classes to be included would be appropriate, Or perhaps the static initializer should be enough to trigger the inclusion, as it references the class CTOR.

Steps to reproduce
public class CodeFormatter implements Goody {
    static {
        ServiceManager.getInstance().register(Goody.class, new CodeFormatter());
    } 
    @Override
    public void process(TextBoxBase textBox, CanRender renderer) {
    }
}
Known workarounds

In a class that is included, call new on each class.

Links to further discussions
tbroyer commented 7 years ago

Per the Java Language Specification, static initializers are only executed when the class is… initialized, which basically only happens when it's "reached" by some code (before it can execute itself). See https://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html#jls-12.4.1

If nothing in your code ever references the CodeFormatter class explicitly triggering its initialization, then its static initializer won't even be called. So, the behavior you're experiencing is expected.

Flamenco commented 7 years ago

So how about an annotation?

Flamenco commented 7 years ago

BTW I do see now that my calls to new are triggering the static block.

tbroyer commented 7 years ago

An annotation by itself wouldn't be enough, and an annotation specific to GWT is out of the question : GWT follows Java here. An annotation could however trigger an annotation processor that could generate some code to explicitly initialize the annotated classes, but the generated code would still have to be called somehow.

Your best bet is to refactor your code: you could use a static method instead of the static initializer (and call those methods from somewhere, or move all your registrations to the key classes (Goody, rather than CodeFormatter).
Either that or the annotation processor approach (which could then somehow mimick the java.util.ServiceLoader for your classes).
I'd personnally recommend explicit initialization, as otherwise you might waste time debugging why your annotation processor skips a class or another and doesn't generate the expected code.

Flamenco commented 7 years ago

Ultimately, I am going for something like this (OSGi DS)

@Component
class Service1 implements Service
{
}

I think your idea to

trigger an annotation processor that could generate some code to explicitly initialize the annotated classes

would work here, as I could generate a single file that registers all the components and then activate them by calling the generated class after the module is loaded.

Doing this would mean I could simply drop in components that have no dependencies to themselves and only a single dependency to the annotation. I think that is better than having to manually add each of them and explicitly call a method.

Flamenco commented 7 years ago

So I created an annotation processor that works like this:

@Init
public class MyInit {
    public static void init() {
        Logger.getGlobal().info("** My Init was called");
    }
}
@Init("hello")
public class MyInit2 {
    public static void hello() {
        Logger.getGlobal().info("** My Init2 was called");
    }
}

I added a module and entry point to the jar that declares the annotation, and it calls the generated init file (in the main project) on startup. So this solves:

  1. Call static initializer on startup.
  2. Include a class that is not referenced.

It would be nice if this was built into the core, and called before ANY other modules. @tbroyer Should I assume that your comment that "an annotation specific to GWT is out of the question" means I should not submit my work to the GWT project?

ibaca commented 7 years ago

So you have added a module, an entry point, a generator, an annotation... and lot of obscure magic 😉 just to avoid this line in your code... IMO use just this line in your APP code.

ServiceManager.getInstance().register(Goody.class, new CodeFormatter());

In 2 years you will appreciate those explicit references 😜. Even if you are really motivated to leave this code in the lib, you can crearte a kind of ServiceManager module using ~lambdas~ one static method.

// lib code
public class CodeFormatter implements Goody {
    public static void module(ServiceManager sm) { sm.register(Goody.class, new CodeFormatter()); } 
    @Override public void process(TextBoxBase textBox, CanRender renderer) { }
}
// app code (some bootstrapping class)
CodeFormatter.module(ServiceManager.getInstance());
// or with minimal change in ServiceManager (register accept Consumer<ServiceManager>)
ServiceManager.getInstance().register(CodeFormatter::module);
Flamenco commented 7 years ago

@ibaca I have also implemented the following annotation. I did not post it because the topic was not about 'service registry'. It was about adding GWT code that is initialized without any direct references. Anyway, the beauty of this approach is that I can add and remove additional services without any references from any other code, and with only a single dependency on 1 annotation. I can even move a service to another GWT module without having to manually update a static file.

BTW I was using one static method explicitly called from my entry point, and given I have over 300 of these classes, it was a PITA to maintain. So yes, there was much effort, but thats all in the past and now I can enjoy the fruits of my labor. Definitely worth the few hours learning about Annotation Processors!

And going forward I can do all sorts of magic, such as using factories to lazy load the services and add key/value map to the annotation to attached information for filtering and prioritizing the services. I have been using OSGi DS for years, and have never regretted it. This use case is exactly what annotations are meant for.

@Component

Here is the usage

@Component
public class CodeFormatter implements Goody {
    @Override
    public String getIcon() {
        return "images/menu/format.png";
    }

    @Override
    public void process(RenderedDocumentInfo currentModel) {     
    }

    @Override
    public String getCaption() {
        return "Format";
    }
}

Here is the generated code that is called automatically (could be refactored a bit...)

package tv.twelvetone.gwt.component.client;

import tv.twelvetone.gwt.quickchords.client.service.ServiceManager;

public class GwtComponentService {

    static public void register() {
        ServiceManager.getInstance().register("tv.twelvetone.gwt.quickchords.client.goodies.Goody", new tv.twelvetone.gwt.quickchords.client.goodies.RemoveLyrics());
        ServiceManager.getInstance().register("tv.twelvetone.gwt.quickchords.client.goodies.Goody", new tv.twelvetone.gwt.quickchords.client.goodies.TextBlockPageBreak());
        ServiceManager.getInstance().register("tv.twelvetone.gwt.quickchords.client.goodies.Goody", new tv.twelvetone.gwt.quickchords.client.dialogs.FindReplaceDlg());
        ServiceManager.getInstance().register("tv.twelvetone.gwt.quickchords.client.goodies.Goody", new tv.twelvetone.gwt.quickchords.client.goodies.ToggleLyrics());
        ServiceManager.getInstance().register("tv.twelvetone.gwt.quickchords.client.goodies.Goody", new tv.twelvetone.gwt.quickchords.client.goodies.QuickCapoGoody());
        ServiceManager.getInstance().register("tv.twelvetone.gwt.quickchords.client.goodies.Goody", new tv.twelvetone.gwt.quickchords.client.goodies.EmailUsGoodie());
        ServiceManager.getInstance().register("tv.twelvetone.gwt.quickchords.client.goodies.Goody", new tv.twelvetone.gwt.quickchords.client.goodies.ReportBugGoodie());
        ServiceManager.getInstance().register("tv.twelvetone.gwt.quickchords.client.goodies.Goody", new tv.twelvetone.gwt.quickchords.client.goodies.BadRenderGoodie());
        ServiceManager.getInstance().register("tv.twelvetone.gwt.quickchords.client.goodies.Goody", new tv.twelvetone.gwt.quickchords.client.goodies.CompareTo());
        ServiceManager.getInstance().register("tv.twelvetone.gwt.quickchords.client.goodies.Goody", new tv.twelvetone.gwt.quickchords.client.goodies.FormAnalyzer());
        ServiceManager.getInstance().register("tv.twelvetone.gwt.quickchords.client.goodies.Goody", new tv.twelvetone.gwt.quickchords.client.goodies.Compare());
        ServiceManager.getInstance().register("tv.twelvetone.gwt.quickchords.client.goodies.Goody", new tv.twelvetone.gwt.quickchords.client.goodies.CodeFormatter());
        ServiceManager.getInstance().register("tv.twelvetone.gwt.quickchords.client.goodies.Goody", new tv.twelvetone.gwt.quickchords.client.goodies.FullScreen());
    }
}
tbroyer commented 7 years ago

It would be nice if this was built into the core, and called before ANY other modules. @tbroyer Should I assume that your comment that "an annotation specific to GWT is out of the question" means I should not submit my work to the GWT project?

That's right; specifically because we don't want things like:

the beauty of this approach is that I can add and remove additional services without any references from any other code

We have this issue with GWT-RPC already, where your generated code will be different (understand: could be unnecessarily bloated) depending on which classes are present in your classpath, whether you use them or not.
The same is true in your case : add a JAR to your classpath and you suddenly get possibly tens of things registered for your, with all their dependencies linked in, whether you'll actually use them or not (because the GWT compiler won't optimize things out just because they are never looked-up in a map).

GWT is about generating code that's as optimized as possible; your proposal is a recipe for failure, as anything that puts developer experience before user experience.

See http://www.gwtproject.org/makinggwtbetter.html, specifically:

Sometimes there are conflicts between what is easy for us developers and what benefits end users the most. When the two are in conflict, end user experience almost always wins.

Flamenco commented 7 years ago

Thanks for the explaination @tbroyer. I'm glad I asked so as to not 'bark up the wrong tree'. I do think however that the developer can always look at the generated code and determine what is best for the user experience, with the option to edit and manually call the initialization if needed. As my initial example had a static initializer, that is also out of the developers control and will be called (although not before the module entry). It will also pull in the same dependencies.