JHipster によるエンタープライズアプリケーションの構築(6) Blueprint でコード生成処理をカスタマイズする 2
JHipster Blueprint の紹介記事で、JHipster のソースコード生成機能をオーバーライドし、プロジェクトにあったソースコードを生成するための Blueprint ⧉ という仕組みについて紹介しました。
この記事では Blueprint に含まれる Side-by-Side Blueprint, Custom CLI, LocalBlueprint などの機能や、テンプレートの作成、文字列の操作をはじめとした実際のコード生成処理について、使い方や具体例を中心に解説していきます。
この記事では、JHipster Blueprint の紹介記事で定義した用語を使用します。当該用語については、初出のタイミングで記事へのリンクをつけておりますので、ご参照ください。
JHipster8 と Blueprint
2023 年 11 月に Spring Framework6 / Spring Boot3 に対応した JHipster8 がリリースされましたので、この記事では JHipster 8.0.0 を採用しています。
Blueprint の解説に入る前に、JHipster8 で Blueprint を作成するにあたって、注意すべき変更点を紹介します。
サブジェネレータの構成変更
client
, entity-client
等のサブジェネレータについて、JHipster8 では構成が変更されました。特に大きな変更について紹介します。
- フロントエンドの
client
,entity-client
サブジェネレータからフレームワーク別の機能が独立し、サブジェネレータangular
,react
,vue
が作成されました。 entity-client
,entity-server
,entity-i18n
等のエンティティ単位のサブジェネレータが廃止され、client
,server
,languages
サブジェネレータに統合されました。
Blueprint を作成する際には、このサブジェネレータの構成に従って配置する必要があるため、注意が必要です。最新のサブジェネレータの構成については、公式の Github ⧉ を参考にしてください。
テンプレートのパス変更
サブジェネレータの構成変更に伴って、テンプレートのファイル構成にも変更が入っています。一例として RESTController のファイルパスを紹介します。
- 変更前:
generators/entity-server/templates/ src/main/java/package/web/rest/EntityResource.java.ejs
- 変更後:
generators/server/templates/entity/ src/main/java/package/web/rest/_EntityClass_Resource.java.ejs
Blueprint では同名のファイルを配置することで、テンプレートを差し替えることが可能なため、ファイル名やパスについては、最新版のサブジェネレータの内容にあわせて対応が必要です。
ジェネレータで実行されるタスクの変更
ジェネレータの generator.mjs
で実行されるエンティティを処理するためのタスクとして writingEntities
や postWritingEntities
などが追加されています。JHipster7 までは entity-client
等のエンティティを処理するためのジェネレータがありましたが、JHipster8 で廃止されました。エンティティ単位でコードの生成処理を行いたい場合は、writingEntities
や postWritingEntities
上に記述することになります。
これらの変更により、JHipster Blueprint の紹介記事で、エンティティの PDF ファイルを出力する Blueprint を作成しましたが、そのままでは JHipster8 で動作しません。これから紹介する Blueprint の機能と実例の中で、JHipster8 に対応したサンプルも掲載しております。
Blueprint の機能・コード生成処理
Side-by-Side Blueprint
Side-by-Side Blueprint を有効化したBlueprintを作成すると、既存のサブジェネレータの生成処理が動作した上で、Blueprint 側で定義したコード生成処理が動作するようになります。
開発においては JHipster 標準のサブジェネレータの動作結果を活用しないケースは少なく、出力結果をカスタマイズしたり、一部のテンプレートを差し替えたりすることが多いため、基本的には Side-by-Side Blueprint は有効化した状態で Blueprint を作成することになります。
逆に JHipster 標準のサブジェネレータによる出力結果を利用しない場合や、標準にはないサブジェネレータを新しく追加する場合は、Side-by-Side Blueprint を使用せず Blueprint を作成します。テンプレートやコードを生成する処理を全て自作する必要があるので、注意が必要です。
Side-by-Side Blueprint の利用方法
Side-by-Side Blueprint の有効化は、Blueprint プロジェクト作成時にサブジェネレータ単位で選択できます。
? Is client generator a side-by-side blueprint? Yes
後から Side-by-Side Blueprint の有効・無効を変更したい場合は、generator.mjs
のコンストラクタで設定できます。
export default class extends BaseApplicationGenerator {
constructor(args, opts, features) {
super(args, opts, { ...features, sbsBlueprint: true });
}
Custom CLI
Custom CLI は Blueprint に対して専用の CLI を作成できます。
Blueprint が my-blueprint
の場合は jhipster-my-blueprint
のようなコマンドが作成されます。
Custom CLI を使用すると Blueprint に依存関係のあるバージョンの JHipster を利用したり、カスタムジェネレータを指定して実行できます。特に作成によるデメリットもないため、Custom CLI については Blueprint 作成時は有効化しておいて問題ありません。
カスタムジェネレータは client
や server
などの JHipster 標準のサブジェネレータをオーバーライドするのではなく、Blueprint 側だけで作成した新しい名称のサブジェネレータです。カスタムジェネレータについては、この記事の後半で個別に紹介します。
Custom CLI の利用方法
Custom CLI は jhipster generate-blueprint
コマンドによる Blueprint プロジェクト作成時に選択できます。
? Add a cli? Yes
Custom CLI を有効化すると、Blueprint プロジェクトで npm install
や npm link
を行った際に、/usr/local/bin/
などにシンボリックリンクが作成され、コマンドが実行できるようになります。
また、カスタムジェネレータを追加し、Custom CLI に含める設定も行えます。以下は csv, report というカスタムジェネレータを追加する場合の例です。
? Comma separated additional sub-generators. csv,report
? Is csv generator a cli command? Yes
? What task do you want do implement at csv generator? writing, writingEntities
? Is report generator a cli command? Yes
? What task do you want do implement at report generator? writing, writingEntities
追加したカスタムジェネレータは以下のようなコマンドで実行でき、それぞれのジェネレータに定義したコード生成処理のみが動作します。
jhipster-my-blueprint csv
jhipster-my-blueprint report
LocalBlueprint
LocalBlueprint は Blueprint を JHipster のプロジェクト配下に作成するための機能です。
LocalBlueprint は JHipster プロジェクト内のフォルダ .blueprint
に作成され、jhipster
コマンド時に自動的に適用されるため、通常の Blueprint プロジェクトのように、npm リポジトリへの登録や、npm install
, npm link
の実行等、利用するための準備が不要となります。
LocalBlueprint は開発中の JHipster プロジェクトにおいて、プロジェクト固有の Blueprint を作成したい場合に活用できます。プロジェクト配下のフォルダに Blueprint も格納されるため、プロジェクト全体のソースコードを一元管理できるメリットがあります。
逆にプロジェクト固有ではなく、複数のプロジェクトで使うことを想定した Blueprint は専用の Blueprint プロジェクトを作成することになります。
開発時は LocalBlueprint でプロジェクト固有のコード生成処理を定義し、他のプロジェクトにも活用できそうなものであれば、個別の Blueprint プロジェクトを作成し、移植する等の活用が考えらえます。
LocalBlueprint の利用方法
LocalBlueprint は jhipster generate-blueprint
コマンドによる Blueprint 作成時に選択できます。
事前に JHipster プロジェクトを作成し、そのフォルダ配下で実行する必要があります。
? Do you want to generate a local blueprint inside your application? Yes
.blueprint
フォルダ配下に、Blueprint 関連ファイルが作成されます。
このフォルダ配下のコード生成処理をカスタマイズした後、jhipster
コマンドで反映できます。
テンプレートの置き換え
JHipster オリジナルのジェネレータ内に定義されているテンプレートとなるファイルを、Blueprint で用意したファイルに置き換えできます。
オリジナルのジェネレータに含まれるロゴや favicon 等の画像、画面デザイン、サーバの処理などを差し替えたい場合に、テンプレートの置き換えを活用することになります。
テンプレートを変更してしまうと JHipster のバージョンアップ等で元のテンプレートが大きく変更された場合に、追従が大変になる場合があります。テンプレートを大きく改変するケースはやむを得ませんが、軽微なコード変更の場合は、後述の文字列の操作を活用したほうがよいです。
テンプレート置き換えの利用方法・サンプル
テンプレートの置き換えについては、JHipster オリジナルのジェネレータ内に定義されているテンプレートのファイルパスと同一となるファイルを、Blueprint 側にも配置するだけで利用できます。generator.mjs
でコードの生成処理などを作成する必要はありません。
JHipster オリジナルのテンプレートは、各ジェネレータの templates
フォルダ配下に存在しています。
npm install
した node_modules/generator-jhipster
配下から各ジェネレータのテンプレートのファイルパスを確認して、配置してください。
例えば、ロゴや favicon を置き換える場合は client
ジェネレータに以下のファイルを用意します。
client/templates/src/main/webapp/favicon.ico
client/templates/src/main/webapp/content/images/logo-jhipster.png
また、フッターのファイルを変更し、コピーライトを入れる場合は react
ジェネレータに以下のファイルを用意します。
react/templates/src/main/webapp/app/shared/layout/footer/footer.tsx.ejs
import './footer.scss';
import React from 'react';
import { Translate } from 'react-jhipster';
import { Col, Row } from 'reactstrap';
const Footer = () => (
<div className="footer page-content">
<Row>
<Col md="12">
<p>
© 2023 ALPHA SYSTEMS INC.
</p>
</Col>
</Row>
</div>
);
export default Footer;
以下のようにロゴやフッター等が置き換えられます。
テンプレートの追加
JHipster オリジナルのジェネレータ内に定義されていないテンプレートを準備し、Blueprint でコード生成できます。
JHipster オリジナルでは用意されていない機能や画面、画像ファイルなどを追加する場合に活用します。
テンプレート追加の利用方法・サンプル
Blueprint 内のフォルダにテンプレートを配置し、コード生成処理を generator.mjs
に記述することで利用できます。
JHipster Blueprint の紹介記事で エンティティの内容を PDF ファイルとしてダウンロードする機能を追加しましたが、そちらではテンプレートの追加を活用しています。
PDF ダウンロード用の RestController を作成するため、server
ジェネレータの templates
配下に以下のファイルを用意します。
src/main/java/package/web/rest/report/ReportEntityResource.java.ejs
src/main/resources/reports/report_entity.jrxml.ejs
コードの詳細(クリックして開く/閉じる)
ReportEntityResource.java.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);
}
}
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>
追加したテンプレートを元に実際のコード生成を行う場合は、server
ジェネレータの generator.mjs
に以下のように記述します。
get [BaseApplicationGenerator.WRITING_ENTITIES]() {
return this.asWritingEntitiesTaskGroup({
async writingEntitiesTemplateTask({ entities }) {
for (const entity of entities.filter(entity => (!entity.skipServer && !entity.builtIn))) {
await this.writeFiles({
sections: {
files: [
{
path: 'src/main/java/',
templates: [
{
file: 'package/web/rest/report/ReportEntityResource.java',
renameTo: generator => `${generator.entityAbsoluteFolder}/web/rest/report/Report${generator.entityClass}Resource.java`
},
],
},
{
path: 'src/main/resources/',
templates: [
{
file: 'reports/report_entity.jrxml',
renameTo: generator => `reports/report_${generator.entityClass}.jrxml`
},
],
},
],
},
context: entity,
});
}
}
});
}
前回の記事と比較して、JHipster8 となったことでコード生成処理の記述先が get [BaseApplicationGenerator.WRITING_ENTITIES]()
になり、for
文によりエンティティ単位で処理する記述に変更となっています。
ファイルの削除
JHipster オリジナルのジェネレータで生成されたファイルやフォルダを削除できます。
開発の際に不要となるファイルを予め削除したり、未使用となる機能を削除するなどの利用が考えられます。
ファイル削除の利用方法・サンプル
ファイルの削除は各ジェネレータの generator.mjs
で deleteDestination
等を使用することで利用できます。
ホーム画面に表示されるキャラクター画像を削除する場合は、client
ジェネレータの generator.mjs
に以下のように記述します。
images
配下の特定のファイル名を含むファイルを削除しています。
get [BaseApplicationGenerator.POST_WRITING]() {
return this.asPostWritingTaskGroup({
async postWritingTemplateTask() {},
async deleteImages() {
this.deleteDestination('src/main/webapp/content/images/jhipster_family_member_*');
},
});
}
以下のように指定した名称に合致するファイルが削除されます。
文字列の操作
生成されたコードに対して、文字列の操作を行えます。これはオリジナルのジェネレータの生成結果と Blueprint による生成結果のどちらのコードにも適用できます。
画面上の文言変更、設定ファイルへの項目追加、ソースコード上の import やアノテーションの追加等、文字列の操作で行えることは多岐にわたります。
前述のテンプレートの置き換えではファイルごと差し替える必要があるため、JHipster のバージョンアップによるテンプレートの変更に追従し辛いデメリットがありますが、文字列の操作の場合は元となる文字列に変更がなければ、継続して利用できます。
文字列の操作の利用方法・サンプル
文字列の操作は各ジェネレータの generator.mjs
で editFile
に対象のファイルと文字列を指定することで利用できます。指定した文字列に対して replace
や slice
等を使って、必要なコードの挿入や置換を実施できます。
画面で使用するアイコン画像を追加する場合は、react
ジェネレータの generator.mjs
に以下のように記述します。
icon-loader.ts
で Font Awesome ⧉ の PDF アイコンを追加設定しています。
get [BaseApplicationGenerator.POST_WRITING]() {
return this.asPostWritingTaskGroup({
async postWritingTemplateTask() {},
async editIconLoaders() {
this.editFile(
`src/main/webapp/app/config/icon-loader.ts`,
contents => contents.includes('faFilePdf')
? contents : contents.replaceAll(
`faWrench,`, `faWrench,faFilePdf,`)
)
},
});
}
追加したアイコン画像を React のコンポーネントで使用する場合は以下のように指定します。
<FontAwesomeIcon icon="file-pdf" /> <span className="d-none d-md-inline" >PDF</span>
JHipster Blueprint の紹介記事で PDF ダウンロードボタンを追加した方法も、文字列の操作を活用しています。
get [BaseApplicationGenerator.POST_WRITING_ENTITIES]() {
return this.asPostWritingEntitiesTaskGroup({
async postWritingEntitiesTemplateTask({ entities }) {
for (const entity of entities.filter(entity => !entity.skipClient && !entity.builtIn)) {
const primaryKeyName = entity.primaryKey.name;
const entityInstance = entity.entityInstance;
const entityApiUrl = entity.entityApiUrl;
const entityFolderName = entity.entityFolderName;
const entityFileName = entity.entityFileName;
this.editFile(
`src/main/webapp/app/entities/` + entityFolderName + `/` + entityFileName + `.reducer.ts`,
contents => contents.includes('downloadEntitiesPdf')
? contents : contents.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`,
contents => contents.includes('downloadEntitiesPdf')
? contents : contents.replace(
`getEntities`,
`getEntities, downloadEntitiesPdf`
).replace(
`const handleSyncList = () => {`,
`const handleDownloadPdf = (${primaryKeyName}?: number) => { dispatch(downloadEntitiesPdf(${primaryKeyName})); };
const handleSyncList = () => {`
).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>`)
);
}
},
});
}
前回の記事と比較して JHipster8 となったことでコード生成処理の記述先が generators/react/generator.mjs
の get [BaseApplicationGenerator.POST_WRITING_ENTITIES]()
になり、for
文によりエンティティ単位で処理する記述に変更となっています。
ライブラリの追加
フロントエンド、サーバサイドそれぞれで使用するライブラリを追加できます。
新しい機能を追加する Blueprint を作成する場合に、追加機能で使用するライブラリを build.gradle
や package.json
に追加する際に利用します。
ライブラリ追加の利用方法・サンプル
ライブラリの追加は各ジェネレータの generator.mjs
で addGradleMavenRepository
, addGradleDependency
, packageJson.merge
等を使用することで利用できます。
build.gradle
に JasperReports の設定を追加するには、server
ジェネレータの generator.mjs
に以下のように記述します。
get [BaseApplicationGenerator.POST_WRITING]() {
return this.asPostWritingTaskGroup({
async postWritingTemplateTask() {},
async addGradleJasperreports({ source }) {
source.addGradleMavenRepository({
url: 'https://jaspersoft.jfrog.io/jaspersoft/third-party-ce-artifacts/'
});
source.addGradleDependency({
groupId: 'net.sf.jasperreports',
artifactId: 'jasperreports',
version: 'latest.release',
scope: 'implementation'
});
}
});
}
package.json
に exceljs の設定を追加するには、client
ジェネレータの generator.mjs
に以下のように記述します。
get [BaseApplicationGenerator.POST_WRITING]() {
return this.asPostWritingTaskGroup({
async postWritingTemplateTask() {},
async mergePackageJson() {
this.packageJson.merge({
dependencies: {
'exceljs': '4.3.0',
},
});
},
});
}
カスタムジェネレータの追加
Custom CLI でも紹介したカスタムジェネレータは JHipster 標準のサブジェネレータをオーバーライドするのではなく、Blueprint 側だけで作成した新しい名称のサブジェネレータです。
CSV 出力や帳票出力などのまとまった機能をカスタムジェネレータとして Blueprint を作成しておき、その機能が必要な場合のみ、Custom CLI を使用してプロジェクトに機能を追加できます。
Blueprint を実装する際に JHipster 標準のサブジェネレータにオーバーライドする対象が存在しない場合や、機能を一つのサブジェネレータに集約したい場合、コマンドラインオプション等でサブジェネレータを個別に実行したい場合に、カスタムジェネレータとして実装します。
カスタムジェネレータの利用方法・サンプル
カスタムジェネレータは jhipster generate-blueprint
コマンドによる Blueprint プロジェクト作成時に選択できます。
additional sub-generators
の設問で、任意のジェネレータ名を指定します。以下は report
ジェネレータを追加する例です。
? Comma separated additional sub-generators. report
? Is report generator a cli command? Yes
? What task do you want do implement at report generator? writing, writingEntities
追加したカスタムジェネレータ配下に、今まで紹介した Blueprint の機能を活用し、テンプレートの追加や generator.mjs
によるコード生成処理を定義します。
JHipster オリジナルに存在しないジェネレータのため、Side-by-Side Blueprint が有効化できない点に注意が必要です。通常のジェネレータで動作していたコード生成処理が、カスタムジェネレータで動作しないケースもあります。
例えば、build.gradle
に JasperReports の設定を追加する処理を行うには、gradle
のジェネレータが含まれている必要があります。
以下のように generator.mjs
で get [BaseApplicationGenerator.COMPOSING]()
を定義し、composeWithJHipster(GENERATOR_GRADLE)
を追加することで、report
ジェネレータ実行時に、gradle
ジェネレータも実行され、build.gradle
に JasperReports の設定を追加する処理が動作可能になります。
get [BaseApplicationGenerator.COMPOSING]() {
return this.asComposingTaskGroup({
async composingTemplateTask() { },
async composeWithJHipster() {
await this.composeWithJHipster(GENERATOR_GRADLE);
},
});
}
あわせてテンプレートの追加や文字列の操作で紹介した PDF ダウンロードに関するコード生成処理を、カスタムジェネレータ report
内にまとめて定義します。クライアント側、サーバ側の区別も特に必要ありません。
追加したカスタムジェネレータは以下のようにコマンラインパラメータで指定して実行できます。
jhipster-my-blueprint report --force
実行するとカスタムジェネレータに関連したコードのみを生成できます。複数のカスタムジェネレータをもった Blueprint プロジェクトから、必要な機能のみを適用したい場合に利用します。
force gradlew
force .yo-rc.json
force .jhipster/Employee.json
force gradlew.bat
force gradle/wrapper/gradle-wrapper.jar
force gradle/wrapper/gradle-wrapper.properties
create src/main/resources/reports/report_Employee.jrxml
force build.gradle
create src/main/java/com/mycompany/myapp/web/rest/report/ReportEmployeeResource.java
force src/main/webapp/app/entities/employee/employee.reducer.ts
force src/main/webapp/app/entities/employee/employee.tsx
また、app
ジェネレータで composeWithJHipster
の設定を行うことで、デフォルトでカスタムジェネレータを動作させることもできます。カスタムジェネレータとして実装した機能を、Blueprint の適応時に自動的に反映させたい場合に利用します。
get [BaseApplicationGenerator.COMPOSING]() {
return this.asComposingTaskGroup({
async composeWithJHipster() {
await this.composeWithJHipster(`jhipster-my-blueprint:report`);
},
});
}
おわりに
本記事では、Blueprint のコード生成機能の使い方や具体例について詳しく解説してきました。ここまでの内容を組み合わせることで、さまざまな機能を簡単に実装できることを実感していただけたのではないでしょうか。
Blueprint で行えることは、この他にもありますので、JHipster 公式の Blueprint ⧉ で実装されているものも参考にしてみてください。
今回の記事が、JHipster と Blueprint を試してみるきっかけとなれば幸いです。
JHipster は 8.0.0 がリリースされたばかりですが、Blueprint はバージョンアップの影響を受けやすい側面がありますので、新しいバージョンでの検証なども続けていきたいと思います。
JHipster 関連記事
- JHipster によるエンタープライズアプリケーションの構築(1) 紹介編
- JHipster によるエンタープライズアプリケーションの構築(2) Spring Nativeで高速起動化しAWS Lambdaで動かす
- JHipster によるエンタープライズアプリケーションの構築(3) Blueprint でコード生成処理をカスタマイズする
- JHipster によるエンタープライズアプリケーションの構築(4) Liquibaseによるデータベース管理
- JHipster によるエンタープライズアプリケーションの構築(5) Spring Modulithでモジュラーモノリス化する