Java SE 8
MARCIN MICHALSKI
Java SE 8 jest w środowisku programistów Java
najbardziej oczekiwaną wersją platformy od 2004
roku. Otrzymamy nie tylko nowe funkcjonalności i poprawki
API, ale wprowadzone zostaną również nowe elementy składni
języka Java. Przyjrzyjmy się bliżej nowej wersji Java SE 8!
Injection po raz kolejny…
Marek Puchalski
Open Web Application Security Project (OWASP)
jest społecznością skupiającą osoby, instytucje
i organizacje zainteresowane poprawą bezpieczeństwa
aplikacji webowych.W ramach realizowanych przez OWASP
projektów tworzone są narzędzia, dokumenty i procesy
wspomagające pracę przy tworzeniu systemów. Dobrze o tym
pamiętać, aby w różnych sytuacjach nie wynajdywać koła na
nowo, tym bardziej że oferowane zasoby wspomagają pracę
nie tylko deweloperów i architektów, lecz również testerów i
menadżerów projektów.
Strefy czasowe w Javie i bazach
danych?
Piotr Wirkus
Jak pamiętamy ze szkoły podstawowej, Ziemia obraca się
wokół własnej osi, co powoduje występowanie po sobie
pory dnia i pory nocy. Raz na dobę ustawienie promieni
słonecznych jest w zenicie (najwyższym punkcie względem
horyzontu), co potocznie nazywamy „południem”.
Wehikuł czasu w komputerze, czyli
o systemach kontroli wersji na
przykładzie Subversion
Sławomir Andrzejewski
Artykuł przybliża Czytelnikowi koncepcje i strategie ozwoju
oprogramowania przy wykorzystaniu systemów kontroli
wersji. Oprócz zagadnień teoretycznych zaprezentowano
podstawowe scenariusze pracy z Subversion.
Sposób na cyberprzestępców czyli
jak się wyszkolić z bezpieczeństwa
informatycznego
Kamil Brzeziński
Najsłabszym ogniwem bezpieczeństwa jest paradoksalnie
człowiek. Nawet najbardziej zaawansowany system
informatycznych zabezpieczeń nie sprawdzi się w walce z
hakerami, jeśli jego użytkownik sam nie zadba o poufność
przetwarzanych danych.
18
12
4
Software Developer’s Journal
jest wydawany przez
NPRACA Sp. z o.o.
Redakcja: sdjpl@sdjournal.pl
Kierownik produkcji: Andrzej Kuca
Adres korespondencyjny:
NPRACA Spółka z o.o.
ul. 3 Maja 46, 72-200 Nowogard, Polska
Oddział w Warszawie:
ul. Postępu 17 D, 02-676 Warszawa, Polska
www.sdjournal.pl
Facebook: https://www.facebook.com/SoftwareDevelopersJournal
Redakcja dokłada wszelkich starań, by publikowane w piśmie informacje i
programy były poprawne, jednakże nie bierze odpowiedzialności za efekty
wykorzystania ich; nie gwarantuje także poprawnego działania programów
shareware, freeware i public domain.
Wszystkie znaki firmowe zawarte w piśmie są własności odpowiednich firm.
Zostały użyte wyłącznie w celach informacyjnych.
Osoby zainteresowane wspópracą prosimy o kontakt:
sdjpl@sdjournal.pl
Skład i łamanie / projekt okładki:
Digital Concept
www.digitalconcept.pl
admin@digitalconcept.pl
26
38
REKLAMAJeżeli jesteś zainteresowany/a
reklamą swoich produktów w naszym
magazynie
Jeżeli jesteś zainteresowany/a
wypromowaniem swojej marki wśród
specjalistów IT
Jeżeli szukasz klientów lub
pracowników
Zareklamuj się u nas!
Posiadamy bazę 90 tysięcy
specjalistów z różnych
działów IT!
W celu uzyskania oferty
reklamowej skontaktuj się z
nami: sdjpl@sdjournal.pl
tel. 799 46 34 76
www.sdjournal.pl
1/20144
W
2004 roku światło dzienne ujrzała Java SE
5. Otrzymaliśmy tak bardzo oczekiwane ty-
py generyczne, enumeracje, pętlę foreach,
autoboxing, możliwość definiowania metod ze zmien-
ną liczbą parametrów... Śmiało można powiedzieć, że
mieliśmy do czynienia z rewolucją w programowaniu
w języku Java, jakości kodu oraz jakości tworzonych
przez nas aplikacji. Następne rewizje przynosiły raczej
kosmetyczne zmiany - poprawki w API, bugfixy, imple-
mentacje nowych funkcji języka dyskutowanych w ra-
mach Java Community Process, jednak sama składnia ję-
zyka nie zmieniła się prawie wcale.
W 2010 roku pojawiły się pierwsze oficjalne doku-
menty opisujące szczegóły wprowadzenia domknięć
(closure) dla języka Java w ramach Lambda Project – dłu-
go oczekiwanych przez programistów. Po rezygnacji z
wprowadzenia domknięc w siódmej wersji platformy
Java, Java SE 8 nareszcie wprowadza Project Lambda
pod strzechy!
Project Lambda
Project Lambda powstał w celu rozwiązania „problemu
wertykalnego” w Java. Zarówno Java API jak i wiele bi-
bliotek i frameworków Java wykorzystuje technikę wy-
wołań zwrotnych realizowanych poprzez interfejsy za-
wierające tylko jedną metodę (SAMType – Single Abstract
MethodType).
Implementacja takiego interfejsu często realizowana
jest poprzez anonimowe klasy wewnętrzne:
button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { long when = e.getWhen(); Date whenDate = new Date(when); System.out.println(″Action prformmed at:″ +
whenDate.toString()); }
});
Przykład 1
Implementacja właściwej logiki wymaga dodatkowego,
powtarzalnego kodu niepotrzebnie wydłużającego kod
proste wywołania metod.
Wykorzystując nowe wyrażenia lambda powyższa im-
plementacja wyglądać będzie następująco:
button.addActionListener(event -> { long when = event.getWhen(); Date whenDate = new Date(when); System.out.println(″Action performmed at:″ +
whenDate.toString());
});
Przykład 2
Stosując wyrażenia lambda możemy pozbyć się nad-
miarowego kodu i skupić na implementacji logiki sys-
temu. Kod staje się zdecydowanie bardziej czytelny.
Przyjrzyjmy się szczegółom implementacji i zobaczmy
jak to działa!
Wyrażenia lambda są ściśle związane z interfejsami
funkcyjnymi – nowym pojęciem wprowadzonym w Ja-
va SE 8. Interfejs funkcyjny jest to interfejs definiują-
cy dokładnie jedną metodę abstrakcyjną. Przykładem
Java SE 8
Java SE 8 jest w środowisku programistów Java
najbardziej oczekiwaną wersją platformy od 2004
roku. Otrzymamy nie tylko nowe funkcjonalności i
poprawki API, ale wprowadzone zostaną również
nowe elementy składni języka Java. Przyjrzyjmy się
bliżej nowej wersji Java SE 8!
Dowiesz się
Czym są wyrażenia lambda, poznasz nowe ele-
menty języka Java oraz nowe możliwości wielowąt-
kowego przetwarzania danych.
Powinieneś wiedzieć
Czym jest dziedziczenie i typy generyczne. Powi-
nieneś posiadać praktyczną wiedzę z zakresu ko-
lekcji w języku Java oraz znać zagadnienie prze-
twarzania wielowątkowego.
www.sdjournal.pl 5
JAVA SE 8
(String a, String b) -> {System.out.println(a+b)};
(String a, String b) -> {return a+b};
Pierwsze wyrażenie przyjmuje 3 argumenty typu
String i zwraca ich konkatenację.
Drugie wyrażenie nie przyjmuje żadnych argumentów
i zwraca typ int
Trzecie wyrażenie przyjmuje 2 obiekty typu String i
wyświetla ich złożenie w konsoli.
Ostanie wyrażenie przyjmuje 2 obiekty typu String i
zwraca ich konkatenację.
Warto zauważyć, że w składni wyrażenia lambda nie
deklarujemy typu interfejsu funkcyjnego, a skoro język
Java jest językiem silnie typowanym, powstaje pytanie
jaki typ reprezentuje dane wyrażenie. Otóż typ wyra-
żenia wnioskowany jest z kontekstu. Oczywiście to sa-
mo wyrażenie lambda może być reprezentowane przez
różne typy, przykład
Callable callable = c -> ″Kilroy was here!″;
PrivilegedAction action = c -> ″Kilroy was
here!″;
Kompilator sprawdza czy wyrażenie lambda może być
reprezentowane przez dany typ następująco:
• typ musi być interfejsem funkcyjnym
• wyrażenie lambda posiada tę samą ilość parame-
trów co metoda interfejsu funkcyjnego i są one tego
samego typu
• typ zwracany przez wyrażenie lambda jest tego sa-
mego typu co typ zwracany przez metode interfejsu
funkcyjnego
• wyjątki rzucane w ciele wyrażenia lambda zdeklaro-
wane są w metodzie interfejsu funkcyjnego
Zatem jeśli chcemy uprościć naszą implementację i wy-
korzystywać wyrażenia lambda – najpierw musimy po-
siadać interfejs funkcyjny.
Niestety nie mamy możliwości zdefiniowania gene-
rycznego typu, który przechowywałby po prostu refe-
rencję do funkcji.
Kolejną konsekwejcją jest to, że wyrażenia lambda
możemy deklarować tylko w przypadkach gdy istnieje
odpowiedni kontekst dla określenia typu interfejsu funk-
cyjnego, czyli w następujących sytuacjach:
• deklaracje zmiennych
• przypisanie wartości
• operacje return
• inicjalizacje tablic
• argumenty metod i konstruktorów
• w ciele wyrażeń lambda
• operator warunkowy ? :
• rzutowanie
takiego interfejsu jest wykorzystany w przykładzie 1
java.awt.event.ActionListener. Przy czym słowo de-
finiujący nie oznacza, że interfejs nie może posiadać
większej ilości metod. Interfejs może definiować do-
wolną ilość metod, jednak pozostałe metody abstrak-
cyjne muszą być nadpisywane przez klasę java.lang.
Object.
Mówiąc prościej – interfejs funkcyjny może posiadać
tylko jedną metodę, która nie została zadeklarowana
w klasie java.lang.Object
@FunctionalInterface
public interface Comparator { int compare(T o1, T o2); boolean equals(Object obj);
}
Przykład 3
Kompilator automatycznie rozpoznaje interfejsy funk-
cyjne, jednak dla celów informacyjnych została wpro-
wadzona adnotacja @FunctionalInterface
Java SE 8 wprowadza nowy pakiet java.util.function
zawierający podstawowy zestaw interfejsów funkcyjnych
reprezentujących podstawowe funkcje:
• Predicate – testowanie własności obiektu – dla
obiektu zwracana jest wartość boolean
• Consumer - wykonanie akcji na obiekcie bez zwra-
cania wartości
• Function - funkcja zwracająca obiekt typu R na
podstawie obiektu T
• Supplier - konstruktor, fabryka obiektów typu T
• UnaryOperator - funkcja zwracająca obiekt typu T
– obpowiednik Function• BinaryOperator - funkcja zwracająca obiekt typu
T, przyjmująca dwa argumenty typu T
Dodatkowo dostępne są specjalizacje powyższych in-
terfejsów, np. specjalizacje interfejsów dla typów pry-
mitywnych int, long oraz double – pozostałe typy moż-
na uzyskać poprzez rzutowanie.
Zobaczmy jak teraz zbudowana jest deklaracja wyraże-
nia lambda. Składa się z następujących elementów:
1. Deklaracja argumentów wejściowych metody inter-
fejsu funkcyjnego oddzielonych przecinkami
2. Operatora ->3. Ciała wyrażenia zawierającego jedną komendę lub
blok kodu zawartego w nawiasach klamrowych { }.
Zastosowania wyrażeń prezentują poniższe przykłady
(String a, String b, String c) -> ″Hello ″ + a + ″ ″
+ b + ″ ″ + c;
() -> 13;
1/20146
Na początku wspomnieliśmy o tym, że wyraże-
nia lambda pozwalają zastąpić anonimowe klasy we-
wnętrzne. Warto zatem przypomnieć problem za-
kresu zmiennych i przesłaniania metod w klasach we-
wnętrznych, który często trapił programistów. Wyra-
żenia lambda nie są reprezentowane przez klasy we-
wnętrzne, nie dziedziczą żadnych metod czy zmien-
nych z typów nadrzędnych. Dostępna przestrzeń
nazw definiuje tylko moment deklaracji. Rzućmy
okiem na następujący przykład:
public class LambdaTest { Runnable printThis = () -> { System.out.
println(this); }; Runnable printToString = () -> { System.out.
println(toString()); }; Runnable printInnerClass = new Runnable() { @Override public void run() { System.out.println(toString()); } }; public String toString() { return «Kilroy was here!»; } public static void main(String... args) { new LambdaTest().printThis.run(); new LambdaTest().printToString.run(); new LambdaTest().printInnerClass.run(); }
}
Dzięki deklaracji wyrażeń lambda wewnątrz obiek-
tu LambdaTest, staje się on przestrzenią nazw dla wy-
rażeń lambda. Po uruchomieniu aplikacji wywoła-
nie printThis oraz printToString wyświetli na kon-
soli tekst ”Kilroy was hare!”, natomiast wywołanie
printInnerClass wyświetli nam coś na kształt Lambda-
Test$1@17c264.
Skoro ograniczna nas tylko moment deklaracji
– co ze zmiennymi lokalnymi? Do tej pory dla klas
wewnętrznych dostęp ograniczony był do finalnych
zmiennych lokalnych. Java SE 8 wprowadza nowe po-
jęcie effective final local variable. Zmienną możemy
określić jako effective final, jeśli jej wartość nie zmie-
nia się od momentu deklaracji. W Java SE 8 dostęp do
takich zmiennych lokalnych możliwy jest zarówno z
poziomu wyrażeń lambda jak i anonimowych klas we-
wnętrznych!
Powyższy przykład pokazuje, że przejście z klas we-
wnętrznych na wyrażenia lambda może doprowadzić
do trudnych do wykrycia błędów. Jeśli nasz kod nie
jest odpowiednio obłożony testami, warto o to zadbać
przed zmianą implementacji – unikniemy nieprzyjem-
nych konsekwencji.
Podsumowując - wyrażenia lambda oszczędzają czas
programisty dając możliwość zastąpienia anonimo-
wych klas wewnętrznych. I na taką rewolucję przyszło
nam czekać 10 lat? Na pewno zmiany w składni języka
upraszczają implementację, na pewno kod aplikacji bę-
dzie bardziej czytelny, jednak musimy nauczyć się korzy-
stać z wyrażeń lambda i konsekwentnie wykorzystywać
dostępne nowe interfejsy funkcyjne. Efekty przyjdą z cza-
sem... Jednak to nie wszystko - zobaczmy jakie inne zmia-
ny zostały wprowadzone w Java API i jak współgrają z
nową filozofią wyrażeń lambda.
Metody domyślne
Rzućmy okiem na implementację interfejsu Iterablepublic interface Iterable { Iterator iterator(); default void forEach(Consumer super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
}
Wzrok nas nie myli – interfejs zawiera implementację
metody forEach. Stało się to możliwe, dzięki wprowa-
dzeniu nowego elementu języka – metodami domyśl-
nymi - default methods, określanymi również jako virtual
extension methods lub defender methods.
Jak zauważyliśmy wcześniej samo wprowadzenie
wyrażeń lambda nie przyniesie rewolucji w sposobie
programowania w języku Java. Konieczne jest umoż-
liwienie wykorzystania nowych możliwości w istnieją-
cych już elementach języka. Jednak zmiana istniejących
interfejsów pociągnęłaby za sobą konieczność zmia-
ny implementacji, co praktycznie uniemożliwiałoby
przejście ze starszych wersji języka. Metody domyślne
umożliwiają rozszerzenie interfejsów bez konieczno-
ści zmian klas implementujących je implementacji. A
jakie daje nam to możliwości? Zamiast dobrze znanej
pętli foreach
for (String string : list) { System.out.println(string);
}
Mamy możliwość wykorzystania wyrażeń lambda
list.forEach(c -> System.out.println(c));
Przeglądając implementację innych interfejsów – np.
interfejs java.util.Comparator nasuwa się wniosek.
www.sdjournal.pl 7
JAVA SE 8
Zgodnie z określeniem defender method, metody do-
myślne na daną chwilę największe zastosowanie znaj-
dą przy refaktoryzacji pod kątem wyrażeń lamb-
da i świetnie się w tym celu sprawdzają. W przypad-
ku tworzenia nowej implementacji – bardziej przydat-
ne będzie przemyślane i konsekwentne wykorzystanie
interfejsów funkcyjnych.
Interfejsy w języku Java rekompensowały brak dzie-
dziczenia wielokrotnego – rekompensowały, ponieważ
do tej pory interfejsy nie mogły zawierać implementa-
cji metod. Czyżby Java SE 8 wprowadzała wsparcie dla
wielokrotnego dziedziczenia? Nie do końca – dziedzi-
czenie metod domyślnych odbywa się na tych samych
zasadach co zwykłych metod, jednak z pewnymi wyjąt-
kami:
• Jeśli klasa dziedziczy więcej niż jeden interfejs defi-
niujący metodę default o tym samym deskryptorze,
występuje błąd kompilacji
• Kolejność w jakiej interfejsy pojawiają się w deklara-
cji implements lub extends nie mają wpływu na me-
chanizm dziedziczenia
• Metody zdefiniowane w klasach przesłaniają meto-
dy domyślne – nawet jeśli są to metody abstrakcyj-
ne, bez względu na kolejność w hierarchii dziedzi-
czenia
• Przeładowana metoda domyślna jest pomijana –
tracimy do niej dostęp
Przyjrzyjmy się następującemu przykładowi
public class Mammal { public void speak() { System.out.println(″I can’t speak″); }
}
public interface Human { default void speak() { System.out.println(″I am a human″); }
}
public interface Male { default void speak() { System.out.println(″I am a male″); }
}
public class Person extends Mammal implements Human,
Male {
}
Zgodnie z zasadami dziedziczenia dla klas domyślnych
wywołanie metody speak() dla obiektu Person spo-
woduje wyświetlenie I can’t speak, ponieważ zwykłe
metody przesłaniają metody domyślne.
Jeśli klasa Person nie będzie rozszerzać klasy
Mammal, tylko implementować interfejsy Human i Male,
otrzymamy błąd kompilacji. Czyli dziedziczenie wie-
lokrotne nie jest możliwe... jednak w takiej sytuacji
istnieje możliwość wyboru, z której klasy metodę
chcemy dziedziczyć, poprzez przeładowanie meto-
dy speak()
public class Person implements Human, Male { public void speak() { Male.super.speak(); }
}
Zatem Java nadal stricte nie wspiera dziedziczenia wie-
lokrotnego. Jednak tak długo jak deskryptory metod
domyślnych będą różne, możemy zamiast rozszerzać
mozolnie kolejne klasy, zaimplementować kilka interfej-
sów z metodami domyślnymi – przy czym będzie to ra-
czej zła praktyka.
public interface Human { default void speak() { System.out.println(″I am a human″); }
}
public interface Male { default void sing() { System.out.println(″I am singing″); }
}
public class Person implements Human, Male {
}
Referencjonowanie metod
Kolejną nowością w składni języka Java jest możliwość
stworzenia referencji do metody. Tworzenie referencji
do metody odbywa się na takich samych zasadach jak
tworzenie wyrażeń lambda – referencja do metody to
instancja interfejsu funkcyjnego. Rzućmy okiem na inter-
fejs Comparator.
Możemy przygotować instancję klasy comparator za
pomocą wyrażeń lambda.
Comparator comp = (a ,b) -> a.getName().
compareTo(b.getName());
Aby jeszcze uprościć deklarację, wykorzystajmy meto-
dę domyślną Copmarator.comparing
Comparator comp = Comparator.comparing(p ->p.getName());
Wykorzystujące referencje do metody deklaracja wy-
glądać będzie następująco
1/20148
Comparator comp = Comparator.
comparing(Person::getName());
Jak widać różnica nie jest duża, jednak kod staje się
bardziej czytelny – widzimy, że do porównania wyko-
rzystujemy metodę getName z klasy Person.
Możemy tworzyć referencje do następujących metod:
• metod statyczne - String::valueOf
• metod instancji obiektu - person::getName
• metod instancji obiektu określonego typu -
Person::getName
• konstruktorów – Person::new
Parametry metody interfejsu statycznego stają się pa-
rametrami metod referencjonowanych, dzięki czemu
możemy je dowolnie rzutować i korzystać z varargs-
ów, np.:
Comparator comp = Comparator.
comparing(Human::getAge());
Runnable r = Example::main - void main(String...
args)
Consumer cons = Male::speak;
Referencjonowanie metod ułatwia wykorzystanie inst-
niejących metod o deskryptorze identycznym z inter-
fejsem funkcyjnym.
public class PersonHelper { public static int getAgeDifference(Person a,
Person b) { return a.getAge() - b.getAge(); }
}
Comparator byAge =
PersonHelper::getAgeDifference;
Comparator byAgeLambda = (a, b) ->PersonHelper.getAgeDifference(a,
b);
W takich przypadkach referencje metod są zdecydo-
wanie bardziej czytelne niż wyrażenia lambda.
Miejmy nadzieję, że nowy element języka przyczyni się
do poprawienia jakości kodu aplikacji – do tej pory na-
zbyt często prosty kod lądował bezpośrednio w ciele
anonimowych klas wewnętrznych i powtarzany był w
różnych miejscach w aplikacji. Referencje do metod, ja-
ko nowy element języka Java zachęca do zaprzestania
tej złej praktyki.
Kolekcje w Java SE 8
Opisując metody domyślne wprowadzone w interfej-
sach w Java SE 8 wspomniałem, że ich głównym zasto-
sowaniem jest rozszerzenie istenijących interfejsów i
dostosowanie ich implementacji pod kątem wyrażeń
lambda.
Implementacja kolekcji przeszła największą metamor-
fozę za sprawą wprowadzenia wyrażeń lambda i metod
domyślnych.
Stream API
Do tej pory najwygodniejszym sposobem przetwa-
rzania elementów kolekcji w Java były obiekty Itera-
tor oraz pętla foreach – wykorzystująca zresztą ite-
ratory.
Z założenia takie przetwarzanie było sekwencyjne –
elementy pobierane były jeden po drugim i przetwarza-
ne.
Strumienie (Stream) wprowadzają nowy sposób ite-
racji z wykorzystaniem operacji agregujących (aggre-
gate operations). Ciekawostką jest to, że definiując ta-
kie operacje nie wykorzystujemy metody next() do
której tak bardzo przyzwyczaiły nas iteratory. Przyj-
rzyjmy się przykładowi:
List list = new ArrayList<>();
listOfPeople.stream()
.filter(p -> p.getAge() > 18)
.filter(p -> p.isUnemployed())
.forEach(p -> storePersonalData());
Powyższy kod z dostępnej listy obiektów typu Person
wykona operację storePersonalData dla każdej bezro-
botnej osoby powyżej 18 roku życia.
Zauważmy, że korzystając ze strumieni i operacji
agregujących nie definiujemy w jaki sposób mamy po-
bierać kolejne elementy, ale jakie operacje mają zo-
stać na nich wykonane. Wykorzystujemy do tego celu
wyrażenia lambda i referencje do metod. Sposób ite-
racji omówimy jednak za chwilę
Zwróćmy uwagę na to, że cała logika została za-
mknięta w jednej „linii” kodu, dzięki wprowadzeniu
możliwości łączenia operacji agregujących w poto-
ki (pipelines) – na wzór potoków znanych z systemu
UNIX.
Potok składa się z następujących elementów:
• źródło – może nim być kolekcja, tablica, generator
obiektów (np. obiekt Iterator), kanał wejścia/wyjścia
(IO channel) – np. zwracany przez metodę Files.
lines(Path file)
• operacje pośrednie (intermediate operations)– opera-
cje zwracające strumienie, np. operacja filter
• operacja kończąca (terminal operation) – opera-
cja kończąca potok, zwracająca obiekt nie będą-
cy strumieniem – np. kolekcję, lub przetwarzające
elementy strumienia bez zwracania żadnej warto-
ści – np. operacja forEach
www.sdjournal.pl 9
JAVA SE 8
Dostępne są następujące operacje pośrednie:
• filter(Predicate super T> predicate) – opera-
cja zwracająca strumień będący podzbiorem speł-
niającym określone kryterium, np.. filter(p ->p.getAge() > 18)
• map(Function super T,? extends R> mapper) -
operacja zwracająca strumień skonwertowanych
obiektów, np. map(p -> Unemployed::new)
• flatMap(Function> mapper)
- operacja mapująca relację jeden do wiele na stru-
mień. Wyobraźmy sobie, że obiekt Person posiada
metodę zwracającą kolekcję obiektów typu Child,
np. Collection getChildren(). Możemy za-
tem zwrócić strumień dal takiej kolekcji. W efekcie
uzyskamy strumień wszystkich dzieci wejściowych
obiektów Person, np. flatMap(p -> p.getChildren().
stream())
• sorted() - operacja zwracająca strumień posorto-
wany za pomocą istniejącej metody compareTo
• sorted(Comparator super T> comparator) - ope-
racja zwracająca strumień posortowany za pomocą
określonego obiektu comparator
• distinct() - operacja zwracająca strumień unikal-
nych obiektów. Do wykrywania duplikatów wyko-
rzystywana jest metoda equals
Metody pośrednie mogą być dowlonie łączone, np.
listOfPeople.stream()
.filter(p -> p.getAge() > 18)
.filter(p -> p.isUnemployed())
.distinct()
.sorted()
Co również bardzo istotne, metody pośredie są meto-
dami lazy – będą wywołane dopiero w momencie wy-
wołania metody końcowej. Możemy zatem stworzyć
potok bez metody końcowej i uzyskany strumień prze-
tworzyć w późniejszym czasie.
Dostępne są następujące operacje końcowe:
• void forEach(Consumer super T> consumer) -
operacja przetwarza elementy strumienia bez
zwracania żadnej wartości, np. forEach(p ->storePersonalData());
• R collect(Collector super T,R> collector) –
operacja przetwarzająca strumień i zwracająca po-
jedynczą wartość lub kolekcję obiektów. Możemy
zwrócić listę posortowanych obiektów Person
listOfPeople.stream()
.filter(p -> p.getAge() > 18)
.filter(p -> p.isUnemployed())
.distinct()
.sorted()
.collect(Collectors.toList())
• Optional reduce(T identity, BinaryOperatoraccumulator) - operacja przetwarzająca strumień i
zwracająca pojedynczą wartość. Możemy np. poli-
czyć sumę wszystkich miesięcznych świadczeń osób
bezrobotnych:
listOfPeople.stream() .filter(p -> p.getAge() > 18) .filter(p -> isUnemployed()) .mapToInt(p -> p.getMonthlyGrants()) .reduce(0, (a,b) -> a + b)
• Iterator iterator() - operacja zwraca iterator
obiektów pozostałych po przetworzeniu potoku
Zarówno dla operacji collect jak i reduce istnie-
ją gotowe implementacji ciekawych funkcji. Dla
operacji reduce, w zależności od typu strumienia,
mamy dostępne, np. dla Strean opera-
cje arytmetyczne takie jak suma czy średnia. Ope-
racje collect zawarte są w klasie Collectors. Do
ciekawszych należą np. funkcje grouppingBy czy
partitioning – zachęcam do lektury javadoc.
Zobaczmy teraz w jaki sposóþ pobierane są ele-
menty kolekcji. Zamiast jawnej iteracji (external ite-
ration), mamy do czynienia z wewnętrzną iteracją
(internal iteration) z wykorzystaniem obiektów Spli-
terator.
Obiekt spliterator – podobnie jak Iterator se-
kwekncyjnie przetwarza elementy kolekcji, ale rów-
nocześnie umożliwia podział elementów kolekcji na
mniejsze porcje za pomocą metody trySplit().
Efektem takiej operacji jest stworzenie podrzęd-
nej instancji Spliterator, przy czym główny Spliterator
pominie elementy przetwarzane przez obiekty pod-
rzędne.
Oczywiście możliwy jest dalszy podział podrzęd-
nych instancji Spliterator, tak aby uzyskać odpowied-
nio małe porcje i przetwarzać je równolegle.
Czyli przetwarzanie sekwencyjne się nie zmienia –
Spliterator zastąpił Iterator. Natomiast przetwarzanie
równoległe z podziałem Spliteratorów brzmi skom-
plikowanie – pula obiektów spliterator, pula wątków
lub ForkJoinPool, przydział obiektów do przetwo-
rzenia, zebranie rezultatów cząstkowych... Nic po-
dobnego! Z Java SE 8 równoległe przetwarzanie
otrzymujemy za darmo!
listOfPeople.parallelStream()
.filter(p -> p.getAge() > 18)
.filter(p -> p.isUnemployed())
.forEach(p -> storePersonalData());
1/201410
Każdy strumień może być przetworzony równole-
gle, dzięki czemu w końcu w prosty sposób będzie-
my mogli wykorzystać potencjał wielordzeniowych
maszyn. Jeśli prawo Moorea ma się obronić – rdze-
ni będzie coraz więcej, więc to bardzo dobra wia-
domość. Jednak jeśli przetwarzanie nie wprowadzało
prawie żadnych ograniczeń, tak w strumieniach rów-
noległych (parallel stream) obowiązują już pewne re-
guły:
• strumień musi być statyczny – kolekcja nie mo-
że być modyfikowana (elementy kolekcji nie mogą
być dodawane, usuwane, przemieszczane) podczas
przetwarzania. Operacje pośrednie również nie
modyfikować kolekcji
• po zakończeniu przetwarzania strumień nie może
być ponownie wykorzystany
• przetwarzanie strumieni z zachowaniem kolej-
ności wymaga użycia specjalnej operacji końco-
wej forEachOrdered gwarantującej przetworzenie
rezultatu dokładnie w kolejności w której znaj-
dował się na wejściu strumienia. W przypad-
ku użycia zwykłej metody forEach podczas prze-
twarzania równoległego – kolejność zostanie
utracona
• konieczność użycia metod collect odpowied-
nich do przetwarzania równoległego. Każ-
dy obiekt Collector opisany jest przez enume-
rację Characteristics. Wymagane jest stoso-
wanie obiektów Collector z charakterystyką
CONCURRENT.
Na pewno zmiany w Java Collections API zdecydo-
wanie uproszczą kod naszych aplikacji – zwłaszcza
tych wykorzystujących wielowątkowość. Funkcjo-
nalności, które do tej pory wymagały implementa-
cji kilku klas, będziemy mogli zamknąć nawet w kil-
ku linijkach kodu.
Jednak zanim zaczniemy wykorzystywać nowe API
warto zagłębić się w szczegóły dokumentacji oraz
przejrzeć implementację podstawowych obiektów
Spliterator – jest duże pole do zwiększenia wydajno-
ści naszego kodu. Nie należy ulegać pokusie i wyko-
rzystywać równoległego przetwarzania dla wszyst-
kich strumieni. Zanim nasz kod trafi na docelowe
środowisko, warto przeprowadzić porównawcze te-
sty wydajnościowe rozwiązań wielowątkowych i se-
kwencyjnych.
Powtarzalne adnotacje (Repeating
annotations)
Jednym z ciekawszych przykładów adnotacji w Java do
tej pory była adnotacja @EJBs – jedyną jej rolą było gru-
powanie adnotacji @EJB
@EJBs({@EJB(name=″name1″,beanInterface=Type1.class),
@EJB(name=″name2″,beanInterface=Type2.class)})
Java SE 8 rozwiązuje ostatecznie ten problem przez
prowadzenie powtarzalnych adnotacji – możliwe bę-
dzie uzycie jednej adnotacji wiele razy w tym samym
miejscu.
@EJB(name=″name1″,beanInterface=Type1.class)
@EJB(name=″name2″,beanInterface=Type2.class)
Mała rzecz, a cieszy.
Adnotacje typów (Type Annotations)
Java 8 SE wprowadza możliwość dodawania adnotacji dla
typów, np.
@NotNull String text = ″test″;
@Readonly List users = ...
public String getFullText(@Readonly Paper
sheetOfPaper)
Celem wprowadzenia tych adnotacji była poprawa ja-
kości kodu przez umożliwienie bardziej szczegółowej
jego kontroli za pomocą narzędzi analizujących kod.
Spójrzmy na przykład:
Deklaracja metody
public class Paper { public String getText(@Readonly Paper
sheetOfPaper) { ...
Kod wywołujący metodę:
@NotNull String text = sheetOfPaper.getText();
Zauważmy, że metoda getText może potencjalnie
zwracać wartość null i przypisać ją do zmiennej zade-
klarowanej jako @NotNull. Oczywiście kompilacja i wy-
konanie programu będzie działać prawidłowo. Możliwe
jest natomiast wychwycenie potencjalnego problemu
przez narzędzia analizujące kod takie jak CheckerFrame-
work, Javarifier czy inne.
Możliwość dodawania adnotacji typów na pewno
sprawdzi się w zaawansowanych projektach z bar-
dzo restrykcyjnymi normami jakościowymi i popar-
tymi dobra analizą. Na pewno wywrą wpływ już na
etapie projektowania klas i interfejsów – koniecz-
ne będzie nie tylko zaplanowanie nazw i struktur
danych, ale także ich zachowania. Na pewno war-
to przyjrzeć się dostępnym rozwiązaniom wspiera-
jącym analizę kodu. Ciekawym testem byłby również
pomiar czasu potrzebnego na analizę średniej wiel-
kości projektu.
www.sdjournal.pl 11
JAVA SE 8
Java Date - Time API
Na koniec, wisienka na torcie. Istniejąca wersja Date
-Time API oparta na klasie java.util.Calendar odcho-
dzi do lamusa. Dostaliśmy dokładnie to o co prosili-
śmy od lat.
Po pierwsze, Java SE 8 wprowadza pakiet java.time
gdzie znjadziemy wszystkie potrzebne klasy. Po dru-
gie API jest przejrzyste, model prosty i pozbawiony
wprowadzających zamieszanie metod. Pierwszy z gó-
ry przykład.
LocalDateTime dt = LocalDateTime.now();
Calendar cal = Calendar.getInstance();
Metoda now niesie ze sobą jasny przekaz , a metoda
getInstance hmm… singleton?
A co ze strefami czasowymi? Spróbujmy obliczyć go-
dzinę przylotu gdy poruszamy się po różnych strefach
czasowych, gdzie lot trwa 8 godzin:
LocalDateTime now = LocalDateTime.now();
ZonedDateTime sydneyTime = dt.atZone(ZoneId.
of(″Australia/Sydney″));
ZonedDateTime tokioTime = sydneyZoneTime.
withZoneSameInstant(dt.
atZone(ZoneId.of(″Asia/Tokyo″)));
ZonedDateTime tokioArrivalTime = tokioTime.
plusHours(8);
Wykorzystując java.util.Calendar sami musielibyśmy
obliczać przesunięcie między strefami tworząc dodat-
kowe obiekty TimeZone i przekazywać bieżącą datę w
postaci wartości long, a wykonując operację przesunię-
cia czasu musielibyśmy użyć metody cal.add(Calendar.
HOUR, 8).
Ijeszczejednadobrainformacja.KlasaSimpleDateFormat
również doczekała się następcy i tym razem jedną instan-
cję swobodnie możemy używać w wielu wątkach!
DateTimeFormatter dtf = DateTimeFormatter.
ofPattern(″MM-dd-yyyy HH:mm″);
dtf.format(tokioArrivalTime);
Geniusz tkwi w prostocie - po wielu latach w końcu
doczekaliśmy się biblioteki wspierającej operacje na
czasie!
Podsumowanie
Java SE 8 na pewno jest krokiem milowym w rozwo-
ju języka java. Wyrażenia lambda zależne od interfej-
sów funkcyjnych na pewno nie dają pełnej elastycz-
ności, ale są użyteczne i powinny rozwiązać problem
wewnętrznych klas anonimowych. Wprowadzenie
metod domyślnych do interfejsów, może nie wyglą-
da zbyt elegancko, ale świetnie realizuje swoje cele
– czego przykładem są chociażby zmiany w Collec-
tions API. Czas pokaże, które z nowości przyjmą się w
środowisku Java, a które będą musiały zostać jeszcze
dopracowane. Jedno jest pewne – mamy nowe i do-
bre narzędzia, musimy nauczyć się z nich korzystać,
czekamy na reakcję społeczności i na pewno czeka-
my na więcej!
MARCIN MICHALSKI
Współtwórca i współwłaściciel
marki Sagiton. Posiada 7 lat do-
świadczenia w programowaniu.
Obecnie realizuje się jako Sys-
tem Architect w projektach wy-
konywanych przez ArrowGroup
Sp. z o.o., której jest współzałoży-
cielem. Ukończył Informatykę na
Wydziale Elektroniki na Politechnice Wrocław-
skiej. Odbył kursy i szkolenia organizowane
przez: Cisco Network Academy, Sun Microsy-
tems, Oracle. Uzyskał certyfikaty eksperckie w
zakresie sieci komputerowych, programowa-
nia (Java/JEE) oraz baz danych. Jest autorem
licznych publikacji naukowych, prowadzi blo-
ga technicznego.
Marka: www.sagiton.pl [1]
Kontakt: marcin.michalski@sagiton.pl
Blog: marcin-michalski.pl [2]
1/201412
D
ziś chciałbym wspomnieć tylko o jednym - choć
pewnie najbardziej znanym - projekcie ze stajni
OWASP, mianowicie o OWASP Top 10. Lista
OWASP Top 10 jest listą dziesięciu najpopularniejszych
błędów bezpieczeństwa znajdowanych w aplikacjach webo-
wych. Istnienie listy ma skupić uwagę zespołów programi-
stów na te zagrożenia, których pojawienie w projekcie jest
najbardziej prawdopodobne, edukować oraz być punktem
startowym do korzystania z narzędzi i projektów OWASP.
Lista Top 10 aktualizowana jest przeciętnie co trzy lata, a
ostatnia odsłona listy miała miejsce w czerwcu tego roku.
NajkrótszarecenzjazmiandokonanychnaliścieOWASP
Top 10 2013 brzmiała by: “nic nowego”. Wszystkie roz-
poznane przez OWASP w roku 2010 zagrożenia pozosta-
ją nadal aktualne. Najnowsza odsłona listy bardzo dobrze
pokazuje, że w pogoni za wypełnianiem wymagań funk-
cjonalnych nadal zdarza nam się zapomnieć o bezpieczeń-
stwie tworzonej aplikacji. W epoce automatycznych ska-
nerów, przeczesujących internet w poszukiwaniu luk, nie
jest to krzepiący wniosek. Listę OWASP Top 10 2013 po
raz kolejny otwierają błędy typu injection (z najznamienit-
szym ich reprezentantem - SQL injection (SQLi)) i to o nich
chciałbym dzisiaj opowiedzieć.
Czym są błędy typu injection?
Najkrótsza, obrazowa (i nieoficjalna) definicja - błędy typu
injection powstają wtedy, gdy oddajemy w ręce użytkow-
nika aplikacji możliwość oprogramowania np. naszej bazy
danych. Robimy to najczęściej nieświadomie poprzez kon-
katenację elementów naszego kodu z tekstem wprowa-
dzonym do systemu. Rozpatrzmy następujące przykłady.
Przykład 1:
Runtime runtime = Runtime.getRuntime();
Process proc = runtime.exec(„find . -name „ +
args[0]);
Z kodu naszej aplikacji próbujemy uruchomić komen-
dę systemową dodatkową parametryzowaną przez użyt-
kownika.
Czego oczekujemy?
Oczekujemy nazwy pliku lub katalogu, który ma zostać
odnaleziony. Co może zrobić użytkownik? W zależności
od fantazji może na przykład spróbować usunąć całą za-
wartość dysku doklejając do polecenia tekst “foo; rm -rf
/” i uzyskując przez to:
find . -name foo; rm -rf /
Nie uruchamiacie serwerów aplikacji z uprawnieniami
administratora, prawda?
Przykład 2 (ze strony OWASP https://www.owasp.org/in-
dex.php/XPATH_Injection): Uwierzytelniamy użytkowników
naszej aplikacji na podstawie danych poniższego XMLa:ArnoldBakerABakerSoSecretAdminPeterPanPPanNotTellingUserInjection po raz kolejny…
Open Web Application Security Project (OWASP) jest społecznością
skupiającą osoby, instytucje i organizacje zainteresowane poprawą
bezpieczeństwa aplikacji webowych. W ramach realizowanych
przez OWASP projektów tworzone są narzędzia, dokumenty i
procesy wspomagające pracę przy tworzeniu systemów. Dobrze
o tym pamiętać, aby w różnych sytuacjach nie wynajdywać koła na
nowo, tym bardziej że oferowane zasoby wspomagają pracę nie tylko
deweloperów i architektów, lecz również testerów i menadżerów
projektów.
www.sdjournal.pl 13
INJECTION
SQL injection
Większość nietrywialnych systemów i aplikacji wymaga
dostępu do bazy danych. Wszędzie tam, gdzie wystę-
puje relacyjna baza danych, istnieje również zagroże-
nie wystąpienia błędów SQL injection. Co może zrobić
złośliwy użytkownik mogąc oprogramować naszą bazę?
Pojawiają się tutaj trzy zagrożenia:
Omijanie kryteriów klauzuli WHERE w zapytaniu
SQL
Klasyczny przykład zakłada uwierzytelnianie użytkowni-
ków w systemie na podstawie wykonania poniższego za-
pytania:
select * from users where name = ‘’
and password = ‘’;
Niebezpieczna parametryzacja miała by tu postać:
name = ″’ or 1=1 or ′a′=′a″
password = ″anything″
Pozyskiwanie dodatkowych informacji z bazy
Przy założeniu, że wykonywane w systemie zapytanie ma
postać:
select content from data where id = poniższa parametryzacja sprawi, że udostępnimy więcej
informacji niż byśmy tego chcieli:
id = ″21 UNION ALL SELECT password from users″
Przekazywanie dodatkowych poleceń do bazy
I tu kolejny przykład. Mając zapytanie:
select name from record where id = możemy się obawiać wstrzyknięcia parametryzacji ty-
pu:
id = ″1; DROP table record; DROP table address--″
budując parametryzowalne zapytanie XPath:
“//Employee[UserName/text()=‘“ + name+ “’And
Password/text()=‘“ + password +
“‚]”
Następująca parametryzacja pozwala całkowicie omi-
nąć nasze mechanizmy kontroli:
name: blah’ or 1=1 or ‚a’=’a
password: foo
Z powyższych przykładów wynika parę wniosków.
Po pierwsze błędy typu injection nie są jedynie ce-
chą aplikacji webowych i równie dobrze mogą wystę-
pować w aplikacjach desktopowych oraz mobilnych.
To czyni je jeszcze bardziej niebezpiecznymi. Po dru-
gie powodem powstawania błędów jest konkatenacja
elementów naszych zapytań z niefiltrowanymi para-
metrami wprowadzonymi przez użytkownika. To bar-
dzo istotne spostrzeżenie. Powinno uwrażliwić każ-
dego czytającego kod na potencjalne błędy wynikające
z faktu łączenia łańcuchów znaków celem parametry-
zacji zapytań i poleceń.
Czy jest się czego obawiać?
Podstawowe pytania jakie należy sobie zadać myśląc o
bezpieczeństwie budowanego systemu brzmią: Co ma-
my do stracenia?
Co stanowi o wartości naszej aplikacji? Chcemy
chronić nasze algorytmy, czy też chcemy chronić nasze
dane? Co się stanie, jeżeli utracimy całkowicie i jedno
i drugie?
Przedstawione powyżej przykłady są co prawda kry-
tyczne z punktu widzenia potencjalnych zniszczeń lub
innych konsekwencji w naszym systemie, jednak praw-
dopodobieństwo ich wystąpienia jest znikome. (Kto z
nas wywołuje w swojej aplikacji parametryzowalne po-
lecenia systemowe? Kto z nas uwierzytelnia użytkow-
ników na podstawie wpisów w pliku XML?) Co więc
sprawiło, że wielcy tego świata (Nokia, Sony Music,
Yahoo, FBI, NASA, itd.) padli ofiarą błędów typu in-
jection?
OWASP Top 10 2013 (https://www.owasp.org/index.php/Top_10_2013-Top_10)
• Injection
• Broken authentication and session management (Błędy uwierzytelniania i obsługi sesji)
• Cross-Site Scripting (XSS)
• Insecure direct object references (Niezabezpieczone, bezpośrednie linki do obiektów)
• Security misconfiguration (Błędy konfiguracji)
• Sensitive data exposure (Błędy obsługi poufnych danych)
• Missing function level access control (Brak weryfikacji uprawnień dostępu)
• Cross-Site Request Forgery (CSRF)
• Using components with known voulnerabilities (Wykorzystanie komponentów z błędami bezpieczeństwa)
• Unvalidated redirects and forwards (Niewalidowane przekierowania)
1/201414
Poza pomysłowością potencjalnego intruza, chcące-
go wykorzystać błędy w implementacji naszego sys-
temu, ograniczać będzie wykorzystana przez nas
technologia oraz biblioteki. Na przykład ataki ma-
jące na celu wykonanie w bazie dodatkowych po-
leceń (punkt 3) są możliwe w niektórych frame-
workach wykorzystywanych przez PHP, nie są na-
tomiast możliwe i wspierane przez np. sterownik
JDBC w Javie.
Podsumowując powyższe przykłady trzeba jednak
zauważyć, że konsekwencje błędów typu injection w
aplikacji idą bardzo daleko i umożliwiają pozyskanie i
nadpisanie wszystkich danych zawartych w bazie (przy
jednoczesnym ominięciu mechanizmów kontroli dostę-
pu do tych danych). Konsekwencje utraty informacji na
taką skalę mogą doprowadzić niektóre przedsiębior-
stwa nawet do bankructwa.
Co nie pomaga?
Rozwiązania wszelki problemów dzielą się na dwie kate-
gorie: na te które działają i na całą resztę. Próbę odpo-
wiedzi na pytanie, jak uniknąć problemów niechcianych
wstrzyknięć obcego kodu w naszej aplikacji, zaczniemy
od analizy tych drugich. Na pewno rozwiązaniem pro-
blemu błędów typu injection nie jest ignorancja. Nie
należy oczekiwać, że nikt nie odkryje problemów z for-
mularzami na trzeciorzędnej stronie naszego portalu,
gdyż są zbyt głęboko ukryte. Narzędzia do automaty-
zacji procesu skanowania aplikacji webowych biorą na
siebie ciężar takich poszukiwań.
Rozwiązaniem naszych problemów nie będzie prawie
nigdy blacklisting niechcianych wartości parametryzacji
zapytań. Rozpatrzmy następujący przykład: W naszym
zapytaniu do bazy select content from data where
id = obawiamy się wstrzyknięcia 21 UNION ALL
SELECT (...). Tworzymy więc wyrażenie regularne,
które sprawdzi, czy elementem naszego parametru nie
jest czasem wartość UNION. Jeżeli tak, aplikacja zwróci
błąd. Złośliwy użytkownik będzie mógł obejść nasz filtr
przekazując zamiast UNION wartość UNIO/**/N (dla bazy
danych MySQL).
Problem z blacklistingiem jest taki, że rzadko kiedy de-
weloper zna wszystkie sztuczki możliwe do wykonania
ze składnią zapytań do bazy danych. Stąd zabezpiecze-
nia tego rodzaju dają najczęściej tylko złudne poczucie
bezpieczeństwa.
Co pomaga?
Możliwe rozwiązania problemów wstrzyknięć obcego
kodu do aplikacji można podzielić na trzy kategorie:
• wykorzystanie wbudowanych mechanizmów szablo-
nów
• escaping
• whitelisting
Wykorzystanie wbudowanych mechanizmów szablonów -
(na przykładzie sterownika JDBC dla Javy) popraw-
ne wykorzystanie szablonów PreparedStatement nie
tylko zwiększa wydajność bazy danych (plan zapyta-
nia musi być policzony tylko raz dla szablonu), lecz
również rozwiązuje problem SQL injection. W po-
niższym przykładzie wartość zmiennej id nie jest w
stanie przeformułować treści zapytania do bazy:
String sql = „SELECT firstname, lastname, age FROM
Employees where id=?”;
PreparedStatement statement = connection.
prepareStatement(sql);
Statement.setString(1, id);
ResultSet rs = statement.executeQuery(sql);
Escaping - czyli unieszkodliwienie wszystkich znaków
modyfikujących zapytanie, które przychodzą spoza
systemu, poprzez wykorzystanie odpowiedniej biblio-
teki (np. ESAPI). Często spotykanym przykładem jest
escapowanie wszystkich znaków będących częścią ko-
du HTML przy dynamicznym generowaniu stron in-
ternetowych w aplikacji webowej.
Whitelisting - w przeciwieństwie do blacklistingu, gdzie
szukaliśmy wzorców, które należało wykluczyć jako nie-
bezpieczne, przy whitelistingu sprawdzamy, czy przekaza-
ne wartości parametrów spełniają określone wyrażenie
do którego bezpieczeństwa jesteśmy pewni, a odrzucamy
wszystkie wartości, które wyrażenia nie spełniają.
Tabela 1. SQL-Injection Hall of Shame Lista wybranych błędów SQL Injection, które doprowadziły
do kompromitacji systemów. Pełna lista dostępna na stronie http://codecurmudgeon.com/wp/sql-
injection-hall-of-shame/
Firma/Instytucja Data Utracono
Nasa 2013-11 Dane ponad 100.000 użytkowników
FBI 2012-12 Ponad 1,6 mln adresów e-mail i haseł użytkowników
Yahoo 2012-07 450.000 niezaszyfrowanych haseł
LinkedIn 2012-06 6,5 mln haszy haseł
Sony Pictures 2011-06 Dane ponad 1 mln użytkowników
www.sdjournal.pl 15
INJECTION
Dodatkowe zabezpieczenia
Przestrzeganie powyższych wskazówek daje stupro-
centową ochronę przed błędami typu injection. Nadal
jednak istnieć będzie ryzyko, że - wskutek błędnej im-
plementacji - luki tego typu zaistnieją w rozwijanym sys-
temie. Ochronę naszej aplikacji powinniśmy dlatego roz-
wijać wielowarstwowo (tzw. defense in depth), a naszym
celem powinno być z jednej strony minimalizowanie ry-
zyka wystąpienia, a z drugiej - minimalizowanie strat w
przypadku zaistnienia błędu. Co więc możemy zrobić,
aby się lepiej zabezpieczyć?
Edukacja - gdyby istniało szybkie i sprawne rozwiąza-
nie problemów bezpieczeństwa, już dawno byśmy je
stosowali. Droga do bezpiecznego systemu jest jednak
bardziej wyboista. Po pierwsze nie możemy nigdy zapo-
mnieć o tych, którzy te systemy tworzą. Brak doświad-
czenia oraz szybkie tempo realizacji projektów wpływa-
ją negatywnie na bezpieczeństwo aplikacji. Okresowe
szkolenia poszerzają wiedzę, przypominają o prioryte-
tach i redukują ryzyko wystąpienia błędów.
Architektura - dobra architektura może reduko-
wać straty powstałe w przypadku zaistnienia błę-
dów. Często praktykowanym na poziomie bazy da-
nych podejściem jest oddzielenie zadań administracji
bazą od wykorzystywania bazy danych na potrzeby
aplikacji. Cel taki można osiągnąć tworząc osobnego
użytkownika (resource user) posiadającego upraw-
nienia do zasobów takich jak tabele, widoki, pro-
cedury, linki do innych baz danych, itd. Użytkownik
ten udostępnia poprzez granty wybrane widoki użyt-
kownikowi (connect user), przez którego realizowane
są połączenia z aplikacji. Efektem takiej architektury
jest ograniczenie uprawnień connect usera do nie-
zbędnego minimum i uniemożliwia wykonanie opera-
cji na zasobach administracyjnych bazy danych z jego
poziomu.
Kryptografia - dobrze wiedzieć, że istnieje również po-
za stosem protokołów TLS i że można ją wykorzystać
do ukrycia wrażliwych danych w systemie. W przypadku
przechowywania haseł użytkowników dobrze pamiętać
o jednokierunkowych funkcjach skrótu (PBKDF2, bcrypt,
scrypt, koniecznie z unikalną solą, a najlepiej solą i pie-
przem), a w przypadku numerów kart kredytowych - o
szyfrowaniu symetrycznym w którym klucz szyfrowania
przechowywany będzie poza bazą danych.
Obsługa błędów - jeżeli podczas działania aplikacji wy-
stąpi błąd krytyczny, serwerowi na środowisku produk-
cyjnym wolno odpowiadać jedynie błędami HTTP (np.
500 - błąd serwera). Pokazywanie listingu stosu jest co
prawda bardzo przydatne dla dewelopera, jednocześnie
zdradza jednak szczegóły implementacyjne naszego sys-
temu, co niepotrzebnie ułatwia sprawę osobie atakującej
naszą aplikację.
Monitoring - częste analizowanie stanu naszego syste-
mu na środowisku produkcyjnym jest wskazane. Pro-
ces ten można zautomatyzować integrując aplikację z
systemem IDS/IPS, który odpowiednio szybko będzie w
stanie zabić na alarm w momencie wykrycia ataku.
Procesy - najlepsze systemy IDS/IPS niewiele dadzą,
jeżeli ich działanie nie będzie przez nikogo monitoro-
wane. Brak błędów bezpieczeństwa w danej wersji sys-
temu nie gwarantuje, że nie pojawią się one w kolej-
nej. Bezpieczeństwo to nie stan, to proces, stąd istotną
rzeczą jest przedsięwzięcie odpowiednich kroków (qu-
ality gates) celem ciągłego dbania o bezpieczeństwo ja-
ko element jakości systemu. Dobrym pomysłem jest na
przykład audyt krytycznych fragmentów kodu przepro-
wadzany każdorazowo przez doświadczonych pracow-
ników w ramach budowania kolejnej wersji systemu,
lub uwzględnienie testów penetracyjnych w fazie testo-
wania aplikacji.
Marek Puchalski
Od ponad 7 lat specyfikuje, imple-
mentuje i wdraża systemy informa-
tyczne dla największych graczy
niemieckiego rynku motoryzacyj-
nego. Pracownik firmy Capgemi-
ni, tata siedemnastomiesięcznego
Janka, gracz Go, fascynat wszel-
kiej tematyki związanej z bezpie-
czeństwem oprogramowania i aplikacji webo-
wych.
Wrocławskie Capgemini Software Solutions Center, jako centrum rozwoju oprogramowania i usług in-
formatycznych firmy Capgemini, specjalizuje się w rozwiązaniach IT dla znanych na całym świecie klien-
tów. Centrum to współpracuje ściśle z niemieckimi oddziałami Capgemini.
Obecnie Software Solutions Center zatrudnia niemal 600 specjalistów branży IT, którzy zajmują się
głównie:
· projektowaniem, tworzeniem, testowaniem oraz wdrażaniem indywidualnych rozwiązań IT w
oparciu o nowoczesne technologie programistyczne, takie jak Java, .NET, platformy bazodanowe (np.
Oracle), Business Intelligence oraz technologie mobilne (np. Android, iOS, Windows Phone);
· projektowaniem, tworzeniem i wdrażaniem nowych rozwiązań informatycznych z obszaru SAP;
· architekturą złożonych systemów IT - projektowanie zaawansowanych rozwiązań klasy enterprise
oraz biznesowy konsulting dla wielu branż;
· zarządzaniem projektami informatycznymi w oparciu o metodyki zwinne i klasyczne - stosowa-
ne są metodyki stworzone na bazie międzynarodowych trendów (takich jak Scrum, RUP) wzbogacone
o nasze doświadczenia; w zarządzaniu projektami opieramy się również na klasycznych standardach i
dobrych praktykach (np. PMBOK, ITIL);
· wsparciem funkcjonowania systemów indywidualnych oraz SAP – wsparcie w zakresie monito-
ringu systemów SAP; rozwiązywanie incydentów, problemów oraz realizacja zmian w oparciu o ITIL;
wspieranie procesów w poszczególnych obszarach SAP; rozbudowywanie systemu SAP o dodatkowe
funkcjonalności;
1/201418
Z
e względu na wspomniany ruch wokół wła-
snej osi, punkt południa przesuwa się z pręd-
kością 1 stopnia na 4 minuty. Gdyby każdy
człowiek na Ziemi chciał mieć na zegarku godzinę
12:00 (z dokładnością co do minuty) wtedy gdy słoń-
ce jest u niego nad horyzontem w zenicie, to mieliby-
śmy 1440 stref czasowych - tyle jest minut w czasie
doby. Z oczywistych względów jest to niemożliwe, i
w latach 1878 – 1884 zaproponowano i wprowadzo-
no podział na 24 strefy czasowe oddzielone od siebie
o 1h (z drobnymi wyjątkami). Ustalono że strefą „0”
będzie czas dla południka „0” przebiegającym przez
obecną dzielnicę Greenwich w Londynie i nazywa się
go zamiennie: UT - Czas uniwersalny (ang. Univer-
sal Time), GMT (ang. Greenwich Mean Time) lub UTC
- uniwersalny czas koordynowany (ang. Coordinated
Universal Time).
Ze względów praktycznych strefy czasowe zosta-
ły wyznaczone wg granic oceanów i poszczególnych
państw tak, aby mniejsze z nich znalazły się w obrębie
jednej strefy czasowej. Większe są położone w kilku
strefach, np. Rosja (12), USA (7), Kanada (6), chociaż
np. Chiny tylko w jednej.
Żeby sprawa była jeszcze bardziej skomplikowana, to
część krajów ma jeszcze tzw. czas letni, czyli okresowo
wprowadzaną zmianę w czasie o 1h. Zostało to wpro-
wadzone ze względów oszczędnościowych, żeby lepiej
wykorzystać światło dzienne. Zmiany czasu letniego są
organizacyjnie kłopotliwe i wiele krajów już się z niego
wycofało.
Strefy czasowe
w Javie i bazach danych
Jak pamiętamy ze szkoły podstawowej, Ziemia obraca się wokół
własnej osi, co powoduje występowanie po sobie pory dnia i pory
nocy. Raz na dobę ustawienie promieni słonecznych jest w zenicie
(najwyższym punkcie względem horyzontu), co potocznie nazywamy
„południem”.
Rysunek. Strefy czasowe na świecie, źródło: Wikipedia.
www.sdjournal.pl 19
STREFY CZASOWE W JAVIE I BAZACH DANYCH
lub zmienną środowiskową TZ=Europe/Warsaw. Możemy
też ustawić lub zmienić aktualną strefę metodą setDefault:
TimeZone.setDefault(TimeZone.getTimeZone(″Australia/
Darwin″)).
Listę dostępnych identyfikatorów stref czasowych oraz
ich nazwy można wyświetlić w prosty sposób:
for(String tzId : TimeZone.getAvailableIDs()) {
TimeZone tz = TimeZone.getTimeZone(tzId);
System.out.println(String.format(″%35s -> %s″,
tzId, tz.getDisplayName()));
}
Dalej musimy już pamiętać, że uruchomiony pro-
gram ma przypisaną strefę czasową, z której korzy-
stają standardowe klasy, takie jak SimpleDateFormat i
Calendar.
Przykład: jeżeli w naszym programie przy pomocy
SimpleDateFormat wyświetlimy czas „0” to otrzymamy:
System.out.println(″Strefa czasowa:
" + TimeZone.getDefault().getID());
System.out.println(″Czas ′0’: ″ + new
SimpleDateFormat().format(new Date(0)));
-----------------------------------------------------
Strefa czasowa: Europe/Warsaw
Czas ‚0’: 01.01.70 01:00
Otrzymaliśmy godzinę pierwszą („01”) a nie, jak
można by się spodziewać, północ.
Wynika to z tego, że metoda klasy SimpleDateFormat
wykonała za nas przeliczenie do domyślnej stre-
fy czasowej. Podobnie sytuacja wygląda z klasą
Calendar:
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(0);
System.out.println(″Godzina czasu ′0′:
″ + calendar.get(Calendar.HOUR_OF_DAY));
-----------------------------------------------------
Godzina czasu ′0′: 1
Wykonując operacje na czasie należy pamiętać, aby
posługiwać się reprezentacją obiektową, np. klasą
Calendar, a nie bezpośrednio reprezentacją milisekun-
dową. Wykonanie poniższego kodu ilustruje problem
Kiedy musimy martwić się strefami
czasowymi
W świecie „pozainformatycznym” z koniecznością prze-
stawiania zegarków mamy do czynienia gdy podróżujemy,
np. na niedaleką Ukrainę czy Litwę. Z kolei w świecie „in-
formatycznym” musimy zainteresować się kwestią stref
czasowych wtedy, gdy tworzymy aplikacje z których ko-
rzystają klienci w więcej niż jednej strefie czasowej.
W dalszej części tego artykułu opiszę sposób rozwią-
zania tego problemu na realnym przykładzie.
Jak działają strefy czasowe w systemie
operacyjnym
Współczesne systemy operacyjne, np. Linux, posiadają
dane pozwalające na prezentowanie czasu zegara syste-
mowego w strefie czasowej wybranej przez użytkowni-
ka, najczęściej w czasie instalacji systemu operacyjnego,
automatycznie uwzględniając w tym czas letni.
$ date
pią, 29 lis 2013, 16:09:42 CET
„CET” oznacza czas środkowoeuropejski (ang. Central
European Time). Ponadto możemy wyświetlić aktualny
czas w innej strefie czasowej:
$ TZ=″America/Chicago″ date
pią, 29 lis 2013, 09:10:09 CST
„CST” oznacza czas centralny (ang. Central Standard
Time). Możemy również poprosić o wyświetlenie za-
danej daty i w odpowiedzi otrzymamy informacje o
obowiązującym wtedy czasie:
$ date --date ″2013-09-01 00:00:00″
nie, 1 wrz 2013, 00:00:00 CEST
„CEST” oznacza czas środkowoeuropejski letni (ang.
Central European Summer Time).
System operacyjny potrafi zwrócić też czas w mili-
sekundach od 1970.1.1 00:00:00 UTC co dalej będzie
istotne:
$ date +%s%3N
1385738304763
Jak działają strefy czasowe w Javie
W javie metoda System.currentTimeMillis() zwraca
czas w milisekundach od 1970.1.1 00:00:00 UTC, iden-
tycznie jak ostatnia komenda date +%s%3N systemu ope-
racyjnego Linux.
Uruchomiony program w JVM automatycznie otrzy-
muje domyślną strefę czasową z systemu operacyjnego,
np. „Europe/Warsaw”. Możemy ją zmienić przez argument
przekazywany do JVM -Duser.timezone=Europe/Warsaw
Tip
Trzeba pamiętać, że na liście dostępnych stref cza-
sowych są także takie z identyfikatorami trzylitero-
wymi, ale istnieją one tylko dla zapewnienia zgod-
ności wstecz i nie powinny być używane.
1/201420
jaki może się pojawić gdy korzystamy z reprezentacji
milisekundowej.
Timestamp ts = Timestamp.valueOf(″2013-10-26
12:00:00″);
System.out.println(″Data początkowa: ″ + ts);
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(ts.getTime());
cal.add(Calendar.DAY_OF_MONTH, 1);
System.out.println(″Dzień później (calendar): ″ + new Timestamp(cal.getTimeInMillis()));
System.out.println(″Dzień później (millis): ″ + new Timestamp(ts.getTime() + 24 * 60 * 60 *
1000));
-----------------------------------------------------
Data początkowa: 2013-10-26 12:00:00.0
Dzień później (calendar): 2013-10-27 12:00:00.0
Dzień później (millis): 2013-10-27 11:00:00.0
Na tym przykładzie widać że „jeden dzień później” to
nie zawsze to samo co „24 godziny później”, ponieważ
w nocy z 26 na 27 października 2013 jest zmiana czasu
letniego na zimowy i godziny 12:00 obydwu tych dni są
oddalone od siebie o 13 a nie 12 godzin. Metody klasy
Calendar skutecznie obsługują takie przypadki brzegowe.
Strefy czasowe w bazach danych
Każda baza danych potrafi przechowywać dane typu
„data z czasem”, potocznie zwane timestamp’em. Więk-
szość baz danych ma też różne typy pól „daty z cza-
sem” w zależności od tego czy mają one obsługiwać
strefy czasowe czy nie.
Pola bez obsługi stref czasowych działają jak zwykłe
napisy, tzn. jeżeli wstawimy do bazy danych wartość
„1970-01-02 01:00:00” to niezależnie od tego w jakiej
strefie czasowej będzie działać klient bazy danych to za-
wsze otrzymamy tą samą wartość.
Pola z obsługą stref czasowych działają w oparciu o in-
formacje w jakiej strefie czasowej działa program klienc-
ki podłączony to bazy danych, terminal konsolowy lub
aplikacja Java. Gdy klient jest połączony w strefie cza-
sowej „Europe/Warsaw” (UTC+1) i zapisze do bazy
danych datę „1970-01-02 01:00:00” a następnie zmieni
strefę czasową na „America/Chicago” (UTC-6) to od-
czyta tą samą datę ale, dla nowo wybranej strefy czaso-
wej, czyli „1970-01-01 18:00:00”. W tym przypadku nie
ma znaczenia w jakiej strefie czasowej działa silnik bazy
danych ani system operacyjny na którym jest zainstalo-
wany.
Przyjrzyjmy się szczegółowo jak to wygląda w konkret-
nych bazach danych.
MySQL
MySQL ma dwa typy pól pozwalających na przechowy-
wanie daty z czasem:
• datetime – nie interpretuje strefy czasowej.
• timestamp – przy zapisie czas jest konwertowany do
UTC i tak trzymany w bazie, a następnie przy wy-
świetleniu konwertowany do aktualnej strefy czaso-
wej klienta.
Poniższy przykład ilustruje różnice w działaniu obydwu
typów danych:
mysql> set time_zone = ″+01:00″;
mysql> insert into field_type_test (datetime_field,
timestamp_field) values (″1970-
01-02 01:00:00″, ″1970-01-02
01:00:00″);
mysql> select * from field_type_test;
+---------------------+---------------------+
| datetime_field | timestamp_field |
+---------------------+---------------------+
| 1970-01-02 01:00:00 | 1970-01-02 01:00:00 |
+---------------------+---------------------+
mysql> set time_zone = ″-06:00″;
mysql> select * from field_type_test;
+---------------------+---------------------+
| datetime_field | timestamp_field |
+---------------------+---------------------+
| 1970-01-02 01:00:00 | 1970-01-01 18:00:00 |
+---------------------+---------------------+
Podejmując decyzje co do wyboru typu pola na datę z
czasem trzeba też zwrócić uwagę na możliwy do prze-
chowywania zakres dat:
• datetime – od „1000-01-01 00:00:00” do „9999-12-
31 23:59:59”
• timestamp – od „1970-01-01 00:00:01” UTC do
„2038-01-19 03:14:07” UTC
I jeszcze jedno związane z tym ostrzeżenie: przy pró-
bie wstawienia daty spoza dopuszczalnego zakresu przez
JDBC otrzymamy wyjątek „MysqlDataTruncation: Data
truncation: Incorrect datetime value”. Jednak przy
próbie wstawienia niepoprawnej wartości z linii komend
polecenie się powiedzie, ale zamiast spodziewanej war-
tości zostanie tam umieszczona wartość „0000-00-00
00:00:00” oznaczająca „błąd”. Na wyjściu konsoli zoba-
czymy obok standardowego „Query OK, 1 row affected”
dodatkowy napis „1 warning”, który łatwo pominąć.
PostgreSQL
Podobnie jak MySQL, także PostgreSQL ma dwa typy
pól na datę z czasem o analogicznych funkcjach i zasto-
sowaniu tylko o innych nazwach:
• timestamp without time zone – nie interpretuje
strefy czasowej
www.sdjournal.pl 21
STREFY CZASOWE W JAVIE I BAZACH DANYCH
• timestamp with time zone – interpretuje strefy cza-
sowe
Inne są także zakresy obsługiwanych wartości, ponie-
waż obydwa pola pozwalają na zapisanie wartości od
od „-4713-12-31 23:59:59” UTC do „294276-12-31
23:59:59” UTC, więc zapas jest spory.
Oracle
W Oracle jest najwięcej typów danych służących do re-
prezentowania daty z czasem:
• date – typ uproszczony, z dokładnością tylko do se-
kund, nie interpretuje strefy czasowej.
• timestamp – przechowuje części sekundy z dokład-
nością do 9 miejsc po przecinku, nie interpretuje
strefy czasowej.
• timestamp with time zone – jak timestamp, tylko że
zawiera informacje o strefie czasowej ale nie doko-
nuje żadnych przeliczeń.
• timestamp with local time zone – interpretuje
strefy czasowe, tzn. dokonuje przeliczeń do strefy
czasowej klienta.
DB2
DB2 w wersji Express-C 10.5 oraz wersjach komer-
cyjnych do wersji 9.x posiada tylko jeden typ pola
timestamp, który nie interpretuje strefy czasowej. W
dokumentacji DB2 jest zapowiedź nowego typu pola
timestamp with time zone od wersji 10.5 dla systemu z/
OS, ale nie miałem możliwości jej przetestowania.
Porównanie zachowania pól timestamp w bazie
Załóżmy, że łączymy się klientem konsolowym w strefie
czasowej Europe/Warsaw (UTC+1) i wpisujemy do ko-
lumn każdego typu datę 1970-01-02 01:00:00. Następnie
rozłączamy się, łączymy ponownie, ale w strefie Ameri-
ca/Chicago (UTC-6) i odczytujemy wynik.
Zachowanie oczekiwane jest takie, że w typach z
przeliczaniem stref czasowych otrzymamy godzinę
18:00 a bez strefy 1:00 (tak jak zapisaliśmy). Tabela 1
prezentuje typy pól w poszczególnych bazach i otrzy-
many wynik.
Jak widzimy, wszystkie operacje wykonywane przez
terminal są zgodne z oczekiwaniami.
Obsługa stref czasowych przez JDBC
Do tego momentu wszystko powinno być zrozumiałe i
dosyć spójne, niestety przez różne implementacje ste-
rowników JDBC, dostęp z kodu aplikacji Java do tak za-
pisanych danych nieco się komplikuje.
Zacznijmy od wyjaśnienia działania klasy java.sql.Time-
stamp do reprezentowania daty z czasem pobranej z ba-
zy. Załóżmy że w bazie danych mamy zapisaną datę „0”
bez sprecyzowania strefy czasowej, np.:
Table ″public.timestamp_test″
Column | Type | Modifiers
--------+-----------------------------+-----------
id | integer | not null
test | timestamp without time zone |
select * from timestamp_test
-----------------------------------------------------
1 1970-01-01 00:00:00.0
Po jej pobraniu przez JDBC metodą ResultSet.
getTimestamp(...) do obiektu Timestamp intuicyjnie wy-
dawałoby się że przechowuje on datę ″0″. I tak właśnie
wyświetli się ona po wywołaniu metody getString.
Jednakże gdy sprawdzimy „wewnętrzną” reprezenta-
cję przy pomocy getTime() to nie otrzymamy spodzie-
wanego ″0″:
Timestamp timestamp = rs.getTimestamp(1);
System.out.println(″Timestamp ′0’ toString():
″ + timestamp);
System.out.println(″Timestamp ′0’ time:
″ + timestamp.getTime());
System.out.println(″Timestamp ′0’ sformatowany:
″ + DateFormat.getDateTimeInstance(
DateFormat.FULL, DateFormat.FULL).
format(timestamp));
-----------------------------------------------------
Timestamp ′0’ toString(): 1970-01-01 00:00:00.0
Timestamp ′0’ time: -3600000
Tabela 1.
Timestamp bez strefy czasowej Timestamp ze strefą czasową Inne
MySQL datetime
1970-01-02 01:00:00
timestamp
1970-01-01 18:00:00
PostgreSQL timestamp without time zone
1970-01-02 01:00:00
timestamp with time zone
1970-01-01 18:00:00-06
Oracle timestamp
1970-01-02 01:00:00.000000
timestamp with local time zone
1970-01-01 18:00:00.000000
date
1970-01-02
01:00:00
timestamp with time zone
1970-01-02
01:00:00.000000 +01:00
DB2 timestamp
1970-01-02-01.00.00
-
1/201422
Timestamp ′0’ sformatowany: czwartek, 1 styczeń 1970
00:00:00 CET
Co więc się stało? Otóż sterownik JDBC (w tym
przypadku do PostgreSQL) po pobraniu daty z bazy
„cofnął” nam ją wg aktualnej strefy czasowej, tak aby
przy „wyświetleniu” z powrotem reprezentowała do-
kładnie tą samą wartość co w bazie.
Wracając do kwestii komunikacji z poszczególnymi ba-
zami danych: jeżeli chcemy skorzystać z zalet wykorzy-
stania pól przeliczających strefy czasowe to klient Java
musi poinformować bazę danych w jakiej strefie czaso-
wej działa – podobnie jak w przypadku testu z bezpo-
średnim dostępem do bazy. Niestety w przypadku każ-
dej bazy danych działa to nieco inaczej.
MySQL
Korzystając z MySQL wystarczy że użyjemy pole-
cenia TimeZone.setDefault(...) i data z pola typu
″timestamp″ (to obsługujące strefy) będzie przeliczana.
Niestety, to ustawienie skutkuje także przeliczeniem
daty z pola ″datetime″, które nie powinno obsługiwać
przeliczeń.
PostgreSQL
Korzystając z PostgreSQL samo ustawienie domyślnej
strefy nie wystarczy, trzeba posługiwać się metodami
które przy zapisie i odczycie dat z czasami przekazują
dodatkową informacje o strefie czasowej:
PreparedStatement.setTimestamp(columnIndex,
timestamp, calendar)
ResultSet.getTimestamp(columnIndex, calendar)
Oracle
Korzystając z Oracle sytuacja jest najbardziej skompli-
kowana:
• Należy ustawić domyślną strefę czasową polece-
niem TimeZone.setDefault(...)
• Nie wolno korzystać z metod, które przy zapisie i
odczycie dat z czasami przekazują dodatkową infor-
macje o strefie czasowej (inaczej niż w PostgreSQL)
• Należy skorzystać z dodatkowej metody specyficz-
nej dla Oracle ustawiającej strefę czasową bezpo-
średnio na obiekcie Connection:
OracleConnection.setSessionTimeZone(timeZoneName);
DB2
W DB nie ma rozwiązań specyficznych, bo nie ma typu
pola w bazie, które by wspierało przeliczanie stref cza-
sowych.
Porównanie zachowania pól timestamp w
aplikacji Java
Tabela 2 porównuje wyniki testu analogicznego do wy-
konanego poprzednio bezpośrednio na bazie, tylko teraz
przy pomocy aplikacji Java. Wpisaliśmy do kolumn każ-
dego typu datę 1970-01-02 01:00:00 w strefie czasowej
Europe/Warsaw (UTC+1) a następnie odczytujemy wy-
nik z aplikacji Java w strefie America/Chicago (UTC-6) z
zastosowaniem metod opisanych wyżej.
Jak widać, baza danych MySQL w przypadku pobiera-
nia przez JDBC błędnie aplikuje przesunięcie strefy cza-
sowej w odniesieniu do pola typu które nie powinno
mieć takiego przesunięcia. Wiedząc o tym trzeba staran-
nie stosować odpowiednie typy pól i ustawianie domyśl-
nej strefy czasowej w zależności od tego jakiego efektu
potrzebujemy.
Pozostałe bazy danych nie mają tego błędu i przesu-
nięcia są aplikowane tylko na właściwych typach kolumn.
Implementacja systemów globalnych
Załóżmy, że mamy do implementacji globalny system au-
kcyjny, w którym każdy może wystawić przedmiot, np.
Kowalski z Warszawy o godzinie 11:00, i każdy może go
licytować, np. Kowalsky z Chicago. Funkcjonalnie mu-
si to działać tak, że Kowalsky z Chicago widzi że au-
kcja rozpoczęła się o 4:00 rano jego czasu lokalnego.
Opcjonalnie może widzieć, że była to godzina 11:00 cza-
su środkowo europejskiego, żeby nie dziwić się dlaczego
sprzedawca nie śpi po nocach.
Opisane powyżej trudności z korzystaniem przelicza-
nia stref czasowych przy pomocy bazy danych skła-
niają do rozwiązania nie polegającego na przeliczaniu
Tabela 2.
Timestamp bez strefy czasowej Timestamp ze strefą czasową Inne
MySQL Datetime
1970-01-01 18:00:00
Timestamp
1970-01-01 18:00:00
PostgreSQL timestamp without time zone
1970-01-02 01:00
timestamp with time zone
1970-01-01 18:00
Oracle timestamp
1970-01-02 01:00
timestamp with local time zone
1970-01-02 18:00:00
date
1970-01-02
01:00:00
timestamp with time zone
1970-01-02 18:00:00
DB2 timestamp
1970-01-02 01:00:00.0
-
www.sdjournal.pl 23
STREFY CZASOWE W JAVIE I BAZACH DANYCH
stref przez bazę danych. Szczególnie korzystanie w me-
tody TimeZone.setDefault(...) i OracleConnection.
setSessionTimeZone(...) w aplikacjach JEE, czyli wie-
lowątkowych i korzystających z połączeń pobieranych
z puli serwera aplikacyjnego jest proszeniem się o pro-
blemy.
Najprostszym sposobem implementacji jest założenie,
że domyślnie aplikacja działa w czasie GMT i tak zapisuje
dane do bazy.
Żeby użytkownikowi wyświetlić dane w jego stre-
fie czasowej, musimy ją najpierw „zgadnąć”. Określe-
nie to jest nieprzypadkowe, ponieważ nie ma standar-
dowych mechanizmów, które pozwalałyby aplikacji
po stronie serwera ustalić w jakiej strefie czasowej
pracuje przeglądarka WWW klienta, np. poprzez na-
główki protokołu HTTP.
Jednym z rozwiązań jest posłużenie się usługą „IP ad-
dress geocoding”, która na podstawie adresu IP zwraca
dane o położeniu geograficznym i strefie czasowej. Nie-
stety tego typu usługi są płatne. Aby obejść ten problem
można posłużyć się prostym kodem JavaScript:
var myDate = new Date();
myDate.getTimezoneOffset();
Zwraca on informacje o przesunięciu aktual-
nej strefy czasowej systemu operacyjnego klien-
ta względem czasu GMT wyrażony w minutach, np.
dla Polski jest to „-60” w czasie zimowym i „-120”
w czasie letnim. Żeby poprawnie zinterpretować, z
którym czasem mamy aktualnie do czynienia nale-
ży dokonać dodatkowych obliczeń na stworzonych
dwóch dodatkowych datach: jednej w styczniu (zi-
mowy) i jednej w lipcu (letni) i porównanie przesu-
nięć z datą bieżącą.
Ale łatwiej jest skorzystać z małej biblioteki JS pt.
„jsTimezoneDetect” (http://pellepim.bitbucket.org/jstz/),
w której to już zostało zrobione, a ponadto obsłu-
ga konwersji przesunięcia wyrażonego w minutach na
identyfikatory stref czasowych rozpoznawanych przez
Javę.
Niedoskonałość biblioteki polega na tym że jeżeli
dla jednego przesunięcia, np. „+1h” istnieje kilka stref
czasowych, to ona podpowie tylko jedną z nich. Nie
jest to przeszkodą, jeżeli pozwolimy ją użytkownikowi
potwierdzić albo ewentualnie zmienić. Przykład dzia-
łania wygląda tak:
var timezone = jstz.determine();
timezone.name();
-----------------------------------------------------
″Europe/Berlin″
Jak widać na tym przykładzie, biblioteka popraw-
nie zgadła przesuniecie +1h, ale przyporządkowała
mu strefę czasową ″Europe/Berlin″ zamiast ″Europe/
Warsaw″ - trudno. Taką informacje musimy przesłać z
przeglądarki do serwera i zapamiętać w sesji użytkow-
nika.
Gdy już znamy strefę czasową klienta, wszystkie daty,
które chcemy mu wyświetlić musimy przeliczyć z GMT
(tak są zapisane w bazie) na jego czas lokalny:
Calendar gmtCalendar = Calendar.getInstance(TimeZone.
getTimeZone(″GMT″));
PreparedStatement ps = c.prepareStatement(″select
date_gmt from gmt_test″);
ResultSet rs = ps.executeQuery();
if(rs.next()) {
Timestamp timestampGmt = rs.getTimestamp(1,
gmtCalendar);
System.out.println(″Czas GMT: ″ + timestampGmt);
// konwersja do czasu ″Europe/Warsaw″:
TimeZone tz = TimeZone.getTimeZone(″Europe/
Warsaw″);
Calendar calendar = Calendar.getInstance(tz);
calendar.setTimeInMillis(timestampGmt.getTime());
System.out.format(″Czas lokalny: %tc\n″,
calendar);
// konwersja do czasu ″Europe/Chicago″:
tz = TimeZone.getTimeZone(″America/Chicago″);
calendar = Calendar.getInstance(tz);
calendar.setTimeInMillis(timestampGmt.getTime());
System.out.format(″Czas lokalny: %tc\n″,
calendar);
}
-----------------------------------------------------
Czas GMT: 1970-01-02 00:00:00.0
Czas lokalny: Pt sty 02 01:00:00 CET 1970
Czas lokalny: Cz sty 01 18:00:00 CST 1970
Żeby z kolei datę w lokalnej strefie czasowej wprowa-
dzoną przez użytkownika zapisać do bazy danych w
strefie GMT należy:
Calendar gmtCalendar = Calendar.getInstance(TimeZone.
getTimeZone(″GMT″));
PreparedStatement ps = c.prepareStatement(″UPDATE
gmt_test SET date_gmt = ?″);
SimpleDateFormat df = new SimpleDateFormat(″yyyy-
MM-dd HH:mm″);
df.setCalendar(Calendar.getInstance(TimeZone.
getTimeZone(″Europe/Warsaw″)));
Date date = df.parse(″1970-01-02 01:00″); // to czas
wprowadzony przez usera
1/201424
System.out.println(″Czas użytkownika: ″ + date.
toString());
ps.setTimestamp(1, new Timestamp(date.getTime()),
gmtCalendar);
ps.executeUpdate();
-----------------------------------------------------
Czas użytkownika: Fri Jan 02 00:00:00 GMT 1970
Jak widać, zmienna date jest już w domyślnej strefie
GMT.
Ciekawe problemy
Niebanalnym problemem do rozwiązania jest kwestia
ustalania czasu zdarzeń „systemowych”, czyli nie związa-
nych z sesją konkretnego użytkownika. Przykładami ta-
kich zdarzeń mogą być:
• publikacja / archiwizowanie aktualności,
• zakończenie czasu trwania promocji,
• zakończenie terminu opłacenia usługi.
Gdyby uzależnić takie zdarzenia od czasu użytkownika,
to on zmieniając strefę czasową mógłby „gonić promo-
cje”. W systemach globalnych najprostszym sposobem
uniknięcia takich problemów i nieporozumień z klienta-
mi jest wykonywanie zmian statusów takich obiektów
przez crona systemowego działającego w strefie GMT
i wyraźne informowanie swoich użytkowników, że tak
to działa.
Podsumowanie
Jak widać, obsługa stref czasowych w aplikacjach global-
nych nie jest taka prosta jak by można początkowo są-
dzić. Mam nadzieję że niniejszy artykuł wyjaśnił podsta-
wowe kwestie w tym zakresie.
W ramach podsumowania warto jeszcze zaznaczyć że
użyteczność klas związanych z obsługą dat w JDK (Date,
Calendar) jest mało wygodna i powstały biblioteki, które
to nieco ułatwiają, np. Joda-Time (http://www.joda.org/joda-
time/). Także w JDK 8 zapowiadany jest nowy pakiet java.
time, który ma ułatwić operacje na typach daty i czasu.
Marek berkan
Marek Berkan, Zastępca Dyrekto-
ra ds. Realizacji w firmie e-point
S.A. Odpowiada za proces im-
plementacji systemów informa-
tycznych, a następnie za ich dłu-
gofalowy rozwój i utrzymanie. Od
13 lat programuje w J2EE, od 8 lat
pracuje nad dynamicznie rozwija-
jącym się projektem w technologii J2EE, koor-
dynując prace zespołu programistów.
Użyteczne komendy
Oracle:
• Ustawienie TZ klienta: TZ=America/Chicago sqlplus …
• Ustawienie TZ całej bazy: ALTER DATABASE SET TIME _ ZONE = ″-06:00″;
• Sprawdzenie TZ klienta: SELECT SESSIONTIMEZONE FROM DUAL;
• Ładne formatowanie pola typu timestamp:
ALTER SESSION SET nls _ timestamp _ format = ″yyyy-mm-dd hh24:mi:ss″;
• Ładne formatowanie pola typu timestamp ze strefą czasową:
ALTER SESSION SET nls _ timestamp _ tz _ format = ″yyyy-mm-dd hh24:mi:ss.ff tzh:tzm″;
• Ładne formatowanie pola typu date z czasem:
ALTER SESSION SET nls _ date _ format = ″yyyy-mm-dd hh24:mi:ss″;
MySQL:
• Ustawienie TZ klienta: SET TIME ZONE = ″+01:00″;
• Sprawdzenie TZ klienta: SELECT @@global.time _ zone, @@session.time _ zone;
PostgreSQL:
• Ustawienie TZ klienta: SET TIME ZONE ″America/Chicago″;
• Sprawdzenie TZ klienta: SHOW TIMEZONE;
Java SE 8 MARCIN MICHALSKI Java SE 8 jest w środowisku programistów Java najbardziej oczekiwaną wersją platformy od 2004 roku. Otrzymamy nie tylko nowe funkcjonalności i poprawki API, ale wprowadzone zostaną również nowe elementy składni języka Java. Przyjrzyjmy się bliżej nowej wersji Java SE 8! Injection po raz kolejny… Marek Puchalski Open Web Application Security Project (OWASP) jest społecznością skupiającą osoby, instytucje i organizacje zainteresowane poprawą bezpieczeństwa aplikacji webowych.W ramach realizowanych przez OWASP projektów tworzone są narzędzia, dokumenty i procesy wspomagające pracę przy tworzeniu systemów. Dobrze o tym pamiętać, aby w różnych sytuacjach nie wynajdywać koła na nowo, tym bardziej że oferowane zasoby wspomagają pracę nie tylko deweloperów i architektów, lecz również testerów i menadżerów projektów. Strefy czasowe w Javie i bazach danych? Piotr Wirkus Jak pamiętamy ze szkoły podstawowej, Ziemia obraca się wokół własnej osi, co powoduje występowanie po sobie pory dnia i pory nocy. Raz na dobę ustawienie promieni słonecznych jest w zenicie (najwyższym punkcie względem horyzontu), co potocznie nazywamy „południem”. Wehikuł czasu w komputerze, czyli o systemach kontroli wersji na przykładzie Subversion Sławomir Andrzejewski Artykuł przybliża Czytelnikowi koncepcje i strategie ozwoju oprogramowania przy wykorzystaniu systemów kontroli wersji. Oprócz zagadnień teoretycznych zaprezentowano podstawowe scenariusze pracy z Subversion. Sposób na cyberprzestępców czyli jak się wyszkolić z bezpieczeństwa informatycznego Kamil Brzeziński Najsłabszym ogniwem bezpieczeństwa jest paradoksalnie człowiek. Nawet najbardziej zaawansowany system informatycznych zabezpieczeń nie sprawdzi się w walce z hakerami, jeśli jego użytkownik sam nie zadba o poufność przetwarzanych danych. 18 12 4 Software Developer’s Journal jest wydawany przez NPRACA Sp. z o.o. Redakcja: sdjpl@sdjournal.pl Kierownik produkcji: Andrzej Kuca Adres korespondencyjny: NPRACA Spółka z o.o. ul. 3 Maja 46, 72-200 Nowogard, Polska Oddział w Warszawie: ul. Postępu 17 D, 02-676 Warszawa, Polska www.sdjournal.pl Facebook: https://www.facebook.com/SoftwareDevelopersJournal Redakcja dokłada wszelkich starań, by publikowane w piśmie informacje i programy były poprawne, jednakże nie bierze odpowiedzialności za efekty wykorzystania ich; nie gwarantuje także poprawnego działania programów shareware, freeware i public domain. Wszystkie znaki firmowe zawarte w piśmie są własności odpowiednich firm. Zostały użyte wyłącznie w celach informacyjnych. Osoby zainteresowane wspópracą prosimy o kontakt: sdjpl@sdjournal.pl Skład i łamanie / projekt okładki: Digital Concept www.digitalconcept.pl admin@digitalconcept.pl 26 38 REKLAMAJeżeli jesteś zainteresowany/a reklamą swoich produktów w naszym magazynie Jeżeli jesteś zainteresowany/a wypromowaniem swojej marki wśród specjalistów IT Jeżeli szukasz klientów lub pracowników Zareklamuj się u nas! Posiadamy bazę 90 tysięcy specjalistów z różnych działów IT! W celu uzyskania oferty reklamowej skontaktuj się z nami: sdjpl@sdjournal.pl tel. 799 46 34 76 www.sdjournal.pl
1/20144 W 2004 roku światło dzienne ujrzała Java SE 5. Otrzymaliśmy tak bardzo oczekiwane ty- py generyczne, enumeracje, pętlę foreach, autoboxing, możliwość definiowania metod ze zmien- ną liczbą parametrów... Śmiało można powiedzieć, że mieliśmy do czynienia z rewolucją w programowaniu w języku Java, jakości kodu oraz jakości tworzonych przez nas aplikacji. Następne rewizje przynosiły raczej kosmetyczne zmiany - poprawki w API, bugfixy, imple- mentacje nowych funkcji języka dyskutowanych w ra- mach Java Community Process, jednak sama składnia ję- zyka nie zmieniła się prawie wcale. W 2010 roku pojawiły się pierwsze oficjalne doku- menty opisujące szczegóły wprowadzenia domknięć (closure) dla języka Java w ramach Lambda Project – dłu- go oczekiwanych przez programistów. Po rezygnacji z wprowadzenia domknięc w siódmej wersji platformy Java, Java SE 8 nareszcie wprowadza Project Lambda pod strzechy! Project Lambda Project Lambda powstał w celu rozwiązania „problemu wertykalnego” w Java. Zarówno Java API jak i wiele bi- bliotek i frameworków Java wykorzystuje technikę wy- wołań zwrotnych realizowanych poprzez interfejsy za- wierające tylko jedną metodę (SAMType – Single Abstract MethodType). Implementacja takiego interfejsu często realizowana jest poprzez anonimowe klasy wewnętrzne: button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { long when = e.getWhen(); Date whenDate = new Date(when); System.out.println(″Action prformmed at:″ + whenDate.toString()); } }); Przykład 1 Implementacja właściwej logiki wymaga dodatkowego, powtarzalnego kodu niepotrzebnie wydłużającego kod proste wywołania metod. Wykorzystując nowe wyrażenia lambda powyższa im- plementacja wyglądać będzie następująco: button.addActionListener(event -> { long when = event.getWhen(); Date whenDate = new Date(when); System.out.println(″Action performmed at:″ + whenDate.toString()); }); Przykład 2 Stosując wyrażenia lambda możemy pozbyć się nad- miarowego kodu i skupić na implementacji logiki sys- temu. Kod staje się zdecydowanie bardziej czytelny. Przyjrzyjmy się szczegółom implementacji i zobaczmy jak to działa! Wyrażenia lambda są ściśle związane z interfejsami funkcyjnymi – nowym pojęciem wprowadzonym w Ja- va SE 8. Interfejs funkcyjny jest to interfejs definiują- cy dokładnie jedną metodę abstrakcyjną. Przykładem Java SE 8 Java SE 8 jest w środowisku programistów Java najbardziej oczekiwaną wersją platformy od 2004 roku. Otrzymamy nie tylko nowe funkcjonalności i poprawki API, ale wprowadzone zostaną również nowe elementy składni języka Java. Przyjrzyjmy się bliżej nowej wersji Java SE 8! Dowiesz się Czym są wyrażenia lambda, poznasz nowe ele- menty języka Java oraz nowe możliwości wielowąt- kowego przetwarzania danych. Powinieneś wiedzieć Czym jest dziedziczenie i typy generyczne. Powi- nieneś posiadać praktyczną wiedzę z zakresu ko- lekcji w języku Java oraz znać zagadnienie prze- twarzania wielowątkowego.
www.sdjournal.pl 5 JAVA SE 8 (String a, String b) -> {System.out.println(a+b)}; (String a, String b) -> {return a+b}; Pierwsze wyrażenie przyjmuje 3 argumenty typu String i zwraca ich konkatenację. Drugie wyrażenie nie przyjmuje żadnych argumentów i zwraca typ int Trzecie wyrażenie przyjmuje 2 obiekty typu String i wyświetla ich złożenie w konsoli. Ostanie wyrażenie przyjmuje 2 obiekty typu String i zwraca ich konkatenację. Warto zauważyć, że w składni wyrażenia lambda nie deklarujemy typu interfejsu funkcyjnego, a skoro język Java jest językiem silnie typowanym, powstaje pytanie jaki typ reprezentuje dane wyrażenie. Otóż typ wyra- żenia wnioskowany jest z kontekstu. Oczywiście to sa- mo wyrażenie lambda może być reprezentowane przez różne typy, przykład Callable callable = c -> ″Kilroy was here!″;
PrivilegedAction action = c -> ″Kilroy was
here!″;
Kompilator sprawdza czy wyrażenie lambda może być
reprezentowane przez dany typ następująco:
• typ musi być interfejsem funkcyjnym
• wyrażenie lambda posiada tę samą ilość parame-
trów co metoda interfejsu funkcyjnego i są one tego
samego typu
• typ zwracany przez wyrażenie lambda jest tego sa-
mego typu co typ zwracany przez metode interfejsu
funkcyjnego
• wyjątki rzucane w ciele wyrażenia lambda zdeklaro-
wane są w metodzie interfejsu funkcyjnego
Zatem jeśli chcemy uprościć naszą implementację i wy-
korzystywać wyrażenia lambda – najpierw musimy po-
siadać interfejs funkcyjny.
Niestety nie mamy możliwości zdefiniowania gene-
rycznego typu, który przechowywałby po prostu refe-
rencję do funkcji.
Kolejną konsekwejcją jest to, że wyrażenia lambda
możemy deklarować tylko w przypadkach gdy istnieje
odpowiedni kontekst dla określenia typu interfejsu funk-
cyjnego, czyli w następujących sytuacjach:
• deklaracje zmiennych
• przypisanie wartości
• operacje return
• inicjalizacje tablic
• argumenty metod i konstruktorów
• w ciele wyrażeń lambda
• operator warunkowy ? :
• rzutowanie
takiego interfejsu jest wykorzystany w przykładzie 1
java.awt.event.ActionListener. Przy czym słowo de-
finiujący nie oznacza, że interfejs nie może posiadać
większej ilości metod. Interfejs może definiować do-
wolną ilość metod, jednak pozostałe metody abstrak-
cyjne muszą być nadpisywane przez klasę java.lang.
Object.
Mówiąc prościej – interfejs funkcyjny może posiadać
tylko jedną metodę, która nie została zadeklarowana
w klasie java.lang.Object
@FunctionalInterface
public interface Comparator { int compare(T o1, T o2); boolean equals(Object obj);
}
Przykład 3
Kompilator automatycznie rozpoznaje interfejsy funk-
cyjne, jednak dla celów informacyjnych została wpro-
wadzona adnotacja @FunctionalInterface
Java SE 8 wprowadza nowy pakiet java.util.function
zawierający podstawowy zestaw interfejsów funkcyjnych
reprezentujących podstawowe funkcje:
• Predicate – testowanie własności obiektu – dla
obiektu zwracana jest wartość boolean
• Consumer - wykonanie akcji na obiekcie bez zwra-
cania wartości
• Function - funkcja zwracająca obiekt typu R na
podstawie obiektu T
• Supplier - konstruktor, fabryka obiektów typu T
• UnaryOperator - funkcja zwracająca obiekt typu T
– obpowiednik Function• BinaryOperator - funkcja zwracająca obiekt typu
T, przyjmująca dwa argumenty typu T
Dodatkowo dostępne są specjalizacje powyższych in-
terfejsów, np. specjalizacje interfejsów dla typów pry-
mitywnych int, long oraz double – pozostałe typy moż-
na uzyskać poprzez rzutowanie.
Zobaczmy jak teraz zbudowana jest deklaracja wyraże-
nia lambda. Składa się z następujących elementów:
1. Deklaracja argumentów wejściowych metody inter-
fejsu funkcyjnego oddzielonych przecinkami
2. Operatora ->3. Ciała wyrażenia zawierającego jedną komendę lub
blok kodu zawartego w nawiasach klamrowych { }.
Zastosowania wyrażeń prezentują poniższe przykłady
(String a, String b, String c) -> ″Hello ″ + a + ″ ″
+ b + ″ ″ + c;
() -> 13;
1/20146 Na początku wspomnieliśmy o tym, że wyraże- nia lambda pozwalają zastąpić anonimowe klasy we- wnętrzne. Warto zatem przypomnieć problem za- kresu zmiennych i przesłaniania metod w klasach we- wnętrznych, który często trapił programistów. Wyra- żenia lambda nie są reprezentowane przez klasy we- wnętrzne, nie dziedziczą żadnych metod czy zmien- nych z typów nadrzędnych. Dostępna przestrzeń nazw definiuje tylko moment deklaracji. Rzućmy okiem na następujący przykład: public class LambdaTest { Runnable printThis = () -> { System.out. println(this); }; Runnable printToString = () -> { System.out. println(toString()); }; Runnable printInnerClass = new Runnable() { @Override public void run() { System.out.println(toString()); } }; public String toString() { return «Kilroy was here!»; } public static void main(String... args) { new LambdaTest().printThis.run(); new LambdaTest().printToString.run(); new LambdaTest().printInnerClass.run(); } } Dzięki deklaracji wyrażeń lambda wewnątrz obiek- tu LambdaTest, staje się on przestrzenią nazw dla wy- rażeń lambda. Po uruchomieniu aplikacji wywoła- nie printThis oraz printToString wyświetli na kon- soli tekst ”Kilroy was hare!”, natomiast wywołanie printInnerClass wyświetli nam coś na kształt Lambda- Test$1@17c264. Skoro ograniczna nas tylko moment deklaracji – co ze zmiennymi lokalnymi? Do tej pory dla klas wewnętrznych dostęp ograniczony był do finalnych zmiennych lokalnych. Java SE 8 wprowadza nowe po- jęcie effective final local variable. Zmienną możemy określić jako effective final, jeśli jej wartość nie zmie- nia się od momentu deklaracji. W Java SE 8 dostęp do takich zmiennych lokalnych możliwy jest zarówno z poziomu wyrażeń lambda jak i anonimowych klas we- wnętrznych! Powyższy przykład pokazuje, że przejście z klas we- wnętrznych na wyrażenia lambda może doprowadzić do trudnych do wykrycia błędów. Jeśli nasz kod nie jest odpowiednio obłożony testami, warto o to zadbać przed zmianą implementacji – unikniemy nieprzyjem- nych konsekwencji. Podsumowując - wyrażenia lambda oszczędzają czas programisty dając możliwość zastąpienia anonimo- wych klas wewnętrznych. I na taką rewolucję przyszło nam czekać 10 lat? Na pewno zmiany w składni języka upraszczają implementację, na pewno kod aplikacji bę- dzie bardziej czytelny, jednak musimy nauczyć się korzy- stać z wyrażeń lambda i konsekwentnie wykorzystywać dostępne nowe interfejsy funkcyjne. Efekty przyjdą z cza- sem... Jednak to nie wszystko - zobaczmy jakie inne zmia- ny zostały wprowadzone w Java API i jak współgrają z nową filozofią wyrażeń lambda. Metody domyślne Rzućmy okiem na implementację interfejsu Iterablepublic interface Iterable { Iterator iterator(); default void forEach(Consumer super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
}
Wzrok nas nie myli – interfejs zawiera implementację
metody forEach. Stało się to możliwe, dzięki wprowa-
dzeniu nowego elementu języka – metodami domyśl-
nymi - default methods, określanymi również jako virtual
extension methods lub defender methods.
Jak zauważyliśmy wcześniej samo wprowadzenie
wyrażeń lambda nie przyniesie rewolucji w sposobie
programowania w języku Java. Konieczne jest umoż-
liwienie wykorzystania nowych możliwości w istnieją-
cych już elementach języka. Jednak zmiana istniejących
interfejsów pociągnęłaby za sobą konieczność zmia-
ny implementacji, co praktycznie uniemożliwiałoby
przejście ze starszych wersji języka. Metody domyślne
umożliwiają rozszerzenie interfejsów bez konieczno-
ści zmian klas implementujących je implementacji. A
jakie daje nam to możliwości? Zamiast dobrze znanej
pętli foreach
for (String string : list) { System.out.println(string);
}
Mamy możliwość wykorzystania wyrażeń lambda
list.forEach(c -> System.out.println(c));
Przeglądając implementację innych interfejsów – np.
interfejs java.util.Comparator nasuwa się wniosek.
www.sdjournal.pl 7 JAVA SE 8 Zgodnie z określeniem defender method, metody do- myślne na daną chwilę największe zastosowanie znaj- dą przy refaktoryzacji pod kątem wyrażeń lamb- da i świetnie się w tym celu sprawdzają. W przypad- ku tworzenia nowej implementacji – bardziej przydat- ne będzie przemyślane i konsekwentne wykorzystanie interfejsów funkcyjnych. Interfejsy w języku Java rekompensowały brak dzie- dziczenia wielokrotnego – rekompensowały, ponieważ do tej pory interfejsy nie mogły zawierać implementa- cji metod. Czyżby Java SE 8 wprowadzała wsparcie dla wielokrotnego dziedziczenia? Nie do końca – dziedzi- czenie metod domyślnych odbywa się na tych samych zasadach co zwykłych metod, jednak z pewnymi wyjąt- kami: • Jeśli klasa dziedziczy więcej niż jeden interfejs defi- niujący metodę default o tym samym deskryptorze, występuje błąd kompilacji • Kolejność w jakiej interfejsy pojawiają się w deklara- cji implements lub extends nie mają wpływu na me- chanizm dziedziczenia • Metody zdefiniowane w klasach przesłaniają meto- dy domyślne – nawet jeśli są to metody abstrakcyj- ne, bez względu na kolejność w hierarchii dziedzi- czenia • Przeładowana metoda domyślna jest pomijana – tracimy do niej dostęp Przyjrzyjmy się następującemu przykładowi public class Mammal { public void speak() { System.out.println(″I can’t speak″); } } public interface Human { default void speak() { System.out.println(″I am a human″); } } public interface Male { default void speak() { System.out.println(″I am a male″); } } public class Person extends Mammal implements Human, Male { } Zgodnie z zasadami dziedziczenia dla klas domyślnych wywołanie metody speak() dla obiektu Person spo- woduje wyświetlenie I can’t speak, ponieważ zwykłe metody przesłaniają metody domyślne. Jeśli klasa Person nie będzie rozszerzać klasy Mammal, tylko implementować interfejsy Human i Male, otrzymamy błąd kompilacji. Czyli dziedziczenie wie- lokrotne nie jest możliwe... jednak w takiej sytuacji istnieje możliwość wyboru, z której klasy metodę chcemy dziedziczyć, poprzez przeładowanie meto- dy speak() public class Person implements Human, Male { public void speak() { Male.super.speak(); } } Zatem Java nadal stricte nie wspiera dziedziczenia wie- lokrotnego. Jednak tak długo jak deskryptory metod domyślnych będą różne, możemy zamiast rozszerzać mozolnie kolejne klasy, zaimplementować kilka interfej- sów z metodami domyślnymi – przy czym będzie to ra- czej zła praktyka. public interface Human { default void speak() { System.out.println(″I am a human″); } } public interface Male { default void sing() { System.out.println(″I am singing″); } } public class Person implements Human, Male { } Referencjonowanie metod Kolejną nowością w składni języka Java jest możliwość stworzenia referencji do metody. Tworzenie referencji do metody odbywa się na takich samych zasadach jak tworzenie wyrażeń lambda – referencja do metody to instancja interfejsu funkcyjnego. Rzućmy okiem na inter- fejs Comparator.
Możemy przygotować instancję klasy comparator za
pomocą wyrażeń lambda.
Comparator comp = (a ,b) -> a.getName().
compareTo(b.getName());
Aby jeszcze uprościć deklarację, wykorzystajmy meto-
dę domyślną Copmarator.comparing
Comparator comp = Comparator.comparing(p ->p.getName());
Wykorzystujące referencje do metody deklaracja wy-
glądać będzie następująco
1/20148 Comparator comp = Comparator.
comparing(Person::getName());
Jak widać różnica nie jest duża, jednak kod staje się
bardziej czytelny – widzimy, że do porównania wyko-
rzystujemy metodę getName z klasy Person.
Możemy tworzyć referencje do następujących metod:
• metod statyczne - String::valueOf
• metod instancji obiektu - person::getName
• metod instancji obiektu określonego typu -
Person::getName
• konstruktorów – Person::new
Parametry metody interfejsu statycznego stają się pa-
rametrami metod referencjonowanych, dzięki czemu
możemy je dowolnie rzutować i korzystać z varargs-
ów, np.:
Comparator comp = Comparator.
comparing(Human::getAge());
Runnable r = Example::main - void main(String...
args)
Consumer cons = Male::speak;
Referencjonowanie metod ułatwia wykorzystanie inst-
niejących metod o deskryptorze identycznym z inter-
fejsem funkcyjnym.
public class PersonHelper { public static int getAgeDifference(Person a,
Person b) { return a.getAge() - b.getAge(); }
}
Comparator byAge =
PersonHelper::getAgeDifference;
Comparator byAgeLambda = (a, b) ->PersonHelper.getAgeDifference(a,
b);
W takich przypadkach referencje metod są zdecydo-
wanie bardziej czytelne niż wyrażenia lambda.
Miejmy nadzieję, że nowy element języka przyczyni się
do poprawienia jakości kodu aplikacji – do tej pory na-
zbyt często prosty kod lądował bezpośrednio w ciele
anonimowych klas wewnętrznych i powtarzany był w
różnych miejscach w aplikacji. Referencje do metod, ja-
ko nowy element języka Java zachęca do zaprzestania
tej złej praktyki.
Kolekcje w Java SE 8
Opisując metody domyślne wprowadzone w interfej-
sach w Java SE 8 wspomniałem, że ich głównym zasto-
sowaniem jest rozszerzenie istenijących interfejsów i
dostosowanie ich implementacji pod kątem wyrażeń
lambda.
Implementacja kolekcji przeszła największą metamor-
fozę za sprawą wprowadzenia wyrażeń lambda i metod
domyślnych.
Stream API
Do tej pory najwygodniejszym sposobem przetwa-
rzania elementów kolekcji w Java były obiekty Itera-
tor oraz pętla foreach – wykorzystująca zresztą ite-
ratory.
Z założenia takie przetwarzanie było sekwencyjne –
elementy pobierane były jeden po drugim i przetwarza-
ne.
Strumienie (Stream) wprowadzają nowy sposób ite-
racji z wykorzystaniem operacji agregujących (aggre-
gate operations). Ciekawostką jest to, że definiując ta-
kie operacje nie wykorzystujemy metody next() do
której tak bardzo przyzwyczaiły nas iteratory. Przyj-
rzyjmy się przykładowi:
List list = new ArrayList<>();
listOfPeople.stream()
.filter(p -> p.getAge() > 18)
.filter(p -> p.isUnemployed())
.forEach(p -> storePersonalData());
Powyższy kod z dostępnej listy obiektów typu Person
wykona operację storePersonalData dla każdej bezro-
botnej osoby powyżej 18 roku życia.
Zauważmy, że korzystając ze strumieni i operacji
agregujących nie definiujemy w jaki sposób mamy po-
bierać kolejne elementy, ale jakie operacje mają zo-
stać na nich wykonane. Wykorzystujemy do tego celu
wyrażenia lambda i referencje do metod. Sposób ite-
racji omówimy jednak za chwilę
Zwróćmy uwagę na to, że cała logika została za-
mknięta w jednej „linii” kodu, dzięki wprowadzeniu
możliwości łączenia operacji agregujących w poto-
ki (pipelines) – na wzór potoków znanych z systemu
UNIX.
Potok składa się z następujących elementów:
• źródło – może nim być kolekcja, tablica, generator
obiektów (np. obiekt Iterator), kanał wejścia/wyjścia
(IO channel) – np. zwracany przez metodę Files.
lines(Path file)
• operacje pośrednie (intermediate operations)– opera-
cje zwracające strumienie, np. operacja filter
• operacja kończąca (terminal operation) – opera-
cja kończąca potok, zwracająca obiekt nie będą-
cy strumieniem – np. kolekcję, lub przetwarzające
elementy strumienia bez zwracania żadnej warto-
ści – np. operacja forEach
www.sdjournal.pl 9 JAVA SE 8 Dostępne są następujące operacje pośrednie: • filter(Predicate super T> predicate) – opera- cja zwracająca strumień będący podzbiorem speł- niającym określone kryterium, np.. filter(p ->p.getAge() > 18) • map(Function super T,? extends R> mapper) - operacja zwracająca strumień skonwertowanych obiektów, np. map(p -> Unemployed::new) • flatMap(Function> mapper)
- operacja mapująca relację jeden do wiele na stru-
mień. Wyobraźmy sobie, że obiekt Person posiada
metodę zwracającą kolekcję obiektów typu Child,
np. Collection getChildren(). Możemy za-
tem zwrócić strumień dal takiej kolekcji. W efekcie
uzyskamy strumień wszystkich dzieci wejściowych
obiektów Person, np. flatMap(p -> p.getChildren().
stream())
• sorted() - operacja zwracająca strumień posorto-
wany za pomocą istniejącej metody compareTo
• sorted(Comparator super T> comparator) - ope-
racja zwracająca strumień posortowany za pomocą
określonego obiektu comparator
• distinct() - operacja zwracająca strumień unikal-
nych obiektów. Do wykrywania duplikatów wyko-
rzystywana jest metoda equals
Metody pośrednie mogą być dowlonie łączone, np.
listOfPeople.stream()
.filter(p -> p.getAge() > 18)
.filter(p -> p.isUnemployed())
.distinct()
.sorted()
Co również bardzo istotne, metody pośredie są meto-
dami lazy – będą wywołane dopiero w momencie wy-
wołania metody końcowej. Możemy zatem stworzyć
potok bez metody końcowej i uzyskany strumień prze-
tworzyć w późniejszym czasie.
Dostępne są następujące operacje końcowe:
• void forEach(Consumer super T> consumer) -
operacja przetwarza elementy strumienia bez
zwracania żadnej wartości, np. forEach(p ->storePersonalData());
• R collect(Collector super T,R> collector) –
operacja przetwarzająca strumień i zwracająca po-
jedynczą wartość lub kolekcję obiektów. Możemy
zwrócić listę posortowanych obiektów Person
listOfPeople.stream()
.filter(p -> p.getAge() > 18)
.filter(p -> p.isUnemployed())
.distinct()
.sorted()
.collect(Collectors.toList())
• Optional reduce(T identity, BinaryOperatoraccumulator) - operacja przetwarzająca strumień i
zwracająca pojedynczą wartość. Możemy np. poli-
czyć sumę wszystkich miesięcznych świadczeń osób
bezrobotnych:
listOfPeople.stream() .filter(p -> p.getAge() > 18) .filter(p -> isUnemployed()) .mapToInt(p -> p.getMonthlyGrants()) .reduce(0, (a,b) -> a + b)
• Iterator iterator() - operacja zwraca iterator
obiektów pozostałych po przetworzeniu potoku
Zarówno dla operacji collect jak i reduce istnie-
ją gotowe implementacji ciekawych funkcji. Dla
operacji reduce, w zależności od typu strumienia,
mamy dostępne, np. dla Strean opera-
cje arytmetyczne takie jak suma czy średnia. Ope-
racje collect zawarte są w klasie Collectors. Do
ciekawszych należą np. funkcje grouppingBy czy
partitioning – zachęcam do lektury javadoc.
Zobaczmy teraz w jaki sposóþ pobierane są ele-
menty kolekcji. Zamiast jawnej iteracji (external ite-
ration), mamy do czynienia z wewnętrzną iteracją
(internal iteration) z wykorzystaniem obiektów Spli-
terator.
Obiekt spliterator – podobnie jak Iterator se-
kwekncyjnie przetwarza elementy kolekcji, ale rów-
nocześnie umożliwia podział elementów kolekcji na
mniejsze porcje za pomocą metody trySplit().
Efektem takiej operacji jest stworzenie podrzęd-
nej instancji Spliterator, przy czym główny Spliterator
pominie elementy przetwarzane przez obiekty pod-
rzędne.
Oczywiście możliwy jest dalszy podział podrzęd-
nych instancji Spliterator, tak aby uzyskać odpowied-
nio małe porcje i przetwarzać je równolegle.
Czyli przetwarzanie sekwencyjne się nie zmienia –
Spliterator zastąpił Iterator. Natomiast przetwarzanie
równoległe z podziałem Spliteratorów brzmi skom-
plikowanie – pula obiektów spliterator, pula wątków
lub ForkJoinPool, przydział obiektów do przetwo-
rzenia, zebranie rezultatów cząstkowych... Nic po-
dobnego! Z Java SE 8 równoległe przetwarzanie
otrzymujemy za darmo!
listOfPeople.parallelStream()
.filter(p -> p.getAge() > 18)
.filter(p -> p.isUnemployed())
.forEach(p -> storePersonalData());
1/201410 Każdy strumień może być przetworzony równole- gle, dzięki czemu w końcu w prosty sposób będzie- my mogli wykorzystać potencjał wielordzeniowych maszyn. Jeśli prawo Moorea ma się obronić – rdze- ni będzie coraz więcej, więc to bardzo dobra wia- domość. Jednak jeśli przetwarzanie nie wprowadzało prawie żadnych ograniczeń, tak w strumieniach rów- noległych (parallel stream) obowiązują już pewne re- guły: • strumień musi być statyczny – kolekcja nie mo- że być modyfikowana (elementy kolekcji nie mogą być dodawane, usuwane, przemieszczane) podczas przetwarzania. Operacje pośrednie również nie modyfikować kolekcji • po zakończeniu przetwarzania strumień nie może być ponownie wykorzystany • przetwarzanie strumieni z zachowaniem kolej- ności wymaga użycia specjalnej operacji końco- wej forEachOrdered gwarantującej przetworzenie rezultatu dokładnie w kolejności w której znaj- dował się na wejściu strumienia. W przypad- ku użycia zwykłej metody forEach podczas prze- twarzania równoległego – kolejność zostanie utracona • konieczność użycia metod collect odpowied- nich do przetwarzania równoległego. Każ- dy obiekt Collector opisany jest przez enume- rację Characteristics. Wymagane jest stoso- wanie obiektów Collector z charakterystyką CONCURRENT. Na pewno zmiany w Java Collections API zdecydo- wanie uproszczą kod naszych aplikacji – zwłaszcza tych wykorzystujących wielowątkowość. Funkcjo- nalności, które do tej pory wymagały implementa- cji kilku klas, będziemy mogli zamknąć nawet w kil- ku linijkach kodu. Jednak zanim zaczniemy wykorzystywać nowe API warto zagłębić się w szczegóły dokumentacji oraz przejrzeć implementację podstawowych obiektów Spliterator – jest duże pole do zwiększenia wydajno- ści naszego kodu. Nie należy ulegać pokusie i wyko- rzystywać równoległego przetwarzania dla wszyst- kich strumieni. Zanim nasz kod trafi na docelowe środowisko, warto przeprowadzić porównawcze te- sty wydajnościowe rozwiązań wielowątkowych i se- kwencyjnych. Powtarzalne adnotacje (Repeating annotations) Jednym z ciekawszych przykładów adnotacji w Java do tej pory była adnotacja @EJBs – jedyną jej rolą było gru- powanie adnotacji @EJB @EJBs({@EJB(name=″name1″,beanInterface=Type1.class), @EJB(name=″name2″,beanInterface=Type2.class)}) Java SE 8 rozwiązuje ostatecznie ten problem przez prowadzenie powtarzalnych adnotacji – możliwe bę- dzie uzycie jednej adnotacji wiele razy w tym samym miejscu. @EJB(name=″name1″,beanInterface=Type1.class) @EJB(name=″name2″,beanInterface=Type2.class) Mała rzecz, a cieszy. Adnotacje typów (Type Annotations) Java 8 SE wprowadza możliwość dodawania adnotacji dla typów, np. @NotNull String text = ″test″; @Readonly List users = ...
public String getFullText(@Readonly Paper
sheetOfPaper)
Celem wprowadzenia tych adnotacji była poprawa ja-
kości kodu przez umożliwienie bardziej szczegółowej
jego kontroli za pomocą narzędzi analizujących kod.
Spójrzmy na przykład:
Deklaracja metody
public class Paper { public String getText(@Readonly Paper
sheetOfPaper) { ...
Kod wywołujący metodę:
@NotNull String text = sheetOfPaper.getText();
Zauważmy, że metoda getText może potencjalnie
zwracać wartość null i przypisać ją do zmiennej zade-
klarowanej jako @NotNull. Oczywiście kompilacja i wy-
konanie programu będzie działać prawidłowo. Możliwe
jest natomiast wychwycenie potencjalnego problemu
przez narzędzia analizujące kod takie jak CheckerFrame-
work, Javarifier czy inne.
Możliwość dodawania adnotacji typów na pewno
sprawdzi się w zaawansowanych projektach z bar-
dzo restrykcyjnymi normami jakościowymi i popar-
tymi dobra analizą. Na pewno wywrą wpływ już na
etapie projektowania klas i interfejsów – koniecz-
ne będzie nie tylko zaplanowanie nazw i struktur
danych, ale także ich zachowania. Na pewno war-
to przyjrzeć się dostępnym rozwiązaniom wspiera-
jącym analizę kodu. Ciekawym testem byłby również
pomiar czasu potrzebnego na analizę średniej wiel-
kości projektu.
www.sdjournal.pl 11 JAVA SE 8 Java Date - Time API Na koniec, wisienka na torcie. Istniejąca wersja Date -Time API oparta na klasie java.util.Calendar odcho- dzi do lamusa. Dostaliśmy dokładnie to o co prosili- śmy od lat. Po pierwsze, Java SE 8 wprowadza pakiet java.time gdzie znjadziemy wszystkie potrzebne klasy. Po dru- gie API jest przejrzyste, model prosty i pozbawiony wprowadzających zamieszanie metod. Pierwszy z gó- ry przykład. LocalDateTime dt = LocalDateTime.now(); Calendar cal = Calendar.getInstance(); Metoda now niesie ze sobą jasny przekaz , a metoda getInstance hmm… singleton? A co ze strefami czasowymi? Spróbujmy obliczyć go- dzinę przylotu gdy poruszamy się po różnych strefach czasowych, gdzie lot trwa 8 godzin: LocalDateTime now = LocalDateTime.now(); ZonedDateTime sydneyTime = dt.atZone(ZoneId. of(″Australia/Sydney″)); ZonedDateTime tokioTime = sydneyZoneTime. withZoneSameInstant(dt. atZone(ZoneId.of(″Asia/Tokyo″))); ZonedDateTime tokioArrivalTime = tokioTime. plusHours(8); Wykorzystując java.util.Calendar sami musielibyśmy obliczać przesunięcie między strefami tworząc dodat- kowe obiekty TimeZone i przekazywać bieżącą datę w postaci wartości long, a wykonując operację przesunię- cia czasu musielibyśmy użyć metody cal.add(Calendar. HOUR, 8). Ijeszczejednadobrainformacja.KlasaSimpleDateFormat również doczekała się następcy i tym razem jedną instan- cję swobodnie możemy używać w wielu wątkach! DateTimeFormatter dtf = DateTimeFormatter. ofPattern(″MM-dd-yyyy HH:mm″); dtf.format(tokioArrivalTime); Geniusz tkwi w prostocie - po wielu latach w końcu doczekaliśmy się biblioteki wspierającej operacje na czasie! Podsumowanie Java SE 8 na pewno jest krokiem milowym w rozwo- ju języka java. Wyrażenia lambda zależne od interfej- sów funkcyjnych na pewno nie dają pełnej elastycz- ności, ale są użyteczne i powinny rozwiązać problem wewnętrznych klas anonimowych. Wprowadzenie metod domyślnych do interfejsów, może nie wyglą- da zbyt elegancko, ale świetnie realizuje swoje cele – czego przykładem są chociażby zmiany w Collec- tions API. Czas pokaże, które z nowości przyjmą się w środowisku Java, a które będą musiały zostać jeszcze dopracowane. Jedno jest pewne – mamy nowe i do- bre narzędzia, musimy nauczyć się z nich korzystać, czekamy na reakcję społeczności i na pewno czeka- my na więcej! MARCIN MICHALSKI Współtwórca i współwłaściciel marki Sagiton. Posiada 7 lat do- świadczenia w programowaniu. Obecnie realizuje się jako Sys- tem Architect w projektach wy- konywanych przez ArrowGroup Sp. z o.o., której jest współzałoży- cielem. Ukończył Informatykę na Wydziale Elektroniki na Politechnice Wrocław- skiej. Odbył kursy i szkolenia organizowane przez: Cisco Network Academy, Sun Microsy- tems, Oracle. Uzyskał certyfikaty eksperckie w zakresie sieci komputerowych, programowa- nia (Java/JEE) oraz baz danych. Jest autorem licznych publikacji naukowych, prowadzi blo- ga technicznego. Marka: www.sagiton.pl [1] Kontakt: marcin.michalski@sagiton.pl Blog: marcin-michalski.pl [2]
1/201412 D ziś chciałbym wspomnieć tylko o jednym - choć pewnie najbardziej znanym - projekcie ze stajni OWASP, mianowicie o OWASP Top 10. Lista OWASP Top 10 jest listą dziesięciu najpopularniejszych błędów bezpieczeństwa znajdowanych w aplikacjach webo- wych. Istnienie listy ma skupić uwagę zespołów programi- stów na te zagrożenia, których pojawienie w projekcie jest najbardziej prawdopodobne, edukować oraz być punktem startowym do korzystania z narzędzi i projektów OWASP. Lista Top 10 aktualizowana jest przeciętnie co trzy lata, a ostatnia odsłona listy miała miejsce w czerwcu tego roku. NajkrótszarecenzjazmiandokonanychnaliścieOWASP Top 10 2013 brzmiała by: “nic nowego”. Wszystkie roz- poznane przez OWASP w roku 2010 zagrożenia pozosta- ją nadal aktualne. Najnowsza odsłona listy bardzo dobrze pokazuje, że w pogoni za wypełnianiem wymagań funk- cjonalnych nadal zdarza nam się zapomnieć o bezpieczeń- stwie tworzonej aplikacji. W epoce automatycznych ska- nerów, przeczesujących internet w poszukiwaniu luk, nie jest to krzepiący wniosek. Listę OWASP Top 10 2013 po raz kolejny otwierają błędy typu injection (z najznamienit- szym ich reprezentantem - SQL injection (SQLi)) i to o nich chciałbym dzisiaj opowiedzieć. Czym są błędy typu injection? Najkrótsza, obrazowa (i nieoficjalna) definicja - błędy typu injection powstają wtedy, gdy oddajemy w ręce użytkow- nika aplikacji możliwość oprogramowania np. naszej bazy danych. Robimy to najczęściej nieświadomie poprzez kon- katenację elementów naszego kodu z tekstem wprowa- dzonym do systemu. Rozpatrzmy następujące przykłady. Przykład 1: Runtime runtime = Runtime.getRuntime(); Process proc = runtime.exec(„find . -name „ + args[0]); Z kodu naszej aplikacji próbujemy uruchomić komen- dę systemową dodatkową parametryzowaną przez użyt- kownika. Czego oczekujemy? Oczekujemy nazwy pliku lub katalogu, który ma zostać odnaleziony. Co może zrobić użytkownik? W zależności od fantazji może na przykład spróbować usunąć całą za- wartość dysku doklejając do polecenia tekst “foo; rm -rf /” i uzyskując przez to: find . -name foo; rm -rf / Nie uruchamiacie serwerów aplikacji z uprawnieniami administratora, prawda? Przykład 2 (ze strony OWASP https://www.owasp.org/in- dex.php/XPATH_Injection): Uwierzytelniamy użytkowników naszej aplikacji na podstawie danych poniższego XMLa:Arnold Baker ABaker SoSecret Admin Peter Pan PPan NotTelling User Injection po raz kolejny…
Open Web Application Security Project (OWASP) jest społecznością
skupiającą osoby, instytucje i organizacje zainteresowane poprawą
bezpieczeństwa aplikacji webowych. W ramach realizowanych
przez OWASP projektów tworzone są narzędzia, dokumenty i
procesy wspomagające pracę przy tworzeniu systemów. Dobrze
o tym pamiętać, aby w różnych sytuacjach nie wynajdywać koła na
nowo, tym bardziej że oferowane zasoby wspomagają pracę nie tylko
deweloperów i architektów, lecz również testerów i menadżerów
projektów.
www.sdjournal.pl 13 INJECTION SQL injection Większość nietrywialnych systemów i aplikacji wymaga dostępu do bazy danych. Wszędzie tam, gdzie wystę- puje relacyjna baza danych, istnieje również zagroże- nie wystąpienia błędów SQL injection. Co może zrobić złośliwy użytkownik mogąc oprogramować naszą bazę? Pojawiają się tutaj trzy zagrożenia: Omijanie kryteriów klauzuli WHERE w zapytaniu SQL Klasyczny przykład zakłada uwierzytelnianie użytkowni- ków w systemie na podstawie wykonania poniższego za- pytania: select * from users where name = ‘’
and password = ‘’;
Niebezpieczna parametryzacja miała by tu postać:
name = ″’ or 1=1 or ′a′=′a″
password = ″anything″
Pozyskiwanie dodatkowych informacji z bazy
Przy założeniu, że wykonywane w systemie zapytanie ma
postać:
select content from data where id = poniższa parametryzacja sprawi, że udostępnimy więcej
informacji niż byśmy tego chcieli:
id = ″21 UNION ALL SELECT password from users″
Przekazywanie dodatkowych poleceń do bazy
I tu kolejny przykład. Mając zapytanie:
select name from record where id = możemy się obawiać wstrzyknięcia parametryzacji ty-
pu:
id = ″1; DROP table record; DROP table address--″
budując parametryzowalne zapytanie XPath:
“//Employee[UserName/text()=‘“ + name+ “’And
Password/text()=‘“ + password +
“‚]”
Następująca parametryzacja pozwala całkowicie omi-
nąć nasze mechanizmy kontroli:
name: blah’ or 1=1 or ‚a’=’a
password: foo
Z powyższych przykładów wynika parę wniosków.
Po pierwsze błędy typu injection nie są jedynie ce-
chą aplikacji webowych i równie dobrze mogą wystę-
pować w aplikacjach desktopowych oraz mobilnych.
To czyni je jeszcze bardziej niebezpiecznymi. Po dru-
gie powodem powstawania błędów jest konkatenacja
elementów naszych zapytań z niefiltrowanymi para-
metrami wprowadzonymi przez użytkownika. To bar-
dzo istotne spostrzeżenie. Powinno uwrażliwić każ-
dego czytającego kod na potencjalne błędy wynikające
z faktu łączenia łańcuchów znaków celem parametry-
zacji zapytań i poleceń.
Czy jest się czego obawiać?
Podstawowe pytania jakie należy sobie zadać myśląc o
bezpieczeństwie budowanego systemu brzmią: Co ma-
my do stracenia?
Co stanowi o wartości naszej aplikacji? Chcemy
chronić nasze algorytmy, czy też chcemy chronić nasze
dane? Co się stanie, jeżeli utracimy całkowicie i jedno
i drugie?
Przedstawione powyżej przykłady są co prawda kry-
tyczne z punktu widzenia potencjalnych zniszczeń lub
innych konsekwencji w naszym systemie, jednak praw-
dopodobieństwo ich wystąpienia jest znikome. (Kto z
nas wywołuje w swojej aplikacji parametryzowalne po-
lecenia systemowe? Kto z nas uwierzytelnia użytkow-
ników na podstawie wpisów w pliku XML?) Co więc
sprawiło, że wielcy tego świata (Nokia, Sony Music,
Yahoo, FBI, NASA, itd.) padli ofiarą błędów typu in-
jection?
OWASP Top 10 2013 (https://www.owasp.org/index.php/Top_10_2013-Top_10)
• Injection
• Broken authentication and session management (Błędy uwierzytelniania i obsługi sesji)
• Cross-Site Scripting (XSS)
• Insecure direct object references (Niezabezpieczone, bezpośrednie linki do obiektów)
• Security misconfiguration (Błędy konfiguracji)
• Sensitive data exposure (Błędy obsługi poufnych danych)
• Missing function level access control (Brak weryfikacji uprawnień dostępu)
• Cross-Site Request Forgery (CSRF)
• Using components with known voulnerabilities (Wykorzystanie komponentów z błędami bezpieczeństwa)
• Unvalidated redirects and forwards (Niewalidowane przekierowania)
1/201414 Poza pomysłowością potencjalnego intruza, chcące- go wykorzystać błędy w implementacji naszego sys- temu, ograniczać będzie wykorzystana przez nas technologia oraz biblioteki. Na przykład ataki ma- jące na celu wykonanie w bazie dodatkowych po- leceń (punkt 3) są możliwe w niektórych frame- workach wykorzystywanych przez PHP, nie są na- tomiast możliwe i wspierane przez np. sterownik JDBC w Javie. Podsumowując powyższe przykłady trzeba jednak zauważyć, że konsekwencje błędów typu injection w aplikacji idą bardzo daleko i umożliwiają pozyskanie i nadpisanie wszystkich danych zawartych w bazie (przy jednoczesnym ominięciu mechanizmów kontroli dostę- pu do tych danych). Konsekwencje utraty informacji na taką skalę mogą doprowadzić niektóre przedsiębior- stwa nawet do bankructwa. Co nie pomaga? Rozwiązania wszelki problemów dzielą się na dwie kate- gorie: na te które działają i na całą resztę. Próbę odpo- wiedzi na pytanie, jak uniknąć problemów niechcianych wstrzyknięć obcego kodu w naszej aplikacji, zaczniemy od analizy tych drugich. Na pewno rozwiązaniem pro- blemu błędów typu injection nie jest ignorancja. Nie należy oczekiwać, że nikt nie odkryje problemów z for- mularzami na trzeciorzędnej stronie naszego portalu, gdyż są zbyt głęboko ukryte. Narzędzia do automaty- zacji procesu skanowania aplikacji webowych biorą na siebie ciężar takich poszukiwań. Rozwiązaniem naszych problemów nie będzie prawie nigdy blacklisting niechcianych wartości parametryzacji zapytań. Rozpatrzmy następujący przykład: W naszym zapytaniu do bazy select content from data where id = obawiamy się wstrzyknięcia 21 UNION ALL
SELECT (...). Tworzymy więc wyrażenie regularne,
które sprawdzi, czy elementem naszego parametru nie
jest czasem wartość UNION. Jeżeli tak, aplikacja zwróci
błąd. Złośliwy użytkownik będzie mógł obejść nasz filtr
przekazując zamiast UNION wartość UNIO/**/N (dla bazy
danych MySQL).
Problem z blacklistingiem jest taki, że rzadko kiedy de-
weloper zna wszystkie sztuczki możliwe do wykonania
ze składnią zapytań do bazy danych. Stąd zabezpiecze-
nia tego rodzaju dają najczęściej tylko złudne poczucie
bezpieczeństwa.
Co pomaga?
Możliwe rozwiązania problemów wstrzyknięć obcego
kodu do aplikacji można podzielić na trzy kategorie:
• wykorzystanie wbudowanych mechanizmów szablo-
nów
• escaping
• whitelisting
Wykorzystanie wbudowanych mechanizmów szablonów -
(na przykładzie sterownika JDBC dla Javy) popraw-
ne wykorzystanie szablonów PreparedStatement nie
tylko zwiększa wydajność bazy danych (plan zapyta-
nia musi być policzony tylko raz dla szablonu), lecz
również rozwiązuje problem SQL injection. W po-
niższym przykładzie wartość zmiennej id nie jest w
stanie przeformułować treści zapytania do bazy:
String sql = „SELECT firstname, lastname, age FROM
Employees where id=?”;
PreparedStatement statement = connection.
prepareStatement(sql);
Statement.setString(1, id);
ResultSet rs = statement.executeQuery(sql);
Escaping - czyli unieszkodliwienie wszystkich znaków
modyfikujących zapytanie, które przychodzą spoza
systemu, poprzez wykorzystanie odpowiedniej biblio-
teki (np. ESAPI). Często spotykanym przykładem jest
escapowanie wszystkich znaków będących częścią ko-
du HTML przy dynamicznym generowaniu stron in-
ternetowych w aplikacji webowej.
Whitelisting - w przeciwieństwie do blacklistingu, gdzie
szukaliśmy wzorców, które należało wykluczyć jako nie-
bezpieczne, przy whitelistingu sprawdzamy, czy przekaza-
ne wartości parametrów spełniają określone wyrażenie
do którego bezpieczeństwa jesteśmy pewni, a odrzucamy
wszystkie wartości, które wyrażenia nie spełniają.
Tabela 1. SQL-Injection Hall of Shame Lista wybranych błędów SQL Injection, które doprowadziły
do kompromitacji systemów. Pełna lista dostępna na stronie http://codecurmudgeon.com/wp/sql-
injection-hall-of-shame/
Firma/Instytucja Data Utracono
Nasa 2013-11 Dane ponad 100.000 użytkowników
FBI 2012-12 Ponad 1,6 mln adresów e-mail i haseł użytkowników
Yahoo 2012-07 450.000 niezaszyfrowanych haseł
LinkedIn 2012-06 6,5 mln haszy haseł
Sony Pictures 2011-06 Dane ponad 1 mln użytkowników
www.sdjournal.pl 15 INJECTION Dodatkowe zabezpieczenia Przestrzeganie powyższych wskazówek daje stupro- centową ochronę przed błędami typu injection. Nadal jednak istnieć będzie ryzyko, że - wskutek błędnej im- plementacji - luki tego typu zaistnieją w rozwijanym sys- temie. Ochronę naszej aplikacji powinniśmy dlatego roz- wijać wielowarstwowo (tzw. defense in depth), a naszym celem powinno być z jednej strony minimalizowanie ry- zyka wystąpienia, a z drugiej - minimalizowanie strat w przypadku zaistnienia błędu. Co więc możemy zrobić, aby się lepiej zabezpieczyć? Edukacja - gdyby istniało szybkie i sprawne rozwiąza- nie problemów bezpieczeństwa, już dawno byśmy je stosowali. Droga do bezpiecznego systemu jest jednak bardziej wyboista. Po pierwsze nie możemy nigdy zapo- mnieć o tych, którzy te systemy tworzą. Brak doświad- czenia oraz szybkie tempo realizacji projektów wpływa- ją negatywnie na bezpieczeństwo aplikacji. Okresowe szkolenia poszerzają wiedzę, przypominają o prioryte- tach i redukują ryzyko wystąpienia błędów. Architektura - dobra architektura może reduko- wać straty powstałe w przypadku zaistnienia błę- dów. Często praktykowanym na poziomie bazy da- nych podejściem jest oddzielenie zadań administracji bazą od wykorzystywania bazy danych na potrzeby aplikacji. Cel taki można osiągnąć tworząc osobnego użytkownika (resource user) posiadającego upraw- nienia do zasobów takich jak tabele, widoki, pro- cedury, linki do innych baz danych, itd. Użytkownik ten udostępnia poprzez granty wybrane widoki użyt- kownikowi (connect user), przez którego realizowane są połączenia z aplikacji. Efektem takiej architektury jest ograniczenie uprawnień connect usera do nie- zbędnego minimum i uniemożliwia wykonanie opera- cji na zasobach administracyjnych bazy danych z jego poziomu. Kryptografia - dobrze wiedzieć, że istnieje również po- za stosem protokołów TLS i że można ją wykorzystać do ukrycia wrażliwych danych w systemie. W przypadku przechowywania haseł użytkowników dobrze pamiętać o jednokierunkowych funkcjach skrótu (PBKDF2, bcrypt, scrypt, koniecznie z unikalną solą, a najlepiej solą i pie- przem), a w przypadku numerów kart kredytowych - o szyfrowaniu symetrycznym w którym klucz szyfrowania przechowywany będzie poza bazą danych. Obsługa błędów - jeżeli podczas działania aplikacji wy- stąpi błąd krytyczny, serwerowi na środowisku produk- cyjnym wolno odpowiadać jedynie błędami HTTP (np. 500 - błąd serwera). Pokazywanie listingu stosu jest co prawda bardzo przydatne dla dewelopera, jednocześnie zdradza jednak szczegóły implementacyjne naszego sys- temu, co niepotrzebnie ułatwia sprawę osobie atakującej naszą aplikację. Monitoring - częste analizowanie stanu naszego syste- mu na środowisku produkcyjnym jest wskazane. Pro- ces ten można zautomatyzować integrując aplikację z systemem IDS/IPS, który odpowiednio szybko będzie w stanie zabić na alarm w momencie wykrycia ataku. Procesy - najlepsze systemy IDS/IPS niewiele dadzą, jeżeli ich działanie nie będzie przez nikogo monitoro- wane. Brak błędów bezpieczeństwa w danej wersji sys- temu nie gwarantuje, że nie pojawią się one w kolej- nej. Bezpieczeństwo to nie stan, to proces, stąd istotną rzeczą jest przedsięwzięcie odpowiednich kroków (qu- ality gates) celem ciągłego dbania o bezpieczeństwo ja- ko element jakości systemu. Dobrym pomysłem jest na przykład audyt krytycznych fragmentów kodu przepro- wadzany każdorazowo przez doświadczonych pracow- ników w ramach budowania kolejnej wersji systemu, lub uwzględnienie testów penetracyjnych w fazie testo- wania aplikacji. Marek Puchalski Od ponad 7 lat specyfikuje, imple- mentuje i wdraża systemy informa- tyczne dla największych graczy niemieckiego rynku motoryzacyj- nego. Pracownik firmy Capgemi- ni, tata siedemnastomiesięcznego Janka, gracz Go, fascynat wszel- kiej tematyki związanej z bezpie- czeństwem oprogramowania i aplikacji webo- wych.
Wrocławskie Capgemini Software Solutions Center, jako centrum rozwoju oprogramowania i usług in- formatycznych firmy Capgemini, specjalizuje się w rozwiązaniach IT dla znanych na całym świecie klien- tów. Centrum to współpracuje ściśle z niemieckimi oddziałami Capgemini. Obecnie Software Solutions Center zatrudnia niemal 600 specjalistów branży IT, którzy zajmują się głównie: · projektowaniem, tworzeniem, testowaniem oraz wdrażaniem indywidualnych rozwiązań IT w oparciu o nowoczesne technologie programistyczne, takie jak Java, .NET, platformy bazodanowe (np. Oracle), Business Intelligence oraz technologie mobilne (np. Android, iOS, Windows Phone); · projektowaniem, tworzeniem i wdrażaniem nowych rozwiązań informatycznych z obszaru SAP; · architekturą złożonych systemów IT - projektowanie zaawansowanych rozwiązań klasy enterprise oraz biznesowy konsulting dla wielu branż; · zarządzaniem projektami informatycznymi w oparciu o metodyki zwinne i klasyczne - stosowa- ne są metodyki stworzone na bazie międzynarodowych trendów (takich jak Scrum, RUP) wzbogacone o nasze doświadczenia; w zarządzaniu projektami opieramy się również na klasycznych standardach i dobrych praktykach (np. PMBOK, ITIL); · wsparciem funkcjonowania systemów indywidualnych oraz SAP – wsparcie w zakresie monito- ringu systemów SAP; rozwiązywanie incydentów, problemów oraz realizacja zmian w oparciu o ITIL; wspieranie procesów w poszczególnych obszarach SAP; rozbudowywanie systemu SAP o dodatkowe funkcjonalności;
1/201418 Z e względu na wspomniany ruch wokół wła- snej osi, punkt południa przesuwa się z pręd- kością 1 stopnia na 4 minuty. Gdyby każdy człowiek na Ziemi chciał mieć na zegarku godzinę 12:00 (z dokładnością co do minuty) wtedy gdy słoń- ce jest u niego nad horyzontem w zenicie, to mieliby- śmy 1440 stref czasowych - tyle jest minut w czasie doby. Z oczywistych względów jest to niemożliwe, i w latach 1878 – 1884 zaproponowano i wprowadzo- no podział na 24 strefy czasowe oddzielone od siebie o 1h (z drobnymi wyjątkami). Ustalono że strefą „0” będzie czas dla południka „0” przebiegającym przez obecną dzielnicę Greenwich w Londynie i nazywa się go zamiennie: UT - Czas uniwersalny (ang. Univer- sal Time), GMT (ang. Greenwich Mean Time) lub UTC - uniwersalny czas koordynowany (ang. Coordinated Universal Time). Ze względów praktycznych strefy czasowe zosta- ły wyznaczone wg granic oceanów i poszczególnych państw tak, aby mniejsze z nich znalazły się w obrębie jednej strefy czasowej. Większe są położone w kilku strefach, np. Rosja (12), USA (7), Kanada (6), chociaż np. Chiny tylko w jednej. Żeby sprawa była jeszcze bardziej skomplikowana, to część krajów ma jeszcze tzw. czas letni, czyli okresowo wprowadzaną zmianę w czasie o 1h. Zostało to wpro- wadzone ze względów oszczędnościowych, żeby lepiej wykorzystać światło dzienne. Zmiany czasu letniego są organizacyjnie kłopotliwe i wiele krajów już się z niego wycofało. Strefy czasowe w Javie i bazach danych Jak pamiętamy ze szkoły podstawowej, Ziemia obraca się wokół własnej osi, co powoduje występowanie po sobie pory dnia i pory nocy. Raz na dobę ustawienie promieni słonecznych jest w zenicie (najwyższym punkcie względem horyzontu), co potocznie nazywamy „południem”. Rysunek. Strefy czasowe na świecie, źródło: Wikipedia.
www.sdjournal.pl 19 STREFY CZASOWE W JAVIE I BAZACH DANYCH lub zmienną środowiskową TZ=Europe/Warsaw. Możemy też ustawić lub zmienić aktualną strefę metodą setDefault: TimeZone.setDefault(TimeZone.getTimeZone(″Australia/ Darwin″)). Listę dostępnych identyfikatorów stref czasowych oraz ich nazwy można wyświetlić w prosty sposób: for(String tzId : TimeZone.getAvailableIDs()) { TimeZone tz = TimeZone.getTimeZone(tzId); System.out.println(String.format(″%35s -> %s″, tzId, tz.getDisplayName())); } Dalej musimy już pamiętać, że uruchomiony pro- gram ma przypisaną strefę czasową, z której korzy- stają standardowe klasy, takie jak SimpleDateFormat i Calendar. Przykład: jeżeli w naszym programie przy pomocy SimpleDateFormat wyświetlimy czas „0” to otrzymamy: System.out.println(″Strefa czasowa: " + TimeZone.getDefault().getID()); System.out.println(″Czas ′0’: ″ + new SimpleDateFormat().format(new Date(0))); ----------------------------------------------------- Strefa czasowa: Europe/Warsaw Czas ‚0’: 01.01.70 01:00 Otrzymaliśmy godzinę pierwszą („01”) a nie, jak można by się spodziewać, północ. Wynika to z tego, że metoda klasy SimpleDateFormat wykonała za nas przeliczenie do domyślnej stre- fy czasowej. Podobnie sytuacja wygląda z klasą Calendar: Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(0); System.out.println(″Godzina czasu ′0′: ″ + calendar.get(Calendar.HOUR_OF_DAY)); ----------------------------------------------------- Godzina czasu ′0′: 1 Wykonując operacje na czasie należy pamiętać, aby posługiwać się reprezentacją obiektową, np. klasą Calendar, a nie bezpośrednio reprezentacją milisekun- dową. Wykonanie poniższego kodu ilustruje problem Kiedy musimy martwić się strefami czasowymi W świecie „pozainformatycznym” z koniecznością prze- stawiania zegarków mamy do czynienia gdy podróżujemy, np. na niedaleką Ukrainę czy Litwę. Z kolei w świecie „in- formatycznym” musimy zainteresować się kwestią stref czasowych wtedy, gdy tworzymy aplikacje z których ko- rzystają klienci w więcej niż jednej strefie czasowej. W dalszej części tego artykułu opiszę sposób rozwią- zania tego problemu na realnym przykładzie. Jak działają strefy czasowe w systemie operacyjnym Współczesne systemy operacyjne, np. Linux, posiadają dane pozwalające na prezentowanie czasu zegara syste- mowego w strefie czasowej wybranej przez użytkowni- ka, najczęściej w czasie instalacji systemu operacyjnego, automatycznie uwzględniając w tym czas letni. $ date pią, 29 lis 2013, 16:09:42 CET „CET” oznacza czas środkowoeuropejski (ang. Central European Time). Ponadto możemy wyświetlić aktualny czas w innej strefie czasowej: $ TZ=″America/Chicago″ date pią, 29 lis 2013, 09:10:09 CST „CST” oznacza czas centralny (ang. Central Standard Time). Możemy również poprosić o wyświetlenie za- danej daty i w odpowiedzi otrzymamy informacje o obowiązującym wtedy czasie: $ date --date ″2013-09-01 00:00:00″ nie, 1 wrz 2013, 00:00:00 CEST „CEST” oznacza czas środkowoeuropejski letni (ang. Central European Summer Time). System operacyjny potrafi zwrócić też czas w mili- sekundach od 1970.1.1 00:00:00 UTC co dalej będzie istotne: $ date +%s%3N 1385738304763 Jak działają strefy czasowe w Javie W javie metoda System.currentTimeMillis() zwraca czas w milisekundach od 1970.1.1 00:00:00 UTC, iden- tycznie jak ostatnia komenda date +%s%3N systemu ope- racyjnego Linux. Uruchomiony program w JVM automatycznie otrzy- muje domyślną strefę czasową z systemu operacyjnego, np. „Europe/Warsaw”. Możemy ją zmienić przez argument przekazywany do JVM -Duser.timezone=Europe/Warsaw Tip Trzeba pamiętać, że na liście dostępnych stref cza- sowych są także takie z identyfikatorami trzylitero- wymi, ale istnieją one tylko dla zapewnienia zgod- ności wstecz i nie powinny być używane.
1/201420 jaki może się pojawić gdy korzystamy z reprezentacji milisekundowej. Timestamp ts = Timestamp.valueOf(″2013-10-26 12:00:00″); System.out.println(″Data początkowa: ″ + ts); Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(ts.getTime()); cal.add(Calendar.DAY_OF_MONTH, 1); System.out.println(″Dzień później (calendar): ″ + new Timestamp(cal.getTimeInMillis())); System.out.println(″Dzień później (millis): ″ + new Timestamp(ts.getTime() + 24 * 60 * 60 * 1000)); ----------------------------------------------------- Data początkowa: 2013-10-26 12:00:00.0 Dzień później (calendar): 2013-10-27 12:00:00.0 Dzień później (millis): 2013-10-27 11:00:00.0 Na tym przykładzie widać że „jeden dzień później” to nie zawsze to samo co „24 godziny później”, ponieważ w nocy z 26 na 27 października 2013 jest zmiana czasu letniego na zimowy i godziny 12:00 obydwu tych dni są oddalone od siebie o 13 a nie 12 godzin. Metody klasy Calendar skutecznie obsługują takie przypadki brzegowe. Strefy czasowe w bazach danych Każda baza danych potrafi przechowywać dane typu „data z czasem”, potocznie zwane timestamp’em. Więk- szość baz danych ma też różne typy pól „daty z cza- sem” w zależności od tego czy mają one obsługiwać strefy czasowe czy nie. Pola bez obsługi stref czasowych działają jak zwykłe napisy, tzn. jeżeli wstawimy do bazy danych wartość „1970-01-02 01:00:00” to niezależnie od tego w jakiej strefie czasowej będzie działać klient bazy danych to za- wsze otrzymamy tą samą wartość. Pola z obsługą stref czasowych działają w oparciu o in- formacje w jakiej strefie czasowej działa program klienc- ki podłączony to bazy danych, terminal konsolowy lub aplikacja Java. Gdy klient jest połączony w strefie cza- sowej „Europe/Warsaw” (UTC+1) i zapisze do bazy danych datę „1970-01-02 01:00:00” a następnie zmieni strefę czasową na „America/Chicago” (UTC-6) to od- czyta tą samą datę ale, dla nowo wybranej strefy czaso- wej, czyli „1970-01-01 18:00:00”. W tym przypadku nie ma znaczenia w jakiej strefie czasowej działa silnik bazy danych ani system operacyjny na którym jest zainstalo- wany. Przyjrzyjmy się szczegółowo jak to wygląda w konkret- nych bazach danych. MySQL MySQL ma dwa typy pól pozwalających na przechowy- wanie daty z czasem: • datetime – nie interpretuje strefy czasowej. • timestamp – przy zapisie czas jest konwertowany do UTC i tak trzymany w bazie, a następnie przy wy- świetleniu konwertowany do aktualnej strefy czaso- wej klienta. Poniższy przykład ilustruje różnice w działaniu obydwu typów danych: mysql> set time_zone = ″+01:00″; mysql> insert into field_type_test (datetime_field, timestamp_field) values (″1970- 01-02 01:00:00″, ″1970-01-02 01:00:00″); mysql> select * from field_type_test; +---------------------+---------------------+ | datetime_field | timestamp_field | +---------------------+---------------------+ | 1970-01-02 01:00:00 | 1970-01-02 01:00:00 | +---------------------+---------------------+ mysql> set time_zone = ″-06:00″; mysql> select * from field_type_test; +---------------------+---------------------+ | datetime_field | timestamp_field | +---------------------+---------------------+ | 1970-01-02 01:00:00 | 1970-01-01 18:00:00 | +---------------------+---------------------+ Podejmując decyzje co do wyboru typu pola na datę z czasem trzeba też zwrócić uwagę na możliwy do prze- chowywania zakres dat: • datetime – od „1000-01-01 00:00:00” do „9999-12- 31 23:59:59” • timestamp – od „1970-01-01 00:00:01” UTC do „2038-01-19 03:14:07” UTC I jeszcze jedno związane z tym ostrzeżenie: przy pró- bie wstawienia daty spoza dopuszczalnego zakresu przez JDBC otrzymamy wyjątek „MysqlDataTruncation: Data truncation: Incorrect datetime value”. Jednak przy próbie wstawienia niepoprawnej wartości z linii komend polecenie się powiedzie, ale zamiast spodziewanej war- tości zostanie tam umieszczona wartość „0000-00-00 00:00:00” oznaczająca „błąd”. Na wyjściu konsoli zoba- czymy obok standardowego „Query OK, 1 row affected” dodatkowy napis „1 warning”, który łatwo pominąć. PostgreSQL Podobnie jak MySQL, także PostgreSQL ma dwa typy pól na datę z czasem o analogicznych funkcjach i zasto- sowaniu tylko o innych nazwach: • timestamp without time zone – nie interpretuje strefy czasowej
www.sdjournal.pl 21 STREFY CZASOWE W JAVIE I BAZACH DANYCH • timestamp with time zone – interpretuje strefy cza- sowe Inne są także zakresy obsługiwanych wartości, ponie- waż obydwa pola pozwalają na zapisanie wartości od od „-4713-12-31 23:59:59” UTC do „294276-12-31 23:59:59” UTC, więc zapas jest spory. Oracle W Oracle jest najwięcej typów danych służących do re- prezentowania daty z czasem: • date – typ uproszczony, z dokładnością tylko do se- kund, nie interpretuje strefy czasowej. • timestamp – przechowuje części sekundy z dokład- nością do 9 miejsc po przecinku, nie interpretuje strefy czasowej. • timestamp with time zone – jak timestamp, tylko że zawiera informacje o strefie czasowej ale nie doko- nuje żadnych przeliczeń. • timestamp with local time zone – interpretuje strefy czasowe, tzn. dokonuje przeliczeń do strefy czasowej klienta. DB2 DB2 w wersji Express-C 10.5 oraz wersjach komer- cyjnych do wersji 9.x posiada tylko jeden typ pola timestamp, który nie interpretuje strefy czasowej. W dokumentacji DB2 jest zapowiedź nowego typu pola timestamp with time zone od wersji 10.5 dla systemu z/ OS, ale nie miałem możliwości jej przetestowania. Porównanie zachowania pól timestamp w bazie Załóżmy, że łączymy się klientem konsolowym w strefie czasowej Europe/Warsaw (UTC+1) i wpisujemy do ko- lumn każdego typu datę 1970-01-02 01:00:00. Następnie rozłączamy się, łączymy ponownie, ale w strefie Ameri- ca/Chicago (UTC-6) i odczytujemy wynik. Zachowanie oczekiwane jest takie, że w typach z przeliczaniem stref czasowych otrzymamy godzinę 18:00 a bez strefy 1:00 (tak jak zapisaliśmy). Tabela 1 prezentuje typy pól w poszczególnych bazach i otrzy- many wynik. Jak widzimy, wszystkie operacje wykonywane przez terminal są zgodne z oczekiwaniami. Obsługa stref czasowych przez JDBC Do tego momentu wszystko powinno być zrozumiałe i dosyć spójne, niestety przez różne implementacje ste- rowników JDBC, dostęp z kodu aplikacji Java do tak za- pisanych danych nieco się komplikuje. Zacznijmy od wyjaśnienia działania klasy java.sql.Time- stamp do reprezentowania daty z czasem pobranej z ba- zy. Załóżmy że w bazie danych mamy zapisaną datę „0” bez sprecyzowania strefy czasowej, np.: Table ″public.timestamp_test″ Column | Type | Modifiers --------+-----------------------------+----------- id | integer | not null test | timestamp without time zone | select * from timestamp_test ----------------------------------------------------- 1 1970-01-01 00:00:00.0 Po jej pobraniu przez JDBC metodą ResultSet. getTimestamp(...) do obiektu Timestamp intuicyjnie wy- dawałoby się że przechowuje on datę ″0″. I tak właśnie wyświetli się ona po wywołaniu metody getString. Jednakże gdy sprawdzimy „wewnętrzną” reprezenta- cję przy pomocy getTime() to nie otrzymamy spodzie- wanego ″0″: Timestamp timestamp = rs.getTimestamp(1); System.out.println(″Timestamp ′0’ toString(): ″ + timestamp); System.out.println(″Timestamp ′0’ time: ″ + timestamp.getTime()); System.out.println(″Timestamp ′0’ sformatowany: ″ + DateFormat.getDateTimeInstance( DateFormat.FULL, DateFormat.FULL). format(timestamp)); ----------------------------------------------------- Timestamp ′0’ toString(): 1970-01-01 00:00:00.0 Timestamp ′0’ time: -3600000 Tabela 1. Timestamp bez strefy czasowej Timestamp ze strefą czasową Inne MySQL datetime 1970-01-02 01:00:00 timestamp 1970-01-01 18:00:00 PostgreSQL timestamp without time zone 1970-01-02 01:00:00 timestamp with time zone 1970-01-01 18:00:00-06 Oracle timestamp 1970-01-02 01:00:00.000000 timestamp with local time zone 1970-01-01 18:00:00.000000 date 1970-01-02 01:00:00 timestamp with time zone 1970-01-02 01:00:00.000000 +01:00 DB2 timestamp 1970-01-02-01.00.00 -
1/201422 Timestamp ′0’ sformatowany: czwartek, 1 styczeń 1970 00:00:00 CET Co więc się stało? Otóż sterownik JDBC (w tym przypadku do PostgreSQL) po pobraniu daty z bazy „cofnął” nam ją wg aktualnej strefy czasowej, tak aby przy „wyświetleniu” z powrotem reprezentowała do- kładnie tą samą wartość co w bazie. Wracając do kwestii komunikacji z poszczególnymi ba- zami danych: jeżeli chcemy skorzystać z zalet wykorzy- stania pól przeliczających strefy czasowe to klient Java musi poinformować bazę danych w jakiej strefie czaso- wej działa – podobnie jak w przypadku testu z bezpo- średnim dostępem do bazy. Niestety w przypadku każ- dej bazy danych działa to nieco inaczej. MySQL Korzystając z MySQL wystarczy że użyjemy pole- cenia TimeZone.setDefault(...) i data z pola typu ″timestamp″ (to obsługujące strefy) będzie przeliczana. Niestety, to ustawienie skutkuje także przeliczeniem daty z pola ″datetime″, które nie powinno obsługiwać przeliczeń. PostgreSQL Korzystając z PostgreSQL samo ustawienie domyślnej strefy nie wystarczy, trzeba posługiwać się metodami które przy zapisie i odczycie dat z czasami przekazują dodatkową informacje o strefie czasowej: PreparedStatement.setTimestamp(columnIndex, timestamp, calendar) ResultSet.getTimestamp(columnIndex, calendar) Oracle Korzystając z Oracle sytuacja jest najbardziej skompli- kowana: • Należy ustawić domyślną strefę czasową polece- niem TimeZone.setDefault(...) • Nie wolno korzystać z metod, które przy zapisie i odczycie dat z czasami przekazują dodatkową infor- macje o strefie czasowej (inaczej niż w PostgreSQL) • Należy skorzystać z dodatkowej metody specyficz- nej dla Oracle ustawiającej strefę czasową bezpo- średnio na obiekcie Connection: OracleConnection.setSessionTimeZone(timeZoneName); DB2 W DB nie ma rozwiązań specyficznych, bo nie ma typu pola w bazie, które by wspierało przeliczanie stref cza- sowych. Porównanie zachowania pól timestamp w aplikacji Java Tabela 2 porównuje wyniki testu analogicznego do wy- konanego poprzednio bezpośrednio na bazie, tylko teraz przy pomocy aplikacji Java. Wpisaliśmy do kolumn każ- dego typu datę 1970-01-02 01:00:00 w strefie czasowej Europe/Warsaw (UTC+1) a następnie odczytujemy wy- nik z aplikacji Java w strefie America/Chicago (UTC-6) z zastosowaniem metod opisanych wyżej. Jak widać, baza danych MySQL w przypadku pobiera- nia przez JDBC błędnie aplikuje przesunięcie strefy cza- sowej w odniesieniu do pola typu które nie powinno mieć takiego przesunięcia. Wiedząc o tym trzeba staran- nie stosować odpowiednie typy pól i ustawianie domyśl- nej strefy czasowej w zależności od tego jakiego efektu potrzebujemy. Pozostałe bazy danych nie mają tego błędu i przesu- nięcia są aplikowane tylko na właściwych typach kolumn. Implementacja systemów globalnych Załóżmy, że mamy do implementacji globalny system au- kcyjny, w którym każdy może wystawić przedmiot, np. Kowalski z Warszawy o godzinie 11:00, i każdy może go licytować, np. Kowalsky z Chicago. Funkcjonalnie mu- si to działać tak, że Kowalsky z Chicago widzi że au- kcja rozpoczęła się o 4:00 rano jego czasu lokalnego. Opcjonalnie może widzieć, że była to godzina 11:00 cza- su środkowo europejskiego, żeby nie dziwić się dlaczego sprzedawca nie śpi po nocach. Opisane powyżej trudności z korzystaniem przelicza- nia stref czasowych przy pomocy bazy danych skła- niają do rozwiązania nie polegającego na przeliczaniu Tabela 2. Timestamp bez strefy czasowej Timestamp ze strefą czasową Inne MySQL Datetime 1970-01-01 18:00:00 Timestamp 1970-01-01 18:00:00 PostgreSQL timestamp without time zone 1970-01-02 01:00 timestamp with time zone 1970-01-01 18:00 Oracle timestamp 1970-01-02 01:00 timestamp with local time zone 1970-01-02 18:00:00 date 1970-01-02 01:00:00 timestamp with time zone 1970-01-02 18:00:00 DB2 timestamp 1970-01-02 01:00:00.0 -
www.sdjournal.pl 23 STREFY CZASOWE W JAVIE I BAZACH DANYCH stref przez bazę danych. Szczególnie korzystanie w me- tody TimeZone.setDefault(...) i OracleConnection. setSessionTimeZone(...) w aplikacjach JEE, czyli wie- lowątkowych i korzystających z połączeń pobieranych z puli serwera aplikacyjnego jest proszeniem się o pro- blemy. Najprostszym sposobem implementacji jest założenie, że domyślnie aplikacja działa w czasie GMT i tak zapisuje dane do bazy. Żeby użytkownikowi wyświetlić dane w jego stre- fie czasowej, musimy ją najpierw „zgadnąć”. Określe- nie to jest nieprzypadkowe, ponieważ nie ma standar- dowych mechanizmów, które pozwalałyby aplikacji po stronie serwera ustalić w jakiej strefie czasowej pracuje przeglądarka WWW klienta, np. poprzez na- główki protokołu HTTP. Jednym z rozwiązań jest posłużenie się usługą „IP ad- dress geocoding”, która na podstawie adresu IP zwraca dane o położeniu geograficznym i strefie czasowej. Nie- stety tego typu usługi są płatne. Aby obejść ten problem można posłużyć się prostym kodem JavaScript: var myDate = new Date(); myDate.getTimezoneOffset(); Zwraca on informacje o przesunięciu aktual- nej strefy czasowej systemu operacyjnego klien- ta względem czasu GMT wyrażony w minutach, np. dla Polski jest to „-60” w czasie zimowym i „-120” w czasie letnim. Żeby poprawnie zinterpretować, z którym czasem mamy aktualnie do czynienia nale- ży dokonać dodatkowych obliczeń na stworzonych dwóch dodatkowych datach: jednej w styczniu (zi- mowy) i jednej w lipcu (letni) i porównanie przesu- nięć z datą bieżącą. Ale łatwiej jest skorzystać z małej biblioteki JS pt. „jsTimezoneDetect” (http://pellepim.bitbucket.org/jstz/), w której to już zostało zrobione, a ponadto obsłu- ga konwersji przesunięcia wyrażonego w minutach na identyfikatory stref czasowych rozpoznawanych przez Javę. Niedoskonałość biblioteki polega na tym że jeżeli dla jednego przesunięcia, np. „+1h” istnieje kilka stref czasowych, to ona podpowie tylko jedną z nich. Nie jest to przeszkodą, jeżeli pozwolimy ją użytkownikowi potwierdzić albo ewentualnie zmienić. Przykład dzia- łania wygląda tak: var timezone = jstz.determine(); timezone.name(); ----------------------------------------------------- ″Europe/Berlin″ Jak widać na tym przykładzie, biblioteka popraw- nie zgadła przesuniecie +1h, ale przyporządkowała mu strefę czasową ″Europe/Berlin″ zamiast ″Europe/ Warsaw″ - trudno. Taką informacje musimy przesłać z przeglądarki do serwera i zapamiętać w sesji użytkow- nika. Gdy już znamy strefę czasową klienta, wszystkie daty, które chcemy mu wyświetlić musimy przeliczyć z GMT (tak są zapisane w bazie) na jego czas lokalny: Calendar gmtCalendar = Calendar.getInstance(TimeZone. getTimeZone(″GMT″)); PreparedStatement ps = c.prepareStatement(″select date_gmt from gmt_test″); ResultSet rs = ps.executeQuery(); if(rs.next()) { Timestamp timestampGmt = rs.getTimestamp(1, gmtCalendar); System.out.println(″Czas GMT: ″ + timestampGmt); // konwersja do czasu ″Europe/Warsaw″: TimeZone tz = TimeZone.getTimeZone(″Europe/ Warsaw″); Calendar calendar = Calendar.getInstance(tz); calendar.setTimeInMillis(timestampGmt.getTime()); System.out.format(″Czas lokalny: %tc\n″, calendar); // konwersja do czasu ″Europe/Chicago″: tz = TimeZone.getTimeZone(″America/Chicago″); calendar = Calendar.getInstance(tz); calendar.setTimeInMillis(timestampGmt.getTime()); System.out.format(″Czas lokalny: %tc\n″, calendar); } ----------------------------------------------------- Czas GMT: 1970-01-02 00:00:00.0 Czas lokalny: Pt sty 02 01:00:00 CET 1970 Czas lokalny: Cz sty 01 18:00:00 CST 1970 Żeby z kolei datę w lokalnej strefie czasowej wprowa- dzoną przez użytkownika zapisać do bazy danych w strefie GMT należy: Calendar gmtCalendar = Calendar.getInstance(TimeZone. getTimeZone(″GMT″)); PreparedStatement ps = c.prepareStatement(″UPDATE gmt_test SET date_gmt = ?″); SimpleDateFormat df = new SimpleDateFormat(″yyyy- MM-dd HH:mm″); df.setCalendar(Calendar.getInstance(TimeZone. getTimeZone(″Europe/Warsaw″))); Date date = df.parse(″1970-01-02 01:00″); // to czas wprowadzony przez usera
1/201424 System.out.println(″Czas użytkownika: ″ + date. toString()); ps.setTimestamp(1, new Timestamp(date.getTime()), gmtCalendar); ps.executeUpdate(); ----------------------------------------------------- Czas użytkownika: Fri Jan 02 00:00:00 GMT 1970 Jak widać, zmienna date jest już w domyślnej strefie GMT. Ciekawe problemy Niebanalnym problemem do rozwiązania jest kwestia ustalania czasu zdarzeń „systemowych”, czyli nie związa- nych z sesją konkretnego użytkownika. Przykładami ta- kich zdarzeń mogą być: • publikacja / archiwizowanie aktualności, • zakończenie czasu trwania promocji, • zakończenie terminu opłacenia usługi. Gdyby uzależnić takie zdarzenia od czasu użytkownika, to on zmieniając strefę czasową mógłby „gonić promo- cje”. W systemach globalnych najprostszym sposobem uniknięcia takich problemów i nieporozumień z klienta- mi jest wykonywanie zmian statusów takich obiektów przez crona systemowego działającego w strefie GMT i wyraźne informowanie swoich użytkowników, że tak to działa. Podsumowanie Jak widać, obsługa stref czasowych w aplikacjach global- nych nie jest taka prosta jak by można początkowo są- dzić. Mam nadzieję że niniejszy artykuł wyjaśnił podsta- wowe kwestie w tym zakresie. W ramach podsumowania warto jeszcze zaznaczyć że użyteczność klas związanych z obsługą dat w JDK (Date, Calendar) jest mało wygodna i powstały biblioteki, które to nieco ułatwiają, np. Joda-Time (http://www.joda.org/joda- time/). Także w JDK 8 zapowiadany jest nowy pakiet java. time, który ma ułatwić operacje na typach daty i czasu. Marek berkan Marek Berkan, Zastępca Dyrekto- ra ds. Realizacji w firmie e-point S.A. Odpowiada za proces im- plementacji systemów informa- tycznych, a następnie za ich dłu- gofalowy rozwój i utrzymanie. Od 13 lat programuje w J2EE, od 8 lat pracuje nad dynamicznie rozwija- jącym się projektem w technologii J2EE, koor- dynując prace zespołu programistów. Użyteczne komendy Oracle: • Ustawienie TZ klienta: TZ=America/Chicago sqlplus … • Ustawienie TZ całej bazy: ALTER DATABASE SET TIME _ ZONE = ″-06:00″; • Sprawdzenie TZ klienta: SELECT SESSIONTIMEZONE FROM DUAL; • Ładne formatowanie pola typu timestamp: ALTER SESSION SET nls _ timestamp _ format = ″yyyy-mm-dd hh24:mi:ss″; • Ładne formatowanie pola typu timestamp ze strefą czasową: ALTER SESSION SET nls _ timestamp _ tz _ format = ″yyyy-mm-dd hh24:mi:ss.ff tzh:tzm″; • Ładne formatowanie pola typu date z czasem: ALTER SESSION SET nls _ date _ format = ″yyyy-mm-dd hh24:mi:ss″; MySQL: • Ustawienie TZ klienta: SET TIME ZONE = ″+01:00″; • Sprawdzenie TZ klienta: SELECT @@global.time _ zone, @@session.time _ zone; PostgreSQL: • Ustawienie TZ klienta: SET TIME ZONE ″America/Chicago″; • Sprawdzenie TZ klienta: SHOW TIMEZONE;