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.
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.
Quellen
https://spring.io/guides/gs/rest-service/
Eine Antwort auf „Teil 1: Rest Service mit Spring Boot 2.5 und Java 17“