Basic Spring Part 3 - Dependency injection ใน Spring framework

Basic Spring Part 3 - Dependency injection ใน Spring framework

ผมเขียนเกี่ยวกับ Spring ไว้หลายตอน คุณสามารถกด Link ด้านล่างเพื่ออ่านที่เกี่ยวกับ Spring ตอนต่างๆได้เลย

ตอนที่แล้วเราสร้าง Project สร้าง Controller รับ Http request ได้แล้ว ตอนนี้เราจะมาทำความเข้าใจและใช้งาน Dependency injection ซึ่งบอกเลยว่าใช้งานง่ายมาก ง่ายจนคนไม่รู้ว่ามันคืออะไรแค่ใส่ Annotation ก็ทำงานได้ ตอนนี้อาจจะร่ายยาวหน่อย อาจจะมีน่าเบื่อบ้างแต่จะพยายามทำให้เข้าใจง่ายครับ ส่วน Code ทั้งหมดสามารถไปดูได้ที่ github เผื่อขี้เกียจเขียน

อยากสร้าง Web ที่ทำนายว่าจะได้แต่งงานตอนอายุเท่าไหร่จากวันเดือนปีเกิด

โอเคโจทย์เรื่องนี้ไม่ยากใช่ไหมครับ ผมแค่สร้าง Controller ขึ้นมา 1 ตัว จากนั้นเขียน method รับ Request ที่มาวันเดือนปีเกิดเข้ามาแล้วทำนายว่าจะได้แต่งงานตอนอายุเท่าไหร่ ซึ่งหน้าตา Code น่าจะออกมาประมาณนี้

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package test.spring.web.controller;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import test.spring.web.dto.request.ForecastMarryRequestDto;
import test.spring.web.dto.response.ForecastMarryResponseDto;

import java.util.Random;

@RestController
public class ForecastController {

@PostMapping("forecast/marry")
public ForecastMarryResponseDto forecastMarry(
@RequestBody ForecastMarryRequestDto request
) {
Random random = new Random();
int age = ( random.nextInt(80) ) + 20;
return new ForecastMarryResponseDto()
.setAge(age);
}

}

ไม่ต้องแปลกใจเรื่องทำนายนะครับ ผมไม่รู้ว่ามันต้องใช้อะไรเป็นการทำนายว่าจะแต่งงานเท่าไหร่ เลยใช้การ Random เอาซะเลย (คนทำเว็บทำนายอาจจะทำแบบนี้ก็ได้) พอลองยิงก็จะได้ผลลัพธ์ประมาณนี้

คนเริ่มนิยมกับการทำนาย

หลังจาก Web เปิดตัวคนแห่กันมาใช้งานแล้วติดใจ เริ่มมีการมาขอใช้งานในช่องทางที่งมากขึ้น เช่น ส่งมาเป็นไฟล์ อาจเรียกผ่าน Backgrond process, เรียกผ่าน event trigger ซึ่งแน่นอนว่าไม่ได้เรียกผ่าน Controller ที่เดียวอีกแล้ว ด้วยความต้องการแบบนี้คุณจึงคิดว่าควรจะย้ายวิธีการทำนายไปไว้ที่เดียว ซึ่งถ้าเรามองดีๆการทำนายของเรานั้นเป็น Business logic คือส่วนที่เป็นเรื่องเกี่ยวกับ Flow การทำงาน เงื่อนไข ทำแบบนี้ได้ไม่ได้ วิธีคำนวณคืออะไร สิ่งเหล่านี้เราจะเรียกมันว่า Business logic ซึ่งเราจะแยกส่วน Business logic ออกมาจากส่วนที่เป็น Controller หรือส่วนเชื่อมต่ออื่นๆ โดยการออกแบบในแนวทางนี้มีรูปแบบหนึ่งที่นิยมคือ Hexagonal Architecture โดยสามารถไปอ่านเพิ่มเติมได้ตามลิ้งเหล่านี้

https://netflixtechblog.com/ready-for-changes-with-hexagonal-architecture-b315ec967749

https://blog.octo.com/en/hexagonal-architecture-three-principles-and-an-implementation-example/

https://medium.com/@TKonuklar/hexagonal-ports-adapters-architecture-e3617bcf00a0

ด้วยเหตุนี้ผมเลยย้าย Logic การคำนวณไปไว้ใน Class ใหม่ที่ชื่อ : ForecastService ซึ่งจะได้ code ประมาณนี้

1
2
3
4
5
6
7
8
9
10
11
package test.spring.web.service;

import java.util.Random;

public class ForecastService {
public int forecastMarry(int day, int month, int year) {
Random random = new Random();
int age = ( random.nextInt(80) ) + 20;
return age;
}
}

แล้วจะเอา ForcastService ไปใส่ใน Controller ยังไง

อ่านชื่อหัวข้อแล้วคุ้นๆไหมครับ ตอนนี้เรากำลังพยายามจะทำ Dependency Injection กันอยู่นั่นเอง เรื่อง Dependency Injection ผมได้เคยเล่าไปแล้วใน ตอนที่ 1 ลองไปอ่านดูได้

ด้วยความรู้ที่เรามีเรารู้ว่ามันมีวิธี Injection คร่าวๆ 3 วิธีคือ

  • Constuctor Injection
  • Getter Setter Injection
  • Method Injection

ปัญหามันอยู่ที่ทั้ง 3 วิธีนั้นเราต้องยุ่งเกี่ยวกับ Object ที่เราต้องการจะ Inject ตัว Dependency เข้าไป ซึ่งจากตัวอย่างของเรา เราต้องการ Inject ตัว ForecastService เข้าไปใน ForecastController ซึ่งตัว ForecastController นั้นเราไม่ได้สร้างตัว Spring framework มันเป็นคนสร้าง เราเลยไปยุ่งอะไรกับมันไม่ได้ ซึ่งไม่ต้องกลัวคนเขียน Spring เขาเห็นถึงปัญหานี้แล้วจึงทำการเพิ่ม Annotation ขึ้นมาช่วยเราก็คือ @Autowired และ @Service โอเคมาดูกัน

@Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package test.spring.web.service;

import org.springframework.stereotype.Service;

import java.util.Random;

@Service
public class ForecastService {
public int forecastMarry(int day, int month, int year) {
Random random = new Random();
int age = ( random.nextInt(80) ) + 20;
return age;
}
}

จาก Code จะเห็นว่าเราทำการเพิ่ม @Service เข้าไปตรงด้านบน Class การใส่ Annotation แบบนี้เป็นการบอกว่า Class : ForecastService นั้นเป็นตัว Dependency ตัวนึงนะ พอตัว Spring มัน Start ขึ้นมามันไล่อ่านเจอ Annotation Service มันก็จะสร้าง ForecastService ขึ้นมาแล้วไปเก็บไว้ที่ที่นึงซึ่งมันคือ IOC Container ลองไปอ่านเพิ่มเติมดู แต่ถ้าไม่อยากอ่านมองว่ามันไปสร้างตัวแปร Global ไว้ละกัน

@Autowired

คราวนี้เราจะมาแก้ Code ที่ ForecastController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@RestController
public class ForecastController {

@Autowired
private ForecastService forecastService;

// อันนี้เก็บไว้ให้ดูความแตกต่าง
@PostMapping("forecast/marry")
public ForecastMarryResponseDto forecastMarry(
@RequestBody ForecastMarryRequestDto request
) {
Random random = new Random();
int age = ( random.nextInt(80) ) + 20;
return new ForecastMarryResponseDto()
.setAge(age);
}

@PostMapping("forecast/marry-with-service")
public ForecastMarryResponseDto forecastMarryWithService(
@RequestBody ForecastMarryRequestDto request
) {
int age = forecastService.forecastMarry(request.getDay(), request.getMonth(), request.getYear());
return new ForecastMarryResponseDto().setAge(age);
}
}

จะเห็นมีการประกาศ Attribute : ForecastService และด้านบนมีการประกาศ @Autowired ให้กับ Attribute นั้น การทำแบบนี้เป็นการบอก Spring ให้ทราบว่า เราต้องการให้ Inject ตัว ForecastService เข้ามาให้กับ Class ForecastController นี้ ซึ่งพอสั่ง Start server Spring ไล่ scan annotation มาเจอว่าต้องการให้ Inject : ForecastService เข้าไป มันจะไปหาใน IOC Container ว่ามีไหม ถ้าไม่มีมันจะไล่ Scan ต่อหาตัว Dependency : ForecastService ซึ่งถ้าไล่แล้วไม่เจอจริงๆมันจะ Error

และเมื่อเราลอง Start server และลองยิง Request ดูจะได้ผลลัพธ์แบบนี้

อยากสร้างตัว Dependency เอง

จากตัวอย่างที่แล้วเราใช้ @Service เพื่อให้ Spring ทำการสร้างตัว Dependency ขึ้นมาให้ (ตัว ForecastService) แต่ในบางกรณีเราอยากสร้าง Dependency ขึ้นมาเองด้วยวิธีการเขียน Code ไม่ใช่ประกาศ Annotation ซึ่งจะเจอบ่อยกรณีที่ไปเรียกใช้ Lib คนอื่นแล้วอยากให้ Object นั้นเป็น Dependency ซึ่งจะเกิดปัญหาคือเราไม่สามารถไปใส่ @Service บน Code ของคนอื่นได้ ซึ่งทางคนเขียน Spring ก็รู้ว่าน่าจะมีความต้องการประมาณนี้จึงสร้างเพิ่มตัว Annotation ให้เราเพิ่มเติมคือ @Configuration , @Bean

เราจะสร้าง Class : DependencyConfig ขึ้นมาดัง Code ด้านล่าง

1
2
3
4
5
6
7
8
9
10
11
12
13
package test.spring.web.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import test.spring.web.service.ForecastService;

@Configuration
public class DependencyConfig {
@Bean
public ForecastService forecastService() {
return new ForecastService();
}
}

@Configuration นั้นเป็นตัวที่บอกว่าให้ Spring รู้ว่าต้องไป Scan annotation ทั้งหมดใน Class นี้นะ

@Bean นั้นเป็นการบอกว่า ผลลัพธ์จาก Method นี้นั้นเป็นตัว Dependency นะ ให้เอาไปใส่ที่ IOC Container ด้วย

ปล. อย่าลืมไป Comment Annotation Service ที่ ForecastService นะครับ

ซึ่งเมื่อลอง Start server แล้วลองยิง Request ไปที่ Server จะพบว่ายังทำได้เหมือนเดิม

สรุป

สำหรับตอนนี้เราได้เรียนรู้วิธีการทำ Dependency injection ของ Spring ว่ามันมีการทำงานยังไง ต้องประกาศยังไงถึงจะใช้งานได้ โดยหากเข้าใจ Concept นี้แล้ว คุณจะสามารถนำไปประยุกต์ใช้ได้กับการเขียน Code ของคุณได้อีกมากมาย แค่ Concept การใช้ Dependency injection ก็ทำให้ Code ยืดหยุ่นได้โคตรๆแล้ว ในตอนต่อไปเราจะลองใช้งาน database กันดูครับ มาดูว่า Spring จะมีอะไรมาช่วยเราในการทำงานกับ Database บ้าง

เพลงประกอบการเขียน Blog

ตอนฟังเพลงนี้ครั้งแรกรู้สึกชอบความหมายเพลงนี้มากๆ ตรงกับประสบการณ์ตัวเองมากๆ แบบเจอผู้หญิงดีๆคนนึงแล้วมีคนชอบเขาเยอะมากๆ แต่พอเห็นเขาไม่มีแฟนเราก็ดีใจนะรู้สึกเหมือนในเพลงเลย (แต่จะดีกว่าถ้าเป็นแฟนเราอะนะ ฮ่าๆๆๆๆๆๆ)