Posledních pět let jsem měl na zdi své kanceláře citát „všechno začíná nápadem“.
Moje žena našla tento produkt na Etsy krátce poté, co jsem začal vyvíjet kolekci API pro fitness aplikaci. Miluji toto prohlášení, protože zachycuje vášeň, která mě pohlcuje během fází tvorby nového projektu. I po třech desetiletích mé kariéry je to stále můj oblíbený aspekt práce inženýra.
Během této doby jsem se naučil, že na nápadu záleží pouze tehdy, má-li někdo možnost ho zažít. Pokud nápadu trvá příliš dlouho, než se stane skutečností, skončíte s promarněnou příležitostí, protože vás někdo jiný porazí. To je důvod, proč se startupy vždy předhánějí, aby dostaly své nápady na trh co nejrychleji.
Pojďme si projít, jak můžeme udělat nápad ve skutečnost... rychle.
V tomto článku budeme mít věci jednoduché. K vytvoření RESTful API použijeme Java 17 a Spring Boot 3. V tomto příkladu použijeme Gradle pro naši automatizaci sestavování.
Zatímco myšlenka služby, kterou plánujeme uvést na trh, by normálně používala vrstvu persistence, necháme ji stranou pro tento příklad a staticky definujeme naše data v rámci třídy úložiště.
Nebudeme si dělat starosti s přidáním jakéhokoli zabezpečení pro tento příklad, jednoduše povolíme anonymní přístup pro tento důkaz konceptu.
Předpokládejme, že naším nápadem je API motivačních nabídek. Abych se ujistil, že závodíme co nejrychleji, požádal jsem ChatGPT, aby pro mě vytvořil specifikaci OpenAPI.
ChatGPT během několika sekund poskytl odpověď:
Zde je specifikace OpenAPI v YAML, kterou ChatGPT vygeneroval:
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
Potřeboval jsem provést pouze jednu ruční aktualizaci – ujistit se, že pro schéma Quote
jsou vyžadovány vlastnosti id
a quote
. A to jen proto, že jsem zapomněl zmínit toto omezení pro ChatGPT ve své původní výzvě.
Díky tomu jsme připraveni vyvinout novou službu pomocí přístupu API-First .
V tomto příkladu použiji Spring Boot CLI k vytvoření nového projektu. Zde je návod, jak nainstalovat CLI pomocí Homebrew:
$ brew tap spring-io/tap $ brew install spring-boot
Projekt nazveme quotes
, vytvoříme jej pomocí následujícího příkazu:
$ spring init --dependencies=web quotes
Podívejme se na obsah složky s 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
Dále upravíme soubor build.gradle
, jak je ukázáno níže, abychom přijali přístup 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() }
Nakonec umístíme vygenerovanou specifikaci OpenAPI do složky resources/static
jako openapi.yaml
.
Po otevření projektu v IntelliJ jsem provedl následující příkaz k vytvoření pahýlů API a modelových objektů.
./gradlew clean build
Nyní můžeme vidět objekty api
a model
vytvořené z naší specifikace OpenAPI. Zde je soubor QuotesAPI.java
:
Když je základní služba připravena a již dodržuje naši smlouvu OpenAPI, začneme do služby přidávat určitou obchodní logiku.
Nejprve vytvoříme třídu QuotesRepository
, která vrací data pro naši službu. Jak bylo uvedeno výše, normálně by to bylo uloženo v nějaké vyhrazené perzistentní vrstvě. V tomto příkladu napevno zakódování dat v hodnotě pěti uvozovek funguje dobře a udržuje nás to soustředěné.
@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)); } }
Dále vytvoříme QuotesService
, která bude interagovat s QuotesRepository
. Tento přístup udrží data oddělená od obchodní logiky.
@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())); } }
Nakonec musíme implementovat QuotesApi
generované z našeho přístupu 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); } }
V tuto chvíli máme plně funkční API motivačních nabídek, doplněné malou sbírkou odpovědí.
Spring Boot nám dává možnost webového uživatelského rozhraní Swagger Docs prostřednictvím závislosti springdoc-openapi-starter-webmvc-ui
.
dependencies { ... implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.5' ... }
Zatímco framework umožňuje inženýrům používat jednoduché anotace k popisu jejich API, můžeme použít náš stávající soubor openapi.yaml
ve složce resources/static
.
Tento přístup můžeme implementovat v souboru application-properties.yaml
spolu s několika dalšími drobnými aktualizacemi konfigurace:
server: port: ${PORT:8080} spring: application: name: quotes springdoc: swagger-ui: path: /swagger-docs url: openapi.yaml
Jen pro zajímavost přidáme soubor banner.txt
pro použití při spuštění služby. Tento soubor umístíme do složky resources
.
${AnsiColor.BLUE} _ __ _ _ _ ___ | |_ ___ ___ / _` | | | |/ _ \| __/ _ \/ __| | (_| | |_| | (_) | || __/\__ \ \__, |\__,_|\___/ \__\___||___/ |_| ${AnsiColor.DEFAULT} :: Running Spring Boot ${AnsiColor.BLUE}${spring-boot.version}${AnsiColor.DEFAULT} :: Port #${AnsiColor.BLUE}${server.port}${AnsiColor.DEFAULT} ::
Nyní, když spustíme službu lokálně, můžeme vidět banner:
Po spuštění můžeme ověřit, že Swagger Docs fungují, návštěvou koncového bodu /swagger-docs
.
Nakonec vytvoříme nové úložiště založené na Git, abychom mohli sledovat jakékoli budoucí změny:
$ git init $ git add . $ git commit -m "Initial commit for the Motivational Quotes API"
Nyní se podívejme, jak rychle můžeme naši službu nasadit .
Doposud bylo primárním cílem při představení mého nového nápadu vytvořit specifikaci OpenAPI a napsat nějakou obchodní logiku pro mou službu. Všechno ostatní za mě zvládl Spring Boot.
Pokud jde o provozování mé služby, raději používám Heroku, protože se skvěle hodí pro služby Spring Boot. Své služby mohu nasadit rychle, aniž bych se dostal do problémů s cloudovou infrastrukturou. Heroku také usnadňuje předávání konfiguračních hodnot pro mé aplikace založené na Java .
Abychom odpovídali verzi Java, kterou používáme, vytvoříme soubor system.properties
v kořenové složce projektu. Soubor má jeden řádek:
java.runtime.version = 17
Poté vytvořím Procfile
ve stejném umístění pro přizpůsobení chování nasazení. Tento soubor má také jeden řádek:
web: java -jar build/libs/quotes-0.0.1-SNAPSHOT.jar
Je čas nasadit. S Heroku CLI mohu službu nasadit pomocí několika jednoduchých příkazů. Nejprve ověřím CLI a poté vytvořím novou aplikaci 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
Moje instance aplikace Heroku se jmenuje vast-crag-43256
(mohl jsem předat zadaný název) a služba poběží na https://vast-crag-43256-bb5e35ea87de.herokuapp.com/.
Poslední věcí, kterou musíte udělat, je nasadit službu pomocí příkazu Git k odeslání kódu do Heroku:
$ git push heroku master
Po dokončení tohoto příkazu můžeme ověřit úspěšné nasazení prostřednictvím řídicího panelu Heroku:
Nyní jsme připraveni vzít naši novou službu na testovací jízdu!
Se službou Motivational Quotes spuštěnou na Heroku můžeme pomocí řady příkazů curl
ověřit, zda vše funguje podle očekávání.
Nejprve si ukažme kompletní seznam všech pěti motivačních citátů:
$ 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." } ]
Pojďme získat jeden motivační citát podle ID:
$ 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." }
Pojďme si dát náhodný motivační citát:
$ 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." }
Můžeme dokonce procházet i Swagger Docs.
Čas uvedení na trh může vytvořit nebo rozbít jakýkoli nápad. To je důvod, proč se startupy laserově zaměřují na co nejrychlejší dodání svých inovací. Čím déle trvá dojetí do cíle, tím větší je riziko, že závodník dorazí před vámi.
Moji čtenáři si možná vzpomenou na mé osobní prohlášení o poslání, které se podle mého názoru může vztahovat na každého IT profesionála:
"Zaměřte svůj čas na poskytování funkcí/funkcí, které rozšiřují hodnotu vašeho duševního vlastnictví. Využijte rámce, produkty a služby pro vše ostatní."
— J. Vester
V tomto článku jsme viděli, jak Spring Boot zvládl vše potřebné k implementaci RESTful API. S využitím ChatGPT jsme dokonce byli schopni vyjádřit lidskými slovy, co jsme chtěli, aby naše služba byla, a během několika sekund pro nás vytvořil specifikaci OpenAPI. To nám umožnilo využít přístup API-First. Jakmile jsme byli připraveni, byli jsme schopni doručit náš nápad pomocí Heroku vydáním několika příkazů CLI.
Spring Boot, ChatGPT a Heroku poskytly rámce a služby, takže jsem mohl zůstat laserově zaměřený na realizaci svého nápadu. Díky tomu jsem se mohl držet svého osobního poslání, a co je důležitější, rychle realizovat svůj nápad. Jediné, co jsem musel udělat, bylo zaměřit se na obchodní logiku za mým nápadem – a tak to má být!
Pokud máte zájem, zdrojový kód tohoto článku najdete na GitLabu .
Mějte opravdu skvělý den!