JavaにはMVCモデルという考え方があります。
その中のCがControllerに当たります。
今回はControllerとは何かを、サンプルコードを交えてご紹介していきます。
詳細説明についても、サンプルコードにコメントに載せています。
また今回のサンプルについては、以下のGitHubにも載せています。
合わせてご覧ください。
Controllerとは?
Controllerとは、プログラムの処理の命令をするところです。
司令塔のようなものです。
主な流れとして、
Controlller → Service → Repository
というように繋がっていきます。
Controllerが指令を出し、その指令によってServiceが処理をし、
DBにまつわるところはRepositoryへお願いする。
そして処理結果をControllerへ返し、View(MVCのVの部分)へ渡して、
適切な結果を画面表示すると言った流れです。
このように処理に役割を持たせることで、可読性が増し、
修正もしやすくなっていきます。
では、実際にサンプルコードを見ていきましょう。
環境・定義
build.gradle
plugins {
id 'java'
id 'war'
id 'org.springframework.boot' version '3.3.0'
id 'io.spring.dependency-management' version '1.1.5'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
//コンソールに取得データや発行したSQLを表示するために使用
runtimeOnly 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4:1.16'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
//JUnit
testImplementation 'org.junit.jupiter:junit-jupiter:5.5.2'
}
tasks.named('test') {
useJUnitPlatform()
}
application.properties
spring.application.name=ControllerSample
#DBの接続方法やコンソールに発行したSQLや取得したデータを表示するための設定
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
#MySQLの設定
spring.datasource.url=jdbc:log4jdbc:mysql://localhost/ENTITY_TEST
spring.datasource.username=root
spring.datasource.password=パスワード(独自で設定)
#springDataJPA設定
spring.jpa.hibernate.ddl-auto=validate
DBはMySQLを使用します。
CREATE_DATABASE_ENTITY_TEST.sql
CREATE DATABASE ENTITY_TEST;
CREATE_TABLE_ENTITY_TEST_TABLE.sql
CREATE TABLE ENTITY_TEST_TABLE
(id INT NOT NULL
,full_name VARCHAR(255) NULL
,insert_date INT NULL
,PRIMARY KEY (id));
INSERT_DATA
INSERT INTO ENTITY_TEST_TABLE (id, full_name ,insert_date) VALUES (1, 'エンティティ ダンプティ', 20240418);
INSERT INTO ENTITY_TEST_TABLE (id, full_name ,insert_date) VALUES (2, 'エンティティ ティティ', 20240418);
INSERT INTO ENTITY_TEST_TABLE (id, full_name ,insert_date) VALUES (3, 'エンティティ えん', 20240418);
INSERT INTO ENTITY_TEST_TABLE (id, full_name ,insert_date) VALUES (4, 'エンティティ ダ ヴィンチ', 20240418);
ファイル階層
- /
- src/
- main/
- java/
- com/example/demo/
- controller/
- Controller.java
- entity/
- Entity.java
- form/
- Form.java
- repository/
- Repository.java
- service/
- AbstractService.java
- IService.java
- extend/
- Service.java
- util/
- Constants.java
- ControllerSampleApplication.java
- ServletInitializer.java
- controller/
- com/example/demo/
- resources/
- templates
- search.html
- static/
- css/
- search.css
- css/
- application.properties
- messages.properties
- CREATE_DATABASE_ENTITY_TEST.sql
- CREATE_TABLE_ENTITY_TEST_TABLE.sql
- INSERT_DATA.sql
- templates
- java/
- test/
- java/
- …
- java/
- main/
- build.gradle
- settings.gradle
- …
- src/
サンプルコード
Search.html、Search.cssと表示画面
コード
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="${title}"></title>
<link rel="stylesheet" th:href="@{/css/search.css}">
</head>
<body>
<div th:unless="${#strings.isEmpty(errorMessage)}">
<div th:text="${errorMessage}" style="color:red"></div>
</div>
<div class="input-container">
<form method="post" th:action="@{search}" th:object="${form}">
<label for="full_name">ユーザー名</label>
<input th:field="*{full_name}" type="text"></input>
<button type="submit">検索</button>
</form>
</div>
<div th:unless="${#lists.isEmpty(formList)}">
<table>
<thead>
<tr>
<th>ユーザー名</th>
<th>挿入日</th>
</tr>
</thead>
<tbody>
<tr th:each="data : ${formList}">
<td th:text="${data.fullName}"></td>
<td th:text="${data.insert_date}"></td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
@charset "UTF-8";
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f4f4f9;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
font-size: 18px;
text-align: left;
}
table th,
table td {
padding: 12px 15px;
}
table thead tr {
background-color: #009879;
color: #ffffff;
text-align: left;
font-weight: bold;
}
table tbody tr {
border-bottom: 1px solid #dddddd;
}
table tbody tr:nth-of-type(even) {
background-color: #f3f3f3;
}
table tbody tr:last-of-type {
border-bottom: 2px solid #009879;
}
table tbody tr:hover {
background-color: #f1f1f1;
cursor: pointer;
}
.input-container {
display: flex;
align-items: center;
background-color: #ffffff;
padding: 10px 20px;
border-radius: 25px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.input-container form {
width: 100%;
}
.input-container input {
width: 50%;
border:1px solid #dddddd;
outline: none;
padding: 10px;
font-size: 16px;
border-radius: 20px;
margin-right: 10px;
flex: 1;
}
.input-container button {
background-color: #009879;
color: white;
border: none;
padding: 10px 20px;
font-size: 16px;
border-radius: 20px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.input-container button:hover {
background-color: #007b5e;
}
画面
Serviceクラスでは、データの編集や追加などのメソッドがありますが、
今回は検索のみの画面を作成しています。
検索前
検索後
messages.properties
valid.pattern.id=idは数値のみ入力可能です
valid.pattern.fullName=検索する名前を入力してください
valid.pattern.insertData=insert_dateは数値のみ入力可能です
valid.empty.dataList=データがありません
valid.not.exist.data=idが{0}のデータは存在しません
valid.exist.data=idが{0}のデータは既に存在します
defaultErrorMessage=エラーが発生しました
メッセージ管理のファイルです。
Spring Bootでは、このファイル名でメッセージ管理しておけば、
MessageSourceというインターフェースを用いることで、メッセージを取り出すことができます。
MessageSourceについては、AbstractService.javaで使用している部分がございますので、
使用方法はそちらをご覧ください。
Controller.java
いきなりControllerのサンプルをご覧いただきます。
ここから遡る形でサンプルを見ていきましょう。
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.example.demo.form.Form;
import com.example.demo.form.Form.Search;
import com.example.demo.service.extend.Service;
/**
* <pre>
* {@link org.springframework.stereotype.Controller Controller}アノテーションを付与することで、
* DIコンテナにコントローラーとして登録される。
* {@link RequestMapping}アノテーションを付与することで、
* どのパスでこのコントローラーが動作するのかを指定する。
* 今回は、パスの指定がないため、ローカル軌道で、http://localhost:8080/ でこのコントローラーが動作する
* パスの指定をする場合は、{@code @RequestMapping("パス")}のような書き方をして指定する。
* すると、http://localhost:8080/パス でこのコントローラーを通る
* </pre>
* @author Takumi
*
*/
@org.springframework.stereotype.Controller
@RequestMapping
public class Controller {
/** DIコンテナに登録してある{@link Service}クラスを取り出す */
@Autowired
private Service service;
/**
* <pre>
* Getでリクエストが来た際にこのメソッドを通る。
* 今回は{@link GetMapping}にパスの指定がないため、
* 全体に付与されている{@link RequestMapping}のパスと合わせて、
* http://localhost:8080/ でgetリクエストが来た際にこのメソッドを通る。
* </pre>
* @param model viewからもらう、viewへ渡す情報のようなもの
* @param form modelの中でも、formというkey名がつけられているmodel
* @return 表示したいhtmlのファイルパス(拡張子抜きで相対パスで指定) 今回は」search.htmlを表示
*/
@GetMapping
public String init(ModelMap model, @ModelAttribute(Service.FORM) Form form) {
service.setCommonModel(model);
return "search";
}
/**
* <pre>
* パス http://localhost:8080/search でpostリクエストを受けると
* このメソッドを通る
* </pre>
* @param form {@link Validated}の引数にインターフェースを記載することでどのフィールドをバリデーションするのかを指定する
* @param error formのエラーをこのerrorで受けとる
* @param redirect メソッドへパスを指定して投げなおす際の情報を格納する
* @return redirect;を指定することで、そのあとのパスの指定があるコントローラーへメソッドを投げることができる。
* 今回は何も指定がないため、http;//localhost:8080/でリクエストを受けた際に通るメソッドへ投げる。
* このControllerの場合は{@link #init(ModelMap, Form)}へ繋げる
*/
@PostMapping("/search")
public String search(@Validated(Search.class) Form form, BindingResult error, RedirectAttributes redirect) {
if (!service.existError(error, redirect)) {
service.search(form, redirect);
}
return "redirect:";
}
}
Service.java
package com.example.demo.service.extend;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.thymeleaf.util.ListUtils;
import com.example.demo.entity.Entity;
import com.example.demo.form.Form;
import com.example.demo.repository.Repository;
import com.example.demo.service.AbstractService;
import com.example.demo.util.Constants;
@org.springframework.stereotype.Service
public class Service extends AbstractService<Entity> {
/** 検索の際のinputタグを格納 */
public static final String FORM = "form";
/** 検索結果を格納 */
public static final String FORM_LIST = "formList";
@Autowired
private Repository repository;
/** 処理対象のデータを格納 */
private List<Entity> entityList;
@Override
public void findAll() {
List<Entity> list = repository.findAll();
setDataList(list);
}
@Override
public Entity findById(int userId) {
Optional<Entity> dataOpt = repository.findById(Integer.valueOf(userId));
//データがない場合はnullを返す
return dataOpt.isPresent() ? dataOpt.get() : null;
}
@Override
public void findByUserName(String userName) {
List<Entity> list = repository.findByFullName(userName);
setDataList(list);
}
@Override
public void addUser(Form form) {
//データの潜在チェックをする。存在したらエラーメッセージ出す。
if (isExist(form)) {
System.err.println(getMessage(Constants.VALID_EXIST_DATA, new String[] {form.getId()}));
} else {
save(form);
setAddCount(getAddCount() + 1);
}
}
@Override
public void addUser(List<Form> formList) {
List<Form> existList = existList(formList);
//データの潜在チェックをする。存在したらエラーメッセージ出す。
if (existList.size() == 0) {
List<Entity> savedList = saveAll(formList);
setAddCount(getAddCount() + savedList.size());
} else {
isExistDatasErrorLog(existList, true);
}
}
@Override
public void deleteUser(int userId) {
//データの潜在チェックをする。存在しなかったらエラーメッセージ出す。
if (isExist(userId)) {
Integer id = Integer.valueOf(userId);
repository.deleteById(id);
setDeleteCount(getDeleteCount() + 1);
} else {
System.err.println(getMessage(Constants.VALID_NOT_EXIST_DATA, new Integer[] {userId}));
}
}
@Override
public void editUser(Form form) {
//データの潜在チェックをする。存在しなかったらエラーメッセージ出す。
if (isExist(form)) {
save(form);
setEditCount(getEditCount() + 1);
} else {
System.err.println(getMessage(Constants.VALID_EXIST_DATA, new String[] {form.getId()}));
}
}
@Override
public void editUser(List<Form> formList) {
//データの潜在チェックをする。存在しなかったらエラーメッセージ出す。
if (existList(formList).size() == 0) {
isExistDatasErrorLog(formList, false);
} else {
List<Entity> savedList = saveAll(formList);
setEditCount(getEditCount() + savedList.size());
}
}
/**
* 共通の{@link ModelMap}をセット
* @param model
*/
public void setCommonModel(ModelMap model) {
model.put(Constants.TITLE_KEY, Constants.SEARCH_TITLE);
}
/**
* エラーの存在をチェックする
* @param error
* @param redirect
* @return エラーがあればtrue
*/
public boolean existError(BindingResult error, RedirectAttributes redirect) {
if (error.hasFieldErrors()) {
StringBuilder errorMessage = new StringBuilder();
for (var errors : error.getAllErrors()) {
errorMessage.append(errors.getDefaultMessage());
}
redirect.addFlashAttribute(Constants.ERROR_MESSAGE, errorMessage.toString());
return true;
} else {
return false;
}
}
/**
* 検索ボタン押下後のメソッド
* @param form 検索の際のinputタグ内の値を格納
* @param redirect
*/
public void search(Form form, RedirectAttributes redirect) {
findByUserName(form.getFull_name());
if (ListUtils.isEmpty(dataList)) {
redirect.addFlashAttribute(Constants.ERROR_MESSAGE, getMessage(Constants.VALID_EMPTY_DATALIST, null));
} else {
redirect.addFlashAttribute(FORM_LIST, getDataList());
}
redirect.addFlashAttribute(FORM, form);
}
/**
* 存在する複数データのログを出力
* @param existList 存在する複数データ
* @param isExist 存在するログを出力する場合はtrue
*/
private void isExistDatasErrorLog(List<Form> existList, boolean isExist) {
String[] existIdArray = new String[existList.size()];
for (int i = 0; i < existList.size(); i++) {
existIdArray[i] = existList.get(i).getId();
}
String errorMessage = String.join(", ", existIdArray);
if (isExist) {
System.err.println(getMessage(Constants.VALID_EXIST_DATA, new String[] {errorMessage}));
} else {
System.err.println(getMessage(Constants.VALID_NOT_EXIST_DATA, new String[] {errorMessage}));
}
}
/**
* データの存在確認をする
* @param form 存在確認をするデータ
* @return 存在すればtrue
*/
private boolean isExist(Form form) {
Integer ID = Integer.parseInt(form.getId());
int id = ID.intValue();
return findById(id) != null;
}
/**
* データの存在確認をする
* @param userId 存在確認をするデータのid
* @return 存在すればtrue
*/
private boolean isExist(int userId) {
return findById(userId) != null;
}
/**
* 存在するデータのみを返す
* @param form 存在確認するデータの{@code List<Form}
* @return 存在したデータの{@code List<Form}
*/
private List<Form> existList(List<Form> form) {
List<Form> existList = new ArrayList<>();
for (Form data : form) {
Integer ID = Integer.parseInt(data.getId());
int id = ID.intValue();
Entity d = findById(id);
if (d != null) {
existList.add(data);
}
}
return existList;
}
/**
* {@link Form}クラスを{@link Entity}クラスへ変換する
* @param form
* @return 変換後の{@link Entity}クラス
*/
private Entity setEntity(Form form) {
Entity entity = new Entity();
entity.setId(Integer.parseInt(form.getId()));
entity.setFullName(form.getFull_name());
entity.setInsert_date(Integer.parseInt(form.getInsert_date()));
return entity;
}
/**
* INSERTもしくはUPDATEを実行
* @param form 更新、挿入したいデータ
* @return 更新、挿入したデータの{@link Entity}クラス
*/
private Entity save(Form form) {
Entity entity = setEntity(form);
return repository.save(entity);
}
/**
* 複数のINSERTもしくはUPDATEを実施
* @param formList 更新、挿入したいデータ
* @return 更新、挿入したいデータ
*/
private List<Entity> saveAll(List<Form> formList) {
setEntityListByFromList(formList);
return repository.saveAll(entityList);
}
/**
* {@link this#entityList}を{@link Form}クラスからセットする
* @param formList
*/
private void setEntityListByFromList(List<Form> formList) {
List<Entity> entityList = new ArrayList<>();
for (Form form : formList) {
entityList.add(setEntity(form));
}
setEntityList(entityList);
}
public List<Entity> getEntityList() {
return entityList;
}
public void setEntityList(List<Entity> entityList) {
this.entityList = entityList;
}
}
AbstractService.java
package com.example.demo.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import com.example.demo.util.Constants;
import lombok.Data;
/**
* <pre>
* サービスクラスの抽象クラス
* このクラスを継承してサービスクラスを作成
* </pre>
* @author Takumi
*
* @param <T> 作成するサービスクラスで使用するEntityクラスを渡す
*/
@Data
public abstract class AbstractService<T> implements IService {
/** 取得したデータ */
protected List<T> dataList = new ArrayList<T>();
/** 取得したデータ数 */
protected int dataListSize;
/** 追加したデータ数 */
protected int addCount;
/** 削除したデータ数 */
protected int deleteCount;
/** 編集したデータ数 */
protected int editCount;
/** messages.propertiesの文言取得 */
@Autowired
protected MessageSource messageSource;
/**
* messages.propertiesからメッセージ取得
* @param messageKey messages.propertiesのkey
* @param args 文言に与える引数
* @return メッセージ
*/
public String getMessage(String messageKey, Object[] args) {
//標準メッセージを設定
String defaultMessage = messageSource.getMessage(Constants.DEAULT_ERROR_MESSAGE, null, Locale.JAPANESE);
//第一引数にmessages.propertiesのkeyを、第二引数にそのメッセージに代入したい値を、第三引数に標準のメッセージをセット
return messageSource.getMessage(messageKey, args, defaultMessage, Locale.JAPANESE);
}
public int getDataListSize() {
return dataList.size();
}
}
IService.java
package com.example.demo.service;
import java.util.List;
import com.example.demo.entity.Entity;
import com.example.demo.form.Form;
/**
* サービスクラスのインターフェース
* @author Takumi
*/
public interface IService {
/**
* データをすべて取得
*/
void findAll();
/**
* 対象のIdのデータを取得
*/
Entity findById(int userId);
/**
* ユーザー名からデータを取得
* @param userName ユーザー名
*/
void findByUserName(String userName);
/**
* ユーザーを1件追加(INSERT)
* @param form
*/
void addUser(Form form);
/**
* ユーザーを追加
* @param formList 追加するユーザー
*/
void addUser(List<Form> formList);
/**
* ユーザーを削除
* @param userId 削除するユーザーのID
*/
void deleteUser(int userId);
/**
* データを1件編集(UPDATE)
* @param form 更新するユーザーのデータ
*/
void editUser(Form form);
/**
* データを編集(UPDATE)
* @param formList 編集するデータ
*/
void editUser(List<Form> formList);
}
Serviceについての詳細は、以下の記事をご覧ください。
Repository.java
package com.example.demo.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.demo.entity.Entity;
/**
* Spring Data JPA使用
* @author Takumi
*/
public interface Repository extends JpaRepository<Entity, Integer>{
public List<Entity> findByFullName(String fullName);
}
こちらSpring Data JPAの詳細については、以下の記事をご覧ください。
Entity.java
package com.example.demo.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@jakarta.persistence.Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "ENTITY_TEST_TABLE")
public class Entity {
@Id
private Integer id;
@Column(name = "full_name")
private String fullName;
private Integer insert_date;
}
Entityについての詳細は、以下の記事をご覧ください。
Form.java
package com.example.demo.form;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.validation.annotation.Validated;
import io.micrometer.common.util.StringUtils;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
/**
* <pre>
* 画面より入力された値を格納するクラス
* その際に、NULL許容などのバリデーションもかける
* Controllerクラすに{@link Validated}アノテーションを入れ、引数にインターフェースを入れることで、
* どのフィールドのバリデーションをするのかを指定する
* </pre>
*/
@Data
public class Form {
/** すべてのフィールドのバリデーションを行う */
public interface All{}
/** 検索の際のバリデーションを行う */
public interface Search{}
/** \\d によって数値以外の入力を制限している。 */
@NotEmpty
@Pattern(regexp = "\\d", message = "{valid.pattern.id}", groups = All.class)
private String id;
@NotEmpty(message = "{valid.pattern.fullName}", groups = {Search.class, All.class})
private String full_name;
@Pattern(regexp = "\\d", message = "{valid.pattern.insertData}", groups = {Search.class, All.class} )
private String insert_date;
/**
* 値がセットされていない場合は、
* 現在日時をyyyyMMdd形式で取得
* @return yyyyMMdd
*/
public String getInsert_date() {
if(!StringUtils.isBlank(this.insert_date)) {
return this.insert_date;
}
LocalDateTime nowDate = LocalDateTime.now();
DateTimeFormatter yyyyMMdd_formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
String yyyyMMdd = yyyyMMdd_formatter.format(nowDate);
return yyyyMMdd;
}
}
DTO(Data Transfer Object)に該当するクラスです。
このクラスでバリデーションしてある、
検索の際に出るエラー画面を実際に表示すると、
上記のようになります。
Constants.java
package com.example.demo.util;
/**
* 使用するkey情報などを格納
*/
public final class Constants {
public static final String TITLE_KEY = "title";
public static final String ERROR_MESSAGE = "errorMessage";
public static final String DEAULT_ERROR_MESSAGE = "defaultErrorMessage";
public static final String VALID_EMPTY_DATALIST = "valid.empty.dataList";
public static final String VALID_EXIST_DATA = "valid.exist.data";
public static final String VALID_NOT_EXIST_DATA = "valid.not.exist.data";
public static final String SEARCH_TITLE = "検索";
}