Java REST في مدرسة HeadHunter للمبرمجين

مرحبًا هابر ، نريد التحدث عن أحد مشاريع مدرسة المبرمجين HeadHunter 2018. فيما يلي مقال من خريجنا سيتحدث فيه عن الخبرة المكتسبة أثناء التدريب.



مرحبا بالجميع. تخرجت هذا العام من كلية المبرمجين ، وسأتحدث في هذا المنشور عن المشروع التدريبي الذي شاركت فيه. أثناء التدريب في المدرسة ، وخاصة في المشروع ، افتقرت إلى مثال على تطبيق قتالي (وحتى دليل أفضل) ، حيث يمكنني أن أرى كيفية فصل المنطق بشكل صحيح وبناء بنية قابلة للتطوير. جميع المقالات التي وجدتها كان من الصعب على المبتدئ فهمها ، مثل إما تم استخدام IoC بفاعلية فيها دون توضيحات شاملة لكيفية إضافة مكونات جديدة أو تعديل المكونات القديمة ، أو أنها كانت قديمة وتحتوي على طن من تكوينات xml وواجهة jsp الأمامية. حاولت التركيز على مستواي قبل التدريب ، أي ما يقرب من الصفر مع بعض التحذيرات ، لذلك يجب أن تكون هذه المقالة مفيدة للطلاب المستقبليين في المدرسة ، وكذلك المتحمسين الذين درسوا ذاتيًا والذين قرروا البدء في الكتابة بلغة جافا.


تعطى (بيان المشكلة)


فريق - 5 أشخاص. المدة 3 أشهر ، في نهاية كل عرض تجريبي. الهدف هو إنشاء تطبيق يساعد الموارد البشرية على مرافقة الموظفين في فترة تجريبية ، وأتمتة جميع العمليات التي تتحول. عند المدخل ، قيل لنا كيف يتم ترتيب فترة الاختبار (IP) الآن: بمجرد أن يصبح الموظف الجديد قادمًا ، تبدأ HR في طرد القائد المستقبلي لتعيين المهام لـ IP ، ويجب القيام بذلك قبل يوم العمل الأول. في اليوم الذي يذهب فيه الموظف إلى العمل ، تعقد HR اجتماعًا ترحيبيًا وتتحدث عن البنية التحتية للشركة وتسليم المهام المتعلقة بالملكية الفكرية. بعد 1.5 و 3 أشهر ، يتم عقد اجتماع متوسط ​​ونهائي للموارد البشرية ، والقائد والموظف ، يتم فيه مناقشة نجاح المقطع ووضع استمارة النتائج. في حالة النجاح ، بعد الاجتماع الأخير ، يتم تسليم الموظف استبيانًا مطبوعًا للمبتدئ (أسئلة على غرار "التمتع بمتعة IP") والحصول على مهمة الموارد البشرية لجيرا لإصدارها إلى موظف VHI.


التصميم


قررنا أن نجعل لكل موظف صفحة شخصية تُعرض عليها معلومات عامة (الاسم ، القسم ، المدير ، إلخ) ، حقل للتعليقات وسجل التغييرات ، الملفات المرفقة (المهام على IP ، الاستبيان) وسير عمل الموظف الذي يعكس مستوى مرور IP. تقرر تقسيم سير العمل إلى 8 مراحل ، وهي:


  • المرحلة 1 - إضافة موظف: تكتمل فورًا بعد تسجيل موظف جديد في نظام الموارد البشرية. في الوقت نفسه ، يتم إرسال ثلاثة تقاويم إلى الموارد البشرية من أجل اجتماع جيد ومتوسط ​​ونهائي.
  • المرحلة الثانية - تنسيق المهام على IP: يتم إرسال نموذج إلى الرأس لتحديد المهام على IP ، والتي سيتلقاها HR بعد تعبئتها. بعد ذلك ، تطبعهم الموارد البشرية وتوقعهم وتميز اكتمال المرحلة في الواجهة.
  • المرحلة الثالثة - لقاء ترحيبي: الموارد البشرية تعقد اجتماعًا وتضغط على زر "المرحلة المكتملة".
  • المرحلة الرابعة - اجتماع مؤقت: مشابه للمرحلة الثالثة
  • المرحلة الخامسة - نتائج الاجتماع المؤقت: تملأ الموارد البشرية النتائج في صفحة الموظف وتنقر على "التالي".
  • المرحلة السادسة - اللقاء الختامي: مشابه للمرحلة الثالثة
  • المرحلة السابعة - نتائج الاجتماع الختامي: مماثلة للمرحلة الخامسة
  • المرحلة الثامنة - إكمال IP: في حالة الانتهاء بنجاح من IP ، سيتم إرسال الموظف رابطًا مع نموذج الاستبيان عن طريق البريد الإلكتروني ، وفي jira سيتم إنشاء مهمة لتسجيل التأمين الطبي الطوعي تلقائيًا (حصلنا على المهمة يدويًا).

جميع المراحل لها وقت ، وبعد ذلك تعتبر المرحلة منتهية الصلاحية ويتم تمييزها باللون الأحمر ، ويصل إشعار عبر البريد. يجب أن يكون وقت الانتهاء قابلاً للتحرير ، على سبيل المثال ، في حالة وقوع الاجتماع المؤقت في عطلة عامة أو لسبب ما يجب إعادة جدولة الاجتماع.
لسوء الحظ ، لم يتم الحفاظ على النماذج الأولية المرسومة على قطعة من الورق / الألواح ، ولكن في النهاية ستكون هناك لقطات شاشة للتطبيق النهائي.


العملية


أحد أهداف المدرسة هو إعداد الطلاب للعمل في المشاريع الكبيرة ، لذلك كانت عملية الإفراج عن المهام مناسبة لنا.
في نهاية العمل على المهمة ، نعطيها للمراجعة_1 لطالب آخر من الفريق لتصحيح الأخطاء الواضحة / تبادل الخبرات. ثم تأتي review_2 - يتم التحقق من المهمة من قبل اثنين من الموجهين الذين يتأكدون من أننا لا نطلق govnokod مع المراجع_1. كان من المفترض إجراء المزيد من الاختبارات ، لكن هذه المرحلة ليست مناسبة للغاية ، نظرًا لحجم المشروع المدرسي. لذا بعد استعراض المراجعة ، اعتقدنا أن المهمة كانت جاهزة للإصدار.
الآن بضع كلمات حول النشر. يجب أن يكون التطبيق متاحًا طوال الوقت على الشبكة من أي أجهزة كمبيوتر. للقيام بذلك ، اشترينا جهازًا افتراضيًا رخيصًا (مقابل 100 روبل / شهر) ، ولكن ، كما اكتشفت لاحقًا ، يمكن ترتيب كل شيء مجانًا وبطريقة عصرية في عامل إرساء AWS . للتكامل المستمر ، اخترنا ترافيس. إذا كان أي شخص لا يعرف (أنا شخصياً لم أسمع عن التكامل المستمر على الإطلاق قبل المدرسة) ، فهذا أمر رائع سيراقبه github وعندما يظهر التزام جديد (كيفية تكوينه) ، قم بتجميع الرمز في وعاء ، وإرساله إلى الخادم وإعادة تشغيل التطبيق تلقائيًا. يتم وصف كيفية بنائه في Travis Jam في جذر المشروع ، وهو مشابه تمامًا لباش ، لذلك أعتقد أنه لا توجد تعليقات مطلوبة. اشترينا أيضًا النطاق www.adaptation.host حتى لا يتم تسجيل عنوان IP قبيح في شريط العناوين في العرض التوضيحي. قمنا أيضًا بتكوين postfix (لإرسال البريد) ، و apache (وليس nginx ، لأن apache كان خارج الصندوق) وخادم jira (التجريبي). تم إنشاء الواجهة الأمامية والواجهة الخلفية من خلال خدمتين منفصلتين ستتواصلان عبر http (# 2k18 ، # microservices). ينتهي هذا الجزء من المقالة "في مدرسة HeadHunter للمبرمجين" بسلاسة ، وننتقل إلى خدمة جافا للراحة.


الخلفية


0. مقدمة


استخدمنا التقنيات التالية:


  • JDK 1.8 ؛
  • مخضرم 3.5.2 ؛
  • Postgres 9.6 ؛
  • السبات 5.2.10 ؛
  • رصيف 9.4.8 ؛
  • جيرسي 2.27.

كإطار عمل ، أخذنا NaB 3.5.0 من hh. أولاً ، يتم استخدامه في HeadHunter ، وثانيًا ، يحتوي على رصيف ، وجيرسي ، وإسبات ، وبوسترس مضمن خارج الصندوق ، وهو مكتوب على جيثب. سأوضح بإيجاز للمبتدئين: jetty هو خادم ويب يحدد العملاء وينظم جلسات لكل منهم ؛ جيرسي - إطار عمل يساعد على إنشاء خدمة مريحة. السبات - ORM لتبسيط العمل مع قاعدة البيانات ؛ مخضرم جامع مشروع جافا.
سأعرض مثالًا بسيطًا على كيفية العمل مع هذا. لقد قمت بإنشاء مستودع اختبار صغير ، أضفت فيه كيانين: مستخدم وسيرة ذاتية ، بالإضافة إلى موارد لإنشائها واستلامها باستخدام رابط OneToMany / ManyToOne. للبدء ، ما عليك سوى استنساخ المستودع وتشغيل mvn clean install exec: java في جذر المشروع. قبل التعليق على الشفرة ، سأخبرك عن هيكل خدمتنا. يبدو شيء مثل هذا:



الدلائل الرئيسية:


  • الخدمات - الدليل الرئيسي في التطبيق ، يتم تخزين كل منطق الأعمال هنا. في أماكن أخرى ، لا ينبغي أن يكون العمل مع البيانات دون سبب وجيه.
  • الموارد - معالجات url ، طبقة بين الخدمات والواجهة الأمامية. يسمح هنا بالتحقق من البيانات الواردة وتحويل البيانات الصادرة ، ولكن ليس منطق الأعمال.
  • داو (كائن الوصول إلى البيانات) - طبقة بين قاعدة البيانات والخدمات. يجب أن يحتوي Tao فقط على العمليات الأساسية الأساسية: إضافة ، عد ، تحديث ، حذف واحد / الكل.
  • الكيان - الكائنات التي يتبادلها ORM مع قاعدة البيانات. كقاعدة ، تتطابق بشكل مباشر مع الجداول ويجب أن تحتوي على جميع الحقول ككيان في قاعدة البيانات مع الأنواع المقابلة.
  • Dto (كائن نقل البيانات) - تناظري للكيان ، فقط للموارد (الأمامية) ، يساعد على تكوين json من البيانات التي نريد إرسالها / استقبالها.

1. القاعدة


بطريقة جيدة ، يجب عليك استخدام postgres المثبتة في مكان قريب ، كما هو الحال في التطبيق الرئيسي ، لكنني أردت أن تكون حالة الاختبار بسيطة وتعمل مع أمر واحد ، لذلك أخذت HSQLDB المدمج. يتم توصيل قاعدة البيانات ببنيتنا التحتية عن طريق إضافة DataSource إلى ProdConfig (تذكر أيضًا إخبار السبات بقاعدة البيانات التي تستخدمها):


@Bean(destroyMethod = "shutdown") DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("db/sql/create-db.sql") .build(); } 

لقد قمت بإنشاء البرنامج النصي لإنشاء الجدول في ملف create-db.sql. يمكنك إضافة برامج نصية أخرى تقوم بتهيئة قاعدة البيانات. في مثالنا الخفيف in_memory ، يمكننا الاستغناء عن البرامج النصية على الإطلاق. إذا قمت بتحديد hibernate.hbm2ddl.auto=create في إعدادات hibernate.properties ، فسيقوم السبات نفسه بإنشاء الجداول حسب الكيان عند بدء التطبيق. ولكن إذا كنت بحاجة إلى وجود شيء في قاعدة البيانات ليس لدى الكيان ، فلا يمكنك الاستغناء عن ملف. أنا شخصياً اعتدت على مشاركة قاعدة البيانات والتطبيق ، لذلك أنا عادة لا أثق في السبات للقيام بهذه الأشياء.
db/sql/create-db.sql :


 CREATE TABLE employee ( id INTEGER IDENTITY PRIMARY KEY, first_name VARCHAR(256) NOT NULL, last_name VARCHAR(256) NOT NULL, email VARCHAR(128) NOT NULL ); CREATE TABLE resume ( id INTEGER IDENTITY PRIMARY KEY, employee_id INTEGER NOT NULL, position VARCHAR(128) NOT NULL, about VARCHAR(256) NOT NULL, FOREIGN KEY (employee_id) REFERENCES employee(id) ); 

2. الكيان


entities/employee :


 @Entity @Table(name = "employee") public class Employee { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) private Integer id; @Column(name = "first_name", nullable = false) private String firstName; @Column(name = "last_name", nullable = false) private String lastName; @Column(name = "email", nullable = false) private String email; @OneToMany(mappedBy = "employee") @OrderBy("id") private List<Resume> resumes; //..geters and seters.. } 

entities/resume :


 @Entity @Table(name = "resume") public class Resume { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "employee_id") private Employee employee; @Column(name = "position", nullable = false) private String position; @Column(name = "about") private String about; //..geters and seters.. } 

لا تشير الكيانات إلى بعضها البعض مع حقل الفئة ، ولكن مع الكائن الرئيسي / الكائن الفرعي بالكامل. وبالتالي ، يمكننا الحصول على العودية عندما نحاول أن نأخذ من قاعدة بيانات الموظفين ، التي يتم رسم @OneToMany(mappedBy = "employee") الذاتية ، والتي ... لمنع حدوث ذلك ، أشرنا إلى التعليقات التوضيحية @OneToMany(mappedBy = "employee") و @ManyToOne(fetch = FetchType.LAZY) . سيتم أخذها في الاعتبار في الخدمة عند تنفيذ معاملة الكتابة / القراءة من قاعدة البيانات. يعد إعداد FetchType.LAZY اختياريًا ، ولكن استخدام الاتصال FetchType.LAZY يجعل المعاملة أسهل. لذا ، إذا حصلنا في معاملة على سيرة ذاتية من قاعدة البيانات ولم نتصل بمالكها ، فلن يتم تحميل كيان الموظف. يمكنك التحقق من ذلك بنفسك: قم بإزالة FetchType.LAZY في التصحيح أنه يعود من الخدمة مع السيرة الذاتية. ولكن يجب أن تكون حذرًا - إذا لم نقم بتحميل الموظف في المعاملة ، فإن الوصول إلى حقول الموظف خارج المعاملة يمكن أن يتسبب في LazyInitializationException .


3. داو


في حالتنا ، فإن EmployeeDao و ResumeDao متطابقان تقريبًا ، لذلك سأقدم هنا واحدًا منهم فقط
EmployeeDao :


 public class EmployeeDao { private final SessionFactory sessionFactory; @Inject public EmployeeDao(SessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } public void save(Employee employee) { sessionFactory.getCurrentSession().save(employee); } public Employee getById(Integer id) { return sessionFactory.getCurrentSession().get(Employee.class, id); } } 

@Inject يعني أنه في مُنشئ داو ، يتم استخدام حقن التبعية. في حياتي الماضية ، قام فيزيائي قام بتفريغ الملفات ، ببناء رسوم بيانية استنادًا إلى نتائج الأرقام ، وعلى الأقل ، اكتشف OOP ، في أدلة جافا ، بدت مثل هذه الإنشاءات جنونية إلى حد ما. وفي المدرسة ، ربما ، هذا الموضوع هو الأكثر غموضا ، IMHO. لحسن الحظ ، هناك الكثير من المواد حول DI على الإنترنت. إذا كنت كسولًا جدًا للقراءة ، فيمكنك في الشهر الأول اتباع القاعدة: تسجيل موارد / خدمات جديدة / Tao في تكوين السياق الخاص بنا ، وإضافة الكيانات إلى الخريطة . إذا كنت بحاجة إلى استخدام بعض الخدمات / تاو في خدمات أخرى ، فأنت بحاجة إلى إضافتها في المنشئ مع حقن التعليقات التوضيحية ، كما هو موضح أعلاه ، ويبدأ الربيع في تهيئة كل شيء لك. ولكن لا يزال عليك التعامل مع DI.


4. Dto


Dto ، مثل داو ، متطابقة تقريبًا للموظف والسيرة الذاتية. نحن نعتبر الموظف فقط هنا. سنحتاج إلى فصلين: EmployeeCreateDto ، ضروري عند إنشاء موظف ؛ EmployeeDto المستخدم عند الاستلام (يحتوي على id حقول إضافية resumes ). تمت إضافة حقل id بحيث يمكننا في المستقبل ، بناءً على طلبات من الخارج ، العمل مع الموظف دون إجراء بحث أولي للكيان عبر email . حقل resumes لاستقبال الموظف مع كل سيرته الذاتية في طلب واحد. سيكون من الممكن الإدارة باستخدام dto واحد لجميع العمليات ، ولكن بعد ذلك ، من أجل قائمة جميع السير الذاتية لموظف معين ، سيتعين علينا إنشاء مورد إضافي ، مثل getResumesByEmployeeEmail ، وتلويث الشفرة باستعلامات قاعدة بيانات مخصصة وإلغاء جميع وسائل الراحة التي يوفرها ORM.
EmployeeCreateDto :


 public class EmployeeCreateDto { public String firstName; public String lastName; public String email; } 

EmployeeDto :


 public class EmployeeDto { public Integer id; public String firstName; public String lastName; public String email; public List<ResumeDto> resumes; public EmployeeDto(){ } public EmployeeDto(Employee employee){ id = employee.getId(); firstName = employee.getFirstName(); lastName = employee.getLastName(); email = employee.getEmail(); if (employee.getResumes() != null) { resumes = employee.getResumes().stream().map(ResumeDto::new).collect(Collectors.toList()); } } } 

مرة أخرى ، أود أن ألفت الانتباه إلى حقيقة أن كتابة المنطق في dto غير لائقة للغاية بحيث يتم تعيين جميع الحقول على أنها public ، حتى لا تستخدم الحروف والمستوطنين.


5. الخدمة


EmployeeService :


 public class EmployeeService { private EmployeeDao employeeDao; private ResumeDao resumeDao; @Inject public EmployeeService(EmployeeDao employeeDao, ResumeDao resumeDao) { this.employeeDao = employeeDao; this.resumeDao = resumeDao; } @Transactional public EmployeeDto createEmployee(EmployeeCreateDto employeeCreateDto) { Employee employee = new Employee(); employee.setFirstName(employeeCreateDto.firstName); employee.setLastName(employeeCreateDto.lastName); employee.setEmail(employeeCreateDto.email); employeeDao.save(employee); return new EmployeeDto(employee); } @Transactional public ResumeDto createResume(ResumeCreateDto resumeCreateDto) { Resume resume = new Resume(); resume.setEmployee(employeeDao.getById(resumeCreateDto.employeeId)); resume.setPosition(resumeCreateDto.position); resume.setAbout(resumeCreateDto.about); resumeDao.save(resume); return new ResumeDto(resume); } @Transactional(readOnly = true) public EmployeeDto getEmployeeById(Integer id) { return new EmployeeDto(employeeDao.getById(id)); } @Transactional(readOnly = true) public ResumeDto getResumeById(Integer id) { return new ResumeDto(resumeDao.getById(id)); } } 

تلك المعاملات التي تحمينا من LazyInitializationException (وليس فقط). لفهم المعاملات في السبات ، أوصي بعمل ممتاز على المحور ( اقرأ المزيد ... ) ، مما ساعدني كثيرًا في الوقت المناسب.


6. الموارد


أخيرًا ، أضف الموارد لإنشاء كياناتنا والحصول عليها:
EmployeeResource :


 @Path("/") @Singleton public class EmployeeResource { private final EmployeeService employeeService; public EmployeeResource(EmployeeService employeeService) { this.employeeService = employeeService; } @GET @Produces("application/json") @Path("/employee/{id}") @ResponseBody public Response getEmployee(@PathParam("id") Integer id) { return Response.status(Response.Status.OK) .entity(employeeService.getEmployeeById(id)) .build(); } @POST @Produces("application/json") @Path("/employee/create") @ResponseBody public Response createEmployee(@RequestBody EmployeeCreateDto employeeCreateDto){ return Response.status(Response.Status.OK) .entity(employeeService.createEmployee(employeeCreateDto)) .build(); } @GET @Produces("application/json") @Path("/resume/{id}") @ResponseBody public Response getResume(@PathParam("id") Integer id) { return Response.status(Response.Status.OK) .entity(employeeService.getResumeById(id)) .build(); } @POST @Produces("application/json") @Path("/resume/create") @ResponseBody public Response createResume(@RequestBody ResumeCreateDto resumeCreateDto){ return Response.status(Response.Status.OK) .entity(employeeService.createResume(resumeCreateDto)) .build(); } } 

يلزم Produces(“application/json”) بحيث يتم تحويل json و dto إلى بعضهما البعض بشكل صحيح. يتطلب تبعية pom.xml:


 <dependency> <groupId>org.glassfish.jersey.media</groupId> <artifactId>jersey-media-json-jackson</artifactId> <version>${jersey.version}</version> </dependency> 

تكشف محولات json الأخرى لسبب ما عن MediaType غير صالح.


7. النتيجة


قم بتشغيل وتحقق ما لدينا ( mvn clean install exec:java في جذر المشروع). المنفذ الذي يتم تشغيل التطبيق عليه محدد في service.properties . قم بإنشاء مستخدم واستئناف. أفعل هذا مع حليقة ، ولكن يمكنك استخدام ساعي البريد إذا احتقرت وحدة التحكم.


 curl --header "Content-Type: application/json" \ --request POST \ --data '{"firstName": "Jason", "lastName": "Statham", "email": "jasonst@t.ham"}' \ http://localhost:9999/employee/create curl --header "Content-Type: application/json" \ --request POST \ --data '{"employeeId": 0, "position": "Voditel", "about": "Opyt raboty perevozchikom 15 let"}' \ http://localhost:9999/resume/create curl --header "Content-Type: application/json" --request GET http://localhost:9999/employee/0 curl --header "Content-Type: application/json" --request GET http://localhost:9999/employee/0 

كل شيء يعمل بشكل جيد. لذلك حصلنا على خلفية توفر واجهة برمجة تطبيقات. الآن يمكنك بدء الخدمة مع الواجهة الأمامية ورسم النماذج المقابلة. يعد هذا أساسًا جيدًا للتطبيق الذي يمكنك استخدامه لبدء التشغيل الخاص بك عن طريق تكوين مكونات مختلفة مع تطور المشروع.


الخلاصة


يتم الاحتفاظ برمز التطبيق الرئيسي في ترتيب العمل على github مع تعليمات للبدء في علامة تبويب ويكي.




بالنسبة لمشروع بملايين الدولارات ، يبدو رطبًا قليلاً ، بالطبع ، ولكن كذريعة ، أذكرك بأننا عملنا عليه في المساء ، بعد العمل / الدراسة.
إذا تجاوز عدد الأشخاص المهتمين عدد النعال ، يمكنني في المستقبل تحويل هذا إلى سلسلة من المقالات حيث سأتحدث عن المقدمة ، والترابط ، والفروق الدقيقة التي واجهناها عند العمل مع ملفات البريد / الدهون / الإرساء.


ملاحظة: بعد مرور بعض الوقت على النجاة من الصدمة من المدرسة ، اجتمع بقية الفريق ، وبعد تحليل الرحلات الجوية ، قرروا إجراء التكيف 2.0 ، مع مراعاة جميع الأخطاء. الهدف الرئيسي للمشروع هو نفسه - لتعلم كيفية إنشاء تطبيقات جادة ، وبناء بنية مدروسة جيدًا وتكون مطلوبة من قبل المتخصصين في السوق. يمكنك متابعة العمل في نفس المستودع. طلبات المسبح مرحب بها. شكرا لكم على اهتمامكم ونتمنى لنا حظا سعيدا!


الكعك


محاضرة فيديو خاصة

Source: https://habr.com/ru/post/ar419599/


All Articles