かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Gradle multi-project 内に React アプリ(frontend-app)と Spring Boot アプリ(backend-app)を作成する

概要

記事一覧はこちらです。

Gradle multi-project 内に React+Typescript+Tailwind CSS+React Query 構成の frontend 用アプリと、Sping Boot+springdoc-openapi 構成の REST API を提供する backend 用アプリを作成し、以下の内容を実現します。

f:id:ksby:20210428231340p:plain

  • Spring Boot の REST API には @CrossOrigin を付与しません。
  • 開発中も本番も frontend から backend の REST API を呼び出す時の URL はパス名のみの /user/1 にします(http://localhost:8080/user/1 のようにホスト名等は記述しません)。
  • 開発中は CRACOyarn start で起動する webpack-dev-server に proxy を設定して、webpack-dev-server 経由で backend 用アプリの REST API にアクセスします。  ※Configuring the Proxy Manually に記述されている src/setupProxy.js を作成・設定する方法もありますが、Tailwind CSS を使う時に CRACO を入れているので今回は CRACO を使う方法を採用しています。
  • build タスク実行時に yarn build コマンドを実行 → React アプリのファイルを Spring Boot アプリへコピーしてから、Spring Boot アプリの jar ファイルを生成します。

今回作成したソースは ksbysample-react-springboot に置いています。

参照したサイト・書籍

  1. React Query
    https://react-query.tanstack.com/

  2. CRACO - Configuration
    https://github.com/gsoft-inc/craco/blob/master/packages/craco/README.md

  3. webpack - DevServer - devServer.proxy
    https://webpack.js.org/configuration/dev-server/#devserverproxy

  4. Create React App - Proxying API Requests in Development
    https://create-react-app.dev/docs/proxying-api-requests-in-development/

  5. node-gradle / gradle-node-plugin - Usage
    https://github.com/node-gradle/gradle-node-plugin/blob/master/docs/usage.md

目次

  1. ksbysample-react-springboot レポジトリを作成して clone する
  2. Spring Initializr でサンプルプロジェクトを作成して Gradle Wrapper のファイルをコピーする
  3. gradlew init を実行し、build.gradle があるサブプロジェクトを自動認識するよう settings.gradle を変更する
  4. backend-app サブプロジェクトを作成し、REST API を実装する
  5. npx create-react-app frontend-app --template typescript を実行して frontend-app サブプロジェクトを作成する
  6. Tailwind CSS、CRACO をインストールする
  7. React Query をインストールする
  8. frontend-app に REST API を呼び出して取得したデータを表示する処理を実装する
  9. craco.config.js を変更し webpack-dev-server に proxy の設定を追加する
  10. Spring Boot アプリを IDEA から起動、React アプリを yarn start で起動して動作確認する
  11. frontend-app サブプロジェクトで Gradle の build タスクが実行されるようにし、かつ build タスク実行時に yarn build コマンドが実行されるように設定する
  12. backend-app サブプロジェクトで build タスク実行時に frontend-app の build 下のファイルを backend-app/src/main/resources/static にコピーされるように設定する
  13. プロジェクトのルートディレクトリで Gradle の build タスクを実行する
  14. 生成された jar ファイルで Spring Boot アプリを起動して動作確認する

手順

ksbysample-react-springboot レポジトリを作成して clone する

ksbysample-react-springboot レポジトリ を作成し、D:\project-react\ksbysample-react-springboot に clone します。

Spring Initializr でサンプルプロジェクトを作成して Gradle Wrapper のファイルをコピーする

適当なディレクトリに Spring Initializr で Spring Boot のプロジェクトを作成し(Gradle を指定する)、Gradle Wrapper の以下のファイルを D:\project-react\ksbysample-react-springboot の下にコピーします。

gradlew init を実行し、build.gradle があるサブプロジェクトを自動認識するよう settings.gradle を変更する

gradlew init を実行します。

f:id:ksby:20210429104642p:plain

build.gradle があるサブプロジェクトを自動的に認識してくれるよう settings.gradle を以下の内容に変更します。

rootProject.name = 'ksbysample-react-springboot'

rootDir.eachFileRecurse { f ->
    if (f.name == "build.gradle") {
        String relativePath = f.parentFile.absolutePath - rootDir.absolutePath
        String projectName = relativePath.replaceAll("[\\\\\\/]", ":")
        if (projectName != ":buildSrc") {
            include projectName
        }
    }
}

IntelliJ IDEA で D:\project-react\ksbysample-react-springboot を開きます。

backend-app サブプロジェクトを作成し、REST API を実装する

Spring Initializr で D:\project-react\ksbysample-react-springboot の下に backend-app サブプロジェクトを作成します。

f:id:ksby:20210429124224p:plain f:id:ksby:20210429124324p:plain

D:\project-react\ksbysample-react-springboot\backend-app の下は src ディレクトリ、build.gradle 以外のディレクトリ、ファイルを削除します。

build.gradle を以下の内容に変更します。

plugins {
    id 'org.springframework.boot' version '2.4.5'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'ksbysample'
version = '0.0.1-SNAPSHOT'

sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11

[compileJava, compileTestJava]*.options*.encoding = "UTF-8"
[compileJava, compileTestJava]*.options*.compilerArgs = ["-Xlint:all,-options,-processing,-path"]

configurations {
    compileOnly.extendsFrom annotationProcessor

    // annotationProcessor と testAnnotationProcessor、compileOnly と testCompileOnly を併記不要にする
    testAnnotationProcessor.extendsFrom annotationProcessor
    testImplementation.extendsFrom compileOnly
}

repositories {
    mavenCentral()
}

dependencies {
    def lombokVersion = "1.18.20"

    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // for lombok
    // testAnnotationProcessor、testCompileOnly を併記しなくてよいよう configurations で設定している
    annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    compileOnly("org.projectlombok:lombok:${lombokVersion}")
}

test {
    useJUnitPlatform()
}

backend-app/src/main/java/ksbysample/backendapp/User.java を作成し、以下の内容を記述します。

package ksbysample.backendapp;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {

    private String name;

    private int age;

}

backend-app/src/main/java/ksbysample/backendapp/UserApiController.java を作成し、以下の内容を記述します。

package ksbysample.backendapp;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserApiController {

    @GetMapping("/{id}")
    public User findById(@PathVariable String id) {
        return User.builder()
                .name(String.format("Tanaka Taro No.%s", id))
                .age(25)
                .build();
    }

}

npx create-react-app frontend-app --template typescript を実行して frontend-app サブプロジェクトを作成する

D:\project-react\ksbysample-react-springboot で npx create-react-app frontend-app --template typescript コマンドを実行し、frontend-app サブプロジェクトを作成します。

f:id:ksby:20210429131900p:plain f:id:ksby:20210429132002p:plain f:id:ksby:20210429132059p:plain

cd /d/project-react/ksbysample-react-springboot/frontend-app した後 yarn start コマンドを実行し、画面が表示されることを確認します。

Tailwind CSS、CRACO をインストールする

以下のコマンドを実行し Tailwind CSS、CRACO をインストールします(React アプリと Spring Boot アプリを連携するのに Tailwind CSS は必須ではありません)。

  • yarn add tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat
  • yarn add -D postcss@^7 autoprefixer@^9
  • yarn add @tailwindcss/forms
  • yarn add -D @craco/craco

package.json 内の scripts で react-scriptscraco に変更します。

  "scripts": {
    "start": "craco start",
    "build": "craco build",
    "test": "craco test",
    "eject": "craco eject"
  },

プロジェクトのルートディレクトリ直下に craco.config.js を作成し、以下の内容を記述します。

module.exports = {
  style: {
    postcss: {
      plugins: [
        require('tailwindcss'),
        require('autoprefixer'),
      ],
    },
  },
}

npx tailwindcss init -p を実行して tailwind.config.js、postcss.config.js を作成した後、tailwind.config.js を以下の内容に変更します。

module.exports = {
  purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/forms'),
  ],
}

frontend-app/src/index.css を以下の内容に変更します。

@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind forms;

ここに書いた内容については React+Tailwind CSS+Storybook のプロジェクトを作成する 参照。

React Query をインストールする

React アプリから Spring Boot アプリの REST API を呼び出すのに React Query を使用します。yarn add react-query コマンドを実行してインストールします。

frontend-app に REST API を呼び出して取得したデータを表示する処理を実装する

frontend-app/src/index.tsx を以下のように変更します。

import React from 'react';
import ReactDOM from 'react-dom';
import { QueryClient, QueryClientProvider } from 'react-query';

import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const queryClient = new QueryClient();

ReactDOM.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
  • 以下の行を追加します。
    • import { QueryClient, QueryClientProvider } from 'react-query';
    • const queryClient = new QueryClient();
  • <App /> の前後に <QueryClientProvider client={queryClient}> ... </QueryClientProvider> を追加します。

frontend-app/src/App.tsx を以下の内容に変更します。REST API を呼び出す時の URL は /user/1 固定です。

import { FC } from 'react';
import { useQuery } from 'react-query';

type User = {
  name: string;
  age: number;
};

const getUser = async () => (await fetch('/user/1')).json();

const App: FC = () => {
  const { data: user } = useQuery<User>(['user', 1], () => getUser());

  return (
    <div className="pt-4 pl-4 text-red-600 text-4xl font-extrabold">
      {user?.name}{user?.age} 歳です。
    </div>
  );
};

export default App;

craco.config.js を変更し webpack-dev-server に proxy の設定を追加する

frontend-app/craco.config.js に devServer: { ... } を追加して、/user/... でアクセスされたら Spring Boot アプリに転送するようにします。

module.exports = {
  style: {
    postcss: {
      plugins: [require('tailwindcss'), require('autoprefixer')],
    },
  },
  devServer: {
    proxy: [
      {
        context: ['/user'],
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    ],
  },
};

Spring Boot アプリを IDEA から起動、React アプリを yarn start で起動して動作確認する

Spring Boot アプリを IDEA から起動します。

f:id:ksby:20210429200623p:plain

React アプリを yarn start コマンドで起動すると、

f:id:ksby:20210429200733p:plain f:id:ksby:20210429200844p:plain

ブラウザ上に REST API から取得したデータが表示されました。

f:id:ksby:20210429200959p:plain

Spring Boot アプリ、React アプリを停止します。

frontend-app サブプロジェクトで Gradle の build タスクが実行されるようにし、かつ build タスク実行時に yarn build コマンドが実行されるように設定する

サブプロジェクトに build.gradle を作成すれば Gradle multi-project のサブプロジェクトとして認識されるので、frontend-app の直下に build.gradle を作成し、以下の内容を記述します。

plugins {
    id "base"
    id "com.github.node-gradle.node" version "3.0.1"
}

task yarnBuild(type: YarnTask) {
    args = ["build"]
}
build.dependsOn yarnBuild
  • base プラグインを追加し、frontend-app サブプロジェクトで build、clean タスクが実行できるようにします。
  • gradle-node-plugin プラグインを追加し、build タスク実行時に yarn build コマンドが実行されるようにします。

IDEA の Gradle Tool Window で Reload ボタンをクリックすると frontend-app が Gradle のサブプロジェクトとして認識・表示され、その下に build、clean タスクが表示されます。

f:id:ksby:20210429212926p:plain:w300

backend-app サブプロジェクトで build タスク実行時に frontend-app の build 下のファイルを backend-app/src/main/resources/static にコピーされるように設定する

backend-app/build.gradle の一番下に以下の記述を追加します。

..........

clean.delete fileTree("src/main/resources/static").include("**/*")
task copyToStatic(type: Copy) {
    from project(":frontend-app").file("build")
    into "src/main/resources/static"
}
copyToStatic.dependsOn ":frontend-app:build"
processResources.dependsOn copyToStatic
  • clean タスク実行時に src/main/resources/static の下をクリアするように設定します。
  • build タスク実行時に、processResources タスクの前で copyToStatic タスクを実行し、frontend-app の build ディレクトリの下にあるディレクトリ・ファイル一式を backend-app/src/main/resources/static の下にコピーします。

IDEA の Gradle Tool Window で Reload ボタンをクリックしておきます。

プロジェクトのルートディレクトリで Gradle の build タスクを実行する

IDEA の Gradle Tool Window から clean → build タスクを実行すると、

f:id:ksby:20210429214412p:plain:w300

:frontend-app:yarnBuild タスクの後に :backend-app:copyToStatic タスクが実行されて、

f:id:ksby:20210429214724p:plain

backend-app/src/main/resources/static の下に React アプリのファイルがコピーされています。

f:id:ksby:20210429215139p:plain:w300

生成された jar ファイルで Spring Boot アプリを起動して動作確認する

コマンドプロンプトで backend-app/build/libs の下に移動してから java -jar backend-app-0.0.1-SNAPSHOT.jar コマンドを実行して Spring Boot アプリを起動した後、

f:id:ksby:20210429215514p:plain

ブラウザから http://localhost:8080/ にアクセスすると REST API から取得したデータが表示されました。

f:id:ksby:20210429215654p:plain

履歴

2021/04/29
初版発行。

React+Tailwind CSS+Storybook のプロジェクトを作成する(Typescript 版)

概要

記事一覧はこちらです。

React+Tailwind CSS+Storybook のプロジェクトを作成する の Typescript 版を作成します。React+Tailwind CSS+Storybook のプロジェクトを作成する からの差分だけ記述します。

GitHub はこちら
https://github.com/ksby/react-ts-taiwindcss-storybook-sample

参照したサイト・書籍

  1. Storybook - TypeScript
    https://storybook.js.org/docs/react/configure/typescript

  2. React Typescript storybook implement customized Input component with onChange callBack then setState value backTo Input
    https://stackoverflow.com/questions/64719744/react-typescript-storybook-implement-customized-input-component-with-onchange-ca

    • .stories.tsx の書き方はこの QA を参考にしました。

目次

  1. create-react-app ... --template typescript で react-ts-taiwindcss-storybook-sample プロジェクトを作成する
  2. 「Tailwind CSS をインストールする」~「src/index.css を変更する」は同じ
  3. src/App.tsx を変更する
  4. 「npx sb init コマンドを実行して Storybook をインストールする」~「src/stories を削除する」は同じ
  5. Typescript で component のサンプルを作成して Storybook 上に表示する+Storybook 用の .stories.tsx も Typescript で記述する

手順

create-react-app で react-ts-taiwindcss-storybook-sample プロジェクトを作成する

以下のコマンドを実行し react-ts-taiwindcss-storybook-sample プロジェクトを作成します。

  • npx create-react-app react-ts-taiwindcss-storybook-sample --template typescript

「Tailwind CSS をインストールする」~「src/index.css を変更する」は同じ

以下の手順は同じです。

src/App.tsx を変更する

src/App.tsx を以下のように変更します。

import { FC } from 'react';

import './App.css';

const App: FC = () => (
  <div className="pt-4 pl-4 text-red-600 text-4xl font-extrabold">
    React+Tailwind CSS
  </div>
);

export default App;

「npx sb init コマンドを実行して Storybook をインストールする」~「src/stories を削除する」は同じ

以下の手順は同じです。

Typescript で component のサンプルを作成して Storybook 上に表示する+Storybook 用の .stories.tsx も Typescript で記述する

Storybook - TypeScript を読むと Storybook は built-in で Typescript をサポートしており、特に追加で設定する必要はない模様。

React Typescript storybook implement customized Input component with onChange callBack then setState value backTo Input を参考に component のサンプルを実装します。

まずは src/components/sample/ListItem.tsx から。

import { FC } from 'react';

export type ListItemProps = {
  image: string;
  title: string;
  author: string;
};

const ListItem: FC<ListItemProps> = ({ image, title, author }) => (
  <article className="p-2 flex space-x-4">
    <img className="flex-none w-16 h-16 rounded-lg" src={image} alt="" />
    <div>
      <dl>
        <div>
          <dt className="sr-only">Title</dt>
          <dd className="text-2xl font-bold">{title}</dd>
        </div>
        <div className="mt-0.5">
          <dt className="sr-only">Author</dt>
          <dd className="text-sm font-semibold text-indigo-500">By {author}</dd>
        </div>
      </dl>
    </div>
  </article>
);

export default ListItem;

src/components/sample/ListItem.stories.tsx は以下の内容を記述します。

import React from 'react';
import { Story } from '@storybook/react';

import ListItem, { ListItemProps } from './ListItem';
import dog from './dog.jpg';

export default {
  title: 'sample/ListItem',
  component: ListItem,
};

const Template: Story<ListItemProps> = (args: ListItemProps) => (
  <ListItem image={args.image} title={args.title} author={args.author} />
);

export const Default: Story<ListItemProps> = Template.bind({});
Default.args = {
  image: dog,
  title: 'サンプルブック1',
  author: '作者は犬',
};

次は src/components/sample/List.tsx

import React, { FC } from 'react';

import ListItem, { ListItemProps } from './ListItem';

export type ListProps = {
  items: ListItemProps[];
};

const List: FC<ListProps> = ({ items }) => (
  <ul className="divide-y divide-gray-600">
    {items.map((item) => (
      <ListItem
        key={item.title}
        image={item.image}
        title={item.title}
        author={item.author}
      />
    ))}
  </ul>
);

export default List;

src/components/sample/List.stories.tsx は以下の内容を記述します。

import React from 'react';
import { Story } from '@storybook/react';

import List, { ListProps } from './List';
import dog from './dog.jpg';
import cat from './cat.jpg';
import tiger from './tiger.jpg';

export default {
  title: 'sample/List',
  component: List,
};

const Template: Story<ListProps> = (args: ListProps) => (
  <List items={args.items} />
);

export const Default: Story<ListProps> = Template.bind({});
Default.args = {
  items: [
    {
      image: dog,
      title: 'サンプルブック1',
      author: '作者は犬',
    },
    {
      image: cat,
      title: 'サンプルブック2',
      author: '作者は猫',
    },
    {
      image: tiger,
      title: 'サンプルブック3',
      author: '作者はトラ',
    },
  ],
};

履歴

2021/04/20
初版発行。

React+Tailwind CSS+Storybook のプロジェクトを作成する

概要

記事一覧はこちらです。

ユーティリティーファーストとTailwind CSSのススメ の記事を読んで React+Tailwind CSS の組み合わせに興味を持ったのでサンプルプロジェクトを作成してみます。作成した Component を確認できるよう Storybook も入れてみます。

Typescript は導入せず Javascript で記述する想定です。

Node.js、npm、yarn は以下のバージョンを使用しています(Node.js と npm のバージョンが古いな。。。とは思いつつ今回はこのままでいきます)。

f:id:ksby:20210417130823p:plain

参照したサイト・書籍

  1. ユーティリティーファーストとTailwind CSSのススメ
    https://qiita.com/Takazudo/items/5180f5eb6d798a52074f

  2. Tailwind CSS
    https://tailwindcss.com/

  3. tailwindlabs / tailwindcss-forms
    https://github.com/tailwindlabs/tailwindcss-forms

  4. postcss / postcss - PostCSS 8 for end users
    https://github.com/postcss/postcss/wiki/PostCSS-8-for-end-users

    • ツールの PostCSS 8 の対応状況が表示されています。
  5. Install Tailwind CSS with Create React App
    https://tailwindcss.com/docs/guides/create-react-app

  6. gsoft-inc / craco
    https://github.com/gsoft-inc/craco

  7. Installation
    https://tailwindcss.com/docs/installation

  8. Configuration
    https://tailwindcss.com/docs/configuration

    • tailwindcss init コマンドの -p flag の説明はここに記述されていました。
  9. Storybook
    https://storybook.js.org/

  10. Install Storybook
    https://storybook.js.org/docs/react/get-started/install

  11. Introduction to Storybook for React
    https://storybook.js.org/docs/react/get-started/introduction

  12. Integrating React, Tailwind and Storybook
    https://johnclarke73.medium.com/integrating-react-tailwind-and-storybook-3ae124aff0d9

  13. Storybook-tailwind. How should I add tailwind to storybook
    https://stackoverflow.com/questions/65495912/storybook-tailwind-how-should-i-add-tailwind-to-storybook

目次

  1. create-react-app で react-taiwindcss-storybook-sample プロジェクトを作成する
  2. Tailwind CSS をインストールする
  3. CRACO をインストール・設定する
  4. npx tailwindcss init -p コマンドを実行して tailwind.config.js、postcss.config.js を作成する
  5. src/index.css を変更する
  6. src/App.css、src/App.js を変更して Tailwind CSS が利用できることを確認する
  7. npx sb init コマンドを実行して Storybook をインストールする
  8. babel-loader が 8.2.2 にバージョンアップされて yarn start 実行時にエラーが出るので 8.1.0 にバージョンダウンする
  9. .storybook/preview.js、.storybook/main.js を変更する
  10. src/stories を削除する
  11. component のサンプルを作成して Storybook 上に表示する

手順

create-react-app で react-taiwindcss-storybook-sample プロジェクトを作成する

コマンドプロンプトから以下のコマンドを実行し、プロジェクトを作成します。

  • cd /d/project-react/
  • npx create-react-app react-taiwindcss-storybook-sample
  • cd react-taiwindcss-storybook-sample/
  • yarn test を実行してテストが正常に終了することを確認します。
  • yarn start を実行してブラウザに React のロゴが表示されることを確認します。

Tailwind CSS をインストールする

Install Tailwind CSS with Create React App のページを参考に以下のコマンドを実行します。@tailwindcss/forms もインストールします。Tailwind CSS は PostCSS 8 に対応しているのですが create-react-app がまだ対応していないらしく、PostCSS 7 を使うようにする必要があるとのこと。

  • yarn add tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
    ※(2021/04/20追記)postcss、autoprefixer は yarn add -D でインストールした方がよい。
  • yarn add @tailwindcss/forms

f:id:ksby:20210417134455p:plain f:id:ksby:20210417134553p:plain

CRACO をインストール・設定する

引き続き Install Tailwind CSS with Create React App のページを参考に yarn add @craco/craco コマンドを実行して CRACO をインストールします。create-react-app の PostCSS の設定を変更するためにこのツールが必要とのこと。

f:id:ksby:20210417142247p:plain

package.json 内の scripts で react-scriptscraco に変更します。

  "scripts": {
    "start": "craco start",
    "build": "craco build",
    "test": "craco test",
    "eject": "craco eject"
  },

プロジェクトのルートディレクトリ直下に craco.config.js を作成し、以下の内容を記述します。

module.exports = {
  style: {
    postcss: {
      plugins: [
        require('tailwindcss'),
        require('autoprefixer'),
      ],
    },
  },
}

npx tailwindcss init -p コマンドを実行して tailwind.config.js、postcss.config.js を作成する

Installation のページを参考に npx tailwindcss init -p コマンドを実行して tailwind.config.js、postcss.config.js を作成します。

f:id:ksby:20210417140215p:plain

tailwind.config.js は以下の内容に変更します。

module.exports = {
  purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/forms'),
  ],
}
  • purgeに './src/**/*.{js,jsx,ts,tsx}', './public/index.html' を追加します。
  • plugins に require('@tailwindcss/forms'), を追加します。

postcss.config.js は作成されたままで何も変更しません。

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

src/index.css を変更する

src/index.css の中身を全てクリアし、以下の内容に変更します。

@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind forms;

src/App.css、src/App.js を変更して Tailwind CSS が利用できることを確認する

Tailwind CSS が利用できるようになっていることを確認します。

src/App.css の中身はどれも使用しないので全てクリアします。

src/App.js を Tailwind CSS を利用して文字列を表示するよう以下の内容に変更します。

import './App.css';

function App() {
  return (
    <div className="pt-4 pl-4 text-red-600 text-4xl font-extrabold">
      React+Tailwind CSS
    </div>
  );
}

export default App;

yarn start を実行すると、

f:id:ksby:20210417152746p:plain

ブラウザに以下のように表示されました。問題なく利用できるようになっています。

f:id:ksby:20210417152542p:plain

npx sb init コマンドを実行して Storybook をインストールする

Install Storybook のページを参考に npx sb init コマンドを実行して Storybook をインストールします。

f:id:ksby:20210417154911p:plain .......... f:id:ksby:20210417155013p:plain

yarn storybook コマンドを実行して、

f:id:ksby:20210417160145p:plain .......... f:id:ksby:20210417160243p:plain

Storybook の画面が表示されることを確認します。

f:id:ksby:20210417160941p:plain

babel-loader が 8.2.2 にバージョンアップされて yarn start 実行時にエラーが出るので 8.1.0 にバージョンダウンする

Storybook をインストールした後に yarn start を実行すると "babel-loader": "8.1.0" が必要とのエラーが出ます。

f:id:ksby:20210417161614p:plain

yarn.lock を見ると babel-loader が 8.2.2 になっていました。yarn add -D babel-loader@8.1.0 を実行してバージョンダウンします。

バージョンダウン後、再度 yarn start を実行すると今度は画面が表示されました。

.storybook/preview.js、.storybook/main.js を変更する

Tailwind CSS が適用されるよう .storybook/preview.js、.storybook/main.js を変更します。

.storybook/preview.js を以下のように変更します。

import '../src/index.css';

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
}
  • import '../src/index.css'; を追加します。

.storybook/main.js を以下のように変更します。Integrating React, Tailwind and StorybookStorybook-tailwind. How should I add tailwind to storybook を見てコピペしました。

const path = require('path');

module.exports = {
  "stories": [
    "../src/**/*.stories.mdx",
    "../src/**/*.stories.@(js|jsx|ts|tsx)"
  ],
  "addons": [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/preset-create-react-app"
  ],
  webpackFinal: async (config) => {
    config.module.rules.push({
      test: /\,css&/,
      use: [
        {
          loader: 'postcss-loader',
          options: {
            ident: 'postcss',
            plugins: [
              require('tailwindcss'),
              require('autoprefixer')
            ]
          }
        }
      ],
      include: path.resolve(__dirname, '../'),
    })
    return config
  }
}
  • const path = require('path'); を追加します。
  • webpackFinal: async (config) => { ... } を追加します。

src/stories を削除する

src/stories に Storybook のサンプルが作成されていますが、不要なので削除します。

component のサンプルを作成して Storybook 上に表示する

src/components/sample ディレクトリを作成し、その下に ListItem.js、List.js の2つの component を作成します。

まずは ListItem.js から。以下の内容を記述します。

import React from 'react';

const ListItem = ({image, title, author}) => (
  <article className="p-2 flex space-x-4">
    <img className="flex-none w-16 h-16 rounded-lg" src={image} alt=""/>
    <div>
      <dl>
        <div>
          <dt className="sr-only">Title</dt>
          <dd className="text-2xl font-bold">{title}</dd>
        </div>
        <div className="mt-0.5">
          <dt className="sr-only">Author</dt>
          <dd className="text-sm font-semibold text-indigo-500">By {author}</dd>
        </div>
      </dl>
    </div>
  </article>
);

export default ListItem;

同じ階層に ListItem.stories.js を作成し、以下の内容を記述します。

import React from 'react';

import ListItem from './ListItem';
import dog from './dog.jpg';

export default {
  title: 'sample/ListItem',
  component: ListItem,
};

const Template = (args) => <ListItem {...args}/>;

export const Default = Template.bind({});
Default.args = {
  image: dog,
  title: 'サンプルブック1',
  author: '作者は犬',
};

Storybook で ListItem component を表示すると以下のように表示されました。

f:id:ksby:20210417214246p:plain

次は List.js。以下の内容を記述します。

import React from 'react';

import ListItem from "./ListItem";

const List = ({items}) => (
  <ul className="divide-y divide-gray-600">
    {items.map(item => (
      <ListItem key={item.title} {...item}/>
    ))}
  </ul>
);

export default List;

同じ階層に List.stories.js を作成し、以下の内容を記述します。

import React from 'react';

import List from './List';
import dog from './dog.jpg';
import cat from './cat.jpg';
import tiger from './tiger.jpg';

export default {
  title: 'sample/List',
  component: List,
};

const Template = (args) => <List {...args}/>;

export const Default = Template.bind({});
Default.args = {
  items: [
    {
      image: dog,
      title: 'サンプルブック1',
      author: '作者は犬',
    },
    {
      image: cat,
      title: 'サンプルブック2',
      author: '作者は猫',
    },
    {
      image: tiger,
      title: 'サンプルブック3',
      author: '作者はトラ',
    },
  ]
};

Storybook で List component を表示すると以下のように表示されました。

f:id:ksby:20210417214843p:plain

履歴

2021/04/17
初版発行。
2021/04/20
* yarn add babel-loader@8.1.0yarn add -D babel-loader@8.1.0 に変更しました。
* <ListItem {...item}/><ListItem key={item.title} {...item}/> に変更しました。

Spring Boot 2.3.x の Web アプリを 2.4.x へバージョンアップする ( その11 )( exclude junit-vintage-engine の記述を削除する+domaGen タスクで SQL ファイルが作成されない問題を解消する )

概要

記事一覧はこちらです。

感想 まで書きましたが、修正が必要な点を見つけたので反映します。

参照したサイト・書籍

目次

  1. build.gradle から exclude group: "org.junit.vintage", module: "junit-vintage-engine" を削除する
  2. domaGen タスクを実行した時に SQL ファイルを作成しない問題を解消する

手順

build.gradle から exclude group: "org.junit.vintage", module: "junit-vintage-engine" を削除する

JUnit 5’s Vintage Engine Removed from spring-boot-starter-test の記述があったのに build.gradle から不要な記述を削除するのを忘れていました。

build.gradle の testImplementation("org.springframework.boot:spring-boot-starter-test") に記述していた { exclude group: "org.junit.vintage", module: "junit-vintage-engine" } を削除します。

dependencies {
    ..........
    implementation("io.micrometer:micrometer-registry-prometheus")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.springframework.security:spring-security-test")
    ..........
}

domaGen タスクを実行した時に SQL ファイルを作成しない問題を解消する

新規テーブルを作成して domaGen タスクを実行したところ SQL ファイルを生成しないことに気づきました。domaCodeGenDbSql タスクを実行していないことが原因だったので追加します。

// beforeDomaCodeGen --> domaCodeGenDbEntity --> domaCodeGenDbDao --> domaCodeGenDbSql --> domaGen --> afterDomaCodeGen
domaCodeGenDbEntity.dependsOn beforeDomaCodeGen
domaCodeGenDbDao.dependsOn domaCodeGenDbEntity
domaCodeGenDbSql.dependsOn domaCodeGenDbDao
domaGen.dependsOn domaCodeGenDbSql
domaGen.finalizedBy afterDomaCodeGen
  • domaCodeGenDbSql.dependsOn domaCodeGenDbDao を追加します。
  • domaGen.dependsOn domaCodeGenDbDaodomaGen.dependsOn domaCodeGenDbSql に変更します。

domaGen タスクの全体は以下のようになりました。

// for doma-codegen-plugin
// まず変更が必要なもの
def rootPackageName = "ksbysample.webapp.lending"
def rootPackagePath = "src/main/java/ksbysample/webapp/lending"
def dbUrl = "jdbc:postgresql://localhost/ksbylending"
def dbUser = "ksbylending_user"
def dbPassword = "xxxxxxxx"
def dbTableNamePattern = ".*"
// おそらく変更不要なもの
def packageEntityPath = rootPackagePath + "/entity"
def packageDaoPath = rootPackagePath + "/dao"
def importOfComponentAndAutowiredDomaConfig = "${rootPackageName}.util.doma.ComponentAndAutowiredDomaConfig"
def workPath = "work"
def workEntityPath = "${workPath}/entity"
def workDaoPath = "${workPath}/dao"
task domaGen(group: "doma code generation") {
    // このタスク自体は何もしない。実行する時の起点用タスクとして作成している。
}
task beforeDomaCodeGen {
    doLast {
        // 作業用ディレクトリを削除する
        delete "${workPath}"

        // 現在の dao, entity パッケージのバックアップを取得する
        copy() {
            from "${packageDaoPath}"
            into "${workDaoPath}/org"
        }
        copy() {
            from "${packageEntityPath}"
            into "${workEntityPath}/org"
        }

        // dao, entity パッケージを削除する
        delete "${packageDaoPath}"
        delete "${packageEntityPath}"
    }
}
domaCodeGen {
    db {
        url = "${dbUrl}"
        user = "${dbUser}"
        password = "${dbPassword}"
        tableNamePattern = "${dbTableNamePattern}"
        ignoredTableNamePattern = "flyway_schema_history|SPRING_SESSION.*"
        entity {
            packageName = "${rootPackageName}.entity"
            useListener = false
            useMappedSuperclass = false
        }
        dao {
            packageName = "${rootPackageName}.dao"
        }
    }
}
task afterDomaCodeGen {
    doLast {
        // 生成された Entity クラスを作業用ディレクトリにコピーし、
        // @SuppressWarnings({"PMD.TooManyFields"}) アノテーションを付加する
        copy() {
            from "${packageEntityPath}"
            into "${workEntityPath}/replace"
            filter {
                line ->
                    line.replaceAll('@Entity', '@SuppressWarnings({"PMD.TooManyFields"})\n@Entity')
            }
        }

        // 生成された Dao インターフェースからバックアップにあるクラスを削除する
        // ※生成済の Dao インターフェースを再生成したい場合には事前に削除すること!
        for (workDaoFile in new File("${workDaoPath}/org").listFiles()) {
            def packageDaoFile = new File("${packageDaoPath}/${workDaoFile.name}")
            if (packageDaoFile.exists()) {
                packageDaoFile.delete()
            }
        }

        // 生成された Dao インターフェースを作業用ディレクトリにコピーし、
        // @ComponentAndAutowiredDomaConfig アノテーションを付加し、
        // Javadoc の @param に説明文を追加する
        copy() {
            from "${packageDaoPath}"
            into "${workDaoPath}/replace"
            filter {
                line ->
                    line.replaceAll('import org.seasar.doma.Dao;', "import ${importOfComponentAndAutowiredDomaConfig};\nimport org.seasar.doma.Dao;")
                            .replaceAll('@Dao', '@Dao\n@ComponentAndAutowiredDomaConfig')
                            .replaceAll('@param (\\S+)$', '@param $1 $1')
            }
        }

        // 元々 dao, entity パッケージ内にあったファイルを元に戻す
        copy() {
            from "${workDaoPath}/org"
            into "${packageDaoPath}"
        }
        copy() {
            from "${workEntityPath}/org"
            into "${packageEntityPath}"
        }

        // @ComponentAndAutowiredDomaConfig アノテーションを付加した Dao インターフェースを
        // dao パッケージへ戻す
        copy() {
            from "${workDaoPath}/replace"
            into "${packageDaoPath}"
        }

        // @SuppressWarnings({"PMD.TooManyFields"}) アノテーションを付加した Entity クラスを
        // entity パッケージへ戻す
        copy() {
            from "${workEntityPath}/replace"
            into "${packageEntityPath}"
        }

        // 作業用ディレクトリを削除する
        delete "${workPath}"
    }
}
// beforeDomaCodeGen --> domaCodeGenDbEntity --> domaCodeGenDbDao --> domaCodeGenDbSql --> domaGen --> afterDomaCodeGen
domaCodeGenDbEntity.dependsOn beforeDomaCodeGen
domaCodeGenDbDao.dependsOn domaCodeGenDbEntity
domaCodeGenDbSql.dependsOn domaCodeGenDbDao
domaGen.dependsOn domaCodeGenDbSql
domaGen.finalizedBy afterDomaCodeGen

履歴

2021/04/13
初版発行。

Antora で生成するドキュメントサイトに antora-lunr で検索機能を追加する

概要

記事一覧はこちらです。

antora-lunr を使うと antora で生成するドキュメントサイトに検索機能を追加できると聞いたので、試してみます。

参照したサイト・書籍

  1. Mogztter / antora-lunr
    https://github.com/Mogztter/antora-lunr

  2. Mogztter / antora-site-generator-lunr
    https://github.com/Mogztter/antora-site-generator-lunr

  3. lunr-languages
    https://www.npmjs.com/package/lunr-languages

  4. MihaiValentin / lunr-languages
    https://github.com/MihaiValentin/lunr-languages

  5. LANGUAGE SUPPORT
    https://lunrjs.com/guides/language_support.html

目次

  1. npm install --save-dev antora-site-generator-lunr を実行する
  2. ドキュメントサイトを生成して検索機能を試してみる
  3. DOCSEARCH_LANGS=en,jaDOCSEARCH_LANGS=ja を指定してもダメでした。。。
  4. lunr-languages をインストールして日本語で検索できるようにする
  5. DOCSEARCH_INDEX_VERSION=latest を指定して最新バージョンのドキュメントだけ index が生成されるようにする

手順

npm install --save-dev antora-site-generator-lunr を実行する

antora-lunr のドキュメントを読むと、簡単に導入するのであれば antora-site-generator-lunr をインストールすればよいとのこと。

ksbysample-antora-playbook プロジェクトで npm install --save-dev antora-site-generator-lunr を実行して antora-site-generator-lunr をインストールします。

f:id:ksby:20210328233907p:plain

ドキュメントサイトに search component を表示するために antora-playbook.yml を以下のように変更します。

..........
ui:
  bundle:
    url: https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/master/raw/build/ui-bundle.zip?job=bundle-stable
    snapshot: true
  supplemental_files: ./node_modules/antora-lunr/supplemental_ui

..........
  • supplemental_files: ./node_modules/antora-lunr/supplemental_ui を追加します。

ドキュメントサイトを生成して検索機能を試してみる

build ディレクトリを削除してから DOCSEARCH_ENABLED=true DOCSEARCH_ENGINE=lunr npx antora --generator antora-site-generator-lunr antora-playbook.yml を実行します。

f:id:ksby:20210328235249p:plain

生成された build ディレクトリを見ると build/site/search-index.js が生成されています。

f:id:ksby:20210328235414p:plain

build/site/index.html をブラウザで開くと search component が画面上部に表示されて、asciidoctor と入力すると検索結果が表示されました! ただし結果が2重に表示されています。

f:id:ksby:20210328235722p:plain

また日本語で ツール と入力すると何もヒットしませんでした。

f:id:ksby:20210329000008p:plain

DOCSEARCH_LANGS=en,jaDOCSEARCH_LANGS=ja を指定してもダメでした。。。

Support for other languages の記述があったので、DOCSEARCH_LANGS=en,ja を追加して DOCSEARCH_ENABLED=true DOCSEARCH_ENGINE=lunr DOCSEARCH_LANGS=en,ja npx antora --generator antora-site-generator-lunr antora-playbook.yml でドキュメントサイトを生成し直してみましたが、

f:id:ksby:20210329001053p:plain

結果は変わりませんでした。

f:id:ksby:20210329001213p:plain

DOCSEARCH_LANGS=ja に変更しても結果は変わらず。

lunr-languages をインストールして日本語で検索できるようにする

lunr を日本語で検索できるようにするには lunr-languages をインストールすればよいとの記事を見かけたので、インストールして日本語が検索できるようにしてみます。

まずは npm install --save-dev lunr-languages を実行して lunr-languages をインストールします。

f:id:ksby:20210329002025p:plain

In a web browser に従い、node_modules/antora-lunr/lib/generate-index.js を以下のように変更します。

'use strict'

const lunr = require('lunr')
require("lunr-languages/lunr.stemmer.support")(lunr)
require('lunr-languages/tinyseg')(lunr)
require("lunr-languages/lunr.ja")(lunr)
const cheerio = require('cheerio')
const Entities = require('html-entities').AllHtmlEntities
const entities = new Entities()

..........

  const lunrIndex = lunr(function () {
    const self = this
    self.use(lunr.ja)
    self.ref('url')
    ..........
  • 以下の3行を追加します。
    • require("lunr-languages/lunr.stemmer.support")(lunr)
    • require('lunr-languages/tinyseg')(lunr)
    • require("lunr-languages/lunr.ja")(lunr)
  • lunr(function () { ... } の中に以下の1行を追加します。
    • self.use(lunr.ja)

DOCSEARCH_ENABLED=true DOCSEARCH_ENGINE=lunr npx antora --generator antora-site-generator-lunr antora-playbook.yml を実行します。

f:id:ksby:20210329002616p:plain

日本語で ツール と入力すると今度は検索結果が表示されました!

f:id:ksby:20210329002728p:plain

asciidoctor と入力しても検索結果が表示されます。日本語以外を入力しても問題ないようです。

f:id:ksby:20210329002845p:plain

DOCSEARCH_INDEX_VERSION=latest を指定して最新バージョンのドキュメントだけ index が生成されるようにする

検索結果が2重に表示されたのは v3.0 と v2.0 の2つのバージョンのドキュメントで index が生成されているためでした。Index only the latest version の記述がありましたので、DOCSEARCH_INDEX_VERSION=latest を追加して最新バージョンのドキュメントのみ index が生成されるようにします。

最初は DOCSEARCH_INDEX_VERSION=v3.0 と index を生成したい tag を指定するのかと思ったのですが、この設定では結果は何も変わらず、DOCSEARCH_INDEX_VERSION=latest と指定する必要がありました。

DOCSEARCH_ENABLED=true DOCSEARCH_ENGINE=lunr DOCSEARCH_INDEX_VERSION=latest npx antora --generator antora-site-generator-lunr antora-playbook.yml を実行します。

f:id:ksby:20210329004140p:plain

日本語で ツール と入力すると今度は検索結果が1件だけ表示されました。

f:id:ksby:20210329004241p:plain

履歴

2021/03/29
初版発行。

IntelliJ IDEA を 2020.3.2 → 2020.3.3 へ、Git for Windows を 2.30.0.2 → 2.31.0 へバージョンアップ

IntelliJ IDEA を 2020.3.2 → 2020.3.3 へバージョンアップする

IntelliJ IDEA の 2020.3.3 がリリースされているのでバージョンアップします。

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

  1. IntelliJ IDEA のメインメニューから「Help」-「Check for Updates...」を選択します。

  2. IntelliJ IDE and Plugin Updates」ダイアログが表示されます。右下に「Update and Restart」ボタンが表示されていますので、「Update and Restart」ボタンをクリックします。

    f:id:ksby:20210327103604p:plain

  3. Plugin の update も表示されました。このまま「Update and Restart」ボタンをクリックします。

    f:id:ksby:20210327103658p:plain

  4. Patch がダウンロードされて IntelliJ IDEA が再起動します。

  5. IntelliJ IDEA が起動すると画面下部に「Indexing」のメッセージが表示されますので、終了するまで待機します。

  6. IntelliJ IDEA のメインメニューから「Help」-「About」を選択し、2020.3.3 へバージョンアップされていることを確認します。

  7. clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

    f:id:ksby:20210327105100p:plain

  8. Project Tool Window で src/test でコンテキストメニューを表示して「More Run/Debug」-「Run 'All Tests' with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20210327105703p:plain

Git for Windows を 2.30.0.2 → 2.31.0 へバージョンアップする

Git for Windows の 2.31.0 がリリースされていたのでバージョンアップします。

  1. https://gitforwindows.org/ の「Download」ボタンをクリックして Git-2.31.0-64-bit.exe をダウンロードします。

  2. Git-2.31.0-64-bit.exe を実行します。

  3. 「Git 2.31.0 Setup」ダイアログが表示されます。インストーラーの画面を一通り見たいので「Only show new options」のチェックを外してから [Next >] ボタンをクリックします。

  4. 「Select Components」画面が表示されます。「Git LFS(Large File Support)」だけチェックした状態で [Next >]ボタンをクリックします。

  5. 「Choosing the default editor used by Git」画面が表示されます。「Use Vim (the ubiquitous text editor) as Git's default editor」が選択された状態で [Next >]ボタンをクリックします。

  6. 「Adjusting the name of the initial branch in new repositories」画面が表示されます。「Let Git decide」が選択されていることを確認後、[Next >]ボタンをクリックします。

  7. 「Adjusting your PATH environment」画面が表示されます。中央の「Git from the command line and also from 3rd-party software」が選択されていることを確認後、[Next >]ボタンをクリックします。

  8. 「Choosing HTTPS transport backend」画面が表示されます。「Use the OpenSSL library」が選択されていることを確認後、[Next >]ボタンをクリックします。

  9. 「Configuring the line ending conversions」画面が表示されます。一番上の「Checkout Windows-style, commit Unix-style line endings」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  10. 「Configuring the terminal emulator to use with Git Bash」画面が表示されます。「Use Windows'default console window」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  11. 「Choose the default behavior of git pull」画面が表示されます。「Default (fast-forward or merge)」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  12. 「Choose a credential helper」画面が表示されます。「None」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  13. 「Configuring extra options」画面が表示されます。「Enable file system caching」だけがチェックされていることを確認した後、[Next >]ボタンをクリックします。

  14. 「Configuring experimental options」画面が表示されます。何もチェックせずに [Install]ボタンをクリックします。

  15. インストールが完了すると「Completing the Git Setup Wizard」のメッセージが表示された画面が表示されます。中央の「View Release Notes」のチェックを外した後、[Next >]ボタンをクリックしてインストーラーを終了します。

  16. コマンドプロンプトを起動して git --version を実行し、git のバージョンが git version 2.31.0.windows.1 になっていることを確認します。

    f:id:ksby:20210327110544p:plain

  17. 特に問題はないようですので、2.31.0 で作業を進めたいと思います。

springdoc-openapi メモ書き

概要

記事一覧はこちらです。

springdoc-openapi を試した時のメモ書きです。

参照したサイト・書籍

  1. Documenting Spring Boot Rest API With OpenAPI 3.0
    https://tebatso191.medium.com/documenting-spring-boot-rest-api-with-openapi-3-0-a49be5e836ad

  2. springdoc-openapi
    https://springdoc.org/

  3. Springdoc-openapi Demos
    https://springdoc.org/#demos

  4. springdoc / springdoc-openapi-demos
    https://github.com/springdoc/springdoc-openapi-demos

  5. OpenAPI Specification
    https://swagger.io/specification/

  6. API Documentation & Design Tools for Teams | Swagger
    https://swagger.io/

  7. springdoc / springdoc-openapi-gradle-plugin
    https://github.com/springdoc/springdoc-openapi-gradle-plugin

  8. OpenAPI (Swagger) 超入門
    https://qiita.com/teinen_qiita/items/e440ca7b1b52ec918f1b

  9. Swagger(OAS) 3.0の登場
    https://news.mynavi.jp/itsearch/article/devsoft/3854

  10. Swagger ではない OpenAPI Specification 3.0 による API サーバー開発
    https://www.slideshare.net/techblogyahoo/swagger-openapi-specification-30-api

目次

  1. WebAPI を提供する Web アプリを作成する
  2. springdoc-openapi-ui を依存関係に追加し、application.properties に設定を追加する
  3. @SpringBootApplication 付与クラスに @OpenAPIDefinition アノテーションを付与して API の情報を表示する
  4. @RestController 付与クラスに @Tag、@Operation、@ApiResponses 等のアノテーションを付与して REST API の情報を表示する
  5. @RestControllerAdvice 付与クラスの @ExceptionHandler を付与したメソッドが UI に表示されないようにする
  6. POJO クラスに @Schema アノテーションを付与して UI の Schemas に表示される情報を追加する
  7. Swagger Petstore のページが表示されないようにする
  8. /api-docs にアクセスすると JSON の、/api-docs.yaml にアクセスすると YAML の OpenAPI ドキュメントがダウンロードできる
  9. 最後に

手順

WebAPI を提供する Web アプリを作成する

Web アプリを作成して ksbysample-springdoc-openapi-sample に入れます。

springdoc-openapi-ui を依存関係に追加し、application.properties に設定を追加する

まず build.gradle を変更します。

dependencies {
    ..........

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    implementation("org.springdoc:springdoc-openapi-ui:1.5.5")
    testImplementation("net.javacrumbs.json-unit:json-unit-spring:2.25.0")

    ..........
  • implementation("org.springdoc:springdoc-openapi-ui:1.5.5") を追加します。

src/main/resources/application.properties を以下のように変更します。

server.port=9080

springdoc.api-docs.path=/api-docs
springdoc.swagger-ui.path=/swagger-ui.html
  • 以下の2行を追加します。springdoc.swagger-ui.path の方は default 値と同じ設定なので記述しなくてもよいのですが、書いておいた方が URL を思い出しやすいかなと思い明記しています。springdoc.api-docs.path も default の /v3/api-docs のままでよければ書く必要はありません。
    • springdoc.api-docs.path=/api-docs
    • springdoc.swagger-ui.path=/swagger-ui.html
  • server.port=9080 を設定しているのは default の 8080 以外の場合の動作を確認するためです(実際には何もすることはありませんでした)。

Web アプリを起動して http://localhost:9080/swagger-ui.html にアクセスすると以下の画面が表示されます。タイトルが OpenAPI definition で、バージョンが v0、その下の endpoint や Schemas には何も説明がついていない状態です。

f:id:ksby:20210319010036p:plain

@SpringBootApplication 付与クラスに @OpenAPIDefinition アノテーションを付与して API の情報を表示する

src/main/java/ksbysample/webapp/springdocsample/SpringdocSampleApplication.java に @OpenAPIDefinition アノテーションを付与します。

package ksbysample.webapp.springdocsample;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@OpenAPIDefinition(info = @Info(title = "ksbysample-springdoc-openapi-sample",  // (1)
        description = "Spring Boot + springdoc-openapi のサンプルアプリ",  // (2)
        version = "v1"))  // (3)
public class SpringdocSampleApplication {

    ..........

@OpenAPIDefinition アノテーションの各属性の記述は UI の以下の場所に表示されます。

f:id:ksby:20210324222448p:plain

@RestController 付与クラスに @Tag、@Operation、@ApiResponses 等のアノテーションを付与して REST API の情報を表示する

src/main/java/ksbysample/webapp/springdocsample/webapi/book/WebapiBookController.java に @Tag、@Operation、@ApiResponses 等のアノテーションを付与します。

package ksbysample.webapp.springdocsample.webapi.book;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import ksbysample.webapp.springdocsample.exception.BookInvalidException;
import ksbysample.webapp.springdocsample.exception.BookNotFoundException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequestMapping("/webapi/book")
@Tag(name = "Book", description = "the Book API")
public class WebapiBookController {

    ..........

    @Operation(summary = "Book データを登録する",
            description = "Book データを受け取ってリストに追加する")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "成功"),
            @ApiResponse(responseCode = "405", description = "入力チェックエラー")
    })
    @PostMapping
    public void add(
            @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Book データ")
            @Validated @RequestBody Book book,
            BindingResult bindingResult) {
        ..........
    }

    @Operation(summary = "Book データを検索する",
            description = "指定された ISBN の Book データを検索する")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "成功",
                    content = @Content(mediaType = "application/json", schema = @Schema(implementation = Book.class))),
            @ApiResponse(responseCode = "404", description = "指定された ISBN の Book データが存在しない",
                    content = @Content)
    })
    @GetMapping("/{isbn}")
    public Book findByIsbn(
            @Parameter(required = true, description = "ISBN-13", example = "978-4873119038")
            @PathVariable String isbn) {
        ..........
    }

}

API が以下のように表示されます。

f:id:ksby:20210324225506p:plain

/webapi/book API は以下のように表示されます。Response に @ApiResponses に記述していない 404 が表示されていますが、これは @RestControllerAdvice クラスに定義したものが表示されているからです。この後に表示されないように記述を追加します。

f:id:ksby:20210324225636p:plain f:id:ksby:20210324225724p:plain

Request body の Book データの Schema タブをクリックすると以下の表示に切り替わります。

f:id:ksby:20210324225857p:plain

/webapi/book/{isbn} API は以下のように表示されます。こちらは不要な 405 が表示されています。

f:id:ksby:20210324225955p:plain f:id:ksby:20210324230102p:plain

@RestControllerAdvice 付与クラスの @ExceptionHandler を付与したメソッドが UI に表示されないようにする

@RestControllerAdvice 付与クラスの @ExceptionHandler を付与したメソッドの情報が各 API の Responses に表示されますが、今回は不要なので class に @Hide アノテーションを付与して表示されないようにします。

package ksbysample.webapp.springdocsample.webapi.book;

import io.swagger.v3.oas.annotations.Hidden;
import ksbysample.webapp.springdocsample.exception.BookInvalidException;
import ksbysample.webapp.springdocsample.exception.BookNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * WebAPI 用 Error Handler クラス
 */
@RestControllerAdvice
@Hidden
public class WebapiBookErrorHandler {

    ..........

API の Responses の表示が以下のようになります。

f:id:ksby:20210324232112p:plain f:id:ksby:20210324232213p:plain

POJO クラスに @Schema アノテーションを付与して UI の Schemas に表示される情報を追加する

src/main/java/ksbysample/webapp/springdocsample/webapi/book/Book.java に @Schema アノテーションを付与します。

package ksbysample.webapp.springdocsample.webapi.book;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDate;

/**
 * Book データクラス
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
@Schema(description = "Book データ")
public class Book {

    @NotBlank
    @Schema(type = "string", required = true, description = "ISBN-13", example = "123-1234567890")
    private String isbn;

    @NotBlank
    @Schema(type = "string", required = true, description = "書籍名",
            example = "サンプル本")
    private String name;

    @NotNull
    @Schema(type = "number", required = true, description = "価格", example = "3600")
    private BigDecimal price;

    @Schema(type = "string", format="date", required = true, description = "発売日", example = "2021-03-24")
    private LocalDate releaseDate;

}

UI の Schemas に以下のように表示されます。

f:id:ksby:20210324234045p:plain

Swagger Petstore のページが表示されないようにする

UI が表示されているブラウザのアドレスバーの URL http://localhost:9080/swagger-ui/index.html?configUrl=/api-docs/swagger-config から ?configUrl=/api-docs/swagger-config を取り除くと Swagger Petstore の情報が表示されます。

f:id:ksby:20210324235856p:plain

表示させたくない場合、application.properties に springdoc.swagger-ui.disable-swagger-default-url=true を追加します。

server.port=9080

springdoc.api-docs.path=/api-docs
springdoc.swagger-ui.disable-swagger-default-url=true
springdoc.swagger-ui.path=/swagger-ui.html

f:id:ksby:20210325000644p:plain

/api-docs にアクセスすると JSON の、/api-docs.yaml にアクセスすると YAML の OpenAPI ドキュメントがダウンロードできる

http://localhost:9080/api-docs にアクセスすると JSON の OpenAPI ドキュメントが表示され、

f:id:ksby:20210325002440p:plain

http://localhost:9080/api-docs.yaml にアクセスすると YAML の OpenAPI ドキュメントがダウンロードできます。

f:id:ksby:20210325002623p:plain

最後に

他にも以下のようなことができる(らしい)。テストは試してみましたが、下の2つは記事を見かけただけです。

今回の変更内容はこちらです。
https://github.com/ksby/ksbysample-springdoc-openapi-sample/compare/before...after

履歴

2021/03/25
初版発行。 2021/03/27
* 最後に に今回変更した点が分かるリンクを追加しました。