class: inverse, center, middle
class: inverse, center, middle
- Craig Walls: Spring in Action, Fifth Edition (Manning)
- Kevin Hoffman: Beyond the Twelve-Factor App (O'Reilly)
- Spring Boot Reference Documentation
class: inverse, center, middle
- Keretrendszer nagyvállalati alkalmazásfejlesztésre
- Keretrendszer: komponensek hívása, életciklusa
- Nagyvállalati alkalmazás: Java SE által nem támogatott tulajdonságok
- Spring Framework nem magában ad választ, hanem integrál egy vagy
több megoldást
- Spring Framework nem magában ad választ, hanem integrál egy vagy
- Komponensek életciklus kezelése és kapcsolatok
- Távoli elérés
- Többszálúság
- Perzisztencia
- Tranzakció-kezelés
- Aszinkron üzenetkezelés
- Ütemezés
- Integráció
- Auditálhatóság
- Konfigurálhatóság
- Naplózás, monitorozás és beavatkozás
- Biztonság
- Tesztelhetőség
- Komponensek, melyeket konténerként tartalmaz
(konténer: application context) - Konténer vezérli a komponensek életciklusát
(pl. példányosítás) - Konténer felügyeli a komponensek közötti kapcsolatot
(Dependency Injection, Inversion of Control) - Komponensek és kapcsolataik leírása több módon:
XML, annotáció, Java kód - Pehelysúlyú, non invasive (POJO komponensek)
- Aspektusorientált programozás támogatása
- 3rd party library-k integrálása az egységes modellbe
- Glue kód
- Boilerplate kódok eliminálása
- Fejlesztők az üzleti problémák
megoldására koncentráljanak
- Nem kizárólag erre, de ez a fő felhasználási terület
- Rétegek
- Repository
- Service
- Controller
- Hangsúlyos része a Spring MVC webes alkalmazások írására,
HTTP felé egy absztrakció - HTTP kezelését web konténerre bízza,
pl. Tomcat, Jetty, stb.
class: inverse, center, middle
- Autoconfiguration: classpath-on lévő osztályok, 3rd party library-k, környezeti változók és
egyéb körülmények alapján komponensek automatikus
létrehozása és konfigurálása - Intelligens alapértékek, convention over configuration,
konfiguráció nélkül is működjön- Saját konfiguráció írása csak akkor, ha eltérnénk az alapértelmezettől
- Automatically, automagically
- Self-contained: az alkalmazás tartalmazza
a web konténert is (pl. Tomcat)
- Nagyvállalati üzemeltethetőség: Actuatorok
- Pl. monitorozás, konfiguráció, beavatkozás,
naplózás állítása, stb.
- Pl. monitorozás, konfiguráció, beavatkozás,
- Gyors kezdés: Spring Initializr https://start.spring.io/
- Starter projektek: függőségek,
előre beállított verziószámokkal (tesztelt) - Ezért különösen alkalmas microservice-ek fejlesztésére
├── .gitignore
├── .mvn
├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── training
│ │ └── employees
│ │ └── EmployeesApplication.java
│ └── resources
│ ├── application.properties
│ ├── static
│ └── templates
└── test
└── java
└── training
└── employees
└── EmployeesApplicationTests.java
- Parent:
org.springframework.boot:spring-boot-starter-parent
- Innen öröklődnek a függőségek, verziókkal együtt
- Starter:
org.springframework.boot:spring-boot-starter-web
- Jackson, Tomcat
- Teszt támogatás:
org.springframework.boot:spring-boot-starter-test
- JUnit 5, Mockito, AssertJ, Hamcrest, XMLUnit, JSONassert, JsonPath
class: inverse, center, middle
- Spring bean: tud róla a Spring konténer
- Spring példányosítja
- Spring állítja be a függőségeit
- POJO
- Rétegekbe rendezve
- Alapértelmezetten singleton, egy példányban jön létre
- Alkalmazás belépési pontja
main()
metódussal @SpringBootApplication
:@EnableAutoConfiguration
: autoconfiguration bekapcsolása@SpringBootConfiguration
:@Configuration
: maga az osztály is
tartalmazhasson további konfigurációkat@ComponentScan
:@Component
,
@Repository
,@Service
,@Controller
annotációval ellátott
@SpringBootApplication
public class EmployeesApplication {
public static void main(String[] args) {
SpringApplication.run(EmployeesApplication.class,
args);
}
}
- Spring MVC része
- Felhasználóhoz legközelebb lévő réteg, felelős a
felhasználóval való kapcsolattartásért- Adatmegjelenítés és adatbekérés
- POJO
- Annotációk erős használata
- Nem feltétlenül van Servlet API függősége
- Metódusok neve, paraméterezése flexibilis
- Gyakran REST végpontok kialakítására használjuk
@Controller
: megtalálja a component scan, Spring MVC felismeri@RequestMapping
milyen URL-en hallgat- Ant-szerű megadási mód (pl.
/admin/*.html
) - Megadható a HTTP metódus a
method
paraméterrel
- Ant-szerű megadási mód (pl.
@ResponseBody
visszatérési értékét azonnal a HTTP válaszba kell írni
(valamilyen szerializáció után)
@Controller
public class EmployeesController {
@RequestMapping("/")
@ResponseBody
public String helloWorld() {
return "Hello World!";
}
}
@Service
public class EmployeesService {
public String helloWorld() {
return "Hello World at " + LocalDateTime.now();
}
}
- Dependency injection
- Definiáljuk a függőséget, a konténer állítja be
- Függőségek definiálása:
- Attribútum
- Konstruktor
- Metódus
- Legjobb gyakorlat: kötelező függőség konstruktorban
- Ha csak egy konstruktor, automatikusan megtörténik
a dependency injection - Egyéb esetben
@Autowired
annotáció
@Controller
public class EmployeesController {
private EmployeesService employeesService;
public EmployeesController(EmployeesService employeesService) {
this.employeesService = employeesService;
}
@RequestMapping("/")
@ResponseBody
public String helloWorld() {
return employeesService.helloWorld();
}
}
class: inverse, center, middle
src/main/resources/static
könyvtárban- Welcome Page:
index.html
- JavaScript könyvtárak jar fájlba csomagolva
META-INF/resources
könyvtárban- Hivatkozás pl.:
/webjars/bootstrap/4.5.2/css/bootstrap.css
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>4.5.2</version>
</dependency>
Hivatkozás pl.: /webjars/bootstrap/css/bootstrap.css
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>
class: inverse, center, middle
- Ekkor nem kell a
@Service
annotáció: non-invasive @Configuration
által ellátott osztályban, ittEmployeesApplication
- Legjobb gyakorlat: saját beanek component scannel,
3rd party library-k Java konfiggal - Legjobb gyakorlat: rétegenként külön
@Configuration
annotációval ellátott osztály
@Bean
public EmployeesService employeesService() {
return new EmployeesService();
}
public class EmployeesService {
public String helloWorld() {
return "Hello Dev Tools at " + LocalDateTime.now();
}
}
class: inverse, center, middle
- Build parancssorból Mavennel
mvnw clean package
spring-boot-maven-plugin
: átcsomagolás, beágyazza a web konténertemployees-0.0.1-SNAPSHOT.jar.original
és
employees-0.0.1-SNAPSHOT.jar
- Futtatás parancssorból Mavennel
mvnw spring-boot:run
- Futtatás parancssorból
java -jar employees-0.0.1-SNAPSHOT.jar
class: inverse, center, middle
- A start.spring.io támogatja a Gradle alapú projekt generálását
- A io.spring.dependency-management
Gradle plugin
lehetővé tesz Maven-szerű függőségkezelést
- csak a verziókat deklarálja, de a függőséget explicit kell megadni - A org.springframework.boot
Gradle plugin
képes a jar és war előállítására, figyelembe véve az előző plugint - Generál Gradle wrappert is - ha nincs Gradle telepítve az adott gépre
- JUnit 5 függőség
gradle build
gradle -i build
gradle bootRun
A -i
kapcsoló INFO szintű naplózást állít
class: inverse, center, middle
- JUnit 5
- Non invasive - POJO-ként tesztelhető
- AssertJ, Hamcrest classpath-on
@Test
void testSayHello() {
EmployeesService employeesService = new EmployeesService();
assertThat(employeesService.sayHello())
.startsWith("Hello");
}
- Mockito classpath-on
@ExtendWith(MockitoExtension.class)
public class EmployeesControllerTest {
@Mock
EmployeesService employeesService;
@InjectMocks
EmployeesController employeesController;
@Test
void testSayHello() {
when(employeesService.sayHello())
.thenReturn("Hello Mock");
assertThat(employeesController.helloWorld())
.startsWith("Hello Mock");
}
}
- Üres teszt a konfiguráció ellenőrzésére, elindul-e az application context
@SpringBootTest
annotáció: tartalmazza
az@ExtendWith(SpringExtension.class)
annotációt- Tesztesetek között cache-eli az application contextet
- Beanek injektálhatóak az
@Autowired
annotációval
@SpringBootTest
class EmployeesControllerIT {
@Autowired
EmployeesController employeesController;
@Test
void testSayHello() {
String message = employeesController
.helloWorld();
assertThat(message).startsWith("Hello");
}
}
class: inverse, center, middle
- Felülírt property-k (Property Defaults): pl. template cache kikapcsolása
- Automatikus újraindítás
- LiveReload
- Globális, felhasználónkénti beállítások (Global Settings)
- Távoli alkalmazáson osztály frissítése (Remote Applications)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
- Ha változik valami a classpath-on
- IDE függő
- Eclipse-nél mentésre
- IDEA-nál Build / Rebuild Project (
Ctrl + F9
billentyűzetkombináció)
- Két osztálybetöltő, az egyik a saját kód, másik a függőségek -
változás esetén csak az elsőt tölti újra, függőség változása esetén
manuálisan kell újraindítani - Újraindítja a web konténert is
spring.devtools.restart.poll-interval=2s
spring.devtools.restart.quiet-period=1s
- Böngésző plugin szükséges hozzá
- A Spring Boot elindít egy LiveReload szervert
- LiveReload plugin felépít egy WebSocket kapcsolatot
- Ha változik valami, újratöltés van
- Pl. statikus állomány esetén (
src/main/resources/static/*.html
) - IDE függő
- Eclipse-nél mentésre
- IDEA-nál Build / Rebuild Project
(Ctrl + F9
billentyűzetkombináció)
$HOME/.config/spring-boot
könyvtárban
pl.spring-boot-devtools.properties
állomány
- El lehet indítani becsomagolt alkalmazáson is a DevToolst
(spring-boot-maven-plugin
konfigurálásával) - Remote Client Applicationt kell elindítani, mely lokális gépen indul,
és csatlakozik a távoli alkalmazáshoz - Remote Update: ha valami változik a classpath-on, feltölti
- Ne használjuk éles környezetben
class: inverse, center, middle
- Twelve-factor app egy manifesztó, metodológia felhőbe
telepíthető alkalmazások fejlesztésére - Heroku platform fejlesztőinek ajánlása
- Előtérben a cloud, PaaS, continuous deployment
- PaaS: elrejti az infrastruktúra részleteit
- Pl. Google App Engine, Redhat Open Shift, Pivotal Cloud Foundry,
Heroku, AppHarbor, Amazon AWS, stb.
- Pl. Google App Engine, Redhat Open Shift, Pivotal Cloud Foundry,
- Jelző olyan szervezetekre, melyek képesek az automatizálás előnyeit kihasználva
gyorsabban megbízható és skálázható alkalmazásokat szállítani - Pivotal, többek között a Spring mögött álló cég
- Előtérben a continuous delivery, DevOps, microservices
- Alkalmazás jellemzői
- PaaS-on fut (cloud)
- Elastic: automatikus horizontális skálázódás
- Verziókezelés: "One codebase tracked in revision control, many deploys"
- Függőségek: "Explicitly declare and isolate dependencies"
- Konfiguráció: "Store config in the environment"
- Háttérszolgáltatások: "Treat backing services as attached resources"
- Build, release, futtatás: "Strictly separate build and run stages"
- Folyamatok: "Execute the app as one or more stateless processes"
- Port hozzárendelés: "Export services via port binding"
- Párhuzamosság: "Scale out via the process model"
- Disposability: "Maximize robustness with fast startup and graceful shutdown"
- Éles és fejlesztői környezet hasonlósága:
"Keep development, staging, and production as similar as possible" - Naplózás: "Treat logs as event streams"
- Felügyeleti folyamatok:
"Run admin/management tasks as one-off processes"
- One Codebase, One Application
- API first
- Dependency Management
- Design, Build, Release, Run
- Configuration, Credentials and Code
- Logs
- Disposability
- Backing services
- Environment Parity
- Administrative Processes
- Port Binding
- Stateless Processes
- Concurrency
- Telemetry
- Authentication and Authorization
class: inverse, center, middle
- Operációs rendszer szintű virtualizáció
- Jól elkülönített környezetek, saját fájlrendszerrel és telepített szoftverekkel
- Jól meghatározott módon kommunikálhatnak egymással
- Kevésbé erőforrásigényes, mint a virtualizáció
- Kliens - szerver architektúra, REST API
- Kernelt nem tartalmaz, hanem a host Linux kernel izoláltan futtatja
- namespaces: operációs rendszer szintű elemek izolálására: folyamatok, InterProcess Communication (IPC), fájlrendszer, hálózat, UTS (UNIX Timesharing System - host- és domainnév), felhasználók
- cGroups (Control Groups): erőforrás limitáció
- Union FS (írásvédett, vagy írható/olvasható rétegek)
- Docker Toolbox: VirtualBoxon futó Linuxon
- Docker Desktop
- Hyper-V megoldás: LinuxKit, Linux Containers for Windows (LCOW), MobyVM
- WSL2 - Windows Subsystem for Linux - 2-es verziótól Microsoft által Windowson fordított és futtatott Linux kernel
- Saját fejlesztői környezetben reprodukálható erőforrások
- Adatbázis (relációs/NoSQL), cache, kapcsolódó rendszerek
(kifejezetten microservice környezetben)
- Adatbázis (relációs/NoSQL), cache, kapcsolódó rendszerek
- Saját fejlesztői környezettől való izoláció
- Docker image tartalmazza a teljes környezetet, függőségeket is
- Portabilitás (különböző környezeten futattható, pl. saját gép,
privát vagy publikus felhő)
- Docker Hub - publikus szolgáltatás image-ek megosztására
- Docker Compose - több konténer egyidejű kezelése
- Docker Swarm - natív cluster támogatás
- Docker Machine - távoli Docker környezetek üzemeltetéséhez
- Alkalmazás
- Dockerfile
- Image
- Konténer
docker version
docker run hello-world
docker run -p 8080:80 nginx
docker run -d -p 8080:80 nginx
docker ps
docker stop 517e15770697
docker run -d -p 8080:80 --name nginx nginx
docker stop nginx
docker ps -a
docker start nginx
docker logs -f nginx
docker stop nginx
docker rm nginx
Használható az azonosító első n karaktere is, amely egyedivé teszi
docker images
docker rmi nginx
docker run --name myubuntu -d ubuntu tail -f /dev/null
docker exec -it myubuntu bash
class: inverse, center, middle
Dockerfile
fájl tartalma:
FROM eclipse-temurin:17
WORKDIR app
COPY target/*.jar employees.jar
ENTRYPOINT ["java", "-jar", "employees.jar"]
Parancsok:
docker build -t employees .
docker run -d -p 8080:8080 employees
- Fabric8
- Alternatíva: Spotify dockerfile-maven, Google JIB Maven plugin
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.32.0</version>
<!-- ... -->
</plugin>
.small-code-14[
<configuration>
<verbose>true</verbose>
<images>
<image>
<name>employees</name>
<build>
<dockerFileDir>${project.basedir}/src/main/docker/</dockerFileDir>
<assembly>
<descriptorRef>artifact</descriptorRef>
</assembly>
<tags>
<tag>latest</tag>
<tag>${project.version}</tag>
</tags>
</build>
<run>
<ports>8080:8080</ports>
</run>
</image>
</images>
</configuration>
]
FROM eclipse-temurin:17
WORKDIR app
COPY maven/${project.artifactId}-${project.version}.jar employees.jar
ENTRYPOINT ["java", "-jar", "employees.jar"]
Property placeholder
mvnw package docker:build
mvnw docker:start
mvnw docker:stop
A docker:stop
törli is a konténert
- Nagyon gyorsan induljanak és álljanak le
- Graceful shutdown
- Ne legyen inkonzisztens adat
- Batch folyamatoknál: megszakíthatóvá, újraindíthatóvá (reentrant)
- Tranzakciókezeléssel
- Idempotencia
class: inverse, center, middle
docker image inspect employees
- Külön változó részeket külön layerbe tenni
- Operációs rendszer, JDK, libraries, alkalmazás saját fejlesztésű része külön
layerbe kerüljön
- Jar fájlt ki kell csomagolni, úgy is futtatható
BOOT-INF/lib
- függőségekMETA-INF
- leíró állományokBOOT-INF/classes
- alkalmazás saját fájljai
java -cp BOOT-INF\classes;BOOT-INF\lib\* training.employees.EmployeesApplication
FROM eclipse-temurin:17 as builder
WORKDIR app
COPY target/*.jar employees.jar
RUN jar xvf employees.jar
FROM eclipse-temurin:17
WORKDIR app
COPY --from=builder app/BOOT-INF/lib lib
COPY --from=builder app/META-INF META-INF
COPY --from=builder app/BOOT-INF/classes classes
ENTRYPOINT ["java", "-cp", "classes:lib/*", \
"training.employees.EmployeesApplication"]
- Spring 2.3.0.M2-től
- Layered JAR-s
- Buildpacks
- A JAR felépítése legyen layered
- Ki kell csomagolni
- Létrehozni a Docker image-t
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
java -Djarmode=layertools -jar target/employees-0.0.1-SNAPSHOT.jar list
java -Djarmode=layertools -jar target/employees-0.0.1-SNAPSHOT.jar extract
FROM eclipse-temurin:17 as builder
WORKDIR app
COPY target/*.jar employees.jar
RUN java -Djarmode=layertools -jar employees.jar extract
FROM eclipse-temurin:17
WORKDIR app
COPY --from=builder app/dependencies/ ./
COPY --from=builder app/spring-boot-loader/ ./
COPY --from=builder app/snapshot-dependencies/ ./
COPY --from=builder app/application/ ./
ENTRYPOINT ["java", \
"org.springframework.boot.loader.JarLauncher"]
- Dockerfile-hoz képest magasabb absztrakciós szint (Cloud Foundry vagy Heroku)
- Image készítése közvetlen Maven-ből vagy Grade-ből
- Alapesetben Java 11, spring-boot-maven-plugin konfigurálandó
BP_JAVA_VERSION
értéke13.0.2
mvnw spring-boot:build-image
docker run -d -p 8080:8080 --name employees employees:0.0.1-SNAPSHOT
docker logs -f employees
- Az alkalmazás nem függhet az őt futtató környezetre telepített
semmilyen csomagtól - Függőségeket explicit deklarálni kell
- Nem a függőségek közé soroljuk a háttérszolgáltatásokat,
mint pl. adatbázis, mail szerver, cache szerver, stb. - Docker és Maven/Gradle segít
- Egybe kell csomagolni a függőségekkel,
hiszen a futtató környezetben szükség van rá - Függőségek ritkábban változnak: Docker layers
- Vigyázni az ismételhetőségre: ne használjunk
intervallumokat!
class: inverse, center, middle
- Új GitHub repository létrehozás a webes felületen
git init
git add .
git commit -m "First commit"
git remote add origin https://github.com/username/employees.git
git push -u origin master
- Egy alkalmazás, egy repository
- A többi függőségként definiálandó
- Gyakori megsértés:
- Modularizált fejlesztésnél tűnhet ez jó ötletnek a modulokat
külön repository-ban tartani: nagyon megbonyolítja a buildet - Külön repository, de ugyanazon üzleti
domainen dolgozó különböző alkalmazás darabok - Egy repository, különböző alkalmazások
- Modularizált fejlesztésnél tűnhet ez jó ötletnek a modulokat
- A különböző környezetekre telepített példányoknál
alapvető igény, hogy tudjuk,
hogy mely verzióból készült
(felületen, logban látható legyen)
- Figyelni a Conway törvényre: "azok a szervezetek, amelyek
rendszereket terveznek, ... kénytelenek olyan terveket készíteni,
amelyek saját kommunikációs struktúrájuk másolatai" - Egy codebase, több team - ellentmond a microservice elképzelésnek
- Lehetséges megosztás:
- Library - függőségként felvenni
- Microservice
class: inverse, center, middle
- Roy Fielding: Architectural Styles and the Design of Network-based
Software Architectures, 2000 - Representational state transfer
- Alkalmazás erőforrások gyűjteménye,
melyeken CRUD műveleteket lehet végezni - HTTP protokoll erőteljes használata:
URI, metódusok, státuszkódok - JSON formátum használata
- Egyszerűség, skálázhatóság, platformfüggetlenség
- Richardson Maturity Model
@RestController
, mintha minden metóduson@ResponseBody
annotáció- Alapértelmezetten JSON formátumba
@RequestMapping
annotation helyett@GetMapping
,@PostMapping
, stb.
@RestController
@RequestMapping("/api/employees")
public class EmployeesController {
private final EmployeesService employeesService;
public EmployeesController(EmployeesService employeesService) {
this.employeesService = employeesService;
}
@GetMapping
public List<EmployeeDto> listEmployees() {
return employeesService.listEmployees();
}
}
- Boilerplate kódok generálására, pl. getter/setter, konstruktor,
toString()
,
equals/hashcode, logger, stb. - Annotation processor
- IntelliJ IDEA támogatás: plugin és Enable annotation processing
@Data
annotáció@ToString
,@EqualsAndHashCode
,@Getter
minden attribútumon,@Setter
nem final attribútumon és@RequiredArgsConstructor
@NoArgsConstructor
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Employee {
private long id;
private String name;
public Employee(String name) {
this.name = name;
}
}
- Object mapping
- Hasonló struktúrájú osztályú példányok konvertálására
(pl. entitások és DTO-k között) - Reflection alapú, intelligens alapértékekkel
- Fluent mapping API speciális esetek kezelésére
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>${modelmapper.version}</version>
</dependency>
@Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
Employee employee = // load
EmployeeDto dto = modelMapper.map(employee, EmployeeDto.class);
List<Employee> employees = // load
java.lang.reflect.Type targetListType = new TypeToken<List<EmployeeDto>>() {}.getType();
List<EmployeeDto> dtos = modelMapper.map(employees, targetListType);
class: inverse, center, middle
@RequestParam
annotációval- Kötelező, kivéve a
required = "false"
attribútum megadásakor - Automatikus típuskonverzió
public List<EmployeeDto> listEmployees(@RequestParam Optional<String> prefix) {
return employeesService.listEmployees(prefix);
}
Elérhető a /api/employees?prefix=Jack
címen
- Létrehozni egy objektumot
- Nem szükséges annotáció
public List<EmployeeDto> listEmployees(QueryParameters parameters) {
return employeesService.listEmployees(parameters);
}
@Data
public class QueryParameters {
private String prefix;
private String postfix;
}
- Osztályon lévő
@RequestMapping
és@GetMapping
összeadódik
@GetMapping("/{id}")
public EmployeeDto findEmployeeById(@PathVariable("id") long id) {
return employeesService.findEmployeeById(id);
}
Elérhető a /api/employees/1
címen
- Csak egy kérés időtartamáig van állapot
- Nem baj, ha elveszik
- Nem kell cluster-ezni
- Kevesebb erőforrás
- Nincs párhuzamossági probléma
- Kérések közötti állapot: backing services
- Cache: inkább backing service, ne nőjön szignifikánsan
az alkalmazás memóriaigénye - Shared nothing: egy update csak egy node hatóköre
- Ha állapotmentesen dolgozunk, nem okoz problémát
- Horizontális skálázás
- Backing service szintre kerül
class: inverse, center, middle
- PUT idempotens
@RequestBody
annotáció - deszerializáció, alapból JSON-ből Jacksonnel
@PostMapping
public EmployeeDto createEmployee(
@RequestBody CreateEmployeeCommand command) {
return employeesService.createEmployee(command);
}
@PutMapping("/{id}")
public EmployeeDto updateEmployee(
@PathVariable("id") long id,
@RequestBody UpdateEmployeeCommand command) {
return
employeesService.updateEmployee(id, command);
}
@DeleteMapping("/{id}")
public void deleteEmployee(@PathVariable("id") long id) {
employeesService.deleteEmployee(id);
}
class: inverse, center, middle
ResponseEntity
visszatérési típus: státuszkód, header, body, stb.
@GetMapping("/{id}")
public ResponseEntity findEmployeeById(@PathVariable("id") long id) {
try {
return ResponseEntity.ok(employeesService.findEmployeeById(id));
}
catch (IllegalArgumentException iae) {
return ResponseEntity.notFound().build();
}
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public EmployeeDto createEmployee(
@RequestBody CreateEmployeeCommand command) {
return employeesService.createEmployee(command);
}
@PostMapping
public ResponseEntity<EmployeeDto> createEmployee(@RequestBody CreateEmployeeCommand command,
UriComponentsBuilder uri) {
EmployeeDto employeeDto = employeeService.createEmployee(command);
return ResponseEntity
.created(uri.path("/api/employees/{id}").buildAndExpand(employeeDto.getId()).toUri())
.body(employeeDto);
}
Unit teszt:
UriComponentsBuilder builder = mock(UriComponentsBuilder.class);
UriComponents components = mock(UriComponents.class);
when(builder.path(any())).thenReturn(builder);
when(builder.buildAndExpand(anyLong())).thenReturn(components);
EmployeeDto employeeDto = employeesController
.createEmployee(new CreateEmployeeCommand("John Doe"), builder)
.getBody();
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteEmployee(@PathVariable("id") long id) {
employeesService.deleteEmployee(id);
}
- Status code 500
{
"timestamp": 1596570258672,
"status": 500,
"error": "Internal Server Error",
"message": "",
"path": "/api/employees/3"
}
- Servlet szabvány szerint
web.xml
állományban - Exceptionre tehető
@ResponseStatus
annotáció - Globálisan
ExceptionResolver
osztályokkal @ExceptionHandler
annotációval ellátott metódus a controllerben@ControllerAdvice
annotációval ellátott globális@ExceptionHandler
annotációval ellátott metódus
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public void handleNotFound() {
System.out.println("Employee not found");
}
- Problem Details for HTTP APIs
application/problem+json
mime-type
{
"type": "employees/invalid-json-request",
"title": "JSON error",
"status": 400,
"detail": "JSON parse error: Unexpected character..."
}
type
: URI, mely azonosítja a hiba típusáttitle
: ember által olvasható üzenetstatus
: http státuszkóddetail
: részletek, ember által olvashatóinstance
: URI, mely azonosítja a hibát,
és később is elérhető (pl. valamilyen log hivatkozás)- Egyedi saját mezők definiálhatók
<dependency>
<groupId>org.zalando</groupId>
<artifactId>problem</artifactId>
<version>${problem.version}</version>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>jackson-datatype-problem</artifactId>
<version>${problem.version}</version>
</dependency>
@ExceptionHandler({IllegalArgumentException.class})
public ResponseEntity<Problem> handleNotFound(IllegalArgumentException e) {
Problem problem = Problem.builder()
.withType(URI.create("employees/employee-not-found"))
.withTitle("Not found")
.withStatus(Status.NOT_FOUND)
.withDetail(e.getMessage())
.build();
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
.body(problem);
}
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper()
.findAndRegisterModules();
}
- Integrált Spring MVC kivételkezelés és Problem 3rd-party library
<dependency>
<groupId>org.zalando</groupId>
<artifactId>problem-spring-web-starter</artifactId>
<version>0.26.2</version>
</dependency>
- Beépítetten több kivételt kezel
- Vagy a
AbstractThrowableProblem
kivételtől származik a saját kivétel osztályunk - Saját
AdviceTrait
implementálása, mely sajátProblem
példányt hoz létre saját kivétel osztályunk esetén
public class EmployeeNotFoundException extends AbstractThrowableProblem {
private static final URI TYPE
= URI.create("employees/employee-not-found");
public EmployeeNotFoundException(Long id) {
super(
TYPE,
"Not found",
Status.NOT_FOUND,
String.format("Employee with id '%d' not found", id));
}
}
@ControllerAdvice
public class EmployeesExceptionHandler implements ProblemHandling {
@ExceptionHandler
ResponseEntity<Problem> handleException(EmployeeNotFoundException exception,
NativeWebRequest request) {
Problem problem =
Problem.builder()
.withType(URI.create("employees/employee-not-found"))
.withTitle("Not found")
.withStatus(Status.NOT_FOUND)
.withDetail(exception.getMessage())
.build();
return this.create(exception, problem, request);
}
}
class: inverse, center, middle
- Elindítható csak a Spring MVC réteg:
@SpringBootTest
helyett@WebMvcTest
annotáció használata - Service réteg mockolható Mockitoval,
@MockBean
annotációval MockMvc
injektálható- Kérések összeállítására (path variable, paraméterek, header, stb.)
- Válasz ellenőrzésére (státuszkód, header, tartalom)
- Válasz naplózására
- Válasz akár Stringként, JSON dokumentumként
(jsonPath)
- Nem indít valódi konténert, a Servlet API-t mockolja
- JSON szerializáció
.small-code-14[
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@Test
void testListEmployees() throws Exception {
when(employeesService.listEmployees(any())).thenReturn(List.of(
new EmployeeDto(1L, "John Doe"),
new EmployeeDto(2L, "Jane Doe")
));
mockMvc.perform(get("/api/employees"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].name", equalTo("John Doe")));
}
]
@SpringBootTest
és@AutoConfigureMockMvc
annotáció
@Test
void testListEmployees() throws Exception {
mockMvc.perform(get("/api/employees"))
.andExpect(status().isOk())
.andDo(print())
.andExpect(
jsonPath("$[0].name", equalTo("John Doe")));
}
RANDOM_PORT
- Port
@LocalServerPort
annotációval injektálható - Injektálható
TestRestTemplate
- url és port előre beállítva - JSON szerializáció és deszerializáció
@SpringBootTest(webEnvironment =
SpringBootTest.WebEnvironment.RANDOM_PORT)
@Test
void testListEmployees() {
List<EmployeeDto> employees =
restTemplate.exchange("/api/employees",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<EmployeeDto>>(){})
.getBody();
assertThat(employees)
.extracting(EmployeeDto::getName)
.containsExactly("John Doe", "Jane Doe");
}
class: inverse, center, middle
- Spring Framework 5.0 vezette be alapvetően WebFlux integrációs tesztekhez, de működik Spring MVC-vel is
- Fluent API assertionök írásához
- Szükséges a
org.springframework.boot:spring-boot-starter-webflux
függőség - Spring MVC esetén nem használható, csak valós konténerrel
- Metódus, uri, kérés törzse és státuszkód
webClient.post().uri("/api/employees")
.bodyValue(new CreateEmployeeCommand(("John Doe")))
.exchange()
.expectStatus().isCreated()
A post()
mellett get()
, put()
és delete()
metódusok
Path variable (URI variable)
webClient.get().uri("/api/employees/{id}", 1)
Request parameter (query parameter)
webClient.get().uri(builder -> builder.path("/api/employees").queryParam("prefix", "j").build())
Paraméterként Function
.expectBody(String.class).value(s -> System.out.println(s))
Paraméterként Consumer
JSON Path
.expectBody().jsonPath("name").isEqualTo("John Doe");
.expectBody(EmployeeDto.class).value(e -> assertEquals("John Doe", e.getName()));
Lista
.expectBodyList(EmployeeDto.class).hasSize(2).contains(new EmployeeDto(1L, "John Doe"));
class: inverse, center, middle
- API dokumentáció generálására
- Az API ki is próbálható
- OpenAPI Specification (eredetileg Swagger Specification)
- RESTful webszolgáltatások leírására
- Kód és dokumentáció generálás
- Programozási nyelv független
- JSON/YAML formátumú
- JSON Scheman alapul
- Keretrendszer független
- Annotációkkal személyre szabható
- Swagger UI automatikus elindítása a
/swagger-ui.html
címen - OpenAPI elérhetőség a
/v3/api-docs
címen (vagy/v3/api-docs.yaml
)
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>${springdoc-openapi-ui.version}</version>
</dependency>
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Employees API")
.version("1.0.0")
.description("Operations with employees"));
}
- Figyelembe veszi a Bean Validation annotációkat
public class CreateEmployeeCommand {
@Schema(description="name of the employee", example = "John Doe")
private String name;
}
- Figyelembe veszi a Spring MVC annotációkat
@RestController
@RequestMapping("/api/employees")
@Tag( name = "Operations on employees")
public class EmployeesController {
@GetMapping("/{id}")
@Operation(summary = "Find employee by id",
description = "Find employee by id.")
@ApiResponse(responseCode = "404",
description = "Employee not found")
public EmployeeDto findEmployeeById(
@Parameter(description = "Id of the employee",
example = "12")
@PathVariable("id") long id) {
// ...
}
}
- Contract first alapjain
- Laza csatolás
- Webes és mobil GUI és az üzleti logika is ide tartozik
- Dokumentálva és tesztelve legyen
- API Blueprint: Markdown alapú formátum API dokumentálására
class: inverse, center, middle
- Keretrendszer független eszköz REST API tesztelésére
- Dinamikus nyelvek egyszerűségét próbálja hozni Java nyelven
- JSON, mint szöveg, vagy objektum mapping (Jackson, Gson, JAXB, stb.)
- Megadható
- Path, parameter, header, cookie, content-type, stb.
- Sokszínű assertek
- Támogatja a különböző autentikációs módokat
- XML tartalomra XmlPath, GPath (Groovy-ból, hasonló az XPath-hoz)
- DTD és XSD validáció
- JSON tartalomra JSONPath-szal
- JSON Schema validáció
- Header, status, cookie, content-type
- Response time
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-path</artifactId>
<version>${rest-assured.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>xml-path</artifactId>
<version>${rest-assured.version}</version>
<scope>test</scope>
</dependency>
Csak JSON használata esetén is kell mindkét függőség
- Un. RestAssuredMockMvc API
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>${rest-assured.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>spring-mock-mvc</artifactId>
<version>${rest-assured.version}</version>
<scope>test</scope>
</dependency>
import io.restassured.http.ContentType;
import io.restassured.module.mockmvc.RestAssuredMockMvc;
import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;
import static io.restassured.module.mockmvc.RestAssuredMockMvc.*;
import static org.hamcrest.Matchers.equalTo;
@Autowired
WebApplicationContext webApplicationContext;
@BeforeEach
void init() {
RestAssuredMockMvc.requestSpecification = given()
.contentType(ContentType.JSON)
.accept(ContentType.JSON);
RestAssuredMockMvc
.webAppContextSetup(webApplicationContext);
}
@Test
void testCreateEmployeeThenListEmployees() {
with().body(new CreateEmployeeCommand("Jack Doe")).
when()
.post("/api/employees")
.then()
.body("name", equalTo("Jack Doe"));
when()
.get("/api/employees")
.then()
.body("[0].name", equalTo("Jack Doe"));
}
class: inverse, center, middle
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Employees",
"type": "array",
"items": [
{
"title": "EmployeeDto",
"type": "object",
"required": ["name", "id"],
"properties": {
"id": {
"type": "integer",
"description": "id of the employee",
"format": "int64",
"example": 12
},
"name": {
"type": "string",
"description": "name of the employee",
"example": "John Doe"
}}}]}
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-schema-validator</artifactId>
<scope>test</scope>
</dependency>
when()
.get("/api/employees")
.then()
.body(matchesJsonSchemaInClasspath("employee-dto-schema.json"));
class: inverse, center, middle
- Mechanizmus, mely lehetővé teszi a kliens számára,
hogy az erőforrás megjelenítési formái közül válasszon, pl.- JSON vagy XML (
Accept
fejléc és Mime Type) - GIF vagy JPEG
- Nyelv (
Accept-Language
fejléc alapján)
- JSON vagy XML (
- Controllerben
@RequestMapping(value = "/api/employees",
produces = {MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE})
pom.xml
-ben
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>
- Dto-ban
@XmlRootElement
@Data
@NoArgsConstructor
@AllArgsConstructor
@XmlRootElement(name = "employees")
@XmlAccessorType(XmlAccessType.FIELD)
public class EmployeesDto {
@XmlElement(name = "employee")
private List<EmployeeDto> employees;
}
class: inverse, center, middle
- Bean Validation 2.0 (JSR 380) támogatás
- Ne réteghez legyen kötve, hanem az adatot hordozó beanhez
- Attribútumokra annotáció
- Beépített annotációk
- Saját annotáció implementálható
- Megadható metódus paraméterekre és visszatérési értékre is
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
@AssertFalse
,@AssertTrue
@Null
,@NotNull
@Size
@Max
,@Min
,@Positive
,@PositiveOrZero
,@Negative
,@NegativeOrZero
@DecimalMax
,@DecimalMin
@Digits
@Future
,@Past
,@PastOrPresent
,@FutureOrPresent
@Pattern
@Email
@NotEmpty
,@NotBlank
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateEmployeeCommand {
@NotNull(message = "Name can not be null")
private String name;
}
@PostMapping
public EmployeeDto createEmployee(
@Valid @RequestBody CreateEmployeeCommand command) {
return employeesService.createEmployee(command);
}
{
"timestamp": 1596570707472,
"status": 400,
"error": "Bad Request",
"message": "",
"path": "/api/employees"
}
.small-code-14[
@ExceptionHandler({MethodArgumentNotValidException.class})
public ResponseEntity<Problem> handleValidationError(MethodArgumentNotValidException e) {
List<Violation> violations = e.getBindingResult().getFieldErrors().stream()
.map((FieldError fe) -> new Violation(fe.getField(), fe.getDefaultMessage()))
.collect(Collectors.toList());
Problem problem = Problem.builder()
.withType(URI.create("employees/validation-error"))
.withTitle("Validation error")
.withStatus(Status.BAD_REQUEST)
.withDetail(e.getMessage())
.with("violations", violations)
.build();
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
.body(problem);
}
]
@Data
@AllArgsConstructor
public class Violation {
private String field;
private String defaultMessage;
}
{
"type": "employees/validation-error",
"title": "Validation error",
"status": 400,
"detail": "Validation failed for argument [0] in public ...",
"violations": [
{
"field": "name",
"message": "Name can not be null"
}
]
}
{
"type": "https://zalando.github.io/problem/constraint-violation",
"status": 400,
"violations": [
{
"field": "name",
"message": "Name can not be null"
}
],
"title": "Constraint Violation"
}
class: inverse, center, middle
@Constraint(validatedBy = NameValidator.class)
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
public @interface Name {
String message() default "Invalid name";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int maxLength() default 50;
}
public class NameValidator implements ConstraintValidator<Name, String> {
private int maxLength;
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value != null &&
!value.isBlank() &&
value.length() > 2 &&
value.length() <= maxLength &&
Character.isUpperCase(value.charAt(0));
}
@Override
public void initialize(Name constraintAnnotation) {
maxLength = constraintAnnotation.maxLength();
}
}
class: inverse, center, middle
- Konfiguráció alkalmazáson kívül szervezése,
hogy ugyanazon alkalmazás több környezetben is tudjon futni - Spring
Environment
absztrakcióra épül,PropertySource
implementációk
hierarchiája, melyek különböző helyekről töltenek be property-ket - Majdnem húsz forrása a property-knek
(a magasabb prioritásúak felülírják a később szereplőket) - Leggyakoribb az
application.properties
fájl - YAML formátum is használható
- Az elöl szereplők felülírják a később szereplőket
- Legfontosabbak:
- Parancssori paraméterek
- Operációs rendszer környezeti változók
application.properties
állomány a jar fájlon kívül
(/config
könyvtár, vagy közvetlenül a jar mellett)application.properties
állomány a jar fájlon belül
@Service
public class HelloService {
private String hello;
public HelloService(@Value("${employees.hello}") String hello) {
this.hello = hello;
}
public String sayHello() {
return hello + " " + LocalDateTime.now();
}
}
employees.hello = Hello Spring Boot Config
- Több, esetleg hierarchikus property-k esetén
@ConfigurationProperties(prefix = "employees")
@Data
public class HelloProperties {
private String hello;
}
- Regisztrálni kell a
@EnableConfigurationProperties(HelloProperties.class)
annotációval, pl. a service-en - Használat helyén injektálható
- Setteren keresztül, de használható a
@ConstructorBinding
, ekkor konstruktoron keresztül - Relaxed binding: nem kell pontos egyezőség
- Használható a
@Validated
Spring annotáció,
(majd használható a Bean Validation) - A property-ket definiálni lehet külön állományban,
ekkor felismeri az IDE
META-INF/additional-spring-configuration-metadata.json
- Százas nagyságrendben
- Spring Boot Reference Documentation:
Appendix A: Common Application properties
server.port
- Konfigurálható legyen a port, ahol elindul
- Két alkalmazás ne legyen telepítve ugyanarra a web konténerre,
alkalmazásszerverre
docker run -d -p 8080:8081 -e SERVER_PORT=8081 -e EMPLOYEES_HELLO=HelloDocker employees
- Környezetenként eltérő értékek
- Pl. backing service-ek elérhetőségei
- Ide tartoznak a jelszavak, titkos kulcsok,
melyeket különös figyelemmel kell kezelni - Konfigurációs paraméterek a környezet részét képezzék,
és ne az alkalmazás részét - Konfigurációs paraméterek környezeti változókból jöjjenek
- Kerüljük az alkalmazásban a környezetek nevesítését
- Nem kerülhetnek a kód mellé a verziókezelőbe
(csak a fejlesztőkörnyezet default beállításai) - Verziókezelve legyen, ki, mikor mit módosított
- Lásd még Spring Cloud Config
class: inverse, center, middle
- Spring belül a Commons Loggingot használja
- Előre be van konfigurálva a Java Util Logging, Log4J2, és Logback
- Alapesetben konzolra ír
- Naplózás szintje, és fájlba írás is állítható
azapplication.properties
állományban
- SLF4J használata
- Lombok használata
- Paraméterezett üzenet
private static final org.slf4j.Logger log =
org.slf4j.LoggerFactory.getLogger(LogExample.class);
@Slf4j
log.info("Employee has been created");
log.debug("Employee has been created with name {}",
employee.getName());
application.properties
: szint, fájl- Használható logger library specifikus konfigurációs fájl (pl.
logback.xml
)
logging.level.training = debug
- Time ordered event stream
- Nem az alkalmazás feladata a napló irányítása a megfelelő helyre,
vagy a napló tárolása, kezelése, archiválása, görgetése, stb. - Írjon konzolra
- Központi szolgáltatás: pl. ELK, Splunk, hiszen
az alkalmazás node-ok bármikor eltűnhetnek
class: inverse, center, middle
- Futásidőben funkciók be- és kikapcsolására
- Continuous integration miatt, feature branchek csökkentésére
- Merge conflict minimalizálására
- Blue/green deployment támogatására: bekapcsolni csak ha minden node új verziót futtat
- Canary release: csak a felhasználók egy részének bekapcsolni
- Dark Launch: beérkező kérések csak egy százalékának bekapcsolni, figyelni a rendszer viselkedését
- A/B tesztelés: két különböző megvalósítás tesztelésére
- Circuit Breaker: problémát/terhelést okozó funkció kikapcsolása
- FF4J egy Javas Feature toggles implementáció
- Támogatja a szerepkör szerinti szétválasztást, akár Spring Security keretrendszerrel
- AOP támogatás
- Monitorozás
- Auditálható
- Parancssori, JMX, REST, webes interfész
- Választható adatbázis és cache implementációk
- Saját stratégiákkal bővíthető
- Spring Boot integráció
<dependency>
<groupId>org.ff4j</groupId>
<artifactId>ff4j-spring-boot-starter</artifactId>
<version>${ff4j.version}</version>
</dependency>
<dependency>
<groupId>org.ff4j</groupId>
<artifactId>ff4j-web</artifactId>
<version>${ff4j.version}</version>
<exclusions>
<exclusion>
<groupId>javax.servlet.jsp.jstl</groupId>
<artifactId>jstl-api</artifactId>
</exclusion>
</exclusions>
</dependency>
@Service
public class EmployeesService {
private FF4j ff4j;
@PostConstruct
public void init() {
ff4j.createFeature(new Feature(FEATURE_CHECK_UNIQUE));
}
public EmployeeDto createEmployee(CreateEmployeeCommand command) {
if (ff4j.check(FEATURE_CHECK_UNIQUE)) {
// Kapcsolható funkció
}
// ...
}
}
REST API
### Get feature
GET http://localhost:8080/api/ff4j/store/features/checkUnique
### Enable feature
POST http://localhost:8080/api/ff4j/store/features/checkUnique/enable
Content-Type: application/json
### Disable feature
POST http://localhost:8080/api/ff4j/store/features/checkUnique/disable
Content-Type: application/json
class: inverse, center, middle
- JDBC túl bőbeszédű
- Elavult kivételkezelés
- Egy osztály, üzenet alapján megkülönböztethető
- Checked
- Boilerplate kódok eliminálására template-ek
- Adatbáziskezelés SQL-lel
org.springframework.boot:spring-boot-starter-jdbc
függőség- Embedded adatbázis támogatás, automatikus
DataSource
konfiguráció- Pl H2:
com.h2database:h2
- Developer Tools esetén elérhető webes konzol a
/h2-console
címen
- Pl H2:
- Injektálható
JdbcTemplate
- Service delegál a Repository felé
jdbcTemplate.update(
"insert into employees(emp_name) values ('John Doe')");
jdbcTemplate.update(
"insert into employees(emp_name) values (?)", "John Doe");
jdbcTemplate.update(
"update employees set emp_name = ? where id = ?", "John Doe", 1);
jdbcTemplate.update(
"delete from employees where id = ?", 1);
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(
con -> {
PreparedStatement ps =
con.prepareStatement("insert into employees(emp_name) values (?)",
Statement.RETURN_GENERATED_KEYS);
ps.setString(1, employee.getName());
return ps;
}, keyHolder);
employee.setId(keyHolder.getKey().longValue());
List<Employee> employees = jdbcTemplate.query(
"select id, emp_name from employees",
this::convertEmployee);
Employee employee =
jdbcTemplate.queryForObject(
"select id, emp_name from employees where id = ?",
this::convertEmployee,
id);
private Employee convertEmployee(ResultSet resultSet, int i)
throws SQLException {
long id = resultSet.getLong("id");
String name = resultSet.getString("emp_name");
Employee employee = new Employee(id, name);
return employee;
}
.small-code-14[
@Component
@AllArgsConstructor
public class DbInitializer implements CommandLineRunner {
private JdbcTemplate jdbcTemplate;
@Override
public void run(String... args) throws Exception {
jdbcTemplate.execute("create table employees " +
"(id bigint auto_increment, emp_name varchar(255), " +
"primary key (id))");
jdbcTemplate.execute(
"insert into employees(emp_name) values ('John Doe')");
jdbcTemplate.execute(
"insert into employees(emp_name) values ('Jack Doe')");
}
}
]
class: inverse, center, middle
- Egyszerűbbé teszi a perzisztens réteg implementálását
- Tipikusan CRUD műveletek támogatására, olyan gyakori igények
megvalósításával, mint a rendezés és a lapozás - Interfész alapján repository implementáció generálás
- Query by example
- Ismétlődő fejlesztési feladatok redukálása, boilerplate kódok csökkentése
org.springframework.boot:spring-boot-starter-data-jpa
függőség- Entitás létrehozása
JpaRepository
kiterjesztése@Transactional
alkalmazása a service rétegbenapplication.properties
spring.jpa.show-sql=true
save(S)
,saveAll(Iterable<S>)
,saveAndFlush(S)
findById(Long)
,findOne(Example<S>)
,findAll()
különböző paraméterezésű metódusai (lapozás,Example
),findAllById(Iterable<ID>)
getOne(ID)
(nemOptional
példánnyal tér vissza)exists(Example<S>)
,existsById(ID)
count()
,count(Example<S>)
deleteById(ID)
,delete(S)
,
deleteAll()
üres ésIterable
paraméterezéssel,deleteAllInBatch()
,deleteInBatch(Iterable<S>)
flush()
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "employees")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "emp_name")
private String name;
public Employee(String name) {
this.name = name;
}
}
public interface EmployeesRepository extends JpaRepository<Employee, Long> {
@Query("select e from Employee e where upper(e.name) like upper(:name)")
List<Employee> findAllByPrefix(String name);
}
class: inverse, center, middle
docker run
-d
-e MYSQL_DATABASE=employees
-e MYSQL_USER=employees
-e MYSQL_PASSWORD=employees
-e MYSQL_ALLOW_EMPTY_PASSWORD=yes
-p 3306:3306
--name employees-mariadb
mariadb
pom.xml
-be:
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<scope>runtime</scope>
</dependency>
application.properties
konfiguráció
spring.datasource.url=jdbc:mariadb://localhost/employees
spring.datasource.username=employees
spring.datasource.password=employees
spring.jpa.hibernate.ddl-auto=create-drop
- Adatbázis (akár relációs, akár NoSQL), üzenetküldő middleware-ek,
directory és email szerverek, elosztott cache, Big Data eszközök, stb. - Microservice környezetben egy másik alkalmazás is
- Automatizált telepítés
- Infrastructure as Code, Ansible, Chef, Puppet
- Eléréseik, autentikációs paraméterek környezeti paraméterként
publikálódnak az alkalmazás felé - Fájlrendszer nem tekinthető megfelelő
háttérszolgáltatásnak - Beágyazható háttérszolgáltatások
- Redeploy nélkül megoldható legyen a kapcsolódás
- Circuit breaker: ha nem működik a szolgáltatás,
megszűnteti egy időre a hozzáférést (biztosíték)
class: inverse, center, middle
docker run
-d
-e POSTGRES_PASSWORD=password
-p 5432:5432
--name employees-postgres
postgres
pom.xml
-be:
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
application.properties
konfiguráció
spring.datasource.url=jdbc:postgresql:postgres
spring.datasource.username=postgres
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=create-drop
class: inverse, center, middle
- JPA repository-k tesztelésére
@DataJpaTest
annotáció, csak a repository réteget indítja el- Embedded adatbázis
- Tesztbe injektálható: JPA repository,
DataSource
,JdbcTemplate
,
EntityManager
- Minden teszt metódus saját tranzakcióban, végén rollback
- Service réteg már nem kerül elindításra
- Tesztelni:
- Entitáson lévő annotációkat
- Névkonvenció alapján generált metódusokat
- Custom query
@DataJpaTest
public class EmployeesRepositoryIT {
@Autowired
EmployeesRepository employeesRepository;
@Test
void testPersist() {
Employee employee = new Employee("John Doe");
employeesRepository.save(employee);
List<Employee> employees =
employeesRepository.findAllByPrefix("%");
assertThat(employees)
.extracting(Employee::getName)
.containsExactly("John Doe");
}
}
- Teljes alkalmazás tesztelése
- Valós adatbázis szükséges hozzá, gondoskodni kell az elindításáról
- Séma inicializáció és adatfeltöltés szükséges
src\test\resources\application.properties
fájlban
a teszteléshez használt DataSource
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=sa
spring.jpa.hibernate.ddl-auto
create-drop
alapesetben,
teszt lefutása végén eldobja a sémátcreate
-re állítva megmaradnak a táblák és adatok
- Ha van
schema.sql
a classpath-on, azt futtatja le - Flyway vagy Liquibase használata
data.sql
a classpath-on@Sql
annotáció használata a teszten- Programozott módon
- Teszt osztályban
@BeforeEach
vagy@AfterEach
annotációkkal megjelölt metódusokban - Publikus API-n keresztül
- Injektált controller, service, repository, stb. használatával
- Közvetlen hozzáférés az adatbázishoz
(pl.JdbcTemplate
)
- Teszt osztályban
- Csak külön adatokon dolgozunk - nehéz lehet a kivitelezése
- Teszteset maga előtt vagy után rendet tesz
- Állapot
- Teljes séma törlése, séma inicializáció
- Adatbázis import
- Csak (bizonyos) táblák ürítése
class: inverse, center, middle
docker network ls
docker network create --driver bridge employees-net
docker network inspect employees-net
docker run
-d
* -e SPRING_DATASOURCE_URL=jdbc:mariadb://employees-mariadb/employees
* -e SPRING_DATASOURCE_USERNAME=employees
* -e SPRING_DATASOURCE_PASSWORD=employees
-p 8080:8080
* --network employees-net
--name employees
employees
class: inverse, center, middle
<image>
<name>mariadb</name>
<alias>employees-mariadb</alias>
<run>
<env>
<MYSQL_DATABASE>employees</MYSQL_DATABASE>
<MYSQL_USER>employees</MYSQL_USER>
<MYSQL_PASSWORD>employees</MYSQL_PASSWORD>
<MYSQL_ALLOW_EMPTY_PASSWORD>yes</MYSQL_ALLOW_EMPTY_PASSWORD>
</env>
<ports>3306:3306</ports>
</run>
</image>
FROM adoptopenjdk:14-jre-hotspot
RUN apt update \
&& apt-get install wget \
&& apt-get install -y netcat \
&& wget https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh \
&& chmod +x ./wait-for-it.sh
RUN mkdir /opt/app
ADD maven/${project.artifactId}-${project.version}.jar /opt/app/employees.jar
CMD ["./wait-for-it.sh", "-t", "180", "employees-mariadb:3306", "--", "java", "-jar", "/opt/app/employees.jar"]
<image>
<!--- ... -->
<run>
<env>
<SPRING_DATASOURCE_URL>jdbc:mariadb://employees-mariadb/employees</SPRING_DATASOURCE_URL>
</env>
<ports>8080:8080</ports>
<links>
<link>employees-mariadb:employees-mariadb</link>
</links>
<dependsOn>
<container>employees-mariadb</container>
</dependsOn>
</run>
</image>
class: inverse, center, middle
version: '3'
services:
employees-mariadb:
image: mariadb
restart: always
ports:
- '3306:3306'
environment:
MYSQL_DATABASE: employees
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' # aposztrófok nélkül boolean true-ként értelmezi
MYSQL_USER: employees
MYSQL_PASSWORD: employees
employees-app:
image: employees
ports:
- "8080:8080"
restart: always
depends_on:
- employees-mariadb
environment:
SPRING_DATASOURCE_URL: 'jdbc:mariadb://employees-mariadb:3306/employees'
command: ["./wait-for-it.sh", "-t", "120", "employees-mariadb:3306", "--", "java", "-jar", "/opt/app/employees.jar"]
class: inverse, center, middle
<profile>
<id>startdb</id>
<properties>
<docker.filter>mariadb</docker.filter>
</properties>
<build>
<plugins>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<executions>
<execution>
<id>start</id>
<phase>pre-integration-test</phase>
<goals>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>stop</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
class: inverse, center, middle
- Adatbázis séma létrehozása (táblák, stb.)
- Változások megadása
- Metadata table alapján
- SQL/XML leírás
- Platform függetlenség
- Lightweight
- Visszaállás korábbi verzióra
- Indítás paranccssorból, alkalmazásból
- Cluster támogatás
- Placeholder támogatás
- Modularizáció
- Több séma támogatása
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
Hibernate séma inicializálás kikapcsolás az
application.properties
állományban:
spring.jpa.hibernate.ddl-auto=none
src/resources/db/migration/V1__employees.sql
állomány
create table employees (id bigint auto_increment,
emp_name varchar(255), primary key (id));
insert into employees (emp_name) values ('John Doe');
insert into employees (emp_name) values ('Jack Doe');
flyway_schema_history
tábla
src/resources/db/migration/V1__employees.sql
állomány
create table employees (id int8 generated by default as identity,
emp_name varchar(255), primary key (id));
insert into employees (emp_name) values ('John Doe');
insert into employees (emp_name) values ('Jack Doe');
flyway_schema_history
tábla
class: inverse, center, middle
pom.xml
-ben
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
application.properties
állományban
spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.xml
A db.changelog-master.xml
fájl:
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<changeSet id="create-employee-table" author="vicziani">
<sqlFile path="create-employee-table.sql"
relativeToChangelogFile="true" />
</changeSet>
</databaseChangeLog>
create-employee-table.sql
fájl MariaDB esetén:
create table employees (id bigint not null auto_increment, emp_name varchar(255), primary key (id));
create-employee-table.sql
fájl PostgreSQL esetén:
create table employees (id int8 generated by default as identity, emp_name varchar(255),
primary key (id));
class: inverse, center, middle
docker run -d -p27017:27017 --name employees-mongo mongo
application.properties
fájlban:
spring.data.mongodb.database = employees
pom.xml
függőség
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
@Data
@NoArgsConstructor
@AllArgsConstructor
* @Document("employees")
public class Employee {
* @Id
private String id;
private String name;
public Employee(String name) {
this.name = name;
}
}
public interface EmployeesRepository extends MongoRepository<Employee, String> {
@Query("{ 'name': { $regex: ?0, $options: 'i'} }")
List<Employee> findAll(String name);
}
id
átírásaString
típusra: Dto, Controller metódus paraméterek, Service metódus paraméterekEmployeesService
public EmployeeDto updateEmployee(String id, UpdateEmployeeCommand command) {
Employee employee = employeesRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Employee not found"));
employee.setName(command.getName());
* employeesRepository.save(employee);
return modelMapper.map(employee, EmployeeDto.class);
}
docker exec -it employees-mongo mongosh employees
db.employees.find()
db.employees.insertOne({"name": "John Doe"})
db.employees.findOne({'_id': ObjectId('60780cf974bc5648cf220a96')})
db.employees.deleteOne({'_id': ObjectId('60780cf974bc5648cf220a96')})
class: inverse, center, middle
- Security-vel az elejétől foglalkozni kell
- Endpoint védelem
- Audit naplózás
- RBAC - role based access controll
- OAuth2
- Nyílt szabány erőforrás-hozzáférés kezelésére
- Elválik, hogy a felhasználó mit is akar igénybe venni,
és az, hogy hol jelentkezik be - Google, Facebook vagy saját szerver
- Szereplők
- Resource owner: aki hozzáfér az erőforráshoz, a szolgáltatáshoz,
humán esetben a felhasználó (de lehet alkalmazás is) - Client: a szoftver, ami hozzá akar férni a
felhasználó adataihoz - Authorization Server: ahol a felhasználó adatai
tárolva vannak, és ahol be tud lépni - Resource Server: ahol a felhasználó igénybe veszi
az erőforrásokat, a szolgáltatást
- Resource owner: aki hozzáfér az erőforráshoz, a szolgáltatáshoz,
- Grant Type:
- Authorization Code: klasszikus mód, ahogy egy webes alkalmazásba
lépünk Facebook vagy a Google segítségével - Implicit: mobil alkalmazások, vagy csak böngészőben futó alkalmazások
használják - Resource Owner Password Credentials: ezt olyan megbízható
alkalmazások használják, melyek maguk kérik be a jelszót - Client Credentials: ebben az esetben
nem a felhasználó kerül azonosításra,
hanem az alkalmazás önmaga
- Authorization Code: klasszikus mód, ahogy egy webes alkalmazásba
- A felhasználó elmegy az alkalmazás oldalára
- Az átirányít a Authorization Serverre (pl. Google vagy Facebook),
megadva a saját azonosítóját (client id), hogy hozzá szeretne férni
a felhasználó adataihoz - Az Authorization Serveren a felhasználó bejelentkezik
- Az Authorization Serveren a felhasználó jogosultságot ad az alkalmazásnak,
hogy hozzáférjen a felhasználó adataihoz - Az Authorization Server visszairányítja a felhasználót
az alkalmazás oldalára, url paraméterként átadva neki
egy úgynevezett authorization code-ot - Az alkalmazás a kapott authorization code-ot,
a saját azonosítóját (client id), az alkalmazáshoz
rendelt "jelszót" (client secret) felhasználva lekéri
az Authorization Servertől a felhasználóhoz tartozó
tokent, mely tartalmazza a felhasználó adatait
- Keycloak indítása Dockerben
docker run -e KEYCLOAK_USER=root -e KEYCLOAK_PASSWORD=root -p 8081:8080
--name keycloak jboss/keycloak
- Létre kell hozni egy Realm-et (
EmployeesRealm
) - Létre kell hozni egy klienst, amihez meg kell adni annak azonosítóját,
és hogy milyen url-en érhető el (employees-app
) - Létre kell hozni a szerepköröket (
employees_app_user
) - Létre kell hozni egy felhasználót (a Email Verified legyen On értéken, hogy be lehessen vele jelentkezni), beállítani a jelszavát (a Temporary értéke legyen Off, hogy ne kelljen jelszót módosítani),
valamint hozzáadni a szerepkört (johndoe
)
- Konfiguráció leírása:
.small-code-14[
http://localhost:8081/auth/realms/EmployeesRealm/.well-known/openid-configuration
]
- A következő címen lekérhető a tanúsítvány
.small-code-14[
http://localhost:8081/auth/realms/EmployeesRealm/protocol/openid-connect/certs
]
.small-code-14[
curl -s --data "grant_type=password&client_id=employees-app&username=johndoe&password=johndoe"
http://localhost:8081/auth/realms/EmployeesRealm/protocol/openid-connect/token | jq
]
.small-code-14[
POST http://localhost:8081/auth/realms/EmployeesRealm/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=password&client_id=employees-app&username=johndoe&password=johndoe
]
- A https://jws.io címen ellenőrizhető
Be kell írni egy létező scope-ot (pl. profile
), mert üreset nem tud értelmezni
- Spring Security OAuth deprecated
- Spring Security 5.2 majdnem teljes támogatás
- Authorization Server nélkül
- Külön Keycloak integráció
.small-code-14[
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-adapter-bom</artifactId>
<version>14.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
]
.small-code-14[
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
</dependency>
]
application.properties
:
.small-code-14[
keycloak.auth-server-url=http://localhost:8081/auth
keycloak.realm=EmployeesRealm
keycloak.resource=employees-app
keycloak.bearer-only=true
keycloak.security-constraints[0].authRoles[0]=employees_app_user
keycloak.security-constraints[0].securityCollections[0].patterns[0]=/*
keycloak.principal-attribute=preferred_username
]
GET http://localhost:8080/api/employees
Accept: application/json
Authorization: bearer eyJ...
@GetMapping
public List<EmployeeDto> employees(Principal principal) {
log.info("Logged in user: {}", principal.getName());
return employeeService.listEmployees();
}
class: inverse, center, middle
- Addresses alkalmazás elindítása Docker Hub alapján
docker run -d -p 8081:8080 --name my-addresses training360/addresses
- Addresses alkalmazás elindítása forrás alapján
mvnw package
docker build -t addresses .
docker run -d -p 8081:8080 --name my-addresses addresses
@Service
@Slf4j
public class AddressesGateway {
private final RestTemplate restTemplate;
private String url;
public AddressesGateway(RestTemplateBuilder builder,
@Value("${employees.addresses.url}") String url) {
restTemplate = builder.build();
this.url = url;
}
public AddressDto findAddressByName(String name) {
log.debug("Get address from Addresses application");
return restTemplate.getForObject(url, AddressDto.class, name);
}
}
employees.addresses.url = http://localhost:8081/api/addresses?name={name}
class: inverse, center, middle
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
@Service
public class AddressesGateway {
public AddressDto findAddressByName(String name) {
return WebClient.create("http://localhost:8081")
.get()
.uri(builder -> builder.path("/api/addresses").queryParam("name", name).build())
.retrieve()
.bodyToMono(AddressDto.class)
.block();
}
}
- Konfiguráció
@ConfigurationProperties
használatával - Saját annotáció
- Hibakezelés
- Thread-ek száma
- Timeout
.small-code-14[
String connectionProviderName = "myConnectionProvider";
HttpClient httpClient = HttpClient.create(ConnectionProvider.create(connectionProviderName, 5));
WebClient webClient = WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient))
.baseUrl(gatewayProperties.getUrl())
.build();
return webClient
.get()
.uri(builder -> builder.path("/api/addresses").queryParam("name", name).build())
.retrieve()
.bodyToMono(AddressDto.class)
.timeout(Duration.parse("PT5S"))
.onErrorReturn(new AddressDto())
.block();
]
class: inverse, center, middle
.small-code-14[
@RestClientTest(value = AddressesGateway.class,
properties = "employees.addresses.url = http://localhost:8080/api/addresses?name={name}")
public class AddressesGatewayRestTemplateIT {
@Autowired
AddressesGateway addressesGateway;
@Autowired
MockRestServiceServer server;
@Test
void testFindAddressByName() throws JsonProcessingException {
server.expect(requestTo(startsWith("http://localhost:8080/api/addresses")))
.andExpect(queryParam("name", "John%20Doe"))
.andRespond(withSuccess("{\"city\": \"Budapest\", \"address\": \"Andrássy u. 2.\"}"
, MediaType.APPLICATION_JSON));
AddressDto addressDto = addressesGateway.findAddressByName("John Doe");
assertEquals("Budapest", addressDto.getCity());
assertEquals("Andrássy u. 2.", addressDto.getAddress());
}
}
]
public class AddressesGatewayRestTemplateIT {
// ...
@Autowired
ObjectMapper objectMapper;
@Test
void testFindAddressByName() throws JsonProcessingException {
String json = objectMapper.writeValueAsString(new AddressDto("Budapest", "Andrássy u. 2."));
// ...
}
}
class: inverse, center, middle
- Eszköz HTTP-alapú, pl. REST API mockolásra
- Kapcsolódó rendszer helyettesítésére
- Megadható, hogy milyen URL-en milyen választ adjon vissza
(request matching, stubbing) - Ellenőrizni lehet, hogy milyen kérések mentek felé (verification)
- Szimulálható hibás működés (pl. státuszkódok, timeoutok)
- Futtatható önállóan, vagy JUnit tesztbe ágyazva
- Képes felvenni és visszajátszani kommunikációt
- REST és Java API
- Konfigurálható akár JSON állományokkal is
- Spring Boot integráció: Spring Cloud Contract WireMock
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<version>3.1.3</version>
<scope>test</scope>
</dependency>
.small-code-14[
@SpringBootTest
@AutoConfigureWireMock(port = 8081)
class AddressesGatewayWireMockIT {
@Autowired
AddressesGateway addressesGateway;
@Test
void testFindAddressByName() {
String resource = "/api/addresses";
stubFor(get(urlPathEqualTo("/api/addresses"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody("{\"city\": \"Budapest\", \"address\": \"Andrássy u. 2.\"}")));
Address address = gateway.findAddressByName("John Doe");
verify(getRequestedFor(urlPathEqualTo(resource))
.withQueryParam("name", equalTo("John Doe")));
assertThat(address.getCity()).isEqualTo("Budapest");
assertThat(address.getAddress()).isEqualTo("Andrássy u. 2.");
}
}
]
class: inverse, center, middle
- Rendszerek közötti üzenetküldés
- Megbízható üzenetküldés: store and forward
- Következő esetekben alkalmazható hatékonyan
- Hívott fél megbízhatatlan
- Kommunikációs csatorna megbízhatatlan
- Hívott fél lassan válaszol
- Terheléselosztás
- Heterogén rendszerek
- Lazán kapcsolt rendszerek: nem kell ismerni a
címzettet
- Szabványos Java API MOM-ekhez való hozzáféréshez
- Java EE része, de Java SE-ben is használható
- JMS provider
- IBM MQ, Apache ActiveMQ (ActiveMQ 5 "Classic", ActiveMQ Artemis),
RabbitMQ
- IBM MQ, Apache ActiveMQ (ActiveMQ 5 "Classic", ActiveMQ Artemis),
- Hozzáférés JMS API-n keresztül
- Hálózat létrehozása
docker network create --driver bridge eventstore-net
- Artemis indítása
docker build -t eventstore-mq .
docker run -d -p 8161:8161 -p 61616:61616
--network eventstore-net --name employees-mq eventstore-mq
- EventStore indítása
mvnw package
docker build -t eventstore .
docker run -d -p 8082:8080
-e SPRING_ARTEMIS_HOST=eventstore-mq
--network eventstore-net --name eventstore eventstore
.small-code-14[
FROM adoptopenjdk:14-jdk-hotspot
RUN apt-get update \
&& apt-get install wget \
&& wget -q -O /tmp/apache-artemis-2.13.0-bin.tar.gz \
"https://www.apache.org/dyn/closer.cgi?filename=activemq/activemq-artemis/2.13.0/apache-artemis-2.13.0-bin.tar.gz&action=download" \
&& tar xzf /tmp/apache-artemis-2.13.0-bin.tar.gz -C /opt \
&& rm /tmp/apache-artemis-2.13.0-bin.tar.gz
WORKDIR /var/lib
RUN /opt/apache-artemis-2.13.0/bin/artemis create --http-host 0.0.0.0 --relax-jolokia \
--queues eventsQueue --allow-anonymous --user artemis --password artemis eventstorebroker
RUN sed -i "s|<max-disk-usage>90</max-disk-usage>|<max-disk-usage>100</max-disk-usage>|g"
\ /var/lib/eventstorebroker/etc/broker.xml
EXPOSE 8161
EXPOSE 61616
CMD ["/var/lib/eventstorebroker/bin/artemis", "run"]
]
--relax-jolokia
admin felület elérhető legyen kintrőlmax-disk-usage
, különben blokkolja a küldést
- Hálózat létrehozása
docker network create --driver bridge eventstore-net
- Artemis indítása
docker run -d -p 8161:8161 -p 61616:61616
--network eventstore-net --name eventstore-mq training360/eventstore-mq
- EventStore indítása
docker run -d -p 8082:8080
-e SPRING_ARTEMIS_HOST=eventstore-mq
--network eventstore-net
--name my-eventstore training360/eventstore
version: '3'
services:
eventstore-mq:
image: training360/eventstore-mq
restart: always
ports:
- "8161:8161"
- "61616:61616"
eventstore:
image: training360/eventstore
restart: always
depends_on:
- eventstore-mq
ports:
- "8082:8080"
environment:
SPRING_ARTEMIS_HOST: 'eventstore-mq'
entrypoint: ["./wait-for-it.sh", "-t", "120", "eventstore-mq:61616", "--",
"java", "org.springframework.boot.loader.JarLauncher"]
depends_on
: indítási sorrendrestart: always
: hiba esetén vagy Docker daemon újraindításkor is újraindítja (pl. számítógép reboot esetén is)
RUN apt-get update \
&& apt-get install wget \
&& apt-get install -y netcat \
&& wget https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh \
&& chmod +x ./wait-for-it.sh
docker-compose up
docker-compose up -d
docker-compose down
- Elérhető a
http://localhost:8161
címen. - Alapértelmezett felhasználónév/jelszó:
admin
/admin
class: inverse, center, middle
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-artemis</artifactId>
</dependency>
- Ekkor a localhosthoz, default porton (
61616
) kapcsolódik - Felülbírálható a
spring.artemis.host
ésspring.artemis.port
paraméterekkel
- Injektálható
JmsTemplate
segítségével
public class EventStoreGateway {
private JmsTemplate jmsTemplate;
public EventStoreGateway(JmsTemplate jmsTemplate) {
this.jmsTemplate = jmsTemplate;
}
public void sendEvent(EmployeeHasCreatedEvent event) {
log.debug("Send event to eventstore");
jmsTemplate.convertAndSend("eventsQueue", event);
}
}
- Alapesetben a
SimpleMessageConverter
aktívString
->TextMessage
byte[]
->BytesMessage
Map
->MapMessage
Serializable
->ObjectMessage
MarshallingMessageConverter
(JAXB), vagyMappingJackson2MessageConverter
(JSON)
@Bean
public MessageConverter messageConverter(ObjectMapper objectMapper){
MappingJackson2MessageConverter converter =
new MappingJackson2MessageConverter();
converter.setTypeIdPropertyName("_typeId");
return converter;
}
- A cél a
_typeId
értékéből (header-ben utazik) találja ki,
hogy milyen osztállyá kell alakítani (unmarshal)
- Alapesetben a típus értéke fully qualified classname - lehet, hogy a cél oldalon nem mond semmit
- Ezért hozzárendelünk egy stringet
MappingJackson2MessageConverter converter
= new MappingJackson2MessageConverter();
converter.setTypeIdPropertyName("_typeId");
converter.setTypeIdMappings(
Map.of("CreateEventCommand", EmployeeHasCreatedEvent.class));
class: inverse, center, middle
@JmsListener(destination = "eventsQueue")
public void processMessage(CreateEventCommand command) {
eventsService.createEvent(command, "JMS");
}
class: inverse, center, middle
- Monitorozás, beavatkozás és metrikák
- HTTP és JMX végpontok
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
http://localhost:8080/actuator
címen elérhető az
enabled és exposed endpontok listája- Logban:
o.s.b.a.e.web.EndpointLinksResolver:
Exposing 2 endpoint(s) beneath base path '/actuator'
- További actuator végpontok bekapcsolása:
management.endpoints.web.exposure.include
konfigurációval
- Összes expose:
management.endpoints.web.exposure.include = *
- Mind be van kapcsolva, kivéve a shutdown
management.endpoint.shutdown.enabled = true
- Saját fejleszthető
- Biztonságossá kell tenni
{"status":"UP"}
management.endpoint.health.show-details = always
- Létező JDBC DataSource, MongoDB, JMS providers, stb.
- Saját fejleszthető (
implements HealthIndicator
)
.small-code-14[
{
"status": "UP",
"components": {
"db": {
"status": "UP",
"details": {
"database": "H2",
"result": 1,
"validationQuery": "SELECT 1"
}
},
"diskSpace": {
"status": "UP",
"details": {
"total": 1000202039296,
"free": 680306184192,
"threshold": 10485760
}
},
"ping": {
"status": "UP"
}
}
}
]
- Heap dump:
/heapdump
(bináris állomány) - Thread dump:
/threaddump
- Beans:
/beans
- Conditions:
/conditions
- Autoconfiguration feltételek teljesültek-e vagy sem - ettől függ,
milyen beanek kerültek létrehozásra
- Autoconfiguration feltételek teljesültek-e vagy sem - ettől függ,
- HTTP mappings:
/mappings
- HTTP mappings és a hozzá tartozó kiszolgáló metódusok
- Configuration properties:
/configprops
- Ha van
HttpTraceRepository
az application contextben - Fejlesztői környezetben:
InMemoryHttpTraceRepository
- Éles környezetben: Zipkin vagy Spring Cloud Sleuth
- Megjelenik a
/httptrace
endpoint
/caches
- Cache/scheduledtasks
- Ütemezett feladatok/flyway
- Flyway/liquibase
- Liquibase/integrationgraph
- Spring Integration/sessions
- Spring Session/jolokia
- Jolokia (JMX http-n keresztül)/prometheus
info
prefixszel megadott property-k belekerülnek
# Spring Boot 2.6 óta
management.info.env.enabled=true
info.appname = employees
{"appname":"employees"}
/env
végpont - property source-ok alapján felsorolva/env/info.appname
- értéke, látszik, hogy melyik property source-ból jött- Spring Cloud Config esetén
POST
-ra módosítani is lehet
(Spring Cloud Config Server használja)
spring.jmx.enabled
hatására management endpointok exportálása MBean-ként- Kapcsolódás pl. JConsole-lal
- JMX over HTTP beállítása Jolokiával
<dependency>
<groupId>org.jolokia</groupId>
<artifactId>jolokia-core</artifactId>
</dependency>
- JavaScript, Java API
- Kliens pl. a Jmx4Perl
- Jmx4Perl Docker konténerben
docker run --rm -it jolokia/jmx4perl jmx4perl
http://host.docker.internal:8080/actuator/jolokia
read java.lang:type=Memory HeapMemoryUsage
- Felügyeleti, üzemeltetési folyamatok
- Ne ad-hoc szkriptek
- Alkalmazással együtt kerüljenek verziókezelésre, buildelésre és kiadásra
- Preferálja a REPL (read–eval–print loop) használatát
- Tipikusan command line
- Megosztó, könnyen el lehet rontani
- Tipikusan máshogy kéne megoldani:
- Adatbázis migráció
- Ütemezett folyamatok
- Egyszer lefutó kódok
- Command line-ban elvégezhető feladatok
- Megoldások:
- Flyway, Liquibase
- Magas szintű ütemező (pl. Quartz)
- REST-en, MQ-n meghívható kódrészek
- Új microservice
class: inverse, center, middle
.small-code-14[
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
]
A target/classes/META-INF
könyvtárban build-info.properties
fájl
.small-code-14[
{
"build": {
"artifact": "employees",
"name": "employees",
"time": "2022-10-05T20:50:03.216Z",
"version": "0.0.1-SNAPSHOT",
"group": "training"
}
}
]
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
</plugin>
A target/classes
könyvtárban git.properties
fájl
{
"appname": "employees",
"git": {
"branch": "master",
"commit": {
"id": "d63acd0",
"time": "2020-02-04T11:12:58Z"
}
}
}
- A különböző környezetekre telepített példányoknál alapvető igény,
hogy tudjuk, hogy mely verzióból készült (felületen, logban látható legyen)
class: inverse, center, middle
/loggers
/logfile
### Get logger
GET http://localhost:8080/actuator/loggers/training.employees
### Set logger
POST http://localhost:8080/actuator/loggers/training.employees
Content-Type: application/json
{
"configuredLevel": "INFO"
}
class: inverse, center, middle
/metrics
végponton- Micrometer - application metrics facade (mint az SLF4J a naplózáshoz)
- Több, mint 15 monitoring eszközhöz való csatlakozás
(Elastic, Ganglia, Graphite, New Relic, Prometheus, stb.)
- JVM
- Memória
- GC
- Szálak
- Betöltött osztályok
- CPU
- File descriptors
- Uptime
- Tomcat (
server.tomcat.mbeanregistry.enabled
értéke legyentrue
) - Library-k: Spring MVC, WebFlux, Jersey, HTTP Client,
Cache, DataSource, Hibernate, RabbitMQ - Stb.
Counter.builder(EMPLOYEES_CREATED_COUNTER_NAME)
.baseUnit("employees")
.description("Number of created employees")
.register(meterRegistry);
meterRegistry.counter(EMPLOYEES_CREATED_COUNTER_NAME).increment();
A /metrics/employees.created
címen elérhető
- Adatok különböző kategóriákba sorolhatóak:
- Application performance monitoring
- Domain specifikus értékek
- Health, logs
- Új konténerek születnek és szűnnek meg
- Központi eszköz
class: inverse, center, middle
- Az alkalmazás tölti fel bizonyos időközönként az adatokat
docker run
-d
-p 80:80 -p 2003-2004:2003-2004 -p 2023-2024:2023-2024
-p 8125:8125/udp -p 8126:8126
--name graphite
graphiteapp/graphite-statsd
Felhasználó/jelszó: root
/root
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-graphite</artifactId>
</dependency>
management.metrics.export.graphite.step = 10s
class: inverse, center, middle
io.micrometer:micrometer-registry-prometheus
függőség/actuator/prometheus
endpoint
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
- Prometheus kérdez le a megadott rendszerességgel
- yml konfiguráció,
prometheus.yml
scrape_configs:
- job_name: employees
metrics_path: '/actuator/prometheus'
scrape_interval: 20s
static_configs:
- targets: ['host.docker.internal:8080']
Tegyük fel, hogy a prometheus.yml
a D:\data\prometheus
könyvtárban van
docker run -d -p 9090:9090 -v D:\data\prometheus:/etc/prometheus
--name prom prom/prometheus
class: inverse, center, middle
- Pl. bejelentkezéssel kapcsolatos események
- Saját események vehetőek fel
- Kell egy
AuditEventRepository
implementáció,
beépített:InMemoryAuditEventRepository
- Megjelenik az
/auditevents
endpoint
applicationEventPublisher.publishEvent(
new AuditApplicationEvent("anonymous",
"employee_has_been_created",
Map.of("name", command.getName())));
class: inverse, center, middle
- Extreme Programming
- Termék átadásának gyorsítására, integrációs idő csökkentésére
- Revision control, branching csökkentése, gyakori commit, commit-onként build
- Build folyamat automatizálása, idejének csökkentése
- Tesztelés automatizálása, az éles (production) környezethez hasonló környezetben
- A build eredménye mindenki számára hozzáférhető – „eltört build” fogalma
- A build eredményének azonnali publikálása: hibák mielőbbi megtalálása
- Integrációs problémák mielőbbi feltárása és javítása
- Hibás teszt esetén könnyű visszaállás
- Nem forduló kód mielőbbi feltárása és javítása
- Konfliktusok mielőbbi feltárása és javítása
- Minden módosítás azonnali unit tesztelése
- Verziók azonnali elérhetősége
- Fejlesztőknek szóló rövidebb visszajelzés
Olyan megközelítés, melynek használatával a fejlesztés rövid ciklusokban történik, biztosítva hogy a szoftver bármelyik pillanatban kiadható
- Minden egyes változás (commit) potenciális release
- Build automatikus és megismételhető formában
- Több lépésből áll: fordítás, csomagolás, tesztelés (statikus és dinamikus),
telepítés különböző környezetekre - Deployment pipeline foglalja magába a lépéseket
- Ugyanaz az artifact megy végig a pipeline-on
- Automation server
- Open source
- Build, deploy, automatic
- Nagyon sok plugin
- Jenkins nodes: master és agents
- Dockerben futtatható
- Pluginek
- Continuous delivery pipeline megvalósításához
- Pipelines "as code"
- DSL:
Jenkinsfile
- Stage-ek, melyek step-ekből állnak
- CD túl sokmindenhez hozzáfér
- A CD vége csak az artifact előállítás, de nem a telepítés
- Környezet deklaratív leírás alapján előállítható/frissíthető legyen
- Ha új környezetet kell előállítani, nem kell hozzá CD
Jenkinsfile
tartalma:
.small-code-14[
pipeline {
agent any
stages {
stage('package') {
steps {
git 'https://github.com/vicziain/employees'
sh "./mvnw clean package"
}
}
stage('test') {
steps {
sh "./mvnw verify"
}
}
}
}
]
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
</goals>
</execution>
</executions>
</plugin>
git update-index --chmod=+x mvnw
- Letölti a Mavent
- Maven letölti a függőségeket
FROM jenkins/jenkins:lts-jdk11
ENV JAVA_OPTS -Djenkins.install.runSetupWizard=false
RUN jenkins-plugin-cli --plugins "git workflow-aggregator pipeline-stage-view blueocean"
docker build --file Dockerfile.jenkins -t employees-jenkins .
docker run -d -p 8082:8080 --name employees-jenkins employees-jenkins
- Új Item
- Projektnév megadása, pl.
employees
- Pipeline
- Pipeline/Definition Pipeline script from SCM
- Git
- Repository URL kitöltése, pl.
https://github.com/vicziain/employees
- Futtatható alkalmazás készítése
- Forráskód + build = verziózott immutable artifact
- Verziózott artifact + környezeti konfiguráció: egyedi azonosítóval rendelkező
immutable release - Kezeli a függőségeket
- Visszaállás egy előző release-re
- Build, release, futtatás élesen elválik
- Ne legyen olyan, hogy "nálam működik"
- Különbségek
- Időbeli eltolódás (lassú release)
- Személyi különbségek (fejlesztő nem lát rá az éles rendszerre),
egy gombos deploy - Eszközbeli különbségek
(pl. pehelysúlyú, embedded megoldások, pl. H2)
- Docker segít