Spring Boot with RESTful Web Services 3/3

Phai Panda
Tech INNO
Published in
7 min readJul 22, 2019

จาก Oracle Database 12c บน Docker กระทั่งสร้างเป็นโปรเจกต์ Spring Boot ด้วยความสามารถของ Spring Boot Starter Data JPA ขณะนี้เราเพิ่มหนังสือหนึ่งเล่มไว้ในฐานข้อมูลได้สำเร็จแล้ว หนนี้จะมาเขียน RESTful Web Services เสียที

เนื้อหาบทความก่อนหน้านี้

Spring Boot Starter Web

เราต้องการความสามารถในการจัดการ View เราต้องการหน้าเว็บ แม้แต่ JSON เป็น response กลับไปยังที่เรียก เราจึงต้องการ Spring Boot Starter Web

ดังนั้นเพิ่มมันเข้าไปใน build.gradle

dependencies {
compile("org.springframework.boot:spring-boot-starter-web")
compile("org.springframework.boot:spring-boot-starter-data-jpa")
compile group: 'com.oracle', name: 'ojdbc7', version: '12.1.0.2'
testCompile group: 'junit', name: 'junit', version: '4.12'
}

เพียงเท่านี้เมื่อทำงานโปรแกรมอีกครั้ง (กดปุ่มเขียว run) สภาพโปรเจกต์จะกลายเป็น server ตัวหนึ่งที่ทำงานอยู่บน port 8080 โปรดสังเกตที่ Terminal

Tomcat started on port(s): 8080 (http) with context path ''

และสามารถตามหาด้วยชื่อ domain ว่า localhost ดังนั้นเปิด Browser แล้วพิมพ์

localhost:8080
เพราะเรายังไม่มี view โดย default จึงได้แบบนี้

แต่ก่อนที่จะเดินหน้าต่อ ให้เพื่อนๆกลับไปที่ SQL Developer แล้วกด Refresh

สิ่งที่ได้ทุกครั้งเมื่อทำงานโปรแกรมใหม่ (หยุดก่อนค่อยรันใหม่ หรือเรียกว่า Rerun) จะได้หนังสือเพิ่มรอบละเล่ม

สืบเนื่องจากโค้ดชุดนี้ยังอยู่

@Override
public void run(String... args) throws Exception {
Book book1 = new Book();
book1.setName("Java from Beginner");

bookRepository.save(book1);
}

ฉะนั้นจะ comment หรือลบมันทิ้งไปเลยก็ได้ครับ คลาส Application เหลือแค่นี้

รู้จักกับ Rest Controller

Controller คือความคิดจัดการ URLs จัดการจับคู่ Model กับ View เขียนในรูป annotation เป็น @Controller ทว่าขณะนี้เราต้องการให้มันจัดการลึกลงไปถึงระดับ resources กล่าวคือจัดการ Uniform Resource Identifier (URI) เพราะสิ่งที่ได้คือตัวข้อมูลจริงๆที่เรียกว่า resources

อ่านเพิ่มเติมเกี่ยวกับ URI

Rest Controller ใน Spring Boot ก็คือ @Controller ผนวกกับ @ResponseBody เขียนในรูป annotation เป็น @RestController เพื่ออำนวยความสะดวกในการคืนค่าที่เป็น resources ที่อยู่ในรูปแบบ JSON หรือ XML

@RestController หากจำไม่ผิดน่าจะมีมาตั้งแต่ Spring Framework เวอร์ชัน 4.0

สรุปตรงนี้ขอให้ใช้ @RestController ไปเลย

การจัดการ ​Paths และ HTTP Methods

ขอให้อ่านเพิ่มเติมนะครับ

สรุป HTTP Methods ที่ใช้บ่อยแบบง่ายๆดังนี้

  • GET เพื่อ retrieve resource representation/information only ขอดู resources เท่านั้น
  • POST เพื่อ create new subordinate resources คือสร้าง resources ใหม่
  • PUT เพื่อ update existing resource คือปรับปรุงแก้ไข resource เดิมที่มีอยู่ (แต่หากว่าไม่มี resource ที่ระบุ, API อาจตัดสินใจสร้างให้ใหม่เลยก็ได้)
  • DELETE เพื่อ delete resources คือลบ resources ที่ต้องการ

สร้าง Rest Controller กันเถอะ

เพิ่ม package ใหม่ชื่อว่า

com.pros.controller

สร้าง BookController ขึ้นมา

package com.pros.controller;

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

@RestController
public class BookController { }

แล้วเพิ่ม say Hello ด้วย GET

package com.pros.controller;

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

@RestController
public class BookController {
@GetMapping
public String index() {
return "Hello";
}

}

Rerun server (โปรเจกต์ my_books) แล้วเรียกไปที่ localhost:8080 อีกครั้ง

คลิกขวา ณ​ พื้นที่ว่าง แล้วเลือก Inspect มองไปที่ Network tab

อธิบายได้ว่าเมื่อใช้ Client เรียกไปยัง localhost:8080 ตามปกติแล้ว Browser จะรับทราบว่า Client ต้องการเรียกไปยังหน้าแรกที่มีชื่อเป็น index หรือใช้เครื่องหมาย / ทดแทนได้

ขอให้เพื่อนๆสังเกตที่ General มองไปที่ Request URL มันจะเขียนให้แบบเต็มรูปแบบว่า Client ได้ร้องขอ http://localhost:8080/ และการร้องขอนี้ใช้สมบัติของ GET

ขอให้เพื่อนๆสังเกตที่ Response Headers ตรง Content-Type เป็น text/html นี่คือสิ่งที่ server ตอบกลับมา ว่า Hello ที่เห็นนั้นมีชนิดเป็น text/html หรือกล่าวคือเป็น HTML ไฟล์หนึ่ง ณ ตรงนี้จึงถือว่า server ได้ผลิต View ให้แล้ว

จริงไหม? เลือกที่ Elements tab

ชัดเจนว่าเป็นไฟล์ HTML

แต่เราต้องการ JSON

อย่างนั้นเราเพิ่มอีก method ใน BookController (เมธอดพวกนี้ชื่ออะไรก็ได้นะครับ แต่ควรตั้งชื่อให้เหมาะสม อ่านแล้วรู้ว่ากำลังสื่อถึงอะไร) ชื่อ findAll เขียนได้ว่า

@GetMapping("/books")
public List<Book> findAll() {
return bookRepository.findAll(); //error return type
}
  • findAll นี้ตั้งใจจะคืนค่าเป็น JSON เป็นค่ารายการของหลังสือทั้งหมด
  • เหตุนี้จึง return type เป็น List<Book>
  • และยังต้องใช้ GET พร้อมกับตั้งชื่อ path เป็น /books
  • รายการหนังสือหาได้จาก bookRepository.findAll ตรงนี้คือเราเรียกใช้ BookRepository ซึ่งสืบทอดจาก CrudRepository
  • มันจะ error เพราะ return type ที่ได้จากการเรียก bookRepository.findAll เป็น Iterable ต่างจากสิ่งที่เราต้องการที่เป็น List
  • ทางออกคือใช้สิ่งที่เรียกว่า Query Methods

Query Methods

ง่ายๆว่าเป็นวิธีการเขียน query ในรูปแบบโค้ดจาวา, Spring Data JPA ได้จัดเตรียมเครื่องมือที่เรียกว่า Query Builder เพื่อทำเรื่องนี้ให้เป็นจริง ประกอบด้วยรูปแบบ find…By, read…By, query…By, count…By, and get…By

อ่านเพิ่มเติม

เรามี BookRepository สืบทอดจาก CrudRepository และ CrudRepository สืบทอดจาก Repository Interface ตามลำดับ

Repository Interface นี่แหละคือหนทางบอกกับ framework ว่าด้วยเรื่อง Query Methods ว่าเราต้องการเขียนเพิ่มนะ ตามรูปแบบที่ framework วางไว้ให้ ก็จะเข้าใจกันได้ทั้งสองฝ่าย

เปิดไฟล์ BookRepository เพิ่ม query method ดังด้านล่าง

package com.pros.repository;

import com.pros.model.Book;
import org.springframework.data.repository.CrudRepository;

import java.util.List;

public interface BookRepository extends CrudRepository<Book, Long> {
List<Book> findAll();
}

เท่านี้ error ที่ BookController ก็หายไปแล้ว

package com.pros.controller;

import com.pros.model.Book;
import com.pros.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class BookController {
@Autowired
private BookRepository bookRepository;

@GetMapping
public String index() {
return "Hello";
}

@GetMapping("/books")
public List<Book> findAll() {
return bookRepository.findAll();
}
}

rerun server ครับ แล้วไปเรียก

localhost:8080/books
สำเร็จ! แสดงรายการหนังสือทั้งหมดที่มี ซ้ำกันละไว้ก่อน
เลือก Network tab เลือก resources ชื่อ books แล้วเลือก Preview ผลลัพธ์ ได้ JSON

ขอให้เพื่อนๆสังเกต Response Headers ที่ได้ เห็นไหม ด้วยความสามารถของ @RestController ดูตรง Content-Type มันเป็น application/json แล้ว

เราทำมันได้!

ถ้าอยากได้ XML ล่ะ ตรงนี้ทำอย่างไร?

ปกติแล้ว RESTful Web Services ไม่สนับสนุนให้ใช้ข้อมูลในรูปแบบ XML มีเหตุผลหลายประการณ์เมื่อนำมาเทียบกับ JSON ไม่ว่าจะเป็น

  • XML อ่านยาก ส่วน JSON อ่านง่าย
  • ในกระบวนการ parsing XML ใช้เวลาเยอะกว่า JSON เนื่องจาก DOM manipulation libraries ต้องการหน่วยความจำค่อยข้างมากในการจัดการ XML ไฟล์ขนาดใหญ่
  • โครงสร้างแบบ key/value ของ JSON ในบางกรณีได้จำกัดความหลากหลายของแท็ก XML นั่นเป็นผลให้โครงสร้างข้อมูลจัดการง่ายขึ้น
  • เราเพียงต้องการส่งข้อมูลจากระบบหนึ่งสู่อีกระบบหนึ่ง ดังนั้นต้องคำนึงถึงทรัพยากรบนเครือข่าย ความเร็วและขนาดตลอดจนการใช้หน่วยความจำซึ่งเป็นเรื่องสำคัญมาก และ XML พ่ายแพ้ต่อ JSON อย่างมากในเรื่องนี้
  • บลา บลา บลา

แต่ถ้าอยากได้ ให้ทำแบบนี้

Google ไปว่า
jackson dataformat xml

เลือก https://mvnrepository.com ที่ค้นเจอ

กดเข้าไปเลย อันนี้แหละ
ผมเลือกเวอร์ชันล่าสุด (บนสุด)
เลือก Gradle

แล้วเอาไปแปะใน build.gradle

dependencies {
compile("org.springframework.boot:spring-boot-starter-web")
compile("org.springframework.boot:spring-boot-starter-data-jpa")
compile group: 'com.oracle', name: 'ojdbc7', version: '12.1.0.2'
testCompile group: 'junit', name: 'junit', version: '4.12'

compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.9.9'
}

นี่คืออะไร? นี่คือ lib ที่ทำหน้าที่ binding ทั้งการอ่านและการเขียนข้อมูล (ในภาษาจาวาเรียกว่าออบเจ็กต์) ในรูปแบบ XML, หากทำ XML ให้เป็นจาวาออบเจ็กต์มีศัพท์เรียกว่า unmarshal และหากทำจาวาออบเจ็กต์ให้เป็น XML เรียกว่า marshal

อ่านเพิ่มเติม

ทดสอบ GET method

ผมจะใช้เครื่องมือชื่อ Postman ส่งคำขอ (request) ไปยัง server ในหลายกรณีต่อไปนี้นะครับ

  • GET ข้อมูลในรูปแบบ XML จาก server ที่ไม่รองรับ XML Binding
  • GET ข้อมูลในรูปแบบ XML จาก server ที่รองรับ XML Binding

ให้ client เรียกหาข้อมูลในรูปแบบ XML จาก server ที่ยังไม่รองรับการ binding XML ผลที่ได้คือ 406 Not Acceptable

ดูที่มุมขวาล่าง นั่นคือ HTTP response status code ที่ตอบกลับมาจาก server

และให้ client เรียกหาข้อมูลในรูปแบบ XML จาก server ที่รอบรับการ binding XML แล้วล่ะก็

ออกมาแล้ว!

ทดสอบ POST method

  • POST ข้อมูลในรูปแบบ JSON ไปยัง server ที่ไม่รอบรับการ POST
  • POST ข้อมูลในรูปแบบ JSON ไปยัง server ที่รองรับการ POST

อย่างแรกก่อน เปิด Postman

  1. ไปที่ Headers tab แล้วเอาเครื่องหมายถูกออกจาก Accept คือเราต้องการ JSON ไม่ต้องการ XML แล้ว
  2. ณ มุมบนซ้ายเปลี่ยน GET เป็น POST
  3. มองหา Body tab เลือกแล้วส่งข้อมูลแบบกำหนดเอง raw
  4. จากนั้นเขียนข้อมูลในรูปแบบ JSON ในที่นี้คือหนังสือหนึ่งเล่ม
{ "name": "Maria DB + MySQL ฉบับสมบูรณ์" }
จะพบกับ HTTP response status code 405 Method Not Allowed

กล่าวคือ 405 เพราะ server ไม่มีความสามารถหรือไม่รองรับการ POST นี้

เอาล่ะ จงทำให้ server รองรับการ POST นี้เสีย เปิดไปที่คลาส BookController แล้วเพิ่มโค้ดต่อไปนี้

@PostMapping
public Book create(@RequestBody Book book) {
return bookRepository.save(book);
}

rerun server แล้วทดลองส่งคำขอไปใหม่

หนนี้เจอ 415 Unsupported Media Type

เจอ 415 เพราะ client ส่งสิ่งที่ server เข้าใจว่าเป็น text/plain ไม่ใช่ application/json (JSON)

แก้ไขใหม่ เราไม่ยอมแพ้ ที่ Postman มองหา Headers tab แล้วเพิ่ม metadata ชื่อ Content-Type มีค่าเป็น application/json คือบอกว่าเราจะส่ง JSON ไปกับ POST นะ

สำเร็จ! server response code 200 OK

กลับไปดูที่ฐานข้อมูล อย่าลืม refresh ก่อนล่ะ

ทดสอบ DELETE method

  • ส่งคำขอ DELETE ไปยัง server พร้อมกับแนบ Book ID ไปด้วย ว่าต้องการลบหนังสือ ID นี้นะ

เปิดไปที่คลาส BookController แล้วเพิ่มโค้ดต่อไปนี้

@DeleteMapping(value = "/books/{id}")
public void delete(@PathVariable Long id) {
Optional<Book> optional = bookRepository.findById(id);
if(optional.isPresent()) {
bookRepository.delete(optional.get());
}
}

ใช้ Postman ส่งคำขอ DELETE ไป เอาเป็นหนังสือเล่มที่สอง ที่มี ID 2 แล้วกัน เขียนได้ว่า

localhost:8080/books/2

เมื่อคำขอนี้ถูกส่งไปยัง server มันจะมองหา books resource จากนั้นตามหา @DeleteMapping ที่เข้าใจรูปแบบของ URI นั้น นั่นคือ

value = "/books/{id}"

หมายเหตุ หากเพื่อนๆใช้ Postman ต่อเนื่องมาจากตัวอย่างข้างต้นก่อนหน้านี้ ที่ Headers tab เราไม่จำเป็นต้องใช้ Accept หรือ Content-Type รวมไปถึงที่ Body tab ไม่จำเป็นต้องส่ง JSON ไปให้ ดังนั้นเอาเครื่องหมายถูกออกและเลือก none ตามลำดับ

ไม่ต้องการ Accept และ Content-Type
ไม่ต้องการข้อมูลใดๆ

หากเลือกคำขอเป็น DELETE พร้อมแล้ว กด Send เลย (อย่าลืม rerun server ล่ะครับ)

HTTP response status code 200 OK

ทดสอบ PUT method

  • ส่งคำขอ PUT ไปยัง server พร้อมกับแนบ Book ID ไปด้วย ว่าต้องการแก้ไขหนังสือ ID นี้นะ

PUT ก็คือการแก้ไข resource ในที่นี้จำต้องกำหนดว่าจะแก้ไขหนังสือเล่มไหน ผมจะแก้ไขหนังสือเล่มที่มี ID เป็น 1 จะเปลี่ยนชื่อของมันจาก “Java from Beginner” เป็น “จาวาสำหรับผู้เริ่มต้น”

เปิดไปที่คลาส BookController แล้วเพิ่มโค้ดต่อไปนี้

@PutMapping(value = "/books/{id}")
public Book update(@PathVariable Long id, @RequestBody Book book) {
Optional<Book> optional = bookRepository.findById(id);
if(optional.isPresent()) {
Book existedBook = optional.get();
existedBook.setName(book.getName());
return bookRepository.save(existedBook);
}
return null;
}

จากโค้ดมันจะหาดูว่า ID ที่รับมามีอยู่จริงหรือไม่ ถ้ามี (isPresent มีค่า true) ก็จะกำหนดค่า name ใหม่ที่ได้รับมาแทนค่า name เดิมที่ค้นหาเจอ แล้วบันทึกลงฐานข้อมูล (save) ในที่สุด

สำหรับ client โดย Postman

1.กำหนด URI

localhost:8080/books/1

2.เปลี่ยน HTTP method จากเดิมเป็น PUT

3.กำหนด Content-Type เป็น application/json

4.กำหนด raw ดังนี้

{ "name": "จาวาสำหรับผู้เริ่มต้น" }

5.กด Send จ้า

สำเร็จแล้ว! ดูผลลัพธ์ที่ server ส่งกลับมา

ละแล้วเพื่อนๆก็ได้รู้จักกับ HTTP methods ได้แก่ GET, POST, PUT และ DELETE และได้ทดลองเขียน RESTful Web Services ที่รองรับ GET, POST, PUT และ DELETE สามารถส่ง JSON จาก Client ไปยัง Server ได้ สามารถทำให้ Server response กลับได้ ทั้งยังได้รู้จัก HTTP Response Status Code อีกเล็กน้อยอีกด้วย

ผมเชื่อว่าจากจุดเล็กๆนี้ซึ่งอาจเข้าใจบ้างไม่เข้าใจบ้าง จะมากจะน้อยเราก็ได้เขียนผ่านมือ คือได้ปฏิบัติด้วยตนเอง หวังว่าเพื่อนๆจะฝึกฝนและต่อยอดความสามารถของเพื่อนๆให้ลึกและละเอียดมากขึ้นนะครับ

--

--