JHipster によるエンタープライズアプリケーションの構築(3) Blueprint でコード生成処理をカスタマイズする

[!] この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
JHipster の紹介記事で、JHipster では Java / Spring Framework / Spring Boot によるバックエンド、Angular / React / Vue などのフロントエンド、開発環境の構築など、モダンな Web アプリケーションやマイクロサービスの Scaffold (足場) となるソースコードを生成できるとお伝えしました。
今回はこのソースコードの生成機能をオーバーライドし、プロジェクトにあったソースコードを生成するための Blueprint ⧉ という仕組みについて紹介します。
この記事では、以下のような内容を取り上げていきます。
- Blueprint の概要
- 既に存在している Blueprint をプロジェクトに適用する方法
- 独自の Blueprint を作成し、プロジェクトに適用する方法
Blueprint とは
JHipster のコード生成は、内部で Yeoman ジェネレータという生成ツールを使っています(以後「ジェネレータ」と呼びます)。
Yeoman は、ソースコード生成時にベースとなるテンプレートファイル(ひながた)と、テンプレートファイルを展開する処理を組み合わせて様々なコードを出力するツールです。
ジェネレータの中身は、フロントエンドやバックエンドなどのレイヤーごとに複数に分かれています(以後「サブジェネレータ」と呼びます)。
Blueprint ⧉ は、このサブジェネレータを追加したり、テンプレートやコード生成の処理を置き換えたりして拡張する仕組みです。
これにより、JHipster の特定の部分(例えばフロントエンド側の React のテンプレートだけ)をオーバーライドした独自のテンプレートと機能を提供できます。
公式のマーケットプレイス ⧉や GitHub 上に様々な Blueprint が公開されています。
Blueprint の使い方
公式の Blueprint である generator-jhipster-ionic ⧉ を例に、使い方を紹介します。
Ionic Framework ⧉ とは、オープンソースの UI ツールキットで、HTML, CSS, JavaScript などのウェブ技術を使って、Angular, React, Vue といったフレームワークと併用することでモバイルアプリなどを作成できます。
この Blueprint では、JHipster の REST API と通信する Angular ベースのモバイルアプリケーションのソースコードを生成します。
実際に JHipster のプロジェクトに、この Blueprint を適用し、モバイルアプリケーションの画面からサンプルデータが表示できるか確認します。
ベースとなる JHipster プロジェクトの作成
Blueprint を適用する前に、まずはベースとなる JHipster プロジェクトを作成します。
mkdir myApp && cd myAppjhipster
内容についてはシンプルなアプリケーション構築としています。
? Which *type* of application would you like to create? Monolithic application (recommended for simple projects)? What is the base name of your application? myApp? Do you want to make it reactive with Spring WebFlux? No? What is your default Java package name? com.mycompany.myapp? Which *type* of authentication would you like to use? JWT authentication (stateless, with a token)? Which *type* of database would you like to use? SQL (H2, PostgreSQL, MySQL, MariaDB, Oracle, MSSQL)? Which *production* database would you like to use? PostgreSQL? Which *development* database would you like to use? H2 with disk-based persistence? Which cache do you want to use? (Spring cache abstraction) No cache - Warning, when using an SQL database, this will disable the Hibernate 2nd level cache!? Would you like to use Maven or Gradle for building the backend? Gradle? Do you want to use the JHipster Registry to configure, monitor and scale your application? No? Which other technologies would you like to use?? Which *Framework* would you like to use for the client? React? Do you want to generate the admin UI? Yes? Would you like to use a Bootswatch theme (https://bootswatch.com/)? Default JHipster? Would you like to enable internationalization support? Yes? Please choose the native language of the application English? Please choose additional languages to install? Besides JUnit and Jest, which testing frameworks would you like to use?? Would you like to install other generators from the JHipster Marketplace? No
続いて業務ドメインの情報、主にエンティティとして示されるデータモデルを JHipster Domain Language (JDL) ⧉ 記法で作成します。
entity Employee { FirstName String, LastName String, Email String, PhoneNumber String, HireDate LocalDate, Salary Long, CommisionPct Long,}
上記の内容を jhipster-jdl.jdl
として保存し、jhipster jdl
コマンドでモデルに準じた CRUD 処理に関するコードを生成します。
jhipster jdl jhipster-jdl.jdl --force
コードが生成されたら ./gradlew
コマンドでアプリケーションを起動し、エンティティに対するコードが生成され、アプリケーションが動作していることを確認します。
Blueprint の適用とソースコードの再生成
作成した JHipster プロジェクトに対して、generator-jhipster-ionic
の Blueprint を適用します。
Blueprint を npm
コマンドでインストールし、jhipster
コマンド実行時のパラメータとして、--blueprint < blueprint 名>
を指定することで適用できます。
generator-jhipster-ionic
がターゲットとしている JHipster のバージョンが 7.8.1 であるため、執筆時点の最新バージョンである JHipster 7.9.3 の環境では、実行時にバージョンチェックでエラーとなってしまいます。このように、Blueprint 利用時には JHipster のバージョンアップに伴うエラーが課題となる場合があります。今回はこのエラーを回避するために --skip-checks
オプションを指定します。
npm install -g generator-jhipster-ionicjhipster --blueprint ionic --skip-checks --force
jhipster entities --regenerate
を実行して、エンティティに関連するソースコードを再生成します。
jhipster entities --regenerate --force
通常の JHipster プロジェクトのファイル再生成に加えて、../ionic4j/
配下に ionic 用のコードが生成されます。
動作確認のためアプリケーションを起動します。
./gradlew
新しいターミナルを起動し ionic4j
フォルダーでモバイルアプリケーション用の画面を起動します。
cd ../ionic4j/npm installnpm start
http://localhost:8100/
から、生成されたモバイルアプリケーションの画面が確認できます。
このように、Blueprint を使用することで JHipster 標準のジェネレータで生成されない機能を追加できます。
Blueprint の作り方
今度は独自の Blueprint を作成して JHipster プロジェクトに適用してみます。
今回は JHipster が生成するエンティティ単位の一覧画面に対して、エンティティの内容を PDF ファイルとしてダウンロードするためのボタンを追加してみます。
Blueprint を使用しない場合は、JHipster が生成したコードに対して、その都度 PDF ファイル出力を行うサーバーサイドのロジックを追加したり、一覧画面にボタンを追加したりする必要があります。
また、複数のエンティティに対して同様の機能を追加したい場合には、類似のコードをいくつも作成する必要が出てきます。
予め Blueprint としてこれらのコードを生成する処理を定義しておくことで、Blueprint を適用したプロジェクトでコードの生成を行う際には、自動的に PDF ファイルのダウンロード機能を含めた形で生成を行うことができるようになります。
Blueprint プロジェクトの作成
任意の Blueprint 用のフォルダーを作成し、jhipster generate-blueprint
コマンドで新規の Blueprint プロジェクトを作成します。
mkdir my-blueprint; cd my-blueprintjhipster generate-blueprint
実行すると作成するサブジェネレータなどの選択メニューが表示されます。
サブジェネレータについては、Java / Spring Framework / Spring Boot によるバックエンドに関するものは server
, entity-server
、Angular / React / Vue などのフロントエンドに関するものは client
, entity-client
、ソースコードの生成処理は generator.mjs
の各メソッド、RestController のテンプレートファイルは EntityResource.java.ejs
など、といった形で用意されています。
今回は、一覧画面にボタンを追加するため、クライアント側 entity-client
とサーバー側 entity-server
, server
のサブジェネレータに対して Blueprint を作成します。
それぞれのサブジェネレータの説明と Blueprint で作成するコード生成処理は以下となります。
entity-client
は一覧画面や CRUD 用の画面など、エンティティ単位でクライアント側のソースコードを生成するサブジェネレータです。エンティティの一覧画面に PDF ダウンロードボタンとダウンロード処理を追加します。entity-server
は RestController や DTO など、エンティティ単位でサーバ側のソースコードを生成するサブジェネレータです。PDF ダウンロード用の RestController と PDF 出力に使用するテンプレートファイル(jrxml)を追加します。server
は gradle の設定やログイン機能など、サーバー側に共通したソースコードを生成するサブジェネレータです。PDF の出力処理に JasperReports ⧉ を使用するため、build.gradle に設定を追加します。
選択肢は以下を参考にしてください。
? What is the base name of your application? my-blueprint? What is the project name of your application? My Blueprint Application
Blueprint の名称、アプリケーション名を設定します。
? Do you want to generate a local blueprint inside your application? No
プロジェクトのローカルで動作する Local Blueprint ⧉ として作成するか選択します。今回は Blueprint のプロジェクトとして作成してから JHipster プロジェクトに適用するため No を選択します。
? Which sub-generators do you want to override? entity-client, entity-server, server? Comma separated additional sub-generators.
Blueprint でオーバーライドするサブジェネレータを選択します。今回は前述の3つのサブジェネレータを指定します。
? Add a cli? Yes
jhipster
コマンドのかわりに使用できる Custom CLI ⧉ を追加するか選択します。今回の手順では使用しませんがデフォルト値の Yes とします。
? Is entity-client generator a side-by-side blueprint? Yes
各サブジェネレータに対して JHipster オリジナルのジェネレータを動作させることができる Side-by-Side Blueprint ⧉ を利用するか選択します。今回は元のコード生成処理を動かした上で、部分的な機能追加を行いたいため、Yes とします。
? Is entity-client generator a cli command? No
CLI コマンドに各サブジェネレータを追加するか設定します。デフォルト値の No とします。
? What task do you want do implement at entity-client generator? writing, postWriting
各サブジェネレータに対して実装するタスクを選択します。今回はファイルの作成や、一部のコード差し替えを実施したいため、writing と postWriting を選択します。
? What is the default indentation? 2
インデントを設定します。デフォルト値の 2 とします。
全て選択すると以下のようになります。
⬢ Welcome to the JHipster Project Name ⬢? What is the base name of your application? my-blueprint? What is the project name of your application? My Blueprint Application? Do you want to generate a local blueprint inside your application? No? Which sub-generators do you want to override? entity-client, entity-server, server? Comma separated additional sub-generators.? Add a cli? Yes? Is entity-client generator a side-by-side blueprint? Yes? Is entity-client generator a cli command? No? What task do you want do implement at entity-client generator? writing, postWriting? Is entity-server generator a side-by-side blueprint? Yes? Is entity-server generator a cli command? No? What task do you want do implement at entity-server generator? writing, postWriting? Is server generator a side-by-side blueprint? Yes? Is server generator a cli command? No? What task do you want do implement at server generator? writing, postWriting? What is the default indentation? 2
作成した Blueprint プロジェクトを node_modules にリンクします。
npm installsudo npm link
この Blueprint プロジェクトのサブジェネレータに、PDF 出力に必要な生成処理を追加します。
build.gradle に JasperReports の設定を追加
generators/server/generator.mjs
の get [POST_WRITING_PRIORITY]()
を以下のように修正します。
get [POST_WRITING_PRIORITY]() { return { async postWritingTemplateTask() { this.addGradleMavenRepository('https://jaspersoft.jfrog.io/jaspersoft/third-party-ce-artifacts/'); this.addGradleDependency('implementation', 'net.sf.jasperreports', 'jasperreports:6.20.0'); }, };}
一覧画面に PDF ダウンロードボタンとダウンロード処理を追加
generators/entity-client/generator.mjs
の get [POST_WRITING_PRIORITY]()
メソッドを以下のように修正します。
JHipster オリジナルのサブジェネレータが生成するファイルに対して、content.replace
を用いて文字列置換を行うことで、ダウンロードボタンや REST API の呼び出し処理を追加します。
get [POST_WRITING_PRIORITY]() { return { async postWritingTemplateTask() { const primaryKeyName = this.entity.primaryKey.name; const entityInstance = this.entity.entityInstance; const entityApiUrl = this.entity.entityApiUrl; const entityFolderName = this.entity.entityFolderName; const entityFileName = this.entity.entityFileName; this.editFile( `src/main/webapp/app/entities/` + entityFolderName + `/` + entityFileName + `.reducer.ts`, content => content.replace( `// slice`, `export const downloadEntitiesPdf = createAsyncThunk( '${entityInstance}/fetch_entity_pdf', async (id? : number) => { return await axios.get(\`\${apiUrl}/report/\${id ? id : ''}\`, { responseType: 'blob' }).then(response => { const link = document.createElement('a'); link.href = URL.createObjectURL(new Blob([response.data])); link.download = "${entityApiUrl}.pdf"; link.click(); URL.revokeObjectURL(link.href); }) }, { serializeError: serializeAxiosError } ); // slice`), ); this.editFile( `src/main/webapp/app/entities/` + entityFolderName + `/` + entityFileName + `.tsx`, content => content.replace( `getEntities,`, `getEntities, downloadEntitiesPdf`), content => content.replace( `const handleSyncList = () => {`, `const handleDownloadPdf = (${primaryKeyName}?: number) => { dispatch(downloadEntitiesPdf(${primaryKeyName})); }; const handleSyncList = () => {`), content => content.replace( `<div className="btn-group flex-btn-group-container">`, `<div className="btn-group flex-btn-group-container"> <Button color="warning" size="sm" data-cy="entityReportsButton" onClick={() => void handleDownloadPdf(${entityInstance}.${primaryKeyName})}> <FontAwesomeIcon icon="save" /> <span className="d-none d-md-inline" >PDF</span> </Button>`) ); }, };}
エンティティ単位で PDF ダウンロードを行うためのボタンと、Reducer にダウンロード処理を追加しています。
PDF ダウンロード用の RestController を追加
generators/entity-server/templates/src/main/java/package/web/rest/report/
配下に、ReportEntityResource.java.ejs
を以下の内容で作成します。
ejs 形式のテンプレートを使用することで、パッケージやエンティティ名などを埋め込んだソースコードを生成できます。
package <%= entityAbsolutePackage %>.web.rest.report;
import java.io.IOException;import java.io.InputStream;import java.util.Arrays;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.Optional;import net.sf.jasperreports.engine.JRException;import net.sf.jasperreports.engine.JasperCompileManager;import net.sf.jasperreports.engine.JasperExportManager;import net.sf.jasperreports.engine.JasperFillManager;import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
import org.springframework.beans.factory.annotation.Value;import org.springframework.core.io.ClassPathResource;import org.springframework.core.io.Resource;import org.springframework.web.bind.annotation.*;import org.springframework.http.ResponseEntity;
import <%= entityAbsolutePackage %>.domain.<%= entityClass %>;import <%= entityAbsolutePackage %>.repository.<%= entityClass %>Repository;import <%= entityAbsolutePackage %>.web.rest.errors.BadRequestAlertException;
@RestController@RequestMapping("/api")public class Report<%= entityClass %>Resource {
private static final String ENTITY_NAME = "<%= entityInstance %>";
@Value("${jhipster.clientApp.name}") private String applicationName;
private final <%= entityClass %>Repository <%= entityInstance %>Repository;
public Report<%= entityClass %>Resource(<%= entityClass %>Repository <%= entityInstance %>Repository ) { this.<%= entityInstance %>Repository = <%= entityInstance %>Repository; }
private byte[] exportReportToPdf(Resource resource, Map<String, Object> parameters, List<?> fields) throws IOException, JRException { try (InputStream in = resource.getInputStream();) { return JasperExportManager.exportReportToPdf( JasperFillManager.fillReport(JasperCompileManager.compileReport(in), parameters, new JRBeanCollectionDataSource(fields)) ); } }
@GetMapping("/<%= entityApiUrl %>/report/{id}") public ResponseEntity<byte[]> get<%= entityClass %>(@PathVariable <%= primaryKey.type %> id) throws IOException, JRException { Optional<<%= entityClass %>> <%= entityInstance %> = <%= entityInstance %>Repository.findById(id); if (<%= entityInstance %>.isEmpty()) { throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound"); } Resource resource = new ClassPathResource("./reports/report_<%= entityClass %>.jrxml"); byte[] bytes = exportReportToPdf(resource, new HashMap<String, Object>(), Arrays.asList(<%= entityInstance %>.get())); return ResponseEntity.ok().body(bytes); }}
このテンプレートから生成される RestController は、指定した ID のエンティティを Repository から取得し、JasperExportManager でテンプレートに合わせて PDF に変換して返却しています。
JasperReports のテンプレートファイルを追加
generators/entity-server/templates/src/main/resources/reports/
配下に report_entity.jrxml.ejs
を以下の内容で作成します。
<?xml version="1.0" encoding="UTF-8"?><jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="emproyee" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="e666857c-a4c1-4758-9020-f97c81327097"> <queryString> </queryString><%_ for (const field of fields) { const fieldName = field.fieldName; _%> <%_ if (field.fieldTypeUUID) { _%> <field name="<%= fieldName %>" class="java.util.UUID"/> <%_ } else if (field.fieldTypeLong) { _%> <field name="<%= fieldName %>" class="java.lang.Long"/> <%_ } else if (field.fieldTypeInteger) { _%> <field name="<%= fieldName %>" class="java.lang.Integer"/> <%_ } else if (field.fieldTypeFloat) { _%> <field name="<%= fieldName %>" class="java.lang.Float"/> <%_ } else if (field.fieldTypeDouble) { _%> <field name="<%= fieldName %>" class="java.lang.Double"/> <%_ } else if (field.fieldTypeBigDecimal) { _%> <field name="<%= fieldName %>" class="java.math.BigDecimal"/> <%_ } else if (field.fieldTypeLocalDate) { _%> <field name="<%= fieldName %>" class="java.time.LocalDate"/> <%_ } else if (field.fieldTypeInstant) { _%> <field name="<%= fieldName %>" class="java.time.Instant"/> <%_ } else if (field.fieldTypeDuration) { _%> <field name="<%= fieldName %>" class="java.time.Duration"/> <%_ } else if (field.fieldTypeZonedDateTime) { _%> <field name="<%= fieldName %>" class="java.time.ZonedDateTime"/> <%_ } else if (field.fieldTypeBoolean) { _%> <field name="<%= fieldName %>" class="java.lang.Boolean"/> <%_ } else if (field.fieldTypeString) { _%> <field name="<%= fieldName %>" class="java.lang.String"/> <%_ } else if (field.fieldIsEnum) { _%> <field name="<%= fieldName %>" class="<%= entityAbsolutePackage %>.domain.enumeration.<%= field.javaFieldType %>"/> <%_ } _%><%_ } _%> <pageHeader> <band height="30"> <staticText> <reportElement x="0" y="0" width="550" height="30"/> <textElement textAlignment="Center" verticalAlignment="Middle"> <font size="20"/> </textElement> <text><%= entityClass %></text> </staticText> </band> </pageHeader> <detail> <band height="750" splitType="Stretch"><%_ var counter = 0; var height = Math.min( Math.floor( 750 / fields.length ), 60 ); _%><%_ for (const field of fields) { const fieldName = field.fieldName; _%> <textField textAdjust="ScaleFont"> <reportElement x="0" y="<%= counter * height %>" width="200" height="<%= height %>"/> <box> <topPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/> <leftPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/> <bottomPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/> <rightPen lineWidth="2.0" lineStyle="Double" lineColor="#000000"/> </box> <textElement textAlignment="Center" verticalAlignment="Middle"> <font size="16"/> </textElement> <textFieldExpression><![CDATA["<%= fieldName %>"]]></textFieldExpression> </textField> <textField textAdjust="ScaleFont" isBlankWhenNull="true"> <reportElement x="200" y="<%= counter * height %>" width="350" height="<%= height %>"/> <box> <topPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/> <bottomPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/> <rightPen lineWidth="1.0" lineStyle="Solid" lineColor="#000000"/> </box> <textElement textAlignment="Left" verticalAlignment="Middle"> <font size="14"/> </textElement> <textFieldExpression><![CDATA[$F{<%= fieldName %>}]]></textFieldExpression> </textField> <% counter++; %><%_ } _%> </band> </detail></jasperReport>
エンティティの内容を表として出力するテンプレートです。
追加した ejs ファイルを使用したコード生成処理を追加
my-blueprint/generators/entity-server/generator.mjs
の import 処理に以下を追加します。
import { constants } from 'generator-jhipster';const { SERVER_MAIN_SRC_DIR, SERVER_MAIN_RES_DIR } = constants;
同様に get [WRITING_PRIORITY]()
を以下のように修正します。
writeFiles で先ほど追加した RestController や JasperReports のテンプレートファイルを指定し、renameTo でコード生成先のパスを設定することで、JHipster プロジェクトにコードを生成できます。
コード生成先のパスは Java であれば src 配下のパッケージのフォルダー配下、設定ファイルであれば resource フォルダー配下を指定します。
get [WRITING_PRIORITY]() { return { async writingTemplateTask() { await this.writeFiles({ sections: { files: [ { path: SERVER_MAIN_SRC_DIR, templates: [ { file: 'package/web/rest/report/ReportEntityResource.java', renameTo: generator => `${generator.entityAbsoluteFolder}/web/rest/report/Report${generator.entityClass}Resource.java` }, ], }, { path: SERVER_MAIN_RES_DIR, templates: [ { file: 'reports/report_entity.jrxml', renameTo: generator => `reports/report_${generator.entityClass}.jrxml` }, ], }, ], }, context: this.options.jhipsterContext, }); }, };}
各テンプレートファイルのリネーム時に各エンティティのクラス名を利用することで、エンティティ毎に異なるファイル名で保存しています。
Blueprint の適用とソースコードの再生成
作成した Blueprint を事前に準備していた JHipster アプリケーションに適用します。
cd myAppjhipster --blueprint my-blueprint --force --skip-checksnpm install /usr/local/lib/node_modules/generator-jhipster-my-blueprint
エンティティに関連するソースコードを再生成します。
jhipster entities --regenerate --force
Blueprint で追加したファイルが生成されていることが確認できます。
動作確認
Blueprint の適用とソースコードの再生成が完了したら、アプリケーションを起動します。
./gradlew
http://localhost:8080/
にアクセスし、エンティティの一覧画面を表示すると、PDF ボタンが追加されていることが確認できます。
PDF ボタンを押下すると、当該エンティティに関する PDF ファイルをダウンロードできます。
おわりに
ここまで Blueprint の利用と作成方法について解説してきました。
開発の際に複数のエンティティに跨って追加する機能がわかっている場合は、予め Blueprint を作成しておくことで、ソースコードを個別に修正する必要がなく、同一の品質でコードを追加できます。
また、機能が変更された場合でも、Blueprint を修正してからコードを再生成することで、一括で変更を反映できます。
今回は一つの機能追加を例に紹介しましたが、画面レイアウトやデザインの修正など、「JHipster 標準でプロジェクトを作成してみたけど少し気になる」といった部分からでも試してみるきっかけになれば幸いです。
この記事の中で触れた Side-by-Side Blueprint, Custom CLI, LocalBlueprint などの機能や、テンプレートの作成、文字列の置換など、Blueprint を作成するには様々な手段があります。次回以降の記事では、Blueprint の各機能の解説やどういった場合に採用するのがよいかについて、サンプルを交えつつ紹介したいと思います。
補足 ./lib/util/namespace.js に関するエラー
Node.js 18 + JHipster 7.9.3 利用時に jhipster
コマンドで ./lib/util/namespace
に関するエラーが発生する場合があります。
ERROR! Package subpath './lib/util/namespace' is not defined by "exports" in my-blueprint/node_modules/generator-jhipster/node_modules/yeoman-environment/package.json
こちらについては、Node.js 16 を利用するか、またはエラーが発生している package.json
の exports
に以下の設定を追加することで解消できます。
"./lib/util/namespace": "./lib/util/namespace.js"
JHipster 関連記事
- JHipster によるエンタープライズアプリケーションの構築(1) 紹介編
- JHipster によるエンタープライズアプリケーションの構築(2) Spring Nativeで高速起動化しAWS Lambdaで動かす