Accès rapide :
Qu'est-ce que l'introspection Java ?
Les conventions JavaBeans
Exemple d'utilisation du moteur d'introspection
Lister les propriétés supportées par la classe
Lister les méthodes des différents gestionnaires d'événements (les listeners) supportées par la classe
Création d'un composant Swing d'affichage de propriétés et de listeners pour un Java Bean
Mise en place des deux composants de types JTables
Intégration des deux tables dans un conteneur d'onglets
Intégration du composant dans une fenêtre graphique
En Java, l'introspection complète les possibilités offertes par le moteur de réflexion. Ce principe d'introspection est aussi en lien avec le modèle de composants JavaBeans. Un composant JavaBean est une classe Java qui expose des propriétés et des gestionnaires d'événements (aussi appelés « listeners ») en respectant les conventions de codages « JavaBeans ».
De nombreux frameworks Java sont basés sur cette notion d'introspection. Citons, entre autres, Spring Framework, JSF (Java Server Faces) et plus généralement la plate-forme Jakarta EE (anciennement Java EE).
Le moteur d'introspection est disponible à partir du package java.beans
. Il s'appui sur le moteur de réflexion, lui-même localisé dans le
package java.lang.reflect
.
La convention de codage « JavaBeans » définie les règles suivantes :
Un composant JavaBean doit proposer un constructeur à zéro paramètre. Il est normalement proposé par défaut, mais si vous spécifiez au moins un constructeur avec des paramètres, alors vous devrez aussi coder un constructeur à zéro paramètre.
Un composant JavaBean doit être marqué comme étant sérialisable. Pour ce faire, vous devez implémenter l'interface java.io.Serializable
.
Cette interface ne définit aucune méthode : c'est juste un marqueur de type pour indiquer que vous autorisez la sérialisation sur votre classe.
Dans les faits, ce point n'est pas toujours obligatoire mais reste vivement conseillé.
Une propriété est associée à une ou deux méthodes en fonction qu'elle soit « read/write », « read only » ou « write only ».
Ces méthodes doivent respecter un des trois préfixes suivants : is
pour la méthode de lecture et si le type de la propriété est booléen,
get
pour la méthode de lecture dans tous les autres cas et set
pour une méthode d'accès en écriture.
Nous avons déjà très largement débattu de propriétés dans les chapitres précédents et notamment dans celui portant sur
l'encapsultation.
Une méthode d'enregistrement d'un écouteur (listener en anglais), pour la gestion des événements, doit être préfixée de add
et être
suffixée de Listener
. Par exemple, si vous voulez être notifié de la prise ou de la perte du focus sur une zone de saisie de texte,
if faudra invoquer sur ce composant la méthode addFocusListener
pour y enregistrer une instance de votre gestionnaire d'événements.
Qui plus est, cette méthode doit impérativement accepter un paramètre compatible, en termes de typage, par l'interface d'écoute associée.
Voici un exemple SWING d'enregistrement du listener associé à la prise/perte du focus sur une zone de saisie de texte.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
JTextField txtFirstName = new JTextField(); txtFirstName.addFocusListener( new FocusListener() { @Override public void focusGained( FocusEvent event ) { System.out.println( "Prise de focus pour la zone de saisie de texte." ); } @Override public void focusLost( FocusEvent event ) { System.out.println( "Perte de focus pour la zone de saisie de texte." ); } } ); |
Le fait de respecter ces conventions permettra à des outils (ou des framework) de trouver automatiquement, par introspection, la liste des propriétés exposées ainsi que la liste des gestionnaires d'événements supportés. Imaginez par exemple un panneau latéral permettant de configurer des composants graphiques dans un éditeur d'interfaces graphiques.
Les deux principaux types du moteur d'introspection sont la classe java.beans.Introspector
et l'interface java.beans.BeanInfo
.
La première classe proposée correspond au point d'entrée sur le moteur d'introspection alors de la seconde interface correspond à un ensemble de méta-données associées
à un type de JavaBean.
Voici un premier exemple de code permettant de récupérer une instance de BeanInfo
associée à un JavaBean.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package fr.koor.introspection; import java.beans.BeanInfo; import java.beans.Introspector; import javax.swing.JButton; public class BeanInfoSample { public static void main( String[] args ) throws Exception { BeanInfo beanInfo = Introspector.getBeanInfo( JButton.class ); System.out.println( beanInfo ); } } |
Une instance de BeanInfo
permet de lister l'ensemble des propriétés et des listeners supportés par la classe.
Lister les propriétés d'une classe respectant les conventions « JavaBeans » pourrait se faire via le moteur de réflexion Java : il suffirait
de lister toutes les méthodes publiques et de ne garder que celles commençant par get
, set
ou is
. Ensuite, il
faudrait chercher à regrouper les éventuelles paires de méthodes. Même si cela reste facile à réaliser, l'emploi du moteur d'introspection sera plus simple,
car il réalise ce travail pour vous.
Voici un exemple de code qui parcourt l'ensemble des propriétés disponibles sur la classe java.swing.JButton
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package fr.koor.introspection; import java.beans.BeanInfo; import java.beans.Introspector; import java.beans.PropertyDescriptor; import javax.swing.JButton; public class BeanInfoSample { public static void main( String[] args ) throws Exception { BeanInfo beanInfo = Introspector.getBeanInfo( JButton.class ); PropertyDescriptor [] properties = beanInfo.getPropertyDescriptors(); for ( PropertyDescriptor propertyDescriptor : properties ) { System.out.printf( "%-50s %-30s %c/%c\n", propertyDescriptor.getPropertyType(), propertyDescriptor.getName(), propertyDescriptor.getReadMethod() != null ? 'R' : '-', propertyDescriptor.getWriteMethod() != null ? 'W' : '-' ); } } } |
Et voici les résultats produits par cet exemple :
class javax.swing.plaf.ButtonUI UI R/W class java.lang.String UIClassID R/- class javax.accessibility.AccessibleContext accessibleContext R/- interface javax.swing.Action action R/W class java.lang.String actionCommand R/W class [Ljava.awt.event.ActionListener; actionListeners R/- class javax.swing.ActionMap actionMap R/W float alignmentX R/W float alignmentY R/W class [Ljavax.swing.event.AncestorListener; ancestorListeners R/- boolean autoscrolls R/W class java.awt.Color background R/W class java.awt.Component$BaselineResizeBehavior baselineResizeBehavior R/- interface javax.swing.border.Border border R/W boolean borderPainted R/W class [Ljavax.swing.event.ChangeListener; changeListeners R/- null component -/- int componentCount R/- class javax.swing.JPopupMenu componentPopupMenu R/W class [Ljava.awt.Component; components R/- class [Ljava.awt.event.ContainerListener; containerListeners R/- boolean contentAreaFilled R/W int debugGraphicsOptions R/W boolean defaultButton R/- boolean defaultCapable R/W interface javax.swing.Icon disabledIcon R/W interface javax.swing.Icon disabledSelectedIcon R/W int displayedMnemonicIndex R/W boolean doubleBuffered R/W boolean enabled R/W boolean focusCycleRoot R/W boolean focusPainted R/W null focusTraversalKeys -/- class java.awt.FocusTraversalPolicy focusTraversalPolicy R/W boolean focusTraversalPolicyProvider R/W boolean focusTraversalPolicySet R/- boolean focusable R/W class java.awt.Font font R/W class java.awt.Color foreground R/W class java.awt.Graphics graphics R/- int height R/- boolean hideActionText R/W int horizontalAlignment R/W int horizontalTextPosition R/W interface javax.swing.Icon icon R/W int iconTextGap R/W boolean inheritsPopupMenu R/W class javax.swing.InputMap inputMap R/- class javax.swing.InputVerifier inputVerifier R/W class java.awt.Insets insets R/- class [Ljava.awt.event.ItemListener; itemListeners R/- class java.lang.String label R/W interface java.awt.LayoutManager layout R/W boolean managingFocus R/- class java.awt.Insets margin R/W class java.awt.Dimension maximumSize R/W class java.awt.Dimension minimumSize R/W int mnemonic R/W interface javax.swing.ButtonModel model R/W long multiClickThreshhold R/W class java.lang.String name R/W class java.awt.Component nextFocusableComponent R/W boolean opaque R/W boolean optimizedDrawingEnabled R/- boolean paintingForPrint R/- boolean paintingTile R/- class java.awt.Dimension preferredSize R/W interface javax.swing.Icon pressedIcon R/W class [Ljavax.swing.KeyStroke; registeredKeyStrokes R/- boolean requestFocusEnabled R/W boolean rolloverEnabled R/W interface javax.swing.Icon rolloverIcon R/W interface javax.swing.Icon rolloverSelectedIcon R/W class javax.swing.JRootPane rootPane R/- boolean selected R/W interface javax.swing.Icon selectedIcon R/W class [Ljava.lang.Object; selectedObjects R/- class java.lang.String text R/W class java.lang.String toolTipText R/W class java.awt.Container topLevelAncestor R/- class javax.swing.TransferHandler transferHandler R/W boolean validateRoot R/- boolean verifyInputWhenFocusTarget R/W int verticalAlignment R/W int verticalTextPosition R/W class [Ljava.beans.VetoableChangeListener; vetoableChangeListeners R/- boolean visible R/W class java.awt.Rectangle visibleRect R/- int width R/- int x R/- int y R/-
De même, la recherche des gestionnaires d'événements supportés par une classe respectant les conventions « JavaBeans » pourrait être
réalisée via le moteur de réflexion Java en filtrant, dans les méthodes publiques, celles commençant par le préfixe add
et se terminant
par le suffixe Listener
. Mais, encore une fois, le moteur d'introspection fait cela pour vous : il serait dommage de ne pas y faire
appel.
Voici un exemple de code listant toutes les méthodes de gestion d'événements. Ces méthodes sont portées par les différentes interfaces d'écoute. On manipule ces méthodes via le moteur de réflexion.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
package fr.koor.introspection; import java.beans.BeanInfo; import java.beans.EventSetDescriptor; import java.beans.Introspector; import java.lang.reflect.Method; import javax.swing.JButton; public class BeanInfoSample { public static void main( String[] args ) throws Exception { BeanInfo beanInfo = Introspector.getBeanInfo( JButton.class ); EventSetDescriptor [] eventSetDescriptors = beanInfo.getEventSetDescriptors(); for ( EventSetDescriptor eventSetDescriptor : eventSetDescriptors ) { String addMethodName = eventSetDescriptor.getAddListenerMethod().getName(); String associatedInterface = eventSetDescriptor.getListenerType().toString(); System.out.printf( "%s [with %s]\n", addMethodName, associatedInterface ); Method [] methods = eventSetDescriptor.getListenerMethods(); for( Method method : methods ) { System.out.println( "\t" + method.toString() ); } } } } |
Et voici les résultats produits par cet exemple :
addContainerListener [with interface java.awt.event.ContainerListener] public abstract void java.awt.event.ContainerListener.componentAdded(java.awt.event.ContainerEvent) public abstract void java.awt.event.ContainerListener.componentRemoved(java.awt.event.ContainerEvent) addItemListener [with interface java.awt.event.ItemListener] public abstract void java.awt.event.ItemListener.itemStateChanged(java.awt.event.ItemEvent) addHierarchyListener [with interface java.awt.event.HierarchyListener] public abstract void java.awt.event.HierarchyListener.hierarchyChanged(java.awt.event.HierarchyEvent) addAncestorListener [with interface javax.swing.event.AncestorListener] public abstract void javax.swing.event.AncestorListener.ancestorAdded(javax.swing.event.AncestorEvent) public abstract void javax.swing.event.AncestorListener.ancestorRemoved(javax.swing.event.AncestorEvent) public abstract void javax.swing.event.AncestorListener.ancestorMoved(javax.swing.event.AncestorEvent) addChangeListener [with interface javax.swing.event.ChangeListener] public abstract void javax.swing.event.ChangeListener.stateChanged(javax.swing.event.ChangeEvent) addMouseMotionListener [with interface java.awt.event.MouseMotionListener] public abstract void java.awt.event.MouseMotionListener.mouseMoved(java.awt.event.MouseEvent) public abstract void java.awt.event.MouseMotionListener.mouseDragged(java.awt.event.MouseEvent) addFocusListener [with interface java.awt.event.FocusListener] public abstract void java.awt.event.FocusListener.focusGained(java.awt.event.FocusEvent) public abstract void java.awt.event.FocusListener.focusLost(java.awt.event.FocusEvent) addMouseWheelListener [with interface java.awt.event.MouseWheelListener] public abstract void java.awt.event.MouseWheelListener.mouseWheelMoved(java.awt.event.MouseWheelEvent) addHierarchyBoundsListener [with interface java.awt.event.HierarchyBoundsListener] public abstract void java.awt.event.HierarchyBoundsListener.ancestorMoved(java.awt.event.HierarchyEvent) public abstract void java.awt.event.HierarchyBoundsListener.ancestorResized(java.awt.event.HierarchyEvent) addMouseListener [with interface java.awt.event.MouseListener] public abstract void java.awt.event.MouseListener.mousePressed(java.awt.event.MouseEvent) public abstract void java.awt.event.MouseListener.mouseReleased(java.awt.event.MouseEvent) public abstract void java.awt.event.MouseListener.mouseClicked(java.awt.event.MouseEvent) public abstract void java.awt.event.MouseListener.mouseExited(java.awt.event.MouseEvent) public abstract void java.awt.event.MouseListener.mouseEntered(java.awt.event.MouseEvent) addComponentListener [with interface java.awt.event.ComponentListener] public abstract void java.awt.event.ComponentListener.componentResized(java.awt.event.ComponentEvent) public abstract void java.awt.event.ComponentListener.componentMoved(java.awt.event.ComponentEvent) public abstract void java.awt.event.ComponentListener.componentShown(java.awt.event.ComponentEvent) public abstract void java.awt.event.ComponentListener.componentHidden(java.awt.event.ComponentEvent) addInputMethodListener [with interface java.awt.event.InputMethodListener] public abstract void java.awt.event.InputMethodListener.inputMethodTextChanged(java.awt.event.InputMethodEvent) public abstract void java.awt.event.InputMethodListener.caretPositionChanged(java.awt.event.InputMethodEvent) addActionListener [with interface java.awt.event.ActionListener] public abstract void java.awt.event.ActionListener.actionPerformed(java.awt.event.ActionEvent) addPropertyChangeListener [with interface java.beans.PropertyChangeListener] public abstract void java.beans.PropertyChangeListener.propertyChange(java.beans.PropertyChangeEvent) addKeyListener [with interface java.awt.event.KeyListener] public abstract void java.awt.event.KeyListener.keyTyped(java.awt.event.KeyEvent) public abstract void java.awt.event.KeyListener.keyPressed(java.awt.event.KeyEvent) public abstract void java.awt.event.KeyListener.keyReleased(java.awt.event.KeyEvent) addVetoableChangeListener [with interface java.beans.VetoableChangeListener] public abstract void java.beans.VetoableChangeListener.vetoableChange(java.beans.PropertyChangeEvent) throws java.beans.PropertyVetoException
Bon, pour cette section, je m'aventure un peu loin : si vous avez suivi ce tuto dans l'ordre de lecture, vous n'avez pas encore vu la programmation graphique via la librairie Swing.
Pour autant, je vais tenter de proposer un code, basé sur cette librairie, qui affiche les propriétés et les événements supportés par un composant graphique quelconque.
Il faut savoir que par défaut, les composants graphiques Swing respectent la convention « JavaBeans ».
Bien entendu, nous allons nous baser sur les codes présentés précédemment. Voici une capture d'écran montrant le panneau de propriétés d'un composant de type JButton
.
Pour afficher les propriétés et les gestionnaires d'événements, nous allons utiliser deux composants de type javax.swing.JTable
. Un JTable
permet d'afficher des données
dans une table en utilisant un pattern MVC (Model-View-Controller). Le modèle de données de la table doit être compatible avec l'interface TableModel
: la classe abstraite
AbstractTableModel
étant compatible avec cette interface. Un modèle de données permet de retrouver le nombre de colonnes et de lignes de la table ainsi que les valeurs de chaque
cellule (y compris pour les cellules de titres).
Chaque table pourra recevoir un nouvel objet à introspecter : à chaque changement d'instance, un nouveau modèle de données sera produit. Voici le code pour la table d'affichage du panneau de propriétés.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
package fr.koor.introspection; import java.beans.BeanInfo; import java.beans.Introspector; import java.beans.PropertyDescriptor; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.table.AbstractTableModel; // Cette classe est un JScrollPane, pour avoir des barres de scrolling, // qui contient un composant JTable. public class BeanPropertiesTable extends JScrollPane { // Le composant JTable affichant les propriétés private JTable table; // Le bean sur lequel réaliser l'introspection private Object instance; public BeanPropertiesTable() { // On crée le JScrollPane par-dessus un JTable super( new JTable() ); // On récupère le composant JTable this.table = (JTable) this.getViewport().getView(); } public Object getInstance() { return instance; } // Si on change l'instance à introspecter alors on produit un nouveau modèle. public void setInstance( Object instance ) { this.instance = instance; // Le modèle (la source de données) utilisé par la table // doit être compatible avec le type AbstractTableModel. this.table.setModel( new AbstractTableModel() { private Class<?> metadata; private PropertyDescriptor [] propertyDescriptors; // Le constructeur de la classe anonyme { try { metadata = instance.getClass(); BeanInfo beanInfo = Introspector.getBeanInfo( metadata ); propertyDescriptors = beanInfo.getPropertyDescriptors(); } catch( Exception exception ) { System.err.println( "Cannot access bean properties" ); } } @Override public int getRowCount() { return propertyDescriptors.length; } @Override public int getColumnCount() { return 2; // property name / property value } @Override public Object getValueAt( int rowIndex, int columnIndex ) { switch( columnIndex ) { case 0: return propertyDescriptors[rowIndex].getName(); case 1: try { return "" + propertyDescriptors[rowIndex].getReadMethod().invoke( instance ); } catch ( Exception exception ) { return ""; } default: throw new RuntimeException( "Bad column index" ); } } @Override public String getColumnName( int columnIndex ) { switch( columnIndex ) { case 0: return "Property name"; case 1: return "Value"; default: throw new RuntimeException( "Bad column index" ); } } } ); } } |
Et voici le code de la table associée aux gestionnaires d'événements.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
package fr.koor.introspection; import java.beans.BeanInfo; import java.beans.EventSetDescriptor; import java.beans.Introspector; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.table.AbstractTableModel; //Cette classe est un JScrollPane, pour avoir des barres de scrolling, //qui contient un composant JTable. public class BeanEventsTable extends JScrollPane { // Le composant JTable affichant les gestionnaires d'événements private JTable table; // Le bean sur lequel réaliser l'introspection private Object instance; public BeanEventsTable() { // On crée le JScrollPane par-dessus un JTable super( new JTable() ); // On récupère le composant JTable this.table = (JTable) this.getViewport().getView(); } public Object getInstance() { return instance; } // Si on change l'instance à introspecter alors on produit un nouveau modèle. public void setInstance( Object instance ) { this.instance = instance; // Le modèle (la source de données) utilisé par la table // doit être compatible avec le type AbstractTableModel. this.table.setModel( new AbstractTableModel() { private Class<?> metadata; private List<String> eventListenerMethods = new ArrayList<>(); // Le constructeur de la classe anonyme { try { metadata = instance.getClass(); BeanInfo beanInfo = Introspector.getBeanInfo( metadata ); EventSetDescriptor [] eventSetDescriptors = beanInfo.getEventSetDescriptors(); for ( EventSetDescriptor eventSetDescriptor : eventSetDescriptors ) { Method [] methods = eventSetDescriptor.getListenerMethods(); for( Method method : methods ) { eventListenerMethods.add( method.getName() ); } } // On veut les méthodes présentées dans l'ordre alphabétique. eventListenerMethods.sort( (l1, l2) -> l1.compareTo( l2 ) ); } catch( Exception exception ) { System.err.println( "Cannot access bean listeners" ); } } @Override public int getRowCount() { return eventListenerMethods.size(); } @Override public int getColumnCount() { return 2; // property name / property value } @Override public Object getValueAt( int rowIndex, int columnIndex ) { switch( columnIndex ) { case 0: return eventListenerMethods.get( rowIndex ); case 1: return ""; default: throw new RuntimeException( "Bad column index" ); } } @Override public String getColumnName( int columnIndex ) { switch( columnIndex ) { case 0: return "Event"; case 1: return "Method"; default: throw new RuntimeException( "Bad column index" ); } } } ); } } |
Pour afficher des onglets en Swing, on utilise la classe javax.swing.JTabbedPane
. On y ajoute un composant Swing par onglet : dans notre cas nos tables précédemment développées.
Voici le code du composant assemblé.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
package fr.koor.introspection; import javax.swing.JTabbedPane; public class BeanViewer extends JTabbedPane { private BeanPropertiesTable propertiesTable = new BeanPropertiesTable(); private BeanEventsTable eventsTable = new BeanEventsTable(); private Object instance; public BeanViewer() { // On place les onglets en bas this.setTabPlacement( JTabbedPane.BOTTOM ); // On ajoute les deux JTable this.add( "Properties", propertiesTable ); this.add( "Event listeners", eventsTable ); } public Object getInstance() { return instance; } // En cas de changement d'instance à introspecter, on la transfert aux sous-tables. public void setInstance( Object instance ) { this.instance = instance; this.propertiesTable.setInstance( instance ); this.eventsTable.setInstance( instance ); } } |
Pour finir, on crée une fenêtre de test qui intègre notre composant. On connecte notre explorateur de beans sur une instance de JButton. Voici le code de cette fenêtre.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package fr.koor.introspection; import java.awt.Dimension; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSplitPane; import javax.swing.UIManager; import javax.swing.plaf.nimbus.NimbusLookAndFeel; public class Start extends JFrame { private BeanViewer beanViewer = new BeanViewer(); private JLabel rightPart = new JLabel( "Imagine a graphical editor here!" ); public Start() { super( "Bean viewer" ); JPanel contentPane = (JPanel) this.getContentPane(); contentPane.add( new JSplitPane( JSplitPane.HORIZONTAL_SPLIT, beanViewer, rightPart ) ); beanViewer.setPreferredSize( new Dimension( 340, 0 ) ); beanViewer.setInstance( new JButton( "Click me!" ) ); rightPart.setHorizontalAlignment( JLabel.CENTER ); this.setSize( 800, 400 ); this.setLocationRelativeTo( null ); this.setDefaultCloseOperation( DISPOSE_ON_CLOSE ); this.setVisible( true ); } public static void main( String[] args ) throws Exception { UIManager.setLookAndFeel( new NimbusLookAndFeel() ); new Start(); } } |
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :