Scopul acestui laborator este introducerea studenților în concepte mai avansate care permit crearea framework-urilor și a aplicațiilor enterprise.
Aspectele urmărite sunt:
Aspectele bonus urmărite sunt:
Serializarea reprezintă procesul prin care un obiect Java este transformat într-o reprezentare transferabilă, de regulă o secvență de bytes sau un format text, astfel încât să poată fi:
Operația inversă se numește deserializare și presupune refacerea obiectului original din această reprezentare.
Serializarea este folosită în special pentru:
Pentru ca un obiect să poată fi serializat de JVM folosind mecanismul nativ, clasa sa trebuie să implementeze interfața: java.io.Serializable.
public class User implements Serializable { private String name; private int age; }
Această interfață:
ObjectOutputStream și ObjectInputStream.
Dacă un obiect nu este serializabil, JVM aruncă NotSerializableException.
1. Clasa model
import java.io.Serializable; public class Person implements Serializable { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return name + " (" + age + ")"; } }
2. Serializarea unui obiect
import java.io.FileOutputStream; import java.io.ObjectOutputStream; public class SerializeDemo { public static void main(String[] args) throws Exception { Person p = new Person("Ana", 30); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser")); out.writeObject(p); out.close(); System.out.println("Object serialized."); } }
3. Deserializarea unui obiect
import java.io.FileInputStream; import java.io.ObjectInputStream; public class DeserializeDemo { public static void main(String[] args) throws Exception { ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser")); Person p = (Person) in.readObject(); in.close(); System.out.println("Deserialized: " + p); } }
Nu toate câmpurile unui obiect ar trebui serializate. Exemple tipice:
Pentru a exclude un câmp de la serializare folosim cuvântul cheie transient.
public class Account implements Serializable { private String username; private transient String password; }
password ca fiind transient, acesta nu este salvat, iar după deserializare devine null.
Câmpul serialVersionUID identifică versiunea unei clase serializate.
private static final long serialVersionUID = 1L;
Acest identificator este folosit de JVM pentru a verifica dacă o clasă este compatibilă cu obiectul serializat, astfel putem garanta compatibilitatea operațiilor.
Jackson este o bibliotecă externă foarte populară care permite:
Spre deosebire de serializarea nativă:
Serializable,Este standardul de facto pentru:
1. Dependința Jackson
Într-un proiect real (Maven), Jackson se adaugă astfel:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.16.1</version> </dependency>
2. Clasa model
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; public class User { private String name; private int age; @JsonIgnore private String password; public User() { // constructor fără parametri NECESAR pentru deserializare } public User(String name, int age, String password) { this.name = name; this.age = age; this.password = password; } public String getName() { return name; } public int getAge() { return age; } @JsonProperty("years") public void setAge(int age) { this.age = age; } public String getPassword() { return password; } @Override public String toString() { return "User{name='" + name + "', age=" + age + "}"; } }
3. Serializare: obiect → JSON
import com.fasterxml.jackson.databind.ObjectMapper; public class JacksonSerializeDemo { public static void main(String[] args) throws Exception { ObjectMapper mapper = new ObjectMapper(); User user = new User("Ana", 30, "secret123"); String json = mapper.writeValueAsString(user); System.out.println("JSON rezultat:"); System.out.println(json); } }
password nu apare deoarece are adnotarea @JsonIgnore.
4. Deserializare: JSON → obiect
import com.fasterxml.jackson.databind.ObjectMapper; public class JacksonDeserializeDemo { public static void main(String[] args) throws Exception { ObjectMapper mapper = new ObjectMapper(); String json = """ { "name": "Ion", "years": 25, "password": "shouldBeIgnored" } """; User user = mapper.readValue(json, User.class); System.out.println("Obiect deserializat:"); System.out.println(user); } }
years → age datorită @JsonPropertypassword este ignorat
5. Serializare în fișier și citire din fișier
Scriere în fișier
mapper.writeValue(new File("user.json"), user);
Citire din fișier
User user = mapper.readValue(new File("user.json"), User.class);
Adnotările sunt metadate atașate elementelor din codul Java (clase, metode, câmpuri, parametri).
Ele nu modifică direct logica programului, ci oferă informații suplimentare care pot fi interpretate de:
Cu alte cuvinte, adnotările spun ce este codul, nu ce face.
Adnotările au apărut pentru a elimina:
Astăzi, ele sunt fundamentale în:
@AnnotationName public class Example { }
Adnotările pot avea parametri:
@JsonProperty("user_name") private String name;
sau mai mulți:
@MyAnnotation(value = "test", enabled = true)
Prin @Target putem controla unde este permisă adnotarea:
TYPE – clase, interfețeFIELD – câmpuriMETHOD – metodePARAMETER – parametriCONSTRUCTORLOCAL_VARIABLEExemplu:
@Target(ElementType.FIELD)
@Retention definește cât timp există adnotarea:
@Retention(RetentionPolicy.RUNTIME)
Tipuri:
SOURCE – doar la compilare (ex: @Override)CLASS – în bytecode, dar nu la runtimeRUNTIME – disponibilă prin reflection (cea mai importantă)
@Override public String toString() { ... }
@Deprecated public void oldMethod() { }
@SuppressWarnings("unchecked")
Jackson folosește adnotări pentru a controla serializarea:
@JsonIgnore private String password;
@JsonProperty("years") private int age;
Aceste adnotări:
ObjectMapper1. Definirea adnotării
import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Important { String value() default ""; }
@interface
2. Utilizarea adnotării
public class Person { @Important("primary identifier") private String name; private int age; }
import java.lang.reflect.Field; public class AnnotationDemo { public static void main(String[] args) throws Exception { Class<Person> clazz = Person.class; for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(Important.class)) { Important imp = field.getAnnotation(Important.class); System.out.println(field.getName() + " -> " + imp.value()); } } } }
Java definește trei momente distincte în care adnotările pot fi citite sau folosite:
1. La compilare (compile-time)
Adnotările cu:
@Retention(RetentionPolicy.SOURCE)
sunt:
Exemplu:
@Override
Rol:
2. După compilare, înainte de rulare (build-time / processing)
Unele adnotări sunt procesate prin Annotation Processors (APT – Annotation Processing Tool).
Acestea:
Exemple reale:
@Getter, @Setter)
#define), ci un mecanism controlat de compilator.
3. La runtime (execuție)
Adnotările cu:
@Retention(RetentionPolicy.RUNTIME)
sunt:
Exemple:
@JsonProperty)@Test)@Autowired)Acesta este cel mai important caz pentru Java modern.
Reflection este mecanismul prin care un program Java poate:
Cu alte cuvinte, Reflection permite unui program să se auto-analizeze și să se auto-manipuleze în timpul execuției.
Reflection a fost introdus pentru a permite:
Fără Reflection:
Reflection nu înlocuiește programarea clasică. Este:
De aceea este folosită:
Orice operație de Reflection pornește de la un obiect de tip Class<?>.
Există trei modalități principale de a obține un Class:
Class<String> c1 = String.class; Class<?> c2 = "hello".getClass(); Class<?> c3 = Class.forName("java.lang.String");
Toate cele trei referințe indică aceeași clasă la runtime.
Presupunem următoarea clasă:
public class Person { private String name; private int age; public Person(String name, int age) { } public String getName() { return name; } }
Câmpuri
Field[] fields = Person.class.getDeclaredFields(); for (Field f : fields) { System.out.println(f.getName()); }
Metode
Method[] methods = Person.class.getDeclaredMethods(); for (Method m : methods) { System.out.println(m.getName()); }
Constructori
Constructor<?>[] constructors = Person.class.getDeclaredConstructors(); for (Constructor<?> c : constructors) { System.out.println(c); }
Reflection permite accesarea membrilor private, ocolind encapsularea.
Person p = new Person("Ana", 30); Field field = Person.class.getDeclaredField("name"); field.setAccessible(true); String value = (String) field.get(p); System.out.println(value);
Method m = Person.class.getDeclaredMethod("getName"); String result = (String) m.invoke(p); System.out.println(result);
Reflection permite:
Constructor<Person> ctor = Person.class.getConstructor(String.class, int.class); Person p = ctor.newInstance("Ion", 25);
Acesta este mecanismul folosit de:
Reflection este mecanismul prin care adnotările RUNTIME sunt citite.
Exemplu:
@Retention(RetentionPolicy.RUNTIME) @interface Info { String value(); }
@Retention(RetentionPolicy.RUNTIME) @interface Info { String value(); }
Citire la runtime:
Info info = Person.class.getAnnotation(Info.class); System.out.println(info.value());
Reflection este folosit pentru:
Exemple:
Reflection:
Assertions sunt afirmații folosite pentru a verifica ipoteze interne ale programului în timpul execuției. Ele exprimă condiții care ar trebui să fie mereu adevărate dacă programul funcționează corect.
Aserțiile nu sunt mecanisme de tratare a erorilor și nu înlocuiesc excepțiile.
Assertions sunt folosite pentru:
assert condition;
Sau cu mesaj:
assert condition : "Mesaj de eroare";
condition este false, JVM aruncă un AssertionError.
public int divide(int a, int b) { assert b != 0 : "b must not be zero"; return a / b; }
Dacă b este 0, programul va eșua doar dacă assertions sunt activate.
Assertions sunt dezactivate implicit.
Se activează la rulare:
java -ea Main
| Assertions | Excepții |
|---|---|
| pentru bug-uri interne | pentru erori de runtime |
| pot fi dezactivate | sunt mereu active |
| nu se folosesc pentru input user | validează input extern |
Unit testing reprezintă procesul de testare a celor mai mici unități de cod (de regulă metode sau clase) în izolare față de restul aplicației.
Scopul este verificarea faptului că:
Unit testing-ul se concentrează pe corectitudinea logicii, nu pe integrarea componentelor.
În Java, o unitate este de obicei:
Exemplu de unitate bună:
int add(int a, int b)
Exemplu de unitate slabă:
void saveUserAndSendEmailAndLog()
Un test unitar corect este:
În Java, standardul de facto pentru unit testing este JUnit.
Un test JUnit este:
class CalculatorTest { @Test void additionWorks() { Calculator c = new Calculator(); int result = c.add(2, 3); assertEquals(5, result); } }
Elemente cheie:
@Test marchează metoda ca testassertEquals verifică rezultatulJUnit oferă propriul set de assertions:
assertEquals(expected, actual); assertTrue(condition); assertFalse(condition); assertNotNull(object); assertThrows(Exception.class, () -> code);
Exemplu:
assertThrows(IllegalArgumentException.class, () -> calculator.divide(10, 0));
Majoritatea testelor respectă structura:
@Test void exampleTest() { // Arrange Calculator c = new Calculator(); // Act int result = c.add(2, 3); // Assert assertEquals(5, result); }
Această structură:
Pentru inițializare și cleanup:
@BeforeEach void setup() { calculator = new Calculator(); } @AfterEach void clear() { calculator.reset(); }
Aceste metode:
| Unit Testing | Assertions |
|---|---|
| testare automată | verificări interne |
| rulează mereu | pot fi dezactivate |
| parte din CI | doar pentru debug |
| validează API | validează ipoteze |
Acestea aparțin:
Integration testing verifică dacă mai multe componente ale aplicației funcționează corect împreună, nu doar în izolare.
Spre deosebire de unit testing:
Exemple:
| Unit Testing | Integration Testing |
|---|---|
| testează o unitate | testează colaborarea |
| izolat | dependențe reale |
| foarte rapid | mai lent |
| fără infrastructură | poate necesita DB / config |
În practică există mai multe grade:
Laboratoarele se concentrează de obicei pe integrare parțială, pentru control și viteză.
Mocking permite simularea comportamentului unei dependențe, fără a o executa efectiv.
Folosim mocking când:
Mockito este standardul de facto în Java pentru mocking.
Cu Mockito putem:
UserRepository repo = mock(UserRepository.class); when(repo.findById(1L)) .thenReturn(new User("Ana"));
Ce se întâmplă:
repo NU este implementare realăfindById este simulatăclass UserServiceTest { UserRepository repo = mock(UserRepository.class); UserService service = new UserService(repo); @Test void returnsUserName() { when(repo.findById(1L)) .thenReturn(new User("Ana")); String name = service.getUserName(1L); assertEquals("Ana", name); } }
Aici testăm:
UserServiceMockito permite verificarea apelurilor:
verify(repo).findById(1L);
Acest lucru este util când:
În Java, var permite inferarea tipului la compilare, reducând zgomotul sintactic, fără a pierde siguranța tipurilor.
var nu este tip dinamic. Tipul este determinat o singură dată, la compilare.
Fără var:
UserRepository repository = mock(UserRepository.class); UserService service = new UserService(repository);
Cu var:
var repository = mock(UserRepository.class); var service = new UserService(repository);
Codul este:
var poate fi util în scrierea rapidă a codului, trebuie să avem grijă să nu îl folosim în următoarele situații:
Logging-ul reprezintă mecanismul prin care o aplicație înregistrează informații despre:
Spre deosebire de System.out.println, logging-ul:
Instrucțiunea System.out.println:
Java oferă un mecanism de logging în JDK, fără biblioteci externe java.util.logging.
Exemplu minimal:
import java.util.logging.Logger; public class Example { private static final Logger logger = Logger.getLogger(Example.class.getName()); void run() { logger.info("Application started"); } }
Niveluri de logging (cele mai uzuale)
SEVERE / ERROR – erori criticeWARNING – situații problematiceINFO – informații generaleFINE / DEBUG – detalii de execuție
Pe lângă mecanismul de logging din JDK (java.util.logging), în practică este foarte des întâlnită biblioteca Apache Log4j.
Log4j este:
Un exemplu de inițializare:
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class Example { private static final Logger logger = LogManager.getLogger(Example.class); void run() { logger.info("Application started"); } }
java.util.logging → suficient pentru laborator și aplicații simpleSystem.out.println → nu este logging
În urma paradigmei OOP, au fost concepute mai multe metodologii care pot augmenta performanța sau anumite abilități ale unui proiect conform necesităților.
De exemplu, Data Oriented Design (DOD) este o paradigmă care ajută programarea folosind obiecte prin folosirea cache-ului. Practic, clasele sunt refactorizate astfel încât să țină cont de mărimea cache-ului din procesor pentru a scădea drastic timpii de acces. Această paradigmă începe să fie destul de folosită în mediile în care viteza este importantă, cum ar fi programarea jocurilor video, drept urmare chiar și engine-ul Unity a fost rescris astfel încât să folosească DOD (găsiți articolul aici.
O altă metodologie utilă este Test Driven Development (TDD) în care accentul este pus pe dezvoltarea testelor înainte de a dezvolta codul propriu-zis. Practic, după etapa de ideație, putem trece la crearea testelor care vor defini comportamentul corect al programului nostru, apoi vom dezvolta codul necesar pentru a putea trece cu succes peste testele scrise de noi. Puteți citi mai multe despre această abordare aici.
De asemenea, o altă metodologie folosită este Domain Driven Design (DDD) care este o abordare de proiectare software în care structura aplicației este ghidată de domeniul de business, iar logica esențială este modelată prin concepte clare (entități, valori, agregate) definite împreună cu experți din domeniu, folosind un limbaj comun (ubiquitous language) pentru a menține codul aliniat cu realitatea problemei. Această abordare este detaliată în articolul scris de Redis aici.
De asemenea, vă recomandăm să parcurgeți documentația Mockito și JUnit sau să folosiți ghiduri de pe site-uri de specialitate cum ar fi Baeldung Mockito series.
Se dă următoarea clasă de producție, care modelează un serviciu simplu de procesare a comenzilor. Codul conține:
public class OrderService { public double calculateTotal(double price, int quantity) { assert price >= 0 : "Price must be non-negative"; assert quantity > 0 : "Quantity must be positive"; return price * quantity; } public double applyDiscount(double total, double discountPercent) { if (discountPercent < 0 || discountPercent > 50) { throw new IllegalArgumentException("Invalid discount"); } return total - (total * discountPercent / 100); } public String categorizeOrder(double total) { if (total < 100) { return "SMALL"; } else if (total < 500) { return "MEDIUM"; } else { return "LARGE"; } } public boolean isFreeShipping(double total) { return total >= 200; } }
Scrieți teste unitare folosind JUnit 5 astfel încât:
Se dau următoarele clase:
public class User { private final String name; public User(String name) { this.name = name; } public String getName() { return name; } }
public interface UserRepository { User findById(long id); }
public class UserService { private final UserRepository repository; public UserService(UserRepository repository) { this.repository = repository; } public String getUserName(long id) { User user = repository.findById(id); return user.getName(); } }
Puteți folosi var pentru: