Welcome to spring-boot-doma2-sample¶
User Documents¶
Getting started¶
IDEのインストール¶
- Eclipseの場合
- IntelliJの場合
- Lombok pluginをインストールする。
- Settings > Build, Excecution, Deployment > Compiler > Annotation Processor >
Enable Annotation Processing
をONにする。
- Settings > Build, Excecution, Deployment > Compiler > Annotation Processor >
- Eclipse Code Formatterをインストールする。
- Settings > Other Settings > Eclipse Code Formatter >
Use the Eclipse code formatter
をONにする。 Eclipse Java Formatter config file
にeclipse-formatter.xml
を指定する。
- Settings > Other Settings > Eclipse Code Formatter >
- bootRunを実行している場合でもビルドされるようにする。
- Intellij > Ctrl+Shift+A > type Registry... >
compiler.automake.allow.when.app.running
をONにする。
- Intellij > Ctrl+Shift+A > type Registry... >
- Windowsの場合は、コンソール出力が文字化けするため、
C:¥Program Files¥JetBrains¥IntelliJ Idea xx.x.x¥bin
の中にあるidea64.exe.vmoptions
ファイルに-Dfile.encoding=UTF-8
を追記する。
- Lombok pluginをインストールする。
- 共通
- ブラウザにLiveReload機能拡張をインストールする。
http://livereload.com/extensions/
から各ブラウザの機能拡張をダウンロードする。
- ブラウザにLiveReload機能拡張をインストールする。
静的コンテンツ¶
静的コンテンツの置き場所¶
静的コンテンツの置き場所を設定ファイルに指定する。
デフォルト値は下記が設定されているので、src/main/resources/static
、src/main/resources/public
などに配置されたファイルは静的コンテンツとして公開される。
spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/
キャッシュコントロール¶
キャッシュする時間を設定する。24時間キャッシュする場合は、下記を設定する。
spring.resources.cache-period=86400
静的コンテンツファイルをバージョニングする。ファイル名を変更せずにファイル内容を変えた場合に、キャッシュが効かないようにする。 静的コンテンツの内容からMD5ハッシュ値を計算してファイル名にする。
下記の設定をすると自動的にファイル名にハッシュ値が含まれるようになる。
spring.resources.chain.strategy.content.enabled=true
spring.resources.chain.strategy.content.paths=/**
出力例
<!-- テンプレートは普通にファイルを指定する --> <link rel="shortcut th:href="@{/static/images/favicon.png}" /> <!-- 下記のようにハッシュ値が自動で出力される --> <link rel="shortcut href="/admin/static/images/favicon-ca31b78daf0dd9a106bbf3c6d87d4ec7.png" />
WebJars¶
WebJarsを使ってJavascript、CSSライブラリを管理する。 build.gradleにライブラリを追加する。
compile "org.webjars:webjars-locator"
compile "org.webjars:jquery:2.2.4"
webjars-locatorを使えるようにする。
public class WebAppConfig extends WebMvcConfigurerAdapter {
// ...
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// webjarsをResourceHandlerに登録する
registry.addResourceHandler("/webjars/**")
// JARの中身をリソースロケーションにする
.addResourceLocations("classpath:/META-INF/resources/webjars/")
// webjars-locatorを使うためにリソースチェイン内のキャッシュを無効化する
.resourceChain(false);
}
}
webjars-locatorを使うとバージョン指定が不要になる。
<!-- テンプレートにはバージョンを書かない -->
<link th:src="@{/webjars/jquery/jquery.min.js}" />
<!-- 下記に変換される(2.2.4を依存関係に指定しているため) -->
<link src="/admin/webjars/jquery/2.2.4/jquery.min.js" />
Jarファイルに内包されたGZip済みリソースを使うようにする。
spring.resources.chain.compressed=true
セキュリティ¶
Spring Securityの設定で認証をかけないようにする。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// ...
@Override
public void configure(WebSecurity web) throws Exception {
// 静的ファイルへのアクセスは認証をかけない
web.ignoring()//
.antMatchers("/webjars/**", "/static/**");
}
}
バリデーション¶
クライアントサイド¶
jQuery Validation Pluginを使って、 FormのSubmitをフックして入力エラーがある場合は、動的にエラーメッセージを表示する。 エラーがない場合は、Submitが行われる。
$(function() {
// メッセージを上書きする
$.extend($.validator.messages, {
minlength: $.validator.format("{0}文字以上の文字を入力してください"),
...
});
$.validator.setDefaults({
errorPlacement: function(error, element) {
// エラーが発生した項目の色を変える
},
success: function(error, element) {
// エラーがない状態に変わった時、色を戻す
},
onkeyup: function(element, event ) {
// キーを離した時にバリデーションする
},
onfocusout: function(element) {
// フォーカスが外れた時にバリデーションする
},
submitHandler: function(form){
form.submit();
}
});
// 入力チェックの種類を独自に増やす、または動作を上書きする
$.validator.methods.email = function(value, element) {
// 入力チェックして、エラーがある場合はfalseを返す
};
var options = {
rules: {
firstName: {
required: true,
maxlength: 50
}
}
};
// 初期化
$("#form1").submit(function(e) {
// validation pluginでsubmitするため潰しておく
e.preventDefault();
}).validate(options);
});
サーバーサイド¶
Bean Validation(単一項目チェック)¶
入力フォームのフィールドにアノテーションを指定する。
@Getter
@Setter
public class UserForm {
@NotEmpty // ★空でないことをチェック
@Size(min=2, max=30) // ★文字列長の下限上限チェック
public String name;
@Email // ★メールアドレス書式チェック
public String email;
}
コントローラーの引数に@Validatedアノテーションを指定する。
public class UserController {
@PostMapping("/new")
public String newUser(@Validated UserForm form, BindingResult result, RedirectAttributes attributes) { // ★@Validatedを指定する
if (result.hasErrors()) { // ★バリデーションエラーがある場合はTrueが返る
setFlashAttributeErrors(attributes, result);
return "redirect:/user/users/new";
}
}
}
画面にバリデーションエラーの内容を表示する。
<form th:object="${userForm}" method="post">
<div th:with="valid=${!#fields.hasErrors('name')}">
<input type="text" th:field="*{'name'}" />
<span th:if="${!valid}" th:errors="*{'name'}">Error</span><!-- エラーがある場合はメッセージが表示される -->
</div>
</form>
Spring Validator(相関チェック)¶
サンプルの実装では、org.springframework.validation.Validator
を実装した基底クラスを用意しているので、
下記のようなバリデーターを実装すれば相関チェックができる。
@Component // ★Autowireするため@Componentを指定する
public class UserFormValidator extends AbstractValidator<UserForm> { // ★ジェネリクスにForm型を指定する
@Override
protected void doValidate(UserForm form, Errors errors) { // ★ジェネリクスの型で引数を受け取る
// 確認用パスワードと突き合わせる
if (!equals(form.getPassword(), form.getPasswordConfirm())) { // ★任意にチェックする
errors.rejectValue("password", "users.unmatchPassword"); // ★エラーがあればErrorsオブジェクトに追加する
errors.rejectValue("passwordConfirm", "users.unmatchPassword");
}
}
}
注釈
エラーメッセージは、ValidationMessages.propertiesに定義する。
コントローラーにFormオブジェクトごとにバリデーターを設定する。
単一項目チェックと同様に、@Validated
アノテーションを指定しているとバリデーターが動作する。
@Controller
public class UserController {
// ...
@Autowired
UserFormValidator userFormValidator; // ★Spring Validatorの実装
@InitBinder("userForm")
public void validatorBinder(WebDataBinder binder) {
binder.addValidators(userFormValidator); // ★userFormにバインドする
}
// ...
}
アノテーションの自作¶
共通化したい場合は下記のようなアノテーションクラスを作成して利用できる。
@Documented
@Constraint(validatedBy = { ZipCodeValidator.class }) // ★バリデーターを指定する
@Target({ ElementType.METHOD, ElementType.FIELD })
@Retention(RUNTIME)
public @interface ZipCode {
String message() default "{validator.annotation.ZipCode.message}"; // ★バリデーションでエラーになった場合のメッセージキー
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ FIELD })
@Retention(RUNTIME)
@Documented
@interface List {
ZipCode[] value();
}
}
アノテーションと対になるバリデーターを作成する。
public class ZipCodeValidator implements ConstraintValidator<ZipCode, String> { // ★どのアノテーションを対象とするか指定する
static final Pattern p = Pattern.compile("^[0-9]{7}$");
@Override
public void initialize(ZipCode ZipCode) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
boolean isValid = false;
if (StringUtils.isEmpty(value)) {
isValid = true;
} else {
Matcher m = p.matcher(value);
if (m.matches()) {
isValid = true;
}
}
return isValid;
}
}
メッセージ¶
メッセージ定義ファイルの指定¶
spring.messages.basename=messages,ValidationMessages,PropertyNames # カンマ区切りで複数ファイルを指定できる
spring.messages.cache-duration=-1 # -1でキャッシュが無効になる。本番環境ではある程度キャッシュする
spring.messages.encoding=UTF-8
サンプルの実装では下記の役割りでファイルを分割している。
- messages.properties
- 静的文言などのメッセージを定義する。
- ValidationMessages.properties
- バリデーションエラーのメッセージを定義する。
- PropertyNames.properties
- バリデーションエラーが発生した際に項目名を表示するための項目名を定義する。
ロケールによるメッセージ切替¶
下記のBeanを定義することで、i18n対応が可能になる。
ロケールが「ja」の場合は、messages_ja.properties
やValidationMessages_ja.properties
に定義したメッセージが使われる。
@Bean
public LocaleResolver localeResolver() {
// Cookieに言語を保存する
val resolver = new CookieLocaleResolver();
resolver.setCookieName("lang"); // cookieのlang属性に設定された値を利用してロケールを切り替える
return resolver;
}
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
// langパラメータでロケールを切り替える
val interceptor = new LocaleChangeInterceptor();
interceptor.setParamName("lang"); // ロケール切替に使うパラメータ名を指定する
return interceptor;
}
例外のハンドリング¶
共通処理で例外をハンドリングする¶
サンプルの実装では、@ControllerAdvice
アノテーションを指定した例外ハンドラーで、
共通的に例外をハンドリングしている。
新しく例外クラスを作成する場合は、共通的にハンドリングするべきものであれば、
本クラスにハンドラーメソッドを追加すること。
@ControllerAdvice(assignableTypes = { AbstractHtmlController.class }) // RestControllerでは動作させない
public class HtmlExceptionHandler {
// ...
}
注釈
業務ロジックで発生する機能固有の例外は、業務ロジックの中でハンドリングして、 適切なメッセージを画面に表示することが望ましい。
排他制御¶
楽観的排他制御¶
サンプルの実装では、楽観的排他制御は下記の流れで動作するようになっている。
- 画面表示処理で、改定番号を含めてSELECT文を発行する。
- SessionAttributeに指定されたFormオブジェクトを使って、改定番号をセッションに保存する。
- 保存ボタンが押下された後、保存処理でセッションに保存された改定番号をWHERE句に指定してUPDATE文を発行する。
- 更新件数が0件の場合は、排他エラーとする。
共通のエンティティに、排他制御用のフィールドを定義している。 複数のテーブルに対しての同時の排他制御は、ここでは取り扱わない。
楽観的排他制御は、Doma2の機能を利用しているので下記のドキュメントを参照されたい。 Doma 2.0 SQLの自動生成による更新
public abstract class DomaDtoImpl implements DomaDto, Serializable {
// ..
// 楽観的排他制御で使用する改定番号
@Version // ★楽観的排他制御に使う項目であることを示す
@Column(name = "version") // ★DBのカラム名
@JsonIgnore // レスポンスするJSONに含めない項目
Integer version;
}
悲観的排他制御¶
悲観的排他制御についても、Doma2の機能を利用しているので、下記のドキュメントを参照されたい。 Doma 2.0 検索 - 悲観的排他制御
ページング処理と同様に、SelectOptionsを作成し、forUpdate
メソッドを呼び出してからdaoの引数に渡すことで、
SELECT ...FOR UPDATE文が発行される。
// 悲観的排他制御
val options = createSearchOptions(pageable).forUpdate(); // ★Pageableを元にDoma2のSelectOptionsを作成する
val users = userDao.selectAll(where, options, toList()); // ★SELECT ...FOR UPDATE
二重送信防止¶
二重送信防止とは¶
Webアプリケーションでは、以下の操作を行うと同じ処理が2回実行されて、 データが重複することがある。
- 登録ボタンを連続して押下する。
- 登録処理が終わった後、ブラウザの再読み込みボタンを押すことで、前回の処理が再度実行される。
- 登録完了画面に遷移してから、ブラウザの戻るボタンを押すことで、登録処理が再実行される。
対策方法¶
下記の対策をすると二重送信がある程度防止できる。
- Javascriptを使ってボタンを連続して押せないようにする。
- Post-Redirect-Getパターンを利用して、フォームの再送信をできないようにする。
- トークンによる制御
- 画面表示のタイミングでトークンを払い出す。
- 登録ボタンを押下する。(画面に埋め込まれたトークンも送信)
- サーバーで払い出したトークンと、画面から渡ったトークンを比較して一致しなければ不正とする。
- 正常に処理した場合は、トークンを破棄する。
サンプルの実装での例¶
サンプルの実装では、下記の流れで、 二重送信防止チェックを行っている。
DoubleSubmitCheckingRequestDataValueProcessor
がFormタグにトークンを埋め込む。
public class DoubleSubmitCheckingRequestDataValueProcessor implements RequestDataValueProcessor {
// ...
@Override
public Map<String, String> getExtraHiddenFields(HttpServletRequest request) {
val map = PROCESSOR.getExtraHiddenFields(request);
String token = DoubleSubmitCheckToken.getExpectedToken(request);
if (token == null) {
token = DoubleSubmitCheckToken.renewToken(request);
}
if (!map.isEmpty()) {
// ★トークンを埋め込む
map.put(DoubleSubmitCheckToken.DOUBLE_SUBMIT_CHECK_PARAMETER, token);
}
return map;
}
// ...
}
SetDoubleSubmitCheckTokenInterceptor
で、POSTメソッドのリクエストをインターセプトして、トークンの比較を行う。- 画面からトークンが渡ってきていない場合は、新しいトークンを生成して内部的に保持しておく。
- トークンの比較で同一の場合は、そのまま処理を続ける。
public class SetDoubleSubmitCheckTokenInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// コントローラーの動作前
val expected = DoubleSubmitCheckToken.getExpectedToken(request);
val actual = DoubleSubmitCheckToken.getActualToken(request);
DoubleSubmitCheckTokenHolder.set(expected, actual);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// コントローラーの動作後
if (StringUtils.equalsIgnoreCase(request.getMethod(), "POST")) {
// POSTされたときにトークンが一致していれば新たなトークンを発行する
val expected = DoubleSubmitCheckToken.getExpectedToken(request);
val actual = DoubleSubmitCheckToken.getActualToken(request);
if (expected != null && actual != null && Objects.equals(expected, actual)) {
DoubleSubmitCheckToken.renewToken(request);
}
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// 処理完了後
DoubleSubmitCheckTokenHolder.clear();
}
}
- Doma2のリスナー
DefaultEntityListener
で、トークンの比較を行う。- INSERT文を発行する前のタイミング(preInsert)でトークンの比較を行い、一致しない場合は、例外をスローする。
public class DefaultEntityListener<ENTITY> implements EntityListener<ENTITY> {
@Override
public void preInsert(ENTITY entity, PreInsertContext<ENTITY> context) {
// 二重送信防止チェック
val expected = DoubleSubmitCheckTokenHolder.getExpectedToken();
val actual = DoubleSubmitCheckTokenHolder.getActualToken();
if (expected != null && actual != null && !Objects.equals(expected, actual)) {
throw new DoubleSubmitErrorException(); // ★一致しない場合は、例外をスローする
}
// ...
}
// ...
}
HtmlExceptionHandler
でDoubleSubmitErrorException
をハンドリングする。- 二重送信を検知した旨のメッセージをFlashMapに設定して、元の画面にリダイレクトする。
@ControllerAdvice(assignableTypes = { AbstractHtmlController.class }) // RestControllerでは動作させない
public class HtmlExceptionHandler {
// ...
@ExceptionHandler({ DoubleSubmitErrorException.class })
public RedirectView handleDoubleSubmitErrorException(Exception e, HttpServletRequest request,
HttpServletResponse response) {
// 共通メッセージを取得する
val locale = RequestContextUtils.getLocale(request);
val messageCode = DOUBLE_SUBMIT_ERROR; // ★二重送信エラーのメッセージ
val view = getRedirectView(request, response, locale, messageCode);
return view;
}
// ...
}
業務ロジック¶
宣言的トランザクション管理¶
サンプルの実装では、BaseTransactionalService
を継承することで、
宣言的トランザクション管理が適用される。トランザクション管理が不要な場合は、
BaseService
を継承する。
@Service
public class UserService extends BaseTransactionalService { // ★親クラスで@Transactionalを宣言済み
@Autowired
UserDao userDao;
@Autowired
UserRoleDao userRoleDao;
// ...
// ★AOPでこのメソッドを囲うようにDBトランザクションの開始・終了が行われる
// ★例外が発生した場合はロールバックされる
public User create(final User inputUser) {
Assert.notNull(inputUser, "inputUser must not be null");
// 1件登録
userDao.insert(inputUser); // ★1つ目のテーブル
// 役割権限紐付けを登録する
val userRole = new UserRole();
userRole.setUserId(inputUser.getId());
userRole.setRoleKey("user");
userRoleDao.insert(userRole); // ★2つ目のテーブル
return inputUser;
}
}
BaseTransactionalService
を継承することで、漏れなく@Transactional
の指定が行われる。
ただし、読み取り専用のメソッドには、@Transactional(readOnly = true)
を指定する必要がある。
ページング処理¶
@Transactional(readOnly = true) // 読み取りのみの場合は指定する
public Page<User> findAll(User where, Pageable pageable) { // ★何件ずつ取得するか、何ページ目を取得するかをPageableに設定して引数に渡す
Assert.notNull(where, "where must not be null");
// ページングを指定する
val options = createSearchOptions(pageable).count(); // ★Pageableを元にDoma2のSelectOptionsを作成する
val users = userDao.selectAll(where, options, toList());
// ★SelectOptionsのcountメソッドを呼び出すと、件数取得とレコード取得が一つのSQLで行える
return PageFactory.create(users, pageable, options.getCount()); // ファクトリメソッドにリストを渡してPageオブジェクトで包んで返す
}
注釈
画面側のプロジェクトがDoma2ライブラリを依存関係に持たなくてよくするため、Doma2のSelectOptionsを直接に受け取らないようにする。 Pageインターフェースの実装を後から変更したくなった場合に一括修正しなくても済むようにファクトリメソットを利用する。
メール送信¶
JavaMailSenderを利用する¶
サンプルの実装では、JavaMailSender
を使ってメールを送信するヘルパークラスを実装している。
メール本文のテンプレートは、データベースに持たせることを想定しているので、
まず本文をテンプレートエンジン(Thymeleaf)に掛けてから、
メール送信メソッドの引数に渡す流れで処理する。
@Component
@Slf4j
public class SendMailHelper {
@Autowired
JavaMailSender javaMailSender;
/**
* メールを送信します。
*
* @param fromAddress
* @param toAddress
* @param subject
* @param body // テンプレートエンジンでプレースホルダーを埋めた文字列
*/
public void sendMail(String fromAddress, String[] toAddress, String subject, String body) {
// ...
}
/**
* 指定したテンプレートのメール本文を返します。
*
* @param template // ThymeleafのTEXT型の文法で書かれたテンプレート文
* @param objects // テンプレートのプレースホルダーを埋めるための変数
* @return
*/
public String getMailBody(String template, Map<String, Object> objects) {
// ...
}
}
DBマイグレーション¶
flywayのバージョンを変更する¶
Spring Boot 1.5.6
では、Flyway v3.2.1
が依存関係に入っているが、
下記の設定をbuild.gradle
に指定することでバージョンを引き上げられる。
(Flyway v4
では、リピータブルマイグレーションが利用できる)
ext["flyway.version"] = "4.2.0"
マイグレーションファイル¶
下記のような内容のマイグレーションファイルR__1_create_tables.sql
をsrc/main/resources/db/migration
に配置する。
R
で始まるファイルは、リピータブルマイグレーションが実施される。
CREATE TABLE IF NOT EXISTS users(
user_id INT(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ユーザID'
, first_name VARCHAR(40) NOT NULL COMMENT '名前'
, last_name VARCHAR(40) NOT NULL COMMENT '苗字'
, email VARCHAR(100) DEFAULT NULL COMMENT 'メールアドレス'
, password VARCHAR(100) DEFAULT NULL COMMENT 'パスワード'
, tel VARCHAR(20) DEFAULT NULL COMMENT '電話番号'
, zip VARCHAR(20) DEFAULT NULL COMMENT '郵便番号'
, address VARCHAR(100) DEFAULT NULL COMMENT '住所'
, upload_file_id INT(11) unsigned DEFAULT NULL COMMENT '添付ファイル'
, password_reset_token VARCHAR(50) DEFAULT NULL COMMENT 'パスワードリセットトークン'
, token_expires_at DATETIME DEFAULT NULL COMMENT 'トークン失効日'
, created_by VARCHAR(50) NOT NULL COMMENT '登録者'
, created_at DATETIME NOT NULL COMMENT '登録日時'
, updated_by VARCHAR(50) DEFAULT NULL COMMENT '更新者'
, updated_at DATETIME DEFAULT NULL COMMENT '更新日時'
, deleted_by VARCHAR(50) DEFAULT NULL COMMENT '削除者'
, deleted_at DATETIME DEFAULT NULL COMMENT '削除日時'
, version INT(11) unsigned NOT NULL DEFAULT 1 COMMENT '改訂番号'
, PRIMARY KEY (user_id)
, KEY idx_users (email, deleted_at)
) COMMENT='ユーザー';
flywayの設定¶
サンプルでは開発環境のみでの利用を想定する。 下記の設定を指定するとアプリケーションの起動時にFlywayのマイグレーションが実施される。
spring.flyway.enable=true
コード生成プラグイン(おまけ)¶
プラグインの設定¶
以下のような設定をbuild.gradle
に記述する。
各プロパティは、生成するソースのテンプレートで使用されているので、
出力したい内容に合わせて変更すること。
apply plugin: com.sample.CodeGenPlugin
codegen {
domainProjectName = "sample-domain" // ドメインのプロジェクト名
webProjectName = "sample-web-admin" // メインプロジェクト(SpringBootのメインメソッドを実装している)
commonDtoPackageName = "com.sample.domain.dto.common" // IDなどの共通的なエンティティのパッケージ名
daoPackageName = "com.sample.domain.dao" // Daoを配置するパッケージ名
dtoPackageName = "com.sample.domain.dto" // Dtoを配置するパッケージ名
servicePackageName = "com.sample.domain.service" // サービスを配置するパッケージ名
commonServicePackageName = "com.sample.domain.service" // サービスの基底クラスを配置するパッケージ名
exceptionPackageName = "com.sample.domain.exception" // 例外クラスを配置するパッケージ名
webBasePackageName = "com.sample.web.base" // コントローラー関連の基底クラスを配置するパッケージ名
baseValidatorPackageName = com.sample.domain.validator // バリデーターを配置するパッケージ名
baseControllerPackageName = "com.sample.web.base.controller.html" // コントローラーの基底クラスを配置するパッケージ名
controllerPackageName = "com.sample.web.admin.controller.html" // 生成するコントローラのパッケージ名
}
プラグインの実行¶
下記のコマンドを実行すると最低限のソースファイルが生成される。
$ cd /path/to/sample-web-admin
$ gradlew codegen -PsubSystem=system -Pfunc=employee -PfuncStr=従業員
生成ファイル一覧¶
以下の階層で、それぞれのソースファイルが作成される。
.
├── sample-domain
│ └── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── sample
│ │ └── domain
│ │ ├── dao
│ │ │ └── system
│ │ │ └── EmployeeDao.java
│ │ ├── dto
│ │ │ └── system
│ │ │ └── Employee.java
│ │ └── service
│ │ └── system
│ │ └── EmployeeDervice.java
│ └── resources
│ └── META-INF
│ └── com
│ └── sample
│ └── domain
│ └── dao
│ └── EmployeeDao
│ ├── select.sql
│ ├── selectAll.sql
│ └── selectById.sql
└── sample-web-admin
└── src
└── main
├── java
│ └── com
│ └── sample
│ └── web
│ └── admin
│ └── controller
│ └── html
│ └── system
│ └── employees
│ ├── EmployeeCsv.java
│ ├── EmployeeForm.java
│ ├── EmployeeFormValidator.java
│ ├── EmployeeHtmlController.java
│ └── SearchEmployeeForm.java
└── resources
└── templates
└── modules
└── system
└── employees
├── find.html
├── new.html
└── show.html
注釈
最小限のソースのみを生成するようになっているので、 必要に応じて生成元のテンプレートを編集したり、プラグイン自体を改修しする。 テンプレートは、buildSrc/src/resources/templatesに格納されている。