Nasljednik Lollipopa
Predstavljena nova verzija Androida - Android M
prije 8 godina, 1 mjeseci
U jednom od prethodnih članaka smo vidjeli da je kreiranje "fat" web aplikacije, odnosno aplikacije koja se sastoji od web servera i svih potrebnih biblioteka, izuzetno jednostavno ukoliko koristimo prave alate, u ovom slučaju Spring Boot. Sada ćemo postojeće znanje proširiti kreiranjem web endpointa za REST komunikaciju sa aplikacijom. Konkretno, kreirat ćemo nekoliko endpointa uz pomoć kojih ćemo koristeći REST semantiku čitati, kreirati, ažurirati i brisati objekte iz baze podataka.
Prvi korak u dizajniranju REST aplikacije je uobičajeno identifikacija resursa ili objekata aplikacije. Obzirom da želimo proširiti primjere na kojima smo već radili, naš primarni resurs nastavlja biti Movie objekat.
Nakon ove identifikacije, moramo donijeti odluku na koji način predstaviti navedeni resurs. Izbor formata naravno najviše ovisi od API klijenata, npr. u slučaju da se radi o "internim" korisnicima API-ja možemo izabrati samo JSON reprezentaciju, a za "vanjske" klijente uobičajeno želimo podržati više standardnih formata, uključujući JSON i XML. Za potrebe našeg primjera, format kojeg želimo podržati je JSON, koji omogućava jednostavnu reprezentaciju pojedinačnih i nizovnih objekata.
U ovom trenutku možemo dizajnirati URI endpointe. Bazni URI aplikacije će biti http://localhost:8080. Na ovaj bazni URI ćemo po konvenciji dodati naziv resursa, u množini, čime dobijamo http://localhost:8080/movies. Konačno, individualnim resursima ćemo pristupati tako što u naš URI dodamo i identifikator resursa, npr. http://localhost:8080/movies/1, i http://localhost:8080/movies/5. Zajednički način identifikacije možemo predstaviti uz pomoć uri varijable: http://localhost:8080/movies/{movieId}.
Konačno, nakon što smo identificirali resurse aplikacije, odabrali format prezentacije te definisali URI endpointe, u posljednjem koraku trebamo odabrati HTTP akcije uz pomoć kojih ćemo omogućiti interakciju između klijenta i resursa, putem navedenih endpointa. Za rad sa kolekcijama želimo obično omogućiti GET i POST akciju, a za pojedinačne resurse uobičajeno definišemo akcije GET, PUT i DELETE. GET akcija naravno služi za čitanje i dohvatanje filmova, a metode POST, PUT i DELETE za unos, ažuriranje i brisanje filmova.
HTTP metod |
Endoint |
Status uspjeh |
Status neuspjeh |
Opis |
GET |
/movies |
200 |
500 |
Vraća listu svih filmova |
POST |
/movies |
201 |
500 |
Kreira novi film |
PUT |
/movies/{movieId} |
200 |
404 / 500 |
Ažurira postojeći film |
DELETE |
/movies/{movieId} |
200 |
404 / 500 |
Briše postojeći film |
GET |
/movies/{movieId} |
200 |
404 / 500 |
Vraća detalje jednog filma |
Obratiti pažnju da smo definirali i moguće statuse u slučaju uspjeha ili neuspjeha. Tako npr. ukoliko želimo ažurirati detalje postojećeg filma, u slučaju da film sa navedenom identifikacijom ne postoji, dobićemo status 404 (Not Found), a u slučaju drugih grešaka status 500.
Sada možemo krenuti u implementaciju naše aplikacije. Arhitektura aplikacije izgleda ovako:
Za čuvanje podataka koristimo memorijsku varijantu H2 baze podataka, a kao interfejs za rad sa podacimo smo koristili spring-data-jpa projekat. Obzirom da smo ovaj dio aplikacije pokrili i detaljnije objasnili u ranijim tekstovima, ovdje ćemo se bazirati na API komponentu. Ova komponenta je u osnovi REST kontroler, odnosno klasa koja izlaže definisane endpointe prema vanjskom svijetu i obavlja svoj posao komunicirajući sa repozitorijem baze podataka. Početna implementacija kontrolera izgleda ovako:
@RestController
public class MovieRestController {
}
Anotacija @RestController znači da će klasa biti registrovana kao Spring komponenta (bean) i dostupna u njegovom aplikacionom kontektsu, te da će Springov DispatcherServlet moći prosljeđivati URL zahtjeve ovoj komponenti. Dodatno ova anotacija osigurava da se povratne vrijednosti metoda unutar navedene klase koriste u HTTP odgovorima na klijentske zahtjeve. Drugim riječima, DispatcherServlet sada zna kome da proslijedi HTTP zahtjeve i kako da konstruiše odgovarajući odgovor.
Prva operacija koju želimo implementirati jeste GET operacija za dohvatanje svih filmova. Ukoliko unutar klase omogućemo ubacivanje repozitorijske klase za komunkaciju sa bazom podataka (vidjeti prethodne članke), jedan od načina za implementaciju ove operacije može biti ovaj:
@GetMapping("/movies")
public ResponseEntityIterable> getAllMovies() {
return new ResponseEntity>(movieRepository.findAll(), HttpStatus.OK);
}
Anotacija @GetMapping HTTP web zahtjev na endpointu /movies na metod koji se zove getAllMovies(). Treba primjetiti da metod vraća objekat u kojem se nalazi i payload (skup filmova) i HTTP status (OK = 200). Ako pokrenemo aplikaciju, navedenu operaciju možemo testirati sa curl alatom ili nekim od mnogobrojnih REST klijenata.
curl -X GET http://localhost:8080/movies
Rezultat ovog poziva je slijedeći:
[{"id":1,"title":"Gori Vatra","duration":98}]
Postavlja se pitanje na koji način je vraćeni payload konvertovan u JSON format. Zaslugu za ovaj zadatak preuzima automatski konfigurisani HTTP message converter koji uz pomoć Jackson biblioteke vrši konverziju u JSON.
Na sličan način možemo implementirati i POST akciju. Razlika je što sada očekujemo da dobijemo atribute filma u obliku JSON dokumenta unutar tijela HTTP poruke. U tu svrhu ćemo koristiti @RequestBody anotaciju:
@PostMapping("/movies")
public ResponseEntity createMovie(@RequestBody Movie movie) {
movieRepository.save(movie);
return new ResponseEntity(movie, HttpStatus.CREATED);
}
Sada možemo okinuti slijedeći request:
curl -H "Content-Type: application/json" -X POST http://localhost:8080/movies -d
'{"title": "Ničija zemlja", "duration":92}'
Obzirom da se radi o POST metodi. web zahtjev se mapira na java metod createMovie(movie), a JSON iz zahtjeva se automatski konvertuje u objekat klase Movie.
Akcija za dohvatanje detalja jednog filma uzgleda ovako:
@GetMapping("/movies/{movieId}")
public ResponseEntity getMovie(@PathVariable int movieId) {
Movie movie = movieRepository.findOne(movieId);
return movie != null ? new ResponseEntity>(movie, HttpStatus.OK)
: new ResponseEntity>(HttpStatus.NOT_FOUND);
}
Obzirom da je id filma dio URL puta, koristimo tzv. path varijablu, unutar @GetMapping anotacije označene sa {movieId}, te kao argument metode uz pomoć anotacije @PathVariable. Drugim riječima, ako kreiramo GET zahtjev na http://localhost:8080/movies/5, bit će pozvan metod getMovie(5). Treba primjetiti da u slučaju da film sa datim id-em ne postoji, metod vraća prazan payload i status 404 (NOT FOUND):
curl -v -X GET http://localhost:8080/movies/5
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /movies/5 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.47.0
> Accept: */*
>
HTTP/1.1 404
Content-Length: 0
Ažuriranje filma sadrži sličnu logiku, uz razliku što sada koristimo i path varijablu (identifikacija filma) i JSON body (ažurirani dokument)
@PutMapping("/movies/{movieId}")
public ResponseEntity updateMovie(@PathVariable int movieId, @RequestBody Movie data) {
Movie movie = movieRepository.findOne(movieId);
if (movie == null)
return new ResponseEntity>(HttpStatus.NOT_FOUND);
data.setId(movieId);
movieRepository.save(data);
return new ResponseEntity>(data, HttpStatus.OK);
}
curl -H "Content-Type: application/json" -X PUT http://localhost:8080/movies/1 -d
'{"title": "Gori vatra", "duration":100}'
Konačno, u slučaju brisanja filmova koristimo @DeleteMapping za DELETE HTTP metod:
@DeleteMapping("/movies/{movieId}")
public ResponseEntity deleteMovie(@PathVariable int movieId) {
Movie movie = movieRepository.findOne(movieId);
if (movie == null)
return new ResponseEntity>(HttpStatus.NOT_FOUND);
movieRepository.delete(movie);
return new ResponseEntity>(HttpStatus.OK);
}
curl -H "Content-Type: application/json" -X DELETE http://localhost:8080/movies/1
Vidjeli smo da je kreiranje REST servisa izuzetno lagano, ali ako želimo još pojednostaviti implementaciju, možemo koristiti spring-data-rest projekat. Ovaj projekat je izgrađen direktno nad spring-data projektom i omogućava automatsko kreiranje različitih REST endpointa na osnovu definisanih repozitorija. Sve što je potrebno uraditi jeste dodati maven dependency spring-boot-starter-data-rest i definisati bazni path:
spring.data.rest.basePath=/rest
Nakon ovoga, spring-data-rest će kreirati endpoint za svaki repozitorij u aplikaciji. Npr. posmatrajmo slijedeći repozitorij:
@Repository
public interface MovieRepository extends CrudRepository {
}
Za ovaj repozitorij će biti kreirani endpointi za čitanje, unos, ažuriranje i brisanje podataka. Za konstruisanje endpointa uzima se ime domenskog objekta sa kojim manipulišemo (Movie) i pretvara se u množinu. Primjeri generisanih endpointa su http://localhost:8080/rest/movies, http://localhost:8080/rest/movies/{movieId} itd. Naravno, ovo je samo vrh ledenog brijega jer spring-data-rest omogućava mnogo kompleksnije scenarije i kastomizacije, ali je dovoljno da uvidimo jednostavnost i korisnost upotrebe ove sjajne biblioteke.
Do narednog čitanja,
Almir Pehratović, Infobip BH d.o.o.
Na samom početku ove priče napravit ćemo jedan mali historijski pregled i pokušati shvatiti potrebu za servletima.
Početkom 50-ih godina američki naučnici su uložili mnogo truda da bi bili rame uz rame sa tadašnjim Sovjetskim savezom u oblasti računarstva i kompjuterske nauke. U tu svrhu 1957. godine su osnovali poseban tim ARPA (Advanced Research Projects Agency) koji se bavio naučnim istraživanjma u oblasti informatike. Iako su u to vrijeme imali najsavremeniju opremu i veoma snažnu kompjutersku moć sami računari nisu mogli komunicirati između sebe što je predstavljalo jedan od velikih izazova koje je bilo potrebno riješiti. Sredinom 60tih godina, tačnije 1966 Lawrence G. Roberts je odlučio da rijesi taj problem i predložio je dizajn prve kompjuterske mreže zvanu ARPANET. Pod pokrviteljstvom američkog odjela za odbranu (US Department of Defense) 3 godine poslije implementiran je ARPANET, prva kompjuterska mreža koja radi na principu razmjene paketa i time omogućio komunikaciju između računara.
Pravi uspjeh desio se 1982. kada su Vint Cerf i Bob Kahn implementirali TCP/IP standard i kada je DNS(Domain Name System) uspostavljen. Do kraja 1984. godine registrovano je preko 1000 hostova.
Početkom 90ih godina Tim Berners Lee je predstavio pojam hiperlinkova tj. napravio je cijeli protokol (http) preko kojeg se i do danasnjeg dana vrši razmjena podataka. World Wide Web(www) je rođen a sve ostalo je historija.
Premotat ćemo malo film unaprijed u vrijeme kada statičke stranice nisu bile dovoljne korisnicima i kada se javila potreba za dinamičkim sadržajem. Za prikaza dinamičkog sadržaja koristio se CGI (Common Gateway Interface).
Common gateway interface, u nastavku CGI, je takoreći jedna od prvih tehnika za kreiranje dinamičkih web aplikacija. Koristeći CGI, zahtjev koji dođe od klijeta web server proslijedi do externog programa (cgi script). Rezultat ovog poziva šalje se nazad korisniku u vidu statičkog fajla. Prednost CGI-a bila je ta što se moglo implementirati različite funkcionalnosti i brzo je postao de facto standard.
Nakon svih problema sa CGI kao pravi nasljednik na scenu stupaju servleti. Iako je java programski jezik konceptualiziran 1991. godine njena namjena nije bila tzv. internet programiranje sve dok se 1997. godine nisu pojavili servleti. Nastali su kao alternativa CGI i u mnogo stvari su bili bolji od konkurencije. Najveća prednost je bila to što su za svaki zahtjev otvarali java thread sto je sa aspekta performansi puno bolje od kreiranja novog procesa za svaki zahtjev. Zahvaljujući svojoj arhitekturi servleti su mogli podnijeti veliko opterecenje i time postali broj jedan u svijetu web developmenta.
Pored poboljšanja u performansama servleti su imali još jednu veliku prednost. S obzirom da servleti koriste javu kao programski jezik ovdje vazi sve isto kao i kod jave. Dakle nezavisni su od platforme i mogu se deploy-ati na različite operativne sisteme i web servere. U suštini servlet predstavlja običnu java klasu koja se izvršava na JVM-u (Java Virtual Machine).
Servlet predstavlja java objekat koji obradjuje http requeste i izvršava se unutar servlet kontejnera. Kontejneri se obično nalaze unutar web servera kao što su Tomcat, Jetty i mnogi drugi.
Kada zahtjev dođe sa browsera prema servlet kontejneru, on na osnovu konfiguracije odluči koji servlet da pozove. Servlet se aktivira pozivom service() metode a ona obrađuje poslani request i generiše response nazad u browser.
Kao sve u životu i servleti imaju svoj životni ciklus koji je orkestriran od strane kontejnera. Ciklus se sastoji od sljedećih koraka:
1. Load-anje servlet klase.
2. Kreiranje instance servleta.
3. Inicijalizacija servleta. Poziv init() metode.
4. Poziv service() metode.
5. Uništavanje, poziv destroy() metode.
Koraci 1, 2 i 3 se dešavaju pri samom pokretanju aplikacije load-aju se sve potrebne klase. Kada je servlet klasa load-ana, kontejner kreira instancu servlet klase (korak 2). Priliko kreiranja instance poziva se init() metoda koja omogućava da se servlet inicijalizira prije obrade prvog zahtjeva.
Za svaki napravljeni zahtjev poziva se service() metoda i sve dok je servlet aktivan mogućnost poziva ove metode je otvorena.
Servlet kontejner poziva destroy() metodu kako bi naznačio da servlet nece vise biti dostupan i poziv service() metode nad ovim servletom vise neće biti moguć. Ovo se često desava prilikom restarta ili gašenja aplikacije.
Recimo da želimo da napravimo servlet koji se poziva prilikom logiranja na stranicu. Da bi napravili servlet moramo kreirati klasu koja će extend-ati jednu od dvije abstraktne klase: GenericServlet ili HttpServlet. Osnovna razlika je, kao što i samo ime kaže, http servlet se koristi samo za http protokol dok generic servlet pruža podršku i za ostale protokole. Za naš slučaj mi ćemo koristiti http servlet.
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class MyPersonalServlet extends HttpServlet {
public void doGet(HttpServletRequest request,HttpServletResponse response) throws
ServletException, IOException {
// Do some login logic...
}
}
U konfiguraciji java web aplikacije navedemo mapiranje koje će servlet kontejneru reći kada koji servlet da poziva. Ova konfiguracija se čuva u web.xml i izgleda ovako:
Kada klijent otvori sljedeći URL kroz browser: http://localhost:8080/ServletDemo/myPage
request će doći prvo do servlet kontejnera koji će prema mapiranju iz web.xml fajla pozvati naš LoginServlet i odraditi njegovu doGet metodu. Nakon toga izgenerisat će se response objekat i vratiti nazad na browser klijentu.
Front controller dizajn patern predstavlja centralizovani mehanizam za obrađivanje zahtjeva. Spring dispatcher servlet implementira ovaj patern i kao takav je zaslužan za obradjivanje svih http zahtjeva. Zvuči poznato? Dispatcher servlet je kao i svaki drugi servlet, on se mora definisati u konfiguraciji aplikacije (web.xml). Da bi postigli gore navedeno ponašanje konfiguracija bi trebala izgledati ovako:
Dakle iz konfiguracije možemo vidjeti da svaki zahtjev što dodje na server dolazi do dispatcher servleta na obradu.
Šta se sve dešava ‘pod haubom’ spring dispatcher servleta vidjet ćemo kroz sljedeći primjer. Imamo kontroler:
@Controller
@RequestMapping("/appointments")
public class AppointmentsController {
private final AppointmentBook appointmentBook;
@Autowired
public AppointmentsController(AppointmentBook appointmentBook) {
this.appointmentBook = appointmentBook;
}
@RequestMapping(method = RequestMethod.GET)
public Map get() {
return appointmentBook.getAppointmentsForToday();
}
@RequestMapping(value="/{day}", method = RequestMethod.GET)
public Map getForDay(@PathVariable @DateTimeFormat(iso=ISO.DATE) Date day, Model model) {
return appointmentBook.getAppointmentsForDay(day);
}
@RequestMapping(value="/new", method = RequestMethod.GET)
public AppointmentForm getNewForm() {
return new AppointmentForm();
}
@RequestMapping(method = RequestMethod.POST)
public String add(@Valid AppointmentForm appointment, BindingResult result) {
if (result.hasErrors()) {
return "appointments/new";
}
appointmentBook.addAppointment(appointment);
return "redirect:/appointments";
}
}
Kotroler obezbjeđuje pristup aplikaciji, interpretira korisnikov zahtjev i obrađeni zahtjev vraća nazad korisniku.
Prva stvar koju možemo primjetiti jesu ove dvije anotacije @Controller i @RequestMapping. @Controller kao što i sama riječ kaže naznačava da ova klasa obavlja funkciju kontrolera dok @RequestMapping url kao što je /appointments vrši mapiranje na cijelu klasu.
Kada korisnik unese url http:localhost:8080/appointments/new u browser, zahtjev dolazi do dispatcher servleta. Servlet prihvati zahtjev i ulazi u proces resolvanja zahtjeva. Dispatcher, koristeći HandlerMapping klasu, skenira sve klase sa anotacijom @Controller i pronalazi odgovarajući kontroler za odgovarajući mapping(/appointments/new). Zahtjev odlazi do kontrolera gdje se vrši potrebna obrada i nakon obrade vraća se nazad u vidu modela u dispatcher servlet. Dobiveni model servlet šalje u ViewResolver klasu gdje se iz zahtjeva (appointments/new) resolva stvarni .jsp file. Na kraju dispatcher servlet dobiveni jsp šalje nazad korisniku.
U današnjoj upotrebi spring/spring boot-a detalji resolvanja zahtjeva ostaju skriveni i često postajaju nepoznanica samim korisnicima ovih alata. Ukoliko želite saznati nešto više o ovome detalje možete pronaći na spring-ovoj dokumentaciji ovdje : https://docs.spring.io/spring/docs/3.0.x/spring-framework-reference/html/mvc.html#mvc-handlermapping-interceptor
Do narednog čitanja.
Admir Oković, Infobip BH d.o.o.
Pretpostavimo da moramo kreirati klasu sa osam atributa. Za svaki od tih atributa potrebno je da napravimo getter i setter metode. Osim ovih metoda, obično moramo napisati i potrebne konstruktore, toString(), equals() i hashCode()metode. To znači da je za klasu sa osam atributa potrebno kreirati ni više ni manje nego devetnaest metoda i konstruktore, što čini tu klasu nepreglednom i komplikovanom za održavanje. Lombok je java biblioteka koja omogućava smanjenje koda unutar klasa. Lombok se koristi za automatsko generisanje objektnog koda uz pomoć anotacija. U ovom članku će biti prikazana upotreba Lombok-a kroz par primjera.
Lombok možemo dodati u svoje maven java projekte dodavajući u pom.xml sljedeći dependency:
Nakon što dodamo dependency, potrebno je da odaberemo lombok anotacije koje želimo koristiti. Lombok funkcioniše na način da se tokom build process-a, automatski generiše Java bytecode u .class datoteke u skladu sa odabranim anotacijama.
Osim toga bilo da koristimo IntelliJ IDEA ili Eclipse ili neko drugo IDE, potrebno je da instaliramo i lombok plugin. Iako se prilikom build process-a automatski generiše Java bytecode u skladu sa odabranom lombok anotacijom, IDE ne vidi taj generisan kod i prijavit će nam kompajlersku grešku. Ovaj slučaj je prikazan na sljedećoj slici:
Sada kad nam je poznat način funkcionisanja Lombok-a, opisat ćemo par Lombok anotacija.
Ukoliko želimo da spriječimo da nam se pomoću konstruktora ili setter-a postavi null kao vrijednost nekog polja, za to polje možemo staviti lombok anotaciju @NonNull.
Anotacija @ToString se koristi da generiše toString() metodu za klasu User. Anotacije @Getter i @Setter služe da generišu getter-e i setter-e za sva polja klase User, koja nisu final. Ukoliko polje ima anotaciju @NonNull, anotacija @Setter vrši provjeru da li je vrijednost, koju će to polje primiti, u skladu sa tim ograničenjem.
Anotacija @NoArgsConstructor generiše kontruktor bez parametara za tu klasu. Bitno je naglasiti za ovu anotaciju da ukoliko su sva polja unutar final klase, kontruktor bez parametara se neće izgenerisati i build proces će završiti sa greškom. Pojavljivanje ove greške se može izbjeći ukoliko property force postavimo na true, unutar anotacije (@NoArgsConstructor (force=true)).
package org.infobip.example.models;
import lombok.*;
@ToString
@NoArgsConstructor
@Getter
@Setter
public class User {
private String name;
private String surname;
private String username;
private String password;
private String email;
private String telephone;
private String address;
}
Nakon što se završi build proces u .class folderu možemo pronaći klasu na kojoj smo primjenili anotacije i vidjeti da je stvarno izgenerisan kod za odabrane anotacije. U našem primjeru izgenerisan kod možemo pronaći u target/classes/org/infobip/example/models/User.class.
Osim anotacije @NoArgsConstructor, lombok sadrži anotacije @RequiredArgsConstructor i @AllArgsConstructor za generisanje konstruktora. Prilikom upotrebe anotacija @RequiredArgsConstructor i @AllArgsConstructor za generisanje konstruktora, kontruktor će izvršiti provjeru null vrijednosti. Ukoliko polje ima ograničenje da ne može biti null (lombok-ova anotacija @NonNull), konstruktor će pri kreiranju baciti NullPointerException. @AllArgsConstructor generiše konstruktor sa po jednim parametrom za svako polje klase. Upotreba ove anotacije je prikazana na sljedećem primjeru:
package org.infobip.example.models;
import lombok.*;
@AllArgsConstructor
public class User {
private @NonNull String name;
private String surname;
private String username;
private String password;
private String email;
private String telephone;
private String address;
}
@RequiredArgsConstructor kreira konstruktor sa po jednim parametrom za sva polja koja su final, a nisu već inicijalizirana i za polja koja imaju anotaciju @NonNull. Osim mogućnosti generisanja konstruktora za sve parametre, konstruktora bez parametara, setter-a, lombok nudi mogućnost generisanja builder-a za tu klasu, na osnovu Builder design patterna. To je moguće ukoliko stavimo anotaciju @Builder iznad te klase i onda nema potrebe da pišemo dodatnu Builder klasu.
package org.infobip.example.models;
import lombok.*;
@Builder
public class User {
private @NonNull String name;
private String surname;
private String username;
private String password;
private String email;
private String telephone;
private String address;
}
U narednom primjeru je prikazano kako možemo postaviti vrijednosti polja te klase, nakon što smo stavili anotaciju @Builder.
package org.infobip.example.services;
import lombok.extern.slf4j.Slf4j;
import org.infobip.example.models.User;
@Slf4j
public class UserService {
public static void main(String[] args) {
User example1 = User.builder()
.address("Aleja lipa")
.build();
User example2 = User.builder()
.address("Aleja lipa")
.email("Ova adresa el. pošte je zaštićena od spambotova. Omogućite JavaScript da biste je vidjeli.")
.name("Ilvana")
.build();
User example3 = User.builder()
.password("ilvana")
.username("izajimovic")
.email("Ova adresa el. pošte je zaštićena od spambotova. Omogućite JavaScript da biste je vidjeli.")
.telephone("38761585254")
.surname("Zajimovic")
.build();
User example4 = User.builder().build();
}
}
Na ovom primjeru je pokazana primjena anotacije @Data. Anotacija @Data u sebi sadrži anotacije: @ToString, @EqualsAndHashCode, @Getter (za sva polja) i @Setter (za sva polja koja nisu final) i anotaciju @RequiredArgsConstructor.
package org.infobip.example.models;
import lombok.*;
@Data
public class User {
private String name;
private String surname;
private String username;
private String password;
private String email;
private String telephone;
private String address;
}
Obično kada želimo instancirati Logger iz Slf4j framework-a, uradimo to na sljedeći način:
package org.infobip.example.services;
import org.infobip.example.models.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UserService {
private static Logger log = LoggerFactory.getLogger(UserService.class);
public void create(User user){
log.info("Creating user...");
}
}
Lombok nam omogućava instanciranje Logger-a iz Slf4j frameworka pomoću anotacije @Slf4j.
package org.infobip.example.services;
import lombok.extern.slf4j.Slf4j;
import org.infobip.example.models.User;
@Slf4j
public class UserService {
public void create(User user){
log.info("Creating user...");
}
}
Ista stvar je i sa instanciranjem Logger-a iz Jboss framework-a. Bez Lombok-a bi to uradili na sljedeći način:
package org.infobip.example.services;
import org.infobip.example.models.User;
import org.jboss.logging.Logger;
public class UserService {
private static final Logger log = Logger.getLogger(UserService.class);
public void create(User user){
log.info("Creating user...");
}
}
A sa Lombok-om uz pomoć anotacije @JbossLog na sljedeći način:
package org.infobip.example.services;
import lombok.extern.jbosslog.JBossLog;
import org.infobip.example.models.User;
@JBossLog
public class UserService {
public void create(User user){
log.info("Creating user...");
}
}
Lombok posjeduje anotacije za instanciranje Logger i iz drugih framework-a.
@CommonLogs instancira
private static final org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(UserService.class);
@Log instanicira
private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(UserService.class.getName());
@Log4j instancira
private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(UserService.class);
@Log4j2 instancira
private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager.getLogger(UserService.class);
@XSlf4j instancira
private static final org.slf4j.ext.XLogger log = org.slf4j.ext.XloggerFactory.getXLogger(UserService.class);
Kako bi se izbjeglo stalno ponavljanje try/catch blokova, može se upotrijebiti lombok anotacija @SneakyThrows. Standardan način rukovanja sa izuzecima je prikazan u sljedećem primjeru:
package org.infobip.example.util;
import java.io.UnsupportedEncodingException;
public class UserServiceHelper {
public String utf8ToString(byte[] bytes) {
try {
return new String(bytes, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException();
}
}
}
Koristeći lombok anotaciju @SneakyThrows, prethodni primjer možemo napisati na sljedeći način:
package org.infobip.example.util;
import lombok.SneakyThrows;
public class UserServiceHelper {
@SneakyThrows( UnsupportedEncodingException.class)
public String utf8ToString(byte[] bytes) {
return new String(bytes, "UTF-8");
}
}
Za više detalja o Lomboku pogledajte dokumentaciju na sljedećem linku: http://www.baeldung.com/intro-to-project-lombok. Kod koji je korišten u primjerima možete pronaći na sljedećem linku: https://github.com/Ilvana/infobip-example. Do narednog čitanja!
Ilvana Zajimović, Infobip BH d.o.o.
U prethodnom tekstu smo upoznali osnove Spring Frameworka na primjeru male aplikacije koja omogućava čuvanje detalja o filmovima i glumcima. Aplikacija se sastoji od tri sloja: domenski sloj, sloj repozitorija i servisni sloj. Prije nego što počnemo objašnjavati koncepte Spring Boot-a, nadogradićemo našu aplikaciju uvodeći u igru bazu podataka, odnosno čuvajući detalje o filmovima i glumcima u H2 bazi podataka, a koja će se podizati i gasiti zajedno sa cijelom aplikacijom.
Prva stvar koju ćemo uraditi jeste konfiguracija novih dependency-ja u pom.xml datoteci. U ovom slučaju su nam potrebne dvije nove biblioteke: H2 jdbc driver i spring-jdbc biblioteka:
Obzirom da želimo da naša aplikacija kreira šemu baze podataka i popuni je sa odgovarajućim podacima, unutar src/main/resources foldera ćemo kreirati dva .sql fajla.
#schema.sql
DROP TABLE IF EXISTS ACTORS;
DROP TABLE IF EXISTS MOVIES;
DROP TABLE IF EXISTS ACTORS_MOVIES;
CREATE TABLE IF NOT EXISTS ACTORS(
ID INT PRIMARY KEY,
FIRST_NAME VARCHAR(40) NOT NULL,
LAST_NAME VARCHAR(40) NOT NULL
);
CREATE TABLE IF NOT EXISTS MOVIES(
ID INT PRIMARY KEY,
TITLE VARCHAR(200) NOT NULL,
DURATION INT NOT NULL,
);
CREATE TABLE IF NOT EXISTS ACTORS_MOVIES(
ID INT AUTO_INCREMENT PRIMARY KEY,
ACTOR_ID INT NOT NULL,
MOVIE_ID INT NOT NULL,
FOREIGN KEY (ACTOR_ID) REFERENCES ACTORS(ID),
FOREIGN KEY (MOVIE_ID) REFERENCES MOVIES(ID)
);
#data.sql
INSERT INTO ACTORS (ID, FIRST_NAME, LAST_NAME) VALUES (1, 'Brad', 'Pitt');
INSERT INTO ACTORS (ID, FIRST_NAME, LAST_NAME) VALUES (2, 'Edward', 'Norton');
INSERT INTO MOVIES (ID, TITLE, DURATION) VALUES (1, 'Fight Club', 139);
INSERT INTO MOVIES (ID, TITLE, DURATION) VALUES (2, 'Zodiac', 157);
INSERT INTO ACTORS_MOVIES(ACTOR_ID, MOVIE_ID) VALUES (1, 1);
INSERT INTO ACTORS_MOVIES(ACTOR_ID, MOVIE_ID) VALUES (2, 1);
INSERT INTO ACTORS_MOVIES(ACTOR_ID, MOVIE_ID) VALUES (1, 2);
Da bismo konfigurisali bazu podataka i omogućili rad sa njenim podacima, koristeći navedene biblioteke i .sql fajlove, potrebno je kreirati odgovarajući broj java (bean) objekata. Dva najvažnija objekta su implementacija interfejsa javax.sql.DataSource i objekat tipa org.springframework,jdbc.core.JdbcTemplate:
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource("jdbc:h2:~/cinema", "sa", "");
dataSource.setDriverClassName("org.h2.Driver");
return dataSource;
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
DataSource je objekat koji je zadužen za uspostavljanje konekcije sa bazom podataka. U ovom slučaju, baza podataka se nalazi u datoteci cinema koja se nalazi unutar HOME foldera. Ukoliko datoteka ne postoji, ista će biti kreirana pri prvom pokretanju aplikacije. JdbcTemplate sa druge strane predstavlja Spring-ovu apstrakciju JDBC API-ja, odnosno helper objekat koji nam omogućava da lakše i efikasnije komuniciramo sa bazom podataka. Tako na primjer, ukoliko želimo napisati metod koji čita podatke iz tabele baze podataka i iste konvertuje u odgovarajući java objekat, možemo napisati:
@Repository
public class DatabaseActorRepository implements ActorRepository{
@Override
public Actor findById(int id) {
Movie movie = jdbcTemplate.queryForObject("SELECT * FROM ACTORS WHERE ID = ?", new Object[] {id}, new BeanPropertyRowMapper>(Actor.class));
return movie;
}
}
JdbcTemplate metoda queryForObject omogućava da rezultat SQL upita konvertujemo u objekat klase Actor, koristeći BeanPropertyRowMapper instancu prilikom navedene konverzije. BeanPropertyRowMapper konvertuje dohvaćeni slog baze podataka u java objekat poređenjem naziva kolona tabele i atributa klase. Tako, kolone ID, FIRST_NAME i LAST_NAME će biti konvertovane u id, firstName i lastName atribute klase Actor.
Konačno, potrebna nam je i logika koja će kreirati objekte baze podataka i popuniti ih sa podacima iz .sql fajlova. U ovu svrhu ćemo kreira dva bean objekta:
@Bean
public DatabasePopulator databasePopulator() {
return new ResourceDatabasePopulator(new ClassPathResource("schema.sql"), new ClassPathResource("data.sql"));
}
@Bean
public DataSourceInitializer dataSourceInitializer(DataSource dataSource, DatabasePopulator databasePopulator) {
DataSourceInitializer initializer = new DataSourceInitializer();
initializer.setDataSource(dataSource);
initializer.setDatabasePopulator(databasePopulator);
initializer.setEnabled(true);
return initializer;
}
DataSourceInitializer inicira proces inicijalizacije baze podataka, a DatabasePopulator izvršava sql skripte schema.sql i data.sql.
Svi navedeni bean objekti (dataSource, jdbcTemplate, databasePopulator i dataSourceInitializer) se nalaze u klasi Config koja je označena anotacijom @Configuration. U skladu sa ovom anotacijom, Spring kontejner će kreirati navedene objekte izvršavanjem @Bean metoda što je to moguće ranije u životnom ciklusu aplikacije. Ovo garantuje rano uspostavljanje konekcije na bazu podataka i izvršavanje sql skripti, a jednostavnom upotrebom @Autowired anotacije, bilo koja komponenta aplikacije može dohvatiti jdbcTemplate objekat i raditi sa bazom podataka na gore opisani način.
Spring Boot je projekat koji se naslanja na Spring Framework i koji omogućava mnogo efikasniji i brži pristup izgradnje Spring aplikacija. Boot jednostavno skenira classpath aplikacije i ispituje importovane (Spring i third-party) biblioteke, te na osnovu tih saznanja pokušava zaključiti o kojem tipu aplikacije se radi i kreirati odgovarajuće bean objekte koje developer tipično mora kreirati u skladu sa aplikacionim zahtjevima. Tako na primjer, ukoliko Boot detektuje na classpathu H2 driver i spring-jdbc biblioteke, automatski će kreirati JdbcTemplate i DataSource objekte. Ukoliko Boot detektuje Hibernate biblioteke, automatski će kreirati veći broj objekata, npr. EntityManagetFactoryBean, ili TransactionManager objekat instanciranjem HibernateTransactionManager klase. U slučaju da koristimo Hazelcast, automatski se kreira HazelcastInstance objekat itd. Navedeni pristup se naziva autokonfiguracija.
Da bismo prethodno opisani primjer aplikacije napisali koristeći Spring Boot, potrebno je importovati odgovarajuće Boot biblioteke:
Spring Boot organizuje biblioteke u tzv. starterima, a svaki starter sadrži skup biblioteka koji je neophodan za specifične aplikacione potrebe. Tako, spring-boot-starter-jdbc sadrži sve biblioteke potrebne za rad sa bazom podataka a spring-boot-starter-web obuhvata veliki broj biblioteka za rad sa web aplikacijama. Na ovaj način developer nije oslobođen samo potrebe za importovanjem svih potrebnih biblioteka, već je oslobođen i brige za kompatibilnošću verzija tih biblioteka.
Već smo rekli da će Spring Boot na osnovu ovih informacija autokonfigurisati određene objekte, među njima DataSource i JdbcTemplate. Međutim, sjetimo se načina na koji smo to mi ručno uradili:
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource("jdbc:h2:~/cinema", "sa", "");
dataSource.setDriverClassName("org.h2.Driver");
return dataSource;
}
Postavlja se pitanje: kako Spring Boot zna da gdje se nalazi fajl baze podataka, odnosno koji jdbc url da koristi? Odgovor je da ne zna! Zapravo Boot prilikom autokonfiguracije koristi neke razumne default vrijednosti. U ovom slučaju, za jdbc url se koristi default vrijednost jdbc:h2:mem:testdb, što znači da će baza podataka biti podignuta u memoriji prilikom startupa aplikacije. Kako možemo promijeniti default vrijednost? Spring Boot prilikom autokonfiguracije čita konfiguracione fajlove u kojima možemo postaviti odgovarajuće propertije. Tako, možemo kreirati application.properties fajl unutar /src/main/resources foldera i u njega unijeti slijedeću konfiguraciju:
spring.datasource.url=jdbc:h2:~/cinema
Sada će automatski konfigurisani bean dataSource biti konektovan na cinema bazu podataka. Također, u Spring Boot implementaciji ne moramo kreirati DatabasePopulator i DataSourceInitializer objekte jer će Boot automatski pročitati schema.sql i data.sql, sve dok se ovako zovu i nalaze na classpathu! Bitno je napomenuti da je Boot dovoljno pametan da prepozna da li da koristi autokonfiguraciju ili ne, i to na osnovu pretrage Spring kontejnera. Drugim riječima, ukolimo mi sami kreiramo DataSource bean, Spring Boot neće kreirati ovaj bean za nas. Ovaj pristup omogućava neophodne kastomizacije u slučaju da default postavke ne odgovaraju našim potrebama.
Uključimo sada u naš projekat i web biblioteke:
Ova definicija iz pom.xml u naš projekat importuje veći broj biblioteka neophodnih za razvoj web aplikacija, uključujući i embedded Tomcat web server koji će biti podignut prilikom startanja aplikacije na default portu 8080. Sada možemo kreirati jednostavnog REST kontrolera:
@RestController
@RequestMapping("/")
public class HomeController {
@Autowired
CinemaService cinemaService;
@GetMapping("/movies")
public Set movies() {
return cinemaService.findAllMovies();
}
}
Nakon što pokrenemo aplikaciju, na URL-u http://localhost:8080/movies dobijamo filmove iz baze podataka, u JSON formatu:
curl -X GET http://localhost:8080/movies
[{"id":1,"title":"Fight Club","duration":139,"actors":
[{"id":2,"firstName":"Edward","lastName":"Norton"},
{"id":1,"firstName":"Brad","lastName":"Pitt"}]},
{"id":2,"title":"Zodiac","duration":157,"actors":
[{"id":1,"firstName":"Brad","lastName":"Pitt"}]}]
Boot je u ovom slučaju kreirao određeni broj objekata, uključujući DispatcherServlet, za upravljenje HTTP zahtjevima koji se proslijeđuju odgovarajućim handlerima i kontrolerima, HttpMessageConverters za transformaciju podataka u JSON, kao i Tomtcat specifični TomcatEmbeddedServletContainerFactory.
Da bi sve ovo radilo, klasa App je označena @SpringBootApplication anotacijom. Ova anotacija sadrži u sebi, između ostalih, i @EnableAutoConfiguration anotaciju a upravo ona je zadužena za automatsku konfiguraciju bean objekata o kojoj smo govorili do sada. Također, treba obratiti pažnju na SpringApplication klasu koja omogućava pokretanje aplikacije, kreirajući u pozadini odgovarajući ApplicationContext, što objašnjava činjenicu da mi to nismo morali raditi, kao u prethodnom članku.
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
Ukoliko u pom.xml dodamo spring-boot-maven-plugin, .jar fajl generisan sa komandom mvn package će sadržavati i sve (spring i third-party) biblioteke, uključujući i internog Tomcat servera. To znači da je jedan fat jar fajl dovoljan za distribuciju cijele aplikacije!
Kao i obično, izvorni kod primjera korištenih u ovom članku možete preuzeti na slijedećem linku. Ovim se ujedno i završava naša mini škola Spring Frameworka. U narednim člancima ćemo pokušati predstaviti različite biblioteke, tehnologije i pristupe prilikom izrade Java enterprise-ready aplikacija. Do narednog čitanja!
Almir Pehratović, Infobip BH d.o.o.