Skip to content

A simple Spring Boot/Angular example for ELTE students

Notifications You must be signed in to change notification settings

magyari-adam/issue-tracker

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Issue Tracker

Egy REST API amely Spring Boot segítségével készül el MVC pattern alapján, valamint egy Angular frontend, amely a felhasználói felületet biztosítja, törekszünk az MVVM pattern megtartására. Az adatokat H2 adatbázisban tárljuk a memóriában

Feladat

Készítsünk egy webes alkalmazást, amellyel bejelentezett felhasználóként olyan hibákat jelenthetünk be, amelyek az ELTE egyes termeiben találhatóak (pl. elromlott projektor), a bejelentett hibáinkat megtekinthetjük, ezekhez megjegyszést írhatunk. Adminként mindenki hibáját megtekinthetjük, változtathatjuk a hibák státuszát, és válaszolhatunk a felhasználók üzeneteire. Látogatóként csak statisztikát látunk, és regisztrálhatunk.

További részek

2. óra - REST API, Authentikáció, JOIN

Előkészületek

Előkészítjük a Spring Boot projetünket a Spring Initializer eszközzel:

  • Felül a lenyíló listákban:
    • maven
    • java
    • 1.5.7
  • Group: hu.elte.alkfejl
  • Artifact: issue-tracker
  • Dependencies:
    • H2 - in memory database
    • JPA - spring-data-jpa -> repositoryk használatához
    • Lombok - automatikus getter, setter, constructor generálás
    • Thymeleaf - nézet generálása
    • DevTools - fejlesztéshez segítség
  • Töltsük le a projektet

A H2 adatbázisunkat a böngészőből tudjuk majd nézegetni, debugolni fejlesztés közben, ehhez:

  • Nyissuk meg a projektet a kedvenc IDE-ben
  • Keressük meg az src/main/resources/application.properties fájlt
  • Helyezzük el benne ezt a két sor:
spring.datasource.platform=h2
spring.h2.console.path=/h2
  • Indítsunk egy git repot: git init
  • Commitoljuk az eddig elkészülteket git add . && git commit -m "setup"

MVC

Az MVC egy olyan struktúra amely segítségével a szoftverünk szerepköreit szétválasztjuk, az egyes rétegeket egymástól külön kezeljük.

A struktúra megtartásával az egyes rétegek könnyebben cserélhetőek, kiegészíthetőek, a szoftver jól struktúrált lesz és átlátható. Az újabb funkciók implementációja során a megoldandó problémát is kissebb részekre tudjuk így bontani.

Mi az az MVC:

Model

Az MVC ben az M a Model-t jelenti. Ebben a rétegben írjuk le az adatbázisban található entitásokat és ezek kapcsolatait

  • Készítsünk egy model package-et az issuetracker package-en belül
  • Minden entitásnak szüksége lesz ID és Version mezőkre, ezért készítünk egy BaseEntity-t
package hu.elte.alkfejl.issuetracker.model;

import lombok.Data;

import javax.persistence.*;

@Data
@MappedSuperclass
public class BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private String id;

    @Version
    private int version;
}

A @MappedSuperclass annotáció mondja meg a JPA-nak, hogy ezt az osztályt nem fogjuk önmagában entitásként kezelni, csak felhasználjuk más entitásoknál. A @Data annotáció a Lombok-ból jön, segítségévle automatikusan kigenerálódnak a getterek, setterek equals, és hashcode metódusok. Az @Id mondja meg, hogy a mező fogja azonosítani az egyes entitásokat, és automatikusan generáljuk az értékeit egy inkrementális stratégiával. A @Version használatára azért van szükség, mert egyszerre több felhasználó is módosíthatja az adatokat, ilyenkor ez a változó segíti a fennakadás mentes munkát( minden a háttérben történik, ezzel nem kell foglalkoznunk)

  • Elkészítjük a User táblát is
package hu.elte.alkfejl.issuetracker.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "USERS")
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class User extends BaseEntity {

    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String password;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false, unique = true)
    private Role role;

    public enum Role {
        GUEST, USER, ADMIN
    }
}

Az @Entity annotációval mondjuk meg, hogy ez az osztály egy entitás lesz. Az előbb elkészített BaseEntityből szárámazik. Lombok segítségével legeneráljuk a konstruktorait (@AllArgsConstructor, @NoargsConstructor), valamint jelezzük, hogy az equals és a hashcode generálásakor vegye figyelembe az ősosztálybeli mezők értékeit is. A @Column annotáció használata nem kötelező, a JPA minden entitás beli oszlopot elment (kivéve ha használjuk a @Transient annotációt), viszont segítségével tudjuk az unique és a null constrainteket érvényesíteni a mezőn. Az @Enumerated annotációval jelezzük, hogy az ENUM típúsú mezőket milyen módon szeretnénk persistálni. Itt Stringként, érték szerint fogjuk ezt tenni.

Ha most elindítjuk az alkalmazást mvn spring-boot:run és megnyitjuk a localhost:8080/h2 -t akkor látjuk a H2 conosle ban, hogy sikeresen létrehozta az entitás leírása alapján a Spring Boot a JPA segítségével a táblánkat. (jdbc url: jdbc:h2:mem:testdb)

Mivel a H2 adatbázis a memóriába ment, ezért minden alkalommal, mikor leállítjuk törlődnek az adataink. Ahhoz hogy legyenek adataink az induláskor az adatbázisban a Spring Boot segítségét vesszük. a src/main/resources mappában elkészítjük a data.sql-t(adatok insertálása).Ezt futtatja majd a Spring Boot, amikor az alakalmazásunkat indítjuk.

Készítsünk egy új felhasználót a /src/main/resources/data.sql fájlban: INSERT INTO USERS (ID, VERSION, USERNAME, EMAIL, PASSWORD, ROLE) VALUES (0, 0, 'admin', 'admin@gmail.com', 'admin', 'ADMIN');. Ez létrehoz egy felhasználót ID:0, Version: 0, username:'admin', email:'admin@gmail.com' password: 'admin', role: 'ADMIN'. Később titkosítjuk majd a jelszót.

A select SQL from information_schema.tables where table_name = 'USERS'; parancs kiadásával a H2 konzolban az eredmány az az sql parancs, amit a JPA kiadott a tábla létrehozásához.

Még visszatérünk majd a modelre és kiegészítjük a többi osztállyal.

Controller

A Controller feladata a beérkező HTTP kéréseket feldolgozni, és ezekre válaszolni. Végpontokat definiálunk, amelyeket a külvilág (Angular) alkalmazás elér HTTP kérésekkel és ezekre választ kap. Gyakran készítünk egy Service réteget is a Controller mellé, hogy mg jobban szétválasszuk a felelősségi köröket: A controller csak a kérések fogadásával és a válasz adással foglalkozik, a Service réteg a kérés és válasz közötti adat feldolgozással, adatbázissal való kommunikációval.

Készítsünk el egy UserController-t, amellyel bejelentkeztetünk, regisztrálhatunk:

  • az issuetracker package-en belül hozzunk létre egy controller package-et
  • és abban egy UserController osztályt
package hu.elte.alkfejl.issuetracker.controller;

import hu.elte.alkfejl.issuetracker.model.User;
import hu.elte.alkfejl.issuetracker.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;

import static hu.elte.alkfejl.issuetracker.model.User.Role.USER;

@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/greet")
    public String greeting(@RequestParam(value = "name", required = false, defaultValue = "World") String name, Model model) {
        model.addAttribute("name", name);
        return "greeting";
    }

    @GetMapping("/login")
    public String login(Model model) {
        model.addAttribute(new User());
        return "login";
    }

    @PostMapping("/login")
    public String login(@ModelAttribute User user, Model model) {
        if (userService.isValid(user)) {
            return redirectToGreeting(user);
        }
        model.addAttribute("loginFailed", true);
        return "login";
    }

    @GetMapping("/register")
    public String register(Model model) {
        model.addAttribute("user", new User());
        return "register";
    }

    @PostMapping("/register")
    public String register(@ModelAttribute User user) {
        user.setRole(USER);
        userService.register(user);

        return redirectToGreeting(user);
    }

    private String redirectToGreeting(@ModelAttribute User user) {
        return "redirect:/user/greet?name=" + user.getUsername();
    }
}

Itt már rengeteg dolog történik:

  • A @Controller annotációval jelezzük, hogy ez egy kontroller lesz, ez többek között egy Spring Bean-t készít az osztályból.
  • A Spring Bean olyan osztály, amiket a Spring Dependenxy Injectionben használ, nem kell kézzel new-val létrehoznunk, a Spring akkor létrehozza amikor szüksége van rá
  • A @RequestMapping("/user") annotáció megmondja a Springnek, hogy a /user alatt hallgasson minden végpont, minden HTTP metódusra
  • Bár a UserService osztály még nem íruk meg - ez lesz a következő feladat - itt már támaszkodunk rá. Ez egy service lesz, amely mint már említettem a kontrolelrekben található üzleti logikát tartalmazza. Spring Beanként hozzuk majd létre, ezért tudjuk az @Autowired annotációval beinjektálni
  • A @GetMapping("/greeting") azt állítja be, hogy a metódus a GET HTTP metódus hatására hívódjon meg méghozzá a /greet url-en, DE mivel az egész osztályra rátettük a @RequestMapping("/user")-t ezért a /user alá kerül be és lesz belőle /user/greet.
  • A @RequestParam segítségével érjük el a GET requestek paraméterét és alapértelmezett értéket is tudunk adni nekik. A Model model paraméter segítségével tudunk adatokat juttatni a felületre. Itt például a paraméterként érkező nevet juttatjuk a nézetbe. A metódus visszatérési értéke a template, amelyet megjeleníteni szeretnénk, erről a View részben lesz szó
  • A @PostMapping jelzi azt, hogy a metódus egy POST requestet fog kezelni a megadott route-on pl. /register vagy /login a @ModelAttribute a post metódusban érkező form adatokat parseolja fel és értelmezi User-ként, ehhez szükséges, hogy megefelelő legyen a mezők elnevezése a form-ban
  • Lehetőségünk van az átirányításra is ennek a mintáját látjuk a redirectToGreeting metódusban

Service

A Service réteg szigorúbban véve a Controllerhez tartozik, az üzleti logikát tartamazza. A mi feladatunkban például a felhasználók validálásának menetét és a regisztárlást fogja tartalmazni.

Azért is érdemes kivenni az ilyen logikát a controllerből és áthelyezni a Service-be mert lehet, hogy több helyről is akarjuk ugyanazokat a folyamatokat használni. Például ha egy REST API-t írunk az alkalmazáshoz, de megtartjuk az eredeti MVC appot is, akkor nem kell kétszer leírnunk ugyanazokat a logikákat (felhasználó bejelentkezésének folyamata, stb.) így a kódduplikációt szürhetjük ki, azaz ha megváltozik a logika a bejelentkeztetés mögött (pl. felhasználónév helyett email alapú lesz), akkor csak egy helyen kell módosítani: a service-ben.

Készítsük el a UserService-t a service package-ben:

package hu.elte.alkfejl.issuetracker.service;

import hu.elte.alkfejl.issuetracker.model.User;
import hu.elte.alkfejl.issuetracker.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public void register(User user) {
        userRepository.save(user);
    }

    public boolean isValid(User user) {
        return userRepository.findByUsernameAndPassword(user.getUsername(), user.getPassword()).isPresent();
    }
}
  • A @Service annotációval jelezzük, hogy az osztályunk egy Spring Bean, így tudjuk majd injectelni az @Autowired segítségével más osztályokba, a Spring kezeli az életciklusát
  • Használunk egy UserRepository nevű SpringBean-t ezt még meg kell írnunk. Ő foglalkozi az adatok perzisztálásával, és lekérdezésével, lényegében az adatbázissal kommunikál

Data Access Layer

Lényegében a Model-be tartozik, de csak most jutottunk el a használatához, ezért mutatom itt be.

A DAL egy absztrakt fogalom, azokat az osztályokat értjük bele, amelyek kapcsolatot teremtenek az adatbázissal. Az Entitásokkal ismerkedtünk meg eddik, ők leírják a Spring-nek, hogy milyen formában kell létrehozni a táblákat és a belőlük létrehozott objektumokkal dolgozunk az alkalmazásban/kommunikálunk az adatbázissal (bejelentkezünk és elküldjük a felhasználó adatait, regisztrációval új felhasználó vehető fel). Ahhoz, hogy elérjük az adatbázist szükségünk van még olyan osztályokra amelyek a kommunikációt írják le, eléggé absztrakt, ahhoz, hogy szinte bármilyen relációs adatbázissal működjön:

  • Kapcsolat nyitása
  • Lekérdezések futtatása
  • Adatok visszaadása
  • Kapcsolat lezárása

A DAL rétegünket felépítő osztályokat Springben Repositorynak hívjuk (Java EE-ben DAO, bár az kicsit más). Általában nekünk kell ezeket az adatbázis lekérdezéseket megírnunk, JDBC ben vagy valamilyen absztrakt ORM-ben pl. Hibernate, Querydsl

Ilyenkor általában nagyon hasonló kódok születnek, kb. midnig hasonlóan kell kapcsolatot teremteni egy adatbázissal, perzisztálni entitásokat stb. Itt most nem ez a helyzet. A spring-data-jpa egy olyan eszközkészletet ad a kezünkbe, mely segítségével ezeket rábízhatjuk a Spring-re. A Spring konkrétan le generálja ezeket a kódokat, de ehhez egy megkötött formában kéri megadni a Reopsitoryt. FIGYELEM! Magic következik:

Készítsük el tehát a UserRepository-t:

package hu.elte.alkfejl.issuetracker.repository;

import hu.elte.alkfejl.issuetracker.model.User;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends CrudRepository<User, String> {
    Optional<User> findByEmail(String email);

    Optional<User> findByUsername(String username);

    Optional<User> findByUsernameAndPassword(String username, String password);
}
  • A @Repository annotáció egy injektálható Spring Bean-t hoz létre, amely egy Repository lesz (DAL-ba tartozó osztály).
  • Származtassunk egy interface-t a CrudRepository interface-ből
  • Az első generikus paraméter azt írja el, milyen entitáshoz akarjuk majd használni a repositoryt, a második az ID típusát.

A CrudRepository egy olyan speciális repository amely definiálja a CRUD műveletekhez szükséges metódusokat. Ezt kapjuk meg mi is, ha innen származtatjuk az interface-ünket

View

A View rétegbe kerülnek azok a fájlok, amelyek a megjelenést biztosítják, tehát esetünkbe a html fájlok, amelyekbet a böngésző értelmezni fog.

A Controllernél említettem, hogy az egyes metódusok át tudnak irányítani, vagy visszaadnak egy stringet, amellyel megmondják, melyik html fájlt kell renderelni. Ezeket a fájlokat az src/main/resources/remplates mappában helyezzük el, ilyenkor egyszerűen csak a nevüket kell visszaadni a controllerből pl. a greeting.html esetén greeting. Ha mélyebben lévő fájlt szeretnénk visszaadni azt is megtehetjük pl. templates/a/b.html esetén a/b -t kell visszaadnia a kontrollernek.

Az eredeti HTML fájlok statikusak, ahogy megírjuk őket azok, úgy renderelődnek. Nekünk ez ebben az esetben nem jó, kellene valamilyen módszer, hogy adatokat tudjunk átadni nekik (dinamikus megjelenés kedvéért).

Ehhez használjuk a Thymeleaf-et.

Lássunk egy példát a login oldalra:

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Login</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <h1>Login</h1>
        <h2 th:if="${loginFailed}">Invalid user data!</h2>
        <form action="#" th:action="@{/user/login}" th:object="${user}" method="post">
            <div id="username">
                <label for="username">Username:</label>
                <input type="text" name="username" placeholder="username" th:field="*{username}" />
            </div>
            <div id="password">
                <label for="password">Password:</label>
                <input type="password" name="password" placeholder="password" th:field="*{password}" />
            </div>
            <input type="submit" value="Submit!" />
            <button type="reset">Cancel</button>
        </form>
    </body>
</html>

Mint láthatjuk nem egy egyszerű html-lel van dolgunk, találunk itt xmlns namespace-t (th) és ilyen fura dolgokat, mint @{}, ${}, *{}.

  • @{} - link megjelenítésére használjuk
  • ${} - egy kifejezés kiértékelésére használjuk pl. változó kiírása
  • *{} - szintén kifejezés kiértékelésére használjuk, de mélyebb szinten pl. mivel a *{username}-t bentebbi szinten haszáljuk mint a ${user}-t ezért őt a ${user} fieldjeként kezeli. Itt a *{username} megegyezik a ${user.username} kifejezéssel
  • th:object - használatával a conrollerben látott @ModelAttribute-ba kötjük bele a form adatait
  • th:field - ezt a fieldet "belekötjük" a ModelAttribute-ba

About

A simple Spring Boot/Angular example for ELTE students

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Java 56.2%
  • Shell 20.6%
  • Batchfile 15.9%
  • HTML 7.3%