'. '

DesignForJDK9

From APIDesign

(Difference between revisions)
Jump to: navigation, search
(Want {{JDK|java/beans|PropertyChangeListener}}? Get Swing with it!)
(Designing API for (small) JDK9)
(22 intermediate revisions not shown.)
Line 9: Line 9:
[[Modularity]] is good. As the original [[NetBeans Platform]] architect, I can't say anything else, right? [[Modularity]] is the sole reason we can practice [[DistributedDevelopment]] and release enormously large software products on a flexible schedule. On a schedule which each (distributed) team can choose by itself.
[[Modularity]] is good. As the original [[NetBeans Platform]] architect, I can't say anything else, right? [[Modularity]] is the sole reason we can practice [[DistributedDevelopment]] and release enormously large software products on a flexible schedule. On a schedule which each (distributed) team can choose by itself.
-
It is great that [[JDK]] has finally made its steps towards [[Modular Java SE]]. That means we no longer need to download [[CORBA]] and [[Swing]] when running our headless applications. The [[module]]s introduced in [[JDK]]9 allow us to do that properly and not just an ad-hoc solution as [[JDK]]8 profiles. Since now all [[library]] and [[framework]] architects have a new target to aim at as a runtime platform - the '''java.base''' module. How do you create one?
+
It is great that [[JDK]] has finally made its steps towards [[Modular Java SE]]. That means we no longer need to have [[CORBA]] and [[Swing]] around when running our headless applications. The [[module]]s introduced in [[JDK]]9 allow us to do that properly and not just an ad-hoc solution as [[JDK]]8 profiles. Since now all [[library]] and [[framework]] architects have a new target to aim at as a runtime platform - the '''java.base''' module. How do you create one?
<source lang="bash">
<source lang="bash">
Line 21: Line 21:
Modularization of a large monolithic application has to be associated with compromises. I experienced that while working on [[Modularization of NetBeans Platform]]. It comes with no surprise the [[Jigsaw]] modularization of [[JDK]] comes with compromises too. However one of them is really nasty, [[I]]'d say!
Modularization of a large monolithic application has to be associated with compromises. I experienced that while working on [[Modularization of NetBeans Platform]]. It comes with no surprise the [[Jigsaw]] modularization of [[JDK]] comes with compromises too. However one of them is really nasty, [[I]]'d say!
-
Do you follow the [[JavaBean]] specification? Probably you do: if you don't know you probably do that unconsciously. If you name getters and setter of your properties as ''setPropName'' and ''getPropName'', then you follow the [JavaBean]] spec.
+
Do you follow the [[JavaBean]] specification? Probably you do: if you don't know you probably do that unconsciously. If you name getters and setter of your properties as ''setPropName'' and ''getPropName'', then you follow the [[JavaBean]] spec.
Properties are one essential part of the [[JavaBean]]s. The other important part is about observing their changes. This is done by listeners. The [[JavaBean]] spec allows anybody to create their own listeners, but in addition to that it defines two general purpose ones: the {{JDK|java/beans|PropertyChangeListener}} and {{JDK|java/beans|VetoableChangeListener}}. Especially the first one is generally useful and often used in many [[API]]s that just expose:
Properties are one essential part of the [[JavaBean]]s. The other important part is about observing their changes. This is done by listeners. The [[JavaBean]] spec allows anybody to create their own listeners, but in addition to that it defines two general purpose ones: the {{JDK|java/beans|PropertyChangeListener}} and {{JDK|java/beans|VetoableChangeListener}}. Especially the first one is generally useful and often used in many [[API]]s that just expose:
Line 30: Line 30:
</source>
</source>
-
and allow anyone to observe changes in their properties. [[NetBeans]] APIs use that a lot. For example {{NB|org/openide/filesystems|FileSystem}} uses both - property change as well as and vetoable listeners - and the filesystems [[API]] doesn't have anything with [[UI]]. The same can be said about [[Spring]] [[framework]] - it also uses a lot of classes from ''java.beans'' package.
+
and allow anyone to observe changes in their properties. [[NetBeans]] APIs use that a lot. For example the {{NB|org-openide-filesystems|org/openide/filesystems|FileSystem}} class uses both - property change as well as and vetoable listeners - and the filesystems [[API]] doesn't have anything to do with UI. The same can be said about [[Spring]] [[framework]] - it also uses a lot of classes from ''java.beans'' package and has no relation to ([[Swing]] or [[AWT]]) UI at all.
 +
Alas, the '''java.beans''' package is going to be exposed from '''java.desktop''' [[JDK]]9 module! Try to use [[JavaBean]]s and get [[Swing]] with that!
 +
I know why the ''java.beans'' package ended up in the ''desktop'' [[Jigsaw]] module. Some of its classes (like {{JDK|java/beans|Applet}}) reference [[AWT]] classes. Moreover there is a cyclic dependency - a lot of classes from [[Swing]] & co. reference {{JDK|java/beans|PropertyChangeListener}} (obviously, as that is useful and adviced by [[JavaBean]] specification). As such it would be hard to separate ''java.beans'' package into own module. On the other hand we don't want all our [[API]]s that use {{JDK|java/beans|PropertyChangeListener}} to depend on [[Swing]] - the [[JDK]] team could have done a better job.
-
circular dependency,
+
Anyway, it looks like that the {{JDK|java/beans|PropertyChangeListener}} and related classes are going to be available only from ''java.desktop'' module. What can our projects ([[Spring]], [[Graal]], [[NetBeans]]) do with that?
-
other projects [[JAXB]] or [[Spring]]
+
== The Solution ==
== The Solution ==
-
'''require static'''
+
Of course, we could remove the '''addPropertyChangeListener''' and '''removePropertyChangeListener''' methods from our [[API]]s. Then we need no dependency on ''java.desktop'' [[Jigsaw]] module. However that isn't very [[BackwardCompatible]] and as author of [[TheAPIBook]] I always have to prefer a compatible solution. Luckily there is one!
-
Having {{JDK|java/beans|PropertyChangeListener}} in [[API]] signatures is OK. That means methods like
+
We can declare conditional only dependency on the desktop module:
 +
 
 +
'''require static java.desktop'''
 +
 
 +
Such dependency allows us to compile against classes from desktop (and thus also ''java.beans'' package), but the module doesn't have to be around when running our code. That means it is possible to have {{JDK|java/beans|PropertyChangeListener}} in the signatures of our [[API]] methods and methods like
<source lang="java">
<source lang="java">
Line 53: Line 58:
</source>
</source>
-
is fine. The ''Bean9'' class can still be loaded on the [[JDK]] containing just the ''java.base'' [[module]]. Following code can be used without any issues:
+
are fine. The ''Bean9'' class can still be loaded on the [[JDK]] containing just the ''java.base'' [[module]]. Following code can be used without any issues:
<source lang="java">
<source lang="java">
final class Main {
final class Main {
Line 64: Line 69:
}
}
</source>
</source>
-
the class can be instantiated, one can call its method ''say'' and (surprisingly) it is even possible to call the ''addPropertyChangeListener'' method with '''null''' parameter! [[JDK]] class verifier is fine with that.
+
the class can be instantiated, one can call its method ''say'' and (surprisingly) it is even possible to call the ''addPropertyChangeListener'' method with '''null''' parameter! [[JDK]] class verifier is fine with that. Heuréka! And thanks to the [[JDK]] team for having such a ''lazy'' verifier!
-
=== Implementing the Listener Support ===
+
Of course one has to be careful to use the ''java.beans'' classes only conditionally. When implementing the property change notifications, it is certainly not good idea to use {{JDK|java/beans|PropertyChangeSupport}} directly:
-
 
+
-
It is certainly not good idea to use {{JDK|java/beans|PropertyChangeSupport}} directly:
+
<source lang="java">
<source lang="java">
Line 105: Line 108:
</source>
</source>
-
however making the listener support lazy seems to work find:
+
obviously, there is no {{JDK|java/beans|PropertyChangeSupport}} class around, so we shall not attempt to create its instances. However making the listener support lazy and allocating it only when needed seems to work fine:
-
<source lang="bash">
+
<source lang="java">
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.beans.PropertyChangeSupport;
Line 137: Line 140:
</source>
</source>
-
Does not work with reflection. Once one tries to obtain the list of methods, an error is generated
+
Excellent. We can change our [[API]] to run on a [[JDK]]9 with only '''java.base''' module and still keep compatibility by having the property support conditionally ready.
 +
 
 +
== Designing [[API]] for (small) [[JDK]]9 ==
 +
 
 +
Of course, only people that add dependency on ''java.desktop'' module can actually use call the [[API]] that accepts {{JDK|java/beans|PropertyChangeListener}}. E.g. if one wants to observe what is happening in the sample ''Beans9'' class, one still needs to bring the whole [[Swing]] & co. in. Obviously, that is not what we want. We want people to have access to the full functionality even when running only with ''java.base'' module.
 +
 
 +
I believe the best solution is to design a dedicated listener - that is perfectly valid [[JavaBean]] approach and it can work with ''java.base'' module (because the {{JDK|java/util|EventListener}} and {{JDK|java/util|EventObject}} are both in the ''java.base'' [[Jigsaw]] module). Thus I suggest to enhance the ''Beans9'' example with two more classes:
 +
 
 +
<source lang="java">
 +
import java.beans.PropertyChangeListener;
 +
import java.beans.PropertyChangeSupport;
 +
import java.util.List;
 +
import java.util.concurrent.CopyOnWriteArrayList;
 +
 
 +
public interface MessageListener extends java.util.EventListener {
 +
public void messageShown(MessageEvent ev);
 +
}
 +
 
 +
public final class MessageEvent extends java.util.EventObject {
 +
 
 +
private final String msg;
 +
 
 +
MessageEvent(Object o, String msg) {
 +
super(o);
 +
this.msg = msg;
 +
}
 +
 
 +
public String getMessage() {
 +
return msg;
 +
}
 +
}
 +
 
 +
public final class Bean9 {
 +
public void say(String msg) {
 +
System.out.println(msg);
 +
if (ps != null) {
 +
ps.firePropertyChange("msg", null, msg);
 +
}
 +
fireMessageEvent(msg);
 +
System.err.println("msg");
 +
}
 +
 
 +
private final List<MessageListener> listeners = new CopyOnWriteArrayList<MessageListener>();
 +
 
 +
public void addMessageListener(MessageListener l) {
 +
listeners.add(l);
 +
}
 +
 
 +
public void removeMessageListener(MessageListener l) {
 +
listeners.remove(l);
 +
}
 +
 
 +
private void fireMessageEvent(String msg) {
 +
MessageEvent e = new MessageEvent(this, msg);
 +
MessageListener[] arr = listeners.toArray(new MessageListener[0]);
 +
for (MessageListener l : arr) {
 +
l.messageShown(e);
 +
}
 +
}
 +
}
 +
</source>
 +
 
 +
Now the users of our [[API]] that care about [[JDK]]9 and want to run without ''java.desktop'' module will just stop using the '''addPropertyChangeListener''' method and start to use '''addMessageListener''' one.
 +
 
 +
== Summary ==
 +
 
 +
We have successfully reached our dreamed state. By using '''require static''' we give users of our [[API]] a chance to run just with ''java.base'' module while we maintained [[BackwardCompatibility]] of our [[API]]. Now our users have a choice:
 +
* stick with full [[JDK]]9 and continue to use old {{JDK|java/beans|PropertyChangeListener}} methods
 +
* update their code to use ''MessageListener'' and run with ''java.base'' module only
 +
At the end it looks like that [[JDK]]9 ain't that bad [[JDK]] to use.
 +
 
 +
 
 +
== PS: Beware of Reflection! ==
 +
 
 +
Using the '''require static''' dependency on ''java.desktop'' may bring some problems with reflection. Once one tries to obtain list of methods for the ''Bean9.class'', an error is generated
<source lang="java">
<source lang="java">
Line 155: Line 232:
</source>
</source>
-
but who'd be doing reflection when the {{JDK|java/beans|Introspector}} isn't present, right?
+
but who'd be doing reflection when the {{JDK|java/beans|Introspector}} isn't present, right ;-?
-
 
+
-
== Designing new [[API]] ==
+
-
 
+
-
Listener for a purpose. Design new dedicated one.
+
-
[[TBD]].
+
[[Category:APIDesignPatterns:Encapsulation]] [[Category:APIDesignPatterns:Evolution]] [[Category:APIDesignPatterns]]

Revision as of 16:53, 14 August 2017

Looks like Jigsaw - e.g. JDK9 is unstoppable. The release will happen soon and projects are slowly starting to use the new JDK. This applies to Graal compiler project I am working on as well. We want and need it to run on JDK9 and be good modular citizen. For a while we produce JDK9 builds and test against (whole) JDK9, but last week I got a simple, but very important question:

Does Graal run on java.base only JDK9?

Simple question with many consequences and outcome that may be interesting for everyone who wants to port their application or library to JDK9 and/or make it run on slimmed down version of the JDK.

Contents

Modular Java SE

Modularity is good. As the original NetBeans Platform architect, I can't say anything else, right? Modularity is the sole reason we can practice DistributedDevelopment and release enormously large software products on a flexible schedule. On a schedule which each (distributed) team can choose by itself.

It is great that JDK has finally made its steps towards Modular Java SE. That means we no longer need to have CORBA and Swing around when running our headless applications. The modules introduced in JDK9 allow us to do that properly and not just an ad-hoc solution as JDK8 profiles. Since now all library and framework architects have a new target to aim at as a runtime platform - the java.base module. How do you create one?

jdk-9/bin/jlink --output jdk-9-base --add-modules java.base --module-path jdk-9/jmods/

The above command will create a new JDK structure in the jdk-9-base directory and will pick only the java.base module up. The base module contains just the core Java packages and is relatively (the module still contains about three thousand classes, while Bck2Brwsr VM minimal profile - see its javadoc - can do with less than 150) small - the java.base only JDK size shrinks to less than 50MB.

Want PropertyChangeListener? Get Swing with it!

Modularization of a large monolithic application has to be associated with compromises. I experienced that while working on Modularization of NetBeans Platform. It comes with no surprise the Jigsaw modularization of JDK comes with compromises too. However one of them is really nasty, I'd say!

Do you follow the JavaBean specification? Probably you do: if you don't know you probably do that unconsciously. If you name getters and setter of your properties as setPropName and getPropName, then you follow the JavaBean spec.

Properties are one essential part of the JavaBeans. The other important part is about observing their changes. This is done by listeners. The JavaBean spec allows anybody to create their own listeners, but in addition to that it defines two general purpose ones: the PropertyChangeListener and VetoableChangeListener. Especially the first one is generally useful and often used in many APIs that just expose:

public void addPropertyChangeListener(PropertyChangeListener l);
public void removePropertyChangeListener(PropertyChangeListener l);

and allow anyone to observe changes in their properties. NetBeans APIs use that a lot. For example the FileSystem class uses both - property change as well as and vetoable listeners - and the filesystems API doesn't have anything to do with UI. The same can be said about Spring framework - it also uses a lot of classes from java.beans package and has no relation to (Swing or AWT) UI at all.

Alas, the java.beans package is going to be exposed from java.desktop JDK9 module! Try to use JavaBeans and get Swing with that!

I know why the java.beans package ended up in the desktop Jigsaw module. Some of its classes (like Applet) reference AWT classes. Moreover there is a cyclic dependency - a lot of classes from Swing & co. reference PropertyChangeListener (obviously, as that is useful and adviced by JavaBean specification). As such it would be hard to separate java.beans package into own module. On the other hand we don't want all our APIs that use PropertyChangeListener to depend on Swing - the JDK team could have done a better job.

Anyway, it looks like that the PropertyChangeListener and related classes are going to be available only from java.desktop module. What can our projects (Spring, Graal, NetBeans) do with that?

The Solution

Of course, we could remove the addPropertyChangeListener and removePropertyChangeListener methods from our APIs. Then we need no dependency on java.desktop Jigsaw module. However that isn't very BackwardCompatible and as author of TheAPIBook I always have to prefer a compatible solution. Luckily there is one!

We can declare conditional only dependency on the desktop module:

require static java.desktop

Such dependency allows us to compile against classes from desktop (and thus also java.beans package), but the module doesn't have to be around when running our code. That means it is possible to have PropertyChangeListener in the signatures of our API methods and methods like

public final class Bean9 {
    public void addPropertyChangeListener(PropertyChangeListener l);
    public void removePropertyChangeListener(PropertyChangeListener l);
    public void say(String msg) {
        System.out.println(msg);
    }
}

are fine. The Bean9 class can still be loaded on the JDK containing just the java.base module. Following code can be used without any issues:

final class Main {
    public static void main(String[] args) {
        System.err.println("starting");
        Bean9 b = new Bean9();
        b.say("Hello");
        b.addPropertyChangeListener(null);
        b.say("world");
    }

the class can be instantiated, one can call its method say and (surprisingly) it is even possible to call the addPropertyChangeListener method with null parameter! JDK class verifier is fine with that. Heuréka! And thanks to the JDK team for having such a lazy verifier!

Of course one has to be careful to use the java.beans classes only conditionally. When implementing the property change notifications, it is certainly not good idea to use PropertyChangeSupport directly:

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
 
public final class Bean9 {
    private final PropertyChangeSupport ps = new PropertyChangeSupport(this);
 
    public void addPropertyChangeListener(PropertyChangeListener pcl) {
        ps.addPropertyChangeListener(pcl);
    }
 
    public void removePropertyChangeListener(PropertyChangeListener pcl) {
        ps.removePropertyChangeListener(pcl);
    }
 
    public void say(String msg) {
        System.out.println(msg);
    }
 
}

This time the JVM will complain and raise a NoClassDefFoundError:

starting
Exception in thread "main" java.lang.NoClassDefFoundError: java/beans/PropertyChangeSupport
        at beans9.Bean9.<init>(Bean9.java:7)
        at beans9.Main.main(Main.java:6)
Caused by: java.lang.ClassNotFoundException: java.beans.PropertyChangeSupport
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:185)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:496)

obviously, there is no PropertyChangeSupport class around, so we shall not attempt to create its instances. However making the listener support lazy and allocating it only when needed seems to work fine:

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
 
public final class Bean9 {
    private PropertyChangeSupport ps;
 
    public void addPropertyChangeListener(PropertyChangeListener pcl) {
        if (pcl == null) {
            return;
        }
        if (ps == null) {
            ps = new PropertyChangeSupport(this);
        }
        ps.addPropertyChangeListener(pcl);
    }
 
    public void removePropertyChangeListener(PropertyChangeListener pcl) {
        ps.removePropertyChangeListener(pcl);
    }
 
    public void say(String msg) {
        System.out.println(msg);
        if (ps != null) {
            ps.firePropertyChange("msg", null, msg);
        }
    }
}

Excellent. We can change our API to run on a JDK9 with only java.base module and still keep compatibility by having the property support conditionally ready.

Designing API for (small) JDK9

Of course, only people that add dependency on java.desktop module can actually use call the API that accepts PropertyChangeListener. E.g. if one wants to observe what is happening in the sample Beans9 class, one still needs to bring the whole Swing & co. in. Obviously, that is not what we want. We want people to have access to the full functionality even when running only with java.base module.

I believe the best solution is to design a dedicated listener - that is perfectly valid JavaBean approach and it can work with java.base module (because the EventListener and EventObject are both in the java.base Jigsaw module). Thus I suggest to enhance the Beans9 example with two more classes:

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
 
public interface MessageListener extends java.util.EventListener {
    public void messageShown(MessageEvent ev);
}
 
public final class MessageEvent extends java.util.EventObject {
 
    private final String msg;
 
    MessageEvent(Object o, String msg) {
        super(o);
        this.msg = msg;
    }
 
    public String getMessage() {
        return msg;
    }
}
 
public final class Bean9 {
    public void say(String msg) {
        System.out.println(msg);
        if (ps != null) {
            ps.firePropertyChange("msg", null, msg);
        }
        fireMessageEvent(msg);
        System.err.println("msg");
    }
 
    private final List<MessageListener> listeners = new CopyOnWriteArrayList<MessageListener>();
 
    public void addMessageListener(MessageListener l) {
        listeners.add(l);
    }
 
    public void removeMessageListener(MessageListener l) {
        listeners.remove(l);
    }
 
    private void fireMessageEvent(String msg) {
        MessageEvent e = new MessageEvent(this, msg);
        MessageListener[] arr = listeners.toArray(new MessageListener[0]);
        for (MessageListener l : arr) {
            l.messageShown(e);
        }
    }
}

Now the users of our API that care about JDK9 and want to run without java.desktop module will just stop using the addPropertyChangeListener method and start to use addMessageListener one.

Summary

We have successfully reached our dreamed state. By using require static we give users of our API a chance to run just with java.base module while we maintained BackwardCompatibility of our API. Now our users have a choice:

  • stick with full JDK9 and continue to use old PropertyChangeListener methods
  • update their code to use MessageListener and run with java.base module only

At the end it looks like that JDK9 ain't that bad JDK to use.


PS: Beware of Reflection!

Using the require static dependency on java.desktop may bring some problems with reflection. Once one tries to obtain list of methods for the Bean9.class, an error is generated

System.err.println("methods: " + Arrays.toString(Bean9.class.getMethods()));
 
Exception in thread "main" java.lang.NoClassDefFoundError: java/beans/PropertyChangeListener
        at java.base/java.lang.Class.getDeclaredMethods0(Native Method)
        at java.base/java.lang.Class.privateGetDeclaredMethods(Class.java:3139)
        at java.base/java.lang.Class.privateGetPublicMethods(Class.java:3164)
        at java.base/java.lang.Class.getMethods(Class.java:1861)
        at beans9.Main.main(Main.java:14)
Caused by: java.lang.ClassNotFoundException: java.beans.PropertyChangeListener
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:185)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:496)
        ... 5 more

but who'd be doing reflection when the Introspector isn't present, right ;-?

Personal tools
buy