springboot

Spring Boot แยก environments dev ,staging ,prod ด้วย spring profiles EP3

เมื่อการพัฒนาระบบ ได้มาถึงจุดที่ต้องทำการแยก environments ที่แตกต่างกัน จากตัวอย่างที่เห็นได้ชัด ๆ เช่น endpoint ของ API ที่ dev ก็จะใช้เป็น endpoint เพื่อสำหรับ dev เท่านั้น แต่เมื่ออยากที่จะ deploy เพื่อใช้งานบน production ก็จะมี endpoint ของ API เวอร์ชั่นที่เป็น ของ production จริงและความต้องการแบบนี้ สำหรับ project ที่ build ด้วย spring boot จะแก้ปัญหาได้อย่างไร มาทำความรู้จัก spring profiles กัน เพื่อช่วยให้ระบบสามารถแยก constant variables ตาม environment ต้องทำอย่างไร

ทดลองสร้าง Simple Project เพื่อพิสูจน์ความต้องการในครั้งนี้

โดยปกติ spring boot เมื่อสร้าง project ด้วย spring initialize เช็คโครงสร้าง project จะสังเกตุเห็นไฟล์ src/main/resources/application.properties ถ้าเราต้องการที่จะแยก environment dev , staging ,prod ก็ต้องจัดการไฟล์นี้ใหม่

  • เพิ่มไฟล์ตาม environment ที่ต้องการ ตัวอย่างข้างล่างนี้จะทำตัวอย่าง แยก dev ,prod เพื่อใช้เป็นตัวอย่างง่าย ๆ โดยจะทำการ copy file application.properties มาตั้งชื่อใหม่ให้เป็นตามแต่ละ environment แบบนี้
    • dev: src/main/resources/application-dev.properties
    • prod: src/main/resources/application-prod.properties
  • เพิ่มเติมเนื้อหาข้างในไฟล์ application-dev.properties และ application-prod.properties
#file application-dev.properties
server.port=8090
api.endpoint="dev.poolsawat.com"
#file application-prod.properties
server.port=9090
api.endpoint="prod.poolsawat.com"

โดยกำหนดให้

server.port ของ dev start ที่ port 8090 , prod start ที่ port 9090

api.endpoint ของ dev ใช้ endpoint ที่ “dev.poolsawat.com” , prod จะใช้ที่ “prod.poolsawat.com”

  • แก้ไขไฟล์ Application.java (ไฟล์ สำหรับใช้ boot SpringApplication.run(PoolsawatApplication.class, args); )
package com.poolsawat.starter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class PoolsawatApplication implements CommandLineRunner{
	
	private static final Logger logger = LoggerFactory.getLogger(PoolsawatApplication.class);
	
	@Value("${api.endpoint}")
	private String apiEndpointUrl;
	
	@Value("${server.port}")
	private String serverPort;

	public static void main(String[] args) {
		SpringApplication.run(PoolsawatApplication.class, args);
	}
	
	@Override
	public void run(String... args) throws Exception {
		logger.info("server.port ::=="+serverPort);
		logger.info("api.endpoint ::=="+apiEndpointUrl);
		logger.info("spring boot loaded");
	}
	
}

เริ่มสั่ง start server ที่ env dev (ส่ง argrument -Dspring-boot.run.profiles=dev)

$ mvn -s D:/pool13433/programs/.m2/settings.xml spring-boot:run -Dspring-boot.run.profiles=dev
...
2021-03-08 13:29:35.407  INFO 12204 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2021-03-08 13:29:35.504  INFO 12204 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8090 (http) with context path ''
2021-03-08 13:29:35.512  INFO 12204 --- [           main] c.p.starter.PoolsawatApplication         : Started PoolsawatApplication in 1.253 seconds (JVM running for 1.518)
2021-03-08 13:29:35.514  INFO 12204 --- [           main] c.p.starter.PoolsawatApplication         : server.port ::==8090
2021-03-08 13:29:35.514  INFO 12204 --- [           main] c.p.starter.PoolsawatApplication         : api.endpoint ::=="dev.poolsawat.com"
2021-03-08 13:29:35.514  INFO 12204 --- [           main] c.p.starter.PoolsawatApplication         : spring boot loaded

เริ่มสั่ง start server ที่ env prod (ส่ง argrument -Dspring.profiles.active=prod)

  • pack jar ด้วย mvn install
  • exc jar
$ mvn -s D:/pool13433/programs/.m2/settings.xml install -Dmaven.test.skip=true
...
[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ poolsawat ---
[INFO] Installing D:\pool13433\aging\workspace\Poolsawat.Com\target\poolsawat.jar to D:\pool13433\programs\.m2\repository\com\poolsawat\poolsawat\0.0.1-SNAPSHOT\poolsawat-0.0.1-SNAPSHOT.jar
[INFO] Installing D:\pool13433\aging\workspace\Poolsawat.Com\pom.xml to D:\pool13433\programs\.m2\repository\com\poolsawat\poolsawat\0.0.1-SNAPSHOT\poolsawat-0.0.1-SNAPSHOT.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
$ java -jar -Dspring.profiles.active=prod ./target/poolsawat.jar
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.4.3)

2021-03-08 13:33:49.548  INFO 7448 --- [           main] c.p.starter.PoolsawatApplication         : Starting PoolsawatApplication v0.0.1-SNAPSHOT using Java 1.8.0_281 on LAPTOP-69PUN6C7 with PID 7448 (D:\pool13433\aging\workspace\Poolsawat.Com\target\poolsawat.jar started by pool13433 in D:\pool13433\aging\workspace\Poolsawat.Com)
2021-03-08 13:33:49.550  INFO 7448 --- [           main] c.p.starter.PoolsawatApplication         : The following profiles are active: prod
2021-03-08 13:33:50.698  INFO 7448 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 9090 (http)
2021-03-08 13:33:50.716  INFO 7448 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2021-03-08 13:33:50.717  INFO 7448 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.43]
2021-03-08 13:33:50.807  INFO 7448 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2021-03-08 13:33:50.807  INFO 7448 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1191 ms
2021-03-08 13:33:50.993  INFO 7448 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2021-03-08 13:33:51.156  INFO 7448 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 9090 (http) with context path ''
2021-03-08 13:33:51.167  INFO 7448 --- [           main] c.p.starter.PoolsawatApplication         : Started PoolsawatApplication in 2.062 seconds (JVM running for 2.484)
2021-03-08 13:33:51.168  INFO 7448 --- [           main] c.p.starter.PoolsawatApplication         : server.port ::==9090
2021-03-08 13:33:51.169  INFO 7448 --- [           main] c.p.starter.PoolsawatApplication         : api.endpoint ::=="prod.poolsawat.com"
2021-03-08 13:33:51.169  INFO 7448 --- [           main] c.p.starter.PoolsawatApplication         : spring boot loaded

สรุปท้ายบทความ

วิธีการที่นำเสนอนี้เป็นเพียงวิธีการแยก environment แบบนึง สามารถทำด้วยวิธีแบบอื่น ๆ ได้ แต่ผมมองว่ามันจะเป็นวิธีการที่เข้าใจง่ายที่สุด หากผู้อ่านท่านใดลองนำไปทำตามแล้วเกิดติดปัญหา สามารถฝากคำถามไว้ครับ ขอบคุณสำหรับการติดตามบทความครับ

Github code

Spring Boot สร้าง RESTful API พร้อม Unit Tests แบบเข้าใจง่าย และรวดเร็ว EP2

การพัฒนาแอพพลิเคชั่น ข้อแนะนำคือควรทำ Unit Tests มองการพัฒนาในระยะยาว หากต้องมีการแก้ไข issue เล็กน้อย แต่ไม่แน่ใจว่าจะเกิด defect กับระบบเดิมหรือไม่ การมี unit tests ช่วยประหยัดเวลาการ regression tests ได้เยอะมาก และคุณจะเห็นประโยชน์ของการทำ Unit tests อย่างแน่นอน

รู้จัก @RestController annotation สร้าง routes RESTful API

annotation @RestController implement มาจาก @Controller ของ spring web เพื่อใช้งานสำหรับการสร้าง RESTful API (ไม่ใช่ static view) เพิ่มความสะดวกในการพัฒนา RESTful API ให้ง่ายยิ่งขึ้น

ตัวอย่างการสร้าง Route (text/plain)

package com.poolsawat.starter.controller;

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

@RestController
public class HelloController {
	
	@RequestMapping("/")
	public String index() {
		return "Greetings from Spring Boot!";
	}
	
}

ทดสอบ http://localhost:8080/

Greetings from Spring Boot!

เพิ่ม Route (application/json)

... 
	@RequestMapping("/domain")
	public String[] json() {		
		return new String[] {"www",".","poolsawat",".","com"};
	}
...

restart server จากนั้น ทดสอบ http://localhost:8080/domain

[
  "www",
  ".",
  "poolsawat",
  ".",
  "com"
]

เพิ่ม Test Dependencies ใน POM

ก่อนที่จะเริ่มเขียน Unit Tests จำเป็นต้องเพิ่ม “spring-boot-starter-test” เข้าไปที่ไฟล์ POM (ไฟล์ pom.xml) มาเพิ่มกันเลย

...
<dependencies>
	...
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	...
</dependencies>
...

สร้าง src/test/java/com/poolsawat/starter/controller/HelloControllerTest.java

package com.poolsawat.starter.controller;

import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

@SpringBootTest
@AutoConfigureMockMvc
public class HelloControllerTest {
	@Autowired
	private MockMvc mvc;

	@Test
	public void getHello() throws Exception {
		mvc.perform(MockMvcRequestBuilders.get("/")
				.accept(MediaType.APPLICATION_JSON))
				.andExpect(status().isOk())
				.andExpect(content().string(equalTo("Greetings from Spring Boot!")));
	}
	@Test
	public void getDomain() throws Exception {
		mvc.perform(MockMvcRequestBuilders.get("/domain")
				.accept(MediaType.APPLICATION_JSON))
				.andExpect(status().isOk())
				.andExpect(content().json("[\"www\",\".\",\"poolsawat\",\".\",\"com\"]"));
	}
}

อธิบายการทำงาน

  • @SpringBootTest inject annotation Test เพื่อเริมการ Test
  • @AutoConfigureMockMvc inject annotation AutoConfiMockMVC (MockMVC มีหน้าที่สำหรับทำ Unit Tests มี feature mockup data)
  • @Autowired เป็นการ auto inject MockMVC bean ก่อนการจะเรียกใช้งานใน Testcase
  • @Test ระบุที method บอกให้รู้ว่า รันเคส method นี้
  • mvn.perform mockup route ที่ต้องการทดสอบ ระบุ MockMvcRequestBuilders.get(“/”) หมายถึง (“localhost:8080/”) ตรวจสอบ content-type คือต้องเป็น “MediaType.APPLICATION_JSON” กำหนดให้เป็น application/json เท่านั้น
  • andExpect
    • .andExpect(status().isOk()) expect htto status code OK (status code 200) หากไม่ใช่จะ fail ทันที
    • .andExpect(content().string(equalTo(“Greetings from Spring Boot!”))) expect content body ต้องเป็นคำว่า “Greetings from Spring Boot!” หากไม่ใช่จะ fail ทันที

จากนั้นสร้าง maven test build

apply -> run

ตรวจสอบที่ console panel

[INFO] 
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.poolsawat.starter.controller.HelloControllerTest
...
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.606 s - in com.poolsawat.starter.controller.HelloControllerTest
2020-10-15 10:57:54.262  INFO 5864 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.231 s
[INFO] Finished at: 2020-10-15T10:57:54+07:00
[INFO] Final Memory: 18M/220M
[INFO] ------------------------------------------------------------------------

ในทุกครั้งที่มีการแก้ไข java code จำเป็นต้องมีการ restart web server spring boot มี hot reload (การ auto restart web server เมื่อมี code change)

เพิ่ม “spring-boot-devtools”

...
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-devtools</artifactId>
	<optional>true</optional>
</dependency>
...

จากนั้นทำการ restart อีกครั้ง ต่อไปเวลาแก้ไข code ก็จะไม่ต้อง restart เองแล้ว

สรุปท้ายบทความ

การพัฒนาแอพพลิเคชั่นที่มี Test ถือว่าเป็นแอพพลิเคชั่นที่ดี ข้อดีของการทำ Test มีเยอะมากช่วยลดเวลาการ Regression Test ได้เยอะ ถ้าแอพพลิเคชั่นของคุณยังไม่มี test เริ่มทำได้แล้ว เพราะถ้าไม่ทำคุณจะคุยกับเขา (ทีมพัฒนา แอพพลิเคชั่นที่มี test) ไม่รู้เรื่อง บทความต่อไปจะเป็นเรื่องเกี่ยวกับอะไร คอยติดตามกันนะครับ

Spring Boot ทำความรู้จัก เริ่มติดตั้งและเริ่มใช้งานเบื้องต้น EP1

Spring boot คืออะไร

spring boot เป็น framework package ที่ต่อยอดมาจาก spring framework ช่วยให้เราสามารสร้างแอพพลิเคชั่น (application) แบบ stand-alone แบบรวดเร็ว และง่ายดาย

บ่อยครั้งที่ต้องมานั่งปวดหัวกับเรื่องการ configuration project ในขั้นตอนการ setup เริ่มต้นในการสร้างแอพพลิเคชั่น ปัญหาเหล่านี้จะถูกทำให้เสร็จเรียบร้อยแล้ว เมื่อมาใช้งาน spring boot

Features ต่าง ๆ ที่มีมาใน Spring boot

  • การสร้างแอพพลิเคชั่นแบบ stand-alone
  • embed Tomcat ,Jetty web server ยอดนิยมมาให้เรียบร้อยแล้ว หรือแม้แต่ Underflow ก็มีมาให้แล้ว
  • มีการกำหนด dependencies starter (POM) สำหรับพัฒนาไม่ต้องเพิ่มหลาย dependencies เหมือน spring framework แบบแต่ก่อน
  • มี auto configuration มาให้พร้อม ไม่ต้องยุ่งวุ้นวายกับการ config project เหมือนแต่ก่อน (ดีม๊วก ๆ )
  • ให้ feature monitoring ,metrics, health checks มาพร้อมแล้วเรียกใช้งานได้เลย
  • ไม่มีการสร้าง XML configuration file อีกต่อไปเพราะทุกอย่างจะ code บน java (kotlin) ทั้งหมดเลย

จะเริ่มเขียน Spring boot ต้องเตรียมตัวอย่างไรบ้าง

สำหรับมือใหม่ หลาย ๆ คนที่กำลังจะเรียนรู้ spring framework สั่งที่ต้องทราบก่อนที่จะเริ่มพัฒนาแอพพลิเคชั่นด้วย Java Framework อย่าง Spring Boot ต้องรู้เกี่ยวกับอะไร จะอธิบายย่อย ๆ ตามนี้

  • IDE (Integrated Development Environment) เครื่องมือสำหรับพัฒนาแอพพลิเคชั่น ที่ดัง ๆ สำหรับพัฒนา Java ก็พวก Eclipse ,IntelliJ IDEA เป็นต้น
  • Maven (Project Management Tools) สำหรับ Java Developer ทำงานได้ง่ายยิ่งขึ้น การที่จะต้องคอยหา JAR หรือบางครั้งเราจะเรียกว่า lib (dependencies) ที่มีมากมายซะเหลือเกิน อีกทั้งยังต้องมานั่งตรวจสอบเรื่องเวอชั่นของ JAR อีกละก็ ไม่สนุกแน่ ๆ สำหรับ Developer Java ปัญหาเหล่านี้จะถูกแก้ไขด้วย Maven อีกทั้งยังมีความสามารถในการทำกระบวนการ Build ,Compile , Test ,Deploy ,Documentation ได้อีกมากมาย อีกทั้งยังมี plugins ให้เรียกใช้งานอีกเพียบเลย ถือว่าดีม๊วก ๆ
  • Spring IO หน่วยงานพัฒนา Java Framework ที่โด่งดัง และยังคงได้รับความนิยมจนถึงยุคปัจจุบันด้วยความที่ไม่หยุดพัฒนา จึงทำให้ framework ได้รับความนิยมอยู่จนถึงปัจจุบัน

เริ่มติดตั้ง และสร้าง Hello World กัน

การ initializr Spring Boot Project มีด้วยกันหลายวิธี ผมขอยกตัวอย่างวิธีนี้

  1. เข้าไป generate spring boot project ที่ https://start.spring.io/
    1. Project ให้เลือก Maven Project (Gradle Project) เป็นเครื่องมืออีกแบบ ฝั่ง mobile dev ชอบใช้กัน เพราะมีมาใน android project)
    2. Language ให้เลือก Java (แล้วแต่ความชำนาญ ของผู้พัฒนาจะเลือกเป็น Kotlin หรือ Groovy ก็ได้)
    3. Spring Boot ให้เลือก version 2.3.4 เป็นเวอร์ชั่น ที่ stable แล้ว
    4. Project Metadata
      1. Group ใส่เป็น com.poolsawat (กำหนดเป็น domain ขององค์กร)
      2. Artifact ใส่เป็น Poolsawat (ชื่อสิ่งประดิษฐ์กำหนดเป็นชื่อของแอพพลิเคชั่น )
      3. Name ใส่เป็น Poolsawat (ชื่อแอพพลิเคชั่น)
      4. Description ใส่ Demo project for Spring Boot หรือจะใส่อธิบาย destination ของแอพพลิเคชั่น สั้น ๆ
      5. Package name ใส่ com.poolsawat.starter ใส่เป็น java package สำหรับเก็บ source code จะกำหนดภายใต้ src/com/poolsawat/starter/**
      6. Packaging เลือก Jar เพราะจะเลือกใช้ stand-lone application ที่รันด้วย embed Tomcat web server
      7. Java เลือก 8 (จะเลือก 10 , 15 แล้วแต่ JDK ในเครื่องที่ติดตั้งไว้)
      8. Dependencies เลือก “web”
      9. กด “GENERATE” downloadลงเครื่อง จะได้ไฟล์ .zip มา รอทำในขั้นถัดไป

2. เปิด IDE ตัวอย่างจะใช้ Eclipse IDE
        1. Unzip Poolsawat.zip ย้ายไฟล์ทั้งหมดไปยัง workspace ของ Eclipse ที่สร้างไว้
        2. หน้า Eclipse เลือก File -> Import… -> Maven -> Poolsawat

        3. จะมี Poolsawat เข้ามาที่ Project Explorer ทางซ้ายมือของ Eclipse IDE

3. แก้ไข Code ไฟล์ PoolsawatApplication.java ตามนี้

package com.poolsawat.starter;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class PoolsawatApplication {

	public static void main(String[] args) {
		SpringApplication.run(PoolsawatApplication.class, args);
	}

	@GetMapping("/hello")
	public String hello(@RequestParam(value = "name", defaultValue = "World") String name) {
		return String.format("Hello %s!", name);
	}

}

4. สร้าง Maven build เลือกเมนู Run -> Run Configurations… -> Maven Build -> New Launch configuration -> ใส่ค่าตามภาพนี้

Apply -> Run

5. รอ… maven download dependencies สักครู่…..

...

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.4.RELEASE)

2020-10-15 00:06:06.708  INFO 4832 --- [           main] c.p.starter.PoolsawatApplication         : Starting PoolsawatApplication on PoolPC-PC with PID 4832 (D:\workspace-blog\Poolsawat\target\classes started by PoolPC in D:\workspace-blog\Poolsawat)
2020-10-15 00:06:06.711  INFO 4832 --- [           main] c.p.starter.PoolsawatApplication         : No active profile set, falling back to default profiles: default
2020-10-15 00:06:07.773  INFO 4832 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2020-10-15 00:06:07.781  INFO 4832 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-10-15 00:06:07.782  INFO 4832 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.38]
2020-10-15 00:06:07.848  INFO 4832 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-10-15 00:06:07.848  INFO 4832 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1078 ms
2020-10-15 00:06:08.018  INFO 4832 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-10-15 00:06:08.174  INFO 4832 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-10-15 00:06:08.191  INFO 4832 --- [           main] c.p.starter.PoolsawatApplication         : Started PoolsawatApplication in 1.823 seconds (JVM running for 2.231)

6. ทดสอบเข้าลิ้ง http://localhost:8080/hello

สรุปท้ายบทความ

การพัฒนาเว็บด้วย Spring boot นั้นทำได้ไม่ยากเลย ค่อนข้างง่ายกว่าวิธีการเดิมของ Spring Framework ที่ผ่าน ๆ มา ไม่ต้อง configuration ให้ยุ่งยาก ไม่ต้องลำคาญกับการต้องมา control version ของ dependencies ที่มากมายซะเหลือเกิน เลยทำให้ผมชอบที่จะใช้งาน spring boot กับทุก ๆ project ของผมเลยครับ บทความหน้าจะเป็นเรื่องอะไร คอยติดตามกันนะครับ

SpringBoot2 สร้าง CRUD RESTful API พร้อม UnitTest แบบรวดเร็ว

Web Service แบบ RESTful protocol ได้รับความนิยมเป็นอย่างมากในปัจจุบัน หลายภาษามี web framework ของตัวอย่าง Java Spring Framework ก็เช่นกัน

SpringBoot เป็น Framework ที่ได้รับความนิยมมากกับ Java เพราะด้วยความง่ายที่เป็นสิ่งที่ถูกพัฒนาเพื่อแก้ปัญหาในการ Setup Project ที่ค่อนข้างยุ่งยากและซับซ้อนในการ Build RESTful API ขึ้นมาใช้งาน Springboot จึงได้รับความนิยมและได้เปรียบเรื่องความรวดเร็วในการ Setup Project

สิ่งที่จะได้รับเมื่ออ่านบทความบทนี้จบ

  • Create Springboot Project
  • CRUD Data with H2 Database
  • Use GET,POST,PUT,DELETE Methods
  • Content-Type : “application/json”
  • MockMvc
  • CrudRepository Interface crud



เริ่มกันเลย

  1. init project https://start.spring.io/
    1. 1
  2. เพิ่ม h2database ในไฟล์ pom.xml
    1. <project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
      
        <groupId>com.poolsawat</groupId>
        <artifactId>MediumTestRestful</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>jar</packaging>
      
        <name>MediumTestRestful</name>
        <url>http://maven.apache.org</url>
      
        <properties>
          <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
          <java.version>1.8</java.version>
        </properties>
      
        <parent>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-parent</artifactId>
          <version>2.0.5.RELEASE</version>
        </parent>
      
      
        <dependencies>
          <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
          </dependency>
          <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
          </dependency>
          <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
          </dependency>
          <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
          </dependency>
        </dependencies>
      
      
        <profiles>
          <profile>
            <id>DEV</id>
            <build>
              <plugins>
                <plugin>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
              </plugins>
            </build>
          </profile>
          <profile>
            <id>TEST</id>
            <build>
              <plugins>
                <plugin>
                  <groupId>org.apache.maven.plugins</groupId>
                  <artifactId>maven-surefire-plugin</artifactId>
                  <version>2.15</version><!--$NO-MVN-MAN-VER$ -->
                </plugin>
              </plugins>
            </build>
          </profile>
      
        </profiles>
      
      </project>
      
  3. สร้าง Project Structure ตามนี้
    1. ไฟล์ CrudController.java
      1. package com.poolsawat.medium.testrestful.controller;
        
        import java.util.Optional;
        
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.stereotype.Controller;
        import org.springframework.web.bind.annotation.DeleteMapping;
        import org.springframework.web.bind.annotation.GetMapping;
        import org.springframework.web.bind.annotation.PathVariable;
        import org.springframework.web.bind.annotation.PostMapping;
        import org.springframework.web.bind.annotation.PutMapping;
        import org.springframework.web.bind.annotation.RequestBody;
        import org.springframework.web.bind.annotation.RequestMapping;
        import org.springframework.web.bind.annotation.ResponseBody;
        
        import com.poolsawat.medium.testrestful.entity.Blog;
        import com.poolsawat.medium.testrestful.repository.BlogRepository;
        
        @Controller
        public class CrudController {
          
          @Autowired
          private BlogRepository blogRepository;
          
          
          @RequestMapping("/")
            public @ResponseBody String greeting() {
                return "Hello World";
            }
          
          @GetMapping("/get")
          public @ResponseBody Iterable<Blog> getBlogs(){
            return this.blogRepository.findAll();
          }
          
          @GetMapping("/get/id/{id}")
          public @ResponseBody Optional<Blog> getBlog(@PathVariable(name="id") Long id){
            return this.blogRepository.findById(id);
          }
          
          @PostMapping("/save")
          public @ResponseBody Blog saveBlog(@RequestBody Blog blog){
            return this.blogRepository.save(blog);
          }
          
          @PutMapping("/update")
          public @ResponseBody Blog updateBlog(@RequestBody Blog blog){		
            return this.blogRepository.save(blog);
          }
          
          @DeleteMapping("/id/{id}")
          public @ResponseBody Long deleteBlog(@PathVariable(name="id") Long id) {
            this.blogRepository.deleteById(id);
            return id;
          }
          
        }
        



    2. ไฟล์ Blog.java
      1. package com.poolsawat.medium.testrestful.entity;
        
        import java.io.Serializable;
        
        import javax.persistence.Entity;
        import javax.persistence.GeneratedValue;
        import javax.persistence.GenerationType;
        import javax.persistence.Id;
        
        @Entity
        public class Blog implements Serializable{
          /**
           * 
           */
          private static final long serialVersionUID = 6833355522200232153L;
        
        
          @Id
          @GeneratedValue(strategy = GenerationType.AUTO)
          private Long id;
            
          private String title;
          
          private String content;
          
          private String author;
          
        
          public Blog() {
            super();
            // TODO Auto-generated constructor stub
          }
        
          public Blog(Long id, String title, String content, String author) {
            super();
            this.id = id;
            this.title = title;
            this.content = content;
            this.author = author;
          }
        
          public Long getId() {
            return id;
          }
        
          public void setId(Long id) {
            this.id = id;
          }
        
          public String getTitle() {
            return title;
          }
        
          public void setTitle(String title) {
            this.title = title;
          }
        
          public String getContent() {
            return content;
          }
        
          public void setContent(String content) {
            this.content = content;
          }
        
          public String getAuthor() {
            return author;
          }
        
          public void setAuthor(String author) {
            this.author = author;
          }
        
          @Override
          public String toString() {
            return "Blog [id=" + id + ", title=" + title + ", content=" + content + ", author=" + author + "]";
          }
          
          
        }
        
    3. ไฟล์ BlogRepository.java
      1. package com.poolsawat.medium.testrestful.repository;
        
        import java.util.List;
        
        import org.springframework.data.repository.CrudRepository;
        import org.springframework.stereotype.Repository;
        
        import com.poolsawat.medium.testrestful.entity.Blog;
        
        @Repository
        public interface BlogRepository extends CrudRepository<Blog, Long> {	
          List<Blog> findByTitle(String title) throws Exception;
          List<Blog> findByAuthor(String author) throws Exception;
        }
        
    4. ไฟล์ Application.java
      1. package com.poolsawat.medium.testrestful;
        
        import org.springframework.boot.CommandLineRunner;
        import org.springframework.boot.SpringApplication;
        import org.springframework.boot.autoconfigure.SpringBootApplication;
        import org.springframework.context.annotation.Bean;
        
        import com.poolsawat.medium.testrestful.entity.Blog;
        import com.poolsawat.medium.testrestful.repository.BlogRepository;
        
        @SpringBootApplication
        public class Application {
        
          public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
          }
          
          @Bean
          public CommandLineRunner demo(BlogRepository blogRepository) {
            return (args) -> {				
              blogRepository.save(new Blog(Long.valueOf("1"), "SPA สร้าง Web Site Universal ด้วย Nuxt.js (EP.2) “Nuxt Directory Structure”", 
                  "SPA สร้าง Web Site Universal ด้วย Nuxt.js (EP.2) “Nuxt Directory Structure”","poolsawat"));
              blogRepository.save(new Blog(Long.valueOf("2"), "SPA สร้าง Web Site Universal ด้วย Nuxt.js (EP.1) “Setup Nuxt Project”", 
                  "SPA สร้าง Web Site Universal ด้วย Nuxt.js (EP.1) “Setup Nuxt Project”","poolsawat"));
              blogRepository.save(new Blog(Long.valueOf("3"), "สร้าง Project JSF Primeface ด้วย Maven พร้อมกับสอนทำระบบ Template Layout", 
                  "สร้าง Project JSF Primeface ด้วย Maven พร้อมกับสอนทำระบบ Template Layout","poolsawat"));
            };
          }
        }
    5. ไฟล์ CrudControllerTest.java
      1. package com.poolsawat.medium.testrestful;
        
        
        import static org.hamcrest.CoreMatchers.equalTo;
        import static org.hamcrest.Matchers.hasSize;
        import static org.hamcrest.Matchers.nullValue;
        import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
        import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
        import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
        import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
        import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
        import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
        import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
        
        import org.junit.Test;
        import org.junit.runner.RunWith;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
        import org.springframework.boot.test.context.SpringBootTest;
        import org.springframework.http.MediaType;
        import org.springframework.test.context.junit4.SpringRunner;
        import org.springframework.test.web.servlet.MockMvc;
        
        @RunWith(SpringRunner.class)
        @SpringBootTest
        @AutoConfigureMockMvc
        public class CrudControllerTest {
          
          @Autowired
            private MockMvc mockMvc;	
          
          @Test
          public void testShouldSaveBlog() throws Exception {
            String content = "{\"id\" : 4,\"title\" : \"TestRestful\"}";
            this.mockMvc.perform(
                post("/save")
                .content(content)
                .contentType(MediaType.APPLICATION_JSON)
                )
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.title", equalTo("TestRestful")))
                .andExpect(jsonPath("$.content", nullValue()))
                .andExpect(jsonPath("$.author", nullValue()));
          }
          
          @Test
          public void testShouldGetReturnBlogs() throws Exception {
            this.mockMvc.perform(get("/get"))
            .andDo(print())
            .andExpect(status().isOk())        
                .andExpect(jsonPath("$", hasSize(2)));
          }
          
          @Test
          public void testShouldGetReturnBlogById() throws Exception {
            Integer id = 3;
            this.mockMvc.perform(get("/get/id/"+id))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id", equalTo(id)))
                .andExpect(jsonPath("$.title", equalTo("สร้าง Project JSF Primeface ด้วย Maven พร้อมกับสอนทำระบบ Template Layout")));
          }
          
          @Test
          public void testShouldDeleteBlogById() throws Exception {
            Integer id = 1;
            this.mockMvc.perform(delete("/id/"+id))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$", equalTo(id)));
          }
          
          @Test
          public void testShouldUpdateBlog() throws Exception {		
            String content = "{\"id\" : 2,\"title\" : \"ShouldUpdateBlog\",\"author\" : \"poolsawat\"}";
            this.mockMvc.perform(
                put("/update")
                .content(content)
                .contentType(MediaType.APPLICATION_JSON)
                )
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id", equalTo(2)))
            .andExpect(jsonPath("$.title", equalTo("ShouldUpdateBlog")))
            .andExpect(jsonPath("$.content", nullValue()))
            .andExpect(jsonPath("$.author", equalTo("poolsawat")));
          }
        }
        
  4. สั่ง mvn spring-boot:run
    1. [INFO] --- maven-compiler-plugin:3.7.0:testCompile (default-testCompile) @ MediumTestRestful ---
      [INFO] Nothing to compile - all classes are up to date
      [INFO]
      [INFO] <<< spring-boot-maven-plugin:2.0.5.RELEASE:run (default-cli) < test-compile @ MediumTestRestful <<<
      [INFO]
      [INFO]
      [INFO] --- spring-boot-maven-plugin:2.0.5.RELEASE:run (default-cli) @ MediumTestRestful ---
      
        .   ____          _            __ _ _
       /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
      ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
       \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
        '  |____| .__|_| |_|_| |_\__, | / / / /
       =========|_|==============|___/=/_/_/_/
       :: Spring Boot ::        (v2.0.5.RELEASE)
      ...
      ...
      2018-12-05 18:51:43.509  INFO 3096 --- [           main] c.p.medium.testrestful.Application       : Started Application in 10.0
      65 seconds (JVM running for 16.349)
  5. สั่ง mvn test (ไม่ต้อง spring-boot:run แล้ว) เพื่อ run unittest ทดสอบ api CRUD
    1. ...
      [INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 12.749 s - in com.poolsawat.medium.testrestful.CrudContr
      ollerTest
      2018-12-05 19:02:23.789  INFO 5576 --- [       Thread-3] o.s.w.c.s.GenericWebApplicationContext   : Closing org.springframework
      .web.context.[email protected]: startup date [Wed Dec 05 19:02:12 ICT 2018]; root of context hierar
      chy
      2018-12-05 19:02:23.798  INFO 5576 --- [       Thread-3] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFa
      ctory for persistence unit 'default'
      2018-12-05 19:02:23.798  INFO 5576 --- [       Thread-3] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed
       drop of schema as part of SessionFactory shut-down'
      2018-12-05 19:02:23.805  INFO 5576 --- [       Thread-3] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown ini
      tiated...
      2018-12-05 19:02:23.811  INFO 5576 --- [       Thread-3] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown com
      pleted.
      [INFO] 
      [INFO] Results:
      [INFO] 
      [INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
      [INFO] 
      [INFO] ------------------------------------------------------------------------
      [INFO] BUILD SUCCESS
      [INFO] ------------------------------------------------------------------------
      [INFO] Total time:  17.707 s
      [INFO] Finished at: 2018-12-05T19:02:24+07:00
      [INFO] ------------------------------------------------------------------------

  6. github source