Dynamiczne programowanie pozwala na tworzenie bardzo elastycznych i uniwersalnych aplikacji. W Javie ma ono wiele aspektów.
Java jest językiem interpretowanym. Kompilator tworzy kod bajtowy, ten zaś podlega interpretacji przez wirtualną maszynę Javy. Daje to podstawy do dynamicznego, w fazie wykonania, kształtowaniu mechanizmów
działania programu. Osiągamy to dzięki tzw. refleksji.
Refleksja stanowi również podstawowy, niskopoziomowy mechanizm,
wykorzystywany
przy programowaniu komponentowym (JavaBeans). Termin JavaBeans
wiąże się z koncepcją budowania
programów z gotowych, łatwo dostosowywanych do konkretnych potrzeb,
komponentów
programowych. Idea to niezwykle kusząca, ale jednocześnie – z
niewiadomych
powodów – owiana nimbem "wyższego wtajemniczenia". W bardzo
krótkim wprowadzeniu
do JavaBeans zobaczymy, że nie ma tu nic tajemniczego ani trudnego, a
korzyści wiążą się nie tylko z ponownym uzyciem gotowych komponentów,
ale również z możliwością tworzeniu uniwersalnych kodów dzięki
machanizmowi nasłuchu zmian właściwości.
Kolejny
dynamiczny aspekt Javy - to adnotacje. Pozwalają one na swoistym
metapoziomie kształtować semantykę aplikacji i znacząco
upraszczają wykorzystanie różnych technologii programistycznych.
Ostatni
temat w tym wykładzie dotyczy integracji Javy z językami
skryptowymi. Tu dynamiczne programowanie w Javie przekracza bariery
jednej platformy i otwiera przebogate możliwości w tworzeniu
różnorodnych aplikacji.
1. Refleksja
1.1. Dynamiczne ładowanie klas
W klasie Object zdefiniowano metodę getClass. Zastosowana wobec dowolnego obiektu
zwraca odnośnik do jego klasy, do obiektu klasy java.lang.Class.
Obiekty klasy Class są klasami (to ważne: tu same klasy są obiektami)
Możemy wobec takich obiektów stosować różne metody klasy Class z pakietu java.lang, np.
-
getSuperClass() - zwracającą obiekt klasy Class oznaczający klasę bazową danej klasy,
-
getInterfaces() - zawracającą tablicę obiektów, zawierającą interfejsy danej klasy,
-
newInstance() - tworzącą nowy obiekt danej klasy.
Obiektów-klas nie możemy tworzyć za pomocą wyrażenia new.
Jedynie poprzez użycie odpowiednich metod uzyskujemy odnośniki do tych obiektów.
Jedną z takich metod jest statyczna metoda klasy Class:
forName(String NazwaKlasy);
Np. pisząc:
Class c = Class.forName("javax.swing.JButton");
lub
Class c = javax.swing.JButton.class; <--- nazwa_klasy.class jest literałem oznaczającym
obiekt-klasę
uzyskujemy odnośnik do klasy przycisków i możemy się nim posłużyć przy tworzeniu obiektu:
JButton b = (JButton) c.newInstance();
W statycznym przypadku, gdy wszystko jest ustalone "w źródle" programu, sens
takich konstrukcji jest niewielki. Ale bardzo często warto odłożyć pewne
ustalenia do fazy wykonania programu, zwiększając jego elastyczność i uniwersalność.
Wtedy dynamiczna reprezentacja obiektów-klas bardzo się przydaje.
Najprostszy przykład: odroczenie ustalenia sposobu obsługi akcji na przycisku
do fazy wykonania programu pokazano na poniższym wydruku. Jako argument
podajemy nazwę klasy, której obiekt obsługuje akcję. Możemy mieć wiele takich
(wariantowych) klas i bez żadnej rekompilacji zmieniać sposoby obsługi kliknięcia
w przycisk.
package dynload;
import javax.swing.*;
class Main extends JFrame {
static void exit(String s) {
System.out.println(s);
System.exit(1);
}
Main(String actionClassName) {
Class actionClass = null;
Action act = null;
try {
actionClass = Class.forName(actionClassName);
act = (Action) actionClass.newInstance();
} catch (Exception exc) {
exit("Obiekt klasy akcji nie może być utworzony");
}
JButton b = new JButton();
b.setAction(act);
add(b);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true); }
public static void main(String args[]) {
final String in = JOptionPane.showInputDialog("Podaj nazwę klasy:");
if (in != null) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Main(in);
}
});
}
}
}
Uwaga: w powyższym przypadku newInstance zwraca referencję do obiektu klasy Object - stąd potrzeba konwersji do typu Action.
Jeśli teraz przygotujemy dwie klasy, opisujące różne akcje np.
package dynload;
import javax.swing.*;
import java.awt.event.*;
public class DialogAction extends AbstractAction {
final static String ACTION_NAME = "Show msg";
public DialogAction() {
super(ACTION_NAME);
}
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(null, ACTION_NAME);
}
}
i
package dynload;
import javax.swing.*;
import java.awt.event.*;
public class PrintAction extends AbstractAction {
final static String ACTION_NAME = "Print";
public PrintAction() {
super(ACTION_NAME);
}
public void actionPerformed(ActionEvent e) {
System.out.println("Wykonan akcja: " + ACTION_NAME);
}
}
to uruchomienie głównego programu z argumentem: DialogAction:
java Main dynload.DialogAction
spowoduje, że przycisk w oknie uzyska nazwę Show Msg, a jego klikniecie otworzy okienko komunikaty.
Natomiast po uruchomieniu programu z argumentem PrintAction
java Main dynload.PrintAction
nada przyciskowi nazwę Print, a jego kliknięcie wyprowadzi komunikat na konsolę.
Uwaga: jak widać podawane nazwy klas winny być kwalifikowane nazwą pakietu.
Takie możliwości istniały w Javie od zawsze. Ale samo dynamiczne ładowanie
klas to zdecydowanie za mało, by można było tworzyć naprawdę elastyczne programy.
Dopiero w wersji 1.1 Java zyskała prawdziwą elastyczność dzięki wprowadzeniu
mechanizmów refleksji.
1.2. Mechanizm refleksji
Podstawowy programistyczny interfejs refleksji (Core Reflection API) realizowany jest przez klasy pakietu java.lang.reflect oraz rozbudowaną klasę Class z pakietu java.lang.
Refleksja oznacza możliwoć wykonywania W TRAKCIE WYKONANIA PROGRAMU następujących działań:
- uzyskiwania pełnej informacji o charakterystykach klasy (pola, metody, ich charakterystyki)
- działań na polach danego obiektu, poprzez ich nazwy,
- aktywowanie metod na rzecz danego obiektu poprzez ich nazwy i z podaniem argumentów.
Użycie refleksji pozwala m.in. na:
- stwierdzenie jakie i z jakimi argumentami metody występują w danej
klasie (np. podawanej dynamicznie w trakcie wykonania programu)
- dynamiczne wywoływanie metod (specyfikowanych w trakcie wykonania programu)
na rzecz jakiego obiektu (też dynamicznie ustalanego),
- dynamiczne uzyskiwanie i modyfikacje wartości pól obiektu.
Uzywając mechanizmów refleksji do metod i pól odwołujemy się poprzez ich nazwy, a nie identyfikatory.
Na czym polega różnica wobec statycznego przypadku?
W statyce odwołania są skonkretyzowane na etapie kompilacji.
Piszemy np. b.getText(). I tak już zostanie na zawsze.
W dynamice konstrukcja jest całkiem inna - właśnie posługująca się nazwą metody. Piszemy raczej tak: b.invokeMethod("getText").
Tu invokeMethod jest naszą własną metodą. Jako argument podajemy nazwę metody
klasy, a ponieważ jest to String, możemy go zmieniać w każdym momencie wykonania
programu. Np. możemy napisać: b.invokeMethod(s), a kolejne podstawienia pod
s różnych nazw metod będzie zmieniać znaczenie tej linii programu. Właśnie
w invokeMethod używamy środków refleksji.
Klasy pakietu java.lang.reflect
Klasa | Przeznaczenie |
Array | Tworzenie tablic, uzyskiwanie i ustalanie wartości elementów |
Constructor | Informacja i dostęp do danego konstru
ktora danej klasy. W szczególności wykorzystanie dla tworzenia obiektu. |
Field | Informacja i dostęp do pola obiektu. Pobranie i zmiana wartości pola. |
Method | Informacja o danej metodzie danej klasy. Dynamiczne wywołanie metody na rzecz danego obiektu.
|
Modifier | Uzyskiwanie informacji o modyfikatorach składowej obiektu lub klasy. |
Użyteczne metody klasy java.lang.Class
Metoda | Przeznaczenie |
getClasses() getDeclaredClasses() | Zwraca tablicę obiektów klasy Class, które są składowymi danej klasy. |
getConstructors() getDeclaredConstructors() | Zwraca tablicę obiektów klasy Constructor; są to konstruktory danej klasy |
getConstructor(Class[]) getDeclaredConstructor(Class[]) | Zwraca obiekt konstruktor (obiekt klasy konstruktor), który ma podane typy argumentów |
getMethods() getDeclaredMethods() | Zwraca tablicę, zawierającą odnośniki do metod klasy. Metody są obiektami klasy Method. |
getMethod(String, Class[]) getDeclaredMethod(String, Class[]) | Zwraca metodę o podanej nazwie i podanych argumentach jako obiekt klsy Method. |
Uwaga : Rozróżnienie pomiędzy metodami mającymi i nie mającymi w nazwie tekstu "Declared" jest następujące:
-
metody bez "Declared" zwracają składowe tylko publiczne, ale jednocześnie również dziedziczone,
-
metody z "Declared" zwracają wszystkie składowe (również prywatne i zabezpieczone), ale bez dziedziczonych
1.3 Przykłady wykorzystania refleksji
Przypomnijmy sobie najpierw
prezentację różnych rodzajów ramek (wyklad o komponentach GUI).
Posłużył
jej następujący program, w którym w sposób dynamiczny uzyskujemy informacje
o polach klasy.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;
import java.lang.reflect.*;
public class Ramki extends JFrame {
// Pola klasy określają różne rodzaje ramek
private Border
// empty = BorderFactory.createEmptyBorder(),
blackLine = BorderFactory.createLineBorder(Color.black),
blueLine3 = BorderFactory.createLineBorder(Color.blue, 3),
// titled1 = BorderFactory.createTitledBorder("Tytuł"),
titled2 = new TitledBorder(blueLine3,"Tytuł",
TitledBorder.CENTER, TitledBorder.CENTER,
new Font("Dialog", Font.BOLD | Font.ITALIC, 16),
Color.blue),
etched = BorderFactory.createEtchedBorder(),
etchedC = BorderFactory.createEtchedBorder(Color.red, Color.yellow),
// raisedBevel = BorderFactory.createRaisedBevelBorder(),
// loweredBevel = BorderFactory.createLoweredBevelBorder(),
matteColor = BorderFactory.createMatteBorder(5, 10, 5, 15, Color.red),
matteIcon = new MatteBorder(new ImageIcon("FastForward24.gif")),
softBevR = new SoftBevelBorder(SoftBevelBorder.RAISED),
softBevL = new SoftBevelBorder(SoftBevelBorder.LOWERED),
compound1 = BorderFactory.createCompoundBorder(softBevR, softBevL),
compound2 = BorderFactory.createCompoundBorder(blueLine3, compound1),
compound3 = BorderFactory.createCompoundBorder(compound1, matteIcon);
Ramki() {
super("Prezentacja ramek");
getContentPane().setLayout (new GridLayout(0,4,5,5));
// Klasa tego obiektu
Class c = getClass();
// Uzyskanie tablicy wszystkich zadeklarowanych pól tej klasy
Field[] field = c.getDeclaredFields();
// Przebiegamy po polach-ramkach
for (int i=0; i< field.length; i++) {
// Nazwa pola (zmiennej) - opisującego kolejną ramkę
String fldName = field[i].getName();
// Tę nazwę wypiszemy na etykiecie
JLabel l = new JLabel(fldName, JLabel.CENTER);
// Uzyskanie referencji do obiektu, reprezentowanego przez
// pole field[i] tego (this) obiektu. Czyli - do kolejnej ramki
Object ramka = null;
try {
ramka = field[i].get(this);
} catch (IllegalAccessException exc) { // Ten wyjatek może wystąpić
exc.printStackTrace(); // gdy dostęp do pola jest zabroniony
} // np. z innej klasy do prywatnego pola
// Dostaliśmy oczywiście ramkę, ale jako Object
// - konieczna konwersja do Border
l.setBorder((Border) ramka);
getContentPane().add(l);
}
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack();
show();
}
public static void main(String[] args) {
new Ramki();
}
}
Skrupulatny Czytelnik zapewne zauważy, zę prezentacja jest teraz nieco inna niż w rozdziale o GUI (zob. rysunek).
W programie, w definicjach pól klasy zmieniono (w stosunku do poprzedniej
prezentacji) niektóre własciwości ramek, niektóre zaś ramki usunięto (za
pomocą komentarzy). Wszystkie manipulacje na zestawie ramek do prezentacji
- dzięki refleksji - wykonujemy wyłącznie "na" polach klasy (definiujących
ramki). Możemy więc w prosty sposób dodac nowe ramki, i - jak widzieliśmy
- usunąć inne.
Takie zastosowanie refleksji znacznie ograniczyło pracochłonności pisania kodu.
Drugi przykład dotyczy dynamicznego wywołania metod.
Program daje użytkownikowi wybór co do następstw
przyciśnięcia
jakiego przycisku. Co więcej, wyborów takich użytkownik może dokonywać
w
fazie wykonania programu.
Zestaw możliwych akcji (na przyciskach) będzie zawarty w klasie
ActionsSet np.
public class ActionSet {
public void dodaj() { show("Dodaj"); }
public void usuń() { show("Usuń"); }
public void zastąp() { show("Zastąp"); }
public void szukaj() { show("Szukaj"); }
public void otwórz() { show("Otwórz"); }
private void show(String string) {
JOptionPane.showMessageDialog(null, string);
}
}
Opcje
dla użytkownika będą przedstawione w menu kontekstowym, otwieranym
na przycisku. Z tego menu może on wybrać (wielokrotnie i różnie w
trakcie
działania programu) co konkretnie ma się stać, jeśli przyciśnie ten
przycisk.
Poniższy wydruk pokazuje konstrukcję programu. Warto zwrócić
uwagę na zmienną liczbę argumentow metod refleksji.
import java.lang.reflect.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class ReflectionTest extends JFrame implements ActionListener {
Method currAction = null; // bieżąca metoda obsługi
Class actionClass = null; // klasa obsługi
Object actionObject = null; // obiekt obsługi
JPopupMenu popUp = null; // menu kontekstowe z wyborem obsługi
JButton b;
public ReflectionTest() {
super("Test refleksji");
try {
actionClass = Class.forName("ActionSet");
actionObject = actionClass.newInstance();
} catch (Exception exc) {
throw new RuntimeException("Wadliwa klasa obsługi");
}
popUp = new JPopupMenu();
createMenuItems();
b = new JButton("Użyj prawego klawisza myszki, by ustalić akcję");
b.setFont(new Font("Dialog", Font.BOLD, 24));
b.addActionListener(this);
b.setComponentPopupMenu(popUp);
setLayout(new FlowLayout());
add(b);
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
void createMenuItems() {
Method mets[] = null;
try {
mets = actionClass.getMethods();
} catch (Exception exc) {
throw new RuntimeException("Niedostępna info o metodach klasy obsługi");
}
for (Method m : mets) {
if (m.getDeclaringClass() == actionClass) {
String name = m.getName();
JMenuItem mi = new JMenuItem(name);
mi.addActionListener(this);
popUp.add(mi);
}
}
}
void setCurrentAction(String action) {
try {
currAction = actionClass.getMethod(action); // zm. liczba arg.!!!
b.setText("Teraz akcją jest: " + currAction.getName() + " - kliknij!");
} catch (Exception exc) {
throw new RuntimeException("Nieznana metoda obsługi");
}
}
public void actionPerformed(ActionEvent e) {
Object src = e.getSource();
if (src instanceof JMenuItem)
setCurrentAction(((JMenuItem) src).getText());
else {
try {
currAction.invoke(actionObject); // zmienna liczba arg. !!!
} catch (Exception exc) {
JOptionPane.showMessageDialog(null, "Akcja na przycisku nieustalona!");
}
}
}
public static void main(String args[]) {
new ReflectionTest();
}
}
Zobacz działanie programu
W programie korzystamy z klasy Method. Uzyskując odnośnik do konkretnej metody,
możemu ustalić ją jako obsługującą zdarzenie (metoda setCurrentAction). Przy
zajściu zdarzenia możemy "pośrednio" (dynamicznie) wywołać jego obsługę.
Służy temu metoda invoke z klasy Method, użyta w actionPerformed.
Ogólnie:
- uzyskanie metody o nazwie name zadeklarowanej w klasie c i mającej określone przez t1, t2, ... typy argumentów:
String name = ....
Class t1 = ....
Class t2 - ...
Method m = c.getDeclaredMethod(name [, t1 [, t2 ...] )
w szczegolności metoda o nazwie name bez argumentów może być uzyskana przez getMethod(name).
- wywolanie metody m na rzecz obiektu trg z argumentami x, y, z:
m.invoke(trg, x, y, x)
2. JavaBeans
2.1. Pojęcie JavaBean
JavaBean - (bean – ziarno) – to programowy komponent "wielokretnego
użytku", którego właściwości i funkcjonalność mogą być odczytywane i/lub
zmieniane uniwersalnymi środkami programistycznymi.
Uwaga: JavaBean oznacza zarówno obiekt, jak i klasę tego obiektu. Rozróżnienie
zawsze jasno wynika z kontekstu. O JavaBean będziemy mówić krócej "ziarno".
Co oznacza powyższa definicja?
Gdy wbudowujemy zewnętrzny komponent-ziarno do naszego programu, mamy do
dyspozycji środki programistyczne, które pozwalają uzyskać o nim informacje:
- jakie ma właściwości? jakie metody służą do ich pobierania i ustalania?
- jakie obsługuje zdarzenia? jakie zdarzenia mogą się mu przytrafiać?
- jakie metody udostępnia otoczeniu (eksportuje)?
Uniwersalność sposobów odczytywania i/lub zmieniana charakterystyk obiektu-ziarna opiera się na:
- uzgodnionym protokole. dotyczącym informacji o ziarnie (standardowe wzorce deklaracji metod i/lub klasy informacyjne)
- standardowych środkach pobierania informacji (introspekcja - realizowana przez klasę Introspector z pakietu java.beans)
- standardowych środkach dostosowania obiektu
2.2. Właściwości i akcesory
Ziarna mają właściwości (atrybuty).
Dostęp do własciwości zapewniają metody klasy-ziarna nazywane akcesorami..
Akcesor pobierający właściwości nazywa się getter, a ustalający – setter.
Wyróżniamy własciwości proste (w tym binarne) oraz właściwości indeksowane.
Właściwości proste mają jedną wartość, właściwości indeksowane – wiele wartości, przedstawianych jako tablica.
Standardowe wzorce deklaracji akcesorów są następujące
Dla prostej (niebinarnej) własciwości o nazwie NNN i typie Typ
getter: Typ getNNN()
setter: void setNNN(Typ)
np. dla ziarna javax.swing.JButton i własciwości background mamy
getter: Color getBackground()
i setter: void setBackground(Color).
Dla właściwości binarnej o nazwie NNN:
getter: boolean isNNN()
setter: void setNNN(boolean)
np. boolean isVisible(), setVisible(boolean)
Dla właściwości indeksowanej o nazwie NNN, której wartości reprezentowane są jako tablice elementów typu Typ:
getter elementu: Typ getNNN(int) // zwraca wartość podanego indeksu właściwości
setter elementu: vois setNNN(int, Typ) // ustala wartość podanego indeksu właściwości
getter tablicy: Typ[] getNNN()
setter tablicy: void setNNN(Typ[])
Właściwości ziarna mogą być związane (bounded).
O zmianie związanej właściwości ziarna mogą być zawiadamiane inne komponenty i reagować na tę zmianę.
Właściwości ziarna mogą być ograniczane (constrained).
Ograniczana właściwość – to taka, o której zmianie powiadamiane są zainteresowane
inne komponenty i są pytane o zgodę na tę zmianę. Jeśli którykolwiek z komponentów
nie da takiej zgody (zawetuje zmianę) – zmiana nie dochodzi do skutku.
Setter związanej i/lub ograniczanej właściwości ma obowiązek wygenerować zdarzenie klasy PropertyChangeEvent..
Klasy-ziarna , mające związane właściwości musżą dostarczyć metody przyłączania słuchaczy zmian właściwości: addPropertyChangeListener(PropertyChangeListener)
Klasy-ziarna, mające ograniczane właściwości, muszą dostarczyć metody addVetoableChangeListener(VetoableChangeListener).
Ziarna możemy wykorzystywać, możemy też je tworzyć (w znaczeniu: definiować klasę ziarna)
Tworzenie ziarna (jako klasy) wymaga zdefiniowania klasy, która:
- stosuje ogólnie przyjęte wzorce sygnatur metod i/lub uzupełniona jest
przez dodatkową specjalną klasę opisującą "niestandardowe" informacje o ziarnie
(implementacja interfejsu BeanInfo)
- zapewnia serializację obiektów
- zawiera konstruktor bezparametrowy
- uzwględnia działania w środowisku wielowątkowym (do obiektu klasy może równocześnie odwoływać się kilka wątków)
2.3. Nasłuch i wetowanie zmian właściwosci
Zmiana właściwości związanej lub ograniczanej powinna generować zdarzenie typu PropertyChangeEvent.
Komponenty (obiekty) zainteresowane w śledzeniu zmian tej własciwości muszą implementować interfejs PropertyChangeListener. W ten sposób stają się słuchaczami zmian właściwości..
Komponenty, które mogą wetować zmiany właściwości muszą implementować interfejs VetoableChangeListener (będą więc słuchaczami zmian właściwości ograniczonych i będę miały możliwość wetowania tych zmian).
Zdarzenie typu PropertChangeEvent możemy zapytać o:
- nazwę właściwości - String getPropertyName()
- starą wartość własciwości (przed zmianą) – Object getOldValue()
- nową wartość własciwości – Object getNewValue()
Interfejs PropertyChangeListener ma jedną metodę:
public void propertyChange(PropertyChangeEvent)
W implementacji tej metody, dowiadując się o zmianach właściwości, możemy na nie odpowiednio reagować.
Również interfejs VetoableChangeListener ma jedną metodę: vetoableChange(...)
z argumentem–zdarzeniem typu PropertyChange. W jej implementacji , gdy dowiemy
się już wszystkich niezbędnych szczegółow o zmianie – możemy ją zawetować.
Wetowanie zmiany odbywa się na zasadzie zgłoszenia wyjątku PropertyVetoException, zatem deklaracja metody vetoableChange wygląda następująco:
public void vetoableChange(PropertyChangeEvent e)
throws
PropertyVetoException
a w jej implementacji – gdy po sprawdzeniu warości właściwości chcemy zgłosić veto – sygnalizujemy wyjątek: throw new PropertyVetoException(...).
Słuchacze zmian właściwości związanych (jak zawsze w Javie) musżą
być przyłączeni do źródła zdarzenia., którym jest w tym przypadku ziarno.
Przyłączenie staje się możliwe, jesli w klasie-ziarnie zdefiniowano metodę addPropertyChangeListener(...)
Musi też być zdefiniowana metoda removePropertyChangeListener, odłaczająca słuchacza.
To samo dotyczy nasłuchu zmian właściwości ograniczanych: klasa zaiarno
musi dostarczyć metody przyłączenia słuchacza: addVetoableChangeListener oraz metody odłączania słuchacza: removeVetoableChangeListener.
Obowiązkiem klasy ziarna, która implementuje związane i/lub ograniczane właściwości
jest również dostarczenie odpowiednich definicji setterów dla tych właściwości.
W setterach należey generować zdarzenie PropertyChangeEvent i propagować go pośród przyłączonych słuchaczy.
W pakiecie java.beans znajdują się dwie klasy narzędziowe, znacznie ułatwiające
wykonanie tych zadań: PropertyChangeSupport i VetoableChangeSupport.
Klasy dostarczają metod generowania zdarzeń i propagacji zdarzeń zmian:
- właściwości związanych - firePropertyChange
- właściwości ograniczanych - fireVetoableChange słuchaczy.
- a także metod przyłączanai i odłączania słuchaczy tych zmian (addNNNListener, removeNNListener).
Konstruktory tych klas mają jako argument referencję do obiektu-ziarna.
Schemat postępowania przy implementacji właściwości związanej jest następujący:
class Ziarno .... {
//wsparcie
private PropertyChangeSupport chg = new PropertyChangeSupport(this);
String text; // to będzie właściwośc związana o nazwie "text"
...
// setter
synchronized void setText(String newTxt) { // pamiętamy o wielowątkowości!
String oldTxt = text; // stara wartość
text = newTxt; // ustalenie nowej wartości
// powiadomienie
chg.firePropertyChange("text", oldTxt, newTxt);
}
....
// metody dodawania i usuwania słuchaczy
public synchronized void addPropertyChangeListener(PropertyChangeListener l) {
chg.addPropertyChangeListener(l);
}
public synchronized void removePropertyChangeListener(PropertyChangeListener l) {
chg.removePropertyChangeListener(l);
}
...
}
Implementacja właściwości ograniczanych musi uzwględniać możliwość zawetowania zmiany przez któregoś ze słuchaczy.
Schemat postępowania przy implementacji właściwości ograniczanej jest następujący:
class Ziarno .... {
//wsparcie
private VetoableChangeSupport veto = new VetoableChangeSupport(this);
String text; // to będzie właściwośc ograniczana o nazwie "tekst"
...
// setter
synchronized void setText(String newTxt) throws PropertyVetoException
{
String oldTxt = text; // stara wartość
// wywołujemy metodę fireVotoableChange, która z kolei
// wywołuje metody vetoableChange zarejestrowanych słuchaczy
// jeśli któraś z nich zgłasza veto, setter kończy działanie
// a wyjątek PropertyVetoException jest przekazywany do obsługi
// przez metodę wywołującą setText
veto.fireVetoableChange("tekst", oldTxt, newTxt);
// Tylko jeśli nikt nie zawetował zmiany:
text = newTxt; // ustalenie nowej wartości
}
....
// metody dodawania i usuwania słuchaczy
public synchronized void addVetoableChangeListener(PropertyChangeListener l) {
veto.addVetoableChangeListener(l);
}
public synchronized void removeVetoableChangeListener(PropertyChangeListener l) {
veto.removeVetoableChangeListener(l);
}
...
}
// Uwaga: wsystkie klasy zdarzeniowe i interfejsy nasłuchu
// dla właściwości znajdują się w pakiecie java.beans.
2.4. JavaBean - przykład praktyczny
Będziemy budować klasę-licznik jako JavaBean. Klasę nazwiemy Counter.
Licznik będzie miał jedną właściwość o nazwie count (stan licznika).
W wersji pierwszej uczynimy tę właściwość związaną (bounded), w wersji drugiej – związaną i ograniczoną (constrained).
Przy budowie aplikacji zastosujemy koncepcję "Model-View-Controller".
Sama klasa Counter odzwierciedla logikę działania licznika ("model
"). Obiekty tej klasy "są niewidzialne", a zatem żeby zobaczyć licznik musimy
stworzyć dodatkową klasę, która zdefiniuje widok licznika (view).
Nazwiemy ją CounterView. Warto zwrócić uwagę: separacja kodu jest
korzystna – widok uniezależniamy od modelu, a model od widoku, w ten sposób
możemy mieć np. wiele widoków licznika, lub zmieniać model nie zmieniając
widoku.
Komunikacja między modelem i widokiem będzie się odbywać na zasadzie nasłuchu
zmian właściwości (zmian właściwości count) czyli obiekt klasy CounterView
będzie też słuchaczem zmian właściwości (PropertyChangeListener).
Musimy też mieć jakieś środki zmiany stanu licznika. Interakcję użytkownika z modelem/widokiem zapewnia tzw. kontroler.
Widzieliśmy, że w komponentach Swingu (w naturalny dla nich sposób) kontroler
połączony jest z widokiem. Tu jednak odseparujemy jego kod od widoku, tworząc
klasę CounterControlGui, zapewniającą interfejs interakcji z licznikiem.. Widok zostanie dodany do tego GUI (ale kody obu klas będą odseparowane).
W wersji drugiej – kiedy właściwość count będzie związana i ograniczana musimy
dostarczyć obiektu-nadzorcy, który będzie sprawdzał czy zmiana właściwości
jest dopuszczalna i jeśli stwierdzi, że nie – będzie wetował tę zmianę. Odpowiednią
klasę nazwiemy CounterLimitator.
W metodzie main(...) klasy Main, w której nasza aplikacja zacznie życie stworzymy
wszystkie odpowiednie obiekty w/w klas i ustanowimy niezbędne połączenia
między nimi.
W sumie logika działania aplikacji będzie wyglądać tak:
Zaczynamy od wersji pierwszej, uboższej, w której klasa Counter daje przykład
typowego programowania JavaBean z wlaściwością związaną:
// Klasa Counter
import java.awt.event.*;
import java.beans.*;
import java.io.*;
public class Counter implements Serializable {
private int count = 0; // właściwość count
// Pomocniczy obiekt do prowadzenia listy słuchaczy zmian właściwości oraz
// propagowania zmian wśród zarejestrowanych złuchaczy
private PropertyChangeSupport propertyChange = new PropertyChangeSupport(this);
// Konstruktory
public Counter() {
this(0);
}
public Counter(int aCount) {
setCount( aCount );
}
// Metody przyłączania i odłączania słuchaczy zmian właściwości
public synchronized void addPropertyChangeListener(PropertyChangeListener listener) {
propertyChange.addPropertyChangeListener(listener);
}
public synchronized void removePropertyChangeListener(PropertyChangeListener l) {
propertyChange.removePropertyChangeListener(l);
}
// Proste metody zwiększania i zmniejszania licznika
public void increment() {
setCount(getCount()+1);
}
public void decrement() {
setCount(getCount()-1);
}
// Getter właściwości "count"
public int getCount() {
return count;
}
// Setter właściowści "count"
public synchronized void setCount(int aCount) {
int oldValue = count;
count = aCount;
// wywołanie metody firePropertChange z klasy PropertyChangeSupport
// powoduje wygenerowanie zdarzenia PropertyChangeEvent i rozpropagowanie
// go wśród wszystkich przyłączonych słuchaczy, ale tylko wtedy, gdy nowa
// wartość właściwości różni się od starej wartości
propertyChange.firePropertyChange("count", new Integer(oldValue),
new Integer(aCount));
}
}
Widok licznika przedstawimy jako etykietę - klasa ta jednocześnie będzie
nasłuchiować zmian właściwości count i odpowiednio do tego zmieniać tekst
na etykiecie (a także wyprowadzać informacje o zmianach właściwości count
na konsolę).
//Klasa CounterView
//Widok licznika przedstawiamy w postaci etykiety
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.beans.*;
public class CounterView extends JLabel implements PropertyChangeListener {
// Konstruktor domyślny: inicjalizuje etykietę tekstem "0"
CounterView() {
this("0");
}
// Konstruktor inicjalizujący etykietę podanym tekstem
CounterView(String lab) {
super(lab);
setOpaque(true); // etykieta nie przezroczysta
// ramka
setBorder(BorderFactory.createLineBorder(Color.black));
// rozmiary i wyrównanie tekstu
setPreferredSize(new Dimension(75, 40));
setHorizontalAlignment(CENTER);
}
// obługa zdarzenia PropertyChange
public void propertyChange(PropertyChangeEvent e) {
Integer oldVal = (Integer) e.getOldValue(),
newVal = (Integer) e.getNewValue();
System.out.println("Value changed from " + oldVal + " to " + newVal);
setText("" + newVal + ""); // pokazanie na etykiecie nowego stanu licznika
}
}
Klasa kontrolera - CounterControlGui dostarcza dwóch przycisków (zwiększ,
zmniejsz licznik) oraz pole tekstowe, w którym można wpisać wartość licznika
(ENTER)
Zarówno kliknięcie w przyciski jak i ENTER na polu tekstowym powoduje powstanie
zdarzenia Action, które tu (w tej klasie) obsługujemy ustalając nowe wartości
licznika za pomocą metod incremet() decrement() i setCount(...) z klasy Counter.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.beans.*;
import java.io.*;
public class CounterControlGui extends JFrame implements ActionListener {
Counter counter;
JButton binc = new JButton("Increment");
JButton bdec = new JButton("Decrement");
JTextField txt = new JTextField(10);
// Konstruktor otrzymuje jako argumenty obiekty typu Counter i CounterView
// Pierwszy jest nam potrzebny do komunikacji z licznikiem, drugi - widok
// wbudujemy w to GUI.
CounterControlGui(Counter c, CounterView clab) {
counter = c;
Container cp = getContentPane();
cp.setLayout(new FlowLayout());
binc.addActionListener(this);
cp.add(binc);
cp.add(clab);
bdec.addActionListener(this);
cp.add(bdec);
txt.addActionListener(this);
cp.add(txt);
setDefaultCloseOperation(3);
pack();
show();
}
// Obsługa akcji
public void actionPerformed(ActionEvent e) {
if (e.getSource() == txt) {
int n = 0;
try {
n = Integer.parseInt(txt.getText());
} catch (NumberFormatException exc) { return; }
counter.setCount(n);
return;
}
String cmd = e.getActionCommand();
if (cmd.equals("Increment")) counter.increment();
else if (cmd.equals("Decrement")) counter.decrement();
else System.out.println("Unrecognized command");
}
}
I wreszcie lacząca wszystko klasa Main, która inicjuje działanie aplikacji.
public class Main {
public static void main(String[] args) {
// Tworzymy obiekty: licznik i jego widok
Counter counter = new Counter();
CounterView counterView = new CounterView(""+counter.getCount());
// Rejestrujemy widok jako słuchacza zmian licznika
counter.addPropertyChangeListener(counterView);
// Tworzymu GUI kontrolera i pokazujemy go
CounterControlGui gui = new CounterControlGui(counter, counterView);
gui.pack();
gui.show();
}
}
Dizałanie aplikacji ilustruje rysunek oraz komunikaty na konsoli, powstałe
po kolejnych klilnięciach w przyciski Incremenet i Decrement oraz wprowadzeniu
liczby 23 w polu tekstowym i naciśnieciu ENTER.
Value changed from 0 to 1
Value changed from 1 to 0
Value changed from 0 to 23
Wersja 2
W wersji drugiej chcemy dodać obiekt – nadzorcę, który będzie sprawdzał czy
zmiana licznika jest dopuszczalna, a jeśli nie to będzie wetował tę zmianę.
Mechanizm wetowania polega na sygnalizowaniu wyjątku PropertyVetoException.
Wyjątek ten będzie sygnalizowany przez obiekt-nadzorcę, który jest jednoczesnie
słuchaczem zmian wartości właściwości ograniczonych (VetoableChangeListener).
Klasa definiująca obiekty nadzorujące nasz licznik wygląda tak.
import java.beans.*;
public class CounterLimitator implements VetoableChangeListener {
// minimalne i makszymalne dopuszczalne wartości licznika
private int min, max;
CounterLimitator(int minLim, int maxLim) {
min = minLim;
max = maxLim;
}
// Obsługa zdarzenia vetoableChange
// metoda może sygnalizować PropertyVetoException
public void vetoableChange(PropertyChangeEvent e)
throws PropertyVetoException {
Integer newVal = (Integer) e.getNewValue();
int val = newVal.intValue();
// Sprawdzamy, czy zmiana licznika jest dopuszczalna,
// jeśli nie – sygnalizujemy wyjatek PropertyVetoException
if (val < min || val > max)
throw new PropertyVetoException("Niedopuszczalna zmiana wartości", e);
}
}
W klasie Counter musimy poczynić zmiany, po to by właściwość count była zarazem związana i ograniczana.
Wykorzystamy podobną do PropertyChangeSupport klasę pomocniczą VetoableChangeSupport
Obiekt tej klasy nazwiemy vetos, dostarczymy też metod przyłączania i odłączania słuchaczy zmian ograniczanych.
public class Counter {
//...
private VetoableChangeSupport vetos = new VetoableChangeSupport(this);
public synchronized void addVetoableChangeListener(VetoableChangeListener l) {
vetos.addVetoableChangeListener(l);
}
public synchronized void removeVetoableChangeListener(VetoableChangeListener l) {
vetos.removeVetoableChangeListener(l);
}
// ...
}
Zmianie ulegnie też metoda setCount.
public class Counter {
//...
public synchronized void setCount(int aCount)
throws PropertyVetoException {
int oldValue = count;
// wywołujemy metodę fireVotoableChange, która z kolei
// wywołuje metody vetoableChange zarejestrowanych słuchaczy
// jeśli któraś z nich zgłasza veto, setter kończy działanie
// a wyjątek PropertyVetoException jest przekazywany do obsługi
// przez metodę wywołującą setCount
// (co zaznaczyliśmy w nagłówku metody przez
// throws PropertyVetoException)
vetos.fireVetoableChange("count", new Integer(oldValue), new Integer(aCount));
// tylko jeśli nikt nie zawetował
count = aCount; // ustalamy nową wartość licznika
// ... powiadamiamy nasłuchujących zmiany właściwości count
propertyChange.firePropertyChange("count", new Integer(oldValue),
new Integer(aCount));
}
//...
}
Ponieważ metody increment, decrement i konstruktory wywołują metodę setCount,
to w sygnaturach tych metod i konstruktorów musimy dodać informację, że wyjątek
PropertyVetoException ma być obsługiwany przez wołającego te metody.
A wołamy je z GUI kontrolera.
Tam jest miejsce do obsługi tego wyjątku:
public class CounterControlGui .... {
....
public void actionPerformed(ActionEvent e) {
try {
if (e.getSource() == txt) {
int n = 0;
try {
n = Integer.parseInt(txt.getText());
} catch (NumberFormatException exc) { return; }
counter.setCount(n);
return;
}
String cmd = e.getActionCommand();
if (cmd.equals("Increment")) counter.increment();
else if (cmd.equals("Decrement")) counter.decrement();
else System.out.println("Unrecognized command");
} catch (PropertyVetoException exc) { // obługa wyjatku:
System.out.println(""+ exc); // podanie informacji
//o niedopuszczalnej zmianie wartości
}
}
//...
}
W klasie Main dodajemy fragment dotyczący tworzenia obiektu klasy CounterLimitator
(nadzorcy) i rejestrujemy go jako słuchacza zmian właściwości ograniczanej:
import java.beans.*;
public class Main {
public static void main(String[] args) throws PropertyVetoException {
Counter counter = new Counter();
CounterView counterView = new CounterView(""+counter.getCount());
counter.addPropertyChangeListener(counterView);
// licznik może się zmieniać od –5 do 10
// bo Limitator zawetuje każdą inną zmianę
CounterLimitator clim = new CounterLimitator(-5, 10);
counter.addVetoableChangeListener(clim);
CounterControlGui gui = new CounterControlGui(counter, counterView);
gui.pack();
gui.show();
}
}
Dzialanie programu (przy tym samym GUI kontrolera) zilustrujemy komunikatami
na konsoli po kolejneych kliknięciach w przycisk "Decrement".
Value changed from 0 to -1
Value changed from -1 to -2
Value changed from -2 to -3
Value changed from -3 to -4
Value changed from -4 to -5
java.beans.PropertyVetoException: Niedopuszczalna zmiana wartości
Jak widać, CounterLimitator nie dopuścił do zmiany wartości z -5 na -6.
2.5. Introspekcja
Srodowisko programistyczne, które ma umożliwiać dynamiczne odczytywanie własności
i funkcjonalności dowolnych "podrzucanych" mu ziaren analizuje ziarna za
pomocą uniwerslanych metod introspekcji.
Introspekcja kojarzy dwa mechanizmy:
- analizę komponentów za pomocą metod refleksji przy założeniu, że stosowane
są pewne standardowe wzorce nazewnictwa, umożliwiające określenie własnściwości
i ich typu, metod pobierania i ustalania tych właściwości, rodzajóa zdarzeń,
metod rejestracji słuchaczy i nnych metod udostępnianych przez dane ziarno
"na zewnątrz"
- określanie uzewnętrznianych właściwości i funkcjonalności ziarna na
podstawie dowolnie specyfikowanych przez twórcę ziarna elementów, zapisywanych
w odpowiedniej dla danego ziarna klasie BeanInfo.
Oba mechanizmy mogą być stosowane łącznie. W prostych przypadkach (prostych
ziaren) nie ma potrzeby żmudnego tworzenia klasy BeanInfo, metody refleksji
są całkiem wystarczające.
Introspekcja za pomocą metod refleksji jest możliwa dzięki kontraktowi dotyczącemu wzorców nazewnictwa.
Dla właściwości introspekcja określa pary metod get... (is...) - set...
z tymi samymi nazwami właściwości i z odpowiednimi sygnaturami. Może się
okazać, że niektóre właściwości są tylko do odczytu lub tylko do zapisu.
Uwzględnia się też indeksowane własciwości.
Dla określenia, czy istnieją możliwość obsługi zdarzeń i jakich używane są wzorce: addXXXListener i removeXXXListener.
Samą introspekcje realizuje klasa Introspector.
Introspector analizuje klasę-zarna (i nadrzędne wobec niej klasy oraz implementowane
interfejsy) zbierając informacje o właściowościach, metodach, zdarzeniach.
Informacja ta jest umieszczana w obiekcie typu BeanInfo (BeanInfo jest nazwą interfejsu). Wobec tego obiektu możemy następnie zastosować metody zwracające iinformacje o ziarnie.
Np. analizę klasy-ziarna javax.swing.JButton uzyskujemy przez następujące odwołanie:
BeanInfo info = Introspector.getBeanInfo(Class.forName("javax.swing.JButton"));
lub
BeanInfo info = Introspector.getBeanInfo(javax.swing.JButton.class));
Następnie wobec obiektu info możemu zastosować metody interfejsu BeanInfo:
- EventSetDescriptor[] getEventSetDescriptors() - zwraca tablicę deskryptorów zdarzeń
- PropertyDescriptor[] getPropertyDescriptors() - zwraca tablicę deskryptorów własności
- MethodDescriptor[] getMethodDescriptors() - zwraca tablicę deskryptorów metod
- dla każdgo deskryptora metody ParameterDescriptor[] getParameterDexriptors() - zwraca tablicę deskryptorów parametrów tej metody
Zwracane tablice są tablicami obiektów odpowiednich klas. Wobec tych obiektów
stosujemy metody tych klas pozwalające na uzyskiwanie różnej konkretnej informacji.
Np. program na poiniższym wydruku analizuje klasę podaną jako argument i wypisuje niektóre informacje o niej:
import java.lang.reflect.*;
import java.beans.*;
public class BeanAnalyze {
static void say(String s) { System.out.println(s); }
public static void main(String[] arg) throws Exception {
BeanInfo beanInfo = Introspector.getBeanInfo(Class.forName(arg[0]));
PropertyDescriptor[] pd = beanInfo.getPropertyDescriptors();
MethodDescriptor[] md = beanInfo.getMethodDescriptors();
EventSetDescriptor[] evd = beanInfo.getEventSetDescriptors();
say("Właściwości:");
for (int i = 0; i < pd.length; i++) {
say(pd[i].getShortDescription());
// getReadMethod i getWriteMethod zwracają obiekty typu Method
say(" getter: "+ pd[i].getReadMethod());
say(" setter: "+ pd[i].getWriteMethod());
}
say("\nMetody:");
for (int i=0; i<md.length; i++) {
say(" " + md[i].getMethod());
}
say("\nZdarzenia:");
for (int i = 0; i < evd.length; i++) {
say("Zdarzenie : " + evd[i].getShortDescription());
Method[] met = evd[i].getListenerMethods();
say("Metody obsługi:");
for (int j=0; j < met.length; j++) say(" " + met[j]);
}
}
}
Fragmenty (dużego!) wydruku z programu uruchomionego z argumentem javax.swing.JButton:
Właściwości:
UI
getter: public javax.swing.plaf.ButtonUI javax.swing.AbstractButton.getUI()
setter: public void javax.swing.AbstractButton.setUI(javax.swing.plaf.ButtonUI)
UIClassID
getter: public java.lang.String javax.swing.JButton.getUIClassID()
setter: null
accessibleContext
getter: public javax.accessibility.AccessibleContext javax.swing.JButton.getAccessibleContext()
setter: null
action
getter: public javax.swing.Action javax.swing.AbstractButton.getAction()
setter: public void javax.swing.AbstractButton.setAction(javax.swing.Action)
actionCommand
getter: public java.lang.String javax.swing.AbstractButton.getActionCommand()
setter: public void javax.swing.AbstractButton.setActionCommand(java.lang.String)
actionListeners
getter: public java.awt.event.ActionListener[] javax.swing.AbstractButton.getActionListeners()
setter: null
actionMap
getter: public final javax.swing.ActionMap javax.swing.JComponent.getActionMap()
setter: public final void javax.swing.JComponent.setActionMap(javax.swing.ActionMap)
...
Metody:
public boolean java.awt.Component.action(java.awt.Event,java.lang.Object)
public synchronized void java.awt.Component.add(java.awt.PopupMenu)
public java.awt.Component java.awt.Container.add(java.awt.Component)
public java.awt.Component java.awt.Container.add(java.awt.Component,int)
public void java.awt.Container.add(java.awt.Component,java.lang.Object)
public void java.awt.Container.add(java.awt.Component,java.lang.Object,int)
public java.awt.Component java.awt.Container.add(java.lang.String,java.awt.Component)
public void javax.swing.AbstractButton.addActionListener(java.awt.event.ActionListener)
...
Zdarzenia:
Zdarzenie : action
Metody obsługi:
public abstract void java.awt.event.ActionListener.actionPerformed(java.awt.event.ActionEvent)
Zdarzenie : ancestor
Metody obsługi:
public abstract void javax.swing.event.AncestorListener.ancestorMoved(javax.swing.event.AncestorEvent)
public abstract void javax.swing.event.AncestorListener.ancestorAdded(javax.swing.event.AncestorEvent)
public abstract void javax.swing.event.AncestorListener.ancestorRemoved(javax.swing.event.AncestorEvent)
Zdarzenie : change
Metody obsługi:
public abstract void javax.swing.event.ChangeListener.stateChanged(javax.swing.event.ChangeEvent)
...
Szczerze mowiąc, za pomocą metod instrospekcji sporo można się dowiedziec o dostępnych właściwościach i metodach danej klasy.
Oczywiście, mając obiekty-metody (uzyskane z deskryptorów metod) mozemy je
dynamicznie wyowływac środkami refleksji. Ale istnieje też nieco prostsza,
specjalnie dla JavaBean przygotowana możliwość dynamicznego pobierania i
ustalania wlaściwości oraz wołania innych metod.
2.6. Dynamiczne pobieranie i ustalanie właściwości
W pakiecie java.beans znajdziemy dwie ciekawe klasy Statement i Expression
Obiekt klasy Statement tworzymy za pomocą konstuktora:
Statement s = Statement( Object target,
String methodName,
Object[] arguments );
a wywołanie na jego rzecz metody void execute() spowoduje wywołanie na rzecz obiektu target metody o nazwie methodName z argumentami podanymi w tablicy arguments
Zwykle stosujemy tę procedure do ustalania właściowości JavaBean, ale może
ona być rownież stosowana do wyołania dowolnych metod
Klasa Expression dziedziczy klasę Statement. Czyli tu też możemy wołać metodę
execute(). Jednak jej główne zastosowanie polega na dynamicznym pobieraniu
właściwości, lub - inaczej - dynamicznym wołaniu metod, które zwracają wyniki
i uzyskiwaniu dostępu do tych wyników.
Obiekt klasy Expression tworzymy za pomocą konstuktora:
Expression e = Expression( Object target,
String methodName,
Object[] arguments );
-
wywołanie na jego rzecz metody void execute() spowoduje wywołanie
na rzecz obiektu target metody o nazwie methodName z argumentami podanymi
w tablicy arguments oraz przechowanie wyniku wywołanej metody w obiekcie
Expresion; wynik ten będzie dostępny przez wywolanie metody Object getValue()
na rzecz obeiktu Expressiom
- samo wywołanie metody Object getValue() spowoduje ten sam efekt o ile wynik dla danego obiektu-wyrażenia nie został jeszcze ustalony.
Szczegółowo komentowany program ilustruje zastosowanie tych klas.
W poniższym przykładzie metody klas Statement i Expression bedziemy stosowac
wobec obiektów klasy JButton oraz naszej własnej klasy TestBean, która wygląda
tak:
public class TestBean {
private String[] headers;
private int count;
public TestBean() {
}
public TestBean(int n) {
count = n;
}
public String[] getHeaders() {
return headers;
}
public void setHeaders(String[] value) {
headers = value;
}
public int getCount() {
return count;
}
public void setCount(int value) {
count = value;
}
}
Szczegółowo komentowany program ilustruje zastosowanie klas Statement i Expression,
dając też pewne dodatkowe na ich temat informacje.
import java.beans.*;
import java.awt.*;
import javax.swing.*;
public class DynamicExec {
public static void main(String[] args) throws Exception {
Statement stmt;
Expression expr;
JButton b = new JButton();
// Na rzecz przycisku wołamy dynamicznie metodę setText
// z argumentem "Przycisk"
stmt = new Statement(b, "setText", new Object[] { "Przycisk" });
stmt.execute();
// Jaki wynik? Najpierw statyczne odwołanie
System.out.println("Tekst na przycisku 1: " + b.getText());
// Teraz dynamicznie: stwórzmy wyrażenie, którego wynikiem
// jest wynik podanej metody z podanymi argumentami wywolanej
// na rzecz b
// Uwaga: brak argumentów - czyli tablica Object o rozmiarze 0
expr = new Expression(b, "getText", new Object[0]);
// Jeżeli wyrażenie expr nie ma jeszcze wyniku
// metoda getValue() wywołuje podaną w wyrażeniu metodę
// i zwraca jej wynik; w przeciwnym razie zwraca
// ustalony wczesniej wynik
String txt = (String) expr.getValue();
System.out.println("Tekst na przycisku 2: " + txt);
// Możemy też stosować klasy Statement i Expression
// wobec naszych własnych klas JavaBeans
TestBean tbean = new TestBean();
// Uwaga: przy przekazywaniu argumentów i zwrocie wynikow
// następują automatyczne przeksztalcenia pomiedzy
// typami prostymi i odpowiadającymi im klasami opakowującymi
// np. int - Integer - setCount wymaga argumentu int,
// my podajemy Integer
stmt = new Statement(tbean, "setCount",
new Object[] { new Integer(22) });
stmt.execute();
// Jaka jest teraz wartość właściwości count
// I znowu: getCount() zwraca int, my odbieramy Integer
expr = new Expression( tbean, "getCount", new Object[0] );
Integer val = (Integer) expr.getValue();
System.out.println("Wartość count: " + val);
// Czy możemy działać na tabliach? Ależ tak!
stmt = new Statement(tbean, "setHeaders",
new Object[] { new String[] { "a", "b" } }
);
stmt.execute();
expr = new Expression(tbean, "getHeaders", new Object[0]);
String[] hdr = (String[]) expr.getValue();
System.out.println("Ustalone nagłówki");
for (int i=0; i<hdr.length; i++)
System.out.println(hdr[i]);
// Możemy nawet stworzyć nowy obiekt
// używając specjalnej nazwy metody - new (oczywiście)
expr = new Expression(TestBean.class, "new",
new Object[] { new Integer(111) }
);
TestBean tb2 = (TestBean) expr.getValue();
expr = new Expression (tb2, "getCount", new Object[0]);
val = (Integer) expr.getValue();
System.out.println("W nowym obiecie count = " + val);
}
}
2.7. Serializacja JavaBeans
Oprócz zapisywania do strumieni obiektowych (ObjectOutputStream), obiekty
JavaBeans można serializowac w postaci tekstowej, w formacie XML (wersja
1.0, kodowanie UTF-8). Jest to nawet - w przypadku JavaBeans - bardziej "przenośny"
sposób utrwalania obiektów.
Zapisywaniem obiektów zajmuje się klasa XMLEncoder, a ich odtwarzaniem -
klasa XMLDccoder. Przy tym stan obiektu zapisywany jest w postaci zdatnej
do wykorzystania przez klasy Statement i Expression przy odtwarzaniu tego
obiektu
Najprostsze (przeznaczone wyłącznie dla JavaBeans) zastosowanie tych klas pokazuje poniższy program.
import java.beans.*;
import java.awt.*;
import javax.swing.*;
import java.io.*;
public class SerialBean {
String fname = "test.xml";
public SerialBean() {
JButton b = new JButton("Kąśliwie wróbel ćwierkał");
b.setBackground(Color.red);
b.setForeground(Color.yellow);
try {
XMLEncoder enc = new XMLEncoder(
new BufferedOutputStream(
new FileOutputStream(fname)
)
);
enc.writeObject(b);
enc.close();
} catch (FileNotFoundException exc) {
exc.printStackTrace();
System.exit(1);
}
nowReadAndReport();
}
private void nowReadAndReport() {
try {
XMLDecoder dec = new XMLDecoder(
new BufferedInputStream(
new FileInputStream(fname)));
Object obj = dec.readObject();
JButton b = (JButton) obj;
dec.close();
System.out.println("Napis na przycisku: " + b.getText());
System.out.println("Kolor tła: " + b.getBackground());
System.out.println("Kolor tekstu : " + b.getForeground());
} catch (FileNotFoundException exc) {
exc.printStackTrace();
System.exit(1);
}
}
public static void main(String[] args) {
SerialBean serialbean = new SerialBean();
}
}
Wyprowadzi on na konsolę właściwą informację o stanie zapisanego a później odtworzonego obiektu:
Napis na przycisku: Kąśliwie wróbel ćwierkał
Kolor tła: java.awt.Color[r=255,g=0,b=0]
Kolor tekstu : java.awt.Color[r=255,g=255,b=0]
Z ciekawości można zajrzeć do pliku test.xml.
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.4.1-rc" class="java.beans.XMLDecoder">
<object class="javax.swing.JButton">
<string>KÄĹ>liwie wrĂłbel Ä+wierkaĹ'</string>
<void property="background">
<object class="java.awt.Color">
<int>255</int>
<int>0</int>
<int>0</int>
<int>255</int>
</object>
</void>
<void property="foreground">
<object class="java.awt.Color">
<int>255</int>
<int>255</int>
<int>0</int>
<int>255</int>
</object>
</void>
</object>
</java>
Faktycznie, jest do dokładny szablon gotowy do zastosowania klas Expression
(dla metody-kostsruktora new z argumentem String) i Statement (dla setBackground
i setForeground). Wszystko opiera się na zachowaniu protokołu JavaBeans.
Oczywiście, możemy w ten sposób serializować obiekty prawie wszystkich klas
Javy (bo prawie wszystkie są JavaBeans) oraz własnych klas, spełniających
protokół JavaBeans.
A nawet można uwzględnić pewne odstępstwa od tego
protokołu (np. inicjację właściwości JavaBeans w konstruktorz, bez użycia
setterów) poprzez dostosowanie delegata "persystencji", do którego odwołuje
się XMLEncoder - klasy DefaultPersistanceDelegate.
O takim bardziej zaawansowanym zastosowaniu, jak również o użyciu nasłuchu wyjątków dekodowania za pomocą interfejsu ExceptionListener można przeczytać w dokumentacji API Javy.
2.8. Inne zagadnienia związane z JavaBeans
To skrótowe wprowadzenie do JavaBeans nie wyczerpuje tematu.
Warto więc na koniec zwrócić uwagę na nieporuszone tu ważne kwestie
- przystosowanie ziaren: klasy-edytory właściwości używane m.in. w środowiskach wizualnych
- tworzenie klas BeanInfo, opisujących informacje niedostępną metodami refleksji,
- koteksty (BeanContext) - swoiste kontenery, dostarczające generalnych mechanizmów i serwisów dla JavaBeans.
Informacje na te tematy można zanaleźć w dokumentacji.
3. Metadane i adnotacje
3.1. Wprowadzenie
Metadane to dane opisujące dane. W przypadku programowania chodzi
o takie metadane, które opisują i uzupełniają kod źródłowy w znaczeniu semantycznym
Coraz częściej metadane są obecne w językach programowania.
W Javie częściowo występowały już wcześniej - w samym środowisku (dokumentacyjne np. @author),
a częściowo jako zewnętrzne uzupełnienia (np. XDoclet, JBoss AOP).
W C# nazywają się (nieco nieszczęśliwie) atrybutami.
Java 5 rozszerza i standaryzuje zastosowanie metadanych na platformie Javy poprzez mechanizm adnotacji.
Zastosowania (bardzo różnorodne), m.in.
- do celów poprawy niezawodności programowania (np. @override),
- do generacji dodatkowych kodów programu (uzupełniających klas),
- do określania sposobu funkcjonowaniu programu w fazie wykonania (można krótko zapisać różne rzeczy).
- do celów konfiguracyjnych,
- do generowania pomocniczych plików (np. różnych deskryptorów itp.)
Mechanizm adnotacji jest najważniejszym praktycznie uzupełnieniem Javy
w wersji 5, choćby dlatego, że już teraz kolejne wersje dużych platform,
takich jak J2EE, a także pakiety narzędziowe , takie jak JDBC 4.0, intensywnie używają
adnotacji.
Zalety adnotacji:
- zastosowanie adnotacji może znacznie ułatwić pisanie i wdrażanie aplikacji,
szczególnie dużych; kody mogą być wielokrotnie mniejsze, pracochłonność znacząco
ograniczona itp.
Wady:
- pojawiają się jakby nowe elementy składniowo-semantyczne; ponieważ
każdy może definiować własne adnotacje, to istnieje niebezpieczeństwo, że
pojawi się mnóstwo "dialektów" Javy - czyli standard Javy + wymyślne, niestandardowe
adnotacje,
- adnotacje pozwalają upraszczać wiele rzeczy, zapisywać niejako symbolicznie
jednym słowem, to co bez nich wymagało być może dziesiątek linii kodu; ale
tych zapisów (notacji i ich znaczeń) trzeba się też uczyć (dla różnych środowisk
za każdym razem od nowa) i mieć świadomość, do czego tego tak naprawdę te
skróty prowadzą (co robią),
- na razie nie ma dobrej koncepcji sprawdzania błędowi na poziomie metadanych.
3.2 Rodzaje adnotacji
Można wyróżnić następujące rodzaje adnotacji:
- adnotacje wbudowane w Javę
- adnotacje użytkownika (tworzone przez użytkownika)
- stosowane na etapie kompilacji (przetwarzane przez narzędzia)
- stosowane w fazie wykonania (za pomocą mechanizmów refleksji)
Adnotacje mogą byś stosowane wobec:
-
pakietów
-
klas
-
interfejsów
-
pól
-
konstruktorowi
-
metod
-
zmiennych lokalnych
-
innych adnotacji
Można wyraźnie zaznaczyć do czego odnosi się nowodefiniowana adnotacja za pomocą metaadnotacji @Target (wtedy inne jej zastosowanie będzie wykryte jako błąd w fazie kompilacji).
Możliwe znaczenia (wartości) metaadnotacji @Target
ANNOTATION_TYPE - dana adnotacja adnotuje inną,
PACKAGE - dotyczy pakietu,
TYPE - klas i interfejsów
METHOD - metod
CONSTRUCTOR - konstruktorów
FIELD - pól
PARAMETER - parametrów
LOCAL_VARIABLE - zmiennych lokalnych
Przykład (fragment) definicji adnotacji o nazwie Test, która może dotyczyć tylko metod:
@Target(ElementType.METHOD)
public @interface Test
Uwagi:
- adnotacja nie oznaczona znacznikiem @Target ma zastosowanie wszędzie.
- aby określić kilka możliwych zastosowań piszemy @Target({ a, b, c }
), gdzie a, b, c to elementy w postaci ElementType.rodzaj, a rodzaj to jeden
z PACKAGE, METHOD, FIELD itp.
Siła adnotacji polega na tym, że mogą one być przetwarzane:
-
przez narzędzia do generacji (i ew. równoczesnej kompilacji) kodu (wtedy potrzebne są tylko w fazie kompilacji),
-
albo przez mechanizmy refleksji - wtedy powinny być zapisane w klasie wykonywalnej
i dostępne dla tych mechanizmów w fazie wykonania programu,
-
albo przez narzędzia przetwarzania "binarnej" postaci klas (umożliwiające
m.in. różne iniekcje, zmiany, usuwanie kodu binarnego) - wtedy powinny być
zapisane w klasie, ale nie muszą być dostępne dla mechanizmów refleksji.
Odpowiadają temu trzy polityki utrzymywania adnotacji, specyfikowane przez metaadnotację @Retention.
Polityki utrzymywania adnotacji
RetentionPolicy.SOURCE - tylko w żródle,
RetentionPolicy.CLASS - w klasie skompilowanej, ale niedostępne w fazie wykonania,
RetentionPolicy.RUNTIME - dostępne w fazie wykonania.
Przykład:
@Retention(RetentionPolicy.SOURCE) // utrzymanie tylko w kodzie adnotacji AdapterFor
public interface @AdapterFor
// zostanie przetworzona przez narzędzia w fazie
// kompilacji
Wśrod metaadnotacji dostępne są jeszcze:
@Documented - mowi o tym, że dokumentacja działania
adnotacji ma być włączona do dokumentacji wszystkich oznaczanych przez
nią elementów
@Inherited - mówi o tym, że oznaczana przez nią adnotacja
(zaznaczająca klasy) ma być dziedziczona przez podklasy zaznaczonych
klas.
Zestaw regularnych, wbudowanych w Javę adnotacji jest na razie bardzo niewielki i obejmuje:
@Deprecated - zaznacza dowolny element jako spadkowy ("przestarzały"),
@SupressWarnings - blokuje ostrzeżenia (podanego typu) ze strony kompilatora,
@Override - stosowana wobec metod, oznacza intencję programisty przedefiniowania
metody z nadklasy, dzięki czemu jest możliwość sprawdzenia w fazie kompilacji
czy programista nie popełnił błędu,
Ta ostatnia adnotacja jest b. użyteczna i należy ją stosować.
Dzięki temu unikniemy błędów niewykrywalnych nie tylko w fazie kompilacji, ale również czasem w fazie wykonania.
Np.
class Push extends JButton {
public Dimension getPrefferedSize() { ... }
}
Tutaj nastąpiła pomyłka w nazwie metody - wobec czego pojawia się nowa metoda
(nigdy nie wołana), a właściwa (getPreferredSize()) nie jest przedefiniowana.
Błędu nie ma ani w kompilacji ani w fazie wykonania (oprócz - być może
nie zawsze, nie od razu, nie w każdych okolicznościach - widocznych niewłaściwych
rozmiarów przycisku).
Gdy napiszemy:
class Push extends JButton {
@Override public Dimension getPrefferedSize() { ... }
}
to kompilator wykryje błąd i powiadomi nas o tym.
Jak widać, składnia zastosowania adnotacji jest bardzo prosta.
Adnotacje zaczynają sie znakiem @.
Adnotacje poprzedzają inne kwalifikatory elementów (klas, metod, pól).
3.3. Definiowanie adnotacji
Adnotacje są definiowane jako swego rodzaju interfejsy, za pomocą słowa @interface
Wewnątrz takiego interfejsu dostarcza się deklaracji danych, które adnotacja może zawierać.
[ew. kwalifikacja dostępu] @interface NazwaAdnotacji {
deklaracja1
deklaracja2
. . .
deklaracjaN
}
Każda deklaracja ma postać:
typ nazwaDanej();
albo
typ nazwaDanej() default wartość_domyślna;
Przy zastosowaniu adnotacji możemy podać konkretne dane:
@NazwaAdnotacji(nazwaDanej1=wartość1, nazwaDanej2=wartość2, . . .)
Typy danych w adnotacji mogą być następujące:
-
typy proste (int, short, long, byte, char, double, float, boolean),
-
String,
-
typ referencyjny, reprezentowany przez Class, w tym także w wersji sparametryzowanej,
-
enum.
-
adnotacja,
-
tablica (w/w elementów).
Przykład:
public @interface Opis {
String text() default "Brak opisu";
int version() default 1;
}
// i zastosowanie np. do opisu klasy:
@Opis(text="Klasa warzyw", version=2)
public class Warzywa { ... }
Można też tak:
@Opis(version=5, text="Klasa warzyw")
albo:
@Opis(text="Klasa warzyw")
tu pominięte dane przyjmą wartości domyślne,
w szczególności:
@Opis
oznacza to samo co
@Opis()
i co
@Opis(text="Brak opisu", version=1)
Mamy też szczególny przypadek, kiedy można pominąć nazwę danych i znak =. Mianowicie:
public @interface JakaśAdnotacja {
jakiśTyp value()
}
Wtedy można pisać np. tak (jeśli jakiśTyp to int):
@JakaśAdnotacja(111)
nadając danej oznaczanej przez value wartość 111.
Dane konkretnych adnotacji mogą być uzyskiwane od nich (w fazie wykonania
lub kompilacji) poprzez odwołania do "metod" interfejsu definiującego adnotację
(np. jeśli annot - jest uzyskaną metodami refleksji lub przez narzędzia przetwarzania
w fazie kompilacji adnotacją @Opis, to możemy wołać:
String op = annot.txt();
int v = annot.version();
3.4. Przetwarzanie adnotacji w fazie wykonania
Aby przetwarzać - i odpowiednio stosować - adnotacje w fazie wykonania, należy:
- ustalić rodzaj adnotacji przez @Retention(RetentionPolicy.RUNTIME),
- użyć metod refleksji do uzyskania informacji które elementy i za pomocą jakich adnotacji są zaznaczane,
- od uzyskanych adnotacji dowiedzieć się ew. dodatkowych informacji,
zapisanych w nich jako dane; po czym na podstawie adnotacji i tych danych
dynamicznie wytworzyć i wykonać odpowiedni kod.
Dla każdego elementu programu (uzyskanego dynamicznie): Class, Method, Field
itp. możemy użyć metod, które zwracają informację o adnotacjach zastosowanych
wobec tego elementu.
|
getAnnotation(Class<T> annotationType)
Zwraca adnotację
podanego typu, albu null, jeśli element nie jest oznaczony adnotacją. |
Annotation[] |
getAnnotations()
Zwraca wszystkie adnotacje dla tego elementu |
Annotation[] |
getDeclaredAnnotations()
Zwraca wszystkie
adnotacje bezpośrednio zastosowane (z pominieciem dziedziczonych) |
boolean |
isAnnotationPresent(Class<? extends Annotation> annotationType)
Zwraca true, jeśli wobec elementu zastosowano adnotację podanego typu. |
Co to jest typ adnotacji?
Nic innego jak nazwa interfejsu który ją definiuje. Np. dla adnotacji, zdefiniowanej jako:
public @interface Opis { ... }
typem jest:
Opis.class
albo
Class.forName("Opis");
Uwagi:
- przy zastosowaniu nazwanych pakietów nazwy interfejsów adnotacji muszą być kwalifikowane nazwą pakietu,
- wszystkie adnotacje (jako interfejsy) rozszerzają interfejs Annotation,
wobec czego mogą być traktowane jako Annotation i dlatego takie są wyniki
w/w metod; zwróćmy uwagę, że dzięki parametryzacji nie musimy stosować konwersji
zawężających.
Przykład.
Stworzymy i zastosujemy adnotację, dzięki której w prosty sposób w kodzie
źródłowym będziemy ustalać do jakich kontenerów mają być wkładane wybrane
komponenty GUI.
Adnotację nazwiemy Loc (od locate).
import java.lang.annotation.*;
import java.awt.*;
@Target(ElementType.FIELD) // do oznaczania pól
@Retention(RetentionPolicy.RUNTIME) // faza wykonania
public @interface Loc {
String to();
}
Adnotacja ma jedną "daną", swoisty atrybut, o nazwie to, który będzie reprezentował
za każdym razem nazwę zmiennej oznaczającej kontener do którego dany komponent
(oznaczony tą adnotacją) ma być dodany.
A oto jej zastosowanie:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.lang.reflect.*;
public class Annot0 extends JFrame {
JComponent cp = (JComponent) getContentPane();
@Loc(to="cp") JPanel p1 = new JPanel();
@Loc(to="cp") JPanel p2 = new JPanel();
@Loc(to="p1") JButton b1 = new JButton("Przycisk 1");
@Loc(to="p1") JButton b2 = new JButton("Przycisk 2");
@Loc(to="p2") JButton b3 = new JButton("Przycisk 3");
@Loc(to="p2") JButton b4 = new JButton("Przycisk 4");
@Loc(to="p2") JButton b5 = new JButton("Przycisk 5");
public Annot0() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));
try {
locateComponents(); // metoda ta zajmie się wkładaniem do kontenerow
} catch(Exception exc) {
exc.printStackTrace();
}
pack();
show();
}
//....
}
Metoda locateComponents() nie jest trudna do napisania, jeśli tylko choć trochę władamy metodami refleksji:
private void locateComponents() throws Exception {
Class klasa = getClass();
for (Field f : klasa.getDeclaredFields()) { // po polach klasy
Loc annot = f.getAnnotation(Loc.class); // dla f uzyskać anotację Loc
if (annot == null) continue; // nie ma - następne pole
System.out.println(annot); // zobaczmy jak wygląda
String contName = annot.to(); // od adnotacji: nazwa kontenera
Field contField = klasa.getDeclaredField(contName); // pole, ktore go deklaruje
Object container = contField.get(this); // sam obiekt-kontener
Method m = container.getClass().getMethod("add", Component.class); // metoda add
m.invoke(container, f.get(this)); // i jej wywołanie - dodajemy komponent
}
}
Można powiedzieć, że ten sposób programowania, szczególnie w dużych projektach
może być bardzo użyteczny, bowiem łatwo pozwala zmieniać ułożenie komponentów.
Sama metoda locateComponents() może być nieco zmodyfikowana, tak by mogła
znaleźć się w jakiejś klasie narzędziowej i być zastosowana wobec dowolnych
klas o podobnej j.w. konstrukcji.
3.5. Przetwarzanie adnotacji w fazie kompilacji
Niewątpliwie możliwość przetwarzania adnotacji w fazie kompilacji jest najbardziej ekscytująca.
Umożliwia np. generowanie dodatkowych klas czy niezbędnych plików zewnętrznych.
Do takiego przetwarzania powołane są odpowiednie dodatkowe narzędzia.
Należy do nich
apt
czyli "Annotation Processing Tool", dostępny w pakiecie Javy.
Skąd apt ma wiedzieć, jak należy przetwarzać nasze adnotacje?
Otoż musimy mu to sami powiedzieć, dostarczając tzw. procesora adnotacji.
Procesor adnotacji definiujemy implementując we własnej klasie interfejs AnnotationProcessor i dostarczając definicji jedynej jego metody public void process()
Apt uzyskuje dostęp do naszego procesora za pomocą metody getProcessorFor(...)
z klasy, którą też musimy zdefiniować i która stanowi fabrykę procesorów
- implementację interfejsu AnnotationProcessorFactory.
Podczas wywołania tej metodzie przekazywane jest środowisko działania dla procesora - AnnotationProcessorEnvironment. Z tego środowiska nasz procesor może odczytać wszystkie niezbędne informacje
o strukturze kodu źródłowego oraz sposobach tworzenia i generowania nowych
plików, a także raportowania błędów i ostrzeżeń.
Apt używa naszego procesora, który np. produkuje dodatkowe pliki, po czym wykonuje wszystkie niezbędna kompilacje.
Uwaga: konieczne są importy pakietów z tools.jar - zob. przykładowy kod źródłowy.
Jako przykład rozpatrzmy prosty sposób zapisu klas typu JavaBeans.
Na podstawie tych bardzo uproszczonych zapisów będą mogły być generowane
"prawdziwe" duże klasy JavaBeans. Przykład jest raczej ilustracyjny i do
dalszego znaczącego rozszerzania i modyfikowania.
Umówimy się, że na podstawie zapisu:
@BeanTemplate class NazwaBeanaTemplate {
typ1 nazwa1;
typ2 nazwa2;
....
tyoN nazwaN;
}
ma być wygenerowana klasa JavaBean o nazwie NazwaBeana, zawierające właściwe
deklaracje pól (podanych) oraz odpowiednie dla nich settery i gettery.
Musimy więc mieć adnotację @BeanTemplate:
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface BeanTemplate {
}
i odpowiedni procesor, dostarczany przez naszą implementację fabryki procesorow adnotacji:
import com.sun.mirror.apt.*;
import com.sun.mirror.declaration.*;
import com.sun.mirror.type.*;
import com.sun.mirror.util.*;
import java.beans.*;
import java.io.*;
import java.util.*;
public class BeanTemplateAnnotationFactory
implements AnnotationProcessorFactory
{
// Typ adnotacji
private final String annoType = "BeanTemplate";
public Collection<String> supportedAnnotationTypes() {
return Arrays.asList(annoType);
}
public Collection<String> supportedOptions() {
return Arrays.asList(new String[0]);
}
public AnnotationProcessor // ważna metoda getProcessorFor
getProcessorFor(Set<AnnotationTypeDeclaration> atds,
final AnnotationProcessorEnvironment env)
{
return new AnnotationProcessor() { // procesor - w klasie wewnętrznej
public void process() {
// deklaracje markowane adnotacją annoType (teraz "BeanTemplate")
Collection<Declaration> dcls =
env.getDeclarationsAnnotatedWith(
(AnnotationTypeDeclaration) env.getTypeDeclaration(annoType));
for (Declaration d : dcls) {
if (d instanceof ClassDeclaration) { // jeżeli klasa
ClassDeclaration cdcl = (ClassDeclaration) d;
String name = cdcl.getSimpleName();
if (!name.endsWith("Template")) {
// od env uzyskujemy środki raportowania błędów, ostrzeżeń, info.
env.getMessager().printWarning("Wadliwa nazwa klasy bean template");
continue;
}
String qname = cdcl.getQualifiedName();
env.getMessager().printNotice(qname);
qname = qname.substring(0, qname.lastIndexOf("Template"));
name = name.substring(0, name.lastIndexOf("Template"));
try {
// od env uzyskamy plik i skojarzony z nim PrintWriter
PrintWriter out = env.getFiler().createSourceFile(qname);
// generujemy świeży kod na podstawie info o zaznaczonej klasie
out.println("public class " + name + " {" );
Collection<FieldDeclaration> fdcl = cdcl.getFields();
for (FieldDeclaration f : fdcl) {
String fname = f.getSimpleName();
String mname = Character.toUpperCase(fname.charAt(0)) +
fname.substring(1);
String ftype = f.getType().toString();
out.println(" private " + ftype + " " + fname + ";");
out.println(" public " + ftype + " get" + mname +
"() { return " + fname + "; }");
out.println(" public void set" + mname +
"(" + ftype + " v) { " + fname + " = v; }");
}
out.println("}");
out.close();
} catch(IOException exc) { exc.printStackTrace(); }
}
else // jeśli nie byla klasa, to ktoś się pomylił i zazn. interfejs
env.getMessager().printWarning("Adnotacja dotyczy interfejsu");
}
}
};
}
}
Po skompilowaniu tej fabryki umieszczamy wynik kompilacji w katalogu, w
którym znajdują się inne pliki źródłowe. Np. takie przykładowe zastosowanie:
Uproszczona definicja beana:
import java.awt.*;
@BeanTemplate public class Bean1Template {
String txt;
Color color;
}
Ponieważ, zgodnie z umową ma z tego powstać prawdziwa klasa JavaBean, to
plik ją wykorzystujący będzie się odwoływał do Bean1, a nie Bean1Template:
import java.awt.*;
public class Annot1 {
public Annot1() {
Bean1 b = new Bean1();
b.setTxt("Pies");
b.setColor(Color.BLUE);
System.out.println(b.getTxt() + "\n" + b.getColor());
}
public static void main(String[] args) {
new Annot1();
}
}
Aby to wszystko ze sobą polaączyć, musimy wywołać apt, podając mu lokalizację fabryki procesorow:
apt -factory BeanTemplateAnnotationFactory *.java
APT nie tylko przeprowadzi analizę adnotacji i odpowiednie ich substytucje,
nie tylko skorzysta z naszego procesora i pozwoli mu zapisać nowy plik źródłowy,
ale również wywoła normalny kompilator javy, aby wszystko złożył do kupy.
W efekcie uzyskamy nowowygenerowany plik źródłowy (który "zastępuje" uproszczony "template"):
public class Bean1 {
private java.awt.Color color;
public java.awt.Color getColor() { return color; }
public void setColor(java.awt.Color v) { color = v; }
private java.lang.String txt;
public java.lang.String getTxt() { return txt; }
public void setTxt(java.lang.String v) { txt = v; }
}
a w wyniku kompilacji wszystkiego razem działający plik Annot1.class, który wyprodukuje napis:
Pies
java.awt.Color[r=0,g=0,b=255]
Zobacz multimedialną prezentację użycia APT
Podany przykład jest prosty, a także trochę dyskusyjny. Ale dobrze pokazuje
potencjalnie olbrzymie możliwości tkwiące w przetwarzaniu adnotacji w fazie kompilacji.
3.6. Adnotacje a transformowanie kodu binarnego
Polityka utrzymywania adnotacji oznaczana przez metaadnotację
@Retention jako RetentionPolicy.CLASS utrzymuje adnotację w klasie
(kodzie binarnym), ale nie udostępnia jej mechanizmom refleksji.
Taki typ adnotacji może być wykorzystywany przez różne narzędzia
modyfikacji kodu binarnego klas. Należą do nich takie narzędzia jak:
Javassist pozwala na bardzo łatwe transformowanie kodu binarnego klas, polegające m.in na:
- zastępowaniu metod i konstruktorów.
- dodawanie kodu na początku lub w końcu ciała metod lub konstruktorów.
Modyfikacje są łatwe, ponieważ możemy zapisać je w naturalnym języku Javy.
Javassist ma pewne ograniczenia.
Znacznie potężniejszym narzędziem jest BCEL (Byte Code Engineering
Library), który pozwala robić praktycznie wszystko, ale wymaga
zapisów w postaci podobnej do bajtkodu.
Zobacz więcej o:
Javassist - http://jboss.com/products/javassist
BCEL - http://jakarta.apache.org/bcel/
4. Skrypty w Javie
4.1. Wprowadzenie
W Javie 6 wbudowano możliwość uruchamiania skryptów z poziomu
aplikacji. Naturalnie, taka możliwośc istniała zawsze, ale teraz
mechanizmy i interfejsy zostały ustandaryzowane, a w pakietach Javy
mamy javax.script, dodstarczający gotowych klas i interfejsów do
wykorzystania w tym celu.
Co może robić aplikacja Javy ?
- wywoływać skrypt,
- wywoływac wybrane funkcje/metody ze skryptu,
- odczytywać informacje (zmienne) zmienione lub dostarczone przez skrypt
Co może robić skrypt ?
- korzystać z możliwości swojego języka skryptowego
- korzystać z bibliotek Javy
- odczytywać informacje z aplikacji (dostęp do zmiennych)
Jakie są tego zastosowania
?
- prototypowanie aplikacji
- testowanie aplikacji
- wzbogacanie aplikacji o możliwości języków skryptowych (np. niektóre fragmenty łatwiej napisać w skrypcie)
- dostarczanie użutkownikowi środków prgramistycznego dostępu do środowiska aplikacji
- np. makra-programy do elastycznych obliczeń
4.2. Motory skryptowe i dostęp do nich z aplikacji Javy
Motor skryptowy (script engine)
- to komponent software'wy, który wykonyje programy napisane w
języku skryptowym. Wykonanie polega na interpretacji, składającej się z
następujących faz: parsowanie kodu, utworzenie tablicy symboli do
przechowywania wartości, właściwe wykonanie. Motor skryptowy nazywany
jest także interpreterem
Java Script Engines (JSE) - to motory skryptowe zrealizowane jako moduły napisane w Javie (pliki .jar) i eksponujące jednolity interfejs programistyczny, zgodny ze specyfikacją JSR-223.
Oczywiście,
mogą one używac odwołań do funkcji rodzimych, a zatem mogą "powierzać"
własciwe wykonanie skryptu interpreterom, które mają odpowiednie
API do takiego współdziałania.
W bibliotekach Javy 1. 6 dostępny jest JSE dla języka JavaScript w wersji Rhino.
Dodatkowe motory skryptowe są dostępne na stronie: https://scripting.dev.java.net/.
Mogą powstawać (i powstają) nowe motory skryptowe.
Tabela przedstawia wybrane motory skryptowe
Language
|
Description
|
Implementation
|
AWK
|
AWK is a general purpose language that is designed for processing text-based
data, either in files or data streams. Jawk is Java-like, Awk-like reporting
language.
|
Jawk
|
BeanShell
|
BeanShell is a small, free, embeddable Java interpreter with object scripting
language features, written in Java. BeanShell dynamically executes standard
Java syntax and extends it with common scripting conveniences such as loose
types, commands, and method closures like those in Perl and JavaScript.
|
BeanShell 2.0b5
|
ejs
|
"ejs" (Embedded JavaScript) is JSP-like templating engine for JavaScript.
It supports the usual <%= expr %>, <% code %> syntax. This engine
is completely implemented in JavaScript. You need to use JavaScript engine to
use this script engine.
|
Implementation contained in one JavaScript file.
|
FreeMarker
|
FreeMarker is a Java-based general purpose template engine.
|
FreeMarker 2.3.8
|
Groovy
|
Groovy is an agile dynamic language for the Java 2 Platform that has many of the
features that people like so much in languages like Python, Ruby and Smalltalk,
making them available to Java developers using a Java-like syntax.
|
Groovy 1.0 jsr-06
|
Jaskell
|
Jaskell is a lazy functional programming language. It stands for
"Java Haskell". It features higher-order functions, function currying,
string interpolation, lazy evaluation, dynamic typing.
|
Jaskell 1.0
|
Java
|
http://java.sun.com
|
Java Compiler API (JSR 199)
|
JavaScript
|
Web Browser's native JS interpreter is wrapped as javax.script API.
Note that this script engine works only under web browsers. i.e., only
within Java applets.
|
Web Browser's JS implementation (tested with Firefox 1.5.0)
|
Jelly
|
Jelly is a tool for turning XML into executable code. So Jelly is a Java
and XML based scripting and processing engine. Jelly borrows many good ideas
from both JSP custom tags, Velocity, Cocoon, Ant. In this script engine,
<script> tag has been added. Any JSR-223 compliant language may be used.
Also, expressions [${xxx}] may be from any scripting language rather than
Jexl alone.
|
Jelly 1.0
|
JEP
|
JEP is a Java library for parsing and evaluating mathematical expressions.
JEP supports BigInteger, BigDecimal, complex, Vector/Matrix/Tensor arithmetic.
|
JEP (Java Math Expression Parser) 2.4.0
|
Jexl
|
Java Expression Language (JEXL) is an expression language engine which can be embedded in
applications and frameworks. JEXL is inspired by Jakarta Velocity and the Expression Language
defined in the JavaServer Pages Standard Tag Library version 1.1 (JSTL) and JavaServer Pages
version 2.0 (JSP).
|
Jexl 1.0
|
jst
|
"jst" (JavaScript Templates) is JSP/ASP/PHP-like templating engine for
JavaScript. This engine uses TrimPath's JavaScript Templates implementation.
This script engine is implemented in JavaScript. You need to use
JavaScript engine to use this script engine.
|
TrimPath JavaScript Templates (1.0.38)
|
JudoScript
|
A scripting language built atop Java, and is a powerful general-purpose programming
language with intimate Java scripting support. JudoScript, or Judo for short, is a
general-purpose, Java scripting and multi-domain language. A full-fledged general-purpose
scripting language with full capability of Java scripting, JudoScript intimately supports
most of today's key computing areas.
|
JudoScript 0.9
|
OGNL
|
OGNL stands for Object-Graph Navigation Language; it is an expression language for
getting and setting properties of Java objects. You use the same expression for both
getting and setting the value of a property
|
OGNL 2.6.9
|
Pnuts
|
Pnuts is a simple but powerful scripring language that is embeddable into Java applications.
It has an extensible module system and a lot of ready-to-use modules.
|
Pnuts 1.1
|
Python
|
Python is a dynamic object oriented programming language that can be used for many kinds
of software development. It offers strong support for integration with other languages and tools,
comes with extensive standard libraries, and can be learned in a few days time.
|
Jython 2.1
|
Ruby
|
Ruby is the interpreted scripting language for quick and easy object-oriented programming.
It has many features to process text files and to do system management tasks (as in Perl).
It is simple, straight-forward, extensible, and portable.
|
JRuby 0.9.0
|
Scheme
|
Scheme is a statically scoped and properly tail-recursive dialect of the Lisp programming language
invented by Guy Lewis Steele Jr. and Gerald Jay Sussman. It was designed to have an exceptionally
clear and simple semantics and few different ways to form expressions. A wide variety of programming
paradigms, including imperative, functional, and message passing styles, find convenient expression
in Scheme.
|
SISC 1.15.3
|
Sleep
|
Sleep is a Java-based scripting language heavily inspired by Perl.
|
Sleep 2.0
|
Tcl
|
Tcl (Tool Command Language) is a very powerful but easy to learn dynamic programming language,
suitable for a very wide range of uses, including web and desktop applications, networking,
administration, testing and many more. Open source and business-friendly, Tcl is a mature yet
evolving language that is truly cross platform, easily deployed and highly extensible.
|
Jacl 1.3.3
|
Velocity
|
Velocity is a Java-based general purpose template engine.
|
Velocity 1.4
|
XPath
|
JDK already includes javax.xml.xpath
package for XPath. But to extend XPath with user defined functions and variables, user needs to learn different set of
interfaces/classes in this (and few other) package(s). We are adding a javax.script engine on top of javax.xml.xpath API
- so that user can set variables, functions in ScriptContext and call "eval" method on ScriptEngine. Any Java method,
constructor may be used as XPath extension function - no need to wrap it as "XPathFunction".
|
JDK implementation of javax.xml.xpath is used.
|
XSLT
|
JDK already includes javax.xml.transform
package for XSLT. But to use XSLT, user needs to learn different set of interfaces/classes in
this (and few other) package(s). We are adding a javax.script engine on top of javax.xml.transform API
- so that user can set source, result in ScriptContext and call "eval" method on ScriptEngine.
By default, ScriptContext's input Reader and output Writer are used for transform source and
result.
|
JDK implementation of javax.xml.transform is used.
|
Wszystkie w/w JSE są dostępne z w/w strony jako archiwum katalogów sr223-engines.zip.
Dodatkowe JSE wg specyfikacji JSR 223:
Aby JSE był dostępny z poziomu aplikacji Javy jego JAR
musi być widoczny dla ClassLoadera (np. umieszczony na ścieżce
CLASSPATH.
Dostęp do JSE z poziomu aplikacji Javy jest realizowany za pośrednictwem zarządcy motorów - ScriptEngineManager. Zajmuje się on m.in. wyszukiwaniem i instancjacją odpowiednich JSE.
Do odnajdywania JSE służą następujące metody klasy ScriptEngineManager.
ScriptEngine | getEngineByName(String shortName)
Wyszukuje i tworzy JSE dla podanej nazwy motoru (lub jej aliasu) |
ScriptEngine |
getEngineByExtension(String extension)
Wyszukuje i
tworzy JSE dla podanego rozszerzenia plików skryptowych. |
ScriptEngine |
getEngineByMimeType(String mimeType)
j.w. - tylko dla podanego typu MIME |
Uzyskanie JSE:
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(nazwa_motoru);
A jakie są nazwy?
To
oczywiście podaje dokumentacja konkretnych JSE. Ale możemy się tego
sami szybko dowiedzieć. Sprawdzenie nazw jest użyteczne wtedy, gdy
nazwy się powtarzają dla różnych JSE (np. javascript) i wtedy trzeba
zastosowac rozróżniające aliasy.
Metainformacje o JSE uzyskujemy za pomocą interfejsu ScriptEngineFactory.
Tak
naprawdę ScriptEngineManager odnajduje właśnie odpowiednie fabryki dla
danych JSE i za pomocą ich metod fabrycznych tworzy obiekty
ScriptEngine.
Uwaga: ScriptEngineManager stosuje mechanizm service provider zob.
http://java.sun.com/javase/6/docs/technotes/guides/jar/jar.html#Service%20Provider
do wykrywania fabryk.
Wymaga to aby JARy JSE w katalogu META-INF zawierały podkatalog services z plikiem o nazwie javax.script.ScriptEngineFactory,
zawierającym nazwę klasy będącej fabryką tworzącą obiekty danego JSE.
Np. groovy-engine.jar w tym pliku zawiera nazwę
com.sun.script.groovy.GroovyScriptEngineFactory.
Wszystkie znane zarządzcy fabryki można uzyskać za pomoca metody:
ScriptEngineManager mgr ...
List<ScriptEngineFactory> factories = mgr.getEngineFactories();
Przykładowy program pokazuje dostępne informacje.
import java.util.*;
import javax.script.*;
public class DiscoverEngines {
public static void main(String[] args) {
ScriptEngineManager mgr = new ScriptEngineManager();
List<ScriptEngineFactory> factories = mgr.getEngineFactories();
for (ScriptEngineFactory factory : factories) {
System.out.println("ScriptEngineFactory Info");
String engName = factory.getEngineName();
String engVersion = factory.getEngineVersion();
System.out.println("Script Engine: " + engName + " v. " + engVersion);
List<String> engNames = factory.getNames();
for(String name : engNames) System.out.println("Engine Alias: " + name);
String langName = factory.getLanguageName();
String langVersion = factory.getLanguageVersion();
System.out.println("Language: " + langName + " v. " + langVersion);
List<String> exts = factory.getExtensions();
for(String ext : exts) System.out.println("Script file extension: " + ext);
System.out.println("-------------------------------------------");
}
}
}
W przypadku gdy na ścieżce classpath znajdują się JSE dla browserjs i groovy otrzymamy wynik:
ScriptEngineFactory Info
Script Engine: groovy v.
Engine Alias: groovy
Language: groovy v. 1.0
Script file extension: groovy
-------------------------------------------
ScriptEngineFactory Info
Script Engine: Browser JavaScript Engine v. 1.5
Engine Alias: js
Engine Alias: javascript
Engine Alias: JavaScript
Engine Alias: ecmascript
Engine Alias: ECMAScript
Engine Alias: BrowserJS
Engine Alias: NativeJS
Language: JavaScript v. 1.5
Script file extension: js
-------------------------------------------
ScriptEngineFactory Info
Script Engine: Mozilla Rhino v. 1.6 release 2
Engine Alias: js
Engine Alias: rhino
Engine Alias: JavaScript
Engine Alias: javascript
Engine Alias: ECMAScript
Engine Alias: ecmascript
Language: ECMAScript v. 1.6
Script file extension: js
-------------------------------------------
4.2. Wykonanie skryptów
Skrypty traktowane są jako ciągi znaków:
- zapisane jako String,
- lub czytane z dowolnego strumienia typu Reader.
Skrypt
może być wywołany jako całość lub też - jesli język skryptowy to
dopuszcza - może być wywołana wybrana funkcja lub metoda ze skryptu.
Do wykonania całego skryptu służy metoda eval wołana na rzecz obiektu typu ScriptEngine.
ScriptEngine eng = ...;
eng.eval( skrypt);
gdzie:
skrypt - jest typu String lub typu Reader
Uwaga: przy wywołaniu eval nalezy obsługiwac wyjątek ScriptException
Przykład 1.
import javax.script.*;
public class GroovyTest {
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("groovy");
try {
String script = "\"Ala ma kota i psa\".tokenize().each { println it + ' ' + it.length()}";
engine.eval(script);
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
Uwagi:
- treśc skryptu sformułowano w języku Groovy jako String
- warto
zwrócić uwagę na moc tego języka - metoda tokenize(), konstrukcja each
oraz tzw. domknięcie (zamiast definiowania tradycyjnej funkcji) bardzo
ułatwia programowanie.
Wynik:
Ala 3
ma 2
kota 4
i 1
psa 3
Skrypt można wczytać też bezpośrednio z pliku:
import java.io.*;
import javax.script.*;
public class GroovyTest2 {
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("groovy");
try {
engine.eval(new FileReader("test1.groovy"));
} catch (ScriptException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
Wywołanie określonej funkcji lub metody
- zdefiniowanej w skrypcie - jest możliwe wtedy gdy ScriptEngine
implementuje interfejs Invocable. Należy to zawsze sprawdzić, a
następnie dokonać konwersji do Invocable i wywolac metodę invokeFunction lub invokeMethod.
Object |
invokeFunction(String name,
Object... args)
Woła funkcje
name zdefiniowaną w skrypcie z argumentami args i zwraca wynik w
postaci Object. Konwersje z typów wyniku funkcji do typów Javy są
zalezne od implementacji języka skryptowego. Przy braku funkcji zgłasza wyjątek NoSuchMethodException. Skrypt powinien być wczesniej przygotowany przez wywołanie eval. |
Object |
invokeMethod(Object obj,
String name,
Object... args)
Wywołuje metodę name na rzecz obiektu obj z argumentami args. |
Przykład.
W pliku test1.js mamy następujący zestaw funkcji:
function square(n) {
return n*n;
}
function cube(n) {
return n*n*n;
}
Będziemy je wywoływac z aplikacji Javy podając nazwę funkcji i argument:
import java.io.*;
import javax.script.*;
import javax.swing.*;
public class InvokeTest {
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("rhino");
if (!(engine instanceof Invocable))
throw new RuntimeException("Engine not invocable");
Invocable eng = (Invocable) engine; // konieczna konwersja
try {
engine.eval(new FileReader("test1.js")); // przygotowanie skryptu
String in;
while ((in = JOptionPane.showInputDialog("Podaj nazwe funkcji i argument")) != null) {
String[] call = in.split(" ");
double res = (Double) eng.invokeFunction(call[0], call[1]); // wołanie wybranej funkcji
System.out.println("Wynik " + res);
}
} catch (ScriptException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
Skrypty - o ile dany JSE to dopuszcza - mogą być także kompilowane (do symbolicznej postaci) co przyspiesza wielokrotne wykonanie. W tym przypadku:
- JSE winien implementować interfejs Compilable,
- skompilowaną postać skryptu uzyskujemy z engine.compile(skrypt) w postaci wyniku typu CompiledScript,
- na rzecz tego wyniku wołamy metodę eval().
4.3. Wymiana danych
Wymiana danych pomiędzy aplikacją i skryptem odbywa się poprzez zestawy par: klucz - wartość, zwane wiązaniami (bindings). W tych parach kluczem jest nazwa zmiennej, a wartością - wartość zmiennej.
Wiązania
są obiektami klas implementujących interfejs Bindings. Interfejs ten
rozszerza interfejs Map<String, Object>, zatem wiązania mogą być
traktowane generalnie tak jak mapy (np. dodawanie za pomocą put,
pobieranie za pomocą get).
Wiązania mogą być następujących rodzajów:
- GLOBAL_SCOPE
- wiązanie globalnego zakresu (jego elementy są dostępne dla wszystkich
JSE tworzonych przez tego samego zarządce ScriptEngineManager)
- ENGINE_SCOPE
- wiązanie "zakresu motoru" dostępne dla danego JSE (cały czas w cyklu
jego zycia, nie tylko w trakcie wykonania konkretnego skryptu),
- wiązania dodatkowych zakresów - oprócz GLOABAL_SCOPE i ENGINE_SCOPE mogą być dodatkowe zakresy,
- wiązanie tworzone ad hoc i podawane jako argument metody eval: eval(skrypt, Bindings).
Zakresy są zarządzane przez ScriptContext (który oprócz tego daje dostęp do strumieni we-wy i błędów JSE).
Co jest w wiązaniach i jak z nimi postępować?
Automatycznie
wszystkie zmienne utworzone w trakcie wykonania skryptu (w skrypcie),
są dodawane do wiązania zakresu SCRIPT_ENGINE (lub jeśli w eval podano
argument Bindings - do tego właśnie wiązania). W aplikacji możemy:
- pobrać
mapę wiązań zakresu SCRIPT_ENGINE - metody getBindings(..) z klas
implementujących ScriptEngine lub ScriptContext i z tej mapy pobierać
wartości zmiennych,
- pobrać wartości zmiennych za pomocą metod
get(key, value) ze ScriptEngine lub getAttribute(key,
ScriptContext.ENGINE_SCOPE) z interfejsu ScriptContext
- pobierać wartości z mapy Bindings przekazanej w eval.
Zmienne aplikacji Javy możemy dodawać do wiązań za pomocą metod:
- scriptManager.put(key, value) // dodaje do zakresu GLOBAL_SCOPE
- engine.put(key, value) // dodaje do zakresu ENGINE_SCOPE
- scriptContext.setAttribute(key, value, scope) // dodaje do wybranego zakresu
- put lub putAll na rzecz Bindings danego zakresu lub własnych (przekazanych w eval)
Te zmienne będą bezpośrednio dostępne w skrypcie pod nazwami = kluczom.
Przykłady (JSE = Rhino):
....
String script = "a =1; b = 2; c =2;";
engine.eval(script);
showBindings();
....
private static void showBindings() {
System.out.println("Show bindings");
ScriptContext ctx = engine.getContext(); // uzyskanie biezącego kontekstu
List<Integer> scopes = ctx.getScopes(); // z tego kontekstu - jakie są zakresy
for (Integer scope : scopes) { // dla każdego zakresu.... :
System.out.println("Scope: " + scope);
Bindings bnd = ctx.getBindings(scope); // wiązania
System.out.println(bnd.getClass().getName()); // jaka to klasa?
for (String key : bnd.keySet()) { // co jest w wiązaniach?
System.out.println(key + " = " + bnd.get(key));
}
}
}
Wynik:
Show bindings
Scope:
100
// <--- to jest ENGINE_SCOPE
javax.script.SimpleBindings // wiązania są klasy SimpleBindings
b
= 2.0
// zmienna ze
skryptu
c = 2.0
//
zmienna ze skryptu
println = sun.org.mozilla.javascript.internal.InterpretedFunction@dd5b
a
= 1.0
// zmienna ze
skryptu
context = javax.script.SimpleScriptContext@c4bcdc
print = sun.org.mozilla.javascript.internal.InterpretedFunction@4b4333
Scope:
200
// <--- zakres GLOBAL_SCOPE
javax.script.SimpleBindings // nic w nim nie ma bo nic nie dodaliśmy
Wiązania zakresu ENGINE_SCOPE trwają wraz z motorem - kolejne skrypty mogą je uzupełniać:
String script = "a =1; b = 2; c =2;";
engine.eval(script);
script = "xyz = 10;";
engine.eval(script);
showBindings();
showBindings pokaże m.in.
b = 2.0
c = 2.0
a = 1.0
xyz = 10.0
Łatwo możemy przekazac zmienną do skryptu:
String txt = "Ala ma kota";
engine.put("txt", txt);
script = "println('Ze skryptu : ' + txt + a + b + c + xyz);";
engine.eval(script);
Dostaniemy (txt pochodzi z aplikacji, zmienne a, b, c, xyz z wykonań poprzednich skryptów :
L100
Ze skryptu : Ala ma kota12210
Możemy użyć własnych Bindings:
Bindings sb = new SimpleBindings();
int i = 1000;
Date data = new Date();
sb.put("i", i);
sb.put("data", data);
script = "println('Ze skryptu : ' + data + ' liczba ' + i); hhh = 7777;";
engine.eval(script, sb); // podajemy jako drugi argument Bindings
showBindings();
System.out.println("A co jest w naszych bindings?");
for (String key : sb.keySet()) {
System.out.println(key + " = " + sb.get(key));
}
System.out.println("Koniec naszych");
Wynik:
Ze skryptu : Tue Oct 10 09:35:44 CEST 2008 liczba 1000
Show bindings
Scope: 100
javax.script.SimpleBindings
b = 2.0
c = 2.0
println = sun.org.mozilla.javascript.internal.InterpretedFunction@15dfd77
a = 1.0
context = javax.script.SimpleScriptContext@c4bcdc
txt = Ala ma kota
print = sun.org.mozilla.javascript.internal.InterpretedFunction@1abc7b9
xyz = 10.0
Scope: 200
javax.script.SimpleBindings
A co jest w naszych bindings?
hhh = 7777.0
println = sun.org.mozilla.javascript.internal.InterpretedFunction@1621e42
context = javax.script.SimpleScriptContext@b09e89
data = Tue Oct 10 09:35:44 CEST 2006
print = sun.org.mozilla.javascript.internal.InterpretedFunction@1787038
i = 1000
Koniec naszych
Tutaj
widać, że domyślny ENGINE_SCOPE nie jest zmieniany, w
przekazanych wiązaniach znajdziemy dodane przez nas zmienne (i mogą one
być użyte w skrypcie), dodatkowo znajdą się tam zmienne opisujące
kontekst (contezt, println, print).
Okazuje się, że nasze
wiązania zastępują domyślny ENGINE_SCOPE i dlatego w poniższym
fragmencie zmienna txt nie jest znana (chociaż jest w wiązaniach
zakresu ENGINE):
script = "println('Ze skryptu liczba = ' + i); print('Ze skryptu txt: '); println(txt);";
engine.eval(script, sb);
Wynik:
Ze skryptu liczba = 1000
Ze
skryptu txt: javax.script.ScriptException:
sun.org.mozilla.javascript.internal.EcmaError: ReferenceError: "txt" is
not defined. (<Unknown source>#1) in <Unknown source> at
line number 1
Naturalnie, możemy łączyć wiązania:
Bindings eb = engine.getBindings(ScriptContext.ENGINE_SCOPE); // wiązania motoru
eb.putAll(sb); // dodajemy do nich nasze
script = "println('Ze skryptu : ' + txt + ' liczba ' + i);";
engine.eval(script);
showBindings();
Teraz będą znane zmienne zarówno z zakresu ENGINE jak i z naszych Bindings.
Ze skryptu : Ala ma kota liczba 1000
Show bindings
Scope: 100
javax.script.SimpleBindings
hhh = 7777.0
b = 2.0
c = 2.0
println = sun.org.mozilla.javascript.internal.InterpretedFunction@fa9cf
a = 1.0
data = Tue Oct 10 10:08:19 CEST 2006
context = javax.script.SimpleScriptContext@c4bcdc
txt = Ala ma kota
print = sun.org.mozilla.javascript.internal.InterpretedFunction@55571e
i = 1000
xyz = 10.0
Nalezy zwrócić uwagę, że Bindings dzialają tak jak każda mapa. Nie możemy liczyć na to, że po dodaniu zmiennej i oraz txt do Bindings, wykonaniu skryptu i zmianie wartości zmiennych ponowne wykonanie skryptu dostrzeże te zmiany:
i = 99;
txt = "Nowy tekst";
engine.eval(script);
L100
Ze skryptu : Ala ma kota liczba 1000
Oczywiście,
w mapie są referencje (jako wartości), ale przecież przy dodawaniu
zmiennych typów prostych do mapy następuje boxing (i refrencja wskazuje
na wtedy utworzony obiekt). To samo dotyczy zmiennej txt - referencja w
mapie wskazuje na napis "Ala ma kota" (a nie na nowy napis "Nowy
tekst").
Wyjściem z sytuacji jest albo użycie obiektów klas
modyfikowalnych albo ponowne ładowanie mapy Bindings po zmianie
wartości na poziomie aplikacji.
Uwaga: jako kluczy oznaczających
nazwy zmiennych (lub inną informację przekazywaną pomiędzy skryptem i
aplikacją) nie wolno używać nazw zarezerwowanych, które zaczynają się
od javax.script. Obecnie zdefiniowane są następujące klucze
zarezerwowane.
javax.script.argv | Zobacz w dokumentacji znaczenie podanych zmiennych |
javax.script.filename |
javax.script.engine |
javax.script.engine_version |
javax.script.language |
javax.script.language_version |
4.4. Kontekst
Wspomniany już ScriptContext zapewnia łączność pomiędzy aplikacją i skryptem.
Każdy skrypt wykonywany jest w jakimś kontekście (w szczególności - domyślnym).
Możemy zmienić kontekst domyślny, podając własny kontekst w metodzie eval:
eval(skrypt, ScriptContext)
Możemy też modyfikowac domyślny kontekst.
Oprócz wiązań (zakresów) - kontekst zapewnia dostęp do strumieni wejścia, wyjścia oraz błędów dla skryptu.
Poniższy fragment pokazuje jak można przechwycić wyjście skryptu:
// Zobaczmy co można zrobić z Writerem
StringWriter sw = new StringWriter();
engine.getContext().setWriter(new PrintWriter(sw));
script = "println('To powinno pójść na nowy Writer');";
engine.eval(script);
JOptionPane.showMessageDialog(null, sw.toString());
W rezultacie pokaże się okienko dialogowe (komunikatów) z napisem "To powinno pójść na nowy Writer".
4.5. Dostęp do obiektów Javy ze skryptów
Motory skryptowe zgodne ze specyfikacją JSR-223 umożliwiają w skryptach dostęp do klas z pakietów Javy.
Szczegóły (w tym składnia wywołania konstruktorów i metod) zależne są od implementacji języka skryptowego.
Generalnie poslugujemy się kwalifikowanymi nazwami klas (np. javax.swing.JFrame).
W językach, które umożliwiają dodanie do przestrzeni nazw nazw pakietów i klas Javy można stosowac konstrukcję typu import.
Oczywiście, możemy też odwoływać się do naszych własnych klas - do ich publicznych metod.
Przykłady.
public class SwingFromRhino {
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("rhino");
String script = engine.getFactory().getProgram(
"importPackage (javax.swing)",
"f = new JFrame('Okno')",
"f.setSize(200,200)",
"f.show()"
);
System.out.println(script);
try {
engine.eval(script);
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
Warto
zwrócić uwagę na metodę z getProgram(String ...) interfejsu
ScriptEngineFactory, która zwraca gotowy do wykonania program,
składający się z indtrukcji podanych jako argumenty, zgodnie ze
składnią danego języka skryptowego.
W tym kontekście warto swpomnieć też o innych użytecznych metodach pomocniczych interfejsu ScriptEngineFactory:
String |
getMethodCallSyntax(String obj,
String m,
String... args)
Zwraca napis,
który może być użyty w danym języku skryptowym do wywolania metody
klasy Javy na rzecz obiektu |
String |
getOutputStatement(String toDisplay)
Zwraca napis,
który może być użyty jako instrukcja danego języka skryptowego do
wyprowadzenia napisu toDisplay |
Przykład.
class Person {
String name;
public Person(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Groovy2 {
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("groovy");
try {
Person p = new Person("Ala");
String call = engine.getFactory().getMethodCallSyntax("p", "setName", "\"Pies\"");
System.out.println(call);
engine.put("p", p);
engine.eval(call);
System.out.println(p.getName());
String out = engine.getFactory().getOutputStatement(p.getName());
System.out.println(out);
engine.eval(out);
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
Wynik:
p.setName("Pies")
Pies
println("Pies")
Pies
4.6. Przykład: programowanie skryptowe na zmiennych aplikacji
W poniższym programie przedstawiono:
- automatyczne przekazywanie zmiennych aplikacji do środowiska skryptowego, w tym tablic,
- wykonywanie na nich obliczeń w skrypcie (w tym na tablicach),
- uzyskiwanie wyników obliczeń w aplikacji,
- komunikację skryptu z elementami GUI (np. pole tekstowe).
Wykonywany skrypt wygląda następująco:
importPackage(javax.swing);
a = a + 1;
b = 33;
c = a + b;
sum = 0;
for (i in arr) {
arr[i] = arr[i]*2;
sum += arr[i];
}
JOptionPane.showMessageDialog(null, "Suma = " + sum);
infoText.setText("Suma = " + sum);
przy czym zmienne a, b, c są zmiennymi liczbowymi z aplikacji, arr - tablica z aplikacji, infoText - pole tekstowe z aplikacji.
Aplikacja ma następującą postać:
class Varman {
private Object vars;
private Bindings bindings;
private Field[] fields;
public Varman(Bindings bindings, Object vars) {
this.vars = vars;
this.bindings = bindings;
fields = vars.getClass().getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
try {
String name = fields[i].getName();
if (name.indexOf("this$") != -1) continue;
Object value = fields[i].get(vars);
bindings.put(name, value);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void getVars() {
for (int i = 0; i < fields.length; i++) {
String name = fields[i].getName();
if (name.indexOf("this$") != -1) continue;
Object value = bindings.get(name);
try {
fields[i].set(vars, value);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public String getVarsPageView() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < fields.length; i++) {
try {
String name = fields[i].getName();
if (name.indexOf("this$") != -1) continue;
Object value = fields[i].get(vars);
if (fields[i].getType().isArray()) {
StringBuilder tmp = new StringBuilder("{ ");
tmp.append(Array.get(value, 0));
for (int j=1; j < Array.getLength(value); j++) tmp.append(", ").append(Array.get(value, j));
tmp.append(" }");
value = tmp;
}
sb.append('\n').append(name).append(" = ").append(value);
} catch (Exception e) {
e.printStackTrace();
}
}
return sb.toString();
}
}
public class CalcJs1 {
private ScriptEngineManager manager = new ScriptEngineManager();
private ScriptEngine engine = manager.getEngineByName("rhino");
private Bindings bindings = engine.createBindings();
private class Var {
double a = 7, b, c, d, e, f;
int[] arr = { 1, 2, 3 };
}
public CalcJs1() {
Var v = new Var();
Varman vm = new Varman(bindings, v);
showInfoFrame(bindings);
try {
engine.eval(new FileReader("testCalc.js"), bindings);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (ScriptException e) {
e.printStackTrace();
}
vm.getVars();
JOptionPane.showMessageDialog(null, vm.getVarsPageView(), "Wyniki", 1);
for (int i = 0; i < v.arr.length; i++) {
System.out.println("arr " +i+ " = " + v.arr[i]);
}
}
private void showInfoFrame(Bindings bindings) {
JFrame f = new JFrame("Wyniki");
JTextField tf = new JTextField("Na razie nie ma wyniku ");
f.add(tf);
f.pack();
f.setLocation(600, 300);
f.setVisible(true);
bindings.put("infoText", tf);
}
public static void main(String[] args) {
new CalcJs1();
}
}
Pokaz działania aplikacji.
4.7. Przykład: wykorzystanie możliwości JavaScript w apletach Javy
W tym przykładzie stworzymy aplet, dostarczający "eleganckiego" menu.
Jedna z opcji menu daje możliwość zmiany stylu strony.
Aby
uzyskać zmianę stylu wykorzystamy motor skryptowy dla natywnych
przeglądarek (BrowserJS) i napiszemy odpowiedni skrypt js, który będzie
wykonywany po wyborze opcji w menu apletu.
Demonstracja działania
Skrypt:
function chgStyle(cssFile) {
var link = document.getElementById("slink");
link.setAttribute('href', cssFile);
}
Aplet:
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import javax.script.*;
import javax.swing.*;
@SuppressWarnings("serial")
public class Konfigurator extends JApplet implements ActionListener {
ScriptEngine engine;
String[] cssFiles = { "style/first.css", "style/second.css", "style/third.css", "style/last.css" };
String[] opis = { "Surowy", "Kolorowy", "Powiększony", "Słoneczny" };
String script;
@Override
public void init() {
ClassLoader myloader = getClass().getClassLoader();
ScriptEngineManager manager = new ScriptEngineManager(myloader);
engine = manager.getEngineByName("BrowserJS");
engine.put("applet", this);
BufferedReader br;
StringBuilder sb = new StringBuilder();
try {
br = new BufferedReader(new FileReader("menu.js"));
for (String line; (line = br.readLine()) != null;)
sb.append(line).append('\n');
} catch (IOException e1) {
e1.printStackTrace();
}
script = sb.toString();
JMenu[] menu = { new JMenu("Wygląd"), new JMenu("Spis treści"),
new JMenu("Ważne linki"), new JMenu("Wyszukiwanie"), new JMenu("Pomoc") };
for (int i = 0; i < opis.length; i++) {
JMenuItem mi = new JMenuItem(opis[i]);
mi.setActionCommand(cssFiles[i]);
mi.addActionListener(this);
menu[0].add(mi);
}
JMenuBar mb = new JMenuBar();
for (int i = 0; i < menu.length; i++) {
if (i == menu.length-1) mb.add(Box.createHorizontalGlue());
mb.add(menu[i]);
menu[i].addMouseListener(new MouseAdapter() {
Color back;
@Override
public void mouseEntered(MouseEvent e) {
Component c = e.getComponent();
back = c.getForeground();
c.setForeground(Color.BLUE);
}
@Override
public void mouseExited(MouseEvent e) {
e.getComponent().setForeground(back);
}
});
}
this.setJMenuBar(mb);
}
public void actionPerformed(ActionEvent e) {
try {
String name = e.getActionCommand();
engine.eval(script);
if (engine instanceof Invocable) {
Invocable inv = (Invocable) engine;
inv.invokeFunction("chgStyle", name);
}
else JOptionPane.showMessageDialog(null, "Engine not invocable");
} catch (Exception exp) {
throw new RuntimeException(exp);
}
}
}
5. Zadania i ćwiczenia
Będą podane w trakcie semestru