[Spring Boot2.4.2] API取得

pom.xml

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

WebApiController.java

package com.example.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.apache.commons.text.StringEscapeUtils;


@RestController
@RequestMapping("/api")
public class WebApiController {
	
	@RequestMapping(value="weather/tokyo"
			, produces=MediaType.APPLICATION_JSON_VALUE
			, method=RequestMethod.GET)
	private String call() {
		RestTemplate rest = new RestTemplate();
		
		final String cityCode = "130010";
		final String endpoint = "http://weather.livedoor.com/forecast/webservice/json/v1";
	    
		final String url = endpoint + "?city=" + cityCode;
		
		ResponseEntity<String> response = rest.getForEntity(url, String.class);
		
		String json = response.getBody();
		
		return decode(json);
	}
	
	private static String decode(String string) {
		return StringEscapeUtils.unescapeJava(string);
	}
	
}

うお、Jsonとして機能してないが、やりたいことやpom.xmlの使い方、importの意味などはわかってきた。
早くデータアクセスに行きたい。

[Spring Boot2.4.2] 戻り値

String以外の戻り値にしてみる
java beanはデータを出し入れする倉庫

@RestController
@RequestMapping("/api")
public class WebApiController {
	private static final Logger log = LoggerFactory.getLogger(WebApiController.class);
	
	
	public static class HogeMogeBean {
		public HogeMogeBean(String string, int i) {
			// TODO Auto-generated constructor stub
		}
    }

    @RequestMapping("hogemoge")
    public HogeMogeBean hogemoge() {
        return new HogeMogeBean( "ほげ", 1234 );
    }
}

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Sun Jan 31 19:21:10 JST 2021
There was an unexpected error (type=Internal Server Error, status=500).

$ curl localhost:8080/api/hogemoge
{“timestamp”:”2021-01-31T10:23:31.543+00:00″,”status”:500,”error”:”Internal Server Error”,”message”:””,”path”:”/api/hogemoge”}

なんやこれは。。
いきなりよくわからん。

[Spring Boot2.4.2] 新しいプロジェクトを作る

まず、Spring Starter Projectでプロジェクトを作ったら、packageを作ります。
com.example.demo.controller

WebApiController.java

package com.example.demo.controller;

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

@RestController
@RequestMapping("api")
public class WebApiController {
	
	@RequestMapping("hello")
	private String hello() {
		return "SpringBoot!";
	}
}

あれ、さっきgradleで作った時はRestControllerではなくRequestMethodだったけど、今回はRestControllerか。。ん、requestMappingってパスの事か。

application.properties

server.port=8080

Run As -> SpringBoot app
http://localhost:8080/api/hello

ほう、なるほど

パスパラメータ
-> requestMappingで仕込んで使用する

@RequestMapping("/hello/{param}")
	private String hello(@PathVariable("hoge") String param) {
		return "SpringBoot!";
	}
package com.example.demo.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/sample/api")
public class WebApiController {
	private static final Logger log = LoggerFactory.getLogger(WebApiController.class);
	
	@RequestMapping("/test/{param}")
	private String testPathVariable(@PathVariable String param) {
		log.info(param);
		return "受け取ったパラメータ:" + param;
	}
	
	@RequestMapping("/test")
	private String testRequestParam(@RequestParam() String param) {
		log.info(param);
		return "受け取ったパラメータ:" + param;
	}
	
	@RequestMapping(value = "/test", method = RequestMethod.POST)
	private String testRequestBody(@RequestBody String body) {
		log.info(body);
		return "受け取ったbody:" + body;
	}
}

$ lsof -i:8080
$ kill hoge
Run As -> SpringBoot app
http://localhost:8080/sample/api/test/firstparam

なにこれ? GetParameter実装するの凄い簡単なのに、こんなにコード書かなきゃいけないの。。。

[Spring Boot] HelloWorldから始める

New Spring Starter Project

次の画面で、TymeleafとWebを選択

# MarvenとGradle
### Marven
– POM (Project Od Model) の考え方に基づく。
– ビルドファイルは、pom.xml
– プラグインによる拡張が可能

### Gradle
– AndroidStudioデフォルト
– 依存関係はGroovyに書いている

Run As -> SpringBoot app
. ____ _ __ _ _
/\\ / ___’_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | ‘_ | ‘_| | ‘_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
‘ |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.4.2)

### HTMLの作成
hello-world/src/main/resources/templates
index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h1>Hello SpringBoot Sample</h1>
</body>
</html>

### Controllerの作成

HelloController.java

package com.example.demo;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class HelloController {
	@RequestMapping(value = "/", method = RequestMethod.GET)
	public String index(Model model) {
		return "index";
	}
}

Run As -> SpringBoot app

やべえ、なんか出来てる….

[Laravel8.x] Mailgun + お名前.com + Route53 のメール送信設定方法

Laravel8.x + Mailgun + お名前.com + Route53

まず、Mailgunでカスタムドメインを設定する必要がある
そうすると、DNSに、TXT、MX、CNAMEを登録しろ、と出てくる

なるほど、これをお名前.comに登録すれば良いのね、ということで、お名前.comのDNS設定で追加して、Verify DNS Settingsを押しても一向にverifyされない。。。

何故だ? 24時間くらい待った方が良いの? 常識的にそんな訳ないよね。。お名前に問い合わせしようかな。。と考えていたが、
TXT、MX、CNAMは、お名前側ではなく、Route53のHosted zonesのcreate recordで設定して上手くいった。
設定内容は、record nameと”Enter This Value”をvalueに入れていく。

mxレコードの場合は、valueに”10 mxa.mailgun.org”と入れる。

これで、再度Verify DNS Settingsを押すと、verifyされる。

で、設定したドメインのSMTP credentialのページに行き、ログインの箇所とReset Passwordでパスワードを取得して、この内容をlaravelのenvに記載する

laravel .env
L maildriverはsmtpのままで大丈夫

MAIL_MAILER=smtp
MAIL_HOST=smtp.mailgun.org
MAIL_PORT=587
MAIL_USERNAME=postmaster@${domain name}
MAIL_PASSWORD=${domain password}
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME=""

これでOK
送信テストを行う

まあ、Amazon SESの申請が降りたから、mailgun使わなくて良いんだけど、SESの申請が落ちた時はこちらを使う。
取り敢えず、MailgunのTXT、MX、CNAMEはお名前.comではなく、Route53側で設定するということ。

mailstrapで開発してて、さあSTG、商用環境にデプロイしよか、って時にメール送信できないとか、恐怖でしかないわ、ホンマに。

[AWS SES] Simple Email Serviceで申請が落ちた場合の対処法

アプリケーション開発において、メール送信機能は必須であり、それが出来ないとなると、根幹を大きく揺るぎかねない。

メールリレーとして比較すると、mailgun, ses, sendgrid, ベアメールなど色々ある。
が、AWSで開発するならば、親和性が良いので、SESを使いたい。

Route53で登録したドメインからメール送信できるようSESで設定していくのだが、
初期設定では、sandboxが設定されており、送信先が制限されている。

その為、送信制限解除の申請を行う。
こちらのブログが参考になる。
https://www.grandream.jp/blog/aws-ses/

で、落ちることはないだろうとたかを括っていたら、、、 6時間後くらいに、、、、

Thank you for submitting your request to increase your sending limits for your Amazon SES account in the Asia Pacific (Tokyo) region. We are unable to grant your request at this time.

An account that is related to your current account is paused in the US East (N. Virginia) region.

嘘やろ。。。マジかよ。。。
頭真っ白になった。。。

取り敢えず以下の解答を書いて、一先ず寝た。

If you believe we arrived at this decision in error, please respond to this case and provide the following information:

-- What is the nature of your business, and how do you plan to use Amazon SES to meet the needs of your business?

-- How do you collect the email addresses that are on your mailing list?

-- How do your subscribe and unsubscribe processes work? Include links to your opt-in and opt-out pages.

-- How do you handle bounces and complaints?

-- What type of email (for example, transactional notifications, marketing content, or system notifications) do you plan to send with Amazon SES?

-- What is the URL of your website?

-- Is there any other information that would help us better understand your use case?

SESは、申請が通らないことがあるから、代替手段を考えておいた方が良い。
ちなみに私はMailgunでもテストしていたので、急遽、Mailgunで実装するテストを開始した。

で、更に4時間後くらいに、、、

平素は Amazon Web Services をご利用いただき、誠にありがとうございます。

このたびは、送信制限の引き上げ申請をご送信いただき、ありがとうございます。新たな送信クォータは、1 日あたり 50,000 メッセージとなります。新たな最大送信レートは、毎秒 14 メッセージです。また、お客様のアカウントを Amazon SES サンドボックスから移動いたしました。

この引き上げは、アジアパシフィック (東京)リージョンにおいて、直ちに有効になります。Amazon SES コンソールの送信統計情報のページで、または GetSendQuota API を使って、お客様のアカウントの現在の送信レートと送信クォータを確認することができます。
// 省略

オイオイオイ、通るんかよ。

### 教訓(重要)
– SESは申請が通らないことがあるので、SESが駄目だった時のために、Mailgunなどで準備をしておいた方が絶対に良い(精神的にも)
– SESは英語で申請すると、中国人などが対応するので、日本語で申請した方が良い
– 一度申請が落ちても、きちんと再度回答すると、申請が通る

結果オーライだけど、全くひどい週末になったよ。

### AWSで売れ筋の本です

[Spring Boot][java] ログイン機能

Spring Securityを使用する
pom.xml

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.thymeleaf.extras</groupId>
			<artifactId>thymeleaf-extras-springsecurity4</artifactId>
		</dependency>

ユーザ取得処理の実装
ReservationUserDetails.java

package mrs.domain.service.user;

import java.util.Collection;
import mrs.domain.model.User;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.secuirty.core.authority.AuthorityUtils;
import org.springframework.secuirty.core.userdetails.UserDetails;

public class ReservationUserDetails implements UserDetails {
	private final User user;
	
	public ReservationUserDetails(User user) {
		this.user = user;
	}
	
	public User getUser() {
		return user;
	}
	
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities(){
		return AuthorityUtils.createAuthorityList("ROLE_" + this.user.getRoleName().name());
	}
	
	@Override
	public String getPassword() {
		return this.user.getPassword();
	}
	
	@Override
	public String getUsername() {
		return this.user.getUserId();
	}
	
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}
	
	@Override
	public boolean isAccountNonLocked() {
		return true;
	}
	
	@Override
	public boolean isCredentialsExpired() {
		return true;
	}
	
	@Override
	public boolean isEnabled() {
		return true;
	}
}

ん、認証って、一つ一つ書くんか。

UserRepository.java

package mrs.domain.repository.user;

import org.springframework.data.jpa.repository.JpaRepository;

import mrs.domain.model.User;

public interface UserRepository extends JpaRepository<User, String>{
	
}

ReservationUserDetailsService.java

package mrs.domain.service.user;

import mrs.domain.model.User;
import mrs.domain.repository.user.UserRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class ReservationUserDetailsService implements UserDetailsService {
	@Autowired
	UserRepository userRepository;
	
	@Override
	public UserDetails loadUserByUsername(String username)
		throws UsernameNotFoundException {
		User user = userRepository.findOne(username);
		if(user == null) {
			throw new UsernameNotFoundException(username + " is not found.");
		}
		return new ReservationUserDetails(user);
	}
}

LoginController.java

package mrs.app.login;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class LoginController {
	@RequestMapping("loginForm")
	String loginForm() {
		return "login/loginForm";
	}
}

プロジェクトルートで ./mvnw package で.jarファイルを作成する
うむ、何がわかってないかがわかってない状態だ。

[設計] マルチテナントアーキテクチャー

AWSはvpcが5つまでしか使わないので、複数のクライアントと契約する場合、どうするのかずっと気になっていた。あと、freee, money forwardみたいに、沢山のユーザがいる場合はどう設計しているのか?

マルチテナントアーキテクチャーで設計するらしい。
1. クライアントごとにデータベースを分ける
2. クライアントごとにidを振って、同じデータベースで管理する
3. ハードウェアのみ共有する

軽いアプリケーションなら、2の同じデータベースで良さそうだが、大きくスケールすることを考えたら、2のクライアントごとにデータベースを別ける方法か。。

うーむ、ちょっと時間かけて勉強したいテーマやないか。

[Spring Boot][java] app開発4

ReservationsController.java

@RequestMapping(method = RequestMethod.POST)
	String reserve(@Validated ReservationForm form, BindingResult bindingResult,
			@DateTimeFormat(iso=DateTimeFormat.ISO.DATE) @PathVariable("date") LocalDate date,
			@PathVariable("roomId") Integer roomId, Model model) {
		if(bindingResult.hasErrors()) {
			return reserveForm(date, roomId, model);
		}
		
		ReservableRoom reservableRoom = new ReservableRoom(
				new ReservableRoomId(roomId, date));
		Reservation reservation = new Reservation();
		reservation.setStarttime(form.getStartTime());
		reservation.setEndTime(form.getEndTime());
		reservation.setReservableRoom(reservableRoom);
		reservation.setUser(dummyUser());
		
		try {
			reservationService.reserve(reservation);
		}
		catch (UnavailableReservationException | AlreadyReservedException e) {
			model.addAttribute("error", e.getMessage());
			return reserveForm(date, roomId, model);
		}
		return "redirect:/reservations/{date}/{roomId}";
	}
	
	@RequestMapping(method = RequestMethod.POST, params = "cancel")
	String cancel(@RequestParam("reservationId") Integer reservationId,
			@PathVariable("roomId") Integer roomId,
			@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @PathVariable("date") LocalDate date,
			Model model) {
		User user = dummyUser();
		try {
			reservationService.cancel(reservationId, user);
		}
		catch (IllegalStateException e) {
			model.addAttribute("error", e.getMessage());
			return reserveForm(date, roomId, model);
		}
		return "redirect:/reservations/{date}/{roomId}";
	}

ThirtyMinutesUnit.java

package mrs.app.reservation;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.*;

import javax.validation.*;

@Documented
@Constraint(validatedBy = {ThirtyMinutesUnitValidator.class})
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RINTIME)
public @interface ThirtyMinutesUnit {
	String message() default "{mrs.app.reservation.ThirtyMinutesUnit.message}";
	
	Class<?>[]groups() default {};
	
	Class<? extends Playload>[]playload() default {};
	
	@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
	@Retention(RUNTIME)
	@Documented
	public @interface List {
		ThirtyMinutesUnit[]value();
	}
}

ThirtyMinutesUnitValidator.java

package mrs.app.reservation;

import java.time.LocalTime;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class ThirtyMinutesUnitValidator
	implements ConstraintValidator<ThirtyMinutesUnit, LocalTime>{
	@Override
	public void initialize(ThirtyMinutesUnit constraintAnnotation) {
		
	}
	
	@Override
	public boolean isValid(LocalTime value, ConstraintValidatorContext context) {
		if(value == null) {
			return true;
		}
		return value.getMinute() % 30 == 0;
	}
}

EndTimeMuustBeAfterStartTime.java

package mrs.app.reservation;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.*;

import javax.validation.*;

@Documented
@Constraint(validatedBy = {EndTimeMustBeAfterStartTimeValidator.class})
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
public @interface EntTimeMustBeAfterStartTime{
	String message() default "{mrs.app.reservation.EndTimeMustBeAfterStartTime.message}";
	
	Class<?>[]groups() default{};
	
	Class<? extends Playload>[]payload() default {};
	
	@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
	@Retention(RUNTIME)
	@Documented
	public @interface List {
		EndTimeMustBeAfterStartTime[]value();
	}
}

EndTimeMustBeAfterStartTimeValidator.java

package mrs.app.reservation;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class EndTimeMustBeAfterStartTimeValidator
	implements ConstraintValidator<EndTimeMustBeAfterStartTime, ReservationForm>{
	private String message;
	
	@Override
	public void initialize(EndTimeMustBeAfterStartTime constraintAnnotation) {
		message = constraintAnnotation.message();
	}
	
	@Override
	public boolean isValid(ReservationForm value, ConstraintValidatorContext context) {
		if(value.getStarttime() == null || value.getEndTime() == null) {
			return true;
		}
		boolean isEndTimeMustBeAfterStartTime = value.getEndTime().isAfter(value.getStartTime());
		if(!isEndTimeMustBeAfterStartTime) {
			context.disableDefaultConstraintViolation();
			context.buildConstraintViolationWithTemplate(message).addPropertyNode("endTime").addConstraintViolation();
		}
		return isEndTimeMustBeAfterStartTime;
	}
}

うーむ、ちょっと混乱してきたな。

[Spring Boot][java] app開発3

SpringBootのリポジトリとは?
-> DBへのアクセスを自動で作成する便利インターフェース
-> DBへのCRUDを自動で実装してくれる
なるほど

Listとは?
-> Javaのリスト(List)とは、重複した要素を含むことができる順序の付けられたコレクション
-> 配列とは異なる

ReservationRepository.java

package mrs.domain.repository.reservation;

import java.util.List;

import mrs.domain.model.ReservableRoomId;
import mrs.domain.model.Reservation;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ReservationRepository extends JpaRepository<Reservation, Integer>{
	List<Reservation> findByReservableRoom_ReservableRoomIdOrderByStartTimeAsc(
			ReservableRoomId reservableRoomId);
}

MeetingRoomRepository.java

package mrs.domain.repository.room;

import org.springframework.data.jpa.repository.JpaRepository;

import mrs.domain.model.MeetingRoom;

public interface MeetingRoomRepository extends JpaRepository<MeetingRoom, Integer>{
	
}

Serviceとは?
-> ビジネスロジックを実装するクラス
-> Controllerはリクエストの窓口になるクラス
ん、ビジネスロジックはcontrollerではなく、serviceで書くのか。ちょっと混乱するな。

ReservationService.java

package mrs.domain.service.reservation;

import java.util.List;

import mrs.domain.model.*;
import mrs.domain.repository.reservation.ReservationRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class ReservationService {
	@Autowired
	ReservationRepository reservationRepository;
	
	public List<Reservation> findReservations(ReservableRoomId reservableRoomId){
		return reservationRepository.findByReservableRoom_ReservableRoomIdOrderByStartTimeAsc(
				reservableRoomId);
	}
}

Serviceの中のpublic List<*>がJavaの構文でないような印象だが、どういう事なんだ?

package mrs.domain.service.reservation;

import java.util.List;

import mrs.domain.model.*;
import mrs.domain.repository.reservation.ReservationRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class ReservationService {
	@Autowired
	ReservationRepository reservationRepository;
	@Autowired
	ReservableRoomRepository reservableRoomRepository;
	
	public Reservation reserve(Reservation reservation) {
		ReservableroomId reservableRoomId = reservation.getReservableRoom().getReservableRoomId();
		ReservableRoom reservable = reservableRoomRepository.findOne(reservableRoomId);
		if(reservable == null) {
			
		}
		boolean overlap = reservationRepository.findByReservableRoom_ReservableRoomIdOrderByStartTimeAsc(reservableRoomId)
				.stream()
				.anyMatch(x -> x.overlap(reservation));
		if(overlap) {
			
		}
		reservationRepository.save(reservation);
		return reservation;
	}

ReservationsController.java

package mrs.app.reservation;

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import mrs.domain.model.*;
import mrs.domain.service.reservation.*;
import mrs.domain.service.room.RoomService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@Controller
@RequestMapping("reservations/{date}/{roomId}")
public class ReservationsController {
	@Autowired
	RoomService roomService;
	@Autowired
	ReservationService reservationService;
	
	@ModelAttribute
	ReservationForm setUpForm() {
		RservationForm form = new ReservationForm();
		form.setStartTime(LocalTime.of(9, 0));
		form.setEndTime(LocalTime.of(10, 0));
		return form;
	}
	
	@RequestMapping(method = RequestMethod.GET)
	String reserveForm(@DateTimeFormat(iso=DateTimeFormat.ISO.DATE) @PathVariable("date") LocalDate date,
			@PathVariable("roomId") Integer roomId, Model model) {
		ReservableRoomId reservableRoomId = new ReservableRoomId(roomId, date);
		List<Reservation> reservations = reservationService.findReservations(reservableRoomId);
		
		List<LocalTime> timeList =
				Stream.iterate(LocalTime.of(0, 0), t -> t.plusMinutes(30))
				.limit(24 * 2)
				.collect(Collectors.toList());
		
		model.addAttribute("room", roomService.findMeetingRoom(roomId));
		model.addAttribute("reservations", reservations);
		model.addAttribute("timeList", timeList);
		model.addAttribute("user", dummyUser());
		return "reservation/reserveForm";
	}
	
	private User dummyUser() {
		User user = new User();
		user.setUserId("taro-yamada");
		user.setFirstName("taro");
		user.setLastName("Yamada");
		user.setRoleName(RoleName.USER);
		return user;
	}
}
package mrs.app.reservation;

import java.io.Serializable;
import java.time.LocalTime;

import javax.validation.constraints.NotNull;

import org.springframework.format.annotation.DateTimeFormat;

public class ReservationForm implements Serializable {
	@NotNull(message = "必須です")
	@DateTimeFormat(pattern = "HH:mm")
	private LocalTime startTime;
	
	@NotNull(message = "必須です")
	@DateTimeFormat(pattern = "HH:mm")
	private Localtime endTime;
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title th:text="|${#temporals.format(date, 'yyyy/M/d')}の${room.roomName}|">2021/01/29の豊洲</title>
</head>
<body>

<div>
<a th:href="@{'/rooms/' + ${date}">会議室一覧へ</a>
</div>

<form th:object="${reservationForm}"
	th:action="@{'/reservations/' + ${date} + '/' + ${roomId}}" method="post">
	会議室: <span th:text="${room.roomName}">銀座</span>
	<br>
	予約者名: <span th:text="${user.lastName + ' ' + user.firstName}">山田太郎</span>
	// 省略

うーむ、全体的な流れを掴むには良いが、もう少し入門的なところからやりたい。。