SchoolComplex Persistence Layer, JAX-RS RESTful, Mockito
Óbudai Egyetem, Java Enterprise EditionMűszaki Informatika szakLabor 6
Bedők Dávid2016.10.31.v1.1
Feladat
Hozzunk létre egy Enterprise Java alkalmazást, mely hallgatók érdemjegyeit adott tantárgy vonatkozásában tárolja és kezeli.A diákokat neptun kódjuk egyedien azonosítja és tároljuk el nevüket és intézményüket (BANKI, KANDO, NEUMANN).A tantárgyaknak legyen egy egyedi nevük, oktatójuk (név és neptun kód) és leírásuk.Minden érdemjegyhez tároljunk egy megjegyzést és egy pontos időbélyeget is.
2
Feladat szolgáltatás oldala
A megvalósított tárolási réteg fölé az alábbi RESTful service réteget építsük fel:● GET http://localhost:8080/school/api/student/WI53085
○ Megadott neptun kóddal rendelkező hallgató adatait adja vissza.● GET http://localhost:8080/school/api/student/list
○ Az összes hallgató adatait adja vissza.● POST http://localhost:8080/school/api/mark/stat
○ Payload: “Sybase PowerBuilder”○ Egy adott tantárgy vonatkozásában visszaad egy intézményre és
évekre bontott átlag-eredmény statisztikát.● PUT http://localhost:8080/school/api/mark/add
○ Payload: {"subject": "Sybase PowerBuilder","neptun": "WI53085","grade": "WEAK","note": "Lorem ipsum"}
○ Adott érdemjegyet rögzít a rendszerben.● DELETE http://localhost:8080/school/api/student/TX78476
○ Megadott neptun kóddal rendelkező hallgatót töröl, amennyiben az létezik és nincs egyetlen korábban tárolt érdemjegye sem. 3
Technológia
A megvalósítás során PostgreSQL RDBMS adatmodellre épített JPA-n keresztül megszólított ORM réteg kerül építésre. Immáron a fizikai táblák közötti kapcsolat az entitások közötti kapcsolatokban is meg fog mutatkozni.Speciális lekérdezések mellett az új rekord rögzítésének és rekord törlésének mikéntjét is alaposabban megvizsgáljuk.A RESTful service megvalósítása kapcsán megismerkedünk a JAX-RS alapjaival, mind szerver mind kliens oldalon.A feladat végén egység teszteket fogunk készíteni az adaptor rétegben (TestNG, Mockito).Remote debug lehetőségeire is kitekintünk.
4
Adatbázis oldalSchema létrehozása (create-schema.sql):
Táblák:● institute● student (FK: student_institute_id)● teacher● subject (FK: subject_teacher_id)● mark (FK: mark_student_id, FK: mark_subject_id)Kapcsolatok:● 1-N: institute-student● 1-N: teacher-subject● N-M: student-subject 5
\jboss\school\sch-persistence\databaseGIT
Project struktúra
school (root project) [ear]● sch-persistence [jar]● sch-ejbservice [jar]● sch-weblayer [war]
○ Kizárólag a StudentPingServlet, annak érdekében hogy az sch-webservice elkészültéig meg lehessen szólítani a backend service funkcióit.
● sch-webservice [war]● sch-restclient (standalone)Nincs igény Remote EJB client-re, ezért az sch-ejbservice-t nem kell kétfelé szedni. Az sch-webservice is local EJB hívásokkal éri el az service réteget, ahogyan a sch-weblayer. 6
Root gradle project
7
version = '1.0'
ext {
...
jaxrsVersion = '2.0.1'
webserviceArchiveName = 'sch-webservice.war'
webserviceContextPath = 'school'
}
...dependencies {
deploy project('sch-ejbservice')
deploy project('sch-persistence')
deploy project(path: 'sch-weblayer', configuration: 'archives')
deploy project(path: 'sch-webservice', configuration: 'archives')
}
school/build.gradle
Az új third-party library a JAX-RS 2.0.1 API-ja lesz. JBoss 6.4 ennek RESTEasy 2.3.10 Final implementációját tartalmazza, de mi kizárólag API-n keresztül fogjuk vezérelni.
A kimeneti ear nevében a version információ bekerül. Tetszőleges szabad-szöveges érték lehet.
Persistence réteg
Entity class-ok:● Mark (table: mark)● Student (table: student)● Subject (table: subject)● Teacher (table: teacher)Enumeration:● Institute (table: institute)
EJB Service-ek:● MarkService● StudentService● SubjectService
8
Subject-Teacher relation1 tantárgynak 1 oktatója van
9
@Entity @Table(name = "subject")
public class Subject implements Serializable {
...
@ManyToOne(fetch = FetchType.EAGER, optional = false)
@JoinColumn(name = "subject_teacher_id", referencedColumnName = "teacher_id", nullable = false)
private Teacher teacher;
...
}
Subject.class
FetchType:● EAGER: az entitás lekérésekor automatikusan kapcsolja a ‘teacher’ táblát,
ezáltal elérhetőek lesznek a kapcsolt adatok (pl. tanár neptun kódja)● LAZY: nem csatolja automatikusan, csak ha erre kéri a lekérdezés, vagy az
attached entitás (hatékonyabb, de körültekintést igényel)
Az egyik legfontosabb (és legnehezebb) dolog megfelelően beállítani az EAGER és LAZY kapcsolatokat. Ha ellentétes igények merülnek fel, akkor sincs probléma: ugyanarra a DB táblára akármennyi entitást készíthetünk, egyikben a kapcsolat lehet EAGER, a másikban LAZY.
A @JoinColumn és @Column annotációk hasonlóan a @Table annotációhoz a fizikai adatbázissal való kapcsolatot szolgálják.
Student-Mark relation1 diáknak számos jegye van
10
@Entity
@Table(name = "student")
public class Student implements Serializable {
@OneToMany(fetch = FetchType.LAZY, targetEntity = Mark.class, mappedBy = "student")
private final Set<Mark> marks;
public Student() {
this.marks = new HashSet<>();
}
}
Student.class
Egy hallgatónak N darab érdemjegye van. Gondolva a teljesítményre, az ilyen halmazokat LAZY-vel érdemes felvenni. A Lista használata Hibernate értelmezésében hiba (mivel nincs a sorrendiségnek entrópiája jelen esetben).
A @OneToMany és a @ManyToOne annotáció az ORM modellre vonatkozik, a hivatkozott mezők field-ek nevei (pl. student és nem mark_student_id).
Subject-Mark relation1 tantárgyhoz is számos jegy tartozik
11
@Entity
@Table(name = "subject")
public class Subject implements Serializable {
@OneToMany(fetch = FetchType.LAZY, targetEntity = Mark.class, mappedBy = "subject")
private final Set<Mark> marks;
public Subject() {
this.marks = new HashSet<>();
}
}
Subject.class
Teljesen azonos a Student-Mark kapcsolat jelzésével, azonban ritkábban van szerepe ennek. Egy hallgató jegyeit kigyűjteni “gyakoribb” lehet mint egy tárgyhoz tartozó összes jegyet egyben kezelni (bár minden üzleti igény kérdése).
Mark entitásStudent-Subject N-M kapcsolótábla
12
@Entity@Table(name = "mark")public class Mark implements Serializable {
...@ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL,
optional = false)@JoinColumn(name = "mark_student_id", referencedColumnName =
"student_id", nullable = false)private Student student;
@ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL, optional = false)
@JoinColumn(name = "mark_subject_id", referencedColumnName = "subject_id", nullable = false)
private Subject subject;...
}
Mark.class
A cascade értéke egy CascadeType enum value halmaz (ALL esetén nem kell felsorolni), mely meghatározza hogy milyen művelet során szükséges a kapcsolaton végigmenni. cascade={PERSIST, MERGE, REMOVE, REFRESH, DETACH}. Alapértelmezésben üres lista.
Java API for RESTful WebServicesJAX-RS
● JSR 311● Java EE 6 része
● "Párja" a JAX-WS (Java API for XML Web Services), mely SOAP webservice-ek készítéséhez készült (JSR 224, Java EE 6 része).
13
sch-webservice child project
14
apply plugin: 'war'
war { archiveName webserviceArchiveName }
dependencies {
providedCompile project(':sch-ejbservice')
providedCompile group: 'javax.ws.rs', name:'javax.ws.rs-api', version: jaxrsVersion
providedCompile group: 'javax.servlet', name:'javax.servlet-api', version: servletapiVersion
}
build.gradle
RESTful services
● StudentRestService○ StudentStub getStudent( String neptun )○ List<StudentStub> getAllStudent( )○ removeStudent( String neptun )
● MarkRestService○ List<MarkDetailStub> getMarkDetails( String subject )○ MarkStub addMark( MarkInputStub stub )
15
Szolgáltatás16
Hallgató adatainak lekérdezéseGET http://localhost:8080/school/api/student/{neptun}
Diák adatainaklekérdezése
17
{
"name": "Juanita A. Jenkins",
"neptun": "WI53085",
"institute": "BANKI",
"marks": [{
"subject": {
"name": "C/C++ Programming",
"teacher": {
"name": "Lorine B. Pine",
"neptun": "MD21921"
},
"description": "Maecenas..."
},
"grade": 2,
"note": "Vivamus",
"date": 1401956934000
}, ...]
}
REQ: GET http://localhost:8080/school/api/student/WI53085RESP: 200 OK
EPOCH timestamp
getStudent()Kezdjük a legegyszerűbb esettel :-)
1. webservice: StudentRestServicea. StudentStub getStudent( String neptun ) throws AdaptorException;b. StudentStub, MarkStub, SubjectStub, TeacherStub
2. webservice: StudentRestServiceBeana. call ejbservice layer
3. ejbservice: StudentFacadea. StudentStub getStudent( String neptun ) throws AdaptorException;
4. ejbservice: StudentFacadeImpla. call persistence layerb. call converter service
5. persistence: StudentServicea. Student read( String neptun ) throws PersistenceServiceException;
6. persistence: StudentServiceImpla. GET_BY_NEPTUN: SELECT st FROM Student st LEFT JOIN FETCH
st.marks m LEFT JOIN FETCH m.subject su LEFT JOIN FETCH su.teacher WHERE st.neptun=:studentNeptun
7. ejbservice: StudentConvertera. ejbservice: StudentConverterImplb. ejbservice: MarkConverterc. ejbservice: MarkConverterImpl
18
JOIN: a jegyek bekötése LAZY, vagyis ha nem jelezzük, csak akkor jönnek le, ha még attached entitáson bejárjuk (pl. student.getMarks().size() ). Azonban ha JOIN-al jelezzük a lekérést a Named Query-ben, akkor felülbíráljuk a LAZY-t.Mindez egyetlen natív query lefutását jelenti!
Generált natív lekérdezés
19
SELECTstudent0_.student_id AS student_1_2_0_,marks1_.mark_id AS mark_id1_0_1_,subject2_.subject_id AS subject_1_3_2_,teacher3_.teacher_id AS teacher_1_4_3_,student0_.student_institute_id AS student_2_2_0_,student0_.student_name AS student_3_2_0_,student0_.student_neptun AS student_4_2_0_,marks1_.mark_date AS mark_dat2_0_1_,marks1_.mark_grade AS mark_gra3_0_1_,marks1_.mark_note AS mark_not4_0_1_,marks1_.mark_student_id AS mark_stu5_0_1_,marks1_.mark_subject_id AS mark_sub6_0_1_,marks1_.mark_student_id AS mark_stu5_2_0__,marks1_.mark_id AS mark_id1_0_0__,subject2_.subject_description AS subject_2_3_2_,subject2_.subject_name AS subject_3_3_2_,subject2_.subject_teacher_id AS subject_4_3_2_,teacher3_.teacher_name AS teacher_2_4_3_,teacher3_.teacher_neptun AS teacher_3_4_3_
FROMstudent student0_
INNER JOIN mark marks1_ ON student0_.student_id=marks1_.mark_student_id INNER JOIN subject subject2_ ON marks1_.mark_subject_id=subject2_.subject_id INNER JOIN teacher teacher3_ ON subject2_.subject_teacher_id=teacher3_.teacher_id
WHEREstudent0_.student_neptun=?
Szolgáltatás20
Összes hallgató adatainak lekérdezéseGET http://localhost:8080/school/api/student/list
getAllStudents()
SELECT s FROM Student s ORDER BY s.name21
...
for (final Student student : result) {
student.getMarks().size();
}
...
StudentServiceImpl.java
REQ: GET http://localhost:8080/school/api/student/listRESP: 200 OK
Named Query: StudentQuery.GET_ALL
JOIN: a jegyek bekötése LAZY, ugyanaz az eset mint korábban, azonban itt a lekérdezés mellőzi a JOIN-okat. E végett csak abban az esetben kapjuk vissza a jegyeket, ha még attached állapotban (tranzakció határon belül) lekérjük a jegyeket. A jegyekben a Student és a Subject már EAGER, így azok is lejönnek, ahogyan a tárgy Teacher eleme is (szintén EAGER). Azonban mindez nagyon sok (11) natív query-be kerül!
Szolgáltatás22
Átlag eredmény statisztikaPOST http://localhost:8080/school/api/mark/stat
Postman
GET kérésekhez bármely böngésző megfelelő, azonban POST/PUT (stb.) kérések elküldéséhez és/vagy űrlap/payload megadásához már külön kis kliens alkalmazást kellene írni, vagy XHTML formokat szerkesztgetni. E körülményes megoldások helyett használhatjuk a Postman-t is (Google Chrome kiegészítő vagy Mac app).https://www.getpostman.com/
23
getMarkDetails()
Egy adott tantárgy (payload) vonatkozásában visszaad egy intézményre (group-by) és évekre (group-by) bontott átlag-eredmény statisztikát (average).
24
REQ: POST http://localhost:8080/school/api/mark/statRESP: 200 OK
Sybase PowerBuilder
Payload
[{
"institute": "BANKI",
"year": 2015,
"averageGrade": 4.0
},
{
"institute": "KANDO",
"year": 2012,
"averageGrade": 4.0
},
{
"institute": "KANDO",
"year": 2013,
"averageGrade": 4.0
},
{
"institute": "NEUMANN",
"year": 2014,
"averageGrade": 3.5
}]
Probléma ?!
Timestamp-ből az év kinyerése (PSQL function):● DATE_TRUNC('year', mark_date)
Túl összetett lekérdezés és/vagy speciális db függvény használata (akár saját db függvény használata). → Eltolja az ORM implementációt natív query irányba (erre van technikai lehetőség)
Megoldás: a felelősség egy részét DB oldalon megvalósítani, és ORM szinten tisztán JPA Named Query-vel dolgozni (ahogy eddig is).Megjegyzés: Más alternatív lehetőség is elképzelhető.
25
Natív lekérdezés
26
SELECTmarkdetail.student_institute_id,markdetail.mark_year,AVG(markdetail.mark_grade)
FROM(
SELECTmark_subject_id,student_institute_id,mark_grade,DATE_TRUNC('year', mark_date) AS mark_year
FROM markINNER JOIN student ON ( mark_student_id = student_id )
WHERE ( 1 = 1 ) ) AS markdetail
WHERE ( 1 = 1 )AND ( markdetail.mark_subject_id = 2 )
GROUP BYmarkdetail.student_institute_id,markdetail.mark_year
ORDER BYmarkdetail.student_institute_id,markdetail.mark_year
markdetail.sql
A natív lekérdezés elkészítése nélkül a JPA megoldást sem lehet (nem érdemes) elkészíteni.
Terv
● Tipikus “group-by” lekérdezés.○ intézményre és évre nézve
● DB oldalon VIEW létrehozása a DB függvény használata miatt (DATE_TRUNC)○ A VIEW-ban ki kell vezetni azokat a mezőket,
melyekre üzletileg szűrést kell eszközölni (valamint természetesen a csoportosító mezőket is)■ tantárgy (szűrés)■ intézmény és év (group-by)
Megjegyzés: Nem lehet a VIEW group-by lekérdezés, mert a tantárgy szűrés nem volna kivitelezhető! (ezen a mondaton érdemes gondolkodni…)
27
DB View
28
CREATE VIEW markdetail AS
SELECT
ROW_NUMBER() OVER() AS markdetail_id,
mark_subject_id AS markdetail_subject_id,
student_institute_id AS markdetail_institute_id,
mark_grade AS markdetail_grade,
DATE_TRUNC('year', mark_date) AS markdetail_year
FROM mark
INNER JOIN student ON ( mark_student_id = student_id )
WHERE ( 1 = 1 );
markdetail-view.sql
A VIEW-ból entitás lesz, minden entitásnak szükséges egy primary kulcs. A ROW_NUMBER() alkalmas erre (nem fogjuk update-elni, törölni a view egyetlen sorát sem, ráadásul group-by lekérdezés az egyéni sorokat sem fogja visszaadni.
VIEW “tesztelése”
29
SELECT
markdetail_institute_id,
markdetail_year,
AVG(markdetail_grade)
FROM
markdetail
WHERE ( 1 = 1 )
AND ( markdetail_subject_id = 2 )
GROUP BY
markdetail_institute_id,
markdetail_year
ORDER BY
markdetail_institute_id,
markdetail_year;
markdetail-test.sql
Ezt a lekérdezést kell előállítanunk JPA named query-ként.
View az ORM rétegbenUgyanolyan entitás mint a tábla
30
@Entity @Table(name = "markdetail")public class MarkDetail implements Serializable {
@Id@Column(name = "markdetail_id", nullable = false)private Long id;
@ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL, optional = false)@JoinColumn(name="markdetail_subject_id",referencedColumnName="subject_id",nullable=false)private Subject subject;
@Enumerated(EnumType.ORDINAL)@Column(name = "markdetail_institute_id", nullable = false)private Institute institute;
@Column(name = "markdetail_grade", nullable = false)private Integer grade;
@Column(name = "markdetail_year")private Date year;
}
MarkDetail.java
Primary Key kötelező
Minden mező pont ugyanúgy van kezelve, mint a többi entitásban. Ne zavarjon meg az, hogy a VIEW belül is tartalmaz pl. INNER JOIN-t. Ez az ORM réteg szempontjából láthatatlan. A Subject, Institute úgy van bekötve ide, mintha ezek egy táblában együtt jelen lennének.
Átlagszámítás - Részletek I.● webservice: MarkRestService, MarkRestServiceBean● ejbservice: MarkDetailStub● ejbservice: MarkFacade, MarkFacadeImpl● persistence: SubjectService, SubjectServiceImpl
○ SELECT s FROM Subject s WHERE s.name=:subjectName
● persistence: MarkService, MarkServiceImpl○ SELECT ???? FROM MarkDetail md WHERE
md.subject.id=:subjectId GROUP BY md.institute, md.year ORDER BY md.institute, md.year
○ ????: egy olyan “result”, mely tartalmaz egy intézményt, egy évet és egy származtatott értéket. Egy e célra létrehozott Result példányt kell visszaadnunk (ami NEM egy entitás lesz)!
31
Lekérdezés eredménye
32
public class MarkDetailResult {
private final Institute institute;
private final Date year;
private final double averageGrade;
public MarkDetailResult(Institute institute, Date year, double averageGrade) {
this.institute = institute;
this.year = year;
this.averageGrade = averageGrade;
}
...
}
MarkDetailResult.java
Nem entitás, egyszerű DTO. A konstruktor üzletileg fontos, nem kell default ctor-nak lennie (entitásnál kötelező).
Result példány
Named Query-ben a Result példány létrehozása:SELECT new hu.qwaevisz.school.persistence.result.MarkDetailResult(md.institute, md.year, AVG(md.grade)) FROM MarkDetail md WHERE md.subject.id=:subjectId GROUP BY md.institute, md.year ORDER BY md.institute, md.year
33
A Full Qualified Name megadása szükséges (mivel nincs lehetőség “import”-ra.
Átlagszámítás - Részletek II.● ejbservice: MarkConverter, MarkConverterImpl
○ getYearFromDate (Java konverzió)
34
@Override public List<MarkDetailStub> to(List<MarkDetailResult> results) {
final List<MarkDetailStub> stubs = new ArrayList<>();
for (final MarkDetailResult result : results) {
stubs.add(this.to(result));
}
return stubs; }
private MarkDetailStub to(final MarkDetailResult result) {
return new MarkDetailStub(result.getInstitute().toString(), this.getYearFromDate(result.getYear()), result.getAverageGrade()); }
private int getYearFromDate(Date date) {
final Calendar cal = Calendar.getInstance();
cal.setTime(date);
return cal.get(Calendar.YEAR);
}
MarkConverterImpl.java
Szolgáltatás35
Új érdemjegy rögzítésePUT http://localhost:8080/school/api/mark/add
addMark()
36
{
"subject": "Sybase PowerBuilder",
"neptun": "WI53085",
"grade": "WEAK",
"note": "Lorem ipsum"
}
Request payload
REQ: PUT http://localhost:8080/school/api/mark/addRESP: 200 OK
{
"subject": {
"name": "Sybase PowerBuilder",
"teacher": {
"name": "Richard B. Cambra",
"neptun": "UT84113"
},
"description": "Donec"
},
"grade": 2,
"note": "Lorem ipsum",
"date": 1443797867042
}
Response content
Részletek● webservice: MarkRestService, MarkRestServiceBean● ejbservice: MarkFacade, MarkFacadeImpl● persistence: SubjectService, SubjectServiceImpl (már
meglévő)○ SELECT s FROM Subject s WHERE
s.name=:subjectName● persistence: StudentService, StudentServiceImpl (már
meglévő)○ SELECT s FROM Student s JOIN s.marks WHERE
s.neptun=:studentNeptun● persistence: MarkService, MarkServiceImpl
○ INSERT INTO (nem Named Query, EntityManager operation)
● ejbservice: MarkConverter, MarkConverterImpl37
Rögzítés ORM oldalon
38
@Override
public Mark create(Long studentId, Long subjectId, Integer grade, String note) throws PersistenceServiceException {
try {
final Student student = this.studentService.read(studentId);
final Subject subject = this.subjectService.read(subjectId);
Mark mark = new Mark(student, subject, grade, note);
mark = this.entityManager.merge(mark);
this.entityManager.flush();
return mark;
} catch (final Exception e) {
throw new PersistenceServiceException("Unknown error during merging SubscriberGroup (studentId: " + studentId + ", subjectId: " + subjectId
+ ", grade: " + grade + ", note: " + note + ")! " + e.getLocalizedMessage(), e);
}
}
MarkService.java
Más körülmények között a validáció és az itt megjelenő service hívások azonos tranzakcióba is kerülhetnének.
flush(): empty the internal SQL instructions cache
Költség● Validation at EJB Service layer
○ Check Subject by Name (1 SELECT)■ Do not use JOIN FETCH s.teacher and Teacher’s FetchType is EAGER → +1 SELECT
to acquire Teacher○ Check Student by Neptun (1 SELECT)
■ Use several LEFT JOIN FETCH(es) in that query● Attach the entities at Persistence Service Layer
○ Acquire Student by ID (1 SELECT)■ Subject hasn’t got any EAGER relation
○ Acquire Subject by ID (1 SELECT)■ Do not use JOIN FETCH s.teacher and Teacher’s FetchType is EAGER → +1 SELECT
to acquire Teacher○ Merge Mark
■ We need to get all the marks (+1 SELECT with JOINS via Student’s LAZY Set<Mark> field). Hiberante creates that query and use the Mark’s EAGER Subject field in the same query, but (!)● Each Subject has an EAGER Teacher relation too, and Hibernate acquires
these values via N separate queries (+3 SELECT)■ Related Subject becomes attached via the previous query (NO SELECT required)
○ SELECT the Mark’s Sequence (+1 SELECT)○ Insert the new Mark (+1 INSERT) 39
Total: 11 SELECT + 1 INSERT
Továbbfejlesztés
You may push the validation at the Persistence Layer, but you have to pay attention not to detach the same entity twice (you will get “Multiple representations of the same entity are being merged.” exception in that case and for instance you have to remove the CascadeType.MERGE flag in some relations).
40
Szolgáltatás41
Hallgató törléseDELETE http://localhost:8080/school/api/student/{neptun}
removeStudent()Hibakezelés bemutatása
42
REQ: DELETE http://localhost:8080/school/api/student/ABC123RESP: 400 Bad Request
{ "code": 40, "message": "Resource not found", "fields": "ABC123" }
Response content
REQ: DELETE http://localhost:8080/school/api/student/WI53085RESP: 412 Precondition Failed
{ "code": 50, "message": "Has dependency", "fields": "WI53085" }
Response content
REQ: DELETE http://localhost:8080/school/api/student/TX78476RESP: 204 No Content
HTTP Status Codeshttp://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
43
Successful 2xx
● 200 OK● 201 Created● 202 Accepted● 204 No Content● 206 Partial Content
Redirection 3xx
● 300 Multiple Choices● 301 Moved Permanently● 302 Found● 303 See Other● 304 Not Modified● 307 Temporary Redirect
Client Error 4xx
● 400 Bad Request● 401 Unauthorized ● 402 Payment Required● 403 Forbidden● 404 Not Found● 405 Method Not Allowed● 408 Request Timeout● 412 Precondition Failed● 413 Request Entity Too Large● 414 Request-URI Too Long● 415 Unsupported Media Type
Server Error 5xx
● 500 Internal Server Error● 501 Not Implemented● 503 Service Unavailable
Részletek
● webservice: StudentRestService, StudentRestServiceBean
● ejbservice: StudentFacade, StudentFacadeImpl● persistence: StudentService, StudentServiceImpl
○ SELECT COUNT(s) FROM Student s WHERE s.neptun=:studentNeptun
● persistence: MarkService, MarkServiceImpl○ SELECT COUNT(m) FROM Mark m WHERE
m.student.neptun=:studentNeptun● persistence: StudentService, StudentServiceImpl
○ DELETE FROM Student s WHERE s.neptun=:studentNeptun
44Named Query mellett megvalósíható EntityManager műveletként is a törlés.
Publikus hiba üzenet: ErrorStub (json)
45
public class ErrorStub {
private int code;
private String message;
private String fields;
public ErrorStub(int code, String message, String fields) {
this.code = code;
this.message = message;
this.fields = fields;
}
...
}
ErrorStub.java
Muszáj hogy getter/setter metódusok is legyenek a Stub-ban, így a mezők ajánlottan ne legyen final-ek.
Hibalehetőségek, publikus infok
46
public enum ApplicationError {
UNEXPECTED(10, 500, "Unexpected error"), // Internal Server Error
NOT_EXISTS(40, 400, "Resource not found"), // Bad Request
HAS_DEPENDENCY(50, 412, "Has dependency"); // Precondition Failed
private final int code;
private final int httpStatusCode;
private final String message;
private ApplicationError( int code, int httpStatusCode, String message) {
this.code = code;
this.httpStatusCode = httpStatusCode;
this.message = message; }
public int getHttpStatusCode() { return this.httpStatusCode; }
public ErrorStub build(String field) {
return new ErrorStub(this.code, this.message, field);
}
}
ApplicationError.java
Az enum példány az ErrorStub factory-ja!
Hibaágak kezeléseÜzleti hibák detektálása és átalakítása “nyilvános” hibaüzenetté
47
@Overridepublic void removeStudent(String neptun) throws AdaptorException {
try {if (this.studentService.exists(neptun)) {
if (this.markService.count(neptun) == 0) {this.studentService.delete(neptun);
} else {throw new AdaptorException(ApplicationError.HAS_DEPENDENCY,
"Student has undeleted mark(s)", neptun);}
} else {throw new AdaptorException(ApplicationError.NOT_EXISTS, "Student
doesn't exist", neptun);}
} catch (final PersistenceServiceException e) {LOGGER.error(e, e);throw new AdaptorException(ApplicationError.UNEXPECTED,
e.getLocalizedMessage());}
}
StudentFacadeImpl.java
Továbbfejlesztés
Validate and Delete Student in the same transaction, because this is the safer solution!● We have to push the Application level
business error detection to the Persistence Layer.
● Need to change the TransactionAttributes of the related Business methods.
● The Persistence Layer will become more complex while the EJB Service Layer will be simpler.
48
Persistence LayerModifications
49
package hu.qwaevisz.school.persistence.util;public enum PersistenceApplicationError {
UNEXPECTED,NOT_EXISTS,HAS_DEPENDENCY;
}
PersistenceApplicationError.java
public class StudentServiceImpl implements StudentService {@EJBprivate MarkService markService;
@Override@TransactionAttribute(TransactionAttributeType.REQUIRED)public boolean exists(String neptun) throws PersistenceServiceException {
[..]}
}
StudentServiceImpl.java
public class AdvancedPersistenceServiceException extends PersistenceServiceException {private final PersistenceApplicationError error;private final String field;public AdvancedPersistenceServiceException(PersistenceApplicationError error, String
message, String field) {super(message);this.error = error;this.field = field;
}public PersistenceApplicationError getError() { return this.error; }public String getField() { return this.field; }
}
AdvancedPersistenceServiceException.java
We have to use the same attribute in the MarkServiceImpl count(studentNeptun) business method.
Persistence LayerNew Student delete Business method
50
public class StudentServiceImpl implements StudentService {@Overridepublic void deleteAdvanced(String neptun) throws PersistenceServiceException {
if (this.exists(neptun)) {if (this.markService.count(neptun) == 0) {
try {this.entityManager.createNamedQuery(StudentQuery.REMOVE_BY_NEPTUN).set
Parameter(StudentParameter.NEPTUN, neptun).executeUpdate();} catch (Exception e) {
throw new PersistenceServiceException("Unknown error when removing Student by neptun (" + neptun + ")! " + e.getLocalizedMessage(), e);
}} else {
throw new AdvancedPersistenceServiceException(PersistenceApplicationError.HAS_DEPENDENCY, "Student has undeleted mark(s)", neptun);
}} else {
throw new AdvancedPersistenceServiceException(PersistenceApplicationError.NOT_EXISTS, "Student doesn't exist", neptun);
}}
}
StudentServiceImpl.java
EJB Service LayerNew Student delete Business method
51
package hu.qwaevisz.school.ejbservice.facade;
public class StudentFacadeImpl implements StudentFacade {
@Overridepublic void removeStudentAdvanced(String neptun) throws AdaptorException {
try {this.studentService.deleteAdvanced(neptun);
} catch (AdvancedPersistenceServiceException e) {ApplicationError error = ApplicationError.valueOf(e.getError().name());throw new AdaptorException(error, e.getLocalizedMessage(), e.getField());
} catch (PersistenceServiceException e) {LOGGER.error(e, e);throw new AdaptorException(ApplicationError.UNEXPECTED,
e.getLocalizedMessage());}
}
}
StudentFacadeImpl.java
Transaction AttributesThe TransactionAttribute annotation specifies whether the container is to invoke a business method within a transaction context. The TransactionAttribute annotation can be used for session beans and message driven beans. It can only be specified if container managed transaction demarcation is used.MANDATORY: If a client invokes the enterprise bean's method while the client is associated with a transaction context, the container invokes the enterprise bean's method in the client's transaction context (must use the transaction of the client).NEVER: The client is required to call without a transaction context, otherwise an exception is thrown.NOT_SUPPORTED: The container invokes an enterprise bean method whose transaction attribute NOT_SUPPORTED with an unspecified transaction context (don’t need transactions, may improve performance).REQUIRED (default): If a client invokes the enterprise bean's method while the client is associated with a transaction context, the container invokes the enterprise bean's method in the client's transaction context.REQUIRES_NEW: The container must invoke an enterprise bean method whose transaction attribute is set to REQUIRES_NEW with a new transaction context.SUPPORTS: If the client calls with a transaction context, the container performs the same steps as described in the REQUIRED case (you should use the Supports attribute with caution).
52
53
“A” Business Operation { .. } “B” Business Operation { .. }
REQUIRED / MANDATORY / SUPPORTS
“A” Transaction
“A” Business Operation { .. } “B” Business Operation { .. }
REQUIRED / REQUIRES_NEW
“B” TransactionNo Transaction
“A” Business Operation { .. } “B” Business Operation { .. }
REQUIRES_NEW
“A” Transaction “B” Transaction
“A” Business Operation { .. } “B” Business Operation { .. }
NOT_SUPPORTED
“A” Transaction
“A” Business Operation { .. } “B” Business Operation { .. }
NOT_SUPPORTED / SUPPORTS / NEVER
No Transaction
No TransactionNo Transaction
Hiba esetek
54
“A” Business Operation { .. } “B” Business Operation { .. }
NEVER
RemoteException“A” Transaction
“A” Business Operation { .. } “B” Business Operation { .. }
MANDATORY
No Transaction TransactionRequiredException
Hogyan kapunk JSON hiba választ?
● Regisztrálva van egy olyan @Provider, mely ezt a kivételt átalakítja egy Response-á.
● A JAX-RS RESTful service kivételt dob.● A kliens a kivételből átalakított Response-t
fogja megkapni.
55
RESTful átalakítás
56
@Providerpublic class AdaptorExceptionMapper implements ExceptionMapper<AdaptorException> {
@Contextprivate HttpHeaders headers;@Overridepublic Response toResponse(AdaptorException e) {
return Response.status(e.getErrorCode().getHttpStatusCode() ).entity(e.build()) //
.header(SchoolCrossOriginRequestFilter.ALLOW_ORIGIN, "*") //
.header(SchoolCrossOriginRequestFilter.ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS, HEAD") //
.header(SchoolCrossOriginRequestFilter.MAX_AGE, "1209600") //
.header(SchoolCrossOriginRequestFilter.ALLOW_HEADERS, "x-requested-with, origin, content-type, accept, X-Codingpedia, authorization") //
.header(SchoolCrossOriginRequestFilter.ALLOW_CREDENTIALS, "true") //
.type(MediaType.APPLICATION_JSON ).build();}}
AdaptorExceptionMapper.java
AdaptorException build() metódus hívás!
CrossOriginRequestFilter
● Célja hogy a szolgáltatást meg lehessen hívni idegen host-ról.
● A Servlet API részeként definiálható Filter-ként érdemes megvalósítani.
● A HTTP Header-ben szükség néhány mező értékét beállítani (a példában mindent engedélyezünk).
● A REST hívások előtt egy HTTP OPTIONS method-dal “ping”-el a kliens, tesztelve hogy távolról el tudja-e érni a szolgáltatást. → ezért lett felvéve a REST service-ekbe egy általános választ adó művelet:
57
@OPTIONS
@Path("{path:.*}")
Response optionsAll(@PathParam("path") String path);
Debug Remote JVM (JBoss)
JBoss default debug-port: 8787
58
> [JBOSS_HOME]/bin/standalone.[bat|sh] --debug> [JBOSS_HOME]/bin/standalone.[bat|sh] --debug [DEBUG-PORT]
Bármely JVM-et lehet remote debug-olni (Java-nak kell az alábbi argumentumokat átadni (az -Xdebug a régebbi JVM beállítása, de az újabbak is felismerik):
-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=[DEBUG-PORT]-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=[DEBUG-PORT]
Listening for transport dt_socket at address: 8787
console/terminal
Eclipse - Debug
Run | Debug Configurations |● Remote Java Application
○ Helyi menü: New○ Project: Browse.. (egyébként ez lényegtelen)○ Connection Type: Standard (Socket Attach)
■ Host: localhost■ Port: 8787
○ Apply és Debug● Debug Perspective-ra váltás
○ Debug view-ban látni kell a thread-eket○ Ugyanitt: Helyi menü: Edit source lookup
■ Add Java Project(s)59
Egység tesztek készítése
Unit teszt írásának általános szabályai● Bedők Dávid: Programozási feladatok
megoldási módszertana (Óbudai Egyetem, 2015) 5.2 fejezet: Egység tesztelés
Mockolási technikák● Jelen labor keretein belül elsősorban ezen
lesz a hangsúly● Feltételezi az általános egység teszt írás
szabályainak ismeretét és alapvető technikáját
60
TestNG
● src/test/java könyvtár (source folder)● MarkFacadeImpl osztállyal azonos
package szinten létrehozzuk az osztályhoz készített egység tesztet.
61
package hu.qwaevisz.school.ejbservice.facade;
import org.testng.Assert;
import org.testng.annotations.Test;
public class MarkFacadeImplTest {
@Test
public void someTestMethod() {
Assert.assertEquals( true, true);
}
}
MarkFacadeImplTest.java
Mockitohttp://mockito.org/
Egy adott osztály egység tesztjében minden olyan osztályt, melyet ő felhasznál, mock-olni/fake-elni szükséges.
● Elsősorban azért, mert ha valós osztályként használnánk azokat, akkor ha a felhasznált osztályban hiba van, akkor nem csak annak az egység tesztje bukna el, hanem azok az osztályoknak az egység tesztjei is, melyek őt felhasználják.
● Kivételek mindig előfordulhatnak, ezt megfelelő egység teszt írási tapasztalat után a szakember érezni fogja.
62
ext {
testngVersion = '6.9.+'
mockitoVersion = '1.10.8'
}
subprojects {
dependencies {
testCompile group: 'org.testng', name: 'testng', version: testngVersion
testCompile group: 'org.mockito', name: 'mockito-core', version: mockitoVersion
}
}
build.gradle
Jegy statisztika egység teszteléseFacade rétegben
Mi a metódus felelőssége ebben a rétegben?● Egy bemeneti tantárgy megnevezés alapján előállítani egy kimeneti
MarkDetail stub listát.● A tárgy neve alapján kikérni a hozzá tartozó ID-t a persistence rétegtől
(hogy létezik-e az adott tantárgy)● Tantárgy ID alapján lekérni a hozzá tartozó statisztikát a persistence
rétegtől.● Kérni a konverziót az erre a célra szolgáló service-től hogy az
persistence réteg eredményéből Stub készüljön.● Ha hiba keletkezik a persistence rétegben, akkor nem várt hibát jelez.● Nem felelőssége ezen kívül semmi más (pl. a lekérdezés vagy a
konverzió mikéntje, részletei)!
63
List<MarkDetailStub> getMarkDetails(String subject) throws AdaptorException
PéldaInjectMocks és Mock annotációk használata
64
public class MarkFacadeImplTest {
@InjectMocks
private MarkFacadeImpl facade;
@Mock
private StudentService studentService;
@Mock
private SubjectService subjectService;
@Mock
private MarkService markService;
@Mock
private MarkConverter markConverter;
@BeforeMethod
public void setup() {
MockitoAnnotations.initMocks(this);
}
@Test
public void createListOfMarkDetailsFromSubjectName() throws AdaptorException, PersistenceServiceException {
}
}
MarkFacadeImplTest.java
Az @InjectMocks annotációval az az egyetlen osztály rendelkezik, melyet az adott egység tesztben tesztelünk. Ide kell a mockokat a keretrendszernek “inject”-álnia.
Az @Mock annotációval azok az osztályok szerepelnek, melyeknek egy fake-et/mock-ot kell gyártani, és melyek be lesznek inject-álva a tesztelendő osztályba.
Az MockitoAnnotataions.initMocks(this) nagyon fontos, hogy minden teszt metódus előtt lefusson. Ez végzi el az inject-álást. Ősteszt osztályba áthelyezhető a kódsor.
PéldaGyakorlati példa
65
public void createListOfMarkDetailsFromSubjectName() throws AdaptorException, PersistenceServiceException {
Subject subject = Mockito.mock(Subject.class);
Mockito.when(this.subjectService.read(SUBJECT_NAME)).thenReturn(subject);
Mockito.when(subject.getId()).thenReturn(SUBJECT_ID);
[..]
List<MarkDetailStub> stubs = new ArrayList<>();
MarkDetailStub neumannStub = Mockito.mock(MarkDetailStub.class);
stubs.add(neumannStub);
Mockito.when(this.markConverter.to(results)).thenReturn(stubs);
List<MarkDetailStub> markDetailStubs = this.facade.getMarkDetails(SUBJECT_NAME);
Mockito.verify(this.markService).read(SUBJECT_ID);
Assert.assertEquals(markDetailStubs.size(), stubs.size());
Assert.assertEquals(markDetailStubs.get(0), neumannStub);
[..]
}
MarkFacadeImplTest.java
Tipikus forgatókönyv
Subject subject = Mockito.mock(Subject.class);
● Létrehoz egy Subject mock-ot (a @Mock annotáció is ilyet hoz létre, azonban utóbbit inject-álja is a tesztelendő osztályba, ha erre kérjük).
Mockito.when(this.subjectService.read(SUBJECT_NAME)).thenReturn(subject);
● Felkészít egy mock-ot. Jelen esetben ha a read() metódusát egy adott String paraméterrel meghívjuk, adja vissza a “subject” példányt (ami egy mock, de ez lehet valós osztálypéldány is, vagy pl. érték).
Mockito.verify(this.markService).read(SUBJECT_ID);
● Ellenőrzi a tesztelendő metódus meghívása után a read() metódus meghívását a service mock-ján a megadott String példány paraméterrel. Ha nem hívódik meg (pontosan egyszer), akkor a teszt elbukik, mivel a hívás elvárt!
66
Advanced Mockito
● Lehetőség van when() során kivétel dobására (utóbbit a void visszatérési értékű metódusok is megtehetik).
● Van lehetőség Matcherek segítségével nem pontos értéket átadni paramétereknek, hanem pl. csak az a fontos hogy String osztály példánya legyen. Tetszőlegesen kombinálható mindez.
● Tudunk belső argumentumokat “elkapni” (mivel hívták meg a mock metódusát), majd az egység tesztben erre pl. egy Assert-et írni.
● Megadható pontosan hányszor hívtak meg egy metódust verify() során.
● Megadható when() során ha ugyanazt a metódus többször hívják, sorban miket adjon vissza eredményül.
● stb. 67
MockitoDo not overengineering
Természetesen a Mockito osztálykönyvtár/library számos egyéb lehetőséget tartalmaz, azonban nem szabad megfeledkezni arról sem, ha túlságosan “mélyen” teszteljük a vizsgált osztályt, akkor az nagyon érzékeny lesz az apróbb módosításokra is (nehezebben lesz refaktorálható). E miatt pl. a verify() használatát ahol lehet mellőzzük (a bemutatott példában pl. teljesen szükségtelen).
68
Szolgáltatás69
Diák jegyeinek lekérdezéseszűrési feltételekkel
POST http://localhost:8080/school/api/mark/get/{neptun}
addMark()
70
<markcriteria>
<subject>Python</subject>
<minimumgrade>2</minimumgrade>
<maximumgrade>4</maximumgrade>
</markcriteria>
Request payload
REQ: POST http://localhost:8080/school/api/mark/get/WI53085RESP: 200 OK
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<mark>
<date>2014-09-29T04:15:34+02:00</date>
<grade>3</grade>
<note>Phasellus</note>
<subject>
<description>Fusce ...</description>
<name>Python Programming</name>
<teacher>
<name>Christine W. Culp</name>
<neptun>OK73109</neptun>
</teacher>
</subject>
</mark>
Response content
REST Client alkalmazás
A RESTful service meghívása teljesen nyelvfüggetlen, HTTP request-ek gyártása kell csupán hozzá, “minden” prog. nyelvből lehetséges.Azonban type-safe módon készíthetünk Java klienst, mely gyorsabb és megbízhatóbb fejlesztést tesz lehetővé, ehhez kapunk támogatást 3rd party library-któl.Megjegyzés: Java-ból is lehetne HTTP kéréseket gyártani, majd a válasz payload-ját feldolgozni String-ként, de ezzel külön nem foglalkozunk. 71
REST kliensGradle konfiguráció
72
jar { archiveName 'sch-restclient.jar' }
dependencies {
compile group: 'org.jboss.spec', name: 'jboss-javaee-6.0', version: jbossjee6Version
compile group: 'org.jboss.resteasy', name:'resteasy-jaxrs', version: resteasyVersion
compile group: 'org.jboss.resteasy', name:'resteasy-jaxb-provider', version: resteasyVersion
compile group: 'commons-logging', name: 'commons-logging', version: commonsloggingVersion
}
build.gradle
ext {resteasyVersion = '2.3.7.Final'jbossjee6Version = '3.0.3.Final' commonsloggingVersion = '1.2'
}
JAXB Provider: XML szerializáláshoz és deszerializáláshoz.
REST “remote” interface
73
@Path("/mark")
public interface MarkRestService {
@POST
@Consumes("application/xml")
@Produces("application/xml")
@Path("/get/{neptun}")
ClientResponse<MarkStub> getMarks(@PathParam("neptun") String studentNeptun, MarkCriteria criteria);
}
MarkRestService.java
A server oldali MarkRestService ettől eltérő! Pl. a Consumes/Produces részek is külön-külün vezérelhetőek (de ez esetben biztosítani kell a (de)serializáláshoz szükséges library-kat!@POST@Consumes("application/xml")@Produces("application/xml")@Path("/get/{studentneptun}")MarkStub getMatchingMark(@PathParam("studentneptun") String studentNeptun, MarkCriteria criteria) throws AdaptorException;
A ClientResponse<T> osztály alkalmas arra, hogy a HTTP header részeit is fel tudjuk dolgozni kliens oldalon (pl. HTTP Response Code).
REST hívás
74
public MarkStub process(String studentNeptun, MarkCriteria criteria) {
URI serviceUri = UriBuilder.fromUri( "http://localhost:8080/school/api").build();
ClientRequestFactory crf = new ClientRequestFactory(serviceUri);
MarkRestService api = crf.createProxy( MarkRestService.class);
ClientResponse<MarkStub> response = api.getMarks(studentNeptun, criteria);
LOGGER.info("Response status: " + response.getStatus());
MultivaluedMap<String, Object> header = response.getMetadata();
for (String key : header.keySet()) {
LOGGER.info("HEADER - key: " + key + ", value: " + header.get(key));
}
MarkStub entity = response.getEntity();
LOGGER.info("Response entity: " + entity);
return entity;
}
SchoolRestClient.java
response.getMetadata();
response.getEntity();