[Spring Boot2.4.2] .jarファイルにしてvagrantにデプロイしたい

STSで開発したプログラムをvagrantにデプロイしたい
-> Spring Boot Mavenプラグインを追加することで、実行可能なファイルとしてパッケージ化することができる。
-> pom.xmlに追加する

<packaging>jar</packaging>
// 省略
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

run as -> maven install

targetフォルダに demo-0.0.1-SNAPSHOT.jar が生成された

vagrantに.jarファイルを配置します

$ java -version
openjdk version “1.8.0_265”
OpenJDK Runtime Environment (build 1.8.0_265-b01)
OpenJDK 64-Bit Server VM (build 25.265-b01, mixed mode)

$ wget https://d3pxv6yz143wms.cloudfront.net/11.0.2.9.3/java-11-amazon-corretto-devel-11.0.2.9-3.x86_64.rpm
$ sudo yum localinstall java-11-amazon-corretto-devel-11.0.2.9-3.x86_64.rpm
$ java -version
openjdk version “11.0.2” 2019-01-15 LTS
OpenJDK Runtime Environment Corretto-11.0.2.9.3 (build 11.0.2+9-LTS)
OpenJDK 64-Bit Server VM Corretto-11.0.2.9.3 (build 11.0.2+9-LTS, mixed mode)
$ rm -rf java-11-amazon-corretto-devel-11.0.2.9-3.x86_64.rpm
$ java -jar demo-0.0.1-SNAPSHOT.jar

http://192.168.33.10:8080/view/what-time-is-it

うお、何これ?
サーバにJavaさえ入ってれば動くやん
Sugeeeeeeeeeeeeee
マジでビビった。

[Spring Boot2.4.2] PostgresSQLに連携したい

まず、postgres側でテーブルを作ってデータを入れます。

create table weather (
	id serial primary key,
	location_id int,
	name varchar(20),
	temperature int,
	humidity int,
	date_time timestamp
);

test=> insert into weather (location_id, name, temperature, humidity, date_time) values
(1, ‘東京’, 15, 55, ‘2021-02-02 09:00:00’),
(1, ‘東京’, 16, 53, ‘2021-02-02 10:00:00’),
(1, ‘東京’, 17, 40, ‘2021-02-02 11:00:00’),
(2, ‘那覇’, 20, 65, ‘2021-02-02 09:00:00’),
(2, ‘那覇’, 22, 67, ‘2021-02-02 10:00:00’),
(2, ‘那覇’, 25, 69, ‘2021-02-02 11:00:00’);
INSERT 0 6
test=> select * from weather;
id | location_id | name | temperature | humidity | date_time
—-+————-+——+————-+———-+———————
1 | 1 | 東京 | 15 | 55 | 2021-02-02 09:00:00
2 | 1 | 東京 | 16 | 53 | 2021-02-02 10:00:00
3 | 1 | 東京 | 17 | 40 | 2021-02-02 11:00:00
4 | 2 | 那覇 | 20 | 65 | 2021-02-02 09:00:00
5 | 2 | 那覇 | 22 | 67 | 2021-02-02 10:00:00
6 | 2 | 那覇 | 25 | 69 | 2021-02-02 11:00:00

application.properties

server.port=8080
spring.jpa.database=POSTGRESQL
spring.datasource.url=jdbc:postgresql://localhost:5432/test
spring.datasource.username=postgres
spring.datasource.password=

pom.xml

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<scope>runtime</scope>
		</dependency>

model
Weather.java

package com.example.demo.model;

import java.sql.Timestamp;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="weather")
public class Weather {
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	private Integer id;
	
	private Integer location_id;
	
	private String name;
	
	private Integer temperature;
	
	private Integer humidity;
	
	private Timestamp date_time;
	
	public Integer getId() {
		return id;
	}
	
	public void setId(Integer id) {
		this.id = id;
	}
	
	public Integer getLocation_id() {
		return location_id;
	}
	
	public void setLocation_id(Integer location_id) {
		this.location_id = location_id;
	}
	
	public String getName() {
		return name;
	}
	
	public void setName(String name) {
		this.name = name;
	}
	
	public Integer getTemperature() {
		return temperature;
	}
	
	public void setTemperature(Integer temperature) {
		this.temperature = temperature;
	}
	
	public Integer getHumidity() {
		return humidity;
	}
	
	public void setHumidity(Integer humidity) {
		this.humidity = humidity;
	}
	
	public Timestamp getDate_time() {
		return date_time;
	}
	
	public void setDate_time(Timestamp date_time) {
		this.date_time = date_time;
	}
}

repository/WeatherRepository.java

package com.example.demo.repository;

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

import com.example.demo.model.Weather;

@Repository
public interface WeatherRepository extends JpaRepository<Weather, Integer>{}

service/WeatherService.java

package com.example.demo.service;

import java.util.List;

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

import com.example.demo.model.Weather;
import com.example.demo.repository.WeatherRepository;

@Service
@Transactional
public class WeatherService {
	
	@Autowired
	WeatherRepository weatherRepository;
	
	public List<Weather> findAllWeatherData(){
		return weatherRepository.findAll();
	}
}

HelloController.java

package com.example.demo;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import com.example.demo.model.Weather;
import com.example.demo.service.WeatherService;

@Controller
public class HelloController {
	@Autowired
	WeatherService weatherService;
	
	@RequestMapping("/hello")
	public String hello(Model model) {
		
		model.addAttribute("hello", "Hello World!");
		
		List<Weather> weatherDataList = weatherService.findAllWeatherData();
		model.addAttribute("weatherDataList", weatherDataList);
		
		return "hello";
	}
}

hello.html

<!Doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>SpringBoot</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta charset="UTF-8">
<!-- <link th:href="@{/css/common.css}" rel="stylesheet"></link>
<script th:src="@{/js/common.js}"></script> -->
</head>
<body>
<p>
	<span th:text="${hello}">Hello World!</span>
</p>
<div>
	<table>
		<tr th:each="data: ${weatherDataList}" th:object="${data}">
			<td th:text="*{id}"></td>
			<td th:text="*{location_id}"></td>
			<td th:text="*{name}"></td>
			<td th:text="*{temperature}"></td>
			<td th:text="*{humidity}"></td>
			<td th:text="*{date_time}"></td>
		</tr>
	</table>
</div>
</body>
</html>

lsof -i:8080
Run As -> SpringBoot app

http://localhost:8080/hello

Hello World!

1 1 東京 15 55 2021-02-02 09:00:00.0
2 1 東京 16 53 2021-02-02 10:00:00.0
3 1 東京 17 40 2021-02-02 11:00:00.0
4 2 那覇 20 65 2021-02-02 09:00:00.0
5 2 那覇 22 67 2021-02-02 10:00:00.0
6 2 那覇 25 69 2021-02-02 11:00:00.0

うおおおおおおおおおおおおお
マジか。。。
これ、Javaでアプリ作れんじゃん。

[postgres13.1] 基本操作 CRUD

### create table
$ psql -l
$ psql -U root test
test=> create table mybook(
id integer,
name varchar(10)
);
CREATE TABLE

### テーブル一覧
test=> \d
List of relations
Schema | Name | Type | Owner
——–+——–+——-+——-
public | mybook | table | root
(1 row)

### カラムの確認
test=> \d mybook;
Table “public.mybook”
Column | Type | Collation | Nullable | Default
——–+———————–+———–+———-+———
id | integer | | |
name | character varying(10) | | |

### テーブル削除
test=> drop table mybook;
DROP TABLE

primary keyは、create tableの際に、name varchar(10) primary key,
mysqlでいうauto_incrementは、generated always as identity とする。
id integer generated always as identity,

Postgres放置してたけど、SpringBootやるなら必須じゃんか。

### データの挿入
test=> CREATE TABLE products (
product_no integer,
name text,
price numeric
);
CREATE TABLE
test=> INSERT INTO products (product_no, name, price) VALUES (1, ‘melon’, 9.99);
INSERT 0 1
test=> select * from products;
product_no | name | price
————+——-+——-
1 | melon | 9.99
(1 row)

### データの更新
test=> UPDATE products SET price = 12.0 where product_no = 1;
UPDATE 1
test=> select * from products;
product_no | name | price
————+——-+——-
1 | melon | 12.0
(1 row)

### データの削除
test=> delete from products where product_no = 1;
DELETE 1
test=> select * from products;
product_no | name | price
————+——+——-
(0 rows)

PostgresのCRUDは一通りマスターした。
後はこれをSpringBootから操作するところをやる

[Spring Boot2.4.2] Thymeleafを使おう

SpringBootのサポートするテンプレートエンジン
– Groovy, Thymeleaf, FreeMaker, Mustache

Tymeleaf利点
– 独自タグなし、ブラウザ表示

src/main/resources/templates/index.html

<!Doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>SpringBoot</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta charset="UTF-8">
<style type="text/css">
  form {
  	border-style: solid;
  	border-color: black;
  	border-width: 1px;
  	margin: 5px;
  	padding: 10px;
  }
</style>
</head>
<body>
<h1 th:text="'これはTypeleafですか?'">html-見出し1</h1>
Spring bootで推奨されています。
</body>
</html>

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

WhatTimeIsItController.java

package com.example.demo;

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

@RequestMapping("view/what-time-is-it")
@Controller
public class WhatTimeIsItController {

}

– @RequestMappingを設定
– Controllerで動的な値を生成
– 設定した動的な値をThymeleafのviewで参照

what time is it

package com.example.demo;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

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

@RequestMapping("view/what-time-is-it")
@Controller
public class WhatTimeIsItController {
	
	@GetMapping()
	public String view(Model model) {
		
		String now = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
		
		model.addAttribute("datetime", now);
		return "wtii"; // viewを返す
	}

}

Run As -> SpringBoot app

やべ、だいぶわかってきた。
ちょっと理解すると、アプリ作りたい衝動が抑えられなくなってくる

MVC:ControllerがMVC:Viewにデータを渡すのにui:Modelを使用する
model.addAttribute(“”,)で値を設定している
SpEL(Spring Expression Language)とうい方式を使っている
${変数式}、*{選択変数式}、#{メッセージ式}、@{リンク式} などがある

[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で売れ筋の本です