Programowane dynamiczne
1
1. Ładowanie klas
2. Specyfikacja pliku
class
●
analiza przykładu: Hello world!
●
narzędzie ASM
3. Programowanie aspektowe
●
AspectJ
Klasa abstrakcyjna ClassLoader
2
Do pobrania klasy JVM wykorzystuje instancję klasy
java.lang.ClassLoader
.
Każda klasa zawiera referencję do do ładującego ją obiektu. Programista może
sterować procesem ładowania klas:
ClassLoader
loader
=
new
NetworkClassLoader(
host
,
port
);
Object main = loader.loadClass(
"Main"
,
true
).newInstance();
. . .
Przykładowa klasa:
class
NetworkClassLoader
extends
ClassLoader {
String
host
;
int
port
;
public
Class findClass(String name) {
byte
[] b = loadClassData(name);
return
defineClass(name, b, 0, b.
length
);
}
private
byte
[] loadClassData(String name) {
// load the class data from the connection
}
}
Plik class
3
public
class
Main {
/**
*
@param
args
*/
public
static
void
main(String[] args){
System.
out
.println(
"Hello world!"
);
}
}
Nagłówek klasy:
CA FE BA BE
– identyfikator formatu pliku class.
00 00
00 2E
– numer wersji JVM: (
46
.
0
) – zgodność z Java 1.2
Ogólnie JVM 1.k (k>1) obsługuje klasy od 45.0 do 44+k.0 włącznie
00 22
– ilość deklarowanych elementów (Constant Pool). Po tej deklaracji
następują kolejne, 33 elementy.
Plik class - Constant Pool
4
1.
01
00 04
4D 61 69 6E
–
string kodowany w UTF-8
o długości
4
:
Main
2.
07
00 01
–
pierwszy
z elementów to
nazwa klasy
3.
01
00 10
java/lang/Object
4.
07
00 03
–
trzeci
element to
nazwa klasy
5.
01
00 06
<init>
6.
01
00 03
()V
7.
01
00 04
Code
8.
0C
00 05 00 06
– piąty
element to
nazwa metody lub atrybutu
,
szósty
to
typ
(sygnatura)
: brak argumentów:
()
, zwracany typ
void
:
V
9.
0A
00 04 00 08
–
klasa
(
czwarty element
)
zawiera metodę
(
ósmy element
)
10.
01
00 0F
LineNumberTable
11.
01
00 12
LocalVariableTable
Plik class - Constant Pool
5
12.
01
00 04
this
13.
01
00 06
LMain;
14.
01
00 04
main
15.
01
00 16
([Ljava/lang/String;)V
16.
01
00 10
java/lang/System
17.
07
00 10
–
szesnasty
element to
nazwa klasy
18.
01
00 03
out
19.
01
00 15
Ljava/io/PrintStream;
20.
0C
00 12 00 13
– 18-ty
element to
nazwa metody/atrybutu
,
19-ty
to
jego typ
21.
09
00 11 00 14
– 17-ta
klasa zawiera
20-ty
atrybut
22.
01
00 0C
Hello world!
23.
08
00 16
– 22-gi
element to
stała tekstowa
24.
01
00 13
java/io/PrintStream
25.
07
00 18
– 24-ty
element to
nazwa klasy
Plik class - Constant Pool
6
26.
01
00 07
println
27.
01
00 15
(Ljava/lang/String;)V
28.
0C
00 1A 00 1B
– 26-ty
element to
nazwa metody/atrybutu
,
27-ty
to
jej
sygnatura
29.
0A
00 19 00 1C
– 25-ta
klasa zawiera
28-tą
metodę
30.
01
00 04
args
31.
01
00 13
[Ljava/lang/String;
32.
01
00 0A
SourceFile
33.
01
00 09
Main.java
Plik class
7
00 21
– modyfikatory dostępu dla klasy:
ACC_PUBLIC
(
00 01
)
|
ACC_SUPER
(
00
20
) – ze względu na kompatybilność ze starszymi JVM.
00 02
– numer elementu określającego klasę definiowaną w tym pliku.
00 04
– numer elementu określającego nadklasę klasy definiowanej w tym pliku.
00 00
– liczba interfejsów
00 00
– liczba atrybutów
00 02
– liczba metod
Plik class – metoda <init>
8
00 01
–
ACC_PUBLIC
– metoda publiczna
00 05
– indeks elementu zawierającego nazwę metody:
<init>
00 06
– indeks elementu z sygnaturą metody:
()V
00 01
– liczba dodatkowych atrybutów metody
00 07
– indeks elementu z nazwą atrybutu:
Code
00 00 00 2F
– długość atrybutu
00 01
– rozmiar stosu
00 01
- rozmiar tablicy zmiennych lokalnych
00 00 00 05
– długość kodu
2A B7 00 09 B1
00 00
– długość tablicy wyjątków
00 02
– liczba dodatkowych atrybutów
Plik class – metoda <init>
9
00 0A
–
LineNumberTable
00 00 00 06
– długość atrybutu
00 01
– długość tabeli numerów linii
00 00
– indeks instrukcji w tabeli
Code
00 06
– odpowiadający jej numer linii w pliku źródłowym
00 0B
–
LocalVariableTable
00 00 00 0C
– długość atrybutu
00 01
– długość tabeli zmiennych lokalnych
00 00
– początek zmiennej lokalnej w tablicy zmiennych
00 05
– długość zmiennej
00 0C
– indeks zmiennej:
this
00 0D
– typ zmiennej:
LMain
00 00
– indeks w lokalnej tablicy zmiennych
Plik class – metoda <init>
10
Kod metody
<init>
:
2A B7 00 09 B1
2A
:
ALOAD_0
//
zmienna lokalna o adresie 0 jest wstawiana na stos
B7
00 09
:
INVOKESPECIAL
java/lang/Object.<init>()V;
//
wywołuje metodę
void Object.<init>();
B1
:
RETURN
//
zwraca typ
void
Plik class – metoda main
11
00 09
–
ACC_PUBLIC
(
00 01
)
|
ACC_STATIC
(
00 08
) – publiczna metoda
statyczna
00 0E
– indeks elementu zawierającego nazwę metody:
main
00 0F
– indeks elementu z sygnaturą metody:
([Ljava/lang/String;)V
00 01
– liczba dodatkowych atrybutów metody
00 07
– indeks elementu z nazwą atrybutu:
Code
00 00 00 37
– długość atrybutu
00 02
– rozmiar stosu
00 01
- rozmiar tablicy zmiennych lokalnych
00 00 00 09
– długość kodu
B2 00 15 12 17 B6 00 1D B1
00 00
– długość tablicy wyjątków
00 02
– liczba dodatkowych atrybutów
Plik class – metoda main
12
00 0A
–
LineNumberTable
00 00 00 0A
– długość atrybutu
00 02
– długość tabeli numerów linii
00 00
– indeks instrukcji w tabeli
Code
00 0C
– odpowiadający jej numer linii w pliku źródłowym
00 08 00 0D
to samo dla kolejnej linii
00 0B
–
LocalVariableTable
00 00 00 0C
– długość atrybutu
00 01
– długość tabeli zmiennych lokalnych
00 00
– początek zmiennej lokalnej w tablicy zmiennych
00 09
– długość zmiennej
00 1E
– indeks zmiennej:
Hello world!
00 1F
– typ zmiennej: stała tekstowa
00 00
– indeks w lokalnej tablicy zmiennych
Plik class – metoda main
13
Kod metody
main
:
B2 00 15 12 17 B6 00 1D B1
B2
00 15
:
GETSTATIC
java/lang/System.out :
Ljava/io/PrintStream;
//
inicjalizacja klasy/obiektu
System.out
i odłożenie
//
go na stos
12
17
:
LDC
Hello world!
//
załadowanie na stos stałej tekstowej
B6
00 1D
:
INVOKEVIRTUAL
java/io/PrintStream.println(Ljava/lang/String;)V
//
wywołuje metodę
System.out.println(String)
B1
:
RETURN
//
zwraca typ
void
Plik class
14
00 01
– liczba atrybutów
00 20
–
SourceFile
00 00 00 02
– długość atrybutu
00 21
–
Main.java
KONIEC!!!
Plik class – zapis assemblerowy
15
// class version 46.0 (46)
// access flags 33
public class Main {
// compiled from: Main.java
// access flags 1
public <init>()V
L0 (0)
LINENUMBER 6 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init>()V
RETURN
L1 (4)
LOCALVARIABLE this LMain; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 9
public static main([Ljava/lang/String;)V
L0 (0)
LINENUMBER 12 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Hello world!"
INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V
L1 (4)
LINENUMBER 13 L1
RETURN
L2 (6)
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
}
ASM
16
ASM (http://asm.objectweb.org/index.html) jest narzędziem służącym do operacji na
plikach binarnych Javy. Może być używany do dynamicznego generowania klas lub
interfejsów bezpośrednio w formie binarnej lub modyfikacji istniejących klas
w trakcie ich ładowania przez Wirtualną Maszynę Javy.
Inne narzędzia tego typu:
BCEL: http://jakarta.apache.org/bcel/
ASM
17
Przykład – program generujący klasę z dzisiejszego wykładu (Hello world!):
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;
import java.io.FileOutputStream;
import java.io.PrintStream;
public class Helloworld extends ClassLoader implements Opcodes {
public static void main(final String args[]) throws Exception {
ClassWriter cw = new ClassWriter(false);
cw.visit(V1_1, ACC_PUBLIC, "Example", null, "java/lang/Object", null);
ASM
18
// metoda init
MethodVisitor mw = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null,
null);
mw.visitVarInsn(ALOAD, 0);
mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", ()V");
mw.visitInsn(RETURN);
mw.visitMaxs(1, 1); // glebokość stosu i liczba zmiennych lokalnych
mw.visitEnd();
// metoda main
mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main",
"([Ljava/lang/String;)V", null, null);
mw.visitFieldInsn(GETSTATIC, "java/lang/System", "out",
"Ljava/io/PrintStream;");
mw.visitLdcInsn("Hello world!");
mw.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println",
"(Ljava/lang/String;)V");
mw.visitInsn(RETURN);
ASM
19
mw.visitMaxs(2, 2);
mw.visitEnd();
byte[] code = cw.toByteArray(); // pobieramy kod klasy
FileOutputStream fos = new FileOutputStream("Example.class");
fos.write(code);
fos.close();
Helloworld loader = new Helloworld();
Class exampleClass = loader.defineClass("Example",code,0,code.length);
exampleClass.getMethods()[0].invoke(null, new Object[] { null });
ASM
20
// inny sposob uzyskania tego samego efektu
cw = new ClassWriter(true);
cw.visit(V1_1, ACC_PUBLIC, "Example", null, "java/lang/Object", null);
Method m = Method.getMethod("void <init> ()");
GeneratorAdapter mg = new GeneratorAdapter(ACC_PUBLIC, m, null, null,
cw);
mg.loadThis();
mg.invokeConstructor(Type.getType(Object.class), m);
mg.returnValue();
mg.endMethod();
m = Method.getMethod("void main (String[])");
mg = new GeneratorAdapter(ACC_PUBLIC + ACC_STATIC, m, null, null, cw);
mg.getStatic(Type.getType(System.class), "out",
Type.getType(PrintStream.class));
ASM
21
mg.push("Hello world!");
mg.invokeVirtual(Type.getType(PrintStream.class),
Method.getMethod("void println (String)"));
mg.returnValue();
mg.endMethod();
cw.visitEnd();
code = cw.toByteArray();
loader = new Helloworld();
exampleClass = loader.defineClass("Example", code, 0, code.length);
exampleClass.getMethods()[0].invoke(null, new Object[] { null });
}
}
Programowanie aspektowe
22
Każde realizowane zagadnienie pociąga za sobą w praktyce potrzebę realizacji
zagadnień pobocznych. Najczęściej spotykane zagadnienia są związane
z logowaniem, bezpieczeństwem, spójnością transakcyjną, autoryzacją,
synchronizacją wielowątkową itd. Jest to zjawisko normalne, wynikające ze
złożoności wymagań klienta. Zagadnienia te są w dużym stopniu rozłączne
pomiędzy sobą pod względem funkcjonalnym. Aby je zrealizować programista musi
poprzeplatać ich implementacje (tzw. warkocz), co czyni kod mniej czytelnym,
bardziej podatnym na błędy, trudniejszym w modyfikacji.
Programowanie aspektowe (Aspect Oriented Programming) zapobiega tym
negatywnym skutkom oddzielając fizycznie kod każdego zagadnienia poprzez
umieszczenie go w oddzielnych aspektach wraz z określniem punktów interakcji
pomiędzy nim a kodem programu.
Źródło: http://pl.wikipedia.org/wiki/Programowanie_aspektowe
AspectJ
23
AspectJ to aspektowy język programowania opracowany w Xerox Palo Alto
Research Center. Kluczowe cechy języka:
●
AspectJ jest rozszerzeniem języka Java. Kompilator (ajc) tworzy kod źródłowy
w Javie kompilowany dalej do postaci binarnej (pliki class).
●
aspekty implementowane przez AspectJ są obiektami
●
obsługuje wszystkie techniki programowania aspektowego, w tym: punkty
złączenia, punkty przekroju, rady, mechanizmy zarządzające cyklem życia aspektu
Obecnie AspectJ jest rozwijany w ramach projektu Eclipse:
http://www.eclipse.org/aspectj/. Istnieją również narzędzia developerskie (AJDT).
Model punktów złączenia
24
Każdy język programowania określa zbiór potencjalnych punktów złączenia – model
punktów złączenia. Przykładowe potencjalne punkty złączenia w Javie to:
●
wywołanie metody,
●
wykonanie metody,
●
wejście do statycznego bloku kodu,
●
wywołanie konstruktora.
Punkty złączenia są miejscami w które można wstawić tzw. logikę przekrojową. Po
wstawieniu dodatkowej logiki (kodu programu) w punkt złączenia staje się on
punktem przekroju
Punkty przekroju
25
Deklaracja punktów złączenia:
public pointcut myClassTaskCalled() :
call(public void MyClass.task());
Punkt złączenia dla wszystkich metod
task()
niezależnie od klasy:
public pointcut anyClassTaskCalled() : call(* *.task(..));
Przekazanie wartości do punktu złączenia:
public pointcut anyClassTaskCalled(int value) :
call(* *.task(..)) && args(number);
Typy punktów złączenia:
●
call (Signature)
– wywołanie metody
●
execution (Signature)
– wykonanie metody
●
handler(TypePattern)
– przechwytywanie wyjątków
Rady
26
Rada (advice) określa kod wykonywany w momencie zarejestrowania punktu
złączenia. Od zwykłej metody różni ją dodatkowe słowo kluczowe określające
miejsce wykonania kodu względem punktu złączenia:
before()
– przed punktem złączenia,
around()
– zamiast punktu złączenia, chyba że metoda
proceed()
mówi inaczej
after()
– po punkcie złączenia niezależnie od wyniku jego działania
after() returning
– po punkcie zgłoszenia jeśli nie było zgłoszenia wyjątku
after() throwing
– po punkcie zgłoszenia jeśli został zgłoszony wyjątek.
Przykład:
before(int value) : anyClassTaskCalled(value){
System.out.println("Wartosc" + value);...
}
Aspekty
27
Aspekt przypomina zwykłą klasę Javy. Instancja aspektu jest pełnoprawnym
obiektem Javy i składnia jego obsługi jest analogiczna jak zwykłych obiektów.
Domyślnie pojedyncza instancja aspektu jest tworzona przez aplikację w momencie
uruchomienia i istnieje aż do jej zakończenia (singleton). Istnieje jednak możliwość
wybrania innego cyklu życia dla aspektu:
●
issingleton()
– singleton (domyślne)
●
perthis(Pointcut)
– nowa instancja dla każdego obiektu do którego odnosi
się referencja
this
w punkcie złączenia
●
pertarget(Pointcut)
– nowa instancja dla każdego obiektu, który jest celem
działania punktu złączenia
●
percflow(Pointcut)
– nowa instancja dla każdego wątku inicjowanego
w ramach punktu złączenia
Aspekty
28
Kompletny przykład aspektu:
public aspect MyAspect issingleton(){
public pointcut myClassTaskCalled() : call(public void MyClass.foo());
public pointcut anyClassBarCalled(int number) : call(* *.bar(..)) &&
args(number);
before() : myClassTaskCalled(){
System.out.println("rada before() w MyAspect");
}
before(int value) : anyClassBarCalled(value) {
System.out.println("rada before() w MyAspect");
System.out.println("Parametr " + value);
}
}
Przekroje statyczne
29
AspectJ może działać na statycznej strukturze aplikacji. Umożliwia to m. in.:
dodawanie nowych składowych do klasy
dodawanie nowych metod
deklarowanie nowych interfejsów
deklarowanie nowych zależności dziedziczenia
Przykład:
public aspect AddRunnable {
declare parents : MyClass implements Runnable;
public void MyClass.run(){
System.out.println("Implementacja metody Runnable.run()");
}
}
Podsumowanie
30
Możliwość zmiany kodu klasy Javy w trakcie wykonywania programu wprowadza
wiele nowych możliwości przy tworzeniu oprogramowania. Zmiany takie można
wprowadzać bezpośrednio do kodu ładowanego przez
ClassLoader
'a lub poprzez
różne dostępne narzędzia (np. ASM). Wiele popularnych bibliotek korzysta z tej
techniki aby efektywnie realizować zadania (np. Hibernate). Na koncepcji
dynamicznej podmiany kodu opiera się jeden z paradygmatów tworzenia
oprogramowania – programowanie aspektowe.