Протягом останніх п’яти років на стіні мого офісу висить цитата: «Все починається з ідеї».
Моя дружина знайшла цей продукт на Etsy незабаром після того, як я почав розробляти колекцію API для програми для фітнесу. Мені подобається це твердження, тому що воно відображає пристрасть, яка поглинає мене на етапах створення нового проекту. Це все ще мій улюблений аспект роботи інженера, навіть через три десятиліття моєї кар’єри.
За цей час я зрозумів, що ідея має значення лише тоді, коли хтось має можливість її відчути. Якщо ідея надто довго втілюється в життя, ви втрачаєте можливість, оскільки хтось інший випереджає вас. Ось чому стартапи завжди прагнуть якомога швидше вивести свої ідеї на ринок.
Давайте розглянемо, як ми можемо втілити ідею в реальність… швидко.
Припущення
У цій статті ми зробимо все просто. Ми використаємо Java 17 і Spring Boot 3 для створення RESTful API. У цьому прикладі ми будемо використовувати Gradle для автоматизації збірки.
Хоча ідея сервісу, яку ми плануємо вивести на ринок, зазвичай використовує рівень збереження, ми відкладемо це для цього прикладу та статично визначимо наші дані в класі сховища.
Ми не будемо турбуватися про додавання будь-якої безпеки для цього прикладу, просто дозволяючи анонімний доступ для цього підтвердження концепції.
API мотиваційних цитат
Припустімо, що нашою ідеєю є API мотиваційних цитат. Щоб переконатися, що ми мчаємо якомога швидше, я попросив ChatGPT створити для мене специфікацію OpenAPI.
За кілька секунд ChatGPT надав відповідь:
Ось специфікація OpenAPI в YAML, згенерована ChatGPT:
openapi: 3.0.0 info: title: Motivational Quotes API description: An API that provides motivational quotes. version: 1.0.0 servers: - url: https://api.example.com description: Production server paths: /quotes: get: summary: Get all motivational quotes operationId: getAllQuotes responses: '200': description: A list of motivational quotes content: application/json: schema: type: array items: $ref: '#/components/schemas/Quote' /quotes/random: get: summary: Get a random motivational quote operationId: getRandomQuote responses: '200': description: A random motivational quote content: application/json: schema: $ref: '#/components/schemas/Quote' /quotes/{id}: get: summary: Get a motivational quote by ID operationId: getQuoteById parameters: - name: id in: path required: true schema: type: integer responses: '200': description: A motivational quote content: application/json: schema: $ref: '#/components/schemas/Quote' '404': description: Quote not found components: schemas: Quote: type: object required: - id - quote properties: id: type: integer quote: type: string
Мені потрібно було зробити лише одне оновлення вручну — переконатися, що властивості id
і quote
необхідні для схеми Quote
. І це лише тому, що я забув згадати про це обмеження для ChatGPT у своєму початковому запиті.
Таким чином, ми готові розробляти нову службу, використовуючи підхід API-First .
Створення служби завантаження Spring за допомогою API-First
Для цього прикладу я використаю Spring Boot CLI для створення нового проекту. Ось як ви можете встановити CLI за допомогою Homebrew:
$ brew tap spring-io/tap $ brew install spring-boot
Створіть нову службу завантаження Spring
Ми назвемо проект quotes
, створивши його за допомогою такої команди:
$ spring init --dependencies=web quotes
Розглянемо вміст папки quotes
:
$ cd quotes && ls -la total 72 drwxr-xr-x@ 11 jvester 352 Mar 1 10:57 . drwxrwxrwx@ 90 jvester 2880 Mar 1 10:57 .. -rw-r--r--@ 1 jvester 54 Mar 1 10:57 .gitattributes -rw-r--r--@ 1 jvester 444 Mar 1 10:57 .gitignore -rw-r--r--@ 1 jvester 960 Mar 1 10:57 HELP.md -rw-r--r--@ 1 jvester 545 Mar 1 10:57 build.gradle drwxr-xr-x@ 3 jvester 96 Mar 1 10:57 gradle -rwxr-xr-x@ 1 jvester 8762 Mar 1 10:57 gradlew -rw-r--r--@ 1 jvester 2966 Mar 1 10:57 gradlew.bat -rw-r--r--@ 1 jvester 28 Mar 1 10:57 settings.gradle drwxr-xr-x@ 4 jvester 128 Mar 1 10:57 src
Далі ми редагуємо файл build.gradle
, як показано нижче, щоб прийняти підхід API-First.
plugins { id 'java' id 'org.springframework.boot' version '3.4.3' id 'io.spring.dependency-management' version '1.1.7' id 'org.openapi.generator' version '7.12.0' } openApiGenerate { generatorName = "spring" inputSpec = "$rootDir/src/main/resources/static/openapi.yaml" outputDir = "$buildDir/generated" apiPackage = "com.example.api" modelPackage = "com.example.model" configOptions = [ dateLibrary: "java8", interfaceOnly: "true", useSpringBoot3: "true", useBeanValidation: "true", skipDefaultInterface: "true" ] } group = 'com.example' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.openapitools:jackson-databind-nullable:0.2.6' implementation 'io.swagger.core.v3:swagger-annotations:2.2.20' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } sourceSets { main { java { srcDirs += "$buildDir/generated/src/main/java" } } } compileJava.dependsOn tasks.openApiGenerate tasks.named('test') { useJUnitPlatform() }
Нарешті, ми поміщаємо згенеровану специфікацію OpenAPI в папку resources/static
як openapi.yaml
.
Створіть об’єкти API та Model
Після відкриття проекту в IntelliJ я виконав таку команду, щоб створити заглушки API та об’єкти моделі.
./gradlew clean build
Тепер ми можемо побачити api
та об’єкти model
, створені з нашої специфікації OpenAPI. Ось файл QuotesAPI.java
:
Додайте бізнес-логіку
Коли базова служба готова та вже дотримується нашого контракту OpenAPI, ми починаємо додавати деяку бізнес-логіку до служби.
Спочатку ми створюємо клас QuotesRepository
, який повертає дані для нашої служби. Як зазначалося вище, це зазвичай зберігається на якомусь спеціальному рівні збереження. Для цього прикладу жорстке кодування даних у п’яти цитатах працює просто чудово, і це тримає нас зосередженими.
@Repository public class QuotesRepository { public static final List<Quote> QUOTES = List.of( new Quote() .id(1) .quote("The greatest glory in living lies not in never falling, but in rising every time we fall."), new Quote() .id(2) .quote("The way to get started is to quit talking and begin doing."), new Quote() .id(3) .quote("Your time is limited, so don't waste it living someone else's life."), new Quote() .id(4) .quote("If life were predictable it would cease to be life, and be without flavor."), new Quote() .id(5) .quote("If you set your goals ridiculously high and it's a failure, you will fail above everyone else's success.") ); public List<Quote> getAllQuotes() { return QUOTES; } public Optional<Quote> getQuoteById(Integer id) { return Optional.ofNullable(QUOTES.stream().filter(quote -> quote.getId().equals(id)).findFirst().orElse(null)); } }
Далі ми створюємо QuotesService
, який буде взаємодіяти з QuotesRepository
. Цей підхід дозволить зберігати дані окремо від бізнес-логіки.
@RequiredArgsConstructor @Service public class QuotesService { private final QuotesRepository quotesRepository; public List<Quote> getAllQuotes() { return quotesRepository.getAllQuotes(); } public Optional<Quote> getQuoteById(Integer id) { return quotesRepository.getQuoteById(id); } public Quote getRandomQuote() { List<Quote> quotes = quotesRepository.getAllQuotes(); return quotes.get(ThreadLocalRandom.current().nextInt(quotes.size())); } }
Нарешті, нам просто потрібно реалізувати QuotesApi
створений на основі нашого підходу API-First:
@Controller @RequiredArgsConstructor public class QuotesController implements QuotesApi { private final QuotesService quotesService; @Override public ResponseEntity<List<Quote>> getAllQuotes() { return new ResponseEntity<>(quotesService.getAllQuotes(), HttpStatus.OK); } @Override public ResponseEntity<Quote> getQuoteById(Integer id) { return quotesService.getQuoteById(id) .map(quote -> new ResponseEntity<>(quote, HttpStatus.OK)) .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); } @Override public ResponseEntity<Quote> getRandomQuote() { return new ResponseEntity<>(quotesService.getRandomQuote(), HttpStatus.OK); } }
На даний момент ми маємо повнофункціональний Motivational Quotes API разом із невеликою колекцією відповідей.
Кілька останніх елементів
Spring Boot дає нам можливість використовувати веб-інтерфейс користувача Swagger Docs через залежність springdoc-openapi-starter-webmvc-ui
.
dependencies { ... implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.5' ... }
Хоча структура дозволяє інженерам використовувати прості анотації для опису свого API, ми можемо використовувати наш існуючий файл openapi.yaml
у папці resources/static
.
Ми можемо реалізувати цей підхід у файлі application-properties.yaml
разом із кількома іншими незначними оновленнями конфігурації:
server: port: ${PORT:8080} spring: application: name: quotes springdoc: swagger-ui: path: /swagger-docs url: openapi.yaml
Просто заради розваги, давайте додамо файл banner.txt
для використання під час запуску служби. Ми поміщаємо цей файл у папку resources
.
${AnsiColor.BLUE} _ __ _ _ _ ___ | |_ ___ ___ / _` | | | |/ _ \| __/ _ \/ __| | (_| | |_| | (_) | || __/\__ \ \__, |\__,_|\___/ \__\___||___/ |_| ${AnsiColor.DEFAULT} :: Running Spring Boot ${AnsiColor.BLUE}${spring-boot.version}${AnsiColor.DEFAULT} :: Port #${AnsiColor.BLUE}${server.port}${AnsiColor.DEFAULT} ::
Тепер, коли ми запускаємо службу локально, ми можемо побачити банер:
Після запуску ми можемо перевірити роботу Swagger Docs, відвідавши кінцеву точку /swagger-docs
.
Нарешті, ми створимо новий репозиторій на основі Git, щоб ми могли відстежувати будь-які майбутні зміни:
$ git init $ git add . $ git commit -m "Initial commit for the Motivational Quotes API"
Тепер давайте подивимося, як швидко ми зможемо розгорнути наш сервіс .
Використання Heroku для завершення подорожі
Поки що головною метою представлення моєї нової ідеї було створення специфікації OpenAPI та написання бізнес-логіки для мого сервісу. Spring Boot впорався з усім іншим за мене.
Коли справа доходить до запуску моєї служби, я віддаю перевагу використанню Heroku, оскільки вона чудово підходить для служб Spring Boot. Я можу швидко розгортати свої послуги, не занурюючись у проблеми хмарної інфраструктури. Heroku також полегшує передачу значень конфігурації для моїх програм на основі Java .
Щоб відповідати версії Java, яку ми використовуємо, ми створюємо файл system.properties
у кореневій папці проекту. Файл має один рядок:
java.runtime.version = 17
Потім я створюю Procfile
у тому ж місці для налаштування поведінки розгортання. Цей файл також має один рядок:
web: java -jar build/libs/quotes-0.0.1-SNAPSHOT.jar
Пора розгортатися. За допомогою Heroku CLI я можу розгорнути службу за допомогою кількох простих команд. Спочатку я автентифікую CLI, а потім створюю нову програму Heroku.
$ heroku login $ heroku create Creating app... done, vast-crag-43256 https://vast-crag-43256-bb5e35ea87de.herokuapp.com/ | https://git.heroku.com/vast-crag-43256.git
Мій екземпляр програми Heroku має назву vast-crag-43256
(я міг передати вказане ім’я), і служба працюватиме за адресою https://vast-crag-43256-bb5e35ea87de.herokuapp.com/.
Останнє, що потрібно зробити, це розгорнути службу за допомогою команди Git, щоб надіслати код до Heroku:
$ git push heroku master
Після завершення цієї команди ми можемо перевірити успішне розгортання через інформаційну панель Heroku:
Тепер ми готові взяти наш новий сервіс на тест-драйв!
Мотиваційні цитати в дії
Завдяки службі Motivational Quotes, запущеній на Heroku, ми можемо перевірити, чи все працює належним чином, використовуючи ряд команд curl
.
По-перше, давайте отримаємо повний список усіх п’яти мотиваційних цитат:
$ curl \ --location 'https://vast-crag-43256-bb5e35ea87de.herokuapp.com/quotes'
[ { "id":1, "quote":"The greatest glory in living lies not in never falling, but in rising every time we fall." }, { "id":2, "quote":"The way to get started is to quit talking and begin doing." }, { "id":3, "quote":"Your time is limited, so don't waste it living someone else's life." }, { "id":4, "quote":"If life were predictable it would cease to be life, and be without flavor." }, { "id":5, "quote":"If you set your goals ridiculously high and it's a failure, you will fail above everyone else's success." } ]
Давайте відновимо одну мотиваційну цитату за ідентифікатором:
$ curl \ --location 'https://vast-crag-43256-bb5e35ea87de.herokuapp.com/quotes/3'
{ "id":3, "quote":"Your time is limited, so don't waste it living someone else's life." }
Давайте наведемо випадкову мотиваційну цитату:
$ curl --location \ 'https://vast-crag-43256-bb5e35ea87de.herokuapp.com/quotes/random'
{ "id":5, "quote":"If you set your goals ridiculously high and it's a failure, you will fail above everyone else's success." }
Ми навіть можемо переглянути документи Swagger.
Висновок
Час виходу на ринок може змінити або зламати будь-яку ідею. Ось чому стартапи зосереджені на якнайшвидшому впровадженні своїх інновацій. Чим довше потрібно досягти фінішу, тим більший ризик того, що конкурент прибуде раніше вас.
Мої читачі можуть згадати мою особисту заяву про місію, яку, на мою думку, можна застосувати до будь-якого ІТ-фахівця:
"Зосередьте свій час на наданні функцій/функцій, які підвищують цінність вашої інтелектуальної власності. Використовуйте фреймворки, продукти та послуги для всього іншого".
— Дж. Вестер
У цій статті ми побачили, як Spring Boot обробляє все, що потрібно для впровадження RESTful API. Використовуючи ChatGPT, ми навіть змогли виразити людськими словами те, що ми хотіли бачити від нашої служби, і це створило для нас специфікацію OpenAPI за лічені секунди. Це дозволило нам використовувати підхід API-First. Коли ми були готові, ми змогли реалізувати нашу ідею за допомогою Heroku, видавши кілька команд CLI.
Spring Boot, ChatGPT і Heroku надали фреймворки та послуги, щоб я міг зосередитися на реалізації своєї ідеї. У результаті я зміг дотримуватися своєї особистої місії та, що більш важливо, швидко реалізувати свою ідею. Все, що мені потрібно було зробити, це зосередитися на бізнес-логіці моєї ідеї — і так воно і повинно бути!
Якщо вам цікаво, вихідний код цієї статті можна знайти на GitLab .
Гарного дня!