Table of Contents

Laboratorul 12: Programare Avansată Java

Obiective

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:

  • În acest laborator există mai multe secțiuni marcate [Optional]. Aceste secțiuni cuprind informații bonus care vă pot fi prezentate în timpul laboratorului sau pe care le puteți aprofunda în afara acestuia, ele nefiind necesare pentru laboratoarele viitoare sau pentru teme.
  • De asemenea, veți întâlni câteva secțiuni marcate [Nice to know]. Vă recomandăm ca acestea să aibă prioritate în parcurgerea secțiunilor de tip [Optional], deoarece vă pot oferi informații bonus care să fie și foarte probabil utile pentru teme sau laboratoare viitoare.

🔄 Serializare și Deserializare

Ce este serializarea?

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.

De ce avem nevoie de serializare?

Serializarea este folosită în special pentru:

Serializarea nu înlocuiește o bază de date. Este un mecanism de transport sau stocare temporară, nu unul de persistență relațională.

Serializarea nativă Java

Interfața Serializable

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ță:

Dacă un obiect nu este serializabil, JVM aruncă NotSerializableException.

Exemplu complet: serializare și deserializare

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);
    }
}

Output

Output

Object serialized.
Deserialized: Ana (30)

Câmpuri transient

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;
}

Deoarece am declarat password ca fiind transient, acesta nu este salvat, iar după deserializare devine null.

Versionarea serializării

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.

Dacă nu este declarat explicit:

  • JVM generează unul automat.
  • orice mică modificare va genera un nou UID.

Serializare modernă: Jackson (JSON)

Ce este Jackson?

Jackson este o bibliotecă externă foarte populară care permite:

Spre deosebire de serializarea nativă:

De ce este Jackson preferat în aplicațiile moderne?

Este standardul de facto pentru:

Exemplu cu Jackson

1. Dependința Jackson

Într-un proiect real (Maven), Jackson se adaugă astfel:

pom.xml
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.16.1</version>
</dependency>

Multe proiecte au deja această dependență când se folosește Spring Boot pentru crearea unui API.

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 + "}";
    }
}

  • constructorul fără parametri este obligatoriu pentru Jackson
  • @JsonIgnore exclude câmpul din JSON
  • @JsonProperty permite maparea numelor diferite din JSON

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);
    }
}

output

output

{"name":"Ana","age":30}

Câmpul 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);
    }
}

output

output

User{name='Ion', age=25}

  • yearsage datorită @JsonProperty
  • password este ignorat
  • Jackson a folosit constructorul fără parametri + setter-ele

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ări în Java

Ce sunt adnotările?

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.

De ce folosim adnotări?

Adnotările au apărut pentru a elimina:

Astăzi, ele sunt fundamentale în:

Sintaxa unei adnotări

@AnnotationName
public class Example { }

Adnotările pot avea parametri:

@JsonProperty("user_name")
private String name;

sau mai mulți:

@MyAnnotation(value = "test", enabled = true)

Unde pot fi aplicate adnotările?

Prin @Target putem controla unde este permisă adnotarea:

Exemplu:

@Target(ElementType.FIELD)

Când sunt disponibile adnotările? (Retention)

@Retention definește cât timp există adnotarea:

@Retention(RetentionPolicy.RUNTIME)

Tipuri:

Jackson, JUnit și Spring folosesc RUNTIME.

Adnotări built-in importante

@Override

@Override
public String toString() { ... }

@Deprecated

@Deprecated
public void oldMethod() { }

@SuppressWarnings

@SuppressWarnings("unchecked")

Adnotări și Jackson

Jackson folosește adnotări pentru a controla serializarea:

@JsonIgnore
private String password;
@JsonProperty("years")
private int age;

Aceste adnotări:

Crearea unei adnotări custom

1. Definirea adnotării

import java.lang.annotation.*;
 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Important {
    String value() default "";
}

  • adnotările folosesc @interface
  • metodele NU au corp
  • pot avea valori default

2. Utilizarea adnotării

public class Person {
 
    @Important("primary identifier")
    private String name;
 
    private int age;
}

Citirea adnotărilor cu Reflection

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());
            }
        }
    }
}

output

output

name -> primary identifier

[Optional] Când sunt procesate adnotările?

Cele trei momente de procesare

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:

Nu există acces la aceste adnotări la runtime.

2. După compilare, înainte de rulare (build-time / processing)

Unele adnotări sunt procesate prin Annotation Processors (APT – Annotation Processing Tool).

Acestea:

Exemple reale:

  • Conceptual există un pas de preprocesare, dar nu la nivelul JVM, ci la nivelul build-ului.
  • Acesta NU este un preprocesor ca în C (#define), ci un mecanism controlat de compilator.

3. La runtime (execuție)

Adnotările cu:

@Retention(RetentionPolicy.RUNTIME)

sunt:

Exemple:

Acesta este cel mai important caz pentru Java modern.

🪞 Reflection în Java

Ce este Reflection?

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.

De ce există Reflection?

Reflection a fost introdus pentru a permite:

Fără Reflection:

Reflection ≠ programare normală

Reflection nu înlocuiește programarea clasică. Este:

De aceea este folosită:

Obiectul Class – punctul de intrare

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.

Inspectarea structurii unei clase

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());
}

output

output

name
age

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);
}

Accesarea membrilor private

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);

Acest lucru rupe principiile OOP și trebuie folosit doar în infrastructură sau framework-uri.

Invocarea metodelor prin Reflection

Method m = Person.class.getDeclaredMethod("getName");
String result = (String) m.invoke(p);
 
System.out.println(result);

Reflection permite:

Crearea obiectelor dinamic

Constructor<Person> ctor =
    Person.class.getConstructor(String.class, int.class);
 
Person p = ctor.newInstance("Ion", 25);

Acesta este mecanismul folosit de:

Reflection și Adnotări

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 în framework-uri reale

Reflection este folosit pentru:

Exemple:

Costuri și riscuri

Reflection:

Reflection este o unealtă de framework, nu de business logic. În practică, o vom folosi doar la testarea codului nostru.

🔍 Assertions

Ce sunt assertions?

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.

Scopul assertions

Assertions sunt folosite pentru:

Sintaxa de bază

assert condition;

Sau cu mesaj:

assert condition : "Mesaj de eroare";

Dacă condition este false, JVM aruncă un AssertionError.

Exemplu simplu

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.

Activarea assertions

Assertions sunt dezactivate implicit.

Se activează la rulare:

java -ea Main

Dacă assertions nu sunt activate, codul assert nu se execută deloc.

Assertions vs Excepții

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

Ce este Unit Testing?

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.

Ce este o „unitate”?

În Java, o unitate este de obicei:

Exemplu de unitate bună:

int add(int a, int b)

Exemplu de unitate slabă:

void saveUserAndSendEmailAndLog()

Caracteristicile unui test unitar bun

Un test unitar corect este:

Dacă testul depinde de o bază de date sau de o cerere HTTP către orice resursă online, atunci nu mai este unit test.

Framework standard: JUnit

În Java, standardul de facto pentru unit testing este JUnit.

Un test JUnit este:

Structura unui test JUnit

class CalculatorTest {
 
    @Test
    void additionWorks() {
        Calculator c = new Calculator();
        int result = c.add(2, 3);
 
        assertEquals(5, result);
    }
}

Elemente cheie:

Assertions în JUnit

JUnit 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));

Pattern-ul AAA (Arrange – Act – Assert)

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ă:

Setări comune: @BeforeEach / @AfterEach

Pentru inițializare și cleanup:

@BeforeEach
void setup() {
    calculator = new Calculator();
}
 
@AfterEach
void clear() {
    calculator.reset();
}

Aceste metode:

Unit testing vs Assertions

Unit Testing Assertions
testare automată verificări interne
rulează mereu pot fi dezactivate
parte din CI doar pentru debug
validează API validează ipoteze

Interacțiunile cu:

  • baze de date
  • rețea
  • filesystem
  • servicii externe

Acestea aparțin:

  • integration testing
  • system testing

🧪 Integration Testing & Mocking

Ce este Integration Testing?

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 Tests vs Integration Tests

Unit Testing Integration Testing
testează o unitate testează colaborarea
izolat dependențe reale
foarte rapid mai lent
fără infrastructură poate necesita DB / config

Integration testing nu înseamnă „testăm tot sistemul”.

Niveluri de integrare

În practică există mai multe grade:

Laboratoarele se concentrează de obicei pe integrare parțială, pentru control și viteză.

De ce avem nevoie de Mocking?

Mocking permite simularea comportamentului unei dependențe, fără a o executa efectiv.

Folosim mocking când:

Mockito – framework de mocking

Mockito este standardul de facto în Java pentru mocking.

Cu Mockito putem:

Exemplu simplu de mocking

UserRepository repo = mock(UserRepository.class);
 
when(repo.findById(1L))
    .thenReturn(new User("Ana"));

Ce se întâmplă:

Integrare parțială cu Mockito

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:

Verificarea interacțiunilor

Mockito permite verificarea apelurilor:

verify(repo).findById(1L);

Acest lucru este util când:

Observații importante

[Nice to know] Folosirea lui var în teste

Î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.

Exemplu cu Mockito + var

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:

Deși var poate fi util în scrierea rapidă a codului, trebuie să avem grijă să nu îl folosim în următoarele situații:

  • câmpuri
  • parametri
  • API public
  • când tipul nu e clar din context

[Nice to know] 🪵 Logging în Java

Ce este logging-ul?

Logging-ul reprezintă mecanismul prin care o aplicație înregistrează informații despre:

Spre deosebire de System.out.println, logging-ul:

De ce NU folosim System.out.println?

Instrucțiunea System.out.println:

Logging standard în Java (java.util.logging)

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)

Logging-ul NU trebuie să schimbe comportamentul aplicației, doar să-l observe.

Logging folosind Log4j

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");
    }
}

Log4j a fost implicat în vulnerabilitatea Log4Shell (2021), una dintre cele mai grave din istoria Java. Acest lucru NU înseamnă că logging-ul este periculos, ci că bibliotecile trebuie actualizate și verificate pentru vulnerabilități. În prezent, există utilitare care fac acest lucru automat cum ar fi Snyk

  • java.util.logging → suficient pentru laborator și aplicații simple
  • Log4j → standard de facto în aplicații mari
  • System.out.println → nu este logging

[Optional] 🗺️ Software Development Methodologies

Î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.

Vă recomandăm să parcurgeți secvențe din acest videoclip de la CppCon.

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.

Exerciții

Pentru exercițiile de mai jos va trebui să vă creați propriile proiecte Maven. Vă recomandăm să luați dependențele necesare de pe Maven Repository, concret JUnit și Mockito.

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.

Acest laborator nu va fi încărcat pe Devmind Code, deoarece va fi verificat manual de către laborantul vostru.

Task 1 – Unit Testing cu coverage ≥ 80% (6p)

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:

În industrie, testarea este importantă, iar o metrică consacrată în acest sens este ca 80% din cod să fie acoperit de teste. Pentru a verifica cât cod este acoperit de testele voastre puteți folosi ghidul oficial IntelliJ sau să întrebați laborantul.

Pentru a testa cât mai mult cod, trebuie să generați teste valide și invalide pentru codul dat, astfel încât să testați toate ramurile logice.

Task 2 - Integration Test + Mocking (4p)

Se dau următoarele clase:

User.java
public class User {
    private final String name;
 
    public User(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
}
UserRepository.java
public interface UserRepository {
    User findById(long id);
}
UserService.java
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();
    }
}
  1. Scrieți un test pentru UserService folosind Mockito.
  2. Mock-uiți UserRepository astfel încât:
    • pentru id = 1, să returneze un User(“Ana”)
  3. Verificați că metoda getUserName(1) returnează “Ana”.

Puteți folosi var pentru:

Denumirea și scopul claselor de mai sus respectă un standard folosit de framework-ul Spring.