Skip to main content

ลองเล่นและเรียนรู้พื้นฐานขั้นต้นของ Spring Framework

**สำหรับใครที่ไม่เคยเรียนรู้ในด้านของ Java EE หรือ J2EE อาจจะมึนงงกับศัพท์หน่อยครับ

ทำไมต้อง Spring

Spring เป็น framework ที่นิยมมากในการนำไปสร้างระบบในระดับ enterprise ในเริ่มแรกที่ Spring เกิดมา มีจุดมุ่งหมายเพื่อที่จะมาแทนที่มาตรฐานของ Java อย่าง J2EE (Java 2 Enterprise Edition) ที่มันทั้งหน่วงทั้งอืดและยุ่งยาก โดยเฉพาะในส่วนของ EJB (Enterprise Java Bean) ที่ถือว่าเป็นฝันร้ายของนักพัฒนา ทำให้กูรูสาย Java ในช่วงนั้นถึงกับแนะนำว่า ถ้าจำเป็นที่ต้องพัฒนาระบบด้วย J2EE จงอย่าใช้ EJB ถึงขั้นถึงกับมีหนังสือแนะแนวทางการพัฒนาระบบ J2EE โดยไม่ใช้ EJB

อย่างไรก็ตามทาง Sun ผู้เป็นเจ้าของ Java ในสมัยนั้น ถึงกับต้องมาล้างระบบ J2EE ใหม่ในปี 2006 จัดการใน EJB ให้ใช้ง่ายขึ้น มีประสิทธิภาพมากขึ้น และมีการเปลี่ยนชื่อจาก J2EE เป็น Java EE (Java Enterprise Edition) เพื่อลบภาพอันเลวร้ายของเดิมให้หมด และได้มีการนำฟีเจอร์เด็ดๆ ของ open source framework หลายๆ ตัว อย่างเช่นแกนหลักของ Spring อย่าง IoC (Inversion of Control) หรือ OR Mapping (Object Relational Mapping) ที่เป็นที่นิยมอย่าง Hibernate แต่ก็ไม่สามารถหยุดยั้งความแรงของ Spring ลงได้ ทำให้ Spring นั้นแทบจะเป็นคุณสมบัติหลักที่นักพัฒนาสาย Java ต้องมีและตลาดแรงงานนักพัฒนาต้องการ

แต่ถ้าถามว่ารู้ Spring เพียงอย่างเดียวไม่ต้องรู้ Java EE ได้ไหม คำตอบคือ ควรรู้ทั้งสองอย่าง เพราะ Java EE นั้นเป็นมาตรฐานของ Java ที่พัฒนาให้ดีขึ้นเรื่อยๆ อย่างที่บอกในข้างต้น Java EE เริ่มมีอะไรหลายๆ อย่างที่คล้ายคลึงกับ Spring ดังนั้นถ้าเราเรียนรู้ Spring แล้ว ไม่ยากเลยที่เราจะเรียนรู้ Java EE เพราะทั้ง 2 อย่างนี้มีหลักการที่คล้ายกัน และเป็นการเปิดโอกาสให้เรามากขึ้นในการสมัครงานอีกด้วย

ถึงแม้เราจะบอกว่า Spring นั้นง่ายกว่า J2EE หรือ Java EE แต่เอาเข้าจริง Spring นั้นเป็น framework ที่ใหญ่มากและซับซ้อนพอสมควร และการเรียนรู้ก่อนที่จะสามารถนำไปใช้งานจริงได้นั้นใช้เวลาค่อนข้างนาน เพราะมันมีหลักการหลายๆ อย่างที่นอกเหนือจาก OOP (Object Oriented Programming) ซึ่งเราจะอธิบายในหัวข้อต่อไป

จุดมุ่งหมายของ Spring
Spring ถูกสร้างขึ้นมาเพื่อลดความซับซ้อน ความวุ่นวายในการพัฒนาระบบ เพื่อให้นักพัฒนามุ่งเน้นไปยัง business process ตรงๆ ส่วนจะลดความซับซ้อนยังไงสามารถอ่านเพิ่มเติมได้ในหัวข้อด้านล่าง ลดความวุ่นวายในการพัฒนาด้วย POJO (Plain Old Java Object)
ใน EJB เวอร์ชัน 1 หรือ 2 ของ J2EE (Old EJB) การจะสร้าง bean (business process หรือ OR mapping) ขึ้นมาสักตัวมีขั้นตอนวุ่นวายและซับซ้อนมาก ต้องสร้างไฟล์ *.java ขึ้นมาอย่างน้อย 3 ไฟล์สำหรับ home interface, implement interface และ client interface เพื่อสร้าง bean ขึ้นมาแค่ 1 ตัว และนี่ยังไม่รวมถึงการตั้งค่าที่ซับซ้อนใน xml 
ใน Spring ไม่ต้องสร้างไฟล์ *.java ให้มากมายแบบนั้น ใช้เพียงแค่ POJO ซึ่ง POJO เป็น class ธรรมดาทั่วๆ ไปที่ไม่มีความซับซ้อนใดๆ ทำให้นักพัฒนาสามารถมุ่งเน้นไปยังการเขียนกระบวนการทำงานของ process นั้นๆ (business process) โดยไม่ต้องไปยุ่งเกี่ยวกับสิ่งอื่นๆ 
ในปัจจุบันตั้งแต่ EJB เวอร์ชัน 3 ขึ้นไปได้นำแนวคิดของ POJO ไปใช้ทำให้ไม่ต้องวุ่นวายในการสร้าง bean เหมือนแต่ก่อน
  • ลดความผูกมัดของระบบด้วย IoC (Inversion of Control)

    IoC คือหลักการของการแก้ปัญหาในการพัฒนาระบบที่มีการผูกมัดในระดับ component  หรือ object จนทำให้การบำรุงรักษา (maintain) หรือการทดสอบ (test) เป็นไปได้ยาก

    IoC ถ้ากล่าวตามหลักของ Design Pattern จะพบว่าวิธีแก้ปัญหานี้มีด้วยกันอยู่ 4 แบบ แต่ใน Spring framework นั้นเลือก Dependency Injection มาเลือกใช้ในการ implement หลักการของ IoC

    Design Pattern (DP) เชื่อว่านักพัฒนาหลายๆ ท่านได้ยินชื่อนี้แล้วจะรู้สึกมีอาการเวียนหัวคล้ายจะเป็นลม DP นั้นเป็นรูปแบบหรือแนวทางการการแก้ปัญหาที่มักพบเจอบ่อยๆ ในการพัฒนาโปรแกรม โดย DP เป็นแนวทางการแก้ปัญหาที่ดีที่สุด (Best Practice) แต่จุดที่ทำให้ DP น่าเวียนหัวคือ ความยุ่งยากในการนำหลักการมาใช้จริง (หมายถึงการนำหลักการมา implement เองโดยไม่ได้เรียกใช้ผ่าน library อื่น)

    Dependency Injection คือหนึ่งใน DP ที่ช่วยแก้ปัญหาในการเรียกใช้ bean ซึ่งใน Old EJB กว่าเราจะเรียกใช้ bean ได้สักตัวต้องเขียน code ถึง 4-5 บรรทัด โดย Spring ได้นำหลักการของ DI เข้ามาช่วย ทำให้เราไม่ต้องเขียน code สำหรับการเรียกใช้ bean เพราะ Spring Context จะฉีด (inject) bean ตัวนั้นเข้ามายังจุดที่เราต้องการจะใช้ ทำให้นักพัฒนามุ่งเน้นไปยังการเขียน process ตรงๆ ทำการให้ maintain หรือทดสอบในส่วนนั้นเป็นไปง่ายขึ้น เพราะไม่ต้องมาคอยจัดการในส่วนของการเรียก bean หรือ component อื่น

    หมายเหตุ ใน EJB 3.1 ก็ได้นำ DI เข้ามาช่วยแล้วเช่นกัน ส่วนรายละเอียดเพิ่มเติมสามารถอ่านต่อได้ในหัวข้อของ DI
  • ลดสิ่งที่ต้องทำซ้ำๆ ด้วย AOP (Aspect Oriented Programming)

    ในการพัฒนาโปรแกรมเชื่อว่านักพัฒนาหลายๆ ท่านต้องเคยพบเจอบางสิ่งบางอย่างที่ต้องเขียนซ้ำๆ ในรูปแบบที่เหมือนเดิม เช่น การเปิด transaction เมื่อเริ่มต้นทำงาน การปิด transaction เมื่อจบการทำงานหรือเกิด exception หรือการบันทึกการทำงาน (log) ที่ต้องทำอยู่เสมอเมื่อมีการทำงานบางอย่าง การตรวจสอบสิทธิ (authenication) ก่อนการทำงาน เป็นต้น
    ...
    public void doSomething1(){
       //open transaction
       try{
           //do something
       }catch(SomeException se){
           //manage exception
           //close transaction
           throw se;
       }
       //close transaction
    }
    ....
    public void doSomething2(){
       //open transaction
       try{
           //do something
       }catch(SomeException se){
           //manage exception
           //close transaction
           throw se;
       }
       //close transaction
    }
        
    ถ้าหากเราจำเป็นที่ต้องทำอย่างที่กล่าวไว้ด้านบนใน 50 method แสดงว่าเราต้องเขียน code ที่มีหน้าตาเหมือนกัน แบบเดิมซ้ำๆ ถึง 50 ที่ ถ้าหากมีการเปลี่ยนแปลงบางอย่าง เราก็ต้องเปลี่ยน 50 ที่ ซึ่งปัญหานี้เราสามารถแก้ได้โดยการใช้ AOP

    AOP (Aspect Oreinted Programming) ไม่ใช่หลักการของการเขียนโปรแกรม (program paradigm) แบบ OOP (Object Oriented Programming) แต่เป็นส่วนเติมเต็มของ OOP เพื่อช่วยในสิ่งที่ OOP ไม่สามารถทำได้ อย่างปัญหาที่กล่าวไว้ด้านบน

    AOP จะทำให้เราสามารถประกาศจุด Joint Point (จุดที่สามารถแทรก code) ว่าสามารถที่จะแทรก code (code ที่เราจะแทรกเรียกว่า Aspect Code) ลงไปในจุด Point Cut ที่เรากำหนด (Point Cut คือการระบุว่าจะแทรก aspect code ลงไปใน method ของ class ไหน) โดยเราสามารถระบุได้ว่าจะแทรกเข้าไปในช่วงไหน (Advice) อย่างเช่น ก่อนเข้า หลังเข้า method หรือหลังเกิด exception ได้

    ใน Spring นั้นมี Spring AOP ที่ช่วยจัดการเรื่องของ AOP โดย Spring AOP นั้นไม่ใช่แกนหลักของ Spring แต่เป็นส่วนเติมเต็ม เราสามารถเลือกได้ว่าจะใช้หรือไม่ใช้ AOP สามารถอ่านรายละเอียดเพิ่มเติมได้ในหัวข้อของ Spring AOP
  • ลด Boilerplate code ในการเขียนให้น้อยลง

    Boilerplate code  คือ ส่วนใดส่วนหนึ่งของ code ที่จำเป็นต้องมีและมีรูปแบบคล้ายๆ กันหรือแทบจะไม่เปลี่ยนเลย

    จากนิยามด้านบน อ่านๆ แล้วอาจจะงง แต่ถ้าบอกว่า Boilerplate ใน Java คือ method จำพวก getter setter constructor หรือในบางครั้งอาจจะรวมไปถึง overide method พื้นฐานใน class java.lang.Object ที่สำคัญๆ อย่าง equals หรือ hashcode ซึ่งนักพัฒนา Java ต้องเขียน method เหล่านี้ในรูปแบบที่คล้ายๆ กันในทุกๆ class
    public class Pet {
       private String name;
       private Person owner;
    
       public Pet(String name, Person owner) {
           this.name = name;
           this.owner = owner;
       }
    
       public String getName() {
           return name;
       }
    
       public void setName(String name) {
           this.name = name;
       }
    
       public Person getOwner() {
           return owner;
       }
    
       public void setOwner(Person owner) {
           this.owner = owner;
       }
    }
    นอกจากนี้การ implement interface หรือ implement abstract class บางอย่างที่ไม่ว่าจะ implement กี่ครั้งก็มีลักษณะการ implement ที่เหมือนหรือคล้ายแบบเดิม อย่างนี้เราก็เรียกว่า boilerplate code เช่นกัน

    ซึ่งใน Spring นั้นจะมีตัวช่วยในการพัฒนาเพื่อลดในส่วนของ boilerplate code นี้ เพื่อที่จะได้มุ่งเน้นไปยังในส่วนของ business process โดยตรง
มุมมองโดยรวมของ Spring framework
ถึง Spring framework จะถูกออกแบบมาเพื่อพัฒนาระบบ Enterprise แต่ Spring ก็ไม่ได้ผูกมัดว่าจะรองรับแค่ระบบ Enterprise เหมือน J2EE หรือ Java EE แต่ Spring สามารถนำไปใช้พัฒนา Java application ทั่วๆ ไปได้ เพราะ Spring framework ถูกออกแบบมาในลักษณะ module ทำให้ผู้ใช้สามารถเลือก module ที่เหมาะสมให้กับระบบที่กำลังพัฒนาได้ ไม่จำเป็นที่จะต้องหยิบมาใช้ทั้งหมด

Spring framework มี module กว่า 20 module โดยบาง module นั้นถูก pack รวมกัน ถ้าจะใช้ต้องหยิบมาทั้งหมด โดย module ที่เป็นแกนหลักและใช้ประจำที่ควรรู้จักมีดังนี้

Core Container
Core Container เป็นแกนหลักของ Spring framework ไม่ว่าจะเรียกใช้ module ใดของ Spring ยังไงเราก็ต้องหยิบ Core Container มาใช้
  • Core (spring-core): เป็น module ที่เป็นแกนหลักของ Spring เป็นส่วนที่นำหลักการของ Dependency Injection มา implement ให้นักพัฒนาใช้ได้อย่างง่ายๆ
  • Bean (spring-bean): เป็นส่วนที่จัดการในเรื่องของการสร้าง bean (ฺBeanFactory) เป็นอีก module ที่นำหลักการของ Factory Pattern ที่ Spring implement มาให้นักพัฒนาเรียกใช้
  • Context (spring-context): เป็นส่วนที่ช่วยประสานระหว่าง module Core และ Bean เป็นตัวช่วยให้ผู้ใช้สามารถเข้าถึง bean ผ่านตามการตั้งค่าของผู้ใช้ โดยเราสามารถเรียกใช้ contex ได้ผ่านทาง interface ApplicationContext
  • SpEL (spring-expression): EL หรือ expression language ที่เราคุ้นเคยใน jsp เป็นตัวช่วยที่ทำให้เราเข้าถึง java object ได้บนหน้าเว็บ jsp แต่ SpEL (Spring Expression Language) นั้นถูกพัฒนาต่อยอดมาอีกทีนึง ช่วยในการเข้าถึง java object (bean) บนการตั้งค่า bean เพื่อช่วยในการอ้างอิงถึง bean อื่นๆ ในระบบของ spring container
สรุป Bean สร้าง object แล้ว Context เป็นตัวกลางให้ผู้ใช้เรียกใช้ โดยทั้งหมดจะถูกจัดการด้วย Core ที่เป็นแกนหลักของระบบ ส่วน SpEL ช่วยในการเข้าถึงระหว่าง bean

AOP and Instrumentation

แต่ก่อนส่วนนี้เป็นเพียง module เล็กๆ ที่นักพัฒนาเลือกได้ว่าจะใช้หรือไม่ใช้ แต่ว่าในตอนนี้กลับได้ถูกยกความสำคัญขึ้นมาให้เป็นสิ่งที่นักพัฒนาควรใช้ 
  • AOP (spring-aop) เป็น module ที่ Spring ได้ implement หลักการของ AOP มาให้นักพัฒนาเรียกใช้ได้
  • Aspects (spring-aspect) เป็น module ที่เปิดโอกาสให้เราใช้ AOP จาก library ของเจ้าอื่นมาทำงานร่วมกับ spring ได้ เช่น AspectJ
  • Instrument (spring-instrument) เป็น module ที่ใช้จัดการทรัพยากร (resource) ของ application ซึ่ง implement ตามมาตรฐานของ JMX (๋Java Management Extensions)
Data Access / Integration
เป็น module ที่ช่วยจัดการในการเชื่อมต่อฐานข้อมูล (database) ซึ่งรวมไปถึงฐานข้อมูลแบบ Relational Database และ NoSQL Database
  • JDBC (spring-jdbc) เป็น module ที่ช่วยให้นักพัฒนาเรียกใช้ฐานข้อมูลผ่าน JDBC ได้ง่ายขึ้นกว่า JDBC พื้นฐาน ช่วยให้ลดการเขียน code ในการติดต่อฐานข้อมูลผ่าน JDBC พื้นฐานได้มากกว่า 50%
  • ORM (spring-orm) เป็น module ที่ช่วยในการจัดการเปลี่ยนรูปแบบข้อมูลในฐานข้อมูลให้มาอยู่ในรูปแบบโครงสร้าง java-object (Object / Relational Mapping: ORMapping) ซึ่ง spring-orm นั้นรองรับใน ORMapping พื้นฐานของ Java อย่าง JPA หรือ JDO นอกจากนี้ยังรองรับ Hibernate ซึ่งเป็น ORMapping ยอดนิยมของนักพัฒนาอีกด้วย 
  • OXM (spring-oxm) เป็น module ที่ช่วยในการ mapping ข้อมูลระหว่าง xml และ java-object อย่าง library ภายนอกทั่วไปอย่าง JAXB, Castor, XMLBeans, JiBX หรือ XStream
  • Transaction (spting-tx) เป็น module ที่ช่วยในการจัดการ transaction ของฐานข้อมูลโดยเฉพาะ 
  • JMS :Java Messaging Services (spring-jms) คือ API หรือ library ที่ใช้ในการติดต่อกับ Message Server ซึ่ง Message Server มีความสามารถตามชื่อเป็น server ที่เอาไว้ใช้เก็บ message ประโยชน์ของมันจริงๆแล้ว คือการใช้ message server เป็นตัวสื่อสารกันระหว่าง software แบบ indirect (เป็นการสื่อสารผ่านตัวกลางโดยที่ software ไม่ได้คุยกันตรงๆ) ซึ่งช่วยลดการผูกมัดของระบบ โดย spring-jms จะช่วยให้นักพัฒนาเขียน code ติดต่อกับ JMS server ได้ง่ายขึ้น ลด code เพื่อการติดต่อ JMS server ลงถึง 50%
Web

อย่างที่กล่าวไปว่า Spring framework เกิดมาเพื่อเป็นทางเลือกให้กับนักพัฒนาให้สามารถพัฒนาระบบ Enterprise ได้ง่ายกว่า J2EE หรือ Java EE ซึ่งระบบ Enterprise ที่ว่าคงไม่พ้นในเรื่องของการพัฒนา web application ซึ่งการพัฒนา web application ด้วย Java จะเน้นหนักไปยังแนวทางการพัฒนาแบบ MVC (Model View Controller) โดย spring framework ก็มี module spring-web และ spring-webmvc ที่รองรับการพัฒนา web application แบบ mvc ที่ไม่น้อยหน้า java mvc framework ตัวอื่นที่นิยมกันอย่าง Vaadin, Struts 2, GWT หรือ JSF นอกจากนี้ยังรองรับการทำงานร่วมกับ web mvc ตัวอื่นอย่าง Struts 2 หรือ JSF ได้อีกด้วย

ลองเล่น Spring Framework

ก่อนที่จะลองเล่น spring framework เราควรจะต้องรู้จักกับ IoC ก่อนในบทความนี้ (http://www.plaumkamon.com/2017/09/inversion-of-control-dependency.html) แล้วถึงจะมาลองในส่วนของ core หลักของ spring framework นั้นก็คือ Core Container โดยเราจะสร้าง project จาก maven หรือ gradle ก็ได้ครับโดย setting dependency ของ project ตามนี้
Maven
<dependency>
   <groupid>org.springframework</groupid>
   <artifactid>spring-core</artifactid>
   <version>4.3.10.RELEASE</version>
</dependency>
<dependency>
   <groupid>org.springframework</groupid>
   <artifactid>spring-context</artifactid>
   <version>4.3.10.RELEASE</version>
</dependency>
Gradle
compile group: 'org.springframework', name: 'spring-core', version: '4.3.10.RELEASE'
compile group: 'org.springframework', name: 'spring-context', version: '4.3.10.RELEASE'
application ที่เราจะสร้างเป็นระบบของ log ในตอนเริ่มต้นเราจะสร้างระบบของเราที่ยังไม่มีการใช้ spring framework ก่อน โดยเราจะพัฒนาแบบ incremental เริ่มต้นด้วยสิ่งง่ายๆ ค่อยเพิ่มในส่วนที่ application ต้องการจนเป็น application ที่เสร็จสมบูรณ์

เริ่มต้นเราสร้าง application ด้วยที่รองรับระบบของ log แค่ 1 ตัวก่อน (ConsoleLog) โดยจะมี class LogEngine เป็นตัวเรียกใช้ระบบ log ของเรา
public class ConsoleLog{

   public void openLog(){
       //do something to open log
       System.out.println("ConsoleLog.openLog()");
   }

   public void log(String message){
       //do something to log
       System.out.printf("ConsoleLog.log(): [%s]\n", message);
   }

   public void closeLog(){
       //do something to close log
       System.out.println("ConsoleLog.closeLog()");
   }

}
public class LogEngine {

   private ConsoleLog log;

   public LogEngine(){
       log = new ConsoleLog();
   }

   public void log(String message){
       log.openLog();
       log.log(message);
       log.closeLog();
   }

}
ในตอนนี้เรารองรับระบบ log เฉพาะแค่ผ่าน Console เท่านั้น (ConsoleLog) แต่ถ้าหากเราต้องการเพิ่มระบบ log อย่างอื่นเข้าไปเราจะพบปัญหาทันที สมมุติเราจะเพิ่มระบบการ log ลงไฟล์ (FileLog) เข้าไปใน application
import java.io.*;

public class FileLog{

   private BufferedWriter logWriter;
   private String logPath;

   public FileLog(String logPath){
       this.logPath = logPath;
   }

   public void openLog() throws IOException{
       System.out.println("FileLog.openLog()");
       try {
           logWriter = new BufferedWriter(new FileWriter(logPath));
       }catch (IOException ex){
           System.err.printf("Can't initial log file in this path [%s] \n", logPath);
           throw ex;
       }
   }

   public void log(String message)throws IOException{
       System.out.printf("FileLog.log(): %s\n", message);
       logWriter.write(message);
       logWriter.newLine();
   }

   public void closeLog() throws IOException{
       System.out.println("FileLog.closeLog()");
       logWriter.flush();
       logWriter.close();
   }
}
เมื่อเราจะเรียกใช้ FileLog ใน LogEngine จะมีปัญหา เพราะตอนนี้ใน LogEngine ของเราได้ทำการ hardcode สำหรับการสร้าง object ระบบ log ใน console (ConsoleLog) ดังนั้นถ้าต้องการที่จะเพิ่มระบบ log ตัวใหม่เข้าไปใน LogEngine เราต้องมีการแก้ไข code ของ LogEngine ทุกครั้ง
import java.io.IOException;

public class LogEngine {

   private ConsoleLog consoleLog;
   private FileLog fileLog;

   public LogEngine(){
       consoleLog = new ConsoleLog();
   }

   public LogEngine(String logPath){
       fileLog = new FileLog(logPath);
   }

   public void log(String message) throws IOException{
       if(consoleLog != null) {
           consoleLog.openLog();
           consoleLog.log(message);
           consoleLog.closeLog();
       }else if(fileLog != null){
           fileLog.openLog();
           fileLog.log(message);
           fileLog.closeLog();
       }
   }

}
ที่เป็นเช่นนี้เพราะการเรียกใช้ object ของของระบบ log ใน LogEngine นั้นมีการผูกมัดเกินไป ทำให้เมื่อมีการขยับขยายของระบบจึงไม่สะดวก ดังนั้นเราต้องเอา abstraction มาเป็นตัวขั้นกลางระหว่าง LogEngine และระบบ log ต่างๆ ซึ่งเราจะปรับเปลี่ยน application ของเราตาม diagram ด้านล่างนี้

public interface ILog {
   void openLog() throws Exception;
   void log(String message) throws Exception;
   void closeLog() throws Exception;
}
เมื่อเปลี่ยนมาใช้ interface ILog เป็นตัวกลางในการเรียกระบบ log ชนิดอื่นๆ การเรียกใช้ใน LogEngine ก็จะเปลี่ยนไปด้วย
public class LogEngine {

   private ILog log;

   public LogEngine() {}

   public void setLog(ILog log){
      this.log = log;
   }

   public void log(String message) throws Exception{
       log.openLog();
       log.log(message);
       log.closeLog();
   }

}
โดยหลักการคร่าวๆ ของ DI จะไม่มีการสร้าง object ของ class ที่จะถูกเรียกใช้ (dependency object) ขึ้นมาเอง เพื่อที่จะไม่ให้เกิดข้อผูกมัด (dependency) แต่จะมีการฉีด (inject) เข้ามาจากระบบภายนอกแทน ถ้าหากว่าเราไม่ได้ใช้ spring framework เราจะต้องเป็นผู้ควบคุมการสร้าง dependency object ขึ้นมาเอง และยังต้องเป็นผู้ฉีด dependency ด้วยตัวเอง

***หมายเหตุ dependency object ใน spring framework จะถูกเรียกว่า Spring Bean หรือย่อสั้นๆ ว่า bean

ขั้นตอนการพัฒนา application ด้วย spring framework

1. การตั้งค่า Spring Bean
2. สร้าง Spring Container
3. เรียกใช้ Spring Bean จาก Spring Container

หมายเหตุ spring container คือ ApplicationContext ที่เราเรียกใช้กันใน code

1. การตั้งค่า Spring Bean

การตั้งค่า spring bean คือการตั้งค่าเพื่อเตรียมความพร้อมให้กับเหล่า dependency object ที่เราจะใช้ เพื่อให้ spring container อ่านการตั้งค่าเหล่านี้แล้วนำไปสร้าง bean ให้เราใช้ โดยการตั้งค่าทำได้ 3 วิธี
- ตั้งค่าผ่าน XML ไฟล์
- ตั้งค่าผ่าน Java Annotations
- ตั้งค่าผ่าน Java Source Code
ในบทความนี้เราจะกล่าวถึงการตั้งค่าผ่าน XML และ Java Annotation เท่านั้น

หมายเหตุการตั้งค่าของ spring bean เป็นการตั้งค่าของระบบภายในที่จะไม่เปิดให้แก้ไขค่าจากภายนอกหรือแก้ไขแบบ runtime ได้ ดังนั้นจึงไม่ต้องแปลกว่าทำไมการตั้งค่าของ spring bean ถึงมักจะถูกวางไว้ภายใน project ภายใน

ตั้งค่า Spring Bean ผ่าน XML ไฟล์

การตั้งค่าด้วยวิธีนี้ไม่ค่อยเป็นที่นิยมกันแล้ว เพราะเราสามารถใช้ Java Annotation ซึ่งจะเป็นการตั้งค่าที่อยู่ใน source code ของ java แต่ก็ควรศึกษาไว้เผื่อมีโอกาศที่ต้องไปแก้ application ที่ยังใช้ spring framework ในเวอร์ชันเก่าๆ อยู่ โดยหน้าตาการตั้งค่าในรูปแบบ XML นั้นมีลักษณะตาม XML ด้านล่าง
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemalocation="http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans.xsd">

  <!-- Console LogEngine -->
  <bean class="ConsoleLog" id="consoleLog">
    <bean class="LogEngine" id="consoleLogEngine">
      <property name="log" ref="consoleLog">
      </property>
    </bean>

    <!-- File LogEngine -->
    <bean class="FileLog" id="fileLog">
      <constructor-arg type="java.lang.String" value="c:\temp\temp.txt">
      </constructor-arg>
    </bean>
    <bean class="LogEngine" id="fileLogEngine">
      <property name="log" ref="fileLog">
      </property>
    </bean>
  </bean>
</beans>
เนื่องจากว่าระบบ log เรานั้นมีระบบอยู่ 2 ระบบ คือ ConsoleLog และ FileLog ดังนั้นในการตั้งค่า bean ของเราจะตั้งค่าเพื่อรองรับ log ทั้ง 2 ระบบ โดยการตั้งค่านี้เราจะเริ่มต้นด้วยการประกาศ xml namespace ที่เราจะใช้ก่อน ในกรณีนี้เราใช้เพียงแค่ spring-bean ดังนั้น xml namespace ของเราจึงมีแค่ในส่วนของ spring-bean
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemalocation="http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans.xsd">

   <!--สร้าง bean ตรงนี้ -->

</beans>
หลังจากนั้นเราจะเริ่มสร้าง bean ของ class LogEngine โดยเราจะเริ่มตั้งค่าจาก bean เล็กๆ (FileLog) ไปหา bean ใหญ่ (LogEngine) ดังภาพด้านล่าง

โครงสร้าง XML สำหรับการสร้าง bean
<bean id="ชื่อของ bean" class="ระบุ class ที่จะสร้าง">
   <constructor-arg index="ลำดับตำแหน่งของ argument" type="type ของ argument" value="ค่าของ argument ในรูปแบบ String" ref="อ้างอิง bean อื่น"/>
   <property name="ชื่อของ property ใน class" value="ค่าของ property ในรูปแบบ String" ref="อ้างอิง bean อื่น"/>
</bean>
หลักการตั้งค่า bean ผ่าน XML นั้นก็เหมือนกับการสร้าง object ของ class นึงขึ้นมา ที่เราต้องระบุชื่อของ object ที่เราจะสร้าง แต่ถ้า object นั้นมี constructor เราก็ต้องส่ง argument เข้าไปตามที่ constructor นั้นระบุ หรือ object นั้นมี property ที่จำเป็นต้องตั้งค่า ก็สามารถตั้งค่าผ่าน xml ได้ ตามโครงสร้าง XML ด้านบน

หลักจากที่เรารู้จักโครงสร้าง XML ของ bean แล้ว เรามาลองมาสร้าง XML bean ของ application ที่เราจะสร้าง โดยเราต้องสร้าง bean ของ FileLog ก่อน เพื่อที่จะใช้ bean ของ FileLog สำหรับฉีดเข้าสู่ LogEngine อีกทีหนึ่ง
<bean id="fileLog" class="FileLog">
   <!--ตั้งค่า constructor หรือ properties ของ class ตรงนี้-->
</bean>
เริ่มต้นด้วยการสร้าง bean ของ FileLog ในการสร้าง bean เราจะตั้งชื่อให้กับ bean ที่เราสร้างผ่าน attribute id และระบุ class ที่เราจะสร้างผ่าน attribute class
<bean id="fileLog" class="FileLog">
   <constructor-arg index="0" type="java.lang.String" value="c:\temp\temp.txt"/>
</bean>
เนื่องจากว่า class ของ FileLog นั้นมีการรับ argument ใน constructor ด้วย ดังนั้นในการสร้าง bean ของ FileLog เราต้องมีการตั้งค่าของ argument ใน constructor ด้วย โดยเราจะตั้งผ่าน element constructor-arg ซึ่ง index จะเป็นการระบุลำดับของ argument ใน constructor ถ้ามีแค่ 1 ตัวสามารถละไว้ได้
<bean id="fileLogEngine" class="LogEngine">
    <property name="log" ref="fileLog"/>
</bean>
จากนั้นจึงสร้าง bean ของ class LogEngine ซึ่ง bean ตัวนี้เราตั้งชื่อว่า fileLogEngine และมีการตั้งค่า property ของ bean โดยเรามีการอ้างอิงค่าของ bean ที่เราสร้างขึ้นมาไว้แล้วอย่าง bean ของ fileLog เพื่อนำ bean ของ fileLog นำมาฉีดเข้าสู่ bean ของ fileLogEngine ที่เรากำลังสร้าง
<!-- File LogEngine -->
<bean id="fileLog" class="com.sharp.di.sample.log.FileLog">
   <constructor-arg type="java.lang.String" value="c:\temp\temp.txt"/>
</bean>
<bean id="fileLogEngine" class="com.sharp.di.sample.log.LogEngine">
   <property name="log" ref="fileLog"/>
</bean>
เมื่อเราตั้งค่า bean (fileLogEngine) เสร็จ XML จะมีหน้าตาออกมาดังด้านบนนี้
<!-- Console LogEngine -->
<bean id="consoleLog" class="com.sharp.di.sample.log.ConsoleLog"/>
<bean id="consoleLogEngine" class="com.sharp.di.sample.log.LogEngine">
   <property name="log" ref="consoleLog"/>
</bean>
จากนั้นก็ใช้หลักการเดียวกัน สร้าง bean ของ LogEngine ที่ใช้ระบบ ConsoleLog ขึ้นมา หน้าตา XML จะออกมาเป็นแบบด้านบน

โดยไฟล์ XML สำหรับการตั้งค่า bean เราจะตั้งชื่อเป็นอะไรก็ได้ แต่ควรใส่ไฟล์การตั้งค่านี้ใน directory resource เพื่อให้ง่ายกับการอ้างอิงสำหรับให้ spring container อ่านการตั้งค่า

การตั้งค่าผ่าน Java Annotation

การตั้งค่าแบบนี้ หากดูดีๆ ก็จะคล้ายๆ การตั้งค่าผ่าน XML แต่การเรียกใช้จะง่ายกว่าเพราะเราไม่ต้องระบุ path ของ xml ในการโหลดการตั้งค่า แค่เพียงระบุ class ที่ใช้ในการตั้งค่าเท่านั้น ทำให้วิธีนี้จึงเป็นที่นิยมในปัจจุบัน แต่ก่อนที่จะไปเรียนรู้การตั้งค่า เราควรรู้จัก annotation ที่สำคัญในการตั้งค่า bean กันก่อน


  • @Bean (org.springframework.context.annotation.Bean) เป็น annotation ไว้สำหรับนำไปแปะที่บนหัว method ที่เราสร้างขึ้นไว้เพื่อสำหรับการตั้งค่าใน bean โดย method ที่สร้างไว้สำหรับตั้งค่า bean จะมีลักษณะหน้าตาเหมือน method ทั่วไป เพียงแต่มันจะ return ค่ากลับไป class ของ bean ที่เราต้องการจะสร้างขึ้นมา

    โดย @Bean นั้นมี default property ที่ชื่อว่า name ไว้สำหรับการตั้งชื่อให้กับ bean เพื่อไว้ใช้อ้างถึงเวลาเรียกใช้ bean ผ่าน ApplicationContext
  • @Qualifier (org.springframework.beans.factory.annotation.Qualifier) เป็น annotation ไว้สำหรับมาร์คชื่อของ bean คล้ายๆ กับ default property name ใน @Bean แต่จะต่างกันตรงที่ default property name ใน @Bean จะไว้ให้ spring container อ้างถึง แต่ @Qulifier นั้นมีไว้ให้ bean อ้างถึงกันเอง

    อาจจะยังไม่เห็นภาพ รอดูตัวอย่างจริงใน code
  • @Component (org.springframework.stereotype.Component) annotation ตัวนี้สามารถนำไปแปะไว้บน class ที่ต้องการจะให้ class นั้นเป็น bean ที่อยู่บนระบบ spring container ทำให้ผู้ใช้สามารถสั่ง ApplicationContext ให้สร้าง bean ของ class นี้ได้โดยตรงด้วยการระบุถึงชื่อ class ที่เราจะสร้าง bean
  • @Autowired (org.springframework.beans.factory.annotation.Autowired) annotation ตัวนี้เอาไว้แปะตรงจุดที่เราต้องการให้ spring container ฉีด bean เข้าไป โดย spring container จะเป็นผู้หาเองว่า bean ตัวไหนควรฉีดเข้าไป หรือจะใช้ @Qualifier เข้ามาช่วยในการระบุเจาะจงว่าจะใช้ bean ตัวไหนฉีดเข้าไป
  • @Configuration (org.springframework.context.annotation.Configuration) เป็น annotation ไว้แปะที่บนหัว class ที่เราจะไว้ใช้ทำการตั้งค่า bean เพื่อเป็นการบอก spring container ว่า class นี้มีการตั้งค่าของ bean
  • @ComponentScan (org.springframework.context.annotation.ComponentScan) เป็น annotation ที่ใช้กับ @Configuration ไว้ระบุ class หรือ package ที่มี class ที่มี @Component แปะอยู่ โดย @ComponentScan นั้นจะเป็นตัวบอกให้ spring container ต้องไปสแกนหา bean จาก package ไหน หรือ class ไหนในระบบ
ในตัวอย่างนี้เราจะใช้เพียงแค่ @Configuration, @Bean และ @Qualifier เท่านั้น ส่วนตัวอื่นๆ อาจจะได้เห็นในตอนอื่นๆ
import org.springframework.context.annotation.Configuration;

@Configuration
public class AnnotationConfig {
    //ประกาศ bean ตรงนี้
}
เริ่มต้นสร้าง class หลังจากนั้นก็แปะ @Configuration ลงบน class ที่เราจะไว้ใช้เป็นการตั้งค่าของ bean จากนั้นก็สร้าง method สำหรับการตั้งค่าของ bean

เช่นเดิมจากการตั้งค่า bean ควรสร้างจาก bean เล็กๆ ไปยัง bean ที่ใหญ่กว่า เราจะเริ่มสร้างจาก bean ของ log ก่อนค่อยไปสร้าง bean ของ logEngine เพราะ bean ของ logEngine นั้นมีการเรียกใช้ bean ของ log
@Bean
public String getLogPath(){
   return "c:\\temp\\temp.txt";
}
เริ่มต้นด้วยการสร้าง bean ของ string ที่เก็บ path ของ log ก่อน เพื่อเตรียมไว้ให้กับ bean ของ fileLog
@Bean
@Qualifier("fileLog")
public FileLog getFileLog(String logPath){
   return new FileLog(logPath);
}
จากนั้นเราก็สร้าง bean ของ class FileLog โดยที่ bean FileLog นี้จะมีการรับ string ที่ระบุ path ของ log มาจากอีก bean นึงที่เราสร้างไว้ก่อนหน้านี้

โดยเรามีการตั้งชื่อให้ bean ตัวนี้ว่า “fileLog” ผ่าน @Qualifier ซึ่งจะไว้ใช้สำหรับให้ bean ตัวอื่นอ้างอิงถึงได้
@Bean("fileLogEngine")
public LogEngine getFileLogEngine(@Qualifier("fileLog") ILog fileLog){
   LogEngine log = new LogEngine();
   log.setLog(fileLog);
   return log;
}
ใน bean ของ fileLogEngine นี้เรามีการเรียกใช้ bean ตัวอื่นจาก fileLog (@Qualifier("fileLog")) โดยเราจะระบุ bean ที่จะใช้ตรงหน้า parameter ที่เราจะใช้ตรงๆ เลย

ที่เราต้องมีการระบุชื่อผ่าน @Qualifier เป็นเพราะว่าเราจะมี bean ของ logEngine อีกตัวนั่นก็คือ consoleLogEngine ซึ่งจะมีการรับ argument เป็น ILog เหมือนกัน จึงทำให้มี bean ของ interface ILog ขึ้นมาใน spring container ทั้งหมด 2 ตัว (ConsoleLog และ FileLog) ทำให้ spring container สับสนไม่รู้ว่าจะต้องเอา bean ของตัวไหนมาฉีดเข้า argument ILog ดังนั้นจึงต้องมีการตั้งชื่อผ่าน @Qualifier

นอกจากนี้เรายังตั้งชื่อให้ bean ผ่าน @Bean("fileLogEngine") เพราะการตั้งชื่อตรงนี้เราไว้ใช้อ้างอิงในการเรียกผ่าน ApplicationContext
import com.sharp.di.sample.log.ConsoleLog;
import com.sharp.di.sample.log.FileLog;
import com.sharp.di.sample.log.ILog;
import com.sharp.di.sample.log.LogEngine;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AnnotationConfig {
   @Bean
   public String getLogPath(){
       return "c:\\temp\\temp.txt";
   }

   @Bean
   @Qualifier("fileLog")
   public FileLog getFileLog(String logPath){
       return new FileLog(logPath);
   }

   @Bean("fileLogEngine")
   public LogEngine getFileLogEngine(@Qualifier("fileLog") ILog fileLog){
       LogEngine log = new LogEngine();
       log.setLog(fileLog);
       return log;
   }
  
   @Bean
   @Qualifier("consoleLog")
   public ConsoleLog getConsoleLog(){
       return new ConsoleLog();
   }

   @Bean("consoleLogEngine")
   public LogEngine getConsoleLogEngine(@Qualifier("consoleLog") ILog consoleLog){
       LogEngine log = new LogEngine();
       log.setLog(consoleLog);
       return log;
   }
}
และนี่คือการตั้งค่า bean ผ่าน annotation ของทั้ง fileLogEngine และ consoleLogEngine ตาม code ด้านบน

2. การสร้าง Spring Container

การสร้าง Spring Container เราจะสร้างผ่าน interface ของ ApplicationContext โดยการสร้าง ApplicationContext นั้นจะแตกต่างกันไปตามการตั้งค่าของ bean

- ClassPathXmlApplicationContext (org.springframework.context.support.ClassPathXmlApplicationContext) เป็น ApplicationContext สำหรับการตั้งค่า bean ผ่าน XML
ApplicationContext context =
       new ClassPathXmlApplicationContext("application.xml");
- AnnotationConfigApplicationContext (org.springframework.context.annotation.AnnotationConfigApplicationContext) เป็น ApplicationContext สำหรับการตั้งค่า bean ผ่าน annotation
ApplicationContext context =
       new AnnotationConfigApplicationContext(AnnotationConfig.class);
3. การเรียกใช้ spring bean จาก ApplicationContext
    LogEngine log = context.getBean("consoleLogEngine", LogEngine.class);
    log.log("Hello Spring");
เวลาเรียกก็แค่ระบุชื่อของ bean ที่เราตั้งไว้ในตอนตั้งค่า พร้อมกับระบุ type ของ bean ที่เราจะเรียกเท่านั้น

สรุป

จะเห็นได้ว่าการจะนำ Spring Framework มาใช้งาน จำเป็นต้องมีการออกแบบระบบและโครงสร้างของ application เราให้สอดคล้องกับ Spring Framework ด้วย ซึ่งก็ถือเป็นข้อดีในการใช้ Spring Framework เพราะเป็นเหมือนข้อตกลงที่ช่วยให้ทีมพัฒนา พัฒนา application เป็นไปในทางเดียวกัน

และนี่คือพื้นฐานเบื้องต้นของ spring framework ในส่วนของ core container สำหรับ source code สามารถไปโหลดมาดูได้ที่ https://github.com/MrRiceCooker/sample-spring

Comments

  1. It was such a useful blog! The narration of the facts and figures is very nice. In struggling to hire my remote developers , I discovered Eiliana.com, an emerging freelancing portal, and I found they have a superb talent pool. I hired a few experts from there, and I'm blessed.

    ReplyDelete

Post a Comment

Popular posts from this blog

ลองเล่น Lambda Expression ฟีเจอร์เด่นใน Java 8

ประวัติความเป็นมาของ Lambda expression Lambda expression ไม่ใช่สิ่งแปลกใหม่ในวงการ ภาษาโปรแกรม ( Programming Language ) เพราะ lambda มันเป็นแกนหลักของ การเขียนโปรแกรมเชิงฟังก์ชัน ( Functional Programming ) ซึ่งมีอายุมานานมากแล้ว แต่ Java เพิ่งนำเอาคุณสมบัตินี้เอามาใส่ลงในเวอร์ชัน 8 หากจะกล่าวถึงที่มาของ lambda คงต้องไปดูที่ถึงที่มาของ lambda calculus ซึ่งถูกสร้างขึ้นมาตั้งแต่ปี 1930 โดยนักคณิตศาสตร์ชาวอเมริกัน  Alonzo Church  เพื่อใช้ในการแก้โจทย์ปัญหาทางคณิตศาสตร์ที่มีความซับซ้อน ในบางครั้งสมการทางคณิตศาสตร์ที่ยาวไปอาจจะทำให้เกิดความซับซ้อนโดยใช่เหตุ lambda calculus จะทำการยุบบางส่วนของสมการนั้นออกมาเป็นฟังก์ชันย่อยๆ เพื่อทำให้สมการนั้นเข้าใจง่ายขึ้น ต่อมาหลักการของ lambda calculus ได้ถูกนำไปใช้ใน Turing Machine ซึ่งเป็นแบบจำลองในอุดมคติของ Alan Turing  ที่ต่อมากลายเป็นต้นแบบที่ถูกนำไปใช้ในการผลิต  Von Neumann Machine  ซึ่ง Von Neumann Machine ตัวนี้ได้กลายเป็นต้นแบบของคอมพิวเตอร์เครื่องแรกของโลกในเวลาต่อมา ท้ายที่สุดแนวคิดของ lambda calculus ก็ถูกนำมาแปลงเป็นภาษาโปรแกรมท

ลองเล่น SonarQube คลื่นโซนาร์ช่วยตรวจสอบคุณภาพของ code

SonarQube  คือเครื่องมือช่วยตรวจสอบคุณภาพของ source code ช่วยหาข้อบกพร่องใน source code ไม่ว่าจะเป็น Bug ที่น่าจะเกิดขึ้น ช่องโหว่ทางด้านความปลอดภัยหรือกลิ่นไม่ดีใน source code ของเรา (Code Smell) และ ช่วยตรวจสอบเราเขียน code ทดสอบครอบคลุมหรือดีแล้วยังยัง (code coverage) Code Smell ไม่ได้ใช้วัดว่า source code นี้สามารถทำงานได้ถูกต้อง มี bug หรือช่องโหว่หรือไม่ แต่ Code Smell ใช้วัดถึงคุณภาพของการออกแบบ เพื่อตรวจสอบว่า source code ที่เป็นอยู่ในปัจจุบันจะสามารถต่อเติม แก้ไขหรือทดสอบได้ง่ายหรือไม่ โดยหลักเกณฑ์ที่นำมาใช้วัดในส่วนของ Code Smell คือ ความซ้ำซ้อนของ code มี code แบบเดียวกันไปซ้ำกันในไฟล์ไหนบ้าง ตรวจสอบเงื่อนไขใน if ให้ ว่าเงื่อนไขตรงนี้มันมีโอกาสเป็นไปได้ไหม เพราะบางทีเงื่อนไขที่เราเขียนขึ้นมาเพื่อดักไว้ในบางครั้งมันแทบจะไม่มีโอกาสที่เวลามันทำงานแล้วเข้าเงื่อนไขในส่วนนั้น เป็นต้น สามารถไปอ่านรายละเอียดเพิ่มเติมได้ที่นี่ http://www.somkiat.cc/code-smell-internal-class/ นอกจาก SonarQube จะสามารถบอกถึงคุณภาพของ source code เราได้แล้ว ยังสามารถใช้ในการแจกแจงงานให