10 ข้อผิดพลาดเกี่ยวกับ Spring Framework ที่พบบ่อยที่สุด

เผยแพร่แล้ว: 2022-03-11

Spring เป็นหนึ่งในเฟรมเวิร์ก 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 มากมาย