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
| 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
| @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
| @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
| @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 ที่เร็วกว่าเดิมขึ้นมานิดนึง ฟังแล้วได้อารมณ์แบบเออเอาไง ครึ่งใจที่เหลือเอายังไง