在 Java 開發領域,Spring Boot 已成為建立健壯、可擴充套件且可維護的 Web 應用程式的代名詞。傳統上,構建 Spring Boot 應用程式需要設定一個具有複雜目錄結構、多個配置檔案和各種依賴項的專案。然而,隨著 JBang(一種輕量級 Java 指令碼編寫工具)的出現,您可以簡化此過程並僅使用單個 Java 檔案構建 Spring Boot Rest Api。在這篇博文中,我們將指導您完成在單個 Java 檔案中使用 JBang 建立 Spring Boot Rest Api 的步驟。
JBang是什麼?
JBang 是一個命令列工具,允許您直接從原始檔執行 Java 程式碼,而不需要複雜的專案設定或編譯。它對於建立輕量級指令碼和簡化開發過程特別有用。
在我們深入開發過程之前,請確保您的系統上安裝了 JBang。您可以從JBang的官方網站安裝它。
您可以克隆https://github.com/dmakariev/examples儲存庫。
git clone https:<font>//github.com/dmakariev/examples.git<i> cd examples/jbang/spring-boot-hello-world
|
入門
讓我們建立一個簡單的 Spring Boot Rest 服務來提供“Hello, World!”服務使用 JBang 傳送訊息。按著這些次序:
1、初始化新的 JBang 指令碼
為您的專案建立一個新目錄並使用終端導航到該目錄。然後,建立一個新的 JBang 指令碼檔案.java,副檔名為springbootHelloWorld.java.
$ mkdir spring-boot-hello $ cd spring-boot-hello $ touch springbootHelloWorld.java
|
2、編寫 Spring Boot 程式碼
在您喜歡的文字編輯器或整合開發環境 (IDE) 中開啟該springbootHelloWorld.java檔案並新增以下程式碼。
<font>//usr/bin/env jbang "$0" "$@" ; exit $?<i> //JAVA 21<i> //DEPS org.springframework.boot:spring-boot-starter-web:3.1.4<i>
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestParam;
@SpringBootApplication @RestController public class springbootHelloWorld {
public static void main(String[] args) { SpringApplication.run(springbootHelloWorld.class, args); }
@GetMapping("/") public String sayHi(@RequestParam(required = false, defaultValue = "World") String name) { return "Hello, " + name + "!"; } }
|
執行以下操作:
- 準備要像 shell 指令碼一樣執行的檔案
- 定義應用程式所需的 Java 版本(Java 17 是 Spring Boot 3.x 的最低版本,但 Java 21 是當前的 LTS )
- 匯入必要的 Spring Boot 依賴項。
- 定義 Spring Boot 應用程式類。
- 定義一個帶有返回“Hello, World!”的單個端點的 REST 控制器。
執行應用程式
儲存檔案並返回到您的終端。導航到包含檔案的目錄springbootHelloWorld.java並執行以下命令:
jbang springbootHelloWorld.java
或者
sh springbootHelloWorld.java
執行允許可執行許可權
chmod +x springbootHelloWorld.java
你甚至可以像這樣執行應用程式
./springbootHelloWorld.java
在上述所有情況下,JBang 將下載所需的 Spring Boot 依賴項並啟動應用程式。您將看到指示 Spring Boot 應用程式正在執行的輸出。
開啟您的網路瀏覽器並導航至http://localhost:8080。您應該看到“Hello, World!”瀏覽器中顯示的訊息。
用JBang 來建立 Spring Boot完整單體
僅使用單個 Java 檔案(用於後端)和單個 HTML 檔案(用於前端)以及 JBang 來建立 Spring Boot Monolith。
這種方法對於快速原型設計、輕量級應用程式或當您想要降低開發環境的複雜性時非常方便。隨著您的應用程式變得越來越複雜,您始終可以過渡到更傳統的專案結構。JBang提供了一種靈活、高效的方法來開發 Java 應用程式,而無需進行重量級的專案設定。
1、初始化目錄
為專案建立一個新目錄,並使用終端導航到該目錄。然後,建立 :
- 一個副檔名為 .java 的 JBang 指令碼空檔案,如 springbootJpaVue.java。
- 一個副檔名為 .html 的空檔案,用於 Vue.js UI 應用程式,如 index-fetch.html。
- 一個空的 Dockerfile
- 一個空的 Docker Compose 檔案 compose.yaml
$ mkdir spring-boot-jpa-vue $ cd spring-boot-jpa-vue $ touch springbootJpaVue.java $ touch index-fetch.html $ touch Dockerfile $ touch compose.yaml
|
2、編寫 Spring Boot 程式碼
在您喜歡的文字編輯器或整合開發環境(IDE)中開啟 springbootJpaVue.java 檔案,然後新增以下程式碼。
<font>//usr/bin/env jbang "$0" "$@" ; exit $?<i> //JAVA 22<i> //DEPS org.springframework.boot:spring-boot-dependencies:3.2.4@pom<i> //DEPS org.springframework.boot:spring-boot-starter-web<i> //DEPS org.springframework.boot:spring-boot-starter-data-jpa<i> //DEPS org.springframework.boot:spring-boot-starter-actuator<i> //DEPS com.h2database:h2<i> //DEPS org.postgresql:postgresql<i> //DEPS org.projectlombok:lombok<i> //DEPS org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0<i>
//JAVA_OPTIONS -Dserver.port=8080<i> //JAVA_OPTIONS -Dspring.datasource.url=jdbc:h2:mem:person-db;MODE=PostgreSQL;<i> //JAVA_OPTIONS -Dspring.h2.console.enabled=true -Dspring.h2.console.settings.web-allow-others=true<i> //JAVA_OPTIONS -Dmanagement.endpoints.web.exposure.include=health,env,loggers<i> //FILES META-INF/resources/index.html=index-fetch.html<i>
//REPOS mavencentral,sb_snapshot=https://repo.spring.io/snapshot,sb_milestone=https://repo.spring.io/milestone<i> package com.makariev.examples.jbang;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RequestParam;
import java.util.List; import java.util.Optional;
import jakarta.persistence.Entity; import jakarta.persistence.Table; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id;
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable;
import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component;
@SpringBootApplication public class springbootJpaVue {
public static void main(String[] args) { SpringApplication.run(springbootJpaVue.class, args); }
}
@Component @RequiredArgsConstructor class InitialRecords {
private final PersonRepository personRepository;
@EventListener(ApplicationReadyEvent.class) public void exercise() {
if (personRepository.count() > 0) { return; } List.of( new Person(1L, "Ada", "Lovelace", 1815), new Person(2L, "Niklaus", "Wirth", 1934), new Person(3L, "Donald", "Knuth", 1938), new Person(4L, "Edsger", "Dijkstra", 1930), new Person(5L, "Grace", "Hopper", 1906), new Person(6L, "John", "Backus", 1924) ).forEach(personRepository::save); } }
@RestController class HiController {
@GetMapping("/hi") public String sayHi(@RequestParam(required = false, defaultValue = "World") String name) { return "Hello, " + name + "!"; } }
@RestController @RequestMapping("/api/persons") @RequiredArgsConstructor class PersonController {
private final PersonRepository personRepository;
@GetMapping public Page<Person> findAll(Pageable pageable) { return personRepository.findAll(pageable); }
@GetMapping("{id}") public Optional<Person> findById(@PathVariable("id") Long id) { return personRepository.findById(id); }
@PostMapping public Person create(@RequestBody Person person) { return personRepository.save(person); }
@PutMapping("{id}") public Person updateById(@PathVariable("id") Long id, @RequestBody Person person) { var loaded = personRepository.findById(id).orElseThrow(); loaded.setFirstName(person.getFirstName()); loaded.setLastName(person.getLastName()); loaded.setBirthYear(person.getBirthYear()); return personRepository.save(loaded); }
@DeleteMapping("/{id}") public void deleteById(@PathVariable("id") Long id) { personRepository.deleteById(id); } }
@Data @Entity @Table(name = "person") @NoArgsConstructor @AllArgsConstructor class Person {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String firstName; private String lastName; private int birthYear; }
interface PersonRepository extends JpaRepository<Person, Long> { }
|
3、編寫 Vue.js 程式碼
在您喜歡的文字編輯器或整合開發環境(IDE)中開啟 index-fetch.html 檔案,然後新增以下程式碼。
<!DOCTYPE html> <html> <head> <title>Person CRUD Application</title> <style> body { font-family: -apple-system,<font>"Segoe UI",Helvetica,Arial,sans-serif; margin: 0; padding: 0; background-color: f5f5f5; }
app { max-width: 800px; margin: 0 auto; padding: 20px; background-color: fff; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2); }
h1 { text-align: center; margin-bottom: 20px; }
ul { list-style: none; padding: 0; }
li { border: 1px solid ddd; padding: 10px; margin: 10px 0; background-color: fff; display: flex; justify-content: space-between; align-items: center; }
button { padding: 5px 10px; background-color: #007bff; color: fff; border: none; cursor: pointer; }
form { display: flex; flex-direction: column; }
input { margin: 5px 0; padding: 5px; border: 1px solid ddd; }
button[type="submit"] { background-color: #28a745; }
.modal { position: fixed; z-index: 1; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.4); justify-content: center; align-items: center; }
.modal-content { background-color: fff; border: 1px solid ddd; padding: 20px; width: 70%; margin: 10% auto; }
.pagination { display: flex; justify-content: center; margin-top: 20px; }
.page-item { margin: 0 5px; cursor: pointer; }
.page-item.active { font-weight: bold; } </style> <script src="https://cdn.jsdelivr.net/npm/vue@3.4.21/dist/vue.global.prod.js"></script> </head> <body> <div id="app"> <h1>Person CRUD Application</h1> <button @click="showPersonModal(null)">Add Person</button> <ul> <li v-for="person in persons" :key="person.id"> {{ person.firstName }} {{ person.lastName }} ({{ person.birthYear }} year of birth) <button @click="showPersonModal(person)">Edit</button> <button @click="deletePerson(person.id)">Delete</button> </li> </ul> <div class="pagination"> <span v-for="page in totalPages" :key="page" @click="changePage(page)" class="page-item" :class="{ active: currentPage === page }">{{ page }}</span> </div>
<!-- Modal --> <div class="modal" v-if="modalVisible"> <div class="modal-content"> <h2>{{ editMode ? 'Edit' : 'Add' }} Person</h2> <form @submit.prevent="savePerson"> <input type="text" v-model="formData.firstName" placeholder="First Name" required> <input type="text" v-model="formData.lastName" placeholder="Last Name" required> <input type="number" v-model="formData.birthYear" placeholder="Year of birth" required> <button type="submit">{{ editMode ? 'Update' : 'Add' }}</button> <button @click="closeModal">Cancel</button> </form> </div> </div> </div>
<script> const {createApp, ref, computed} = Vue;
createApp({ data() { return { persons: [], modalVisible: false, editMode: false, formData: { firstName: '', lastName: '', age: '' }, editedPersonId: null, pageSize: 5, currentPage: 1, totalPages: 1 }; }, methods: { getAllPersons(page) { fetch(`/api/persons?page=${page - 1}&size=${this.pageSize}`) .then(response => response.json()) .then(data => { this.persons = data.content; this.totalPages = data.totalPages; }) .catch(error => { console.error('Error fetching persons:', error); }); }, showPersonModal(person) { this.editMode = !!person; this.modalVisible = true; if (person) { this.editedPersonId = person.id; this.formData = {...person}; } else { this.resetForm(); } }, savePerson() { if (this.editMode) { fetch(`/api/persons/${this.editedPersonId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.formData) }) .then(() => { this.getAllPersons(this.currentPage); this.closeModal(); }) .catch(error => { console.error('Error updating person:', error); }); } else { fetch('/api/persons', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.formData) }) .then(() => { this.getAllPersons(this.currentPage); this.closeModal(); }) .catch(error => { console.error('Error adding person:', error); }); } }, deletePerson(personId) { fetch(`/api/persons/${personId}`, { method: 'DELETE' }) .then(() => { this.getAllPersons(this.currentPage); }) .catch(error => { console.error('Error deleting person:', error); }); }, closeModal() { this.modalVisible = false; this.editMode = false; this.resetForm(); }, resetForm() { this.formData = { firstName: '', lastName: '', age: '' }; this.editedPersonId = null; }, changePage(page) { this.currentPage = page; this.getAllPersons(page); } }, mounted() { this.getAllPersons(this.currentPage); } }).mount('app'); </script> </body> </html>
|
4、編寫 Dockerfile
用你喜歡的文字編輯器開啟 Dockerfile 檔案,新增以下程式碼。
FROM public.ecr.aws/docker/library/amazoncorretto:21-alpine AS build
RUN apk --no-cache add bash RUN apk --no-cache add curl RUN mkdir /app WORKDIR /app COPY . /app
RUN curl -Ls https:<font>//sh.jbang.dev | bash -s - export portable springbootJpaVue.java<i>
FROM public.ecr.aws/docker/library/amazoncorretto:21-alpine RUN mkdir /app/ RUN mkdir /app/lib COPY --from=build /app/springbootJpaVue.jar /app/springbootJpaVue.jar COPY --from=build /app/lib/* /app/lib/ WORKDIR /app
ENTRYPOINT ["java","-jar","springbootJpaVue.jar"]
|
編寫 Docker Compose 檔案
用你喜歡的文字編輯器開啟 compose.yaml 檔案,然後新增以下程式碼。
services: backend: build: . ports: - 8080:8088 environment: - SERVER_PORT=8088 - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/example - SPRING_DATASOURCE_USERNAME=postgres - SPRING_DATASOURCE_PASSWORD=pass-example - SPRING_JPA_HIBERNATE_DDL_AUTO=update networks: - spring-postgres db: image: postgres restart: always volumes: - db-data:/var/lib/postgresql/data networks: - spring-postgres environment: - POSTGRES_DB=example - POSTGRES_PASSWORD=pass-example expose: - 5432 pgadmin: container_name: pgadmin image: dpage/pgadmin4 environment: PGADMIN_DEFAULT_EMAIL: admin_not_used@user.com PGADMIN_DEFAULT_PASSWORD: admin_not_used PGADMIN_CONFIG_SERVER_MODE: 'False' volumes: - pgadmin:/var/lib/pgadmin ports: - "5050:80" networks: - spring-postgres restart: always volumes: db-data: pgadmin: networks: spring-postgres:
|
執行應用程式
我們建立了 Spring Boot Monolith 應用程式。它由兩個原始檔和兩個用於 docker 的配置檔案組成。
- springbootJpaVue.java 是後端,作為 Spring Boot Java 應用程式實現,其中還包含一些預設值
- index-fetch.html 是前臺,使用 Vue.js 作為獨立指令碼實現standalone script
這兩個檔案的關聯方式是使用 JBang 指令
//FILES META-INF/resources/index.html=index-fetch.html
|
應用程式有一個可以儲存在資料庫中的 jpa 實體 Person。
返回終端:導航到包含 springbootJpaVue.java 的目錄
應用程式可配置為使用以下兩種資料庫之一執行:
$ jbang -Dspring.datasource.url=jdbc:h2:mem:person-db \ springbootJpaVue.java
|
$ jbang -Dspring.datasource.url=jdbc:h2:file:./person-db-data \ -Dspring.jpa.hibernate.ddl-auto=update \
|
springbootJpaVue.java
- Postgres,它需要 Postgres 的本地主機例項
$ jbang -Dspring.datasource.url=jdbc:postgresql://localhost:5432/example \ -Dspring.datasource.username=postgres \ -Dspring.datasource.password=postgres \ -Dspring.jpa.hibernate.ddl-auto=update springbootJpaVue.java
|
執行預設設定檔案,並執行以下任意命令:
$ jbang springbootJpaVue.java
$ sh springbootJpaVue.java
如您透過執行允許可執行許可權:
$ chmod +x springbootJpaVue.java
你甚至可以像這樣執行應用程式
$ ./springbootJpaVue.java
你可以建立一個fatJar
$ jbang export fatjar springbootJpaVue.java
然後執行它
$ jbang springbootJpaVue-fatjar.jar
或者像普通的java應用程式一樣
$ java -jar springbootJpaVue-fatjar.jar
您可以建立一個包含所有依賴項的./lib資料夾的可移動的 jar 檔案
$ jbang export portable springbootJpaVue.java
然後執行它
$ jbang springbootJpaVue.jar
或者像普通的java應用程式一樣
$ java -jar springbootJpaVue.jar
docker compose:
$ docker compose up
在上述所有情況下,JBang 將下載所需的 Spring Boot 依賴項並啟動應用程式。您將看到指示 Spring Boot 應用程式正在執行的輸出。
CRUD
要建立新人員,請使用 POST 方法並將人員資料作為 JSON 正文:
$ curl -X POST -H "Content-Type: application/json" \ -d '{"firstName":"Katherine", "lastName":"Johnson", "birthYear":1919}' \ http://localhost:8080/api/persons
|
要獲取所有人的列表,請使用 GET 方法:
$ curl -X GET http://localhost:8080/api/persons
要透過 id 獲取特定人員,請使用 GET 方法並將 id 作為路徑變數:
$ curl -X GET http://localhost:8080/api/persons/1
要按 ID 更新現有人員,請使用 PUT 方法並將人員資料作為 JSON 正文:
$ curl -X PUT -H "Content-Type: application/json" \ -d '{"firstName":"Katherine", "lastName":"Johnson", "birthYear":1918}' \ http://localhost:8080/api/persons/1
|
在帶有 HTTPIE 的終端/CLI 中
您可以從此處下載替代終端/CLI 客戶端https://httpie.io/cli
要建立新人員,請使用 POST 方法並將人員資料作為 JSON 正文:
$ http POST http://localhost:8080/api/persons firstName=Alice lastName=Smith birthYear=1996
|
要獲取所有人的列表,請使用 GET 方法:
$ http GET http://localhost:8080/api/persons
|
要透過 id 獲取特定人員,請使用 GET 方法並將 id 作為路徑變數:
$ http GET http://localhost:8080/api/persons/1
|
要按 ID 更新現有人員,請使用 PUT 方法並將人員資料作為 JSON 正文:
$ http PUT http://localhost:8080/api/persons/1 firstName=Bob lastName=Jones birthYear=1990
|
要透過 id 刪除現有人員,請使用 DELETE 方法並將 id 作為路徑變數:
$ http DELETE http://localhost:8080/api/persons/1
|
實施細節
Spring Data Jpa 依賴項
要啟用 JPA(即 Java/Jakarta 永續性 API),我們需要
//DEPS org.springframework.boot:spring-boot-starter-data-jpa:3.1.4
|
我們還需要一個資料庫,因此我們將新增 H2 資料庫的依賴關係,該部分變為
//DEPS org.springframework.boot:spring-boot-starter-web:3.1.4 //DEPS org.springframework.boot:spring-boot-starter-data-jpa:3.1.4 //DEPS com.h2database:h2:2.2.224
|
為了最大限度地減少樣板程式碼,我們還將新增 Lombok
//DEPS org.springframework.boot:spring-boot-starter-web:3.1.4 //DEPS org.springframework.boot:spring-boot-starter-data-jpa:3.1.4 //DEPS com.h2database:h2:2.2.224 //DEPS org.projectlombok:lombok:1.18.30
|
JBang支援檔案匯入.pom,我們將依賴項更改為
//DEPS org.springframework.boot:spring-boot-dependencies:3.1.4@pom //DEPS org.springframework.boot:spring-boot-starter-web //DEPS org.springframework.boot:spring-boot-starter-data-jpa //DEPS com.h2database:h2 //DEPS org.projectlombok:lombok
|
正如您所看到的,依賴版本已被刪除,Spring Boot 版本僅定義一次。
永續性:Person實體和儲存庫
這是 JPA 實體和資料儲存庫
@Data @Entity @Table(name = "person") @NoArgsConstructor @AllArgsConstructor class Person {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String firstName; private String lastName; private int birthYear; }
interface PersonRepository extends JpaRepository<Person, Long> { }
|
PersonController
@RestController @RequestMapping("/api/persons") @RequiredArgsConstructor class PersonController {
private final PersonRepository personRepository;
@GetMapping public Page<Person> findAll(Pageable pageable) { return personRepository.findAll(pageable); }
@GetMapping("{id}") public Optional<Person> findById(@PathVariable("id") Long id) { return personRepository.findById(id); }
@PostMapping public Person create(@RequestBody Person person) { return personRepository.save(person); }
@PutMapping("{id}") public Person updateById(@PathVariable("id") Long id, @RequestBody Person person) { var loaded = personRepository.findById(id).orElseThrow(); loaded.setFirstName(person.getFirstName()); loaded.setLastName(person.getLastName()); loaded.setBirthYear(person.getBirthYear()); return personRepository.save(loaded); }
@DeleteMapping("/{id}") public void deleteById(@PathVariable("id") Long id) { personRepository.deleteById(id); } }
|
OpenAPI 支援並啟用 Swagger UI
我們正在使用該springdoc專案。要啟用它,我們所要做的就是新增以下依賴項
//DEPS org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0
|
重新啟動應用程式後,您將 在以下 URL獲得Swagger UIhttp://localhost:8080/swagger-ui/index.html
啟用 H2 控制檯應用程式
H2 控制檯應用程式允許您使用瀏覽器介面訪問 SQL 資料庫。要啟用它,我們需要在依賴項部分之後新增以下配置
//JAVA_OPTIONS -Dspring.h2.console.enabled=true //JAVA_OPTIONS -Dspring.h2.console.settings.web-allow-others=true //JAVA_OPTIONS -Dspring.datasource.url=jdbc:h2:mem:person-db;MODE=PostgreSQL;
|
訪問應用程式
- http://localhost:8080/:使用 Vue.JS 構建的基於 Web 的使用者介面
- http://localhost:8080/h2-console:H2 SQL 控制檯應用程式
- http://localhost:8080/v3/api-docs:開放API定義
- http://localhost:8080/swagger-ui/index.html:swagger
- http://localhost:8080/actuator:Spring Boot 執行器端點
- http://localhost:5050/:當使用 docker compose 執行時,該應用程式提供對 Web 版本的 PgAdmin 的訪問,使您可以使用瀏覽器介面訪問 SQL 資料庫。
- http://localhost:8080/hi:應該看到“Hello, World!”瀏覽器中顯示的訊息。