Hike News
Hike News

Spring - ทำการ Autowire เข้า Object แบบ Manual

Spring - ทำการ Autowire เข้า Object แบบ Manual

สำหรับตอนนี้มันเริ่มจากต้องการสร้าง Class นึงขึ้นมา แล้วมันต้องการ Dependency จำนวนมาก แต่มีแค่บาง Field เท่านั้นที่ไม่เหมือนกัน หรือใช้แล้วทิ้งไม่อยากเอาไปเก็บใน IOC ซึ่งจริงๆมันก็ทำได้ แต่คุณต้องมานั่งสั่ง setter หรือทำ constructor 10 - 20 field ด้วยความขี้เกียจของ Programmer ก็เลยไปหาวิธีที่มันจะทำการ Autowire แบบ Manual เองไม่ได้ ไม่ต้องให้ Spring ทำให้อัตโนมัติ

ลองทำกันเลย

สำหรับใครต้องการตัว Source code เต็มสามารถไปดูได้ที่ https://github.com/surapong-taotiamton/example-autowireafterstart

ต้องไปเปลี่ยนตัวค่าตัว config เหล่านี้ด้วยนะครับเป็น db host , username , password ของเครื่องท่านเองนะครับ

1
2
3
4
spring.datasource.jdbc-url=jdbc:mariadb://192.168.56.101:3306/test?useUnicode=yes&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&&serverTimezone=Asia/Bangkok
spring.datasource.url=jdbc:mariadb://192.168.56.101:3306/test?useUnicode=yes&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&&serverTimezone=Asia/Bangkok
spring.datasource.username=root
spring.datasource.password=root

ตัวอย่าง AccountService

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
public class AccountService {

@Autowired
private AccountRepository accountRepository;

@Value("${config.account}")
private String accountConfig;

public String getAccountConfig() {
return this.accountConfig;
}

public Account createAccount(String accountId, String accountName) {
return this.accountRepository.save(new Account().setAccountId(accountId).setAccountName(accountName));
}

@Transactional(isolation = Isolation.REPEATABLE_READ)
public Account testTransactional(String accountId, String accountName) {
Account account = this.accountRepository.save(new Account().setAccountId(accountId).setAccountName(accountName));
if (true) {
throw new RuntimeException("Test transactional");
}
return account;
}
}

Class นี้เป็น Service จะเห็นว่ามีการใช้ @Autowired และ @Value ซึ่งถ้าเราใส่ @Service หรือ @Component ไว้เหนือ Class เวลา Start server ตัว Spring จะสร้าง Bean และ Inject Bean ต่างๆให้เราอัตโนมัติ แต่ตอนนี้เราต้องการสร้างแบบ Manual ดังนั้นจึงไม่มี

AccountController

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
26
27
28
29
30
31
32
33
34
35
36
37
38
@RestController
public class AccountController {

@Autowired
private ApplicationContext applicationContext;

@GetMapping("/get-config")
public String getConfig() {
AccountService accountService = new AccountService();
applicationContext.getAutowireCapableBeanFactory().autowireBean(accountService);
return accountService.getAccountConfig();
}

@GetMapping("/test-create")
public String testCreate() {
AccountService accountService = new AccountService();
applicationContext.getAutowireCapableBeanFactory().autowireBean(accountService);
Account account = accountService.createAccount("test-create-id", "test-create-name");
return account.getAccountId() + " : " + account.getAccountName();
}

@GetMapping("/test-transactional-error-case")
public String testCreateErrorCase() {
AccountService accountService = new AccountService();
applicationContext.getAutowireCapableBeanFactory().autowireBean(accountService);
Account account = accountService.testTransactional("test-transactional-create-id", "test-transactional-create-id");
return account.getAccountId() + " : " + account.getAccountName();
}

@GetMapping("/test-error-transactional-success-case")
public String testCreateSuccessCase() {
AccountService accountService = new AccountService();
applicationContext.getAutowireCapableBeanFactory().autowireBean(accountService);
accountService = (AccountService) applicationContext.getAutowireCapableBeanFactory().applyBeanPostProcessorsAfterInitialization(accountService, null);
Account account = accountService.testTransactional("test-transactional-create-id-complete", "test-transactional-create-id-complete");
return account.getAccountId() + " : " + account.getAccountName();
}
}

โอเคมันอาจจะยาวนะครับ เพราะมันมีกรณีที่อยากแสดงให้เห็นว่าทำงานได้จริงมีปัญหาไหม เดี๋ยวเราค่อยๆมาดูทีละส่วนเพื่อทำความเข้าใจกันครับ

1
2
@Autowired
private ApplicationContext applicationContext;

ส่วนนี้คือการการ Autowired ตัว ApplicationContext เข้ามา โดยตัว ApplicationContext นั้นจำเป็นในการ Autowired แบบ Manaul ครับ

1
2
3
4
5
6
@GetMapping("/get-config")
public String getConfig() {
AccountService accountService = new AccountService();
applicationContext.getAutowireCapableBeanFactory().autowireBean(accountService);
return accountService.getAccountConfig();
}

ตรงส่วนนี้ผมมีไว้ Test โดยยิง Http request มาแล้วทดลองสร้าง AccountService และ Autowire ค่าเข้าไป โดยส่วนที่ทำการ Autowire คือบรรทัด

1
applicationContext.getAutowireCapableBeanFactory().autowireBean(accountService);

ส่วนตรงนี้มีไว้เพื่อทดสอบว่าสามารถ Autowire ตรง @Value(“${config.account}”) เข้าไปได้หรือไม่

1
return accountService.getAccountConfig();

โดยเมื่อทำการทดลองยิง http request ผ่าน Browser ก็ได้ผลลัพธ์ดังภาพ

โดยค่านั้นตรงกับที่ผม Set ใน application.properties

ซึ่งทำให้เห็นว่าเราสามารถ Autowire ค่าเข้าไปได้

ทดสอบดูว่าสามารถ Insert ค่าลง Database ได้ไหม

ตัวอย่างที่แล้วเราทดลองตัว @Value ไปแล้ว เรามาลองดู @Autowired นั้นทำงานได้ไหม

1
2
3
4
// AccountService
public Account createAccount(String accountId, String accountName) {
return this.accountRepository.save(new Account().setAccountId(accountId).setAccountName(accountName));
}

Code ส่วนนี้เป็นการ Save ข้อมูล Account ลง Database

1
2
3
4
5
6
7
8
// AccountController
@GetMapping("/test-create")
public String testCreate() {
AccountService accountService = new AccountService();
applicationContext.getAutowireCapableBeanFactory().autowireBean(accountService);
Account account = accountService.createAccount("test-create-id", "test-create-name");
return account.getAccountId() + " : " + account.getAccountName();
}

Code ส่วนนี้เป็นขา Controller ไว้รับ Request แล้วทำเรียกใช้ AccountService อีกที โดย Code ทั้งสองส่วนนี้จะทดลองว่าเราสามารถ Autowire ตัว AccountRepository ลงไปได้ไหม ซึ่งผลการทดลองเป็นดังภาพด้านล่าง

ซึ่งจะเห็นว่าเราสามารถ Insert ข้อมูลลง Database ได้

Transaction ใช้งานได้ไหม

Feature ที่สำคัญ Feture นึงที่เรามักใช้บ่อยๆเพื่อลดความซับซ้อนเวลาเกิดปัญหาในระหว่างการทำงานนั่นคือ Feature : Transaction ส่วนใครไม่รู้ว่า transaction คืออะไรสามารถไปอ่านได้ที่ Concurrency Part 2 - Transaction , ACID และ Isolation level ของ Relational Database เลยครับ

1
2
3
4
5
6
7
8
9
// AccountService
@Transactional(isolation = Isolation.REPEATABLE_READ)
public Account testTransactional(String accountId, String accountName) {
Account account = this.accountRepository.save(new Account().setAccountId(accountId).setAccountName(accountName));
if (true) {
throw new RuntimeException("Test transactional");
}
return account;
}

ตัว method : testTransactional มีไว้ทดลองว่าถ้าเราทำการ insert ข้อมูลแล้วหลังจากนั้นเกิด Error (จะเห็นว่าจงใจ Throw ออกมาเลย) ถ้า Transaction มันทำงานอยู่มันจะต้องทำการ Rollback ไปเป็นเหมือนไม่เคยทำการ Insert ได้

1
2
3
4
5
6
7
8
// AccountController
@GetMapping("/test-transactional-error-case")
public String testCreateErrorCase() {
AccountService accountService = new AccountService();
applicationContext.getAutowireCapableBeanFactory().autowireBean(accountService);
Account account = accountService.testTransactional("test-transactional-create-id", "test-transactional-create-id");
return account.getAccountId() + " : " + account.getAccountName();
}

ตรงนี้จะเห็นว่าตรงการสร้าง AccountService ยังเหมือนเดิมแต่เรียกใช้ method : testTransactional แทน โดยเรามาทดลองกันว่าผลลัพธ์จะเป็นยังไง

ตรงนี้เกิดการ Error เพราะ Code เรากะให้มัน Error อยู่แล้วไม่ต้องตกใจ

ตรงนี้จะเห็นว่ายังมีข้อมูล Insert ลงไปใน Database ซึ่งแปลว่า Feature : Transaction นั้นไม่ทำงาน

เรามาทำให้ Feature : Transaction ทำงานกัน

วิธีนั้นเพิ่ม Code แค่บรรทัดเดียวครับดูได้จากตัว Code ด้านล่าง

1
2
3
4
5
6
7
8
@GetMapping("/test-error-transactional-success-case")
public String testCreateSuccessCase() {
AccountService accountService = new AccountService();
applicationContext.getAutowireCapableBeanFactory().autowireBean(accountService);
accountService = (AccountService) applicationContext.getAutowireCapableBeanFactory().applyBeanPostProcessorsAfterInitialization(accountService, null);
Account account = accountService.testTransactional("test-transactional-create-id-complete", "test-transactional-create-id-complete");
return account.getAccountId() + " : " + account.getAccountName();
}

โดยจะเห็นว่ามีการเพิ่ม Code ด้านล่างเข้าไป โดย Code ตรงนี้เป็นการบอกให้ตัว spring ทำ BeanPostProcessor ด้วย ซึ่งตัว @Transactional นั้นจะทำงานตอน BeanPostProcessor

1
accountService = (AccountService) applicationContext.getAutowireCapableBeanFactory().applyBeanPostProcessorsAfterInitialization(accountService, null);

คราวนี้มาทดลองกันว่ามันทำงานได้ไหมกัน

ซึ่งจากภาพจะเห็นว่าไม่มีการ insert row ที่มี id : test-transactional-create-id-complete ลงไปใน Database เลย ดังนั้นแสดงว่า Feature : Transaction ของเราทำงานได้แล้ว

สรุป

สำหรับตอนนี้เรามาทดลอง Inject Bean แบบ Manual กันแล้ว ซึ่งน่าจะมีประโยชน์สำหรับผู้ที่ต้องการทำอะไรที่ต้องทำแบบ Runtime ไม่ได้ทำตอน Start server สำหรับตอนนี้ก็ขอจบเพียงเท่านี้ครับ

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

เป็นครึ่งใจ version ที่เร็วกว่าเดิมขึ้นมานิดนึง ฟังแล้วได้อารมณ์แบบเออเอาไง ครึ่งใจที่เหลือเอายังไง