10 ข้อผิดพลาดเกี่ยวกับ Spring Framework ที่พบบ่อยที่สุด
เผยแพร่แล้ว: 2022-03-11Spring เป็นหนึ่งในเฟรมเวิร์ก Java ที่ได้รับความนิยมมากที่สุดและเป็นสัตว์ร้ายที่เชื่อง แม้ว่าแนวคิดพื้นฐานจะเข้าใจได้ง่าย แต่การเป็นนักพัฒนา Spring ที่แข็งแกร่งนั้นต้องใช้เวลาและความพยายาม
ในบทความนี้ เราจะพูดถึงข้อผิดพลาดทั่วไปบางประการใน Spring โดยเน้นไปที่เว็บแอปพลิเคชันและ Spring Boot โดยเฉพาะ ตามที่เว็บไซต์ของ Spring Boot ระบุไว้ Spring Boot ใช้ มุมมอง ที่มีความคิดเห็นว่าควรสร้างแอปพลิเคชันที่พร้อมสำหรับการผลิตอย่างไร ดังนั้นบทความนี้จะพยายามเลียนแบบมุมมองนั้นและให้ภาพรวมของเคล็ดลับบางประการ ซึ่งจะรวมเข้ากับการพัฒนาเว็บแอปพลิเคชันมาตรฐานของ Spring Boot ได้เป็นอย่างดี
ในกรณีที่คุณไม่คุ้นเคยกับ Spring Boot มากนัก แต่ยังต้องการลองใช้บางสิ่งที่กล่าวถึง ฉันได้สร้างที่เก็บ GitHub ที่มาพร้อมกับบทความนี้ หากคุณรู้สึกว่าหลงทางในบทความใด ๆ เราขอแนะนำให้คุณโคลนที่เก็บและลองใช้โค้ดบนเครื่องของคุณ
ข้อผิดพลาดทั่วไป #1: ระดับต่ำเกินไป
เรากำลังจัดการกับข้อผิดพลาดทั่วไปนี้ เนื่องจากกลุ่มอาการ "ไม่ได้ประดิษฐ์ขึ้นที่นี่" เป็นเรื่องปกติธรรมดาในโลกของการพัฒนาซอฟต์แวร์ อาการต่างๆ เช่น การเขียนโค้ดที่ใช้กันทั่วไปเป็นประจำ และนักพัฒนาหลายๆ คนดูเหมือนจะประสบปัญหานี้
ในขณะที่การทำความเข้าใจภายในของไลบรารีหนึ่งๆ และการนำไปใช้นั้นส่วนใหญ่ดีและจำเป็น (และอาจเป็นกระบวนการเรียนรู้ที่ยอดเยี่ยมเช่นกัน) แต่ก็ส่งผลเสียต่อการพัฒนาของคุณในฐานะวิศวกรซอฟต์แวร์ที่ต้องจัดการกับการใช้งานระดับต่ำแบบเดียวกันอย่างต่อเนื่อง รายละเอียด. มีเหตุผลว่าทำไมสิ่งที่เป็นนามธรรมและกรอบงาน เช่น Spring มีอยู่ ซึ่งแยกคุณออกจากการทำงานด้วยตนเองที่ทำซ้ำๆ ได้อย่างแม่นยำ และช่วยให้คุณจดจ่อกับรายละเอียดในระดับที่สูงขึ้น เช่น วัตถุโดเมนและตรรกะทางธุรกิจของคุณ
ดังนั้นจงยอมรับสิ่งที่เป็นนามธรรม - ครั้งต่อไปที่คุณต้องเผชิญกับปัญหาเฉพาะ ให้ค้นหาอย่างรวดเร็วก่อนและพิจารณาว่าห้องสมุดที่แก้ปัญหานั้นได้รวมเข้ากับ Spring แล้วหรือไม่ ทุกวันนี้ มีโอกาสที่คุณจะได้พบกับโซลูชันที่เหมาะสมอยู่แล้ว เพื่อเป็นตัวอย่างของห้องสมุดที่มีประโยชน์ ฉันจะใช้คำอธิบายประกอบของ Project Lombok ในตัวอย่างสำหรับส่วนที่เหลือของบทความนี้ ลอมบอกถูกใช้เป็นเครื่องสร้างโค้ดสำเร็จรูป และนักพัฒนาที่ขี้เกียจในตัวคุณ หวังว่าจะไม่มีปัญหาในการทำความคุ้นเคยกับห้องสมุด ตัวอย่างเช่น ลองดูว่า “Java bean มาตรฐาน” เป็นอย่างไรในลอมบอก:
@Getter @Setter @NoArgsConstructor public class Bean implements Serializable { int firstBeanProperty; String secondBeanProperty; }
อย่างที่คุณอาจจินตนาการได้ โค้ดด้านบนคอมไพล์เป็น:
public class Bean implements Serializable { private int firstBeanProperty; private String secondBeanProperty; public int getFirstBeanProperty() { return this.firstBeanProperty; } public String getSecondBeanProperty() { return this.secondBeanProperty; } public void setFirstBeanProperty(int firstBeanProperty) { this.firstBeanProperty = firstBeanProperty; } public void setSecondBeanProperty(String secondBeanProperty) { this.secondBeanProperty = secondBeanProperty; } public Bean() { } }
อย่างไรก็ตาม โปรดทราบว่าคุณมักจะต้องติดตั้งปลั๊กอิน ในกรณีที่คุณต้องการใช้ลอมบอกกับ IDE ของคุณ ปลั๊กอินเวอร์ชันของ IntelliJ IDEA สามารถพบได้ที่นี่
ข้อผิดพลาดทั่วไป #2: 'รั่ว' ภายใน
การเปิดเผยโครงสร้างภายในของคุณไม่ใช่ความคิดที่ดี เพราะจะทำให้เกิดความไม่ยืดหยุ่นในการออกแบบบริการ และเป็นผลให้ส่งเสริมแนวปฏิบัติในการเขียนโค้ดที่ไม่ดี ภายใน 'รั่ว' นั้นแสดงออกโดยการทำให้โครงสร้างฐานข้อมูลสามารถเข้าถึงได้จากจุดปลาย API บางตัว ตัวอย่างเช่น สมมติว่า POJO ต่อไปนี้ (“Plain Old Java Object”) แสดงถึงตารางในฐานข้อมูลของคุณ:
@Entity @NoArgsConstructor @Getter public class TopTalentEntity { @Id @GeneratedValue private Integer id; @Column private String name; public TopTalentEntity(String name) { this.name = name; } }
สมมติว่ามีปลายทางซึ่งจำเป็นต้องเข้าถึงข้อมูล TopTalentEntity
แม้จะเป็นการดึงดูดให้ส่งคืนอินสแตนซ์ TopTalentEntity
โซลูชันที่ยืดหยุ่นกว่าจะสร้างคลาสใหม่เพื่อแสดงข้อมูล TopTalentEntity
บนจุดปลาย API:
@AllArgsConstructor @NoArgsConstructor @Getter public class TopTalentData { private String name; }
ด้วยวิธีนี้ การเปลี่ยนแปลงแบ็คเอนด์ฐานข้อมูลของคุณจะไม่ต้องมีการเปลี่ยนแปลงเพิ่มเติมในชั้นบริการ พิจารณาว่าจะเกิดอะไรขึ้นในกรณีที่เพิ่มฟิลด์ 'รหัสผ่าน' ลงใน TopTalentEntity
เพื่อจัดเก็บแฮชรหัสผ่านของผู้ใช้ของคุณในฐานข้อมูล - หากไม่มีตัวเชื่อมต่อเช่น TopTalentData
การลืมเปลี่ยนส่วนหน้าของบริการจะเปิดเผยข้อมูลลับที่ไม่พึงปรารถนาโดยไม่ได้ตั้งใจ !
ข้อผิดพลาดทั่วไป #3: ขาดการแยกจากความกังวล
เมื่อแอปพลิเคชันของคุณเติบโตขึ้น การจัดระเบียบโค้ดก็เริ่มมีความสำคัญมากขึ้นเรื่อยๆ ที่น่าแปลกก็คือ หลักการทางวิศวกรรมซอฟต์แวร์ที่ดีส่วนใหญ่เริ่มเสื่อมลงทีละน้อย โดยเฉพาะอย่างยิ่งในกรณีที่ไม่ได้คำนึงถึงการออกแบบสถาปัตยกรรมแอปพลิเคชันมากนัก หนึ่งในข้อผิดพลาดที่พบบ่อยที่สุดที่นักพัฒนาซอฟต์แวร์มักจะยอมจำนนคือการผสมข้อกังวลของโค้ด และมันง่ายมากที่จะทำ!
สิ่งที่มักจะแบ่งแยกข้อกังวลคือเพียงแค่ 'ทิ้ง' ฟังก์ชันการทำงานใหม่ลงในคลาสที่มีอยู่ แน่นอนว่านี่เป็นวิธีแก้ปัญหาระยะสั้นที่ยอดเยี่ยม (สำหรับผู้เริ่มต้น ต้องใช้การพิมพ์น้อยกว่า) แต่ก็กลายเป็นปัญหาที่ตามมาอย่างหลีกเลี่ยงไม่ได้ ไม่ว่าจะเป็นระหว่างการทดสอบ การบำรุงรักษา หรือที่ใดที่หนึ่งในระหว่างนั้น พิจารณาตัวควบคุมต่อไปนี้ ซึ่งส่งคืน TopTalentData
จากที่เก็บ:
@RestController public class TopTalentController { private final TopTalentRepository topTalentRepository; @RequestMapping("/toptal/get") public List<TopTalentData> getTopTalent() { return topTalentRepository.findAll() .stream() .map(this::entityToData) .collect(Collectors.toList()); } private TopTalentData entityToData(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); } }
ในตอนแรกอาจดูเหมือนไม่มีอะไรผิดปกติกับโค้ดชิ้นนี้ มันแสดงรายการของ TopTalentData
ที่ดึงมาจากอินสแตนซ์ TopTalentEntity
อย่างไรก็ตาม เมื่อพิจารณาให้ละเอียดยิ่งขึ้น เราจะเห็นได้ว่าจริงๆ แล้วมีบางสิ่งที่ TopTalentController
ดำเนินการอยู่ที่นี่ กล่าวคือ เป็นการแมปคำขอไปยังปลายทางเฉพาะ การดึงข้อมูลจากที่เก็บ และการแปลงเอนทิตีที่ได้รับจาก TopTalentRepository
เป็นรูปแบบอื่น โซลูชันที่ 'สะอาดกว่า' จะแยกข้อกังวลเหล่านั้นออกเป็นชั้นเรียนของตนเอง อาจมีลักษณะดังนี้:
@RestController @RequestMapping("/toptal") @AllArgsConstructor public class TopTalentController { private final TopTalentService topTalentService; @RequestMapping("/get") public List<TopTalentData> getTopTalent() { return topTalentService.getTopTalent(); } } @AllArgsConstructor @Service public class TopTalentService { private final TopTalentRepository topTalentRepository; private final TopTalentEntityConverter topTalentEntityConverter; public List<TopTalentData> getTopTalent() { return topTalentRepository.findAll() .stream() .map(topTalentEntityConverter::toResponse) .collect(Collectors.toList()); } } @Component public class TopTalentEntityConverter { public TopTalentData toResponse(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); } }
ข้อได้เปรียบเพิ่มเติมสำหรับลำดับชั้นนี้คือช่วยให้เราระบุตำแหน่งการทำงานได้โดยการตรวจสอบชื่อคลาส นอกจากนี้ ในระหว่างการทดสอบ เราสามารถแทนที่คลาสใดๆ ด้วยการจำลองการใช้งานได้อย่างง่ายดาย หากจำเป็น
ข้อผิดพลาดทั่วไป #4: ความไม่สอดคล้องกันและการจัดการข้อผิดพลาดที่ไม่ดี
หัวข้อของความสอดคล้องไม่จำเป็นต้องมีเฉพาะใน Spring (หรือ Java สำหรับเรื่องนั้น) แต่ยังคงเป็นแง่มุมที่สำคัญที่ต้องพิจารณาเมื่อทำงานในโครงการ Spring ในขณะที่รูปแบบการเขียนโค้ดสามารถอภิปรายกันได้ (และโดยปกติเป็นเรื่องของข้อตกลงภายในทีมหรือภายในบริษัททั้งหมด) การมีมาตรฐานร่วมกันจะกลายเป็นตัวช่วยที่ยอดเยี่ยม โดยเฉพาะอย่างยิ่งกับทีมที่มีผู้เล่นหลายคน ความสม่ำเสมอช่วยให้การส่งต่อเกิดขึ้นโดยไม่ต้องใช้ทรัพยากรจำนวนมากในการถือครองหรือให้คำอธิบายยาว ๆ เกี่ยวกับความรับผิดชอบของชั้นเรียนที่แตกต่างกัน
พิจารณาโปรเจ็กต์ Spring ที่มีไฟล์การกำหนดค่า บริการ และคอนโทรลเลอร์ต่างๆ การตั้งชื่อให้สอดคล้องกันทางความหมายจะสร้างโครงสร้างที่ค้นหาได้ง่าย โดยที่นักพัฒนาใหม่ทุกคนสามารถจัดการโค้ดต่างๆ ได้ การผนวกส่วนต่อท้าย Config เข้ากับคลาสการกำหนดค่าของคุณ ส่วนต่อท้ายบริการไปยังบริการของคุณ และส่วนต่อท้ายคอนโทรลเลอร์กับคอนโทรลเลอร์ของคุณ เป็นต้น
เกี่ยวข้องกับหัวข้อเรื่องความสอดคล้องอย่างใกล้ชิด การจัดการข้อผิดพลาดบนฝั่งเซิร์ฟเวอร์สมควรได้รับการเน้นย้ำเป็นพิเศษ หากคุณเคยต้องจัดการกับการตอบกลับข้อยกเว้นจาก API ที่เขียนได้ไม่ดี คุณอาจทราบสาเหตุแล้ว การแยกวิเคราะห์ข้อยกเว้นอย่างเหมาะสมอาจเป็นเรื่องที่ยุ่งยาก และเจ็บปวดกว่าเมื่อต้องระบุสาเหตุที่ข้อยกเว้นเหล่านั้นเกิดขึ้นตั้งแต่แรก
ในฐานะนักพัฒนา API คุณต้องการครอบคลุมจุดสิ้นสุดที่ผู้ใช้เผชิญทั้งหมดและแปลให้เป็นรูปแบบข้อผิดพลาดทั่วไป ซึ่งมักจะหมายถึงการมีรหัสข้อผิดพลาดทั่วไปและคำอธิบายแทนที่จะเป็นวิธีแก้ปัญหาของ a) ส่งคืนข้อความ "500 Internal Server Error" หรือ b) เพียงแค่ทิ้งการติดตามสแต็กให้กับผู้ใช้ (ซึ่งจริง ๆ แล้วควรหลีกเลี่ยงค่าใช้จ่ายทั้งหมด เนื่องจากจะเปิดเผยภายในของคุณนอกเหนือจากการจัดการฝั่งไคลเอ็นต์ได้ยาก)
ตัวอย่างของรูปแบบการตอบกลับข้อผิดพลาดทั่วไปอาจเป็น:
@Value public class ErrorResponse { private Integer errorCode; private String errorMessage; }
สิ่งที่คล้ายคลึงกันนี้มักพบใน API ที่ได้รับความนิยมส่วนใหญ่ และมีแนวโน้มที่จะทำงานได้ดี เนื่องจากสามารถจัดทำเป็นเอกสารได้อย่างง่ายดายและเป็นระบบ การแปลข้อยกเว้นเป็นรูปแบบนี้สามารถทำได้โดยระบุคำอธิบายประกอบ @ExceptionHandler
ให้กับเมธอด (ตัวอย่างของคำอธิบายประกอบอยู่ในข้อผิดพลาดทั่วไป #6)
ข้อผิดพลาดทั่วไป #5: การจัดการมัลติเธรดอย่างไม่เหมาะสม
ไม่ว่าจะพบในเดสก์ท็อปหรือเว็บแอป Spring หรือ Spring ก็ตาม มัลติเธรดก็เป็นสิ่งที่ยากต่อการถอดรหัส ปัญหาที่เกิดจากการดำเนินการโปรแกรมแบบคู่ขนานเป็นเรื่องที่เข้าใจยากและมักจะแก้จุดบกพร่องได้ยาก - อันที่จริงแล้ว เนื่องจากธรรมชาติของปัญหา เมื่อคุณตระหนักว่าคุณกำลังจัดการกับปัญหาการดำเนินการแบบคู่ขนานที่คุณอาจจะ ต้องละทิ้งโปรแกรมแก้ไขข้อบกพร่องทั้งหมดและตรวจสอบรหัสของคุณ "ด้วยมือ" จนกว่าคุณจะพบสาเหตุข้อผิดพลาดของรูท น่าเสียดายที่ไม่มีโซลูชันตัวตัดคุกกี้สำหรับแก้ไขปัญหาดังกล่าว คุณจะต้องประเมินสถานการณ์แล้วโจมตีปัญหาจากมุมที่คุณเห็นว่าดีที่สุด ทั้งนี้ขึ้นอยู่กับกรณีของคุณ
แน่นอน คุณต้องการหลีกเลี่ยงจุดบกพร่องแบบมัลติเธรดโดยสิ้นเชิง อีกครั้ง วิธีเดียวที่ใช้ได้ทุกอย่างสำหรับการทำเช่นนั้น แต่ต่อไปนี้คือข้อควรพิจารณาบางประการสำหรับการดีบักและการป้องกันข้อผิดพลาดมัลติเธรด:
หลีกเลี่ยงสถานะโลก
ประการแรก พึงระลึกไว้เสมอว่าปัญหา "สถานะโลก" หากคุณกำลังสร้างแอปพลิเคชันแบบมัลติเธรด ทุกสิ่งที่สามารถแก้ไขได้ทั่วโลกควรได้รับการตรวจสอบอย่างใกล้ชิด และหากเป็นไปได้ ให้ลบออกทั้งหมด หากมีเหตุผลที่ทำให้ตัวแปรส่วนกลางยังคงสามารถแก้ไขได้ ให้ใช้การซิงโครไนซ์อย่างระมัดระวังและติดตามประสิทธิภาพของแอปพลิเคชันของคุณ เพื่อยืนยันว่าตัวแปรนี้ไม่ได้เฉื่อยเนื่องจากช่วงรอที่เพิ่งเปิดตัวใหม่
หลีกเลี่ยงการกลายพันธุ์
อันนี้มาจากการเขียนโปรแกรมเชิงฟังก์ชันและปรับให้เข้ากับ OOP ระบุว่าควรหลีกเลี่ยงความผันแปรของคลาสและสถานะการเปลี่ยนแปลง กล่าวโดยย่อ หมายถึง วิธีเซ็ตเตอร์ที่กล่าวข้างต้นและมีฟิลด์สุดท้ายที่เป็นส่วนตัวในคลาสโมเดลทั้งหมดของคุณ เวลาเดียวที่ค่าของพวกมันกลายพันธุ์คือระหว่างการก่อสร้าง วิธีนี้ทำให้คุณมั่นใจได้ว่าไม่มีปัญหาการโต้แย้งเกิดขึ้น และการเข้าถึงคุณสมบัติของอ็อบเจ็กต์จะให้ค่าที่ถูกต้องตลอดเวลา
บันทึกข้อมูลสำคัญ
ประเมินตำแหน่งที่แอปพลิเคชันของคุณอาจทำให้เกิดปัญหาและบันทึกข้อมูลสำคัญทั้งหมดไว้ล่วงหน้า หากมีข้อผิดพลาดเกิดขึ้น คุณจะรู้สึกขอบคุณที่มีข้อมูลระบุว่าได้รับคำขอใดบ้าง และมีความเข้าใจอย่างถ่องแท้มากขึ้นว่าเหตุใดแอปพลิเคชันของคุณจึงทำงานผิดปกติ จำเป็นอีกครั้งที่จะต้องทราบว่าการบันทึกจะแนะนำไฟล์ I/O เพิ่มเติม ดังนั้นจึงไม่ควรนำไปใช้ในทางที่ผิด เนื่องจากอาจส่งผลกระทบอย่างรุนแรงต่อประสิทธิภาพของแอปพลิเคชันของคุณ

นำการใช้งานที่มีอยู่มาใช้ซ้ำ
เมื่อใดก็ตามที่คุณต้องการสร้างเธรดของคุณเอง (เช่น สำหรับการร้องขอ async ไปยังบริการต่างๆ) ให้นำการใช้งานที่ปลอดภัยที่มีอยู่มาใช้ซ้ำ แทนที่จะสร้างโซลูชันของคุณเอง ส่วนใหญ่จะหมายถึงการใช้ ExecutorServices และ CompletableFutures สไตล์การทำงานที่เรียบร้อยของ Java 8 สำหรับการสร้างเธรด Spring ยังอนุญาตให้ประมวลผลคำขอแบบอะซิงโครนัสผ่านคลาส DeferredResult
ข้อผิดพลาดทั่วไป #6: ไม่ใช้การตรวจสอบตามคำอธิบายประกอบ
ลองนึกภาพว่าบริการ TopTalent ของเราก่อนหน้านี้ต้องการจุดสิ้นสุดในการเพิ่ม Top Talent ใหม่ นอกจากนี้ สมมติว่า ด้วยเหตุผลที่ถูกต้องจริงๆ ชื่อใหม่ทุกชื่อต้องมีความยาว 10 อักขระพอดี วิธีหนึ่งในการดำเนินการนี้อาจเป็นดังนี้:
@RequestMapping("/put") public void addTopTalent(@RequestBody TopTalentData topTalentData) { boolean nameNonExistentOrHasInvalidLength = Optional.ofNullable(topTalentData) .map(TopTalentData::getName) .map(name -> name.length() == 10) .orElse(true); if (nameNonExistentOrInvalidLength) { // throw some exception } topTalentService.addTopTalent(topTalentData); }
อย่างไรก็ตาม ข้างต้น (นอกเหนือจากการสร้างไม่ดี) ไม่ใช่วิธีแก้ปัญหาที่ 'สะอาด' จริงๆ เรากำลังตรวจสอบความถูกต้องมากกว่าหนึ่งประเภท (กล่าวคือ TopTalentData
ไม่ใช่ null และ TopTalentData.name
ไม่ใช่ null และ TopTalentData.name
นั้นมีความยาว 10 อักขระ) รวมทั้งส่งข้อยกเว้นหากข้อมูลไม่ถูกต้อง .
สิ่งนี้สามารถดำเนินการได้อย่างหมดจดยิ่งขึ้นโดยใช้ตัวตรวจสอบไฮเบอร์เนตกับ Spring ขั้นแรก เรามาสร้างโครงสร้างใหม่ให้กับเมธอด addTopTalent
เพื่อสนับสนุนการตรวจสอบความถูกต้อง:
@RequestMapping("/put") public void addTopTalent(@Valid @NotNull @RequestBody TopTalentData topTalentData) { topTalentService.addTopTalent(topTalentData); } @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse handleInvalidTopTalentDataException(MethodArgumentNotValidException methodArgumentNotValidException) { // handle validation exception }
นอกจากนี้ เราจะต้องระบุคุณสมบัติที่เราต้องการตรวจสอบในคลาส TopTalentData
:
public class TopTalentData { @Length(min = 10, max = 10) @NotNull private String name; }
ตอนนี้ Spring จะสกัดกั้นคำขอและตรวจสอบก่อนที่จะเรียกใช้เมธอด ไม่จำเป็นต้องใช้การทดสอบด้วยตนเองเพิ่มเติม
อีกวิธีหนึ่งที่เราสามารถทำได้ในสิ่งเดียวกันคือการสร้างคำอธิบายประกอบของเราเอง แม้ว่าโดยทั่วไปแล้ว คุณจะใช้คำอธิบายประกอบแบบกำหนดเองก็ต่อเมื่อความต้องการของคุณเกินชุดข้อจำกัดในตัวของ Hibernate สำหรับตัวอย่างนี้ ให้สมมติว่าไม่มี @Length คุณจะต้องสร้างเครื่องมือตรวจสอบความถูกต้องซึ่งจะตรวจสอบความยาวของสตริงโดยสร้างคลาสเพิ่มเติมสองคลาส คลาสหนึ่งสำหรับตรวจสอบความถูกต้อง และอีกคลาสสำหรับการอธิบายคุณสมบัติ:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint(validatedBy = { MyAnnotationValidator.class }) public @interface MyAnnotation { String message() default "String length does not match expected"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; int value(); } @Component public class MyAnnotationValidator implements ConstraintValidator<MyAnnotation, String> { private int expectedLength; @Override public void initialize(MyAnnotation myAnnotation) { this.expectedLength = myAnnotation.value(); } @Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { return s == null || s.length() == this.expectedLength; } }
โปรดทราบว่าในกรณีเหล่านี้ แนวทางปฏิบัติที่ดีที่สุดในการแยกข้อกังวลกำหนดให้คุณต้องทำเครื่องหมายคุณสมบัติว่าถูกต้องหากเป็นค่าว่าง ( s == null
ภายในเมธอด isValid
) จากนั้นใช้คำอธิบายประกอบ @NotNull
หากนั่นเป็นข้อกำหนดเพิ่มเติมสำหรับ คุณสมบัติ:
public class TopTalentData { @MyAnnotation(value = 10) @NotNull private String name; }
ข้อผิดพลาดทั่วไป #7: (ยัง) การใช้การกำหนดค่าตาม XML
แม้ว่า XML เป็นสิ่งจำเป็นสำหรับ Spring เวอร์ชันก่อนหน้า แต่ในปัจจุบันการกำหนดค่าส่วนใหญ่สามารถทำได้ผ่านโค้ด Java / คำอธิบายประกอบเท่านั้น การกำหนดค่า XML เป็นเพียงโค้ดต้นแบบเพิ่มเติมที่ไม่จำเป็น
บทความนี้ (รวมถึงที่เก็บ GitHub ที่มาพร้อมกัน) ใช้คำอธิบายประกอบสำหรับการกำหนดค่า Spring และ Spring รู้ว่าควรเชื่อมต่อถั่วใด เนื่องจากแพ็คเกจรูทได้รับการใส่คำอธิบายประกอบด้วยคำอธิบายประกอบ @SpringBootApplication
เช่น:
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
คำอธิบายประกอบแบบรวม (คุณสามารถเรียนรู้เพิ่มเติมเกี่ยวกับมันได้ในเอกสารประกอบของ Spring เพียงแต่ให้คำแนะนำแก่ Spring ว่าควรสแกนแพ็คเกจใดเพื่อดึงข้อมูลถั่ว ในกรณีของเรา ในกรณีนี้ ต่อไปนี้จะถูกนำมาใช้ในแพ็คเกจด้านบน (co.kukurin) สำหรับสายไฟ:
-
@Component
(TopTalentConverter
,MyAnnotationValidator
) -
@RestController
(TopTalentController
) -
@Repository
(TopTalentRepository
) -
@Service
(TopTalentService
) คลาส
หากเรามีคลาสที่มีคำอธิบายประกอบ @Configuration
เพิ่มเติม พวกเขาจะถูกตรวจสอบการกำหนดค่าที่ใช้ Java ด้วย
ข้อผิดพลาดทั่วไป #8: ลืมเกี่ยวกับโปรไฟล์
ปัญหาที่พบบ่อยในการพัฒนาเซิร์ฟเวอร์คือการแยกแยะระหว่างการกำหนดค่าประเภทต่างๆ ซึ่งมักจะเป็นการกำหนดค่าการผลิตและการพัฒนาของคุณ แทนที่จะแทนที่รายการการกำหนดค่าต่างๆ ด้วยตนเองทุกครั้งที่คุณเปลี่ยนจากการทดสอบเป็นการปรับใช้แอปพลิเคชันของคุณ วิธีที่มีประสิทธิภาพมากขึ้นคือการใช้โปรไฟล์
พิจารณากรณีที่คุณใช้ฐานข้อมูลในหน่วยความจำสำหรับการพัฒนาในพื้นที่ โดยมีฐานข้อมูล MySQL อยู่ในระหว่างใช้งานจริง โดยพื้นฐานแล้วหมายความว่าคุณจะใช้ URL ที่แตกต่างกันและ (หวังว่า) ข้อมูลรับรองที่แตกต่างกันสำหรับการเข้าถึงแต่ละ URL มาดูกันว่าจะทำได้อย่างไร ไฟล์คอนฟิกูเรชันที่แตกต่างกันสองไฟล์:
ไฟล์ application.yaml
# set default profile to 'dev' spring.profiles.active: dev # production database details spring.datasource.url: 'jdbc:mysql://localhost:3306/toptal' spring.datasource.username: root spring.datasource.password:
ไฟล์ application-dev.yaml
spring.datasource.url: 'jdbc:h2:mem:' spring.datasource.platform: h2
สมมุติว่าคุณคงไม่อยากดำเนินการใดๆ กับฐานข้อมูลที่ใช้งานจริงของคุณโดยไม่ได้ตั้งใจในขณะที่กำลังแก้ไขโค้ด ดังนั้นจึงควรตั้งค่าโปรไฟล์เริ่มต้นเป็น dev บนเซิร์ฟเวอร์ คุณสามารถแทนที่การกำหนดค่าโปรไฟล์ด้วยตนเองโดยระบุพารามิเตอร์ -Dspring.profiles.active=prod
ให้กับ JVM หรือคุณสามารถตั้งค่าตัวแปรสภาพแวดล้อมของระบบปฏิบัติการเป็นโปรไฟล์เริ่มต้นที่ต้องการได้
ข้อผิดพลาดทั่วไป #9: ล้มเหลวในการยอมรับการฉีดพึ่งพา
การใช้การพึ่งพาการฉีดอย่างถูกต้องด้วย Spring หมายถึงการอนุญาตให้เชื่อมต่อวัตถุทั้งหมดของคุณเข้าด้วยกันโดยการสแกนคลาสการกำหนดค่าที่ต้องการทั้งหมด สิ่งนี้พิสูจน์ให้เห็นว่ามีประโยชน์สำหรับการแยกความสัมพันธ์และทำให้การทดสอบง่ายขึ้นมาก แทนที่จะใช้คลาสคัปปลิ้งแบบแน่นหนาโดยทำสิ่งนี้:
public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController() { this.topTalentService = new TopTalentService(); } }
เราอนุญาตให้ Spring ทำสายไฟให้เรา:
public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController(TopTalentService topTalentService) { this.topTalentService = topTalentService; } }
การพูดคุยใน Google ของ Misko Hevery อธิบาย 'สาเหตุ' ของการฉีดการพึ่งพาในเชิงลึก ดังนั้นเรามาดูวิธีการใช้งานจริงกันดีกว่า ในส่วนการแยกข้อกังวล (ข้อผิดพลาดทั่วไป #3) เราได้สร้างคลาสบริการและตัวควบคุม สมมติว่าเราต้องการทดสอบคอนโทรลเลอร์ภายใต้สมมติฐานว่า TopTalentService
ทำงานอย่างถูกต้อง เราสามารถแทรกวัตถุจำลองแทนการใช้งานบริการจริงโดยจัดเตรียมคลาสการกำหนดค่าแยกต่างหาก:
@Configuration public class SampleUnitTestConfig { @Bean public TopTalentService topTalentService() { TopTalentService topTalentService = Mockito.mock(TopTalentService.class); Mockito.when(topTalentService.getTopTalent()).thenReturn( Stream.of("Mary", "Joel").map(TopTalentData::new).collect(Collectors.toList())); return topTalentService; } }
จากนั้นเราสามารถฉีดวัตถุจำลองโดยบอกให้ Spring ใช้ SampleUnitTestConfig
เป็นผู้จัดหาการกำหนดค่า:
@ContextConfiguration(classes = { SampleUnitTestConfig.class })
ซึ่งช่วยให้เราใช้การกำหนดค่าบริบทเพื่อฉีด custom bean ลงในการทดสอบหน่วย
ข้อผิดพลาดทั่วไป #10: ขาดการทดสอบหรือการทดสอบที่ไม่เหมาะสม
แม้ว่าแนวคิดของการทดสอบหน่วยจะอยู่กับเรามานานแล้ว นักพัฒนาหลายคนดูเหมือนจะ "ลืม" ที่จะทำสิ่งนี้ (โดยเฉพาะถ้าไม่ จำเป็น ) หรือเพียงแค่เพิ่มเข้าไปในภายหลัง เห็นได้ชัดว่าไม่เป็นที่ต้องการ เนื่องจากการทดสอบไม่เพียงแต่ควรตรวจสอบความถูกต้องของโค้ดของคุณเท่านั้น แต่ยังทำหน้าที่เป็นเอกสารเกี่ยวกับวิธีการทำงานของแอปพลิเคชันในสถานการณ์ต่างๆ
เมื่อทำการทดสอบบริการเว็บ คุณแทบจะไม่ได้ทำการทดสอบหน่วยที่ 'บริสุทธิ์' เลย เนื่องจากการสื่อสารผ่าน HTTP มักจะกำหนดให้คุณต้องเรียกใช้ DispatcherServlet
ของ Spring และดูว่าเกิดอะไรขึ้นเมื่อได้รับ HttpServletRequest
จริง (ทำให้เป็นการทดสอบการ รวม การจัดการกับการตรวจสอบความถูกต้อง การซีเรียลไลซ์เซชัน) ฯลฯ) REST Assured Java DSL สำหรับการทดสอบบริการ REST อย่างง่าย นอกเหนือจาก MockMVC ได้รับการพิสูจน์แล้วว่าให้โซลูชันที่หรูหรามาก พิจารณาข้อมูลโค้ดต่อไปนี้ด้วยการฉีดขึ้นต่อกัน:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { Application.class, SampleUnitTestConfig.class }) public class RestAssuredTestDemonstration { @Autowired private TopTalentController topTalentController; @Test public void shouldGetMaryAndJoel() throws Exception { // given MockMvcRequestSpecification givenRestAssuredSpecification = RestAssuredMockMvc.given() .standaloneSetup(topTalentController); // when MockMvcResponse response = givenRestAssuredSpecification.when().get("/toptal/get"); // then response.then().statusCode(200); response.then().body("name", hasItems("Mary", "Joel")); } }
SampleUnitTestConfig
เชื่อมโยงการใช้งานจำลองของ TopTalentService
เข้ากับ TopTalentController
ในขณะที่คลาสอื่น ๆ ทั้งหมดนั้นต่อสายโดยใช้การกำหนดค่ามาตรฐานที่อนุมานจากแพ็คเกจการสแกนที่รูทในแพ็คเกจของคลาสแอปพลิเคชัน RestAssuredMockMvc
ใช้เพื่อตั้งค่าสภาพแวดล้อมที่มีน้ำหนักเบาและส่งคำขอ GET
ไปยังจุดปลาย /toptal/get
การเป็น Spring Master
Spring เป็นเฟรมเวิร์กที่ทรงพลังซึ่งง่ายต่อการเริ่มต้น แต่ต้องใช้ความทุ่มเทและเวลาในการบรรลุความเชี่ยวชาญอย่างเต็มที่ การใช้เวลาทำความคุ้นเคยกับเฟรมเวิร์กจะช่วยปรับปรุงประสิทธิภาพการทำงานของคุณในระยะยาว และช่วยให้คุณเขียนโค้ดที่สะอาดขึ้นและกลายเป็นนักพัฒนาที่ดีขึ้นในที่สุด
หากคุณกำลังมองหาแหล่งข้อมูลเพิ่มเติม Spring In Action เป็นหนังสือเชิงปฏิบัติที่ดีซึ่งครอบคลุมหัวข้อหลักของ Spring มากมาย