Teil 1: Rest Service mit Spring Boot 2.5 und Java 17

Motivation

Ziel ist es einen Baukasten für zukünftige Anwendungen zu erstellen um „mal eben schnell“ einen Service, Webanwendung, Tool oder ein Test erstellen zu können. Das ganze soll mit möglichst aktuellen Mitteln umgesetzt werden.
Sprint Boot: 2.5.5
Java: 17
Gradle: 7.2
Intellij: 2021.2.3

Dieses Beispiel enthält nur einen H2 In-Memory Datenbank und kein Webfrontend. In späteren Teilen sollen weitere Aspekte hinzukommen.

Einrichtung

Am einfachsten ist es über den Spring Initializr ein neues Projekt zu erstellen mit allen gewünschten dependencies.

Spring Initializr dependecies

JPA Entity erstellen

Mit der @Entity Annotation wird eine Java Klasse zu einer Datenbank Mapping Klasse. Mit @Id und @GeneratedValue wird eine Automatisch generierte ID erstellt. Die @Data Annotation von Lombok erstellt automatisch getter(), setter() und weitere Methoden.

@Data
@Entity
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String summary;
    private float value;
}

Zugriff auf die Daten über ein Repository

Ein sehr einfacher Weg Rest Methoden für eine Datenklasse zur Verfügung zu stellen ist mit der @RepositoryRestResource Annotation. Dadurch wird ein ganzer Service mit create, delete und update Methoden erstellt. Auch das implementieren von findBy() Methoden ist sehr einfach möglich. In dem Beispiel reicht es nur die Methode im Interface zu deklarieren um nach einem Attribut in der Tabelle zu suchen.

@RepositoryRestResource(collectionResourceRel = "banking", path = "banking")
public interface TransactionRepository extends PagingAndSortingRepository<Transaction, Long> {

    List<Transaction> findBySummary(@Param("summary") String summary);
}

JUnit Tests

Das Testen des TransactionRepository kann über die @SpringBootTest und @Autowired Annotation einfach getestet werden. Spring bietet aber auch die Möglichkeit das Restservice Verhalten mit zu testen. Dazu muss ein MockMvc eingebunden werden.

@SpringBootTest
@AutoConfigureMockMvc
public class AccessingDataRestApplicationTests {

    private static final String TRANSACTION_JSON = """
            {"summary": "Bäcker", "value": 5.49}
            """;

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private TransactionRepository personRepository;

    @BeforeEach
    public void deleteAllBeforeTests() throws Exception {
        personRepository.deleteAll();
    }

    @Test
    public void shouldReturnRepositoryIndex() throws Exception {

        mockMvc.perform(get("/"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$._links.banking").exists());
    }

    @Test
    public void shouldCreateEntity() throws Exception {

        mockMvc.perform(post("/banking").content(TRANSACTION_JSON))
                .andExpect(status().isCreated())
                .andExpect(header().string("Location", containsString("banking/")));
    }

    @Test
    public void shouldRetrieveEntity() throws Exception {
        String location = createTransaction();
        mockMvc.perform(get(location))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.summary").value("Bäcker"))
                .andExpect(jsonPath("$.value").value(5.49));
    }

    @Test
    public void shouldQueryEntity() throws Exception {
        createTransaction();

        mockMvc.perform(get("/banking/search/findBySummary?summary={summary}", "Bäcker"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$._embedded.banking[0].summary").value("Bäcker"))
                .andExpect(jsonPath("$._embedded.banking[0].value").value("5.49"));
    }

    @Test
    public void shouldUpdateEntity() throws Exception {
        String location = createTransaction();

        mockMvc.perform(put(location).content("{\"summary\": \"Bäcker-Changed\"}"))
                .andExpect(status().isNoContent());

        mockMvc.perform(get(location)).andExpect(status().isOk())
                .andExpect(jsonPath("$.summary").value("Bäcker-Changed"))
                .andExpect(jsonPath("$.value").value(0.0));
    }

    @Test
    public void shouldPartiallyUpdateEntity() throws Exception {
        String location = createTransaction();

        mockMvc.perform(patch(location).content("{\"summary\": \"Bäcker-Changed\"}"))
                .andExpect(status().isNoContent());

        mockMvc.perform(get(location))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.summary").value("Bäcker-Changed"))
                .andExpect(jsonPath("$.value").value(5.49));
    }

    @Test
    public void shouldDeleteEntity() throws Exception {
        String location = createTransaction();
        mockMvc.perform(delete(location)).andExpect(status().isNoContent());

        mockMvc.perform(get(location)).andExpect(status().isNotFound());
    }

    private String createTransaction() throws Exception {
        MvcResult mvcResult = mockMvc.perform(post("/banking").content(TRANSACTION_JSON))
                .andExpect(status().isCreated()).andReturn();

        String location = mvcResult.getResponse().getHeader("Location");
        Assertions.assertNotNull(location);
        return location;
    }
}

Eigener Controller

Für spezielle Anwendungsfälle kann nun ein eigener Controller verwendet werden. In diesem Beispiel werden zwei Transaktionen zusammen gelegt.

@RestController
public class TransactionController {

    @Autowired
    private TransactionRepository transactionRepository;

    @GetMapping("/mergeTransactions")
    public ResponseEntity<Transaction> mergeTransactions(@RequestParam(value = "id1") long id1, @RequestParam(value = "id2") long id2) {
        Optional<Transaction> transaction1Optional = transactionRepository.findById(id1);
        Optional<Transaction> transaction2Optional = transactionRepository.findById(id2);
        if (transaction1Optional.isPresent() && transaction2Optional.isPresent()) {
            Transaction transaction1 = transaction1Optional.get();
            Transaction transaction2 = transaction2Optional.get();

            transaction1.setSummary(transaction1.getSummary() + " + " + transaction2.getSummary());
            transaction1.setValue(transaction1.getValue() + transaction2.getValue());

            transaction1 = transactionRepository.save(transaction1);
            transactionRepository.delete(transaction2);

            //TODO missing error handling

            return ResponseEntity.ok(transaction1);
        }
        return ResponseEntity.notFound().build();
    }
}

Controller Test

Der Controller kann über @Autowired im Testfall eingebunden werden und sehr einfach getestet werden.

@SpringBootTest
class TransactionControllerTest {

    @Autowired
    private TransactionController transactionController;

    @Autowired
    private TransactionRepository transactionRepository;

    @Test
    void mergeTransactions() {
        Transaction transaction1 = new Transaction();
        transaction1.setSummary("Apples");
        transaction1.setValue(1.5f);
        transaction1 = transactionRepository.save(transaction1);

        Transaction transaction2 = new Transaction();
        transaction2.setSummary("Bananas");
        transaction2.setValue(3.5f);
        transaction2 = transactionRepository.save(transaction2);

        ResponseEntity<Transaction> merged = transactionController.mergeTransactions(transaction1.getId(), transaction2.getId());
        Transaction mergedTransaction = merged.getBody();

        Assertions.assertNotNull(mergedTransaction);
        Assertions.assertEquals(transaction1.getId(), mergedTransaction.getId());
        Assertions.assertEquals("Apples + Bananas", mergedTransaction.getSummary());
        Assertions.assertEquals(5f, mergedTransaction.getValue());

    }

}

Gradle 7.2 in Intellij

Die Download URL von Gradle 7.2 musste in der gradle-wrapper.properties manuell angegeben werden.

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https://services.gradle.org/distributions/gradle-7.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

Java 17 in Intellij

Intellij unterstützt bereits Java 17. Es kann auch direkt über die IDE herunter geladen werden. Noch warnt die IDE davor, da experimentell in der IDE ist. Ich musste die Sprachfeatures manuell auf Java 17 stellen.

Intellij project settings

Quellen

https://spring.io/guides/gs/rest-service/

https://spring.io/guides/gs/accessing-data-rest/

https://start.spring.io/

Artikel als PDF laden

Eine Antwort auf „Teil 1: Rest Service mit Spring Boot 2.5 und Java 17“

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.